From 8a754e0858d922e955e71b253c139e071ecec432 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 18:04:21 +0200 Subject: Adding upstream version 2.14.3. Signed-off-by: Daniel Baumann --- test/integration/network-integration.cfg | 14 + .../network-integration.requirements.txt | 1 + test/integration/targets/add_host/aliases | 1 + test/integration/targets/add_host/tasks/main.yml | 186 ++ test/integration/targets/adhoc/aliases | 2 + test/integration/targets/adhoc/runme.sh | 9 + test/integration/targets/ansiballz_python/aliases | 2 + .../library/check_rlimit_and_maxfd.py | 31 + .../ansiballz_python/library/custom_module.py | 19 + .../targets/ansiballz_python/library/sys_check.py | 23 + .../ansiballz_python/module_utils/custom_util.py | 6 + .../targets/ansiballz_python/tasks/main.yml | 68 + test/integration/targets/ansible-doc/aliases | 2 + .../testns/testcol/MANIFEST.json | 30 + .../testns/testcol/plugins/cache/notjsonfile.py | 70 + .../testns/testcol/plugins/inventory/statichost.py | 36 + .../testns/testcol/plugins/lookup/noop.py | 45 + .../testns/testcol/plugins/modules/fakemodule.py | 28 + .../testcol/plugins/modules/notrealmodule.py | 13 + .../testns/testcol/plugins/modules/randommodule.py | 96 + .../testcol/plugins/vars/noop_vars_plugin.py | 30 + .../testns/testcol/MANIFEST.json | 30 + .../testns/testcol/plugins/cache/notjsonfile.py | 69 + .../plugins/filter/filter_subdir/in_subdir.py | 23 + .../testns/testcol/plugins/filter/grouped.py | 28 + .../testcol/plugins/filter/ultimatequestion.yml | 21 + .../testns/testcol/plugins/inventory/statichost.py | 35 + .../testns/testcol/plugins/lookup/noop.py | 45 + .../database/database_type/subdir_module.py | 37 + .../testns/testcol/plugins/modules/fakemodule.py | 27 + .../testcol/plugins/modules/notrealmodule.py | 13 + .../testns/testcol/plugins/modules/randommodule.py | 95 + .../testns/testcol/plugins/test/test_test.py | 16 + .../testns/testcol/plugins/test/yolo.yml | 18 + .../testcol/plugins/vars/noop_vars_plugin.py | 29 + .../testns/testcol/roles/testrole/meta/main.yml | 26 + .../roles/testrole_with_no_argspecs/meta/empty | 0 .../testns/testcol2/MANIFEST.json | 30 + .../testcol2/plugins/doc_fragments/deprecation.py | 16 + .../testcol2/plugins/doc_fragments/module.py | 21 + .../testcol2/plugins/doc_fragments/plugin.py | 47 + .../plugins/doc_fragments/version_added.py | 13 + .../targets/ansible-doc/fakecollrole.output | 15 + .../targets/ansible-doc/fakemodule.output | 16 + .../targets/ansible-doc/fakerole.output | 32 + .../ansible-doc/filter_plugins/donothing.yml | 21 + .../targets/ansible-doc/filter_plugins/other.py | 25 + .../targets/ansible-doc/filter_plugins/split.yml | 21 + test/integration/targets/ansible-doc/inventory | 1 + .../targets/ansible-doc/library/double_doc.py | 19 + .../targets/ansible-doc/library/test_docs.py | 39 + .../library/test_docs_missing_description.py | 40 + .../ansible-doc/library/test_docs_no_metadata.py | 35 + .../ansible-doc/library/test_docs_no_status.py | 38 + .../library/test_docs_non_iterable_status.py | 39 + .../library/test_docs_removed_precedence.py | 40 + .../library/test_docs_removed_status.py | 39 + .../ansible-doc/library/test_docs_returns.py | 56 + .../library/test_docs_returns_broken.py | 40 + .../ansible-doc/library/test_docs_suboptions.py | 70 + .../ansible-doc/library/test_docs_yaml_anchors.py | 71 + .../targets/ansible-doc/library/test_empty.py | 0 .../targets/ansible-doc/library/test_no_docs.py | 23 + .../library/test_no_docs_no_metadata.py | 18 + .../ansible-doc/library/test_no_docs_no_status.py | 22 + .../library/test_no_docs_non_iterable_status.py | 23 + .../ansible-doc/library/test_win_module.ps1 | 21 + .../ansible-doc/library/test_win_module.yml | 9 + .../lookup_plugins/_deprecated_with_adj_docs.py | 5 + .../lookup_plugins/_deprecated_with_docs.py | 26 + .../lookup_plugins/deprecated_with_adj_docs.yml | 16 + test/integration/targets/ansible-doc/noop.output | 32 + .../targets/ansible-doc/noop_vars_plugin.output | 42 + .../targets/ansible-doc/notjsonfile.output | 157 + .../targets/ansible-doc/randommodule-text.output | 101 + .../targets/ansible-doc/randommodule.output | 115 + .../roles/test_role1/meta/argument_specs.yml | 34 + .../ansible-doc/roles/test_role1/meta/main.yml | 13 + .../ansible-doc/roles/test_role2/meta/empty | 0 .../ansible-doc/roles/test_role3/meta/main.yml | 0 test/integration/targets/ansible-doc/runme.sh | 214 ++ test/integration/targets/ansible-doc/test.yml | 172 ++ .../targets/ansible-doc/test_docs_returns.output | 33 + .../ansible-doc/test_docs_suboptions.output | 42 + .../ansible-doc/test_docs_yaml_anchors.output | 46 + .../targets/ansible-doc/test_role1/README.txt | 3 + .../targets/ansible-doc/test_role1/meta/main.yml | 8 + .../targets/ansible-galaxy-collection-cli/aliases | 2 + .../files/expected.txt | 107 + .../files/expected_full_manifest.txt | 108 + .../files/full_manifest_galaxy.yml | 39 + .../ansible-galaxy-collection-cli/files/galaxy.yml | 10 + .../files/make_collection_dir.py | 114 + .../ansible-galaxy-collection-cli/tasks/main.yml | 19 + .../tasks/manifest.yml | 57 + .../targets/ansible-galaxy-collection-scm/aliases | 2 + .../ansible-galaxy-collection-scm/meta/main.yml | 4 + .../tasks/download.yml | 48 + .../tasks/empty_installed_collections.yml | 7 + .../tasks/individual_collection_repo.yml | 18 + .../ansible-galaxy-collection-scm/tasks/main.yml | 61 + .../tasks/multi_collection_repo_all.yml | 45 + .../tasks/multi_collection_repo_individual.yml | 15 + .../tasks/reinstalling.yml | 31 + .../tasks/requirements.yml | 103 + .../tasks/scm_dependency.yml | 29 + .../tasks/scm_dependency_deduplication.yml | 92 + .../ansible-galaxy-collection-scm/tasks/setup.yml | 19 + .../tasks/setup_collection_bad_version.yml | 47 + .../tasks/setup_multi_collection_repo.yml | 70 + .../tasks/setup_recursive_scm_dependency.yml | 33 + .../tasks/test_invalid_version.yml | 58 + .../tasks/test_manifest_metadata.yml | 55 + .../tasks/test_supported_resolvelib_versions.yml | 25 + .../templates/git_prefix_name.yml | 2 + .../templates/name_and_type.yml | 3 + .../templates/name_without_type.yml | 3 + .../templates/source_and_name.yml | 4 + .../templates/source_and_name_and_type.yml | 5 + .../templates/source_only.yml | 3 + .../ansible-galaxy-collection-scm/vars/main.yml | 11 + .../targets/ansible-galaxy-collection/aliases | 4 + .../files/build_bad_tar.py | 84 + .../ansible-galaxy-collection/files/test_module.py | 80 + .../library/reset_pulp.py | 211 ++ .../library/setup_collections.py | 269 ++ .../ansible-galaxy-collection/meta/main.yml | 4 + .../ansible-galaxy-collection/tasks/build.yml | 74 + .../ansible-galaxy-collection/tasks/download.yml | 176 ++ .../tasks/fail_fast_resolvelib.yml | 45 + .../ansible-galaxy-collection/tasks/init.yml | 124 + .../ansible-galaxy-collection/tasks/install.yml | 1035 +++++++ .../tasks/install_offline.yml | 137 + .../ansible-galaxy-collection/tasks/list.yml | 167 + .../ansible-galaxy-collection/tasks/main.yml | 220 ++ .../ansible-galaxy-collection/tasks/publish.yml | 33 + .../ansible-galaxy-collection/tasks/pulp.yml | 11 + .../tasks/revoke_gpg_key.yml | 14 + .../ansible-galaxy-collection/tasks/setup_gpg.yml | 24 + .../tasks/supported_resolvelib.yml | 44 + .../tasks/unsupported_resolvelib.yml | 44 + .../ansible-galaxy-collection/tasks/upgrade.yml | 282 ++ .../ansible-galaxy-collection/tasks/verify.yml | 475 +++ .../templates/ansible.cfg.j2 | 28 + .../ansible-galaxy-collection/vars/main.yml | 164 + .../targets/ansible-galaxy-role/aliases | 2 + .../targets/ansible-galaxy-role/meta/main.yml | 1 + .../targets/ansible-galaxy-role/tasks/main.yml | 61 + test/integration/targets/ansible-galaxy/aliases | 3 + .../targets/ansible-galaxy/cleanup-default.yml | 13 + .../targets/ansible-galaxy/cleanup-freebsd.yml | 12 + .../integration/targets/ansible-galaxy/cleanup.yml | 26 + .../targets/ansible-galaxy/files/testserver.py | 20 + test/integration/targets/ansible-galaxy/runme.sh | 571 ++++ test/integration/targets/ansible-galaxy/setup.yml | 57 + test/integration/targets/ansible-inventory/aliases | 2 + .../ansible-inventory/files/invalid_sample.yml | 7 + .../targets/ansible-inventory/files/unicode.yml | 3 + .../ansible-inventory/files/valid_sample.toml | 2 + .../ansible-inventory/files/valid_sample.yml | 7 + .../integration/targets/ansible-inventory/runme.sh | 7 + .../targets/ansible-inventory/tasks/main.yml | 147 + .../targets/ansible-inventory/tasks/toml.yml | 66 + .../integration/targets/ansible-inventory/test.yml | 3 + test/integration/targets/ansible-pull/aliases | 2 + test/integration/targets/ansible-pull/cleanup.yml | 16 + .../ansible-pull/pull-integration-test/ansible.cfg | 2 + .../ansible-pull/pull-integration-test/inventory | 2 + .../ansible-pull/pull-integration-test/local.yml | 20 + .../pull-integration-test/multi_play_1.yml | 6 + .../pull-integration-test/multi_play_2.yml | 6 + test/integration/targets/ansible-pull/runme.sh | 87 + test/integration/targets/ansible-pull/setup.yml | 11 + test/integration/targets/ansible-runner/aliases | 5 + .../targets/ansible-runner/files/adhoc_example1.py | 29 + .../ansible-runner/files/playbook_example1.py | 41 + .../targets/ansible-runner/filter_plugins/parse.py | 17 + test/integration/targets/ansible-runner/inventory | 1 + test/integration/targets/ansible-runner/runme.sh | 7 + .../ansible-runner/tasks/adhoc_example1.yml | 14 + .../targets/ansible-runner/tasks/main.yml | 4 + .../ansible-runner/tasks/playbook_example1.yml | 21 + .../targets/ansible-runner/tasks/setup.yml | 4 + test/integration/targets/ansible-runner/test.yml | 3 + .../targets/ansible-test-cloud-acme/aliases | 3 + .../targets/ansible-test-cloud-acme/tasks/main.yml | 7 + .../targets/ansible-test-cloud-aws/aliases | 3 + .../targets/ansible-test-cloud-aws/tasks/main.yml | 17 + .../targets/ansible-test-cloud-azure/aliases | 3 + .../ansible-test-cloud-azure/tasks/main.yml | 18 + .../targets/ansible-test-cloud-cs/aliases | 3 + .../targets/ansible-test-cloud-cs/tasks/main.yml | 8 + .../targets/ansible-test-cloud-foreman/aliases | 3 + .../ansible-test-cloud-foreman/tasks/main.yml | 6 + .../targets/ansible-test-cloud-galaxy/aliases | 4 + .../ansible-test-cloud-galaxy/tasks/main.yml | 25 + .../ansible-test-cloud-httptester-windows/aliases | 4 + .../tasks/main.yml | 15 + .../targets/ansible-test-cloud-httptester/aliases | 3 + .../ansible-test-cloud-httptester/tasks/main.yml | 15 + .../targets/ansible-test-cloud-nios/aliases | 3 + .../targets/ansible-test-cloud-nios/tasks/main.yml | 10 + .../targets/ansible-test-cloud-openshift/aliases | 4 + .../ansible-test-cloud-openshift/tasks/main.yml | 6 + .../targets/ansible-test-cloud-vcenter/aliases | 3 + .../ansible-test-cloud-vcenter/tasks/main.yml | 6 + .../targets/ansible-test-config-invalid/aliases | 4 + .../ansible_collections/ns/col/tests/config.yml | 1 + .../ns/col/tests/integration/targets/test/aliases | 1 + .../ns/col/tests/integration/targets/test/runme.sh | 1 + .../tests/unit/plugins/module_utils/test_test.py | 2 + .../targets/ansible-test-config-invalid/runme.sh | 12 + .../targets/ansible-test-config/aliases | 4 + .../ns/col/plugins/module_utils/test.py | 14 + .../ansible_collections/ns/col/tests/config.yml | 2 + .../tests/unit/plugins/module_utils/test_test.py | 5 + .../targets/ansible-test-config/runme.sh | 15 + .../targets/ansible-test-container/aliases | 5 + .../targets/ansible-test-container/runme.py | 1090 +++++++ .../targets/ansible-test-container/runme.sh | 5 + .../targets/ansible-test-coverage/aliases | 4 + .../ns/col/plugins/module_utils/test_util.py | 6 + .../targets/ansible-test-coverage/runme.sh | 16 + .../targets/ansible-test-docker/aliases | 3 + .../ansible_collections/ns/col/galaxy.yml | 6 + .../ns/col/plugins/doc_fragments/ps_util.py | 21 + .../ns/col/plugins/module_utils/PSUtil.psm1 | 16 + .../ns/col/plugins/module_utils/my_util.py | 6 + .../ns/col/plugins/modules/hello.py | 46 + .../ns/col/plugins/modules/win_util_args.ps1 | 16 + .../ns/col/plugins/modules/win_util_args.py | 39 + .../col/tests/integration/targets/minimal/aliases | 1 + .../integration/targets/minimal/tasks/main.yml | 7 + .../unit/plugins/module_utils/test_my_util.py | 8 + .../col/tests/unit/plugins/modules/test_hello.py | 8 + .../targets/ansible-test-docker/runme.sh | 14 + test/integration/targets/ansible-test-git/aliases | 4 + .../ansible_collections/ns/col/tests/.keep | 0 .../collection-tests/git-at-collection-base.sh | 10 + .../collection-tests/git-at-collection-root.sh | 10 + .../collection-tests/git-common.bash | 65 + .../collection-tests/install-git.yml | 5 + .../collection-tests/uninstall-git.yml | 18 + test/integration/targets/ansible-test-git/runme.sh | 24 + .../ansible-test-integration-constraints/aliases | 4 + .../ns/col/tests/integration/constraints.txt | 1 + .../ns/col/tests/integration/requirements.txt | 1 + .../tests/integration/targets/constraints/aliases | 1 + .../integration/targets/constraints/tasks/main.yml | 7 + .../ansible-test-integration-constraints/runme.sh | 7 + .../ansible-test-integration-targets/aliases | 4 + .../integration/targets/destructive_a/aliases | 2 + .../integration/targets/destructive_b/aliases | 2 + .../tests/integration/targets/disabled_a/aliases | 2 + .../tests/integration/targets/disabled_b/aliases | 2 + .../tests/integration/targets/unstable_a/aliases | 2 + .../tests/integration/targets/unstable_b/aliases | 2 + .../integration/targets/unsupported_a/aliases | 2 + .../integration/targets/unsupported_b/aliases | 2 + .../ansible-test-integration-targets/runme.sh | 9 + .../ansible-test-integration-targets/test.py | 35 + .../targets/ansible-test-integration/aliases | 4 + .../ns/col/plugins/module_utils/my_util.py | 6 + .../ns/col/plugins/modules/hello.py | 46 + .../ns/col/tests/integration/targets/hello/aliases | 1 + .../tests/integration/targets/hello/tasks/main.yml | 7 + .../targets/ansible-test-integration/runme.sh | 7 + .../targets/ansible-test-no-tty/aliases | 4 + .../ansible_collections/ns/col/run-with-pty.py | 11 + .../col/tests/integration/targets/no-tty/aliases | 1 + .../integration/targets/no-tty/assert-no-tty.py | 13 + .../col/tests/integration/targets/no-tty/runme.sh | 5 + .../ansible_collections/ns/col/vendored_pty.py | 189 ++ .../targets/ansible-test-no-tty/runme.sh | 13 + .../ansible-test-sanity-ansible-doc/aliases | 4 + .../ns/col/plugins/lookup/a/b/lookup2.py | 28 + .../ns/col/plugins/lookup/lookup1.py | 28 + .../ns/col/plugins/modules/a/b/module2.py | 34 + .../ns/col/plugins/modules/module1.py | 34 + .../ansible-test-sanity-ansible-doc/runme.sh | 9 + .../targets/ansible-test-sanity-import/aliases | 5 + .../ns/col/plugins/lookup/vendor1.py | 33 + .../ns/col/plugins/lookup/vendor2.py | 33 + .../targets/ansible-test-sanity-import/runme.sh | 20 + .../targets/ansible-test-sanity-lint/aliases | 4 + .../targets/ansible-test-sanity-lint/expected.txt | 1 + .../targets/ansible-test-sanity-lint/runme.sh | 47 + .../targets/ansible-test-sanity-shebang/aliases | 4 + .../ns/col/plugins/modules/powershell.ps1 | 1 + .../ns/col/plugins/modules/python-no-shebang.py | 0 .../ns/col/plugins/modules/python.py | 1 + .../ansible_collections/ns/col/scripts/env_bash.sh | 1 + .../ns/col/scripts/env_python.py | 1 + .../ansible_collections/ns/col/scripts/sh.sh | 1 + .../tests/integration/targets/valid/env_bash.sh | 1 + .../tests/integration/targets/valid/env_python.py | 1 + .../ns/col/tests/integration/targets/valid/sh.sh | 1 + .../ansible-test-sanity-shebang/expected.txt | 9 + .../targets/ansible-test-sanity-shebang/runme.sh | 47 + .../ansible-test-sanity-validate-modules/aliases | 4 + .../ns/col/plugins/modules/invalid_yaml_syntax.py | 27 + .../ns/col/plugins/modules/no_callable.py | 23 + .../ns/col/plugins/modules/sidecar.py | 11 + .../ns/col/plugins/modules/sidecar.yaml | 31 + .../ansible_collections/ns/failure/README.rst | 3 + .../ansible_collections/ns/failure/galaxy.yml | 6 + .../ansible_collections/ns/failure/meta/main.yml | 1 + .../ns/failure/plugins/modules/failure_ps.ps1 | 16 + .../ns/failure/plugins/modules/failure_ps.yml | 31 + .../ansible_collections/ns/ps_only/README.rst | 3 + .../ansible_collections/ns/ps_only/galaxy.yml | 6 + .../ns/ps_only/meta/runtime.yml | 1 + .../ps_only/plugins/module_utils/share_module.psm1 | 19 + .../ns/ps_only/plugins/module_utils/validate.psm1 | 8 + .../ns/ps_only/plugins/modules/in_function.ps1 | 7 + .../ns/ps_only/plugins/modules/in_function.yml | 25 + .../ns/ps_only/plugins/modules/sidecar.ps1 | 14 + .../ns/ps_only/plugins/modules/sidecar.yml | 31 + .../ns/ps_only/plugins/modules/validate.ps1 | 8 + .../ns/ps_only/plugins/modules/validate.py | 14 + .../expected.txt | 5 + .../ansible-test-sanity-validate-modules/runme.sh | 34 + .../targets/ansible-test-sanity/aliases | 4 + .../ansible_collections/ns/col/README.rst | 3 + .../ansible_collections/ns/col/galaxy.yml | 6 + .../ansible_collections/ns/col/meta/runtime.yml | 5 + .../ns/col/plugins/filter/check_pylint.py | 23 + .../ns/col/plugins/lookup/bad.py | 31 + .../ns/col/plugins/lookup/world.py | 29 + .../ns/col/plugins/module_utils/__init__.py | 0 .../ns/col/plugins/modules/bad.py | 34 + .../ns/col/plugins/random_directory/bad.py | 8 + .../tests/integration/targets/hello/files/bad.py | 16 + .../ns/col/tests/sanity/ignore.txt | 6 + .../targets/ansible-test-sanity/runme.sh | 7 + .../integration/targets/ansible-test-shell/aliases | 4 + .../ansible_collections/ns/col/.keep | 0 .../targets/ansible-test-shell/expected-stderr.txt | 1 + .../targets/ansible-test-shell/expected-stdout.txt | 1 + .../targets/ansible-test-shell/runme.sh | 30 + .../targets/ansible-test-units-constraints/aliases | 5 + .../ns/col/tests/unit/constraints.txt | 1 + .../tests/unit/plugins/modules/test_constraints.py | 8 + .../ns/col/tests/unit/requirements.txt | 1 + .../ansible-test-units-constraints/runme.sh | 10 + .../integration/targets/ansible-test-units/aliases | 5 + .../ns/col/plugins/module_utils/my_util.py | 6 + .../ns/col/plugins/modules/hello.py | 46 + .../unit/plugins/module_utils/test_my_util.py | 8 + .../col/tests/unit/plugins/modules/test_hello.py | 8 + .../targets/ansible-test-units/runme.sh | 10 + .../ansible-test-unsupported-directory/aliases | 4 + .../ansible_collections/ns/col/.keep | 0 .../ansible-test-unsupported-directory/runme.sh | 29 + test/integration/targets/ansible-test/aliases | 1 + .../targets/ansible-test/venv-pythons.py | 42 + test/integration/targets/ansible-vault/aliases | 2 + .../targets/ansible-vault/empty-password | 0 .../targets/ansible-vault/encrypted-vault-password | 6 + .../encrypted_file_encrypted_var_password | 1 + .../targets/ansible-vault/example1_password | 1 + .../targets/ansible-vault/example2_password | 1 + .../targets/ansible-vault/example3_password | 1 + .../targets/ansible-vault/faux-editor.py | 44 + .../files/test_assemble/nonsecret.txt | 1 + .../ansible-vault/files/test_assemble/secret.vault | 7 + .../targets/ansible-vault/format_1_1_AES256.yml | 6 + .../targets/ansible-vault/format_1_2_AES256.yml | 6 + .../targets/ansible-vault/host_vars/myhost.yml | 7 + .../targets/ansible-vault/host_vars/testhost.yml | 7 + .../targets/ansible-vault/invalid_format/README.md | 1 + .../invalid_format/broken-group-vars-tasks.yml | 23 + .../invalid_format/broken-host-vars-tasks.yml | 7 + .../group_vars/broken-group-vars.yml | 8 + .../host_vars/broken-host-vars.example.com/vars | 11 + .../targets/ansible-vault/invalid_format/inventory | 5 + .../invalid_format/original-broken-host-vars | 6 + .../invalid_format/original-group-vars.yml | 2 + .../targets/ansible-vault/invalid_format/some-vars | 6 + .../ansible-vault/invalid_format/vault-secret | 1 + .../targets/ansible-vault/inventory.toml | 5 + .../targets/ansible-vault/password-script.py | 33 + .../integration/targets/ansible-vault/realpath.yml | 10 + .../ansible-vault/roles/test_vault/tasks/main.yml | 9 + .../ansible-vault/roles/test_vault/vars/main.yml | 9 + .../roles/test_vault_embedded/tasks/main.yml | 13 + .../roles/test_vault_embedded/vars/main.yml | 17 + .../roles/test_vault_embedded_ids/tasks/main.yml | 29 + .../roles/test_vault_embedded_ids/vars/main.yml | 194 ++ .../test_vault_file_encrypted_embedded/README.md | 1 + .../tasks/main.yml | 13 + .../vars/main.yml | 76 + .../roles/test_vaulted_template/tasks/main.yml | 19 + .../templates/vaulted_template.j2 | 6 + test/integration/targets/ansible-vault/runme.sh | 576 ++++ .../targets/ansible-vault/script/vault-secret.sh | 24 + .../ansible-vault/single_vault_as_string.yml | 117 + test/integration/targets/ansible-vault/symlink.yml | 10 + .../ansible-vault/symlink/get-password-symlink | 24 + .../targets/ansible-vault/test-vault-client.py | 66 + .../targets/ansible-vault/test_dangling_temp.yml | 34 + .../ansible-vault/test_utf8_value_in_filename.yml | 16 + .../targets/ansible-vault/test_vault.yml | 6 + .../targets/ansible-vault/test_vault_embedded.yml | 4 + .../ansible-vault/test_vault_embedded_ids.yml | 4 + .../test_vault_file_encrypted_embedded.yml | 4 + .../ansible-vault/test_vaulted_inventory.yml | 5 + .../ansible-vault/test_vaulted_inventory_toml.yml | 9 + .../ansible-vault/test_vaulted_template.yml | 6 + .../ansible-vault/test_vaulted_utf8_value.yml | 15 + .../targets/ansible-vault/vars/vaulted.yml | 15 + .../targets/ansible-vault/vault-caf\303\251.yml" | 6 + .../targets/ansible-vault/vault-password | 1 + .../targets/ansible-vault/vault-password-ansible | 1 + .../targets/ansible-vault/vault-password-wrong | 1 + .../targets/ansible-vault/vault-secret.txt | 6 + .../targets/ansible-vault/vaulted.inventory | 8 + .../targets/ansible/adhoc-callback.stdout | 12 + test/integration/targets/ansible/aliases | 2 + .../targets/ansible/ansible-test\303\251.cfg" | 3 + .../ansible/callback_plugins/callback_debug.py | 24 + .../ansible/callback_plugins/callback_meta.py | 23 + .../ansible/module_common_regex_regression.sh | 15 + test/integration/targets/ansible/no-extension | 2 + test/integration/targets/ansible/playbook.yml | 8 + .../targets/ansible/playbookdir_cfg.ini | 2 + test/integration/targets/ansible/runme.sh | 86 + test/integration/targets/ansible/vars.yml | 1 + .../integration/targets/any_errors_fatal/50897.yml | 19 + test/integration/targets/any_errors_fatal/aliases | 2 + .../targets/any_errors_fatal/always_block.yml | 27 + .../integration/targets/any_errors_fatal/inventory | 6 + .../targets/any_errors_fatal/on_includes.yml | 7 + .../targets/any_errors_fatal/play_level.yml | 15 + test/integration/targets/any_errors_fatal/runme.sh | 37 + .../targets/any_errors_fatal/test_fatal.yml | 12 + test/integration/targets/apt/aliases | 6 + test/integration/targets/apt/defaults/main.yml | 2 + test/integration/targets/apt/handlers/main.yml | 4 + test/integration/targets/apt/meta/main.yml | 3 + .../integration/targets/apt/tasks/apt-builddep.yml | 55 + .../targets/apt/tasks/apt-multiarch.yml | 44 + test/integration/targets/apt/tasks/apt.yml | 547 ++++ test/integration/targets/apt/tasks/downgrade.yml | 77 + test/integration/targets/apt/tasks/main.yml | 40 + test/integration/targets/apt/tasks/repo.yml | 452 +++ test/integration/targets/apt/tasks/upgrade.yml | 64 + .../targets/apt/tasks/url-with-deps.yml | 56 + test/integration/targets/apt/vars/Ubuntu-20.yml | 1 + test/integration/targets/apt/vars/Ubuntu-22.yml | 1 + test/integration/targets/apt/vars/default.yml | 1 + test/integration/targets/apt_key/aliases | 5 + test/integration/targets/apt_key/meta/main.yml | 2 + test/integration/targets/apt_key/tasks/apt_key.yml | 25 + .../targets/apt_key/tasks/apt_key_binary.yml | 12 + .../targets/apt_key/tasks/apt_key_inline_data.yml | 5 + test/integration/targets/apt_key/tasks/file.yml | 52 + test/integration/targets/apt_key/tasks/main.yml | 29 + test/integration/targets/apt_repository/aliases | 6 + .../targets/apt_repository/meta/main.yml | 2 + .../targets/apt_repository/tasks/apt.yml | 243 ++ .../targets/apt_repository/tasks/cleanup.yml | 17 + .../targets/apt_repository/tasks/main.yml | 25 + .../targets/apt_repository/tasks/mode.yaml | 135 + .../targets/apt_repository/tasks/mode_cleanup.yaml | 7 + test/integration/targets/args/aliases | 2 + test/integration/targets/args/runme.sh | 12 + test/integration/targets/argspec/aliases | 2 + .../integration/targets/argspec/library/argspec.py | 268 ++ test/integration/targets/argspec/tasks/main.yml | 659 ++++ .../targets/argspec/tasks/password_no_log.yml | 14 + test/integration/targets/assemble/aliases | 1 + test/integration/targets/assemble/files/fragment1 | 1 + test/integration/targets/assemble/files/fragment2 | 1 + test/integration/targets/assemble/files/fragment3 | 1 + test/integration/targets/assemble/files/fragment4 | 1 + test/integration/targets/assemble/files/fragment5 | 1 + test/integration/targets/assemble/meta/main.yml | 21 + test/integration/targets/assemble/tasks/main.yml | 154 + test/integration/targets/assert/aliases | 2 + .../targets/assert/assert_quiet.out.quiet.stderr | 2 + .../targets/assert/assert_quiet.out.quiet.stdout | 17 + test/integration/targets/assert/inventory | 3 + test/integration/targets/assert/quiet.yml | 16 + test/integration/targets/assert/runme.sh | 71 + test/integration/targets/async/aliases | 3 + test/integration/targets/async/callback_test.yml | 7 + .../targets/async/library/async_test.py | 49 + test/integration/targets/async/meta/main.yml | 2 + test/integration/targets/async/tasks/main.yml | 300 ++ test/integration/targets/async_extra_data/aliases | 2 + .../targets/async_extra_data/library/junkping.py | 15 + test/integration/targets/async_extra_data/runme.sh | 7 + .../targets/async_extra_data/test_async.yml | 10 + .../targets/async_fail/action_plugins/normal.py | 62 + test/integration/targets/async_fail/aliases | 3 + .../targets/async_fail/library/async_test.py | 53 + test/integration/targets/async_fail/meta/main.yml | 2 + test/integration/targets/async_fail/tasks/main.yml | 36 + test/integration/targets/become/aliases | 4 + test/integration/targets/become/files/copy.txt | 1 + test/integration/targets/become/meta/main.yml | 2 + test/integration/targets/become/tasks/become.yml | 49 + test/integration/targets/become/tasks/main.yml | 20 + test/integration/targets/become/vars/main.yml | 14 + test/integration/targets/become_su/aliases | 3 + test/integration/targets/become_su/runme.sh | 6 + .../become_unprivileged/action_plugins/tmpdir.py | 14 + .../targets/become_unprivileged/aliases | 5 + .../become_unprivileged/chmod_acl_macos/test.yml | 26 + .../become_unprivileged/cleanup_unpriv_users.yml | 53 + .../common_remote_group/cleanup.yml | 35 + .../common_remote_group/setup.yml | 43 + .../common_remote_group/test.yml | 36 + .../targets/become_unprivileged/inventory | 10 + .../targets/become_unprivileged/runme.sh | 52 + .../become_unprivileged/setup_unpriv_users.yml | 109 + test/integration/targets/binary/aliases | 2 + test/integration/targets/binary/files/b64_latin1 | 1 + test/integration/targets/binary/files/b64_utf8 | 1 + .../integration/targets/binary/files/from_playbook | 1 + test/integration/targets/binary/meta/main.yml | 3 + test/integration/targets/binary/tasks/main.yml | 131 + .../binary/templates/b64_latin1_template.j2 | 1 + .../targets/binary/templates/b64_utf8_template.j2 | 1 + .../binary/templates/from_playbook_template.j2 | 1 + test/integration/targets/binary/vars/main.yml | 3 + test/integration/targets/binary_modules/Makefile | 15 + test/integration/targets/binary_modules/aliases | 1 + .../binary_modules/download_binary_modules.yml | 9 + .../targets/binary_modules/group_vars/all | 3 + .../targets/binary_modules/library/.gitignore | 1 + .../targets/binary_modules/library/helloworld.go | 89 + .../roles/test_binary_modules/tasks/main.yml | 53 + test/integration/targets/binary_modules/test.sh | 8 + .../targets/binary_modules/test_binary_modules.yml | 5 + .../targets/binary_modules_posix/aliases | 3 + .../targets/binary_modules_posix/runme.sh | 6 + .../targets/binary_modules_winrm/aliases | 4 + .../targets/binary_modules_winrm/runme.sh | 6 + test/integration/targets/blockinfile/aliases | 1 + .../targets/blockinfile/files/sshd_config | 135 + test/integration/targets/blockinfile/meta/main.yml | 3 + .../tasks/add_block_to_existing_file.yml | 52 + .../tasks/block_without_trailing_newline.yml | 30 + .../targets/blockinfile/tasks/create_file.yml | 32 + .../integration/targets/blockinfile/tasks/diff.yml | 18 + .../tasks/file_without_trailing_newline.yml | 36 + .../targets/blockinfile/tasks/insertafter.yml | 37 + .../targets/blockinfile/tasks/insertbefore.yml | 39 + .../integration/targets/blockinfile/tasks/main.yml | 41 + .../targets/blockinfile/tasks/multiline_search.yml | 70 + .../blockinfile/tasks/preserve_line_endings.yml | 24 + .../targets/blockinfile/tasks/validate.yml | 28 + test/integration/targets/blocks/43191-2.yml | 17 + test/integration/targets/blocks/43191.yml | 18 + test/integration/targets/blocks/69848.yml | 5 + test/integration/targets/blocks/72725.yml | 24 + test/integration/targets/blocks/72781.yml | 13 + test/integration/targets/blocks/78612.yml | 16 + test/integration/targets/blocks/79711.yml | 17 + test/integration/targets/blocks/aliases | 2 + .../targets/blocks/always_failure_no_rescue_rc.yml | 13 + .../blocks/always_failure_with_rescue_rc.yml | 16 + .../targets/blocks/always_no_rescue_rc.yml | 12 + test/integration/targets/blocks/block_fail.yml | 5 + .../targets/blocks/block_fail_tasks.yml | 9 + .../integration/targets/blocks/block_in_rescue.yml | 33 + .../targets/blocks/block_rescue_vars.yml | 16 + test/integration/targets/blocks/fail.yml | 2 + test/integration/targets/blocks/finalized_task.yml | 17 + test/integration/targets/blocks/inherit_notify.yml | 19 + test/integration/targets/blocks/issue29047.yml | 4 + .../targets/blocks/issue29047_tasks.yml | 13 + test/integration/targets/blocks/issue71306.yml | 16 + test/integration/targets/blocks/main.yml | 128 + test/integration/targets/blocks/nested_fail.yml | 3 + .../targets/blocks/nested_nested_fail.yml | 3 + .../targets/blocks/roles/fail/tasks/main.yml | 3 + .../blocks/roles/role-69848-1/meta/main.yml | 2 + .../blocks/roles/role-69848-2/meta/main.yml | 2 + .../blocks/roles/role-69848-3/tasks/main.yml | 8 + test/integration/targets/blocks/runme.sh | 138 + .../targets/blocks/unsafe_failed_task.yml | 17 + .../targets/builtin_vars_prompt/aliases | 4 + .../targets/builtin_vars_prompt/runme.sh | 6 + .../builtin_vars_prompt/test-vars_prompt.py | 130 + .../targets/builtin_vars_prompt/unsafe.yml | 20 + .../targets/builtin_vars_prompt/unsupported.yml | 18 + .../targets/builtin_vars_prompt/vars_prompt-1.yml | 15 + .../targets/builtin_vars_prompt/vars_prompt-2.yml | 16 + .../targets/builtin_vars_prompt/vars_prompt-3.yml | 17 + .../targets/builtin_vars_prompt/vars_prompt-4.yml | 16 + .../targets/builtin_vars_prompt/vars_prompt-5.yml | 14 + .../targets/builtin_vars_prompt/vars_prompt-6.yml | 20 + .../targets/builtin_vars_prompt/vars_prompt-7.yml | 12 + test/integration/targets/callback_default/aliases | 1 + .../callback_default.out.check_markers_dry.stderr | 2 + .../callback_default.out.check_markers_dry.stdout | 78 + .../callback_default.out.check_markers_wet.stderr | 2 + .../callback_default.out.check_markers_wet.stdout | 74 + ...callback_default.out.check_nomarkers_dry.stderr | 2 + ...callback_default.out.check_nomarkers_dry.stdout | 74 + ...callback_default.out.check_nomarkers_wet.stderr | 2 + ...callback_default.out.check_nomarkers_wet.stdout | 74 + .../callback_default.out.default.stderr | 2 + .../callback_default.out.default.stdout | 108 + ...back_default.out.display_path_on_failure.stderr | 2 + ...back_default.out.display_path_on_failure.stdout | 111 + .../callback_default.out.failed_to_stderr.stderr | 8 + .../callback_default.out.failed_to_stderr.stdout | 102 + .../callback_default.out.fqcn_free.stdout | 35 + .../callback_default.out.free.stdout | 35 + .../callback_default.out.hide_ok.stderr | 2 + .../callback_default.out.hide_ok.stdout | 86 + .../callback_default.out.hide_skipped.stderr | 2 + .../callback_default.out.hide_skipped.stdout | 93 + .../callback_default.out.hide_skipped_ok.stderr | 2 + .../callback_default.out.hide_skipped_ok.stdout | 71 + .../callback_default.out.host_pinned.stdout | 35 + .../callback_default.out.result_format_yaml.stderr | 2 + .../callback_default.out.result_format_yaml.stdout | 108 + ...ult.out.result_format_yaml_lossy_verbose.stderr | 2 + ...ult.out.result_format_yaml_lossy_verbose.stdout | 300 ++ ...k_default.out.result_format_yaml_verbose.stderr | 2 + ...k_default.out.result_format_yaml_verbose.stdout | 312 ++ ...ault.out.yaml_result_format_yaml_verbose.stderr | 2 + ...ault.out.yaml_result_format_yaml_verbose.stdout | 29 + .../targets/callback_default/include_me.yml | 2 + .../integration/targets/callback_default/inventory | 10 + .../callback_default/no_implicit_meta_banners.yml | 11 + test/integration/targets/callback_default/runme.sh | 241 ++ test/integration/targets/callback_default/test.yml | 128 + .../targets/callback_default/test_2.yml | 6 + .../targets/callback_default/test_async.yml | 14 + .../targets/callback_default/test_dryrun.yml | 93 + .../targets/callback_default/test_non_lockstep.yml | 7 + .../targets/callback_default/test_yaml.yml | 19 + test/integration/targets/changed_when/aliases | 2 + .../integration/targets/changed_when/meta/main.yml | 2 + .../targets/changed_when/tasks/main.yml | 111 + test/integration/targets/check_mode/aliases | 2 + .../targets/check_mode/check_mode-not-on-cli.yml | 37 + .../targets/check_mode/check_mode-on-cli.yml | 36 + test/integration/targets/check_mode/check_mode.yml | 7 + .../check_mode/roles/test_always_run/meta/main.yml | 17 + .../roles/test_always_run/tasks/main.yml | 29 + .../check_mode/roles/test_check_mode/files/foo.txt | 1 + .../roles/test_check_mode/tasks/main.yml | 50 + .../roles/test_check_mode/templates/foo.j2 | 1 + .../check_mode/roles/test_check_mode/vars/main.yml | 1 + test/integration/targets/check_mode/runme.sh | 7 + test/integration/targets/cli/aliases | 6 + test/integration/targets/cli/runme.sh | 9 + test/integration/targets/cli/setup.yml | 42 + test/integration/targets/cli/test-cli.py | 21 + test/integration/targets/cli/test_k_and_K.py | 27 + .../targets/cli/test_syntax/files/vaultsecret | 1 + .../cli/test_syntax/group_vars/all/testvault.yml | 6 + .../cli/test_syntax/roles/some_role/tasks/main.yml | 1 + .../targets/cli/test_syntax/syntax_check.yml | 7 + test/integration/targets/collection/aliases | 1 + test/integration/targets/collection/setup.sh | 29 + .../targets/collection/update-ignore.py | 56 + .../targets/collections/a.statichost.yml | 3 + test/integration/targets/collections/aliases | 4 + .../duplicate/name/plugins/modules/ping.py | 3 + .../test_ansiballz_cache_dupe_shortname.yml | 15 + .../targets/collections/cache.statichost.yml | 7 + .../collections/check_populated_inventory.yml | 11 + .../coll_in_sys/plugins/modules/systestmodule.py | 13 + .../testcoll/plugins/modules/maskedmodule.py | 13 + .../testns/testcoll/plugins/modules/testmodule.py | 13 + .../testcoll/roles/maskedrole/tasks/main.yml | 2 + .../ansible/builtin/plugins/modules/ping.py | 13 + .../ansible/bullcoll/plugins/modules/bullmodule.py | 13 + .../module_utils/formerly_testcoll_pkg/__init__.py | 1 + .../module_utils/formerly_testcoll_pkg/submod.py | 1 + .../testbroken/plugins/filter/broken_filter.py | 13 + .../testns/testcoll/meta/runtime.yml | 52 + .../playbooks/default_collection_playbook.yml | 49 + .../testns/testcoll/playbooks/play.yml | 4 + .../roles/non_coll_role/library/embedded_module.py | 13 + .../playbooks/roles/non_coll_role/tasks/main.yml | 29 + .../roles/non_coll_role_to_call/tasks/main.yml | 7 + .../testns/testcoll/playbooks/type/play.yml | 4 + .../testcoll/playbooks/type/subtype/play.yml | 4 + .../action/action_subdir/subdir_ping_action.py | 19 + .../testcoll/plugins/action/bypass_host_loop.py | 17 + .../testcoll/plugins/action/plugin_lookup.py | 40 + .../testcoll/plugins/action/subclassed_normal.py | 11 + .../plugins/action/uses_redirected_import.py | 20 + .../testcoll/plugins/callback/usercallback.py | 26 + .../testcoll/plugins/connection/localconn.py | 41 + .../testns/testcoll/plugins/doc_fragments/frag.py | 18 + .../filter/filter_subdir/my_subdir_filters.py | 14 + .../testns/testcoll/plugins/filter/myfilters.py | 14 + .../testns/testcoll/plugins/filter/myfilters2.py | 14 + .../lookup/lookup_subdir/my_subdir_lookup.py | 11 + .../testns/testcoll/plugins/lookup/mylookup.py | 11 + .../testns/testcoll/plugins/lookup/mylookup2.py | 12 + .../testcoll/plugins/module_utils/AnotherCSMU.cs | 12 + .../testns/testcoll/plugins/module_utils/MyCSMU.cs | 19 + .../plugins/module_utils/MyCSMUOptional.cs | 19 + .../testcoll/plugins/module_utils/MyPSMU.psm1 | 9 + .../plugins/module_utils/MyPSMUOptional.psm1 | 16 + .../testns/testcoll/plugins/module_utils/base.py | 12 + .../testns/testcoll/plugins/module_utils/leaf.py | 6 + .../plugins/module_utils/nested_same/__init__.py | 0 .../nested_same/nested_same/__init__.py | 0 .../nested_same/nested_same/nested_same.py | 6 + .../testcoll/plugins/module_utils/secondary.py | 6 + .../plugins/module_utils/subpkg/__init__.py | 0 .../testcoll/plugins/module_utils/subpkg/subcs.cs | 13 + .../testcoll/plugins/module_utils/subpkg/submod.py | 6 + .../plugins/module_utils/subpkg/subps.psm1 | 9 + .../plugins/module_utils/subpkg_with_init.py | 11 + .../module_utils/subpkg_with_init/__init__.py | 10 + .../subpkg_with_init/mod_in_subpkg_with_init.py | 6 + .../testcoll/plugins/modules/deprecated_ping.py | 13 + .../modules/module_subdir/subdir_ping_module.py | 14 + .../testns/testcoll/plugins/modules/ping.py | 13 + .../testns/testcoll/plugins/modules/testmodule.py | 21 + .../plugins/modules/testmodule_bad_docfrags.py | 25 + .../modules/uses_base_mu_granular_nested_import.py | 19 + .../modules/uses_collection_redirected_mu.py | 21 + .../plugins/modules/uses_core_redirected_mu.py | 19 + .../plugins/modules/uses_leaf_mu_flat_import.bak | 3 + .../plugins/modules/uses_leaf_mu_flat_import.py | 19 + .../plugins/modules/uses_leaf_mu_flat_import.yml | 3 + .../modules/uses_leaf_mu_granular_import.py | 19 + .../modules/uses_leaf_mu_module_import_from.py | 31 + .../testcoll/plugins/modules/uses_mu_missing.py | 16 + .../modules/uses_mu_missing_redirect_collection.py | 16 + .../modules/uses_mu_missing_redirect_module.py | 16 + .../plugins/modules/uses_nested_same_as_func.py | 19 + .../plugins/modules/uses_nested_same_as_module.py | 19 + .../testcoll/plugins/modules/win_csbasic_only.ps1 | 22 + .../testcoll/plugins/modules/win_selfcontained.ps1 | 9 + .../testcoll/plugins/modules/win_selfcontained.py | 1 + .../plugins/modules/win_uses_coll_csmu.ps1 | 26 + .../plugins/modules/win_uses_coll_psmu.ps1 | 25 + .../testcoll/plugins/modules/win_uses_optional.ps1 | 33 + .../testns/testcoll/plugins/test/mytests.py | 13 + .../testns/testcoll/plugins/test/mytests2.py | 13 + .../plugins/test/test_subdir/my_subdir_tests.py | 13 + .../testns/testcoll/plugins/vars/custom_vars.py | 48 + .../testcoll/roles/call_standalone/tasks/main.yml | 6 + .../meta/main.yml | 2 + .../tasks/main.yml | 7 + .../roles/common_handlers/handlers/main.yml | 27 + .../role_subdir/subdir_testrole/tasks/main.yml | 10 + .../roles/test_fqcn_handlers/meta/main.yml | 2 + .../roles/test_fqcn_handlers/tasks/main.yml | 16 + .../testns/testcoll/roles/testrole/meta/main.yml | 4 + .../testns/testcoll/roles/testrole/tasks/main.yml | 39 + .../roles/testrole_main_yaml/meta/main.yml | 4 + .../roles/testrole_main_yaml/tasks/main.yml | 33 + .../testns/testredirect/meta/runtime.yml | 23 + .../me/mycoll1/plugins/action/action1.py | 29 + .../me/mycoll1/plugins/modules/action1.py | 24 + .../me/mycoll2/plugins/modules/module1.py | 43 + .../content_adj/plugins/cache/custom_jsonfile.py | 63 + .../content_adj/plugins/inventory/statichost.py | 68 + .../content_adj/plugins/module_utils/__init__.py | 0 .../plugins/module_utils/sub1/__init__.py | 0 .../plugins/module_utils/sub1/foomodule.py | 6 + .../plugins/modules/contentadjmodule.py | 13 + .../content_adj/plugins/vars/custom_adj_vars.py | 45 + .../custom_vars_plugins/v1_vars_plugin.py | 37 + .../custom_vars_plugins/v2_vars_plugin.py | 45 + .../override_formerly_core_masked_filter.py | 13 + .../targets/collections/import_collection_pb.yml | 17 + test/integration/targets/collections/includeme.yml | 6 + .../targets/collections/inventory_test.yml | 26 + .../targets/collections/invocation_tests.yml | 5 + .../targets/collections/library/ping.py | 13 + test/integration/targets/collections/noop.yml | 4 + test/integration/targets/collections/posix.yml | 443 +++ .../targets/collections/redirected.statichost.yml | 3 + .../collections/roles/standalone/tasks/main.yml | 2 + .../collections/roles/testrole/tasks/main.yml | 28 + test/integration/targets/collections/runme.sh | 150 + .../targets/collections/test_bypass_host_loop.yml | 19 + .../targets/collections/test_collection_meta.yml | 75 + .../override_formerly_core_masked_test.py | 16 + .../targets/collections/test_redirect_list.yml | 86 + .../collections/test_task_resolved_plugin.sh | 48 + .../action_plugins/legacy_action.py | 14 + .../callback_plugins/display_resolved_action.py | 37 + .../test_ns/test_coll/meta/runtime.yml | 7 + .../test_coll/plugins/action/collection_action.py | 14 + .../test_coll/plugins/modules/collection_module.py | 29 + .../collections/test_task_resolved_plugin/fqcn.yml | 14 + .../library/legacy_module.py | 29 + .../test_task_resolved_plugin/unqualified.yml | 8 + .../unqualified_and_collections_kw.yml | 14 + .../targets/collections/testcoll2/MANIFEST.json | 0 .../testcoll2/plugins/modules/testmodule2.py | 33 + .../targets/collections/vars_plugin_tests.sh | 87 + test/integration/targets/collections/windows.yml | 34 + .../targets/collections_plugin_namespace/aliases | 2 + .../my_ns/my_col/plugins/filter/test_filter.py | 15 + .../my_ns/my_col/plugins/lookup/lookup_name.py | 9 + .../plugins/lookup/lookup_no_future_boilerplate.py | 10 + .../my_ns/my_col/plugins/test/test_test.py | 13 + .../my_ns/my_col/roles/test/tasks/main.yml | 12 + .../targets/collections_plugin_namespace/runme.sh | 5 + .../targets/collections_plugin_namespace/test.yml | 3 + .../targets/collections_relative_imports/aliases | 4 + .../my_ns/my_col/plugins/module_utils/PSRel1.psm1 | 11 + .../my_ns/my_col/plugins/module_utils/PSRel4.psm1 | 12 + .../my_ns/my_col/plugins/module_utils/my_util1.py | 6 + .../my_ns/my_col/plugins/module_utils/my_util2.py | 8 + .../my_ns/my_col/plugins/module_utils/my_util3.py | 8 + .../plugins/module_utils/sub_pkg/PSRel2.psm1 | 11 + .../my_ns/my_col/plugins/modules/my_module.py | 24 + .../my_ns/my_col/plugins/modules/win_relative.ps1 | 10 + .../plugins/modules/win_relative_optional.ps1 | 17 + .../my_ns/my_col/roles/test/tasks/main.yml | 4 + .../my_ns/my_col2/plugins/module_utils/PSRel3.psm1 | 11 + .../my_col2/plugins/module_utils/sub_pkg/CSRel4.cs | 14 + .../targets/collections_relative_imports/runme.sh | 13 + .../targets/collections_relative_imports/test.yml | 3 + .../collections_relative_imports/windows.yml | 20 + .../targets/collections_runtime_pythonpath/aliases | 2 + .../python/dist/plugins/modules/boo.py | 28 + .../pyproject.toml | 6 + .../ansible-collection-python-dist-boo/setup.cfg | 15 + .../python/dist/plugins/modules/boo.py | 28 + .../collections_runtime_pythonpath/runme.sh | 60 + .../targets/command_nonexisting/aliases | 2 + .../targets/command_nonexisting/tasks/main.yml | 4 + test/integration/targets/command_shell/aliases | 3 + .../targets/command_shell/files/create_afile.sh | 3 + .../targets/command_shell/files/remove_afile.sh | 3 + .../targets/command_shell/files/test.sh | 3 + .../targets/command_shell/meta/main.yml | 3 + .../targets/command_shell/tasks/main.yml | 548 ++++ test/integration/targets/common_network/aliases | 2 + .../targets/common_network/tasks/main.yml | 4 + .../targets/common_network/test_plugins/is_mac.py | 14 + test/integration/targets/conditionals/aliases | 2 + test/integration/targets/conditionals/play.yml | 667 ++++ test/integration/targets/conditionals/runme.sh | 5 + .../integration/targets/conditionals/vars/main.yml | 29 + test/integration/targets/config/aliases | 2 + test/integration/targets/config/files/types.env | 11 + test/integration/targets/config/files/types.ini | 13 + test/integration/targets/config/files/types.vars | 15 + .../targets/config/files/types_dump.txt | 8 + .../targets/config/inline_comment_ansible.cfg | 2 + .../targets/config/lookup_plugins/bogus.py | 51 + .../targets/config/lookup_plugins/types.py | 82 + test/integration/targets/config/runme.sh | 43 + test/integration/targets/config/type_munging.cfg | 8 + test/integration/targets/config/types.yml | 25 + test/integration/targets/config/validation.yml | 17 + test/integration/targets/connection/aliases | 1 + test/integration/targets/connection/test.sh | 22 + .../targets/connection/test_connection.yml | 43 + .../targets/connection/test_reset_connection.yml | 5 + .../action_plugins/delegation_action.py | 12 + .../targets/connection_delegation/aliases | 6 + .../connection_plugins/delegation_connection.py | 45 + .../targets/connection_delegation/inventory.ini | 1 + .../targets/connection_delegation/runme.sh | 9 + .../targets/connection_delegation/test.yml | 23 + test/integration/targets/connection_local/aliases | 2 + test/integration/targets/connection_local/runme.sh | 14 + .../connection_local/test_connection.inventory | 7 + .../targets/connection_paramiko_ssh/aliases | 5 + .../targets/connection_paramiko_ssh/runme.sh | 7 + .../targets/connection_paramiko_ssh/test.sh | 14 + .../test_connection.inventory | 7 + test/integration/targets/connection_psrp/aliases | 4 + .../targets/connection_psrp/files/empty.txt | 0 test/integration/targets/connection_psrp/runme.sh | 24 + .../connection_psrp/test_connection.inventory.j2 | 9 + test/integration/targets/connection_psrp/tests.yml | 133 + .../targets/connection_remote_is_local/aliases | 2 + .../connection_plugins/remote_is_local.py | 25 + .../connection_remote_is_local/tasks/main.yml | 15 + .../targets/connection_remote_is_local/test.yml | 8 + test/integration/targets/connection_ssh/aliases | 3 + .../targets/connection_ssh/check_ssh_defaults.yml | 29 + .../connection_ssh/files/port_overrride_ssh.cfg | 2 + test/integration/targets/connection_ssh/posix.sh | 14 + test/integration/targets/connection_ssh/runme.sh | 81 + .../connection_ssh/test_connection.inventory | 7 + .../targets/connection_ssh/test_ssh_defaults.cfg | 5 + .../targets/connection_ssh/verify_config.yml | 21 + .../targets/connection_windows_ssh/aliases | 5 + .../targets/connection_windows_ssh/runme.sh | 54 + .../test_connection.inventory.j2 | 12 + .../targets/connection_windows_ssh/tests.yml | 32 + .../targets/connection_windows_ssh/tests_fetch.yml | 41 + .../targets/connection_windows_ssh/windows.sh | 25 + test/integration/targets/connection_winrm/aliases | 5 + test/integration/targets/connection_winrm/runme.sh | 23 + .../connection_winrm/test_connection.inventory.j2 | 10 + .../integration/targets/connection_winrm/tests.yml | 28 + test/integration/targets/controller/aliases | 2 + test/integration/targets/controller/tasks/main.yml | 9 + test/integration/targets/copy/aliases | 3 + test/integration/targets/copy/defaults/main.yml | 2 + .../files-different/vault/folder/nested-vault-file | 6 + .../targets/copy/files-different/vault/readme.txt | 5 + .../targets/copy/files-different/vault/vault-file | 6 + test/integration/targets/copy/files/foo.txt | 1 + test/integration/targets/copy/files/subdir/bar.txt | 1 + .../targets/copy/files/subdir/subdir1/empty.txt | 0 .../targets/copy/files/subdir/subdir2/baz.txt | 1 + .../files/subdir/subdir2/subdir3/subdir4/qux.txt | 1 + test/integration/targets/copy/meta/main.yml | 4 + test/integration/targets/copy/tasks/acls.yml | 38 + test/integration/targets/copy/tasks/check_mode.yml | 126 + .../tasks/dest_in_non_existent_directories.yml | 29 + ...dest_in_non_existent_directories_remote_src.yml | 43 + test/integration/targets/copy/tasks/main.yml | 126 + test/integration/targets/copy/tasks/no_log.yml | 82 + test/integration/targets/copy/tasks/selinux.yml | 36 + .../src_file_dest_file_in_non_existent_dir.yml | 26 + ...le_dest_file_in_non_existent_dir_remote_src.yml | 32 + .../copy/tasks/src_remote_file_is_not_file.yml | 39 + test/integration/targets/copy/tasks/tests.yml | 2286 ++++++++++++++ test/integration/targets/cron/aliases | 4 + test/integration/targets/cron/defaults/main.yml | 1 + test/integration/targets/cron/meta/main.yml | 2 + test/integration/targets/cron/tasks/main.yml | 328 ++ test/integration/targets/cron/vars/alpine.yml | 1 + test/integration/targets/cron/vars/default.yml | 1 + test/integration/targets/dataloader/aliases | 2 + .../dataloader/attempt_to_load_invalid_json.yml | 4 + test/integration/targets/dataloader/runme.sh | 6 + .../targets/dataloader/vars/invalid.json | 1 + test/integration/targets/debconf/aliases | 1 + test/integration/targets/debconf/meta/main.yml | 2 + test/integration/targets/debconf/tasks/main.yml | 36 + test/integration/targets/debug/aliases | 2 + test/integration/targets/debug/main.yml | 6 + test/integration/targets/debug/main_fqcn.yml | 6 + test/integration/targets/debug/nosetfacts.yml | 21 + test/integration/targets/debug/runme.sh | 20 + test/integration/targets/debugger/aliases | 3 + test/integration/targets/debugger/inventory | 2 + test/integration/targets/debugger/runme.sh | 5 + test/integration/targets/debugger/test_run_once.py | 35 + .../targets/debugger/test_run_once_playbook.yml | 12 + test/integration/targets/delegate_to/aliases | 4 + .../delegate_to/connection_plugins/fakelocal.py | 76 + .../targets/delegate_to/delegate_and_nolog.yml | 8 + .../targets/delegate_to/delegate_facts_block.yml | 25 + .../targets/delegate_to/delegate_facts_loop.yml | 40 + .../delegate_to/delegate_local_from_root.yml | 10 + .../delegate_to/delegate_to_lookup_context.yml | 4 + .../delegate_to/delegate_vars_hanldling.yml | 58 + .../delegate_with_fact_from_delegate_host.yml | 18 + .../targets/delegate_to/discovery_applied.yml | 8 + .../integration/targets/delegate_to/files/testfile | 1 + .../targets/delegate_to/has_hostvars.yml | 64 + test/integration/targets/delegate_to/inventory | 17 + .../targets/delegate_to/inventory_interpreters | 5 + .../delegate_to/library/detect_interpreter.py | 18 + .../targets/delegate_to/resolve_vars.yml | 16 + .../delegate_to_lookup_context/tasks/main.yml | 5 + .../delegate_to_lookup_context/templates/one.j2 | 1 + .../delegate_to_lookup_context/templates/two.j2 | 1 + .../roles/test_template/templates/foo.j2 | 3 + test/integration/targets/delegate_to/runme.sh | 78 + .../targets/delegate_to/test_delegate_to.yml | 82 + .../test_delegate_to_lookup_context.yml | 12 + .../delegate_to/test_delegate_to_loop_caching.yml | 45 + .../test_delegate_to_loop_randomness.yml | 73 + .../targets/delegate_to/test_loop_control.yml | 16 + .../targets/delegate_to/verify_interpreter.yml | 47 + .../targets/dict_transformations/aliases | 2 + .../library/convert_camelCase.py | 48 + .../library/convert_snake_case.py | 55 + .../targets/dict_transformations/tasks/main.yml | 3 + .../tasks/test_convert_camelCase.yml | 33 + .../tasks/test_convert_snake_case.yml | 35 + test/integration/targets/dnf/aliases | 6 + test/integration/targets/dnf/meta/main.yml | 4 + test/integration/targets/dnf/tasks/cacheonly.yml | 16 + test/integration/targets/dnf/tasks/dnf.yml | 834 +++++ .../targets/dnf/tasks/dnfinstallroot.yml | 35 + .../targets/dnf/tasks/dnfreleasever.yml | 47 + test/integration/targets/dnf/tasks/filters.yml | 162 + .../targets/dnf/tasks/filters_check_mode.yml | 118 + test/integration/targets/dnf/tasks/gpg.yml | 88 + test/integration/targets/dnf/tasks/logging.yml | 48 + test/integration/targets/dnf/tasks/main.yml | 74 + test/integration/targets/dnf/tasks/modularity.yml | 104 + test/integration/targets/dnf/tasks/repo.yml | 309 ++ .../targets/dnf/tasks/skip_broken_and_nobest.yml | 318 ++ .../targets/dnf/tasks/test_sos_removal.yml | 19 + test/integration/targets/dnf/vars/CentOS.yml | 2 + test/integration/targets/dnf/vars/Fedora.yml | 6 + test/integration/targets/dnf/vars/RedHat-9.yml | 3 + test/integration/targets/dnf/vars/RedHat.yml | 2 + test/integration/targets/dnf/vars/main.yml | 6 + test/integration/targets/dpkg_selections/aliases | 6 + .../targets/dpkg_selections/defaults/main.yaml | 1 + .../dpkg_selections/tasks/dpkg_selections.yaml | 89 + .../targets/dpkg_selections/tasks/main.yaml | 3 + test/integration/targets/egg-info/aliases | 2 + .../lookup_plugins/import_pkg_resources.py | 11 + test/integration/targets/egg-info/tasks/main.yml | 3 + test/integration/targets/embedded_module/aliases | 2 + .../library/test_integration_module | 3 + .../targets/embedded_module/tasks/main.yml | 9 + test/integration/targets/entry_points/aliases | 2 + test/integration/targets/entry_points/runme.sh | 31 + test/integration/targets/environment/aliases | 2 + test/integration/targets/environment/runme.sh | 5 + .../targets/environment/test_environment.yml | 173 ++ .../targets/error_from_connection/aliases | 2 + .../connection_plugins/dummy.py | 42 + .../targets/error_from_connection/inventory | 2 + .../targets/error_from_connection/play.yml | 20 + .../targets/error_from_connection/runme.sh | 5 + test/integration/targets/expect/aliases | 3 + test/integration/targets/expect/files/foo.txt | 1 + .../targets/expect/files/test_command.py | 25 + test/integration/targets/expect/meta/main.yml | 2 + test/integration/targets/expect/tasks/main.yml | 209 ++ test/integration/targets/facts_d/aliases | 2 + .../targets/facts_d/files/basdscript.fact | 3 + .../targets/facts_d/files/goodscript.fact | 3 + .../targets/facts_d/files/preferences.fact | 2 + .../targets/facts_d/files/unreadable.fact | 1 + test/integration/targets/facts_d/meta/main.yml | 3 + test/integration/targets/facts_d/tasks/main.yml | 53 + .../targets/facts_linux_network/aliases | 7 + .../targets/facts_linux_network/meta/main.yml | 2 + .../targets/facts_linux_network/tasks/main.yml | 51 + test/integration/targets/failed_when/aliases | 2 + .../integration/targets/failed_when/tasks/main.yml | 80 + test/integration/targets/fetch/aliases | 3 + test/integration/targets/fetch/cleanup.yml | 16 + .../targets/fetch/injection/avoid_slurp_return.yml | 26 + test/integration/targets/fetch/injection/here.txt | 1 + .../targets/fetch/injection/library/slurp.py | 29 + .../fetch/roles/fetch_tests/defaults/main.yml | 1 + .../fetch/roles/fetch_tests/handlers/main.yml | 8 + .../targets/fetch/roles/fetch_tests/meta/main.yml | 2 + .../roles/fetch_tests/tasks/fail_on_missing.yml | 53 + .../fetch/roles/fetch_tests/tasks/failures.yml | 41 + .../targets/fetch/roles/fetch_tests/tasks/main.yml | 5 + .../fetch/roles/fetch_tests/tasks/normal.yml | 38 + .../fetch/roles/fetch_tests/tasks/setup.yml | 46 + .../fetch/roles/fetch_tests/tasks/symlink.yml | 13 + .../fetch/roles/fetch_tests/vars/Darwin.yml | 4 + .../fetch/roles/fetch_tests/vars/default.yml | 1 + test/integration/targets/fetch/run_fetch_tests.yml | 5 + test/integration/targets/fetch/runme.sh | 34 + .../targets/fetch/setup_unreadable_test.yml | 40 + .../targets/fetch/test_unreadable_with_stat.yml | 36 + test/integration/targets/file/aliases | 2 + test/integration/targets/file/defaults/main.yml | 2 + test/integration/targets/file/files/foo.txt | 1 + .../targets/file/files/foobar/directory/fileC | 0 .../targets/file/files/foobar/directory/fileD | 0 test/integration/targets/file/files/foobar/fileA | 0 test/integration/targets/file/files/foobar/fileB | 0 test/integration/targets/file/handlers/main.yml | 20 + test/integration/targets/file/meta/main.yml | 4 + test/integration/targets/file/tasks/diff_peek.yml | 10 + .../targets/file/tasks/directory_as_dest.yml | 345 +++ test/integration/targets/file/tasks/initialize.yml | 15 + .../targets/file/tasks/link_rewrite.yml | 47 + test/integration/targets/file/tasks/main.yml | 960 ++++++ .../targets/file/tasks/modification_time.yml | 70 + .../targets/file/tasks/selinux_tests.yml | 33 + test/integration/targets/file/tasks/state_link.yml | 501 +++ .../targets/file/tasks/unicode_path.yml | 10 + test/integration/targets/filter_core/aliases | 1 + .../integration/targets/filter_core/files/9851.txt | 3 + .../targets/filter_core/files/fileglob/one.txt | 0 .../targets/filter_core/files/fileglob/two.txt | 0 test/integration/targets/filter_core/files/foo.txt | 69 + .../filter_core/handle_undefined_type_errors.yml | 29 + .../targets/filter_core/host_vars/localhost | 1 + test/integration/targets/filter_core/meta/main.yml | 3 + test/integration/targets/filter_core/runme.sh | 6 + test/integration/targets/filter_core/runme.yml | 3 + .../integration/targets/filter_core/tasks/main.yml | 708 +++++ .../targets/filter_core/templates/foo.j2 | 62 + .../targets/filter_core/templates/py26json.j2 | 2 + test/integration/targets/filter_core/vars/main.yml | 106 + test/integration/targets/filter_encryption/aliases | 1 + .../integration/targets/filter_encryption/base.yml | 37 + .../integration/targets/filter_encryption/runme.sh | 5 + test/integration/targets/filter_mathstuff/aliases | 1 + .../filter_mathstuff/host_vars/localhost.yml | 1 + test/integration/targets/filter_mathstuff/runme.sh | 7 + .../integration/targets/filter_mathstuff/runme.yml | 4 + .../targets/filter_mathstuff/tasks/main.yml | 320 ++ .../filter_mathstuff/vars/defined_later.yml | 3 + .../targets/filter_mathstuff/vars/main.yml | 1 + test/integration/targets/filter_urls/aliases | 1 + .../integration/targets/filter_urls/tasks/main.yml | 24 + test/integration/targets/filter_urlsplit/aliases | 1 + .../targets/filter_urlsplit/tasks/main.yml | 30 + test/integration/targets/find/aliases | 1 + test/integration/targets/find/files/a.txt | 2 + test/integration/targets/find/files/log.txt | 4 + test/integration/targets/find/meta/main.yml | 3 + test/integration/targets/find/tasks/main.yml | 376 +++ test/integration/targets/fork_safe_stdio/aliases | 3 + .../fork_safe_stdio/callback_plugins/spewstdio.py | 58 + test/integration/targets/fork_safe_stdio/hosts | 5 + .../targets/fork_safe_stdio/run-with-pty.py | 11 + test/integration/targets/fork_safe_stdio/runme.sh | 20 + test/integration/targets/fork_safe_stdio/test.yml | 5 + .../targets/fork_safe_stdio/vendored_pty.py | 189 ++ test/integration/targets/gathering/aliases | 2 + test/integration/targets/gathering/explicit.yml | 14 + test/integration/targets/gathering/implicit.yml | 23 + test/integration/targets/gathering/runme.sh | 7 + test/integration/targets/gathering/smart.yml | 23 + test/integration/targets/gathering/uuid.fact | 10 + test/integration/targets/gathering_facts/aliases | 3 + .../targets/gathering_facts/cache_plugins/none.py | 50 + .../cisco/ios/plugins/modules/ios_facts.py | 38 + test/integration/targets/gathering_facts/inventory | 2 + .../targets/gathering_facts/library/bogus_facts | 12 + .../targets/gathering_facts/library/facts_one | 25 + .../targets/gathering_facts/library/facts_two | 24 + .../targets/gathering_facts/library/file_utils.py | 54 + .../targets/gathering_facts/one_two.json | 27 + .../targets/gathering_facts/prevent_clobbering.yml | 8 + test/integration/targets/gathering_facts/runme.sh | 27 + .../gathering_facts/test_gathering_facts.yml | 536 ++++ .../gathering_facts/test_module_defaults.yml | 130 + .../gathering_facts/test_prevent_injection.yml | 14 + .../targets/gathering_facts/test_run_once.yml | 32 + .../targets/gathering_facts/two_one.json | 27 + test/integration/targets/gathering_facts/uuid.fact | 10 + .../targets/gathering_facts/verify_merge_facts.yml | 41 + .../targets/gathering_facts/verify_subset.yml | 13 + test/integration/targets/get_url/aliases | 3 + .../targets/get_url/files/testserver.py | 23 + test/integration/targets/get_url/meta/main.yml | 4 + test/integration/targets/get_url/tasks/ciphers.yml | 19 + test/integration/targets/get_url/tasks/main.yml | 674 ++++ .../targets/get_url/tasks/use_gssapi.yml | 45 + .../targets/get_url/tasks/use_netrc.yml | 67 + test/integration/targets/getent/aliases | 1 + test/integration/targets/getent/meta/main.yml | 2 + test/integration/targets/getent/tasks/main.yml | 46 + test/integration/targets/git/aliases | 1 + .../targets/git/handlers/cleanup-default.yml | 6 + .../targets/git/handlers/cleanup-freebsd.yml | 5 + test/integration/targets/git/handlers/main.yml | 7 + test/integration/targets/git/meta/main.yml | 4 + .../targets/git/tasks/ambiguous-ref.yml | 37 + test/integration/targets/git/tasks/archive.yml | 122 + .../targets/git/tasks/change-repo-url.yml | 132 + .../targets/git/tasks/checkout-new-tag.yml | 54 + test/integration/targets/git/tasks/depth.yml | 229 ++ .../targets/git/tasks/forcefully-fetch-tag.yml | 38 + test/integration/targets/git/tasks/formats.yml | 40 + .../targets/git/tasks/gpg-verification.yml | 212 ++ test/integration/targets/git/tasks/localmods.yml | 112 + test/integration/targets/git/tasks/main.yml | 42 + .../targets/git/tasks/missing_hostkey.yml | 61 + .../git/tasks/missing_hostkey_acceptnew.yml | 78 + .../targets/git/tasks/no-destination.yml | 13 + .../integration/targets/git/tasks/reset-origin.yml | 25 + .../targets/git/tasks/separate-git-dir.yml | 132 + .../targets/git/tasks/setup-local-repos.yml | 45 + test/integration/targets/git/tasks/setup.yml | 43 + .../targets/git/tasks/single-branch.yml | 87 + .../targets/git/tasks/specific-revision.yml | 238 ++ test/integration/targets/git/tasks/submodules.yml | 150 + test/integration/targets/git/vars/main.yml | 98 + test/integration/targets/group/aliases | 1 + test/integration/targets/group/files/gidget.py | 15 + test/integration/targets/group/files/grouplist.sh | 20 + test/integration/targets/group/meta/main.yml | 2 + test/integration/targets/group/tasks/main.yml | 40 + test/integration/targets/group/tasks/tests.yml | 343 +++ test/integration/targets/group_by/aliases | 1 + .../integration/targets/group_by/create_groups.yml | 39 + test/integration/targets/group_by/group_vars/all | 3 + .../targets/group_by/group_vars/camelus | 1 + .../targets/group_by/group_vars/vicugna | 1 + .../targets/group_by/inventory.group_by | 9 + test/integration/targets/group_by/runme.sh | 6 + .../integration/targets/group_by/test_group_by.yml | 187 ++ .../targets/group_by/test_group_by_skipped.yml | 30 + test/integration/targets/groupby_filter/aliases | 2 + .../targets/groupby_filter/tasks/main.yml | 16 + test/integration/targets/handler_race/aliases | 2 + test/integration/targets/handler_race/inventory | 30 + .../roles/do_handlers/handlers/main.yml | 4 + .../handler_race/roles/do_handlers/tasks/main.yml | 9 + .../handler_race/roles/more_sleep/tasks/main.yml | 8 + .../handler_race/roles/random_sleep/tasks/main.yml | 8 + test/integration/targets/handler_race/runme.sh | 6 + .../targets/handler_race/test_handler_race.yml | 10 + test/integration/targets/handlers/46447.yml | 16 + test/integration/targets/handlers/52561.yml | 20 + test/integration/targets/handlers/54991.yml | 11 + test/integration/targets/handlers/58841.yml | 9 + .../targets/handlers/79776-handlers.yml | 2 + test/integration/targets/handlers/79776.yml | 10 + test/integration/targets/handlers/aliases | 2 + .../integration/targets/handlers/from_handlers.yml | 39 + test/integration/targets/handlers/handlers.yml | 2 + .../include_handlers_fail_force-handlers.yml | 2 + .../handlers/include_handlers_fail_force.yml | 11 + .../targets/handlers/inventory.handlers | 10 + test/integration/targets/handlers/order.yml | 34 + .../import_template_handler_names/tasks/main.yml | 11 + .../roles/template_handler_names/handlers/main.yml | 5 + .../tasks/evaluation_time.yml | 5 + .../tasks/lazy_evaluation.yml | 5 + .../roles/test_force_handlers/handlers/main.yml | 2 + .../roles/test_force_handlers/tasks/main.yml | 26 + .../handlers/roles/test_handlers/handlers/main.yml | 5 + .../handlers/roles/test_handlers/meta/main.yml | 1 + .../handlers/roles/test_handlers/tasks/main.yml | 52 + .../roles/test_handlers_include/handlers/main.yml | 1 + .../roles/test_handlers_include/tasks/main.yml | 4 + .../test_handlers_include_role/handlers/main.yml | 5 + .../roles/test_handlers_include_role/meta/main.yml | 1 + .../test_handlers_include_role/tasks/main.yml | 47 + .../roles/test_handlers_listen/handlers/main.yml | 10 + .../roles/test_handlers_listen/tasks/main.yml | 6 + .../test_handlers_meta/handlers/alternate.yml | 12 + .../roles/test_handlers_meta/handlers/main.yml | 10 + .../roles/test_handlers_meta/tasks/main.yml | 75 + .../handlers/A.yml | 1 + .../handlers/main.yml | 5 + .../test_role_handlers_include_tasks/tasks/B.yml | 1 + .../test_templating_in_handlers/handlers/main.yml | 21 + .../test_templating_in_handlers/tasks/main.yml | 26 + test/integration/targets/handlers/runme.sh | 183 ++ .../handlers/test_block_as_handler-import.yml | 7 + .../handlers/test_block_as_handler-include.yml | 8 + ...st_block_as_handler-include_import-handlers.yml | 8 + .../targets/handlers/test_block_as_handler.yml | 13 + .../handlers/test_flush_handlers_as_handler.yml | 9 + .../handlers/test_flush_handlers_rescue_always.yml | 22 + .../handlers/test_flush_in_rescue_always.yml | 35 + .../targets/handlers/test_force_handlers.yml | 27 + .../handlers/test_fqcn_meta_flush_handlers.yml | 14 + .../integration/targets/handlers/test_handlers.yml | 47 + .../handlers/test_handlers_any_errors_fatal.yml | 24 + .../targets/handlers/test_handlers_include.yml | 14 + .../handlers/test_handlers_include_role.yml | 8 + .../handlers/test_handlers_including_task.yml | 16 + .../handlers/test_handlers_inexistent_notify.yml | 10 + .../handlers/test_handlers_infinite_loop.yml | 25 + .../targets/handlers/test_handlers_listen.yml | 128 + .../targets/handlers/test_handlers_meta.yml | 9 + .../handlers/test_handlers_template_run_once.yml | 12 + .../targets/handlers/test_listening_handlers.yml | 39 + .../handlers/test_notify_included-handlers.yml | 3 + .../targets/handlers/test_notify_included.yml | 16 + .../targets/handlers/test_role_as_handler.yml | 11 + .../test_role_handlers_including_tasks.yml | 18 + .../targets/handlers/test_skip_flush.yml | 13 + .../handlers/test_templating_in_handlers.yml | 62 + test/integration/targets/hardware_facts/aliases | 4 + .../targets/hardware_facts/meta/main.yml | 2 + .../targets/hardware_facts/tasks/Linux.yml | 92 + .../targets/hardware_facts/tasks/main.yml | 2 + test/integration/targets/hash/aliases | 2 + test/integration/targets/hash/group_vars/all | 3 + test/integration/targets/hash/host_vars/testhost | 2 + .../roles/test_hash_behaviour/defaults/main.yml | 21 + .../hash/roles/test_hash_behaviour/meta/main.yml | 17 + .../hash/roles/test_hash_behaviour/tasks/main.yml | 37 + .../hash/roles/test_hash_behaviour/vars/main.yml | 21 + test/integration/targets/hash/runme.sh | 11 + test/integration/targets/hash/test_hash.yml | 21 + test/integration/targets/hash/test_inv1.yml | 10 + test/integration/targets/hash/test_inv2.yml | 8 + .../targets/hash/test_inventory_hash.yml | 41 + .../targets/hash/vars/test_hash_vars.yml | 3 + test/integration/targets/hostname/aliases | 2 + test/integration/targets/hostname/tasks/Debian.yml | 20 + test/integration/targets/hostname/tasks/MacOSX.yml | 52 + test/integration/targets/hostname/tasks/RedHat.yml | 15 + .../targets/hostname/tasks/check_mode.yml | 20 + .../integration/targets/hostname/tasks/default.yml | 2 + test/integration/targets/hostname/tasks/main.yml | 52 + .../targets/hostname/tasks/test_check_mode.yml | 50 + .../targets/hostname/tasks/test_normal.yml | 54 + test/integration/targets/hostname/vars/FreeBSD.yml | 1 + test/integration/targets/hostname/vars/RedHat.yml | 1 + test/integration/targets/hostname/vars/default.yml | 1 + test/integration/targets/hosts_field/aliases | 2 + .../targets/hosts_field/inventory.hosts_field | 1 + test/integration/targets/hosts_field/runme.sh | 49 + .../targets/hosts_field/test_hosts_field.json | 1 + .../targets/hosts_field/test_hosts_field.yml | 62 + test/integration/targets/ignore_errors/aliases | 2 + .../targets/ignore_errors/meta/main.yml | 2 + .../targets/ignore_errors/tasks/main.yml | 22 + .../integration/targets/ignore_unreachable/aliases | 2 + .../ignore_unreachable/fake_connectors/bad_exec.py | 14 + .../fake_connectors/bad_put_file.py | 14 + .../targets/ignore_unreachable/inventory | 3 + .../targets/ignore_unreachable/meta/main.yml | 2 + .../targets/ignore_unreachable/runme.sh | 16 + .../test_base_cannot_connect.yml | 5 + .../ignore_unreachable/test_cannot_connect.yml | 29 + .../ignore_unreachable/test_with_bad_plugins.yml | 24 + test/integration/targets/import_tasks/aliases | 2 + .../targets/import_tasks/inherit_notify.yml | 15 + test/integration/targets/import_tasks/runme.sh | 5 + .../targets/import_tasks/tasks/trigger_change.yml | 2 + .../targets/incidental_ios_file/aliases | 2 + .../targets/incidental_ios_file/defaults/main.yaml | 2 + .../targets/incidental_ios_file/ios1.cfg | 3 + .../targets/incidental_ios_file/nonascii.bin | Bin 0 -> 32768 bytes .../targets/incidental_ios_file/tasks/cli.yaml | 17 + .../targets/incidental_ios_file/tasks/main.yaml | 2 + .../incidental_ios_file/tests/cli/net_get.yaml | 52 + .../incidental_ios_file/tests/cli/net_put.yaml | 73 + .../targets/incidental_vyos_config/aliases | 2 + .../incidental_vyos_config/defaults/main.yaml | 3 + .../targets/incidental_vyos_config/tasks/cli.yaml | 26 + .../incidental_vyos_config/tasks/cli_config.yaml | 18 + .../targets/incidental_vyos_config/tasks/main.yaml | 3 + .../incidental_vyos_config/tests/cli/backup.yaml | 113 + .../tests/cli/check_config.yaml | 63 + .../incidental_vyos_config/tests/cli/comment.yaml | 34 + .../incidental_vyos_config/tests/cli/config.cfg | 3 + .../incidental_vyos_config/tests/cli/save.yaml | 54 + .../incidental_vyos_config/tests/cli/simple.yaml | 53 + .../tests/cli_config/cli_backup.yaml | 114 + .../tests/cli_config/cli_basic.yaml | 28 + .../tests/cli_config/cli_comment.yaml | 30 + .../incidental_vyos_lldp_interfaces/aliases | 2 + .../defaults/main.yaml | 3 + .../incidental_vyos_lldp_interfaces/meta/main.yaml | 3 + .../incidental_vyos_lldp_interfaces/tasks/cli.yaml | 19 + .../tasks/main.yaml | 2 + .../tests/cli/_populate.yaml | 14 + .../tests/cli/_populate_intf.yaml | 10 + .../tests/cli/_remove_config.yaml | 8 + .../tests/cli/deleted.yaml | 46 + .../tests/cli/empty_config.yaml | 36 + .../tests/cli/merged.yaml | 58 + .../tests/cli/overridden.yaml | 49 + .../tests/cli/replaced.yaml | 63 + .../tests/cli/rtt.yaml | 57 + .../incidental_vyos_lldp_interfaces/vars/main.yaml | 130 + .../targets/incidental_vyos_prepare_tests/aliases | 1 + .../incidental_vyos_prepare_tests/tasks/main.yaml | 13 + .../targets/incidental_win_reboot/aliases | 2 + .../targets/incidental_win_reboot/tasks/main.yml | 70 + .../templates/post_reboot.ps1 | 8 + test/integration/targets/include_import/aliases | 2 + .../targets/include_import/apply/import_apply.yml | 31 + .../targets/include_import/apply/include_apply.yml | 50 + .../include_import/apply/include_apply_65710.yml | 11 + .../targets/include_import/apply/include_tasks.yml | 2 + .../apply/roles/include_role/tasks/main.yml | 2 + .../apply/roles/include_role2/tasks/main.yml | 2 + .../empty_group_warning/playbook.yml | 13 + .../include_import/empty_group_warning/tasks.yml | 3 + .../grandchild/block_include_tasks.yml | 2 + .../targets/include_import/grandchild/import.yml | 1 + .../grandchild/import_include_include_tasks.yml | 2 + .../include_import/grandchild/include_level_1.yml | 1 + .../include_import/handler_addressing/playbook.yml | 11 + .../roles/import_handler_test/handlers/main.yml | 2 + .../roles/import_handler_test/tasks/handlers.yml | 2 + .../roles/import_handler_test/tasks/main.yml | 3 + .../roles/include_handler_test/handlers/main.yml | 2 + .../roles/include_handler_test/tasks/handlers.yml | 2 + .../roles/include_handler_test/tasks/main.yml | 3 + .../include_import/include_role_omit/playbook.yml | 12 + .../include_role_omit/roles/foo/tasks/main.yml | 2 + test/integration/targets/include_import/inventory | 6 + .../targets/include_import/issue73657.yml | 8 + .../targets/include_import/issue73657_tasks.yml | 2 + .../include_import/nestedtasks/nested/nested.yml | 2 + .../include_import/parent_templating/playbook.yml | 11 + .../roles/test/tasks/localhost.yml | 1 + .../parent_templating/roles/test/tasks/main.yml | 1 + .../parent_templating/roles/test/tasks/other.yml | 2 + .../include_import/playbook/group_vars/all.yml | 1 + .../targets/include_import/playbook/playbook1.yml | 9 + .../targets/include_import/playbook/playbook2.yml | 9 + .../targets/include_import/playbook/playbook3.yml | 10 + .../targets/include_import/playbook/playbook4.yml | 9 + .../playbook/playbook_needing_vars.yml | 6 + .../roles/import_playbook_role/tasks/main.yml | 2 + .../playbook/sub_playbook/library/helloworld.py | 30 + .../playbook/sub_playbook/sub_playbook.yml | 4 + .../playbook/test_import_playbook.yml | 22 + .../playbook/test_import_playbook_tags.yml | 10 + .../playbook/test_templated_filenames.yml | 47 + .../targets/include_import/playbook/validate1.yml | 10 + .../targets/include_import/playbook/validate2.yml | 10 + .../targets/include_import/playbook/validate34.yml | 11 + .../include_import/playbook/validate_tags.yml | 11 + .../playbook/validate_templated_playbook.yml | 5 + .../playbook/validate_templated_tasks.yml | 1 + .../include_import/public_exposure/no_bleeding.yml | 25 + .../public_exposure/no_overwrite_roles.yml | 4 + .../include_import/public_exposure/playbook.yml | 56 + .../roles/call_import/tasks/main.yml | 6 + .../roles/dynamic/defaults/main.yml | 1 + .../public_exposure/roles/dynamic/tasks/main.yml | 5 + .../public_exposure/roles/dynamic/vars/main.yml | 1 + .../roles/dynamic_private/defaults/main.yml | 1 + .../roles/dynamic_private/tasks/main.yml | 5 + .../roles/dynamic_private/vars/main.yml | 1 + .../public_exposure/roles/from/defaults/from.yml | 1 + .../public_exposure/roles/from/tasks/from.yml | 5 + .../public_exposure/roles/from/vars/from.yml | 1 + .../roles/regular/defaults/main.yml | 1 + .../public_exposure/roles/regular/tasks/main.yml | 5 + .../public_exposure/roles/regular/vars/main.yml | 1 + .../public_exposure/roles/static/defaults/main.yml | 1 + .../public_exposure/roles/static/tasks/main.yml | 5 + .../public_exposure/roles/static/vars/main.yml | 1 + .../include_import/role/test_import_role.yml | 139 + .../include_import/role/test_include_role.yml | 166 + .../role/test_include_role_vars_from.yml | 10 + .../roles/delegated_handler/handlers/main.yml | 4 + .../roles/delegated_handler/tasks/main.yml | 3 + .../roles/dup_allowed_role/meta/main.yml | 2 + .../roles/dup_allowed_role/tasks/main.yml | 3 + .../roles/loop_name_assert/tasks/main.yml | 4 + .../nested/nested_dep_role2/defaults/main.yml | 3 + .../nested/nested/nested_dep_role2/meta/main.yml | 2 + .../nested/nested/nested_dep_role2/tasks/main.yml | 2 + .../nested/nested/nested_dep_role2/tasks/rund.yml | 2 + .../nested/nested/nested_dep_role2/vars/main.yml | 2 + .../nested/nested_dep_role2a/defaults/main.yml | 3 + .../nested/nested/nested_dep_role2a/meta/main.yml | 2 + .../nested/nested/nested_dep_role2a/tasks/main.yml | 2 + .../nested/nested/nested_dep_role2a/tasks/rune.yml | 2 + .../nested/nested/nested_dep_role2a/vars/main.yml | 2 + .../nested/nested_dep_role2b/defaults/main.yml | 3 + .../nested/nested/nested_dep_role2b/meta/main.yml | 1 + .../nested/nested/nested_dep_role2b/tasks/main.yml | 2 + .../nested/nested/nested_dep_role2b/tasks/runf.yml | 2 + .../nested/nested/nested_dep_role2b/vars/main.yml | 2 + .../roles/nested/nested_dep_role/defaults/main.yml | 3 + .../roles/nested/nested_dep_role/meta/main.yml | 2 + .../roles/nested/nested_dep_role/tasks/main.yml | 2 + .../roles/nested/nested_dep_role/tasks/runc.yml | 4 + .../roles/nested/nested_dep_role/vars/main.yml | 2 + .../roles/nested_include_task/meta/main.yml | 2 + .../roles/nested_include_task/tasks/main.yml | 2 + .../roles/nested_include_task/tasks/runa.yml | 3 + .../include_import/roles/role1/tasks/canary1.yml | 2 + .../include_import/roles/role1/tasks/canary2.yml | 2 + .../include_import/roles/role1/tasks/canary3.yml | 2 + .../include_import/roles/role1/tasks/fail.yml | 3 + .../include_import/roles/role1/tasks/main.yml | 3 + .../include_import/roles/role1/tasks/r1t01.yml | 1 + .../include_import/roles/role1/tasks/r1t02.yml | 1 + .../include_import/roles/role1/tasks/r1t03.yml | 1 + .../include_import/roles/role1/tasks/r1t04.yml | 1 + .../include_import/roles/role1/tasks/r1t05.yml | 1 + .../include_import/roles/role1/tasks/r1t06.yml | 1 + .../include_import/roles/role1/tasks/r1t07.yml | 1 + .../include_import/roles/role1/tasks/r1t08.yml | 1 + .../include_import/roles/role1/tasks/r1t09.yml | 1 + .../include_import/roles/role1/tasks/r1t10.yml | 1 + .../include_import/roles/role1/tasks/r1t11.yml | 1 + .../include_import/roles/role1/tasks/r1t12.yml | 2 + .../include_import/roles/role1/tasks/tasks.yml | 2 + .../include_import/roles/role1/tasks/templated.yml | 1 + .../include_import/roles/role1/tasks/vartest.yml | 2 + .../include_import/roles/role1/vars/main.yml | 1 + .../include_import/roles/role1/vars/role1vars.yml | 1 + .../include_import/roles/role2/tasks/main.yml | 3 + .../include_import/roles/role3/defaults/main.yml | 2 + .../include_import/roles/role3/handlers/main.yml | 3 + .../include_import/roles/role3/tasks/main.yml | 3 + .../include_import/roles/role3/tasks/tasks.yml | 2 + .../include_import/roles/role3/tasks/vartest.yml | 2 + .../include_import/roles/role3/vars/main.yml | 1 + .../include_import/roles/role3/vars/role3vars.yml | 2 + .../roles/role_with_deps/meta/main.yml | 3 + .../roles/role_with_deps/tasks/main.yml | 2 + .../targets/include_import/run_once/include_me.yml | 2 + .../targets/include_import/run_once/playbook.yml | 61 + test/integration/targets/include_import/runme.sh | 141 + .../targets/include_import/tasks/debug_item.yml | 2 + .../targets/include_import/tasks/hello/.gitignore | 1 + .../targets/include_import/tasks/hello/keep | 0 .../targets/include_import/tasks/nested/nested.yml | 2 + .../targets/include_import/tasks/tasks1.yml | 5 + .../targets/include_import/tasks/tasks2.yml | 5 + .../targets/include_import/tasks/tasks3.yml | 5 + .../targets/include_import/tasks/tasks4.yml | 5 + .../targets/include_import/tasks/tasks5.yml | 6 + .../targets/include_import/tasks/tasks6.yml | 5 + .../tasks/test_allow_single_role_dup.yml | 8 + .../include_import/tasks/test_import_tasks.yml | 41 + .../tasks/test_import_tasks_tags.yml | 23 + .../tasks/test_include_dupe_loop.yml | 8 + .../include_import/tasks/test_include_tasks.yml | 44 + .../tasks/test_include_tasks_tags.yml | 25 + .../include_import/tasks/test_recursion.yml | 6 + .../targets/include_import/tasks/validate3.yml | 4 + .../targets/include_import/tasks/validate_tags.yml | 8 + .../include_import/test_copious_include_tasks.yml | 44 + .../test_copious_include_tasks_fqcn.yml | 44 + .../test_grandparent_inheritance.yml | 29 + .../test_grandparent_inheritance_fqcn.yml | 29 + .../targets/include_import/test_include_loop.yml | 17 + .../include_import/test_include_loop_fqcn.yml | 17 + .../include_import/test_loop_var_bleed.yaml | 9 + .../targets/include_import/test_nested_tasks.yml | 6 + .../include_import/test_nested_tasks_fqcn.yml | 6 + .../targets/include_import/test_role_recursion.yml | 7 + .../include_import/test_role_recursion_fqcn.yml | 7 + .../include_import/undefined_var/include_tasks.yml | 5 + .../undefined_var/include_that_defines_var.yml | 5 + .../include_import/undefined_var/playbook.yml | 35 + .../valid_include_keywords/include_me.yml | 6 + .../valid_include_keywords/include_me_listen.yml | 2 + .../valid_include_keywords/include_me_notify.yml | 2 + .../valid_include_keywords/playbook.yml | 40 + .../targets/include_import_tasks_nested/aliases | 2 + .../include_import_tasks_nested/tasks/main.yml | 11 + .../tasks/nested/nested_adjacent.yml | 2 + .../tasks/nested/nested_import.yml | 1 + .../tasks/nested/nested_include.yml | 1 + .../targets/include_parent_role_vars/aliases | 2 + .../tasks/included_by_other_role.yml | 37 + .../tasks/included_by_ourselves.yml | 14 + .../include_parent_role_vars/tasks/main.yml | 21 + .../targets/include_vars-ad-hoc/aliases | 2 + .../targets/include_vars-ad-hoc/dir/inc.yml | 1 + .../targets/include_vars-ad-hoc/runme.sh | 6 + test/integration/targets/include_vars/aliases | 1 + .../targets/include_vars/defaults/main.yml | 3 + .../targets/include_vars/tasks/main.yml | 217 ++ .../targets/include_vars/vars/all/all.yml | 3 + .../vars/environments/development/all.yml | 3 + .../environments/development/services/webapp.yml | 4 + .../targets/include_vars/vars/no_auto_unsafe.yml | 1 + .../include_vars/vars/services/service_vars.yml | 2 + .../vars/services/service_vars_fqcn.yml | 3 + .../targets/include_vars/vars/services/webapp.yml | 4 + .../vars/webapp/file_without_extension | 2 + .../targets/include_vars/vars2/hashes/hash1.yml | 5 + .../targets/include_vars/vars2/hashes/hash2.yml | 5 + .../targets/include_when_parent_is_dynamic/aliases | 2 + .../include_when_parent_is_dynamic/playbook.yml | 4 + .../include_when_parent_is_dynamic/runme.sh | 13 + .../syntax_error.yml | 1 + .../include_when_parent_is_dynamic/tasks.yml | 12 + .../targets/include_when_parent_is_static/aliases | 2 + .../include_when_parent_is_static/playbook.yml | 4 + .../targets/include_when_parent_is_static/runme.sh | 13 + .../include_when_parent_is_static/syntax_error.yml | 1 + .../include_when_parent_is_static/tasks.yml | 12 + test/integration/targets/includes/aliases | 2 + .../includes/include_on_playbook_should_fail.yml | 1 + .../targets/includes/includes_loop_rescue.yml | 29 + .../targets/includes/inherit_notify.yml | 18 + .../includes/roles/test_includes/handlers/main.yml | 1 + .../roles/test_includes/handlers/more_handlers.yml | 12 + .../roles/test_includes/tasks/branch_toplevel.yml | 11 + .../includes/roles/test_includes/tasks/empty.yml | 0 .../roles/test_includes/tasks/included_task1.yml | 9 + .../roles/test_includes/tasks/leaf_sublevel.yml | 2 + .../includes/roles/test_includes/tasks/main.yml | 114 + .../roles/test_includes/tasks/not_a_role_task.yml | 4 + .../roles/test_includes_free/tasks/inner.yml | 2 + .../roles/test_includes_free/tasks/inner_fqcn.yml | 2 + .../roles/test_includes_free/tasks/main.yml | 9 + .../test_includes_host_pinned/tasks/inner.yml | 2 + .../roles/test_includes_host_pinned/tasks/main.yml | 6 + test/integration/targets/includes/runme.sh | 16 + .../targets/includes/tasks/trigger_change.yml | 2 + .../targets/includes/test_include_free.yml | 10 + .../targets/includes/test_include_host_pinned.yml | 9 + .../integration/targets/includes/test_includes.yml | 10 + .../targets/includes/test_includes2.yml | 22 + .../targets/includes/test_includes3.yml | 6 + .../targets/includes/test_includes4.yml | 2 + test/integration/targets/includes_race/aliases | 2 + test/integration/targets/includes_race/inventory | 30 + .../roles/random_sleep/tasks/main.yml | 8 + .../includes_race/roles/set_a_fact/tasks/fact1.yml | 4 + .../includes_race/roles/set_a_fact/tasks/fact2.yml | 4 + test/integration/targets/includes_race/runme.sh | 5 + .../targets/includes_race/test_includes_race.yml | 19 + test/integration/targets/infra/aliases | 4 + test/integration/targets/infra/inventory.local | 2 + test/integration/targets/infra/library/test.py | 24 + test/integration/targets/infra/runme.sh | 39 + test/integration/targets/infra/test_test_infra.yml | 25 + .../targets/interpreter_discovery_python/aliases | 3 + .../library/test_echo_module.py | 29 + .../interpreter_discovery_python/tasks/main.yml | 183 ++ .../aliases | 3 + .../delegate_facts.yml | 10 + .../inventory | 2 + .../runme.sh | 5 + .../targets/inventory-invalid-group/aliases | 2 + .../targets/inventory-invalid-group/inventory.ini | 5 + .../targets/inventory-invalid-group/runme.sh | 13 + .../targets/inventory-invalid-group/test.yml | 3 + .../inventory/1/2/3/extra_vars_relative.yml | 19 + .../targets/inventory/1/2/inventory.yml | 3 + test/integration/targets/inventory/1/vars.yml | 1 + test/integration/targets/inventory/aliases | 2 + .../targets/inventory/extra_vars_constructed.yml | 5 + .../targets/inventory/host_vars_constructed.yml | 6 + .../targets/inventory/inv_with_host_vars.yml | 5 + .../integration/targets/inventory/inv_with_int.yml | 6 + .../inventory_plugins/contructed_with_hostvars.py | 44 + test/integration/targets/inventory/playbook.yml | 4 + test/integration/targets/inventory/runme.sh | 114 + test/integration/targets/inventory/strategy.yml | 12 + test/integration/targets/inventory/test_empty.yml | 0 .../targets/inventory_advanced_host_list/aliases | 1 + .../targets/inventory_advanced_host_list/runme.sh | 36 + .../test_advanced_host_list.yml | 9 + test/integration/targets/inventory_cache/aliases | 2 + .../targets/inventory_cache/cache/.keep | 0 .../targets/inventory_cache/cache_host.yml | 4 + .../targets/inventory_cache/exercise_cache.yml | 4 + .../plugins/inventory/cache_host.py | 56 + .../plugins/inventory/exercise_cache.py | 344 +++ test/integration/targets/inventory_cache/runme.sh | 25 + .../targets/inventory_constructed/aliases | 1 + .../targets/inventory_constructed/constructed.yml | 19 + .../invs/1/group_vars/stuff.yml | 1 + .../invs/1/host_vars/testing.yml | 1 + .../targets/inventory_constructed/invs/1/one.yml | 5 + .../inventory_constructed/invs/2/constructed.yml | 7 + .../keyed_group_default_value.yml | 5 + .../keyed_group_list_default_value.yml | 5 + .../keyed_group_str_default_value.yml | 5 + .../keyed_group_trailing_separator.yml | 5 + .../no_leading_separator_constructed.yml | 20 + .../targets/inventory_constructed/runme.sh | 59 + .../inventory_constructed/static_inventory.yml | 8 + .../inventory_constructed/tag_inventory.yml | 12 + test/integration/targets/inventory_ini/aliases | 1 + .../targets/inventory_ini/inventory.ini | 5 + test/integration/targets/inventory_ini/runme.sh | 5 + .../targets/inventory_ini/test_ansible_become.yml | 11 + test/integration/targets/inventory_script/aliases | 1 + .../targets/inventory_script/inventory.json | 1045 +++++++ .../targets/inventory_script/inventory.sh | 7 + test/integration/targets/inventory_script/runme.sh | 5 + test/integration/targets/inventory_yaml/aliases | 1 + test/integration/targets/inventory_yaml/empty.json | 10 + test/integration/targets/inventory_yaml/runme.sh | 6 + .../targets/inventory_yaml/success.json | 61 + test/integration/targets/inventory_yaml/test.yml | 27 + .../targets/inventory_yaml/test_int_hostname.yml | 5 + test/integration/targets/iptables/aliases | 5 + .../targets/iptables/tasks/chain_management.yml | 71 + test/integration/targets/iptables/tasks/main.yml | 36 + test/integration/targets/iptables/vars/alpine.yml | 2 + test/integration/targets/iptables/vars/centos.yml | 2 + test/integration/targets/iptables/vars/default.yml | 2 + test/integration/targets/iptables/vars/fedora.yml | 2 + test/integration/targets/iptables/vars/redhat.yml | 2 + test/integration/targets/iptables/vars/suse.yml | 2 + .../targets/jinja2_native_types/aliases | 2 + .../jinja2_native_types/nested_undefined.yml | 23 + .../targets/jinja2_native_types/runme.sh | 11 + .../targets/jinja2_native_types/runtests.yml | 40 + .../targets/jinja2_native_types/test_bool.yml | 53 + .../targets/jinja2_native_types/test_casting.yml | 31 + .../jinja2_native_types/test_concatentation.yml | 88 + .../targets/jinja2_native_types/test_dunder.yml | 23 + .../targets/jinja2_native_types/test_hostvars.yml | 10 + .../targets/jinja2_native_types/test_none.yml | 11 + .../jinja2_native_types/test_preserving_quotes.yml | 14 + .../targets/jinja2_native_types/test_template.yml | 27 + .../jinja2_native_types/test_template_newlines.j2 | 4 + .../targets/jinja2_native_types/test_types.yml | 20 + .../targets/jinja2_native_types/test_vault.yml | 16 + .../targets/jinja2_native_types/test_vault_pass | 1 + test/integration/targets/jinja_plugins/aliases | 2 + .../bar/plugins/filter/bad_collection_filter.py | 11 + .../bar/plugins/filter/bad_collection_filter2.py | 10 + .../bar/plugins/filter/good_collection_filter.py | 13 + .../foo/bar/plugins/test/bad_collection_test.py | 11 + .../foo/bar/plugins/test/good_collection_test.py | 13 + .../jinja_plugins/filter_plugins/bad_filter.py | 11 + .../jinja_plugins/filter_plugins/good_filter.py | 13 + .../integration/targets/jinja_plugins/playbook.yml | 10 + .../targets/jinja_plugins/tasks/main.yml | 23 + .../targets/jinja_plugins/test_plugins/bad_test.py | 11 + .../jinja_plugins/test_plugins/good_test.py | 13 + test/integration/targets/json_cleanup/aliases | 2 + .../targets/json_cleanup/library/bad_json | 11 + .../json_cleanup/module_output_cleaning.yml | 26 + test/integration/targets/json_cleanup/runme.sh | 5 + .../targets/keyword_inheritance/aliases | 3 + .../roles/whoami/tasks/main.yml | 3 + .../targets/keyword_inheritance/runme.sh | 5 + .../targets/keyword_inheritance/test.yml | 8 + test/integration/targets/known_hosts/aliases | 1 + .../targets/known_hosts/defaults/main.yml | 6 + .../targets/known_hosts/files/existing_known_hosts | 5 + test/integration/targets/known_hosts/meta/main.yml | 3 + .../integration/targets/known_hosts/tasks/main.yml | 409 +++ test/integration/targets/limit_inventory/aliases | 2 + test/integration/targets/limit_inventory/hosts.yml | 5 + test/integration/targets/limit_inventory/runme.sh | 31 + test/integration/targets/lineinfile/aliases | 1 + .../targets/lineinfile/files/firstmatch.txt | 5 + .../integration/targets/lineinfile/files/test.conf | 5 + test/integration/targets/lineinfile/files/test.txt | 5 + .../targets/lineinfile/files/test_58923.txt | 4 + .../targets/lineinfile/files/testempty.txt | 0 .../targets/lineinfile/files/testmultiple.txt | 7 + .../targets/lineinfile/files/testnoeof.txt | 2 + .../targets/lineinfile/files/teststring.conf | 5 + .../targets/lineinfile/files/teststring.txt | 5 + .../targets/lineinfile/files/teststring_58923.txt | 4 + test/integration/targets/lineinfile/meta/main.yml | 21 + test/integration/targets/lineinfile/tasks/main.yml | 1399 +++++++++ .../targets/lineinfile/tasks/test_string01.yml | 142 + .../targets/lineinfile/tasks/test_string02.yml | 166 + test/integration/targets/lineinfile/vars/main.yml | 29 + test/integration/targets/lookup_config/aliases | 1 + .../targets/lookup_config/tasks/main.yml | 74 + test/integration/targets/lookup_csvfile/aliases | 1 + .../lookup_csvfile/files/cool list of things.csv | 3 + .../targets/lookup_csvfile/files/crlf.csv | 2 + .../targets/lookup_csvfile/files/people.csv | 6 + .../targets/lookup_csvfile/files/tabs.csv | 4 + .../targets/lookup_csvfile/files/x1a.csv | 3 + .../targets/lookup_csvfile/tasks/main.yml | 83 + test/integration/targets/lookup_dict/aliases | 1 + .../integration/targets/lookup_dict/tasks/main.yml | 56 + test/integration/targets/lookup_env/aliases | 1 + test/integration/targets/lookup_env/runme.sh | 12 + test/integration/targets/lookup_env/tasks/main.yml | 15 + test/integration/targets/lookup_file/aliases | 1 + .../integration/targets/lookup_file/tasks/main.yml | 13 + test/integration/targets/lookup_fileglob/aliases | 1 + .../find_levels/files/play_adj_subdir.txt | 1 + .../files/somepath/play_adj_subsubdir.txt | 1 + .../targets/lookup_fileglob/find_levels/play.yml | 13 + .../lookup_fileglob/find_levels/play_adj.txt | 1 + .../find_levels/roles/get_file/files/in_role.txt | 1 + .../get_file/files/otherpath/in_role_subdir.txt | 1 + .../find_levels/roles/get_file/tasks/main.yml | 10 + .../targets/lookup_fileglob/issue72873/test.yml | 31 + .../targets/lookup_fileglob/non_existent/play.yml | 6 + test/integration/targets/lookup_fileglob/runme.sh | 18 + .../integration/targets/lookup_first_found/aliases | 1 + .../targets/lookup_first_found/files/bar1 | 1 + .../targets/lookup_first_found/files/foo1 | 1 + .../lookup_first_found/files/vars file spaces.yml | 1 + .../targets/lookup_first_found/tasks/main.yml | 96 + .../targets/lookup_indexed_items/aliases | 1 + .../targets/lookup_indexed_items/tasks/main.yml | 32 + test/integration/targets/lookup_ini/aliases | 1 + test/integration/targets/lookup_ini/duplicate.ini | 3 + .../targets/lookup_ini/duplicate_case_check.ini | 3 + test/integration/targets/lookup_ini/inventory | 2 + .../targets/lookup_ini/lookup-8859-15.ini | 7 + test/integration/targets/lookup_ini/lookup.ini | 25 + .../targets/lookup_ini/lookup.properties | 6 + .../lookup_ini/lookup_case_check.properties | 2 + test/integration/targets/lookup_ini/mysql.ini | 8 + test/integration/targets/lookup_ini/runme.sh | 5 + .../targets/lookup_ini/test_allow_no_value.yml | 23 + .../targets/lookup_ini/test_case_sensitive.yml | 31 + .../integration/targets/lookup_ini/test_errors.yml | 62 + test/integration/targets/lookup_ini/test_ini.yml | 4 + .../targets/lookup_ini/test_lookup_properties.yml | 88 + .../targets/lookup_inventory_hostnames/aliases | 1 + .../targets/lookup_inventory_hostnames/inventory | 18 + .../targets/lookup_inventory_hostnames/main.yml | 23 + .../targets/lookup_inventory_hostnames/runme.sh | 5 + test/integration/targets/lookup_items/aliases | 1 + .../targets/lookup_items/tasks/main.yml | 20 + test/integration/targets/lookup_lines/aliases | 1 + .../targets/lookup_lines/tasks/main.yml | 13 + test/integration/targets/lookup_list/aliases | 1 + .../integration/targets/lookup_list/tasks/main.yml | 19 + test/integration/targets/lookup_nested/aliases | 1 + .../targets/lookup_nested/tasks/main.yml | 18 + test/integration/targets/lookup_password/aliases | 1 + test/integration/targets/lookup_password/runme.sh | 11 + test/integration/targets/lookup_password/runme.yml | 4 + .../targets/lookup_password/tasks/main.yml | 149 + test/integration/targets/lookup_pipe/aliases | 1 + .../integration/targets/lookup_pipe/tasks/main.yml | 9 + .../targets/lookup_random_choice/aliases | 1 + .../targets/lookup_random_choice/tasks/main.yml | 10 + test/integration/targets/lookup_sequence/aliases | 1 + .../targets/lookup_sequence/tasks/main.yml | 198 ++ .../integration/targets/lookup_subelements/aliases | 1 + .../targets/lookup_subelements/tasks/main.yml | 224 ++ .../targets/lookup_subelements/vars/main.yml | 43 + test/integration/targets/lookup_template/aliases | 1 + .../targets/lookup_template/tasks/main.yml | 34 + .../targets/lookup_template/templates/dict.j2 | 1 + .../targets/lookup_template/templates/hello.txt | 1 + .../lookup_template/templates/hello_comment.txt | 2 + .../lookup_template/templates/hello_string.txt | 1 + .../targets/lookup_template/templates/world.txt | 1 + test/integration/targets/lookup_together/aliases | 1 + .../targets/lookup_together/tasks/main.yml | 29 + test/integration/targets/lookup_unvault/aliases | 2 + .../targets/lookup_unvault/files/foot.txt | 1 + .../targets/lookup_unvault/files/foot.txt.vault | 6 + test/integration/targets/lookup_unvault/runme.sh | 6 + test/integration/targets/lookup_unvault/secret | 1 + .../integration/targets/lookup_unvault/unvault.yml | 9 + test/integration/targets/lookup_url/aliases | 4 + test/integration/targets/lookup_url/meta/main.yml | 2 + test/integration/targets/lookup_url/tasks/main.yml | 54 + .../targets/lookup_url/tasks/use_netrc.yml | 37 + test/integration/targets/lookup_varnames/aliases | 1 + .../targets/lookup_varnames/tasks/main.yml | 38 + test/integration/targets/lookup_vars/aliases | 1 + .../integration/targets/lookup_vars/tasks/main.yml | 56 + test/integration/targets/loop-connection/aliases | 2 + .../ansible_collections/ns/name/meta/runtime.yml | 4 + .../ns/name/plugins/connection/dummy.py | 50 + test/integration/targets/loop-connection/main.yml | 33 + test/integration/targets/loop-connection/runme.sh | 5 + test/integration/targets/loop-until/aliases | 2 + test/integration/targets/loop-until/tasks/main.yml | 160 + test/integration/targets/loop_control/aliases | 2 + test/integration/targets/loop_control/extended.yml | 23 + test/integration/targets/loop_control/inner.yml | 9 + test/integration/targets/loop_control/label.yml | 23 + test/integration/targets/loop_control/runme.sh | 12 + test/integration/targets/loops/aliases | 2 + test/integration/targets/loops/files/data1.txt | 1 + test/integration/targets/loops/files/data2.txt | 1 + .../targets/loops/tasks/index_var_tasks.yml | 3 + test/integration/targets/loops/tasks/main.yml | 407 +++ .../loops/tasks/templated_loop_var_tasks.yml | 4 + test/integration/targets/loops/vars/64169.yml | 2 + test/integration/targets/loops/vars/main.yml | 8 + test/integration/targets/meta_tasks/aliases | 2 + test/integration/targets/meta_tasks/inventory.yml | 9 + .../targets/meta_tasks/inventory_new.yml | 8 + .../targets/meta_tasks/inventory_old.yml | 8 + .../targets/meta_tasks/inventory_refresh.yml | 8 + test/integration/targets/meta_tasks/refresh.yml | 38 + .../meta_tasks/refresh_preserve_dynamic.yml | 69 + test/integration/targets/meta_tasks/runme.sh | 78 + .../targets/meta_tasks/test_end_batch.yml | 13 + .../targets/meta_tasks/test_end_host.yml | 14 + .../targets/meta_tasks/test_end_host_all.yml | 13 + .../targets/meta_tasks/test_end_host_all_fqcn.yml | 13 + .../targets/meta_tasks/test_end_host_fqcn.yml | 14 + .../targets/meta_tasks/test_end_play.yml | 12 + .../targets/meta_tasks/test_end_play_fqcn.yml | 12 + .../meta_tasks/test_end_play_multiple_plays.yml | 18 + .../meta_tasks/test_end_play_serial_one.yml | 13 + .../targets/missing_required_lib/aliases | 2 + .../library/missing_required_lib.py | 37 + .../targets/missing_required_lib/runme.sh | 5 + .../targets/missing_required_lib/runme.yml | 57 + .../targets/missing_required_lib/tasks/main.yml | 3 + .../module_defaults/action_plugins/debug.py | 80 + test/integration/targets/module_defaults/aliases | 2 + .../othercoll/plugins/action/other_echoaction.py | 8 + .../othercoll/plugins/modules/other_echo1.py | 13 + .../testns/testcoll/meta/runtime.yml | 85 + .../testns/testcoll/plugins/action/echoaction.py | 19 + .../testns/testcoll/plugins/action/eos.py | 18 + .../testns/testcoll/plugins/action/ios.py | 18 + .../testns/testcoll/plugins/action/vyos.py | 18 + .../testcoll/plugins/module_utils/echo_impl.py | 15 + .../testns/testcoll/plugins/modules/echo1.py | 13 + .../testns/testcoll/plugins/modules/echo2.py | 13 + .../testns/testcoll/plugins/modules/eosfacts.py | 35 + .../testns/testcoll/plugins/modules/ios_facts.py | 35 + .../testns/testcoll/plugins/modules/metadata.py | 45 + .../testns/testcoll/plugins/modules/module.py | 35 + .../testns/testcoll/plugins/modules/ping.py | 83 + .../testns/testcoll/plugins/modules/vyosfacts.py | 35 + .../targets/module_defaults/library/legacy_ping.py | 83 + .../library/test_module_defaults.py | 30 + test/integration/targets/module_defaults/runme.sh | 14 + .../targets/module_defaults/tasks/main.yml | 89 + .../templates/test_metadata_warning.yml.j2 | 8 + .../module_defaults/test_action_group_metadata.yml | 123 + .../targets/module_defaults/test_action_groups.yml | 132 + .../targets/module_defaults/test_defaults.yml | 249 ++ test/integration/targets/module_no_log/aliases | 5 + .../module_no_log/library/module_that_logs.py | 18 + .../targets/module_no_log/tasks/main.yml | 61 + test/integration/targets/module_precedence/aliases | 2 + .../module_precedence/lib_no_extension/ping | 71 + .../module_precedence/lib_with_extension/a.ini | 13 + .../module_precedence/lib_with_extension/a.py | 13 + .../module_precedence/lib_with_extension/ping.ini | 13 + .../module_precedence/lib_with_extension/ping.py | 71 + .../targets/module_precedence/modules_test.yml | 10 + .../module_precedence/modules_test_envvar.yml | 11 + .../module_precedence/modules_test_envvar_ext.yml | 16 + .../modules_test_multiple_roles.yml | 17 + .../modules_test_multiple_roles_reverse_order.yml | 16 + .../module_precedence/modules_test_role.yml | 13 + .../module_precedence/modules_test_role_ext.yml | 18 + .../multiple_roles/bar/library/ping.py | 71 + .../multiple_roles/bar/tasks/main.yml | 10 + .../multiple_roles/foo/library/ping.py | 71 + .../multiple_roles/foo/tasks/main.yml | 10 + .../roles_no_extension/foo/library/ping | 71 + .../roles_no_extension/foo/tasks/main.yml | 10 + .../roles_with_extension/foo/library/a.ini | 13 + .../roles_with_extension/foo/library/a.py | 13 + .../roles_with_extension/foo/library/ping.ini | 13 + .../roles_with_extension/foo/library/ping.py | 71 + .../roles_with_extension/foo/tasks/main.yml | 10 + .../integration/targets/module_precedence/runme.sh | 49 + test/integration/targets/module_tracebacks/aliases | 3 + .../targets/module_tracebacks/inventory | 5 + .../integration/targets/module_tracebacks/runme.sh | 5 + .../targets/module_tracebacks/traceback.yml | 21 + test/integration/targets/module_utils/aliases | 6 + .../targets/module_utils/callback/pure_json.py | 31 + .../testns/testcoll/plugins/module_utils/legit.py | 6 + .../targets/module_utils/library/test.py | 87 + .../module_utils/library/test_alias_deprecation.py | 16 + .../module_utils/library/test_cwd_missing.py | 33 + .../module_utils/library/test_cwd_unreadable.py | 28 + .../targets/module_utils/library/test_datetime.py | 19 + .../module_utils/library/test_env_override.py | 14 + .../targets/module_utils/library/test_failure.py | 14 + .../targets/module_utils/library/test_network.py | 28 + .../targets/module_utils/library/test_no_log.py | 35 + .../targets/module_utils/library/test_optional.py | 84 + .../targets/module_utils/library/test_override.py | 11 + .../module_utils/library/test_recursive_diff.py | 29 + .../targets/module_utils/module_utils/__init__.py | 0 .../module_utils/module_utils/a/__init__.py | 0 .../module_utils/module_utils/a/b/__init__.py | 0 .../module_utils/module_utils/a/b/c/__init__.py | 0 .../module_utils/module_utils/a/b/c/d/__init__.py | 0 .../module_utils/a/b/c/d/e/__init__.py | 0 .../module_utils/a/b/c/d/e/f/__init__.py | 0 .../module_utils/a/b/c/d/e/f/g/__init__.py | 0 .../module_utils/a/b/c/d/e/f/g/h/__init__.py | 1 + .../module_utils/module_utils/ansible_release.py | 4 + .../module_utils/module_utils/bar0/__init__.py | 0 .../targets/module_utils/module_utils/bar0/foo.py | 1 + .../module_utils/module_utils/bar1/__init__.py | 1 + .../module_utils/module_utils/bar2/__init__.py | 1 + .../module_utils/module_utils/baz1/__init__.py | 0 .../targets/module_utils/module_utils/baz1/one.py | 1 + .../module_utils/module_utils/baz2/__init__.py | 0 .../targets/module_utils/module_utils/baz2/one.py | 1 + .../targets/module_utils/module_utils/foo.py | 3 + .../targets/module_utils/module_utils/foo0.py | 1 + .../targets/module_utils/module_utils/foo1.py | 1 + .../targets/module_utils/module_utils/foo2.py | 1 + .../module_utils/module_utils/qux1/__init__.py | 0 .../targets/module_utils/module_utils/qux1/quux.py | 1 + .../module_utils/module_utils/qux2/__init__.py | 0 .../targets/module_utils/module_utils/qux2/quux.py | 1 + .../targets/module_utils/module_utils/qux2/quuz.py | 1 + .../targets/module_utils/module_utils/service.py | 1 + .../module_utils/module_utils/spam1/__init__.py | 0 .../module_utils/spam1/ham/__init__.py | 0 .../module_utils/spam1/ham/eggs/__init__.py | 1 + .../module_utils/module_utils/spam2/__init__.py | 0 .../module_utils/spam2/ham/__init__.py | 0 .../module_utils/spam2/ham/eggs/__init__.py | 1 + .../module_utils/module_utils/spam3/__init__.py | 0 .../module_utils/spam3/ham/__init__.py | 0 .../module_utils/module_utils/spam3/ham/bacon.py | 1 + .../module_utils/module_utils/spam4/__init__.py | 0 .../module_utils/spam4/ham/__init__.py | 0 .../module_utils/module_utils/spam4/ham/bacon.py | 1 + .../module_utils/module_utils/spam5/__init__.py | 0 .../module_utils/spam5/ham/__init__.py | 0 .../module_utils/module_utils/spam5/ham/bacon.py | 1 + .../module_utils/module_utils/spam5/ham/eggs.py | 1 + .../module_utils/module_utils/spam6/__init__.py | 0 .../module_utils/spam6/ham/__init__.py | 2 + .../module_utils/module_utils/spam7/__init__.py | 0 .../module_utils/spam7/ham/__init__.py | 1 + .../module_utils/module_utils/spam7/ham/bacon.py | 1 + .../module_utils/module_utils/spam8/__init__.py | 0 .../module_utils/spam8/ham/__init__.py | 1 + .../module_utils/module_utils/spam8/ham/bacon.py | 1 + .../module_utils/module_utils/sub/__init__.py | 0 .../targets/module_utils/module_utils/sub/bam.py | 3 + .../module_utils/module_utils/sub/bam/__init__.py | 0 .../module_utils/module_utils/sub/bam/bam.py | 3 + .../module_utils/module_utils/sub/bar/__init__.py | 0 .../module_utils/module_utils/sub/bar/bam.py | 3 + .../module_utils/module_utils/sub/bar/bar.py | 3 + .../module_utils/module_utils/yak/__init__.py | 0 .../module_utils/yak/zebra/__init__.py | 0 .../module_utils/module_utils/yak/zebra/foo.py | 1 + .../module_utils/module_utils_basic_setcwd.yml | 53 + .../module_utils_common_dict_transformation.yml | 34 + .../module_utils/module_utils_common_network.yml | 10 + .../targets/module_utils/module_utils_envvar.yml | 51 + .../targets/module_utils/module_utils_test.yml | 121 + .../module_utils/module_utils_test_no_log.yml | 12 + .../targets/module_utils/module_utils_vvvvv.yml | 29 + .../targets/module_utils/other_mu_dir/__init__.py | 0 .../module_utils/other_mu_dir/a/__init__.py | 0 .../module_utils/other_mu_dir/a/b/__init__.py | 0 .../module_utils/other_mu_dir/a/b/c/__init__.py | 0 .../module_utils/other_mu_dir/a/b/c/d/__init__.py | 0 .../other_mu_dir/a/b/c/d/e/__init__.py | 0 .../other_mu_dir/a/b/c/d/e/f/__init__.py | 0 .../other_mu_dir/a/b/c/d/e/f/g/__init__.py | 0 .../other_mu_dir/a/b/c/d/e/f/g/h/__init__.py | 1 + .../targets/module_utils/other_mu_dir/facts.py | 1 + .../module_utils/other_mu_dir/json_utils.py | 1 + .../targets/module_utils/other_mu_dir/mork.py | 1 + test/integration/targets/module_utils/runme.sh | 19 + .../module_utils_Ansible.AccessToken/aliases | 3 + .../library/ansible_access_token_tests.ps1 | 407 +++ .../tasks/main.yml | 29 + .../targets/module_utils_Ansible.Basic/aliases | 3 + .../library/ansible_basic_tests.ps1 | 3206 ++++++++++++++++++++ .../module_utils_Ansible.Basic/tasks/main.yml | 9 + .../targets/module_utils_Ansible.Become/aliases | 3 + .../library/ansible_become_tests.ps1 | 1022 +++++++ .../module_utils_Ansible.Become/tasks/main.yml | 28 + .../aliases | 3 + .../library/add_type_test.ps1 | 332 ++ .../tasks/main.yml | 10 + .../aliases | 3 + .../library/argv_parser_test.ps1 | 93 + .../meta/main.yml | 3 + .../tasks/main.yml | 9 + .../aliases | 3 + .../library/backup_file_test.ps1 | 92 + .../tasks/main.yml | 10 + .../aliases | 3 + .../library/camel_conversion_test.ps1 | 81 + .../tasks/main.yml | 8 + .../aliases | 3 + .../library/command_util_test.ps1 | 139 + .../meta/main.yml | 3 + .../tasks/main.yml | 9 + .../aliases | 3 + .../library/file_util_test.ps1 | 114 + .../tasks/main.yml | 8 + .../aliases | 3 + .../library/testlist.ps1 | 12 + .../library/testpath.ps1 | 9 + .../tasks/main.yml | 41 + .../aliases | 3 + .../library/symbolic_link_test.ps1 | 174 ++ .../tasks/main.yml | 8 + .../aliases | 3 + .../library/privilege_util_test.ps1 | 113 + .../tasks/main.yml | 8 + .../module_utils_Ansible.ModuleUtils.SID/aliases | 3 + .../library/sid_utils_test.ps1 | 101 + .../tasks/main.yml | 22 + .../aliases | 4 + .../library/web_request_test.ps1 | 473 +++ .../meta/main.yml | 3 + .../tasks/main.yml | 10 + .../targets/module_utils_Ansible.Privilege/aliases | 3 + .../library/ansible_privilege_tests.ps1 | 278 ++ .../module_utils_Ansible.Privilege/tasks/main.yml | 9 + .../targets/module_utils_Ansible.Process/aliases | 3 + .../library/ansible_process_tests.ps1 | 242 ++ .../module_utils_Ansible.Process/tasks/main.yml | 9 + .../targets/module_utils_Ansible.Service/aliases | 3 + .../library/ansible_service_tests.ps1 | 953 ++++++ .../module_utils_Ansible.Service/tasks/main.yml | 9 + .../targets/module_utils_ansible_release/aliases | 2 + .../library/ansible_release.py | 40 + .../module_utils_ansible_release/tasks/main.yml | 9 + .../targets/module_utils_common.respawn/aliases | 1 + .../library/respawnme.py | 44 + .../module_utils_common.respawn/tasks/main.yml | 24 + .../targets/module_utils_distro/aliases | 2 + .../targets/module_utils_distro/meta/main.yml | 2 + .../targets/module_utils_distro/runme.sh | 24 + .../module_utils_facts.system.selinux/aliases | 1 + .../tasks/main.yml | 38 + .../tasks/selinux.yml | 93 + test/integration/targets/module_utils_urls/aliases | 2 + .../module_utils_urls/library/test_peercert.py | 98 + .../targets/module_utils_urls/meta/main.yml | 3 + .../targets/module_utils_urls/tasks/main.yml | 32 + test/integration/targets/network_cli/aliases | 3 + .../targets/network_cli/passworded_user.yml | 14 + test/integration/targets/network_cli/runme.sh | 27 + test/integration/targets/network_cli/setup.yml | 14 + test/integration/targets/network_cli/teardown.yml | 14 + test/integration/targets/no_log/aliases | 2 + test/integration/targets/no_log/dynamic.yml | 27 + test/integration/targets/no_log/library/module.py | 45 + test/integration/targets/no_log/no_log_local.yml | 92 + .../targets/no_log/no_log_suboptions.yml | 24 + .../targets/no_log/no_log_suboptions_invalid.yml | 45 + test/integration/targets/no_log/runme.sh | 21 + test/integration/targets/noexec/aliases | 4 + test/integration/targets/noexec/inventory | 1 + test/integration/targets/noexec/runme.sh | 9 + test/integration/targets/noexec/test-noexec.yml | 8 + .../targets/old_style_cache_plugins/aliases | 6 + .../targets/old_style_cache_plugins/cleanup.yml | 41 + .../old_style_cache_plugins/inspect_cache.yml | 36 + .../old_style_cache_plugins/inventory_config | 1 + .../plugins/cache/configurable_redis.py | 147 + .../plugins/cache/legacy_redis.py | 141 + .../plugins/inventory/test.py | 59 + .../targets/old_style_cache_plugins/runme.sh | 47 + .../old_style_cache_plugins/setup_redis_cache.yml | 51 + .../test_fact_gathering.yml | 22 + .../test_inventory_cache.yml | 45 + .../targets/old_style_modules_posix/aliases | 2 + .../old_style_modules_posix/library/helloworld.sh | 29 + .../targets/old_style_modules_posix/meta/main.yml | 2 + .../targets/old_style_modules_posix/tasks/main.yml | 44 + .../targets/old_style_vars_plugins/aliases | 2 + .../deprecation_warning/vars.py | 8 + .../targets/old_style_vars_plugins/runme.sh | 20 + .../vars_plugins/auto_enabled.py | 8 + .../vars_plugins/implicitly_auto_enabled.py | 7 + .../vars_plugins/require_enabled.py | 8 + test/integration/targets/omit/48673.yml | 4 + test/integration/targets/omit/75692.yml | 31 + test/integration/targets/omit/C75692.yml | 44 + test/integration/targets/omit/aliases | 3 + test/integration/targets/omit/runme.sh | 11 + test/integration/targets/order/aliases | 2 + test/integration/targets/order/inventory | 9 + test/integration/targets/order/order.yml | 39 + test/integration/targets/order/runme.sh | 24 + test/integration/targets/package/aliases | 2 + test/integration/targets/package/meta/main.yml | 2 + test/integration/targets/package/tasks/main.yml | 242 ++ test/integration/targets/package_facts/aliases | 3 + .../targets/package_facts/tasks/main.yml | 115 + test/integration/targets/parsing/aliases | 2 + test/integration/targets/parsing/bad_parsing.yml | 12 + test/integration/targets/parsing/good_parsing.yml | 9 + .../parsing/roles/test_bad_parsing/tasks/main.yml | 60 + .../roles/test_bad_parsing/tasks/scenario1.yml | 4 + .../roles/test_bad_parsing/tasks/scenario2.yml | 4 + .../roles/test_bad_parsing/tasks/scenario3.yml | 4 + .../roles/test_bad_parsing/tasks/scenario4.yml | 4 + .../parsing/roles/test_bad_parsing/vars/main.yml | 2 + .../parsing/roles/test_good_parsing/tasks/main.yml | 217 ++ .../roles/test_good_parsing/tasks/test_include.yml | 1 + .../tasks/test_include_conditional.yml | 1 + .../tasks/test_include_nested.yml | 2 + .../parsing/roles/test_good_parsing/vars/main.yml | 2 + test/integration/targets/parsing/runme.sh | 6 + test/integration/targets/path_lookups/aliases | 2 + test/integration/targets/path_lookups/play.yml | 49 + .../path_lookups/roles/showfile/tasks/main.yml | 2 + test/integration/targets/path_lookups/runme.sh | 5 + test/integration/targets/path_lookups/testplay.yml | 20 + .../targets/path_with_comma_in_inventory/aliases | 2 + .../path_with_comma_in_inventory/playbook.yml | 9 + .../targets/path_with_comma_in_inventory/runme.sh | 5 + .../this,path,has,commas/group_vars/all.yml | 1 + .../this,path,has,commas/hosts | 1 + test/integration/targets/pause/aliases | 3 + test/integration/targets/pause/pause-1.yml | 11 + test/integration/targets/pause/pause-2.yml | 12 + test/integration/targets/pause/pause-3.yml | 12 + test/integration/targets/pause/pause-4.yml | 13 + test/integration/targets/pause/pause-5.yml | 35 + test/integration/targets/pause/runme.sh | 43 + test/integration/targets/pause/setup.yml | 4 + .../targets/pause/test-pause-background.yml | 10 + .../targets/pause/test-pause-no-tty.yml | 7 + test/integration/targets/pause/test-pause.py | 292 ++ test/integration/targets/pause/test-pause.yml | 72 + test/integration/targets/ping/aliases | 1 + test/integration/targets/ping/tasks/main.yml | 53 + test/integration/targets/pip/aliases | 2 + .../pip/files/ansible_test_pip_chdir/__init__.py | 6 + test/integration/targets/pip/files/setup.py | 17 + test/integration/targets/pip/meta/main.yml | 4 + .../targets/pip/tasks/default_cleanup.yml | 5 + .../targets/pip/tasks/freebsd_cleanup.yml | 6 + test/integration/targets/pip/tasks/main.yml | 54 + test/integration/targets/pip/tasks/pip.yml | 601 ++++ test/integration/targets/pip/vars/main.yml | 13 + test/integration/targets/pkg_resources/aliases | 2 + .../lookup_plugins/check_pkg_resources.py | 23 + .../targets/pkg_resources/tasks/main.yml | 3 + test/integration/targets/play_iterator/aliases | 2 + .../integration/targets/play_iterator/playbook.yml | 10 + test/integration/targets/play_iterator/runme.sh | 5 + test/integration/targets/playbook/aliases | 2 + test/integration/targets/playbook/empty.yml | 1 + test/integration/targets/playbook/empty_hosts.yml | 4 + .../targets/playbook/malformed_post_tasks.yml | 2 + .../targets/playbook/malformed_pre_tasks.yml | 2 + .../targets/playbook/malformed_roles.yml | 2 + .../targets/playbook/malformed_tasks.yml | 2 + .../targets/playbook/malformed_vars_prompt.yml | 3 + .../targets/playbook/old_style_role.yml | 3 + .../targets/playbook/remote_user_and_user.yml | 6 + test/integration/targets/playbook/roles_null.yml | 3 + test/integration/targets/playbook/runme.sh | 92 + test/integration/targets/playbook/some_vars.yml | 2 + test/integration/targets/playbook/timeout.yml | 12 + test/integration/targets/playbook/types.yml | 21 + test/integration/targets/playbook/user.yml | 23 + .../targets/playbook/vars_files_null.yml | 3 + .../targets/playbook/vars_files_string.yml | 6 + .../targets/playbook/vars_prompt_null.yml | 3 + .../targets/plugin_config_for_inventory/aliases | 2 + .../cache_plugins/none.py | 62 + .../config_with_parameter.yml | 5 + .../config_without_parameter.yml | 1 + .../targets/plugin_config_for_inventory/runme.sh | 22 + .../plugin_config_for_inventory/test_inventory.py | 84 + test/integration/targets/plugin_filtering/aliases | 2 + test/integration/targets/plugin_filtering/copy.yml | 10 + .../targets/plugin_filtering/filter_lookup.ini | 4 + .../targets/plugin_filtering/filter_lookup.yml | 6 + .../targets/plugin_filtering/filter_modules.ini | 4 + .../targets/plugin_filtering/filter_modules.yml | 9 + .../targets/plugin_filtering/filter_ping.ini | 4 + .../targets/plugin_filtering/filter_ping.yml | 5 + .../targets/plugin_filtering/filter_stat.ini | 4 + .../targets/plugin_filtering/filter_stat.yml | 5 + .../targets/plugin_filtering/lookup.yml | 14 + .../plugin_filtering/no_blacklist_module.ini | 3 + .../plugin_filtering/no_blacklist_module.yml | 3 + .../targets/plugin_filtering/no_filters.ini | 4 + .../integration/targets/plugin_filtering/pause.yml | 6 + test/integration/targets/plugin_filtering/ping.yml | 6 + test/integration/targets/plugin_filtering/runme.sh | 137 + test/integration/targets/plugin_filtering/stat.yml | 6 + .../targets/plugin_filtering/tempfile.yml | 9 + test/integration/targets/plugin_loader/aliases | 2 + .../normal/action_plugins/self_referential.py | 29 + .../targets/plugin_loader/normal/filters.yml | 13 + .../plugin_loader/normal/library/_underscore.py | 13 + .../plugin_loader/normal/self_referential.yml | 5 + .../targets/plugin_loader/normal/underscore.yml | 15 + .../plugin_loader/override/filter_plugins/core.py | 18 + .../targets/plugin_loader/override/filters.yml | 15 + test/integration/targets/plugin_loader/runme.sh | 36 + .../targets/plugin_loader/use_coll_name.yml | 7 + test/integration/targets/plugin_namespace/aliases | 2 + .../plugin_namespace/filter_plugins/test_filter.py | 15 + .../plugin_namespace/lookup_plugins/lookup_name.py | 9 + .../targets/plugin_namespace/tasks/main.yml | 11 + .../plugin_namespace/test_plugins/test_test.py | 16 + .../integration/targets/preflight_encoding/aliases | 2 + .../targets/preflight_encoding/tasks/main.yml | 62 + .../targets/preflight_encoding/vars/main.yml | 2 + .../targets/prepare_http_tests/defaults/main.yml | 5 + .../targets/prepare_http_tests/handlers/main.yml | 4 + .../prepare_http_tests/library/httptester_kinit.py | 138 + .../targets/prepare_http_tests/meta/main.yml | 3 + .../targets/prepare_http_tests/tasks/default.yml | 55 + .../targets/prepare_http_tests/tasks/kerberos.yml | 65 + .../targets/prepare_http_tests/tasks/main.yml | 35 + .../targets/prepare_http_tests/tasks/windows.yml | 33 + .../prepare_http_tests/templates/krb5.conf.j2 | 25 + .../targets/prepare_http_tests/vars/Alpine.yml | 3 + .../targets/prepare_http_tests/vars/Debian.yml | 3 + .../targets/prepare_http_tests/vars/FreeBSD.yml | 2 + .../targets/prepare_http_tests/vars/RedHat-9.yml | 4 + .../targets/prepare_http_tests/vars/Suse.yml | 3 + .../targets/prepare_http_tests/vars/default.yml | 3 + .../targets/prepare_http_tests/vars/httptester.yml | 6 + .../targets/prepare_tests/tasks/main.yml | 0 test/integration/targets/pyyaml/aliases | 2 + test/integration/targets/pyyaml/runme.sh | 11 + test/integration/targets/raw/aliases | 2 + test/integration/targets/raw/meta/main.yml | 3 + test/integration/targets/raw/runme.sh | 6 + test/integration/targets/raw/runme.yml | 4 + test/integration/targets/raw/tasks/main.yml | 107 + test/integration/targets/reboot/aliases | 5 + test/integration/targets/reboot/handlers/main.yml | 4 + .../targets/reboot/tasks/check_reboot.yml | 10 + .../targets/reboot/tasks/get_boot_time.yml | 3 + test/integration/targets/reboot/tasks/main.yml | 43 + .../reboot/tasks/test_invalid_parameter.yml | 11 + .../reboot/tasks/test_invalid_test_command.yml | 8 + .../targets/reboot/tasks/test_molly_guard.yml | 20 + .../targets/reboot/tasks/test_reboot_command.yml | 22 + .../reboot/tasks/test_standard_scenarios.yml | 32 + test/integration/targets/reboot/vars/main.yml | 9 + test/integration/targets/register/aliases | 2 + test/integration/targets/register/can_register.yml | 21 + test/integration/targets/register/invalid.yml | 11 + .../targets/register/invalid_skipped.yml | 12 + test/integration/targets/register/runme.sh | 12 + .../integration/targets/rel_plugin_loading/aliases | 2 + .../targets/rel_plugin_loading/notyaml.yml | 5 + .../targets/rel_plugin_loading/runme.sh | 5 + .../subdir/inventory_plugins/notyaml.py | 169 ++ .../targets/rel_plugin_loading/subdir/play.yml | 6 + test/integration/targets/remote_tmp/aliases | 3 + test/integration/targets/remote_tmp/playbook.yml | 59 + test/integration/targets/remote_tmp/runme.sh | 5 + test/integration/targets/replace/aliases | 1 + test/integration/targets/replace/meta/main.yml | 3 + test/integration/targets/replace/tasks/main.yml | 265 ++ .../targets/retry_task_name_in_callback/aliases | 2 + .../targets/retry_task_name_in_callback/runme.sh | 13 + .../targets/retry_task_name_in_callback/test.yml | 28 + test/integration/targets/roles/aliases | 2 + test/integration/targets/roles/allowed_dupes.yml | 18 + test/integration/targets/roles/data_integrity.yml | 4 + test/integration/targets/roles/no_dupes.yml | 29 + test/integration/targets/roles/no_outside.yml | 7 + .../targets/roles/roles/a/tasks/main.yml | 1 + .../targets/roles/roles/b/meta/main.yml | 4 + .../targets/roles/roles/b/tasks/main.yml | 1 + .../targets/roles/roles/c/meta/main.yml | 2 + .../targets/roles/roles/c/tasks/main.yml | 1 + .../targets/roles/roles/data/defaults/main/00.yml | 1 + .../targets/roles/roles/data/defaults/main/01.yml | 0 .../targets/roles/roles/data/tasks/main.yml | 5 + test/integration/targets/roles/runme.sh | 28 + test/integration/targets/roles/tasks/dummy.yml | 1 + test/integration/targets/roles_arg_spec/aliases | 2 + .../ansible_collections/foo/bar/MANIFEST.json | 0 .../foo/bar/roles/blah/meta/argument_specs.yml | 8 + .../foo/bar/roles/blah/tasks/main.yml | 3 + .../roles_arg_spec/roles/a/meta/argument_specs.yml | 17 + .../targets/roles_arg_spec/roles/a/meta/main.yml | 13 + .../roles_arg_spec/roles/a/tasks/alternate.yml | 3 + .../targets/roles_arg_spec/roles/a/tasks/main.yml | 3 + .../roles/a/tasks/no_spec_entrypoint.yml | 3 + .../roles/b/meta/argument_specs.yaml | 13 + .../targets/roles_arg_spec/roles/b/tasks/main.yml | 9 + .../targets/roles_arg_spec/roles/c/meta/main.yml | 7 + .../targets/roles_arg_spec/roles/c/tasks/main.yml | 12 + .../roles/empty_argspec/meta/argument_specs.yml | 2 + .../roles/empty_argspec/tasks/main.yml | 3 + .../roles/empty_file/meta/argument_specs.yml | 1 + .../roles_arg_spec/roles/empty_file/tasks/main.yml | 3 + .../role_with_no_tasks/meta/argument_specs.yml | 7 + .../roles_arg_spec/roles/test1/defaults/main.yml | 3 + .../roles/test1/meta/argument_specs.yml | 112 + .../roles_arg_spec/roles/test1/tasks/main.yml | 11 + .../roles_arg_spec/roles/test1/tasks/other.yml | 11 + .../roles/test1/tasks/test1_other.yml | 11 + .../roles_arg_spec/roles/test1/vars/main.yml | 4 + .../roles_arg_spec/roles/test1/vars/other.yml | 4 + test/integration/targets/roles_arg_spec/runme.sh | 32 + test/integration/targets/roles_arg_spec/test.yml | 356 +++ .../roles_arg_spec/test_complex_role_fails.yml | 197 ++ .../roles_arg_spec/test_play_level_role_fails.yml | 5 + .../targets/roles_arg_spec/test_tags.yml | 11 + .../targets/roles_var_inheritance/aliases | 2 + .../targets/roles_var_inheritance/play.yml | 4 + .../roles_var_inheritance/roles/A/meta/main.yml | 4 + .../roles_var_inheritance/roles/B/meta/main.yml | 4 + .../roles/child_nested_dep/vars/main.yml | 1 + .../roles/common_dep/meta/main.yml | 4 + .../roles/common_dep/vars/main.yml | 1 + .../roles/nested_dep/meta/main.yml | 3 + .../roles/nested_dep/tasks/main.yml | 5 + .../targets/roles_var_inheritance/runme.sh | 9 + test/integration/targets/rpm_key/aliases | 2 + .../integration/targets/rpm_key/defaults/main.yaml | 0 test/integration/targets/rpm_key/meta/main.yml | 2 + test/integration/targets/rpm_key/tasks/main.yaml | 2 + .../integration/targets/rpm_key/tasks/rpm_key.yaml | 180 ++ test/integration/targets/run_modules/aliases | 2 + test/integration/targets/run_modules/args.json | 1 + .../targets/run_modules/library/test.py | 10 + test/integration/targets/run_modules/runme.sh | 6 + test/integration/targets/script/aliases | 1 + .../targets/script/files/create_afile.sh | 3 + .../integration/targets/script/files/no_shebang.py | 6 + .../targets/script/files/remove_afile.sh | 3 + .../targets/script/files/space path/test.sh | 3 + test/integration/targets/script/files/test.sh | 3 + .../targets/script/files/test_with_args.sh | 5 + test/integration/targets/script/meta/main.yml | 3 + test/integration/targets/script/tasks/main.yml | 241 ++ test/integration/targets/service/aliases | 4 + .../targets/service/files/ansible-broken.upstart | 10 + test/integration/targets/service/files/ansible.rc | 16 + .../targets/service/files/ansible.systemd | 11 + .../integration/targets/service/files/ansible.sysv | 134 + .../targets/service/files/ansible.upstart | 9 + .../targets/service/files/ansible_test_service.py | 74 + test/integration/targets/service/meta/main.yml | 20 + test/integration/targets/service/tasks/main.yml | 62 + .../targets/service/tasks/rc_cleanup.yml | 9 + .../integration/targets/service/tasks/rc_setup.yml | 21 + .../targets/service/tasks/systemd_cleanup.yml | 25 + .../targets/service/tasks/systemd_setup.yml | 17 + .../targets/service/tasks/sysv_cleanup.yml | 9 + .../targets/service/tasks/sysv_setup.yml | 11 + test/integration/targets/service/tasks/tests.yml | 258 ++ .../targets/service/tasks/upstart_cleanup.yml | 17 + .../targets/service/tasks/upstart_setup.yml | 19 + .../integration/targets/service/templates/main.yml | 0 test/integration/targets/service_facts/aliases | 4 + .../targets/service_facts/files/ansible.systemd | 11 + .../service_facts/files/ansible_test_service.py | 73 + .../targets/service_facts/tasks/main.yml | 29 + .../service_facts/tasks/systemd_cleanup.yml | 32 + .../targets/service_facts/tasks/systemd_setup.yml | 26 + .../targets/service_facts/tasks/tests.yml | 36 + test/integration/targets/set_fact/aliases | 2 + test/integration/targets/set_fact/incremental.yml | 35 + test/integration/targets/set_fact/inventory | 3 + .../targets/set_fact/nowarn_clean_facts.yml | 10 + test/integration/targets/set_fact/runme.sh | 36 + .../targets/set_fact/set_fact_auto_unsafe.yml | 10 + .../targets/set_fact/set_fact_bool_conv.yml | 35 + .../set_fact/set_fact_bool_conv_jinja2_native.yml | 35 + .../targets/set_fact/set_fact_cached_1.yml | 324 ++ .../targets/set_fact/set_fact_cached_2.yml | 57 + .../targets/set_fact/set_fact_empty_str_key.yml | 15 + .../targets/set_fact/set_fact_no_cache.yml | 39 + test/integration/targets/set_stats/aliases | 2 + test/integration/targets/set_stats/runme.sh | 13 + .../targets/set_stats/test_aggregate.yml | 13 + test/integration/targets/set_stats/test_simple.yml | 79 + .../targets/setup_cron/defaults/main.yml | 1 + test/integration/targets/setup_cron/meta/main.yml | 2 + test/integration/targets/setup_cron/tasks/main.yml | 99 + .../integration/targets/setup_cron/vars/alpine.yml | 1 + .../integration/targets/setup_cron/vars/debian.yml | 3 + .../targets/setup_cron/vars/default.yml | 0 .../integration/targets/setup_cron/vars/fedora.yml | 3 + .../targets/setup_cron/vars/freebsd.yml | 3 + .../integration/targets/setup_cron/vars/redhat.yml | 4 + test/integration/targets/setup_cron/vars/suse.yml | 3 + .../files/package_specs/stable/foo-1.0.0 | 10 + .../files/package_specs/stable/foo-1.0.1 | 10 + .../files/package_specs/stable/foobar-1.0.0 | 11 + .../files/package_specs/stable/foobar-1.0.1 | 10 + .../files/package_specs/testing/foo-2.0.0 | 10 + .../files/package_specs/testing/foo-2.0.1 | 10 + .../targets/setup_deb_repo/meta/main.yml | 2 + .../targets/setup_deb_repo/tasks/main.yml | 75 + test/integration/targets/setup_epel/tasks/main.yml | 10 + .../targets/setup_gnutar/handlers/main.yml | 6 + .../targets/setup_gnutar/tasks/main.yml | 18 + .../targets/setup_nobody/handlers/main.yml | 5 + .../targets/setup_nobody/tasks/main.yml | 7 + test/integration/targets/setup_paramiko/aliases | 1 + .../targets/setup_paramiko/constraints.txt | 1 + .../setup_paramiko/install-Alpine-3-python-3.yml | 9 + .../setup_paramiko/install-CentOS-6-python-2.yml | 3 + .../setup_paramiko/install-Darwin-python-3.yml | 9 + .../setup_paramiko/install-Fedora-35-python-3.yml | 9 + .../setup_paramiko/install-FreeBSD-python-3.yml | 8 + .../setup_paramiko/install-RedHat-8-python-3.yml | 8 + .../setup_paramiko/install-RedHat-9-python-3.yml | 9 + .../setup_paramiko/install-Ubuntu-16-python-2.yml | 3 + .../targets/setup_paramiko/install-fail.yml | 7 + .../targets/setup_paramiko/install-python-2.yml | 3 + .../targets/setup_paramiko/install-python-3.yml | 3 + .../integration/targets/setup_paramiko/install.yml | 19 + test/integration/targets/setup_paramiko/inventory | 1 + .../setup_paramiko/library/detect_paramiko.py | 31 + .../setup_paramiko/setup-remote-constraints.yml | 12 + test/integration/targets/setup_paramiko/setup.sh | 8 + .../setup_paramiko/uninstall-Alpine-3-python-3.yml | 4 + .../setup_paramiko/uninstall-Darwin-python-3.yml | 4 + .../uninstall-Fedora-35-python-3.yml | 5 + .../setup_paramiko/uninstall-FreeBSD-python-3.yml | 4 + .../setup_paramiko/uninstall-RedHat-8-python-3.yml | 4 + .../setup_paramiko/uninstall-RedHat-9-python-3.yml | 7 + .../setup_paramiko/uninstall-apt-python-2.yml | 5 + .../setup_paramiko/uninstall-apt-python-3.yml | 5 + .../targets/setup_paramiko/uninstall-dnf.yml | 2 + .../targets/setup_paramiko/uninstall-fail.yml | 7 + .../targets/setup_paramiko/uninstall-yum.yml | 2 + .../setup_paramiko/uninstall-zypper-python-2.yml | 2 + .../setup_paramiko/uninstall-zypper-python-3.yml | 2 + .../targets/setup_paramiko/uninstall.yml | 21 + .../targets/setup_passlib/tasks/main.yml | 4 + .../targets/setup_pexpect/files/constraints.txt | 2 + .../targets/setup_pexpect/meta/main.yml | 2 + .../targets/setup_pexpect/tasks/main.yml | 19 + .../targets/setup_remote_constraints/aliases | 1 + .../targets/setup_remote_constraints/meta/main.yml | 2 + .../setup_remote_constraints/tasks/main.yml | 8 + .../targets/setup_remote_tmp_dir/defaults/main.yml | 2 + .../targets/setup_remote_tmp_dir/handlers/main.yml | 7 + .../setup_remote_tmp_dir/tasks/default-cleanup.yml | 5 + .../targets/setup_remote_tmp_dir/tasks/default.yml | 12 + .../targets/setup_remote_tmp_dir/tasks/main.yml | 10 + .../setup_remote_tmp_dir/tasks/windows-cleanup.yml | 4 + .../targets/setup_remote_tmp_dir/tasks/windows.yml | 11 + test/integration/targets/setup_rpm_repo/aliases | 1 + .../targets/setup_rpm_repo/defaults/main.yml | 1 + .../targets/setup_rpm_repo/files/comps.xml | 36 + .../targets/setup_rpm_repo/handlers/main.yml | 5 + .../targets/setup_rpm_repo/library/create_repo.py | 125 + .../targets/setup_rpm_repo/meta/main.yml | 2 + .../targets/setup_rpm_repo/tasks/main.yml | 100 + .../targets/setup_rpm_repo/vars/Fedora.yml | 4 + .../targets/setup_rpm_repo/vars/RedHat-6.yml | 5 + .../targets/setup_rpm_repo/vars/RedHat-7.yml | 5 + .../targets/setup_rpm_repo/vars/RedHat-8.yml | 5 + .../targets/setup_rpm_repo/vars/RedHat-9.yml | 4 + .../targets/setup_rpm_repo/vars/main.yml | 1 + .../targets/setup_test_user/handlers/main.yml | 6 + .../targets/setup_test_user/tasks/default.yml | 14 + .../targets/setup_test_user/tasks/macosx.yml | 14 + .../targets/setup_test_user/tasks/main.yml | 37 + .../targets/setup_win_printargv/files/PrintArgv.cs | 13 + .../targets/setup_win_printargv/meta/main.yml | 3 + .../targets/setup_win_printargv/tasks/main.yml | 9 + .../targets/shell/action_plugins/test_shell.py | 19 + test/integration/targets/shell/aliases | 1 + .../connection_plugins/test_connection_default.py | 41 + .../connection_plugins/test_connection_override.py | 42 + test/integration/targets/shell/tasks/main.yml | 36 + test/integration/targets/slurp/aliases | 2 + test/integration/targets/slurp/files/bar.bin | Bin 0 -> 256 bytes test/integration/targets/slurp/meta/main.yml | 3 + test/integration/targets/slurp/tasks/main.yml | 69 + .../targets/slurp/tasks/test_unreadable.yml | 74 + test/integration/targets/special_vars/aliases | 3 + .../integration/targets/special_vars/meta/main.yml | 2 + .../targets/special_vars/tasks/main.yml | 100 + .../targets/special_vars/templates/foo.j2 | 7 + .../integration/targets/special_vars/vars/main.yml | 0 .../integration/targets/special_vars_hosts/aliases | 2 + .../targets/special_vars_hosts/inventory | 3 + .../targets/special_vars_hosts/playbook.yml | 53 + .../targets/special_vars_hosts/runme.sh | 7 + test/integration/targets/split/aliases | 3 + test/integration/targets/split/tasks/main.yml | 36 + test/integration/targets/stat/aliases | 1 + test/integration/targets/stat/files/foo.txt | 1 + test/integration/targets/stat/meta/main.yml | 3 + test/integration/targets/stat/tasks/main.yml | 179 ++ test/integration/targets/strategy_free/aliases | 1 + test/integration/targets/strategy_free/inventory | 2 + .../targets/strategy_free/last_include_tasks.yml | 2 + test/integration/targets/strategy_free/runme.sh | 10 + .../strategy_free/test_last_include_in_always.yml | 9 + test/integration/targets/strategy_linear/aliases | 1 + test/integration/targets/strategy_linear/inventory | 3 + .../strategy_linear/roles/role1/tasks/main.yml | 6 + .../strategy_linear/roles/role1/tasks/tasks.yml | 7 + .../strategy_linear/roles/role2/tasks/main.yml | 7 + test/integration/targets/strategy_linear/runme.sh | 7 + .../strategy_linear/task_action_templating.yml | 26 + .../strategy_linear/test_include_file_noop.yml | 16 + test/integration/targets/subversion/aliases | 7 + .../subversion/roles/subversion/defaults/main.yml | 10 + .../roles/subversion/files/create_repo.sh | 6 + .../subversion/roles/subversion/tasks/cleanup.yml | 8 + .../subversion/roles/subversion/tasks/main.yml | 20 + .../subversion/roles/subversion/tasks/setup.yml | 72 + .../roles/subversion/tasks/setup_selinux.yml | 11 + .../subversion/roles/subversion/tasks/tests.yml | 145 + .../subversion/roles/subversion/tasks/warnings.yml | 7 + .../roles/subversion/templates/subversion.conf.j2 | 71 + test/integration/targets/subversion/runme.sh | 35 + test/integration/targets/subversion/runme.yml | 15 + .../integration/targets/subversion/vars/Alpine.yml | 7 + .../integration/targets/subversion/vars/Debian.yml | 6 + .../targets/subversion/vars/FreeBSD.yml | 7 + .../integration/targets/subversion/vars/RedHat.yml | 10 + test/integration/targets/subversion/vars/Suse.yml | 6 + .../targets/subversion/vars/Ubuntu-18.yml | 6 + .../targets/subversion/vars/Ubuntu-20.yml | 6 + test/integration/targets/systemd/aliases | 1 + test/integration/targets/systemd/defaults/main.yml | 1 + test/integration/targets/systemd/handlers/main.yml | 12 + test/integration/targets/systemd/meta/main.yml | 2 + test/integration/targets/systemd/tasks/main.yml | 122 + .../systemd/tasks/test_indirect_service.yml | 37 + .../targets/systemd/tasks/test_unit_template.yml | 50 + .../targets/systemd/templates/dummy.service | 11 + .../targets/systemd/templates/dummy.socket | 8 + .../targets/systemd/templates/sleeper@.service | 8 + test/integration/targets/systemd/vars/Debian.yml | 3 + test/integration/targets/systemd/vars/default.yml | 3 + test/integration/targets/tags/aliases | 2 + test/integration/targets/tags/ansible_run_tags.yml | 49 + test/integration/targets/tags/runme.sh | 75 + test/integration/targets/tags/test_tags.yml | 36 + test/integration/targets/task_ordering/aliases | 2 + .../targets/task_ordering/meta/main.yml | 2 + .../targets/task_ordering/tasks/main.yml | 15 + .../task_ordering/tasks/taskorder-include.yml | 10 + test/integration/targets/tasks/aliases | 2 + test/integration/targets/tasks/playbook.yml | 19 + test/integration/targets/tasks/runme.sh | 3 + test/integration/targets/tempfile/aliases | 1 + test/integration/targets/tempfile/meta/main.yml | 2 + test/integration/targets/tempfile/tasks/main.yml | 63 + test/integration/targets/template/6653.yml | 10 + test/integration/targets/template/72262.yml | 6 + test/integration/targets/template/72615.yml | 18 + test/integration/targets/template/aliases | 3 + .../targets/template/ansible_managed.cfg | 2 + .../targets/template/ansible_managed.yml | 14 + test/integration/targets/template/badnull1.cfg | 2 + test/integration/targets/template/badnull2.cfg | 2 + test/integration/targets/template/badnull3.cfg | 2 + test/integration/targets/template/corner_cases.yml | 55 + .../targets/template/custom_tasks/tasks/main.yml | 15 + .../targets/template/custom_tasks/templates/test | 1 + .../targets/template/custom_template.yml | 4 + .../template/files/custom_comment_string.expected | 2 + .../template/files/encoding_1252_utf-8.expected | 1 + .../files/encoding_1252_windows-1252.expected | 1 + .../targets/template/files/foo-py26.txt | 9 + .../integration/targets/template/files/foo.dos.txt | 3 + test/integration/targets/template/files/foo.txt | 9 + .../targets/template/files/foo.unix.txt | 3 + .../targets/template/files/import_as.expected | 3 + .../template/files/import_as_with_context.expected | 2 + .../template/files/import_with_context.expected | 3 + .../template/files/lstrip_blocks_false.expected | 4 + .../template/files/lstrip_blocks_true.expected | 3 + .../template/files/override_colon_value.expected | 1 + .../template/files/string_type_filters.expected | 4 + .../template/files/trim_blocks_false.expected | 4 + .../template/files/trim_blocks_true.expected | 2 + .../targets/template/filter_plugins.yml | 9 + .../targets/template/in_template_overrides.j2 | 5 + .../targets/template/in_template_overrides.yml | 28 + test/integration/targets/template/lazy_eval.yml | 24 + test/integration/targets/template/meta/main.yml | 3 + .../role_filter/filter_plugins/myplugin.py | 12 + .../targets/template/role_filter/tasks/main.yml | 3 + test/integration/targets/template/runme.sh | 54 + .../targets/template/tasks/backup_test.yml | 60 + test/integration/targets/template/tasks/main.yml | 811 +++++ test/integration/targets/template/template.yml | 4 + .../targets/template/templates/6653-include.j2 | 1 + .../integration/targets/template/templates/6653.j2 | 4 + .../targets/template/templates/72262-included.j2 | 1 + .../targets/template/templates/72262-vars.j2 | 1 + .../targets/template/templates/72262.j2 | 3 + .../template/templates/72615-macro-nested.j2 | 4 + .../targets/template/templates/72615-macro.j2 | 8 + .../targets/template/templates/72615.j2 | 4 + test/integration/targets/template/templates/bar | 1 + .../targets/template/templates/caf\303\251.j2" | 1 + .../template/templates/custom_comment_string.j2 | 3 + .../targets/template/templates/empty_template.j2 | 0 .../targets/template/templates/encoding_1252.j2 | 1 + test/integration/targets/template/templates/foo.j2 | 3 + .../integration/targets/template/templates/foo2.j2 | 3 + .../integration/targets/template/templates/foo3.j2 | 3 + .../targets/template/templates/for_loop.j2 | 4 + .../targets/template/templates/for_loop_include.j2 | 3 + .../template/templates/for_loop_include_nested.j2 | 1 + .../targets/template/templates/import_as.j2 | 4 + .../template/templates/import_as_with_context.j2 | 3 + .../template/templates/import_with_context.j2 | 4 + .../targets/template/templates/indirect_dict.j2 | 1 + .../targets/template/templates/json_macro.j2 | 2 + .../targets/template/templates/lstrip_blocks.j2 | 8 + .../template/templates/macro_using_globals.j2 | 3 + .../template/templates/override_colon_value.j2 | 4 + .../template/templates/override_separator.j2 | 1 + .../targets/template/templates/parent.j2 | 3 + test/integration/targets/template/templates/qux | 1 + .../targets/template/templates/short.j2 | 1 + .../targets/template/templates/subtemplate.j2 | 2 + .../template/templates/template_destpath_test.j2 | 1 + .../templates/template_import_macro_globals.j2 | 2 + .../targets/template/templates/trim_blocks.j2 | 4 + .../template/templates/unused_vars_include.j2 | 1 + .../template/templates/unused_vars_template.j2 | 2 + .../targets/template/undefined_in_import-import.j2 | 1 + .../targets/template/undefined_in_import.j2 | 1 + .../targets/template/undefined_in_import.yml | 11 + .../targets/template/undefined_var_info.yml | 15 + test/integration/targets/template/unsafe.yml | 64 + .../targets/template/unused_vars_include.yml | 8 + test/integration/targets/template/vars/main.yml | 20 + .../targets/template_jinja2_non_native/46169.yml | 31 + .../targets/template_jinja2_non_native/aliases | 2 + .../targets/template_jinja2_non_native/runme.sh | 7 + .../templates/46169.json.j2 | 3 + test/integration/targets/templating/aliases | 2 + test/integration/targets/templating/tasks/main.yml | 35 + .../templating/templates/invalid_test_name.j2 | 1 + .../integration/targets/templating_lookups/aliases | 2 + .../targets/templating_lookups/runme.sh | 9 + .../targets/templating_lookups/runme.yml | 4 + .../templating_lookups/template_deepcopy/hosts | 1 + .../template_deepcopy/playbook.yml | 10 + .../template_deepcopy/template.in | 1 + .../template_lookup_vaulted/playbook.yml | 13 + .../templates/vaulted_hello.j2 | 6 + .../template_lookup_vaulted/test_vault_pass | 1 + .../template_lookups/mock_lookup_plugins/77788.py | 6 + .../template_lookups/tasks/errors.yml | 31 + .../template_lookups/tasks/main.yml | 101 + .../template_lookups/vars/main.yml | 9 + .../targets/templating_settings/aliases | 2 + .../templating_settings/dont_warn_register.yml | 6 + .../targets/templating_settings/runme.sh | 6 + .../test_templating_settings.yml | 14 + test/integration/targets/test_core/aliases | 1 + test/integration/targets/test_core/inventory | 1 + test/integration/targets/test_core/runme.sh | 5 + test/integration/targets/test_core/runme.yml | 4 + test/integration/targets/test_core/tasks/main.yml | 350 +++ test/integration/targets/test_core/vault-password | 1 + test/integration/targets/test_files/aliases | 1 + test/integration/targets/test_files/tasks/main.yml | 60 + test/integration/targets/test_mathstuff/aliases | 1 + .../targets/test_mathstuff/tasks/main.yml | 27 + test/integration/targets/test_uri/aliases | 1 + test/integration/targets/test_uri/tasks/main.yml | 43 + test/integration/targets/throttle/aliases | 2 + .../targets/throttle/group_vars/all.yml | 4 + test/integration/targets/throttle/inventory | 6 + test/integration/targets/throttle/runme.sh | 7 + test/integration/targets/throttle/test_throttle.py | 34 + .../integration/targets/throttle/test_throttle.yml | 84 + test/integration/targets/unarchive/aliases | 3 + test/integration/targets/unarchive/files/foo.txt | 1 + ...217\343\202\211\343\201\250\343\201\277.tar.gz" | Bin 0 -> 4947 bytes .../targets/unarchive/handlers/main.yml | 3 + test/integration/targets/unarchive/meta/main.yml | 4 + test/integration/targets/unarchive/tasks/main.yml | 22 + .../targets/unarchive/tasks/prepare_tests.yml | 115 + .../tasks/test_different_language_var.yml | 41 + .../targets/unarchive/tasks/test_download.yml | 44 + .../targets/unarchive/tasks/test_exclude.yml | 54 + .../targets/unarchive/tasks/test_include.yml | 81 + .../unarchive/tasks/test_invalid_options.yml | 27 + .../unarchive/tasks/test_missing_binaries.yml | 87 + .../targets/unarchive/tasks/test_missing_files.yml | 47 + .../targets/unarchive/tasks/test_mode.yml | 151 + .../unarchive/tasks/test_non_ascii_filename.yml | 66 + .../targets/unarchive/tasks/test_owner_group.yml | 164 + .../unarchive/tasks/test_ownership_top_folder.yml | 50 + .../unarchive/tasks/test_parent_not_writeable.yml | 32 + .../unarchive/tasks/test_quotable_characters.yml | 38 + .../targets/unarchive/tasks/test_symlink.yml | 64 + .../targets/unarchive/tasks/test_tar.yml | 33 + .../targets/unarchive/tasks/test_tar_gz.yml | 35 + .../unarchive/tasks/test_tar_gz_creates.yml | 53 + .../unarchive/tasks/test_tar_gz_keep_newer.yml | 57 + .../unarchive/tasks/test_tar_gz_owner_group.yml | 50 + .../targets/unarchive/tasks/test_tar_zst.yml | 40 + .../unarchive/tasks/test_unprivileged_user.yml | 63 + .../targets/unarchive/tasks/test_zip.yml | 57 + test/integration/targets/unarchive/vars/Darwin.yml | 1 + .../integration/targets/unarchive/vars/FreeBSD.yml | 4 + test/integration/targets/unarchive/vars/Linux.yml | 4 + test/integration/targets/undefined/aliases | 2 + test/integration/targets/undefined/tasks/main.yml | 16 + .../action_plugins/unexpected.py | 8 + .../targets/unexpected_executor_exception/aliases | 2 + .../unexpected_executor_exception/tasks/main.yml | 7 + test/integration/targets/unicode/aliases | 2 + test/integration/targets/unicode/inventory | 5 + .../ansible.cfg" | 2 + test/integration/targets/unicode/runme.sh | 13 + .../targets/unicode/unicode-test-script | 7 + test/integration/targets/unicode/unicode.yml | 149 + test/integration/targets/unsafe_writes/aliases | 7 + test/integration/targets/unsafe_writes/basic.yml | 83 + test/integration/targets/unsafe_writes/runme.sh | 12 + .../until/action_plugins/shell_no_failed.py | 28 + test/integration/targets/until/aliases | 2 + test/integration/targets/until/tasks/main.yml | 84 + test/integration/targets/unvault/aliases | 2 + test/integration/targets/unvault/main.yml | 9 + test/integration/targets/unvault/password | 1 + test/integration/targets/unvault/runme.sh | 6 + test/integration/targets/unvault/vault | 6 + test/integration/targets/uri/aliases | 3 + test/integration/targets/uri/files/README | 9 + test/integration/targets/uri/files/fail0.json | 1 + test/integration/targets/uri/files/fail1.json | 1 + test/integration/targets/uri/files/fail10.json | 1 + test/integration/targets/uri/files/fail11.json | 1 + test/integration/targets/uri/files/fail12.json | 1 + test/integration/targets/uri/files/fail13.json | 1 + test/integration/targets/uri/files/fail14.json | 1 + test/integration/targets/uri/files/fail15.json | 1 + test/integration/targets/uri/files/fail16.json | 1 + test/integration/targets/uri/files/fail17.json | 1 + test/integration/targets/uri/files/fail18.json | 1 + test/integration/targets/uri/files/fail19.json | 1 + test/integration/targets/uri/files/fail2.json | 1 + test/integration/targets/uri/files/fail20.json | 1 + test/integration/targets/uri/files/fail21.json | 1 + test/integration/targets/uri/files/fail22.json | 1 + test/integration/targets/uri/files/fail23.json | 1 + test/integration/targets/uri/files/fail24.json | 1 + test/integration/targets/uri/files/fail25.json | 1 + test/integration/targets/uri/files/fail26.json | 2 + test/integration/targets/uri/files/fail27.json | 2 + test/integration/targets/uri/files/fail28.json | 1 + test/integration/targets/uri/files/fail29.json | 1 + test/integration/targets/uri/files/fail3.json | 1 + test/integration/targets/uri/files/fail30.json | 1 + test/integration/targets/uri/files/fail4.json | 1 + test/integration/targets/uri/files/fail5.json | 1 + test/integration/targets/uri/files/fail6.json | 1 + test/integration/targets/uri/files/fail7.json | 1 + test/integration/targets/uri/files/fail8.json | 1 + test/integration/targets/uri/files/fail9.json | 1 + test/integration/targets/uri/files/formdata.txt | 1 + test/integration/targets/uri/files/pass0.json | 58 + test/integration/targets/uri/files/pass1.json | 1 + test/integration/targets/uri/files/pass2.json | 6 + test/integration/targets/uri/files/pass3.json | 1 + test/integration/targets/uri/files/pass4.json | 1 + test/integration/targets/uri/files/testserver.py | 23 + test/integration/targets/uri/meta/main.yml | 4 + test/integration/targets/uri/tasks/ciphers.yml | 32 + test/integration/targets/uri/tasks/main.yml | 779 +++++ .../integration/targets/uri/tasks/redirect-all.yml | 272 ++ .../targets/uri/tasks/redirect-none.yml | 296 ++ .../targets/uri/tasks/redirect-safe.yml | 274 ++ .../targets/uri/tasks/redirect-urllib2.yml | 294 ++ .../targets/uri/tasks/return-content.yml | 49 + .../targets/uri/tasks/unexpected-failures.yml | 26 + test/integration/targets/uri/tasks/use_gssapi.yml | 62 + test/integration/targets/uri/tasks/use_netrc.yml | 51 + test/integration/targets/uri/templates/netrc.j2 | 3 + test/integration/targets/uri/vars/main.yml | 20 + test/integration/targets/user/aliases | 2 + test/integration/targets/user/files/userlist.sh | 20 + test/integration/targets/user/meta/main.yml | 2 + test/integration/targets/user/tasks/main.yml | 42 + .../targets/user/tasks/test_create_system_user.yml | 12 + .../targets/user/tasks/test_create_user.yml | 67 + .../targets/user/tasks/test_create_user_home.yml | 136 + .../user/tasks/test_create_user_password.yml | 90 + .../targets/user/tasks/test_create_user_uid.yml | 26 + .../targets/user/tasks/test_expires.yml | 147 + .../targets/user/tasks/test_expires_min_max.yml | 73 + .../user/tasks/test_expires_new_account.yml | 55 + .../test_expires_new_account_epoch_negative.yml | 112 + test/integration/targets/user/tasks/test_local.yml | 196 ++ .../targets/user/tasks/test_local_expires.yml | 333 ++ .../targets/user/tasks/test_no_home_fallback.yml | 106 + .../targets/user/tasks/test_password_lock.yml | 140 + .../user/tasks/test_password_lock_new_user.yml | 63 + .../targets/user/tasks/test_remove_user.yml | 19 + .../targets/user/tasks/test_shadow_backup.yml | 21 + .../targets/user/tasks/test_ssh_key_passphrase.yml | 30 + test/integration/targets/user/tasks/test_umask.yml | 57 + test/integration/targets/user/vars/main.yml | 13 + test/integration/targets/var_blending/aliases | 2 + .../targets/var_blending/group_vars/all | 9 + .../targets/var_blending/group_vars/local | 1 + .../targets/var_blending/host_vars/testhost | 4 + test/integration/targets/var_blending/inventory | 26 + .../roles/test_var_blending/defaults/main.yml | 4 + .../roles/test_var_blending/files/foo.txt | 77 + .../roles/test_var_blending/tasks/main.yml | 57 + .../roles/test_var_blending/templates/foo.j2 | 77 + .../roles/test_var_blending/vars/main.yml | 4 + .../roles/test_var_blending/vars/more_vars.yml | 3 + test/integration/targets/var_blending/runme.sh | 5 + .../targets/var_blending/test_var_blending.yml | 8 + .../integration/targets/var_blending/test_vars.yml | 1 + .../integration/targets/var_blending/vars_file.yml | 12 + test/integration/targets/var_inheritance/aliases | 2 + .../targets/var_inheritance/tasks/main.yml | 16 + test/integration/targets/var_precedence/aliases | 2 + .../var_precedence/ansible-var-precedence-check.py | 544 ++++ .../targets/var_precedence/host_vars/testhost | 2 + test/integration/targets/var_precedence/inventory | 13 + .../roles/test_var_precedence/meta/main.yml | 4 + .../roles/test_var_precedence/tasks/main.yml | 10 + .../test_var_precedence_dep/defaults/main.yml | 5 + .../roles/test_var_precedence_dep/tasks/main.yml | 14 + .../roles/test_var_precedence_dep/vars/main.yml | 4 + .../tasks/main.yml | 5 + .../test_var_precedence_role1/defaults/main.yml | 5 + .../roles/test_var_precedence_role1/meta/main.yml | 2 + .../roles/test_var_precedence_role1/tasks/main.yml | 14 + .../roles/test_var_precedence_role1/vars/main.yml | 4 + .../test_var_precedence_role2/defaults/main.yml | 5 + .../roles/test_var_precedence_role2/tasks/main.yml | 14 + .../roles/test_var_precedence_role2/vars/main.yml | 5 + .../test_var_precedence_role3/defaults/main.yml | 7 + .../roles/test_var_precedence_role3/tasks/main.yml | 14 + .../roles/test_var_precedence_role3/vars/main.yml | 3 + test/integration/targets/var_precedence/runme.sh | 9 + .../targets/var_precedence/test_var_precedence.yml | 44 + .../var_precedence/vars/test_var_precedence.yml | 5 + test/integration/targets/var_reserved/aliases | 2 + .../var_reserved/reserved_varname_warning.yml | 6 + test/integration/targets/var_reserved/runme.sh | 5 + test/integration/targets/var_templating/aliases | 2 + .../var_templating/ansible_debug_template.j2 | 1 + .../targets/var_templating/group_vars/all.yml | 7 + test/integration/targets/var_templating/runme.sh | 21 + .../var_templating/task_vars_templating.yml | 58 + .../var_templating/test_connection_vars.yml | 26 + .../var_templating/test_vars_with_sources.yml | 9 + test/integration/targets/var_templating/undall.yml | 6 + .../targets/var_templating/undefined.yml | 13 + .../targets/var_templating/vars/connection.yml | 3 + test/integration/targets/wait_for/aliases | 2 + .../targets/wait_for/files/testserver.py | 19 + .../targets/wait_for/files/write_utf16.py | 20 + test/integration/targets/wait_for/files/zombie.py | 13 + test/integration/targets/wait_for/meta/main.yml | 3 + test/integration/targets/wait_for/tasks/main.yml | 199 ++ test/integration/targets/wait_for/vars/main.yml | 4 + .../targets/wait_for_connection/aliases | 2 + .../targets/wait_for_connection/tasks/main.yml | 30 + .../targets/want_json_modules_posix/aliases | 2 + .../want_json_modules_posix/library/helloworld.py | 34 + .../targets/want_json_modules_posix/meta/main.yml | 2 + .../targets/want_json_modules_posix/tasks/main.yml | 43 + test/integration/targets/win_async_wrapper/aliases | 3 + .../win_async_wrapper/library/async_test.ps1 | 47 + .../targets/win_async_wrapper/tasks/main.yml | 257 ++ test/integration/targets/win_become/aliases | 2 + test/integration/targets/win_become/tasks/main.yml | 251 ++ test/integration/targets/win_exec_wrapper/aliases | 2 + .../win_exec_wrapper/library/test_all_options.ps1 | 12 + .../library/test_common_functions.ps1 | 43 + .../targets/win_exec_wrapper/library/test_fail.ps1 | 66 + .../library/test_invalid_requires.ps1 | 9 + .../library/test_min_os_version.ps1 | 8 + .../library/test_min_ps_version.ps1 | 8 + .../targets/win_exec_wrapper/tasks/main.yml | 274 ++ test/integration/targets/win_fetch/aliases | 1 + test/integration/targets/win_fetch/meta/main.yml | 2 + test/integration/targets/win_fetch/tasks/main.yml | 212 ++ test/integration/targets/win_module_utils/aliases | 2 + .../win_module_utils/library/csharp_util.ps1 | 12 + .../library/legacy_only_new_way.ps1 | 5 + .../legacy_only_new_way_win_line_ending.ps1 | 6 + .../library/legacy_only_old_way.ps1 | 5 + .../legacy_only_old_way_win_line_ending.ps1 | 4 + .../library/recursive_requires.ps1 | 13 + .../win_module_utils/library/uses_bogus_utils.ps1 | 6 + .../win_module_utils/library/uses_local_utils.ps1 | 9 + .../Ansible.ModuleUtils.Recursive1.psm1 | 9 + .../Ansible.ModuleUtils.Recursive2.psm1 | 12 + .../Ansible.ModuleUtils.Recursive3.psm1 | 20 + .../Ansible.ModuleUtils.ValidTestModule.psm1 | 3 + .../win_module_utils/module_utils/Ansible.Test.cs | 26 + .../targets/win_module_utils/tasks/main.yml | 71 + test/integration/targets/win_raw/aliases | 2 + test/integration/targets/win_raw/tasks/main.yml | 143 + test/integration/targets/win_script/aliases | 2 + .../targets/win_script/defaults/main.yml | 5 + test/integration/targets/win_script/files/fail.bat | 1 + .../win_script/files/space path/test_script.ps1 | 1 + .../targets/win_script/files/test_script.bat | 2 + .../targets/win_script/files/test_script.cmd | 2 + .../targets/win_script/files/test_script.ps1 | 2 + .../targets/win_script/files/test_script_bool.ps1 | 6 + .../win_script/files/test_script_creates_file.ps1 | 3 + .../win_script/files/test_script_removes_file.ps1 | 3 + .../win_script/files/test_script_whoami.ps1 | 2 + .../win_script/files/test_script_with_args.ps1 | 6 + .../win_script/files/test_script_with_env.ps1 | 1 + .../win_script/files/test_script_with_errors.ps1 | 8 + .../files/test_script_with_splatting.ps1 | 6 + test/integration/targets/win_script/tasks/main.yml | 316 ++ test/integration/targets/windows-minimal/aliases | 4 + .../targets/windows-minimal/library/win_ping.ps1 | 21 + .../targets/windows-minimal/library/win_ping.py | 55 + .../windows-minimal/library/win_ping_set_attr.ps1 | 31 + .../library/win_ping_strict_mode_error.ps1 | 30 + .../library/win_ping_syntax_error.ps1 | 30 + .../windows-minimal/library/win_ping_throw.ps1 | 30 + .../library/win_ping_throw_string.ps1 | 30 + .../targets/windows-minimal/tasks/main.yml | 67 + test/integration/targets/windows-paths/aliases | 3 + .../targets/windows-paths/tasks/main.yml | 191 ++ test/integration/targets/yaml_parsing/aliases | 2 + test/integration/targets/yaml_parsing/playbook.yml | 5 + .../targets/yaml_parsing/tasks/main.yml | 37 + .../targets/yaml_parsing/tasks/unsafe.yml | 36 + .../integration/targets/yaml_parsing/vars/main.yml | 1 + test/integration/targets/yum/aliases | 5 + test/integration/targets/yum/files/yum.conf | 5 + .../filter_list_of_tuples_by_first_param.py | 25 + test/integration/targets/yum/meta/main.yml | 4 + test/integration/targets/yum/tasks/cacheonly.yml | 16 + .../targets/yum/tasks/check_mode_consistency.yml | 61 + test/integration/targets/yum/tasks/lock.yml | 28 + test/integration/targets/yum/tasks/main.yml | 82 + test/integration/targets/yum/tasks/multiarch.yml | 154 + test/integration/targets/yum/tasks/proxy.yml | 186 ++ test/integration/targets/yum/tasks/repo.yml | 729 +++++ test/integration/targets/yum/tasks/yum.yml | 884 ++++++ .../targets/yum/tasks/yum_group_remove.yml | 152 + .../targets/yum/tasks/yuminstallroot.yml | 132 + test/integration/targets/yum/vars/main.yml | 1 + test/integration/targets/yum_repository/aliases | 2 + .../targets/yum_repository/defaults/main.yml | 5 + .../targets/yum_repository/handlers/main.yml | 4 + .../targets/yum_repository/meta/main.yml | 4 + .../targets/yum_repository/tasks/main.yml | 196 ++ 3012 files changed, 98142 insertions(+) create mode 100644 test/integration/network-integration.cfg create mode 100644 test/integration/network-integration.requirements.txt create mode 100644 test/integration/targets/add_host/aliases create mode 100644 test/integration/targets/add_host/tasks/main.yml create mode 100644 test/integration/targets/adhoc/aliases create mode 100755 test/integration/targets/adhoc/runme.sh create mode 100644 test/integration/targets/ansiballz_python/aliases create mode 100644 test/integration/targets/ansiballz_python/library/check_rlimit_and_maxfd.py create mode 100644 test/integration/targets/ansiballz_python/library/custom_module.py create mode 100644 test/integration/targets/ansiballz_python/library/sys_check.py create mode 100644 test/integration/targets/ansiballz_python/module_utils/custom_util.py create mode 100644 test/integration/targets/ansiballz_python/tasks/main.yml create mode 100644 test/integration/targets/ansible-doc/aliases create mode 100644 test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/MANIFEST.json create mode 100644 test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/cache/notjsonfile.py create mode 100644 test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py create mode 100644 test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py create mode 100644 test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/fakemodule.py create mode 100644 test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/notrealmodule.py create mode 100644 test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py create mode 100644 test/integration/targets/ansible-doc/broken-docs/collections/ansible_collections/testns/testcol/plugins/vars/noop_vars_plugin.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/MANIFEST.json create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/cache/notjsonfile.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/filter_subdir/in_subdir.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/grouped.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/filter/ultimatequestion.yml create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/inventory/statichost.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/lookup/noop.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/database/database_type/subdir_module.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/fakemodule.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/notrealmodule.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/modules/randommodule.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/test_test.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/test/yolo.yml create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/plugins/vars/noop_vars_plugin.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole/meta/main.yml create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol/roles/testrole_with_no_argspecs/meta/empty create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/MANIFEST.json create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/deprecation.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/module.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/plugin.py create mode 100644 test/integration/targets/ansible-doc/collections/ansible_collections/testns/testcol2/plugins/doc_fragments/version_added.py create mode 100644 test/integration/targets/ansible-doc/fakecollrole.output create mode 100644 test/integration/targets/ansible-doc/fakemodule.output create mode 100644 test/integration/targets/ansible-doc/fakerole.output create mode 100644 test/integration/targets/ansible-doc/filter_plugins/donothing.yml create mode 100644 test/integration/targets/ansible-doc/filter_plugins/other.py create mode 100644 test/integration/targets/ansible-doc/filter_plugins/split.yml create mode 100644 test/integration/targets/ansible-doc/inventory create mode 100644 test/integration/targets/ansible-doc/library/double_doc.py create mode 100644 test/integration/targets/ansible-doc/library/test_docs.py create mode 100644 test/integration/targets/ansible-doc/library/test_docs_missing_description.py create mode 100644 test/integration/targets/ansible-doc/library/test_docs_no_metadata.py create mode 100644 test/integration/targets/ansible-doc/library/test_docs_no_status.py create mode 100644 test/integration/targets/ansible-doc/library/test_docs_non_iterable_status.py create mode 100644 test/integration/targets/ansible-doc/library/test_docs_removed_precedence.py create mode 100644 test/integration/targets/ansible-doc/library/test_docs_removed_status.py create mode 100644 test/integration/targets/ansible-doc/library/test_docs_returns.py create mode 100644 test/integration/targets/ansible-doc/library/test_docs_returns_broken.py create mode 100644 test/integration/targets/ansible-doc/library/test_docs_suboptions.py create mode 100644 test/integration/targets/ansible-doc/library/test_docs_yaml_anchors.py create mode 100644 test/integration/targets/ansible-doc/library/test_empty.py create mode 100644 test/integration/targets/ansible-doc/library/test_no_docs.py create mode 100644 test/integration/targets/ansible-doc/library/test_no_docs_no_metadata.py create mode 100644 test/integration/targets/ansible-doc/library/test_no_docs_no_status.py create mode 100644 test/integration/targets/ansible-doc/library/test_no_docs_non_iterable_status.py create mode 100644 test/integration/targets/ansible-doc/library/test_win_module.ps1 create mode 100644 test/integration/targets/ansible-doc/library/test_win_module.yml create mode 100644 test/integration/targets/ansible-doc/lookup_plugins/_deprecated_with_adj_docs.py create mode 100644 test/integration/targets/ansible-doc/lookup_plugins/_deprecated_with_docs.py create mode 100644 test/integration/targets/ansible-doc/lookup_plugins/deprecated_with_adj_docs.yml create mode 100644 test/integration/targets/ansible-doc/noop.output create mode 100644 test/integration/targets/ansible-doc/noop_vars_plugin.output create mode 100644 test/integration/targets/ansible-doc/notjsonfile.output create mode 100644 test/integration/targets/ansible-doc/randommodule-text.output create mode 100644 test/integration/targets/ansible-doc/randommodule.output create mode 100644 test/integration/targets/ansible-doc/roles/test_role1/meta/argument_specs.yml create mode 100644 test/integration/targets/ansible-doc/roles/test_role1/meta/main.yml create mode 100644 test/integration/targets/ansible-doc/roles/test_role2/meta/empty create mode 100644 test/integration/targets/ansible-doc/roles/test_role3/meta/main.yml create mode 100755 test/integration/targets/ansible-doc/runme.sh create mode 100644 test/integration/targets/ansible-doc/test.yml create mode 100644 test/integration/targets/ansible-doc/test_docs_returns.output create mode 100644 test/integration/targets/ansible-doc/test_docs_suboptions.output create mode 100644 test/integration/targets/ansible-doc/test_docs_yaml_anchors.output create mode 100644 test/integration/targets/ansible-doc/test_role1/README.txt create mode 100644 test/integration/targets/ansible-doc/test_role1/meta/main.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-cli/aliases create mode 100644 test/integration/targets/ansible-galaxy-collection-cli/files/expected.txt create mode 100644 test/integration/targets/ansible-galaxy-collection-cli/files/expected_full_manifest.txt create mode 100644 test/integration/targets/ansible-galaxy-collection-cli/files/full_manifest_galaxy.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-cli/files/galaxy.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-cli/files/make_collection_dir.py create mode 100644 test/integration/targets/ansible-galaxy-collection-cli/tasks/main.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-cli/tasks/manifest.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/aliases create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/meta/main.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/download.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/empty_installed_collections.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/individual_collection_repo.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/main.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_all.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/multi_collection_repo_individual.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/reinstalling.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/requirements.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/scm_dependency_deduplication.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/setup.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_collection_bad_version.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_multi_collection_repo.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/setup_recursive_scm_dependency.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/test_invalid_version.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/test_manifest_metadata.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/tasks/test_supported_resolvelib_versions.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/templates/git_prefix_name.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/templates/name_and_type.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/templates/name_without_type.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/templates/source_and_name.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/templates/source_and_name_and_type.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/templates/source_only.yml create mode 100644 test/integration/targets/ansible-galaxy-collection-scm/vars/main.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/aliases create mode 100644 test/integration/targets/ansible-galaxy-collection/files/build_bad_tar.py create mode 100644 test/integration/targets/ansible-galaxy-collection/files/test_module.py create mode 100644 test/integration/targets/ansible-galaxy-collection/library/reset_pulp.py create mode 100644 test/integration/targets/ansible-galaxy-collection/library/setup_collections.py create mode 100644 test/integration/targets/ansible-galaxy-collection/meta/main.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/build.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/download.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/fail_fast_resolvelib.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/init.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/install.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/install_offline.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/list.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/main.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/publish.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/pulp.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/revoke_gpg_key.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/setup_gpg.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/supported_resolvelib.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/unsupported_resolvelib.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/upgrade.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/tasks/verify.yml create mode 100644 test/integration/targets/ansible-galaxy-collection/templates/ansible.cfg.j2 create mode 100644 test/integration/targets/ansible-galaxy-collection/vars/main.yml create mode 100644 test/integration/targets/ansible-galaxy-role/aliases create mode 100644 test/integration/targets/ansible-galaxy-role/meta/main.yml create mode 100644 test/integration/targets/ansible-galaxy-role/tasks/main.yml create mode 100644 test/integration/targets/ansible-galaxy/aliases create mode 100644 test/integration/targets/ansible-galaxy/cleanup-default.yml create mode 100644 test/integration/targets/ansible-galaxy/cleanup-freebsd.yml create mode 100644 test/integration/targets/ansible-galaxy/cleanup.yml create mode 100644 test/integration/targets/ansible-galaxy/files/testserver.py create mode 100755 test/integration/targets/ansible-galaxy/runme.sh create mode 100644 test/integration/targets/ansible-galaxy/setup.yml create mode 100644 test/integration/targets/ansible-inventory/aliases create mode 100644 test/integration/targets/ansible-inventory/files/invalid_sample.yml create mode 100644 test/integration/targets/ansible-inventory/files/unicode.yml create mode 100644 test/integration/targets/ansible-inventory/files/valid_sample.toml create mode 100644 test/integration/targets/ansible-inventory/files/valid_sample.yml create mode 100755 test/integration/targets/ansible-inventory/runme.sh create mode 100644 test/integration/targets/ansible-inventory/tasks/main.yml create mode 100644 test/integration/targets/ansible-inventory/tasks/toml.yml create mode 100644 test/integration/targets/ansible-inventory/test.yml create mode 100644 test/integration/targets/ansible-pull/aliases create mode 100644 test/integration/targets/ansible-pull/cleanup.yml create mode 100644 test/integration/targets/ansible-pull/pull-integration-test/ansible.cfg create mode 100644 test/integration/targets/ansible-pull/pull-integration-test/inventory create mode 100644 test/integration/targets/ansible-pull/pull-integration-test/local.yml create mode 100644 test/integration/targets/ansible-pull/pull-integration-test/multi_play_1.yml create mode 100644 test/integration/targets/ansible-pull/pull-integration-test/multi_play_2.yml create mode 100755 test/integration/targets/ansible-pull/runme.sh create mode 100644 test/integration/targets/ansible-pull/setup.yml create mode 100644 test/integration/targets/ansible-runner/aliases create mode 100644 test/integration/targets/ansible-runner/files/adhoc_example1.py create mode 100644 test/integration/targets/ansible-runner/files/playbook_example1.py create mode 100644 test/integration/targets/ansible-runner/filter_plugins/parse.py create mode 100644 test/integration/targets/ansible-runner/inventory create mode 100755 test/integration/targets/ansible-runner/runme.sh create mode 100644 test/integration/targets/ansible-runner/tasks/adhoc_example1.yml create mode 100644 test/integration/targets/ansible-runner/tasks/main.yml create mode 100644 test/integration/targets/ansible-runner/tasks/playbook_example1.yml create mode 100644 test/integration/targets/ansible-runner/tasks/setup.yml create mode 100644 test/integration/targets/ansible-runner/test.yml create mode 100644 test/integration/targets/ansible-test-cloud-acme/aliases create mode 100644 test/integration/targets/ansible-test-cloud-acme/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-cloud-aws/aliases create mode 100644 test/integration/targets/ansible-test-cloud-aws/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-cloud-azure/aliases create mode 100644 test/integration/targets/ansible-test-cloud-azure/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-cloud-cs/aliases create mode 100644 test/integration/targets/ansible-test-cloud-cs/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-cloud-foreman/aliases create mode 100644 test/integration/targets/ansible-test-cloud-foreman/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-cloud-galaxy/aliases create mode 100644 test/integration/targets/ansible-test-cloud-galaxy/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-cloud-httptester-windows/aliases create mode 100644 test/integration/targets/ansible-test-cloud-httptester-windows/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-cloud-httptester/aliases create mode 100644 test/integration/targets/ansible-test-cloud-httptester/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-cloud-nios/aliases create mode 100644 test/integration/targets/ansible-test-cloud-nios/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-cloud-openshift/aliases create mode 100644 test/integration/targets/ansible-test-cloud-openshift/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-cloud-vcenter/aliases create mode 100644 test/integration/targets/ansible-test-cloud-vcenter/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-config-invalid/aliases create mode 100644 test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/config.yml create mode 100644 test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/aliases create mode 100755 test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/integration/targets/test/runme.sh create mode 100644 test/integration/targets/ansible-test-config-invalid/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py create mode 100755 test/integration/targets/ansible-test-config-invalid/runme.sh create mode 100644 test/integration/targets/ansible-test-config/aliases create mode 100644 test/integration/targets/ansible-test-config/ansible_collections/ns/col/plugins/module_utils/test.py create mode 100644 test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/config.yml create mode 100644 test/integration/targets/ansible-test-config/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_test.py create mode 100755 test/integration/targets/ansible-test-config/runme.sh create mode 100644 test/integration/targets/ansible-test-container/aliases create mode 100755 test/integration/targets/ansible-test-container/runme.py create mode 100755 test/integration/targets/ansible-test-container/runme.sh create mode 100644 test/integration/targets/ansible-test-coverage/aliases create mode 100644 test/integration/targets/ansible-test-coverage/ansible_collections/ns/col/plugins/module_utils/test_util.py create mode 100755 test/integration/targets/ansible-test-coverage/runme.sh create mode 100644 test/integration/targets/ansible-test-docker/aliases create mode 100644 test/integration/targets/ansible-test-docker/ansible_collections/ns/col/galaxy.yml create mode 100644 test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/doc_fragments/ps_util.py create mode 100644 test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/module_utils/PSUtil.psm1 create mode 100644 test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/module_utils/my_util.py create mode 100644 test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/hello.py create mode 100644 test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/win_util_args.ps1 create mode 100644 test/integration/targets/ansible-test-docker/ansible_collections/ns/col/plugins/modules/win_util_args.py create mode 100644 test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/integration/targets/minimal/aliases create mode 100644 test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/integration/targets/minimal/tasks/main.yml create mode 100644 test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py create mode 100644 test/integration/targets/ansible-test-docker/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py create mode 100755 test/integration/targets/ansible-test-docker/runme.sh create mode 100644 test/integration/targets/ansible-test-git/aliases create mode 100644 test/integration/targets/ansible-test-git/ansible_collections/ns/col/tests/.keep create mode 100755 test/integration/targets/ansible-test-git/collection-tests/git-at-collection-base.sh create mode 100755 test/integration/targets/ansible-test-git/collection-tests/git-at-collection-root.sh create mode 100755 test/integration/targets/ansible-test-git/collection-tests/git-common.bash create mode 100644 test/integration/targets/ansible-test-git/collection-tests/install-git.yml create mode 100644 test/integration/targets/ansible-test-git/collection-tests/uninstall-git.yml create mode 100755 test/integration/targets/ansible-test-git/runme.sh create mode 100644 test/integration/targets/ansible-test-integration-constraints/aliases create mode 100644 test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/constraints.txt create mode 100644 test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/requirements.txt create mode 100644 test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/targets/constraints/aliases create mode 100644 test/integration/targets/ansible-test-integration-constraints/ansible_collections/ns/col/tests/integration/targets/constraints/tasks/main.yml create mode 100755 test/integration/targets/ansible-test-integration-constraints/runme.sh create mode 100644 test/integration/targets/ansible-test-integration-targets/aliases create mode 100644 test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/destructive_a/aliases create mode 100644 test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/destructive_b/aliases create mode 100644 test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/disabled_a/aliases create mode 100644 test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/disabled_b/aliases create mode 100644 test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unstable_a/aliases create mode 100644 test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unstable_b/aliases create mode 100644 test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unsupported_a/aliases create mode 100644 test/integration/targets/ansible-test-integration-targets/ansible_collections/ns/col/tests/integration/targets/unsupported_b/aliases create mode 100755 test/integration/targets/ansible-test-integration-targets/runme.sh create mode 100755 test/integration/targets/ansible-test-integration-targets/test.py create mode 100644 test/integration/targets/ansible-test-integration/aliases create mode 100644 test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/module_utils/my_util.py create mode 100644 test/integration/targets/ansible-test-integration/ansible_collections/ns/col/plugins/modules/hello.py create mode 100644 test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/aliases create mode 100644 test/integration/targets/ansible-test-integration/ansible_collections/ns/col/tests/integration/targets/hello/tasks/main.yml create mode 100755 test/integration/targets/ansible-test-integration/runme.sh create mode 100644 test/integration/targets/ansible-test-no-tty/aliases create mode 100755 test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/run-with-pty.py create mode 100644 test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/aliases create mode 100755 test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/assert-no-tty.py create mode 100755 test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/tests/integration/targets/no-tty/runme.sh create mode 100644 test/integration/targets/ansible-test-no-tty/ansible_collections/ns/col/vendored_pty.py create mode 100755 test/integration/targets/ansible-test-no-tty/runme.sh create mode 100644 test/integration/targets/ansible-test-sanity-ansible-doc/aliases create mode 100644 test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/a/b/lookup2.py create mode 100644 test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/lookup/lookup1.py create mode 100644 test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/a/b/module2.py create mode 100644 test/integration/targets/ansible-test-sanity-ansible-doc/ansible_collections/ns/col/plugins/modules/module1.py create mode 100755 test/integration/targets/ansible-test-sanity-ansible-doc/runme.sh create mode 100644 test/integration/targets/ansible-test-sanity-import/aliases create mode 100644 test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor1.py create mode 100644 test/integration/targets/ansible-test-sanity-import/ansible_collections/ns/col/plugins/lookup/vendor2.py create mode 100755 test/integration/targets/ansible-test-sanity-import/runme.sh create mode 100644 test/integration/targets/ansible-test-sanity-lint/aliases create mode 100644 test/integration/targets/ansible-test-sanity-lint/expected.txt create mode 100755 test/integration/targets/ansible-test-sanity-lint/runme.sh create mode 100644 test/integration/targets/ansible-test-sanity-shebang/aliases create mode 100644 test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/powershell.ps1 create mode 100644 test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/python-no-shebang.py create mode 100644 test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/plugins/modules/python.py create mode 100755 test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/env_bash.sh create mode 100755 test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/env_python.py create mode 100755 test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/scripts/sh.sh create mode 100755 test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/env_bash.sh create mode 100755 test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/env_python.py create mode 100755 test/integration/targets/ansible-test-sanity-shebang/ansible_collections/ns/col/tests/integration/targets/valid/sh.sh create mode 100644 test/integration/targets/ansible-test-sanity-shebang/expected.txt create mode 100755 test/integration/targets/ansible-test-sanity-shebang/runme.sh create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/aliases create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/invalid_yaml_syntax.py create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/no_callable.py create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.py create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/col/plugins/modules/sidecar.yaml create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/README.rst create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/galaxy.yml create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/meta/main.yml create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.ps1 create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/failure/plugins/modules/failure_ps.yml create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/README.rst create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/galaxy.yml create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/meta/runtime.yml create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/module_utils/share_module.psm1 create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/module_utils/validate.psm1 create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/in_function.ps1 create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/in_function.yml create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/sidecar.ps1 create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/sidecar.yml create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.ps1 create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/ansible_collections/ns/ps_only/plugins/modules/validate.py create mode 100644 test/integration/targets/ansible-test-sanity-validate-modules/expected.txt create mode 100755 test/integration/targets/ansible-test-sanity-validate-modules/runme.sh create mode 100644 test/integration/targets/ansible-test-sanity/aliases create mode 100644 test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/README.rst create mode 100644 test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/galaxy.yml create mode 100644 test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/meta/runtime.yml create mode 100644 test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/filter/check_pylint.py create mode 100644 test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/bad.py create mode 100644 test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/lookup/world.py create mode 100644 test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/module_utils/__init__.py create mode 100644 test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/modules/bad.py create mode 100644 test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/plugins/random_directory/bad.py create mode 100644 test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/integration/targets/hello/files/bad.py create mode 100644 test/integration/targets/ansible-test-sanity/ansible_collections/ns/col/tests/sanity/ignore.txt create mode 100755 test/integration/targets/ansible-test-sanity/runme.sh create mode 100644 test/integration/targets/ansible-test-shell/aliases create mode 100644 test/integration/targets/ansible-test-shell/ansible_collections/ns/col/.keep create mode 100644 test/integration/targets/ansible-test-shell/expected-stderr.txt create mode 100644 test/integration/targets/ansible-test-shell/expected-stdout.txt create mode 100755 test/integration/targets/ansible-test-shell/runme.sh create mode 100644 test/integration/targets/ansible-test-units-constraints/aliases create mode 100644 test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/constraints.txt create mode 100644 test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/plugins/modules/test_constraints.py create mode 100644 test/integration/targets/ansible-test-units-constraints/ansible_collections/ns/col/tests/unit/requirements.txt create mode 100755 test/integration/targets/ansible-test-units-constraints/runme.sh create mode 100644 test/integration/targets/ansible-test-units/aliases create mode 100644 test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/module_utils/my_util.py create mode 100644 test/integration/targets/ansible-test-units/ansible_collections/ns/col/plugins/modules/hello.py create mode 100644 test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/module_utils/test_my_util.py create mode 100644 test/integration/targets/ansible-test-units/ansible_collections/ns/col/tests/unit/plugins/modules/test_hello.py create mode 100755 test/integration/targets/ansible-test-units/runme.sh create mode 100644 test/integration/targets/ansible-test-unsupported-directory/aliases create mode 100644 test/integration/targets/ansible-test-unsupported-directory/ansible_collections/ns/col/.keep create mode 100755 test/integration/targets/ansible-test-unsupported-directory/runme.sh create mode 100644 test/integration/targets/ansible-test/aliases create mode 100755 test/integration/targets/ansible-test/venv-pythons.py create mode 100644 test/integration/targets/ansible-vault/aliases create mode 100644 test/integration/targets/ansible-vault/empty-password create mode 100644 test/integration/targets/ansible-vault/encrypted-vault-password create mode 100644 test/integration/targets/ansible-vault/encrypted_file_encrypted_var_password create mode 100644 test/integration/targets/ansible-vault/example1_password create mode 100644 test/integration/targets/ansible-vault/example2_password create mode 100644 test/integration/targets/ansible-vault/example3_password create mode 100755 test/integration/targets/ansible-vault/faux-editor.py create mode 100644 test/integration/targets/ansible-vault/files/test_assemble/nonsecret.txt create mode 100644 test/integration/targets/ansible-vault/files/test_assemble/secret.vault create mode 100644 test/integration/targets/ansible-vault/format_1_1_AES256.yml create mode 100644 test/integration/targets/ansible-vault/format_1_2_AES256.yml create mode 100644 test/integration/targets/ansible-vault/host_vars/myhost.yml create mode 100644 test/integration/targets/ansible-vault/host_vars/testhost.yml create mode 100644 test/integration/targets/ansible-vault/invalid_format/README.md create mode 100644 test/integration/targets/ansible-vault/invalid_format/broken-group-vars-tasks.yml create mode 100644 test/integration/targets/ansible-vault/invalid_format/broken-host-vars-tasks.yml create mode 100644 test/integration/targets/ansible-vault/invalid_format/group_vars/broken-group-vars.yml create mode 100644 test/integration/targets/ansible-vault/invalid_format/host_vars/broken-host-vars.example.com/vars create mode 100644 test/integration/targets/ansible-vault/invalid_format/inventory create mode 100644 test/integration/targets/ansible-vault/invalid_format/original-broken-host-vars create mode 100644 test/integration/targets/ansible-vault/invalid_format/original-group-vars.yml create mode 100644 test/integration/targets/ansible-vault/invalid_format/some-vars create mode 100644 test/integration/targets/ansible-vault/invalid_format/vault-secret create mode 100644 test/integration/targets/ansible-vault/inventory.toml create mode 100755 test/integration/targets/ansible-vault/password-script.py create mode 100644 test/integration/targets/ansible-vault/realpath.yml create mode 100644 test/integration/targets/ansible-vault/roles/test_vault/tasks/main.yml create mode 100644 test/integration/targets/ansible-vault/roles/test_vault/vars/main.yml create mode 100644 test/integration/targets/ansible-vault/roles/test_vault_embedded/tasks/main.yml create mode 100644 test/integration/targets/ansible-vault/roles/test_vault_embedded/vars/main.yml create mode 100644 test/integration/targets/ansible-vault/roles/test_vault_embedded_ids/tasks/main.yml create mode 100644 test/integration/targets/ansible-vault/roles/test_vault_embedded_ids/vars/main.yml create mode 100644 test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/README.md create mode 100644 test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/tasks/main.yml create mode 100644 test/integration/targets/ansible-vault/roles/test_vault_file_encrypted_embedded/vars/main.yml create mode 100644 test/integration/targets/ansible-vault/roles/test_vaulted_template/tasks/main.yml create mode 100644 test/integration/targets/ansible-vault/roles/test_vaulted_template/templates/vaulted_template.j2 create mode 100755 test/integration/targets/ansible-vault/runme.sh create mode 100755 test/integration/targets/ansible-vault/script/vault-secret.sh create mode 100644 test/integration/targets/ansible-vault/single_vault_as_string.yml create mode 100644 test/integration/targets/ansible-vault/symlink.yml create mode 100755 test/integration/targets/ansible-vault/symlink/get-password-symlink create mode 100755 test/integration/targets/ansible-vault/test-vault-client.py create mode 100644 test/integration/targets/ansible-vault/test_dangling_temp.yml create mode 100644 test/integration/targets/ansible-vault/test_utf8_value_in_filename.yml create mode 100644 test/integration/targets/ansible-vault/test_vault.yml create mode 100644 test/integration/targets/ansible-vault/test_vault_embedded.yml create mode 100644 test/integration/targets/ansible-vault/test_vault_embedded_ids.yml create mode 100644 test/integration/targets/ansible-vault/test_vault_file_encrypted_embedded.yml create mode 100644 test/integration/targets/ansible-vault/test_vaulted_inventory.yml create mode 100644 test/integration/targets/ansible-vault/test_vaulted_inventory_toml.yml create mode 100644 test/integration/targets/ansible-vault/test_vaulted_template.yml create mode 100644 test/integration/targets/ansible-vault/test_vaulted_utf8_value.yml create mode 100644 test/integration/targets/ansible-vault/vars/vaulted.yml create mode 100644 "test/integration/targets/ansible-vault/vault-caf\303\251.yml" create mode 100644 test/integration/targets/ansible-vault/vault-password create mode 100644 test/integration/targets/ansible-vault/vault-password-ansible create mode 100644 test/integration/targets/ansible-vault/vault-password-wrong create mode 100644 test/integration/targets/ansible-vault/vault-secret.txt create mode 100644 test/integration/targets/ansible-vault/vaulted.inventory create mode 100644 test/integration/targets/ansible/adhoc-callback.stdout create mode 100644 test/integration/targets/ansible/aliases create mode 100644 "test/integration/targets/ansible/ansible-test\303\251.cfg" create mode 100644 test/integration/targets/ansible/callback_plugins/callback_debug.py create mode 100644 test/integration/targets/ansible/callback_plugins/callback_meta.py create mode 100755 test/integration/targets/ansible/module_common_regex_regression.sh create mode 100644 test/integration/targets/ansible/no-extension create mode 100644 test/integration/targets/ansible/playbook.yml create mode 100644 test/integration/targets/ansible/playbookdir_cfg.ini create mode 100755 test/integration/targets/ansible/runme.sh create mode 100644 test/integration/targets/ansible/vars.yml create mode 100644 test/integration/targets/any_errors_fatal/50897.yml create mode 100644 test/integration/targets/any_errors_fatal/aliases create mode 100644 test/integration/targets/any_errors_fatal/always_block.yml create mode 100644 test/integration/targets/any_errors_fatal/inventory create mode 100644 test/integration/targets/any_errors_fatal/on_includes.yml create mode 100644 test/integration/targets/any_errors_fatal/play_level.yml create mode 100755 test/integration/targets/any_errors_fatal/runme.sh create mode 100644 test/integration/targets/any_errors_fatal/test_fatal.yml create mode 100644 test/integration/targets/apt/aliases create mode 100644 test/integration/targets/apt/defaults/main.yml create mode 100644 test/integration/targets/apt/handlers/main.yml create mode 100644 test/integration/targets/apt/meta/main.yml create mode 100644 test/integration/targets/apt/tasks/apt-builddep.yml create mode 100644 test/integration/targets/apt/tasks/apt-multiarch.yml create mode 100644 test/integration/targets/apt/tasks/apt.yml create mode 100644 test/integration/targets/apt/tasks/downgrade.yml create mode 100644 test/integration/targets/apt/tasks/main.yml create mode 100644 test/integration/targets/apt/tasks/repo.yml create mode 100644 test/integration/targets/apt/tasks/upgrade.yml create mode 100644 test/integration/targets/apt/tasks/url-with-deps.yml create mode 100644 test/integration/targets/apt/vars/Ubuntu-20.yml create mode 100644 test/integration/targets/apt/vars/Ubuntu-22.yml create mode 100644 test/integration/targets/apt/vars/default.yml create mode 100644 test/integration/targets/apt_key/aliases create mode 100644 test/integration/targets/apt_key/meta/main.yml create mode 100644 test/integration/targets/apt_key/tasks/apt_key.yml create mode 100644 test/integration/targets/apt_key/tasks/apt_key_binary.yml create mode 100644 test/integration/targets/apt_key/tasks/apt_key_inline_data.yml create mode 100644 test/integration/targets/apt_key/tasks/file.yml create mode 100644 test/integration/targets/apt_key/tasks/main.yml create mode 100644 test/integration/targets/apt_repository/aliases create mode 100644 test/integration/targets/apt_repository/meta/main.yml create mode 100644 test/integration/targets/apt_repository/tasks/apt.yml create mode 100644 test/integration/targets/apt_repository/tasks/cleanup.yml create mode 100644 test/integration/targets/apt_repository/tasks/main.yml create mode 100644 test/integration/targets/apt_repository/tasks/mode.yaml create mode 100644 test/integration/targets/apt_repository/tasks/mode_cleanup.yaml create mode 100644 test/integration/targets/args/aliases create mode 100755 test/integration/targets/args/runme.sh create mode 100644 test/integration/targets/argspec/aliases create mode 100644 test/integration/targets/argspec/library/argspec.py create mode 100644 test/integration/targets/argspec/tasks/main.yml create mode 100644 test/integration/targets/argspec/tasks/password_no_log.yml create mode 100644 test/integration/targets/assemble/aliases create mode 100644 test/integration/targets/assemble/files/fragment1 create mode 100644 test/integration/targets/assemble/files/fragment2 create mode 100644 test/integration/targets/assemble/files/fragment3 create mode 100644 test/integration/targets/assemble/files/fragment4 create mode 100644 test/integration/targets/assemble/files/fragment5 create mode 100644 test/integration/targets/assemble/meta/main.yml create mode 100644 test/integration/targets/assemble/tasks/main.yml create mode 100644 test/integration/targets/assert/aliases create mode 100644 test/integration/targets/assert/assert_quiet.out.quiet.stderr create mode 100644 test/integration/targets/assert/assert_quiet.out.quiet.stdout create mode 100644 test/integration/targets/assert/inventory create mode 100644 test/integration/targets/assert/quiet.yml create mode 100755 test/integration/targets/assert/runme.sh create mode 100644 test/integration/targets/async/aliases create mode 100644 test/integration/targets/async/callback_test.yml create mode 100644 test/integration/targets/async/library/async_test.py create mode 100644 test/integration/targets/async/meta/main.yml create mode 100644 test/integration/targets/async/tasks/main.yml create mode 100644 test/integration/targets/async_extra_data/aliases create mode 100644 test/integration/targets/async_extra_data/library/junkping.py create mode 100755 test/integration/targets/async_extra_data/runme.sh create mode 100644 test/integration/targets/async_extra_data/test_async.yml create mode 100644 test/integration/targets/async_fail/action_plugins/normal.py create mode 100644 test/integration/targets/async_fail/aliases create mode 100644 test/integration/targets/async_fail/library/async_test.py create mode 100644 test/integration/targets/async_fail/meta/main.yml create mode 100644 test/integration/targets/async_fail/tasks/main.yml create mode 100644 test/integration/targets/become/aliases create mode 100644 test/integration/targets/become/files/copy.txt create mode 100644 test/integration/targets/become/meta/main.yml create mode 100644 test/integration/targets/become/tasks/become.yml create mode 100644 test/integration/targets/become/tasks/main.yml create mode 100644 test/integration/targets/become/vars/main.yml create mode 100644 test/integration/targets/become_su/aliases create mode 100755 test/integration/targets/become_su/runme.sh create mode 100644 test/integration/targets/become_unprivileged/action_plugins/tmpdir.py create mode 100644 test/integration/targets/become_unprivileged/aliases create mode 100644 test/integration/targets/become_unprivileged/chmod_acl_macos/test.yml create mode 100644 test/integration/targets/become_unprivileged/cleanup_unpriv_users.yml create mode 100644 test/integration/targets/become_unprivileged/common_remote_group/cleanup.yml create mode 100644 test/integration/targets/become_unprivileged/common_remote_group/setup.yml create mode 100644 test/integration/targets/become_unprivileged/common_remote_group/test.yml create mode 100644 test/integration/targets/become_unprivileged/inventory create mode 100755 test/integration/targets/become_unprivileged/runme.sh create mode 100644 test/integration/targets/become_unprivileged/setup_unpriv_users.yml create mode 100644 test/integration/targets/binary/aliases create mode 100644 test/integration/targets/binary/files/b64_latin1 create mode 100644 test/integration/targets/binary/files/b64_utf8 create mode 100644 test/integration/targets/binary/files/from_playbook create mode 100644 test/integration/targets/binary/meta/main.yml create mode 100644 test/integration/targets/binary/tasks/main.yml create mode 100644 test/integration/targets/binary/templates/b64_latin1_template.j2 create mode 100644 test/integration/targets/binary/templates/b64_utf8_template.j2 create mode 100644 test/integration/targets/binary/templates/from_playbook_template.j2 create mode 100644 test/integration/targets/binary/vars/main.yml create mode 100644 test/integration/targets/binary_modules/Makefile create mode 100644 test/integration/targets/binary_modules/aliases create mode 100644 test/integration/targets/binary_modules/download_binary_modules.yml create mode 100644 test/integration/targets/binary_modules/group_vars/all create mode 100644 test/integration/targets/binary_modules/library/.gitignore create mode 100644 test/integration/targets/binary_modules/library/helloworld.go create mode 100644 test/integration/targets/binary_modules/roles/test_binary_modules/tasks/main.yml create mode 100755 test/integration/targets/binary_modules/test.sh create mode 100644 test/integration/targets/binary_modules/test_binary_modules.yml create mode 100644 test/integration/targets/binary_modules_posix/aliases create mode 100755 test/integration/targets/binary_modules_posix/runme.sh create mode 100644 test/integration/targets/binary_modules_winrm/aliases create mode 100755 test/integration/targets/binary_modules_winrm/runme.sh create mode 100644 test/integration/targets/blockinfile/aliases create mode 100644 test/integration/targets/blockinfile/files/sshd_config create mode 100644 test/integration/targets/blockinfile/meta/main.yml create mode 100644 test/integration/targets/blockinfile/tasks/add_block_to_existing_file.yml create mode 100644 test/integration/targets/blockinfile/tasks/block_without_trailing_newline.yml create mode 100644 test/integration/targets/blockinfile/tasks/create_file.yml create mode 100644 test/integration/targets/blockinfile/tasks/diff.yml create mode 100644 test/integration/targets/blockinfile/tasks/file_without_trailing_newline.yml create mode 100644 test/integration/targets/blockinfile/tasks/insertafter.yml create mode 100644 test/integration/targets/blockinfile/tasks/insertbefore.yml create mode 100644 test/integration/targets/blockinfile/tasks/main.yml create mode 100644 test/integration/targets/blockinfile/tasks/multiline_search.yml create mode 100644 test/integration/targets/blockinfile/tasks/preserve_line_endings.yml create mode 100644 test/integration/targets/blockinfile/tasks/validate.yml create mode 100644 test/integration/targets/blocks/43191-2.yml create mode 100644 test/integration/targets/blocks/43191.yml create mode 100644 test/integration/targets/blocks/69848.yml create mode 100644 test/integration/targets/blocks/72725.yml create mode 100644 test/integration/targets/blocks/72781.yml create mode 100644 test/integration/targets/blocks/78612.yml create mode 100644 test/integration/targets/blocks/79711.yml create mode 100644 test/integration/targets/blocks/aliases create mode 100644 test/integration/targets/blocks/always_failure_no_rescue_rc.yml create mode 100644 test/integration/targets/blocks/always_failure_with_rescue_rc.yml create mode 100644 test/integration/targets/blocks/always_no_rescue_rc.yml create mode 100644 test/integration/targets/blocks/block_fail.yml create mode 100644 test/integration/targets/blocks/block_fail_tasks.yml create mode 100644 test/integration/targets/blocks/block_in_rescue.yml create mode 100644 test/integration/targets/blocks/block_rescue_vars.yml create mode 100644 test/integration/targets/blocks/fail.yml create mode 100644 test/integration/targets/blocks/finalized_task.yml create mode 100644 test/integration/targets/blocks/inherit_notify.yml create mode 100644 test/integration/targets/blocks/issue29047.yml create mode 100644 test/integration/targets/blocks/issue29047_tasks.yml create mode 100644 test/integration/targets/blocks/issue71306.yml create mode 100644 test/integration/targets/blocks/main.yml create mode 100644 test/integration/targets/blocks/nested_fail.yml create mode 100644 test/integration/targets/blocks/nested_nested_fail.yml create mode 100644 test/integration/targets/blocks/roles/fail/tasks/main.yml create mode 100644 test/integration/targets/blocks/roles/role-69848-1/meta/main.yml create mode 100644 test/integration/targets/blocks/roles/role-69848-2/meta/main.yml create mode 100644 test/integration/targets/blocks/roles/role-69848-3/tasks/main.yml create mode 100755 test/integration/targets/blocks/runme.sh create mode 100644 test/integration/targets/blocks/unsafe_failed_task.yml create mode 100644 test/integration/targets/builtin_vars_prompt/aliases create mode 100755 test/integration/targets/builtin_vars_prompt/runme.sh create mode 100644 test/integration/targets/builtin_vars_prompt/test-vars_prompt.py create mode 100644 test/integration/targets/builtin_vars_prompt/unsafe.yml create mode 100644 test/integration/targets/builtin_vars_prompt/unsupported.yml create mode 100644 test/integration/targets/builtin_vars_prompt/vars_prompt-1.yml create mode 100644 test/integration/targets/builtin_vars_prompt/vars_prompt-2.yml create mode 100644 test/integration/targets/builtin_vars_prompt/vars_prompt-3.yml create mode 100644 test/integration/targets/builtin_vars_prompt/vars_prompt-4.yml create mode 100644 test/integration/targets/builtin_vars_prompt/vars_prompt-5.yml create mode 100644 test/integration/targets/builtin_vars_prompt/vars_prompt-6.yml create mode 100644 test/integration/targets/builtin_vars_prompt/vars_prompt-7.yml create mode 100644 test/integration/targets/callback_default/aliases create mode 100644 test/integration/targets/callback_default/callback_default.out.check_markers_dry.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.check_markers_dry.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.check_markers_wet.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.check_markers_wet.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.check_nomarkers_dry.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.check_nomarkers_dry.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.check_nomarkers_wet.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.check_nomarkers_wet.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.default.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.default.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.display_path_on_failure.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.display_path_on_failure.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.failed_to_stderr.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.failed_to_stderr.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.fqcn_free.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.free.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.hide_ok.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.hide_ok.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.hide_skipped.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.hide_skipped.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.hide_skipped_ok.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.hide_skipped_ok.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.host_pinned.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.result_format_yaml.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.result_format_yaml.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.result_format_yaml_lossy_verbose.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.result_format_yaml_verbose.stdout create mode 100644 test/integration/targets/callback_default/callback_default.out.yaml_result_format_yaml_verbose.stderr create mode 100644 test/integration/targets/callback_default/callback_default.out.yaml_result_format_yaml_verbose.stdout create mode 100644 test/integration/targets/callback_default/include_me.yml create mode 100644 test/integration/targets/callback_default/inventory create mode 100644 test/integration/targets/callback_default/no_implicit_meta_banners.yml create mode 100755 test/integration/targets/callback_default/runme.sh create mode 100644 test/integration/targets/callback_default/test.yml create mode 100644 test/integration/targets/callback_default/test_2.yml create mode 100644 test/integration/targets/callback_default/test_async.yml create mode 100644 test/integration/targets/callback_default/test_dryrun.yml create mode 100644 test/integration/targets/callback_default/test_non_lockstep.yml create mode 100644 test/integration/targets/callback_default/test_yaml.yml create mode 100644 test/integration/targets/changed_when/aliases create mode 100644 test/integration/targets/changed_when/meta/main.yml create mode 100644 test/integration/targets/changed_when/tasks/main.yml create mode 100644 test/integration/targets/check_mode/aliases create mode 100644 test/integration/targets/check_mode/check_mode-not-on-cli.yml create mode 100644 test/integration/targets/check_mode/check_mode-on-cli.yml create mode 100644 test/integration/targets/check_mode/check_mode.yml create mode 100644 test/integration/targets/check_mode/roles/test_always_run/meta/main.yml create mode 100644 test/integration/targets/check_mode/roles/test_always_run/tasks/main.yml create mode 100644 test/integration/targets/check_mode/roles/test_check_mode/files/foo.txt create mode 100644 test/integration/targets/check_mode/roles/test_check_mode/tasks/main.yml create mode 100644 test/integration/targets/check_mode/roles/test_check_mode/templates/foo.j2 create mode 100644 test/integration/targets/check_mode/roles/test_check_mode/vars/main.yml create mode 100755 test/integration/targets/check_mode/runme.sh create mode 100644 test/integration/targets/cli/aliases create mode 100755 test/integration/targets/cli/runme.sh create mode 100644 test/integration/targets/cli/setup.yml create mode 100644 test/integration/targets/cli/test-cli.py create mode 100644 test/integration/targets/cli/test_k_and_K.py create mode 100644 test/integration/targets/cli/test_syntax/files/vaultsecret create mode 100644 test/integration/targets/cli/test_syntax/group_vars/all/testvault.yml create mode 100644 test/integration/targets/cli/test_syntax/roles/some_role/tasks/main.yml create mode 100644 test/integration/targets/cli/test_syntax/syntax_check.yml create mode 100644 test/integration/targets/collection/aliases create mode 100755 test/integration/targets/collection/setup.sh create mode 100755 test/integration/targets/collection/update-ignore.py create mode 100644 test/integration/targets/collections/a.statichost.yml create mode 100644 test/integration/targets/collections/aliases create mode 100644 test/integration/targets/collections/ansiballz_dupe/collections/ansible_collections/duplicate/name/plugins/modules/ping.py create mode 100644 test/integration/targets/collections/ansiballz_dupe/test_ansiballz_cache_dupe_shortname.yml create mode 100644 test/integration/targets/collections/cache.statichost.yml create mode 100644 test/integration/targets/collections/check_populated_inventory.yml create mode 100644 test/integration/targets/collections/collection_root_sys/ansible_collections/testns/coll_in_sys/plugins/modules/systestmodule.py create mode 100644 test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/plugins/modules/maskedmodule.py create mode 100644 test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/plugins/modules/testmodule.py create mode 100644 test/integration/targets/collections/collection_root_sys/ansible_collections/testns/testcoll/roles/maskedrole/tasks/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/ansible/builtin/plugins/modules/ping.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/ansible/bullcoll/plugins/modules/bullmodule.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/othercoll/plugins/module_utils/formerly_testcoll_pkg/__init__.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/othercoll/plugins/module_utils/formerly_testcoll_pkg/submod.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testbroken/plugins/filter/broken_filter.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/meta/runtime.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/default_collection_playbook.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/play.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/library/embedded_module.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role/tasks/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/roles/non_coll_role_to_call/tasks/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/type/play.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/playbooks/type/subtype/play.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/action_subdir/subdir_ping_action.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/bypass_host_loop.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/plugin_lookup.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/subclassed_normal.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/action/uses_redirected_import.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/callback/usercallback.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/connection/localconn.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/doc_fragments/frag.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/filter_subdir/my_subdir_filters.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/myfilters.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/filter/myfilters2.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/lookup_subdir/my_subdir_lookup.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/mylookup.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/lookup/mylookup2.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/AnotherCSMU.cs create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMU.cs create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyCSMUOptional.cs create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMU.psm1 create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/MyPSMUOptional.psm1 create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/base.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/leaf.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/__init__.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/nested_same/__init__.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/nested_same/nested_same/nested_same.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/secondary.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/__init__.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/subcs.cs create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/submod.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg/subps.psm1 create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init/__init__.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/module_utils/subpkg_with_init/mod_in_subpkg_with_init.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/deprecated_ping.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/module_subdir/subdir_ping_module.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/ping.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/testmodule.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/testmodule_bad_docfrags.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_base_mu_granular_nested_import.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_collection_redirected_mu.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_core_redirected_mu.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.bak create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_flat_import.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_granular_import.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_leaf_mu_module_import_from.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_collection.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_mu_missing_redirect_module.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_nested_same_as_func.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/uses_nested_same_as_module.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_csbasic_only.ps1 create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_selfcontained.ps1 create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_selfcontained.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_coll_csmu.ps1 create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_coll_psmu.ps1 create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/modules/win_uses_optional.ps1 create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/mytests.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/mytests2.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/test/test_subdir/my_subdir_tests.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/plugins/vars/custom_vars.py create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/call_standalone/tasks/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/calls_intra_collection_dep_role_unqualified/meta/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/calls_intra_collection_dep_role_unqualified/tasks/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/common_handlers/handlers/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/role_subdir/subdir_testrole/tasks/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/test_fqcn_handlers/meta/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/test_fqcn_handlers/tasks/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole/meta/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole/tasks/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole_main_yaml/meta/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testcoll/roles/testrole_main_yaml/tasks/main.yml create mode 100644 test/integration/targets/collections/collection_root_user/ansible_collections/testns/testredirect/meta/runtime.yml create mode 100644 test/integration/targets/collections/collections/ansible_collections/me/mycoll1/plugins/action/action1.py create mode 100644 test/integration/targets/collections/collections/ansible_collections/me/mycoll1/plugins/modules/action1.py create mode 100644 test/integration/targets/collections/collections/ansible_collections/me/mycoll2/plugins/modules/module1.py create mode 100644 test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/cache/custom_jsonfile.py create mode 100644 test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/inventory/statichost.py create mode 100644 test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/__init__.py create mode 100644 test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/sub1/__init__.py create mode 100644 test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/module_utils/sub1/foomodule.py create mode 100644 test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/modules/contentadjmodule.py create mode 100644 test/integration/targets/collections/collections/ansible_collections/testns/content_adj/plugins/vars/custom_adj_vars.py create mode 100644 test/integration/targets/collections/custom_vars_plugins/v1_vars_plugin.py create mode 100644 test/integration/targets/collections/custom_vars_plugins/v2_vars_plugin.py create mode 100644 test/integration/targets/collections/filter_plugins/override_formerly_core_masked_filter.py create mode 100644 test/integration/targets/collections/import_collection_pb.yml create mode 100644 test/integration/targets/collections/includeme.yml create mode 100644 test/integration/targets/collections/inventory_test.yml create mode 100644 test/integration/targets/collections/invocation_tests.yml create mode 100644 test/integration/targets/collections/library/ping.py create mode 100644 test/integration/targets/collections/noop.yml create mode 100644 test/integration/targets/collections/posix.yml create mode 100644 test/integration/targets/collections/redirected.statichost.yml create mode 100644 test/integration/targets/collections/roles/standalone/tasks/main.yml create mode 100644 test/integration/targets/collections/roles/testrole/tasks/main.yml create mode 100755 test/integration/targets/collections/runme.sh create mode 100644 test/integration/targets/collections/test_bypass_host_loop.yml create mode 100644 test/integration/targets/collections/test_collection_meta.yml create mode 100644 test/integration/targets/collections/test_plugins/override_formerly_core_masked_test.py create mode 100644 test/integration/targets/collections/test_redirect_list.yml create mode 100755 test/integration/targets/collections/test_task_resolved_plugin.sh create mode 100644 test/integration/targets/collections/test_task_resolved_plugin/action_plugins/legacy_action.py create mode 100644 test/integration/targets/collections/test_task_resolved_plugin/callback_plugins/display_resolved_action.py create mode 100644 test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/meta/runtime.yml create mode 100644 test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/plugins/action/collection_action.py create mode 100644 test/integration/targets/collections/test_task_resolved_plugin/collections/ansible_collections/test_ns/test_coll/plugins/modules/collection_module.py create mode 100644 test/integration/targets/collections/test_task_resolved_plugin/fqcn.yml create mode 100644 test/integration/targets/collections/test_task_resolved_plugin/library/legacy_module.py create mode 100644 test/integration/targets/collections/test_task_resolved_plugin/unqualified.yml create mode 100644 test/integration/targets/collections/test_task_resolved_plugin/unqualified_and_collections_kw.yml create mode 100644 test/integration/targets/collections/testcoll2/MANIFEST.json create mode 100644 test/integration/targets/collections/testcoll2/plugins/modules/testmodule2.py create mode 100755 test/integration/targets/collections/vars_plugin_tests.sh create mode 100644 test/integration/targets/collections/windows.yml create mode 100644 test/integration/targets/collections_plugin_namespace/aliases create mode 100644 test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/filter/test_filter.py create mode 100644 test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/lookup/lookup_name.py create mode 100644 test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/lookup/lookup_no_future_boilerplate.py create mode 100644 test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/plugins/test/test_test.py create mode 100644 test/integration/targets/collections_plugin_namespace/collection_root/ansible_collections/my_ns/my_col/roles/test/tasks/main.yml create mode 100755 test/integration/targets/collections_plugin_namespace/runme.sh create mode 100644 test/integration/targets/collections_plugin_namespace/test.yml create mode 100644 test/integration/targets/collections_relative_imports/aliases create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel1.psm1 create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/PSRel4.psm1 create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util1.py create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util2.py create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/my_util3.py create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/module_utils/sub_pkg/PSRel2.psm1 create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/my_module.py create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative.ps1 create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/plugins/modules/win_relative_optional.ps1 create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col/roles/test/tasks/main.yml create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col2/plugins/module_utils/PSRel3.psm1 create mode 100644 test/integration/targets/collections_relative_imports/collection_root/ansible_collections/my_ns/my_col2/plugins/module_utils/sub_pkg/CSRel4.cs create mode 100755 test/integration/targets/collections_relative_imports/runme.sh create mode 100644 test/integration/targets/collections_relative_imports/test.yml create mode 100644 test/integration/targets/collections_relative_imports/windows.yml create mode 100644 test/integration/targets/collections_runtime_pythonpath/aliases create mode 100644 test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/ansible_collections/python/dist/plugins/modules/boo.py create mode 100644 test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/pyproject.toml create mode 100644 test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-boo/setup.cfg create mode 100644 test/integration/targets/collections_runtime_pythonpath/ansible-collection-python-dist-foo/ansible_collections/python/dist/plugins/modules/boo.py create mode 100755 test/integration/targets/collections_runtime_pythonpath/runme.sh create mode 100644 test/integration/targets/command_nonexisting/aliases create mode 100644 test/integration/targets/command_nonexisting/tasks/main.yml create mode 100644 test/integration/targets/command_shell/aliases create mode 100755 test/integration/targets/command_shell/files/create_afile.sh create mode 100755 test/integration/targets/command_shell/files/remove_afile.sh create mode 100755 test/integration/targets/command_shell/files/test.sh create mode 100644 test/integration/targets/command_shell/meta/main.yml create mode 100644 test/integration/targets/command_shell/tasks/main.yml create mode 100644 test/integration/targets/common_network/aliases create mode 100644 test/integration/targets/common_network/tasks/main.yml create mode 100644 test/integration/targets/common_network/test_plugins/is_mac.py create mode 100644 test/integration/targets/conditionals/aliases create mode 100644 test/integration/targets/conditionals/play.yml create mode 100755 test/integration/targets/conditionals/runme.sh create mode 100644 test/integration/targets/conditionals/vars/main.yml create mode 100644 test/integration/targets/config/aliases create mode 100644 test/integration/targets/config/files/types.env create mode 100644 test/integration/targets/config/files/types.ini create mode 100644 test/integration/targets/config/files/types.vars create mode 100644 test/integration/targets/config/files/types_dump.txt create mode 100644 test/integration/targets/config/inline_comment_ansible.cfg create mode 100644 test/integration/targets/config/lookup_plugins/bogus.py create mode 100644 test/integration/targets/config/lookup_plugins/types.py create mode 100755 test/integration/targets/config/runme.sh create mode 100644 test/integration/targets/config/type_munging.cfg create mode 100644 test/integration/targets/config/types.yml create mode 100644 test/integration/targets/config/validation.yml create mode 100644 test/integration/targets/connection/aliases create mode 100755 test/integration/targets/connection/test.sh create mode 100644 test/integration/targets/connection/test_connection.yml create mode 100644 test/integration/targets/connection/test_reset_connection.yml create mode 100644 test/integration/targets/connection_delegation/action_plugins/delegation_action.py create mode 100644 test/integration/targets/connection_delegation/aliases create mode 100644 test/integration/targets/connection_delegation/connection_plugins/delegation_connection.py create mode 100644 test/integration/targets/connection_delegation/inventory.ini create mode 100755 test/integration/targets/connection_delegation/runme.sh create mode 100644 test/integration/targets/connection_delegation/test.yml create mode 100644 test/integration/targets/connection_local/aliases create mode 100755 test/integration/targets/connection_local/runme.sh create mode 100644 test/integration/targets/connection_local/test_connection.inventory create mode 100644 test/integration/targets/connection_paramiko_ssh/aliases create mode 100755 test/integration/targets/connection_paramiko_ssh/runme.sh create mode 100755 test/integration/targets/connection_paramiko_ssh/test.sh create mode 100644 test/integration/targets/connection_paramiko_ssh/test_connection.inventory create mode 100644 test/integration/targets/connection_psrp/aliases create mode 100644 test/integration/targets/connection_psrp/files/empty.txt create mode 100755 test/integration/targets/connection_psrp/runme.sh create mode 100644 test/integration/targets/connection_psrp/test_connection.inventory.j2 create mode 100644 test/integration/targets/connection_psrp/tests.yml create mode 100644 test/integration/targets/connection_remote_is_local/aliases create mode 100644 test/integration/targets/connection_remote_is_local/connection_plugins/remote_is_local.py create mode 100644 test/integration/targets/connection_remote_is_local/tasks/main.yml create mode 100644 test/integration/targets/connection_remote_is_local/test.yml create mode 100644 test/integration/targets/connection_ssh/aliases create mode 100644 test/integration/targets/connection_ssh/check_ssh_defaults.yml create mode 100644 test/integration/targets/connection_ssh/files/port_overrride_ssh.cfg create mode 100755 test/integration/targets/connection_ssh/posix.sh create mode 100755 test/integration/targets/connection_ssh/runme.sh create mode 100644 test/integration/targets/connection_ssh/test_connection.inventory create mode 100644 test/integration/targets/connection_ssh/test_ssh_defaults.cfg create mode 100644 test/integration/targets/connection_ssh/verify_config.yml create mode 100644 test/integration/targets/connection_windows_ssh/aliases create mode 100755 test/integration/targets/connection_windows_ssh/runme.sh create mode 100644 test/integration/targets/connection_windows_ssh/test_connection.inventory.j2 create mode 100644 test/integration/targets/connection_windows_ssh/tests.yml create mode 100644 test/integration/targets/connection_windows_ssh/tests_fetch.yml create mode 100755 test/integration/targets/connection_windows_ssh/windows.sh create mode 100644 test/integration/targets/connection_winrm/aliases create mode 100755 test/integration/targets/connection_winrm/runme.sh create mode 100644 test/integration/targets/connection_winrm/test_connection.inventory.j2 create mode 100644 test/integration/targets/connection_winrm/tests.yml create mode 100644 test/integration/targets/controller/aliases create mode 100644 test/integration/targets/controller/tasks/main.yml create mode 100644 test/integration/targets/copy/aliases create mode 100644 test/integration/targets/copy/defaults/main.yml create mode 100644 test/integration/targets/copy/files-different/vault/folder/nested-vault-file create mode 100644 test/integration/targets/copy/files-different/vault/readme.txt create mode 100644 test/integration/targets/copy/files-different/vault/vault-file create mode 100644 test/integration/targets/copy/files/foo.txt create mode 100644 test/integration/targets/copy/files/subdir/bar.txt create mode 100644 test/integration/targets/copy/files/subdir/subdir1/empty.txt create mode 100644 test/integration/targets/copy/files/subdir/subdir2/baz.txt create mode 100644 test/integration/targets/copy/files/subdir/subdir2/subdir3/subdir4/qux.txt create mode 100644 test/integration/targets/copy/meta/main.yml create mode 100644 test/integration/targets/copy/tasks/acls.yml create mode 100644 test/integration/targets/copy/tasks/check_mode.yml create mode 100644 test/integration/targets/copy/tasks/dest_in_non_existent_directories.yml create mode 100644 test/integration/targets/copy/tasks/dest_in_non_existent_directories_remote_src.yml create mode 100644 test/integration/targets/copy/tasks/main.yml create mode 100644 test/integration/targets/copy/tasks/no_log.yml create mode 100644 test/integration/targets/copy/tasks/selinux.yml create mode 100644 test/integration/targets/copy/tasks/src_file_dest_file_in_non_existent_dir.yml create mode 100644 test/integration/targets/copy/tasks/src_file_dest_file_in_non_existent_dir_remote_src.yml create mode 100644 test/integration/targets/copy/tasks/src_remote_file_is_not_file.yml create mode 100644 test/integration/targets/copy/tasks/tests.yml create mode 100644 test/integration/targets/cron/aliases create mode 100644 test/integration/targets/cron/defaults/main.yml create mode 100644 test/integration/targets/cron/meta/main.yml create mode 100644 test/integration/targets/cron/tasks/main.yml create mode 100644 test/integration/targets/cron/vars/alpine.yml create mode 100644 test/integration/targets/cron/vars/default.yml create mode 100644 test/integration/targets/dataloader/aliases create mode 100644 test/integration/targets/dataloader/attempt_to_load_invalid_json.yml create mode 100755 test/integration/targets/dataloader/runme.sh create mode 100644 test/integration/targets/dataloader/vars/invalid.json create mode 100644 test/integration/targets/debconf/aliases create mode 100644 test/integration/targets/debconf/meta/main.yml create mode 100644 test/integration/targets/debconf/tasks/main.yml create mode 100644 test/integration/targets/debug/aliases create mode 100644 test/integration/targets/debug/main.yml create mode 100644 test/integration/targets/debug/main_fqcn.yml create mode 100644 test/integration/targets/debug/nosetfacts.yml create mode 100755 test/integration/targets/debug/runme.sh create mode 100644 test/integration/targets/debugger/aliases create mode 100644 test/integration/targets/debugger/inventory create mode 100755 test/integration/targets/debugger/runme.sh create mode 100755 test/integration/targets/debugger/test_run_once.py create mode 100644 test/integration/targets/debugger/test_run_once_playbook.yml create mode 100644 test/integration/targets/delegate_to/aliases create mode 100644 test/integration/targets/delegate_to/connection_plugins/fakelocal.py create mode 100644 test/integration/targets/delegate_to/delegate_and_nolog.yml create mode 100644 test/integration/targets/delegate_to/delegate_facts_block.yml create mode 100644 test/integration/targets/delegate_to/delegate_facts_loop.yml create mode 100644 test/integration/targets/delegate_to/delegate_local_from_root.yml create mode 100644 test/integration/targets/delegate_to/delegate_to_lookup_context.yml create mode 100644 test/integration/targets/delegate_to/delegate_vars_hanldling.yml create mode 100644 test/integration/targets/delegate_to/delegate_with_fact_from_delegate_host.yml create mode 100644 test/integration/targets/delegate_to/discovery_applied.yml create mode 100644 test/integration/targets/delegate_to/files/testfile create mode 100644 test/integration/targets/delegate_to/has_hostvars.yml create mode 100644 test/integration/targets/delegate_to/inventory create mode 100644 test/integration/targets/delegate_to/inventory_interpreters create mode 100644 test/integration/targets/delegate_to/library/detect_interpreter.py create mode 100644 test/integration/targets/delegate_to/resolve_vars.yml create mode 100644 test/integration/targets/delegate_to/roles/delegate_to_lookup_context/tasks/main.yml create mode 100644 test/integration/targets/delegate_to/roles/delegate_to_lookup_context/templates/one.j2 create mode 100644 test/integration/targets/delegate_to/roles/delegate_to_lookup_context/templates/two.j2 create mode 100644 test/integration/targets/delegate_to/roles/test_template/templates/foo.j2 create mode 100755 test/integration/targets/delegate_to/runme.sh create mode 100644 test/integration/targets/delegate_to/test_delegate_to.yml create mode 100644 test/integration/targets/delegate_to/test_delegate_to_lookup_context.yml create mode 100644 test/integration/targets/delegate_to/test_delegate_to_loop_caching.yml create mode 100644 test/integration/targets/delegate_to/test_delegate_to_loop_randomness.yml create mode 100644 test/integration/targets/delegate_to/test_loop_control.yml create mode 100644 test/integration/targets/delegate_to/verify_interpreter.yml create mode 100644 test/integration/targets/dict_transformations/aliases create mode 100644 test/integration/targets/dict_transformations/library/convert_camelCase.py create mode 100644 test/integration/targets/dict_transformations/library/convert_snake_case.py create mode 100644 test/integration/targets/dict_transformations/tasks/main.yml create mode 100644 test/integration/targets/dict_transformations/tasks/test_convert_camelCase.yml create mode 100644 test/integration/targets/dict_transformations/tasks/test_convert_snake_case.yml create mode 100644 test/integration/targets/dnf/aliases create mode 100644 test/integration/targets/dnf/meta/main.yml create mode 100644 test/integration/targets/dnf/tasks/cacheonly.yml create mode 100644 test/integration/targets/dnf/tasks/dnf.yml create mode 100644 test/integration/targets/dnf/tasks/dnfinstallroot.yml create mode 100644 test/integration/targets/dnf/tasks/dnfreleasever.yml create mode 100644 test/integration/targets/dnf/tasks/filters.yml create mode 100644 test/integration/targets/dnf/tasks/filters_check_mode.yml create mode 100644 test/integration/targets/dnf/tasks/gpg.yml create mode 100644 test/integration/targets/dnf/tasks/logging.yml create mode 100644 test/integration/targets/dnf/tasks/main.yml create mode 100644 test/integration/targets/dnf/tasks/modularity.yml create mode 100644 test/integration/targets/dnf/tasks/repo.yml create mode 100644 test/integration/targets/dnf/tasks/skip_broken_and_nobest.yml create mode 100644 test/integration/targets/dnf/tasks/test_sos_removal.yml create mode 100644 test/integration/targets/dnf/vars/CentOS.yml create mode 100644 test/integration/targets/dnf/vars/Fedora.yml create mode 100644 test/integration/targets/dnf/vars/RedHat-9.yml create mode 100644 test/integration/targets/dnf/vars/RedHat.yml create mode 100644 test/integration/targets/dnf/vars/main.yml create mode 100644 test/integration/targets/dpkg_selections/aliases create mode 100644 test/integration/targets/dpkg_selections/defaults/main.yaml create mode 100644 test/integration/targets/dpkg_selections/tasks/dpkg_selections.yaml create mode 100644 test/integration/targets/dpkg_selections/tasks/main.yaml create mode 100644 test/integration/targets/egg-info/aliases create mode 100644 test/integration/targets/egg-info/lookup_plugins/import_pkg_resources.py create mode 100644 test/integration/targets/egg-info/tasks/main.yml create mode 100644 test/integration/targets/embedded_module/aliases create mode 100644 test/integration/targets/embedded_module/library/test_integration_module create mode 100644 test/integration/targets/embedded_module/tasks/main.yml create mode 100644 test/integration/targets/entry_points/aliases create mode 100755 test/integration/targets/entry_points/runme.sh create mode 100644 test/integration/targets/environment/aliases create mode 100755 test/integration/targets/environment/runme.sh create mode 100644 test/integration/targets/environment/test_environment.yml create mode 100644 test/integration/targets/error_from_connection/aliases create mode 100644 test/integration/targets/error_from_connection/connection_plugins/dummy.py create mode 100644 test/integration/targets/error_from_connection/inventory create mode 100644 test/integration/targets/error_from_connection/play.yml create mode 100755 test/integration/targets/error_from_connection/runme.sh create mode 100644 test/integration/targets/expect/aliases create mode 100644 test/integration/targets/expect/files/foo.txt create mode 100644 test/integration/targets/expect/files/test_command.py create mode 100644 test/integration/targets/expect/meta/main.yml create mode 100644 test/integration/targets/expect/tasks/main.yml create mode 100644 test/integration/targets/facts_d/aliases create mode 100644 test/integration/targets/facts_d/files/basdscript.fact create mode 100644 test/integration/targets/facts_d/files/goodscript.fact create mode 100644 test/integration/targets/facts_d/files/preferences.fact create mode 100644 test/integration/targets/facts_d/files/unreadable.fact create mode 100644 test/integration/targets/facts_d/meta/main.yml create mode 100644 test/integration/targets/facts_d/tasks/main.yml create mode 100644 test/integration/targets/facts_linux_network/aliases create mode 100644 test/integration/targets/facts_linux_network/meta/main.yml create mode 100644 test/integration/targets/facts_linux_network/tasks/main.yml create mode 100644 test/integration/targets/failed_when/aliases create mode 100644 test/integration/targets/failed_when/tasks/main.yml create mode 100644 test/integration/targets/fetch/aliases create mode 100644 test/integration/targets/fetch/cleanup.yml create mode 100644 test/integration/targets/fetch/injection/avoid_slurp_return.yml create mode 100644 test/integration/targets/fetch/injection/here.txt create mode 100644 test/integration/targets/fetch/injection/library/slurp.py create mode 100644 test/integration/targets/fetch/roles/fetch_tests/defaults/main.yml create mode 100644 test/integration/targets/fetch/roles/fetch_tests/handlers/main.yml create mode 100644 test/integration/targets/fetch/roles/fetch_tests/meta/main.yml create mode 100644 test/integration/targets/fetch/roles/fetch_tests/tasks/fail_on_missing.yml create mode 100644 test/integration/targets/fetch/roles/fetch_tests/tasks/failures.yml create mode 100644 test/integration/targets/fetch/roles/fetch_tests/tasks/main.yml create mode 100644 test/integration/targets/fetch/roles/fetch_tests/tasks/normal.yml create mode 100644 test/integration/targets/fetch/roles/fetch_tests/tasks/setup.yml create mode 100644 test/integration/targets/fetch/roles/fetch_tests/tasks/symlink.yml create mode 100644 test/integration/targets/fetch/roles/fetch_tests/vars/Darwin.yml create mode 100644 test/integration/targets/fetch/roles/fetch_tests/vars/default.yml create mode 100644 test/integration/targets/fetch/run_fetch_tests.yml create mode 100755 test/integration/targets/fetch/runme.sh create mode 100644 test/integration/targets/fetch/setup_unreadable_test.yml create mode 100644 test/integration/targets/fetch/test_unreadable_with_stat.yml create mode 100644 test/integration/targets/file/aliases create mode 100644 test/integration/targets/file/defaults/main.yml create mode 100644 test/integration/targets/file/files/foo.txt create mode 100644 test/integration/targets/file/files/foobar/directory/fileC create mode 100644 test/integration/targets/file/files/foobar/directory/fileD create mode 100644 test/integration/targets/file/files/foobar/fileA create mode 100644 test/integration/targets/file/files/foobar/fileB create mode 100644 test/integration/targets/file/handlers/main.yml create mode 100644 test/integration/targets/file/meta/main.yml create mode 100644 test/integration/targets/file/tasks/diff_peek.yml create mode 100644 test/integration/targets/file/tasks/directory_as_dest.yml create mode 100644 test/integration/targets/file/tasks/initialize.yml create mode 100644 test/integration/targets/file/tasks/link_rewrite.yml create mode 100644 test/integration/targets/file/tasks/main.yml create mode 100644 test/integration/targets/file/tasks/modification_time.yml create mode 100644 test/integration/targets/file/tasks/selinux_tests.yml create mode 100644 test/integration/targets/file/tasks/state_link.yml create mode 100644 test/integration/targets/file/tasks/unicode_path.yml create mode 100644 test/integration/targets/filter_core/aliases create mode 100644 test/integration/targets/filter_core/files/9851.txt create mode 100644 test/integration/targets/filter_core/files/fileglob/one.txt create mode 100644 test/integration/targets/filter_core/files/fileglob/two.txt create mode 100644 test/integration/targets/filter_core/files/foo.txt create mode 100644 test/integration/targets/filter_core/handle_undefined_type_errors.yml create mode 100644 test/integration/targets/filter_core/host_vars/localhost create mode 100644 test/integration/targets/filter_core/meta/main.yml create mode 100755 test/integration/targets/filter_core/runme.sh create mode 100644 test/integration/targets/filter_core/runme.yml create mode 100644 test/integration/targets/filter_core/tasks/main.yml create mode 100644 test/integration/targets/filter_core/templates/foo.j2 create mode 100644 test/integration/targets/filter_core/templates/py26json.j2 create mode 100644 test/integration/targets/filter_core/vars/main.yml create mode 100644 test/integration/targets/filter_encryption/aliases create mode 100644 test/integration/targets/filter_encryption/base.yml create mode 100755 test/integration/targets/filter_encryption/runme.sh create mode 100644 test/integration/targets/filter_mathstuff/aliases create mode 100644 test/integration/targets/filter_mathstuff/host_vars/localhost.yml create mode 100755 test/integration/targets/filter_mathstuff/runme.sh create mode 100644 test/integration/targets/filter_mathstuff/runme.yml create mode 100644 test/integration/targets/filter_mathstuff/tasks/main.yml create mode 100644 test/integration/targets/filter_mathstuff/vars/defined_later.yml create mode 100644 test/integration/targets/filter_mathstuff/vars/main.yml create mode 100644 test/integration/targets/filter_urls/aliases create mode 100644 test/integration/targets/filter_urls/tasks/main.yml create mode 100644 test/integration/targets/filter_urlsplit/aliases create mode 100644 test/integration/targets/filter_urlsplit/tasks/main.yml create mode 100644 test/integration/targets/find/aliases create mode 100644 test/integration/targets/find/files/a.txt create mode 100644 test/integration/targets/find/files/log.txt create mode 100644 test/integration/targets/find/meta/main.yml create mode 100644 test/integration/targets/find/tasks/main.yml create mode 100644 test/integration/targets/fork_safe_stdio/aliases create mode 100644 test/integration/targets/fork_safe_stdio/callback_plugins/spewstdio.py create mode 100644 test/integration/targets/fork_safe_stdio/hosts create mode 100755 test/integration/targets/fork_safe_stdio/run-with-pty.py create mode 100755 test/integration/targets/fork_safe_stdio/runme.sh create mode 100644 test/integration/targets/fork_safe_stdio/test.yml create mode 100644 test/integration/targets/fork_safe_stdio/vendored_pty.py create mode 100644 test/integration/targets/gathering/aliases create mode 100644 test/integration/targets/gathering/explicit.yml create mode 100644 test/integration/targets/gathering/implicit.yml create mode 100755 test/integration/targets/gathering/runme.sh create mode 100644 test/integration/targets/gathering/smart.yml create mode 100644 test/integration/targets/gathering/uuid.fact create mode 100644 test/integration/targets/gathering_facts/aliases create mode 100644 test/integration/targets/gathering_facts/cache_plugins/none.py create mode 100644 test/integration/targets/gathering_facts/collections/ansible_collections/cisco/ios/plugins/modules/ios_facts.py create mode 100644 test/integration/targets/gathering_facts/inventory create mode 100644 test/integration/targets/gathering_facts/library/bogus_facts create mode 100644 test/integration/targets/gathering_facts/library/facts_one create mode 100644 test/integration/targets/gathering_facts/library/facts_two create mode 100644 test/integration/targets/gathering_facts/library/file_utils.py create mode 100644 test/integration/targets/gathering_facts/one_two.json create mode 100644 test/integration/targets/gathering_facts/prevent_clobbering.yml create mode 100755 test/integration/targets/gathering_facts/runme.sh create mode 100644 test/integration/targets/gathering_facts/test_gathering_facts.yml create mode 100644 test/integration/targets/gathering_facts/test_module_defaults.yml create mode 100644 test/integration/targets/gathering_facts/test_prevent_injection.yml create mode 100644 test/integration/targets/gathering_facts/test_run_once.yml create mode 100644 test/integration/targets/gathering_facts/two_one.json create mode 100644 test/integration/targets/gathering_facts/uuid.fact create mode 100644 test/integration/targets/gathering_facts/verify_merge_facts.yml create mode 100644 test/integration/targets/gathering_facts/verify_subset.yml create mode 100644 test/integration/targets/get_url/aliases create mode 100644 test/integration/targets/get_url/files/testserver.py create mode 100644 test/integration/targets/get_url/meta/main.yml create mode 100644 test/integration/targets/get_url/tasks/ciphers.yml create mode 100644 test/integration/targets/get_url/tasks/main.yml create mode 100644 test/integration/targets/get_url/tasks/use_gssapi.yml create mode 100644 test/integration/targets/get_url/tasks/use_netrc.yml create mode 100644 test/integration/targets/getent/aliases create mode 100644 test/integration/targets/getent/meta/main.yml create mode 100644 test/integration/targets/getent/tasks/main.yml create mode 100644 test/integration/targets/git/aliases create mode 100644 test/integration/targets/git/handlers/cleanup-default.yml create mode 100644 test/integration/targets/git/handlers/cleanup-freebsd.yml create mode 100644 test/integration/targets/git/handlers/main.yml create mode 100644 test/integration/targets/git/meta/main.yml create mode 100644 test/integration/targets/git/tasks/ambiguous-ref.yml create mode 100644 test/integration/targets/git/tasks/archive.yml create mode 100644 test/integration/targets/git/tasks/change-repo-url.yml create mode 100644 test/integration/targets/git/tasks/checkout-new-tag.yml create mode 100644 test/integration/targets/git/tasks/depth.yml create mode 100644 test/integration/targets/git/tasks/forcefully-fetch-tag.yml create mode 100644 test/integration/targets/git/tasks/formats.yml create mode 100644 test/integration/targets/git/tasks/gpg-verification.yml create mode 100644 test/integration/targets/git/tasks/localmods.yml create mode 100644 test/integration/targets/git/tasks/main.yml create mode 100644 test/integration/targets/git/tasks/missing_hostkey.yml create mode 100644 test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml create mode 100644 test/integration/targets/git/tasks/no-destination.yml create mode 100644 test/integration/targets/git/tasks/reset-origin.yml create mode 100644 test/integration/targets/git/tasks/separate-git-dir.yml create mode 100644 test/integration/targets/git/tasks/setup-local-repos.yml create mode 100644 test/integration/targets/git/tasks/setup.yml create mode 100644 test/integration/targets/git/tasks/single-branch.yml create mode 100644 test/integration/targets/git/tasks/specific-revision.yml create mode 100644 test/integration/targets/git/tasks/submodules.yml create mode 100644 test/integration/targets/git/vars/main.yml create mode 100644 test/integration/targets/group/aliases create mode 100644 test/integration/targets/group/files/gidget.py create mode 100644 test/integration/targets/group/files/grouplist.sh create mode 100644 test/integration/targets/group/meta/main.yml create mode 100644 test/integration/targets/group/tasks/main.yml create mode 100644 test/integration/targets/group/tasks/tests.yml create mode 100644 test/integration/targets/group_by/aliases create mode 100644 test/integration/targets/group_by/create_groups.yml create mode 100644 test/integration/targets/group_by/group_vars/all create mode 100644 test/integration/targets/group_by/group_vars/camelus create mode 100644 test/integration/targets/group_by/group_vars/vicugna create mode 100644 test/integration/targets/group_by/inventory.group_by create mode 100755 test/integration/targets/group_by/runme.sh create mode 100644 test/integration/targets/group_by/test_group_by.yml create mode 100644 test/integration/targets/group_by/test_group_by_skipped.yml create mode 100644 test/integration/targets/groupby_filter/aliases create mode 100644 test/integration/targets/groupby_filter/tasks/main.yml create mode 100644 test/integration/targets/handler_race/aliases create mode 100644 test/integration/targets/handler_race/inventory create mode 100644 test/integration/targets/handler_race/roles/do_handlers/handlers/main.yml create mode 100644 test/integration/targets/handler_race/roles/do_handlers/tasks/main.yml create mode 100644 test/integration/targets/handler_race/roles/more_sleep/tasks/main.yml create mode 100644 test/integration/targets/handler_race/roles/random_sleep/tasks/main.yml create mode 100755 test/integration/targets/handler_race/runme.sh create mode 100644 test/integration/targets/handler_race/test_handler_race.yml create mode 100644 test/integration/targets/handlers/46447.yml create mode 100644 test/integration/targets/handlers/52561.yml create mode 100644 test/integration/targets/handlers/54991.yml create mode 100644 test/integration/targets/handlers/58841.yml create mode 100644 test/integration/targets/handlers/79776-handlers.yml create mode 100644 test/integration/targets/handlers/79776.yml create mode 100644 test/integration/targets/handlers/aliases create mode 100644 test/integration/targets/handlers/from_handlers.yml create mode 100644 test/integration/targets/handlers/handlers.yml create mode 100644 test/integration/targets/handlers/include_handlers_fail_force-handlers.yml create mode 100644 test/integration/targets/handlers/include_handlers_fail_force.yml create mode 100644 test/integration/targets/handlers/inventory.handlers create mode 100644 test/integration/targets/handlers/order.yml create mode 100644 test/integration/targets/handlers/roles/import_template_handler_names/tasks/main.yml create mode 100644 test/integration/targets/handlers/roles/template_handler_names/handlers/main.yml create mode 100644 test/integration/targets/handlers/roles/template_handler_names/tasks/evaluation_time.yml create mode 100644 test/integration/targets/handlers/roles/template_handler_names/tasks/lazy_evaluation.yml create mode 100644 test/integration/targets/handlers/roles/test_force_handlers/handlers/main.yml create mode 100644 test/integration/targets/handlers/roles/test_force_handlers/tasks/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers/handlers/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers/meta/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers/tasks/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers_include/handlers/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers_include/tasks/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers_include_role/handlers/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers_include_role/meta/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers_include_role/tasks/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers_listen/handlers/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers_listen/tasks/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers_meta/handlers/alternate.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers_meta/handlers/main.yml create mode 100644 test/integration/targets/handlers/roles/test_handlers_meta/tasks/main.yml create mode 100644 test/integration/targets/handlers/roles/test_role_handlers_include_tasks/handlers/A.yml create mode 100644 test/integration/targets/handlers/roles/test_role_handlers_include_tasks/handlers/main.yml create mode 100644 test/integration/targets/handlers/roles/test_role_handlers_include_tasks/tasks/B.yml create mode 100644 test/integration/targets/handlers/roles/test_templating_in_handlers/handlers/main.yml create mode 100644 test/integration/targets/handlers/roles/test_templating_in_handlers/tasks/main.yml create mode 100755 test/integration/targets/handlers/runme.sh create mode 100644 test/integration/targets/handlers/test_block_as_handler-import.yml create mode 100644 test/integration/targets/handlers/test_block_as_handler-include.yml create mode 100644 test/integration/targets/handlers/test_block_as_handler-include_import-handlers.yml create mode 100644 test/integration/targets/handlers/test_block_as_handler.yml create mode 100644 test/integration/targets/handlers/test_flush_handlers_as_handler.yml create mode 100644 test/integration/targets/handlers/test_flush_handlers_rescue_always.yml create mode 100644 test/integration/targets/handlers/test_flush_in_rescue_always.yml create mode 100644 test/integration/targets/handlers/test_force_handlers.yml create mode 100644 test/integration/targets/handlers/test_fqcn_meta_flush_handlers.yml create mode 100644 test/integration/targets/handlers/test_handlers.yml create mode 100644 test/integration/targets/handlers/test_handlers_any_errors_fatal.yml create mode 100644 test/integration/targets/handlers/test_handlers_include.yml create mode 100644 test/integration/targets/handlers/test_handlers_include_role.yml create mode 100644 test/integration/targets/handlers/test_handlers_including_task.yml create mode 100644 test/integration/targets/handlers/test_handlers_inexistent_notify.yml create mode 100644 test/integration/targets/handlers/test_handlers_infinite_loop.yml create mode 100644 test/integration/targets/handlers/test_handlers_listen.yml create mode 100644 test/integration/targets/handlers/test_handlers_meta.yml create mode 100644 test/integration/targets/handlers/test_handlers_template_run_once.yml create mode 100644 test/integration/targets/handlers/test_listening_handlers.yml create mode 100644 test/integration/targets/handlers/test_notify_included-handlers.yml create mode 100644 test/integration/targets/handlers/test_notify_included.yml create mode 100644 test/integration/targets/handlers/test_role_as_handler.yml create mode 100644 test/integration/targets/handlers/test_role_handlers_including_tasks.yml create mode 100644 test/integration/targets/handlers/test_skip_flush.yml create mode 100644 test/integration/targets/handlers/test_templating_in_handlers.yml create mode 100644 test/integration/targets/hardware_facts/aliases create mode 100644 test/integration/targets/hardware_facts/meta/main.yml create mode 100644 test/integration/targets/hardware_facts/tasks/Linux.yml create mode 100644 test/integration/targets/hardware_facts/tasks/main.yml create mode 100644 test/integration/targets/hash/aliases create mode 100644 test/integration/targets/hash/group_vars/all create mode 100644 test/integration/targets/hash/host_vars/testhost create mode 100644 test/integration/targets/hash/roles/test_hash_behaviour/defaults/main.yml create mode 100644 test/integration/targets/hash/roles/test_hash_behaviour/meta/main.yml create mode 100644 test/integration/targets/hash/roles/test_hash_behaviour/tasks/main.yml create mode 100644 test/integration/targets/hash/roles/test_hash_behaviour/vars/main.yml create mode 100755 test/integration/targets/hash/runme.sh create mode 100644 test/integration/targets/hash/test_hash.yml create mode 100644 test/integration/targets/hash/test_inv1.yml create mode 100644 test/integration/targets/hash/test_inv2.yml create mode 100644 test/integration/targets/hash/test_inventory_hash.yml create mode 100644 test/integration/targets/hash/vars/test_hash_vars.yml create mode 100644 test/integration/targets/hostname/aliases create mode 100644 test/integration/targets/hostname/tasks/Debian.yml create mode 100644 test/integration/targets/hostname/tasks/MacOSX.yml create mode 100644 test/integration/targets/hostname/tasks/RedHat.yml create mode 100644 test/integration/targets/hostname/tasks/check_mode.yml create mode 100644 test/integration/targets/hostname/tasks/default.yml create mode 100644 test/integration/targets/hostname/tasks/main.yml create mode 100644 test/integration/targets/hostname/tasks/test_check_mode.yml create mode 100644 test/integration/targets/hostname/tasks/test_normal.yml create mode 100644 test/integration/targets/hostname/vars/FreeBSD.yml create mode 100644 test/integration/targets/hostname/vars/RedHat.yml create mode 100644 test/integration/targets/hostname/vars/default.yml create mode 100644 test/integration/targets/hosts_field/aliases create mode 100644 test/integration/targets/hosts_field/inventory.hosts_field create mode 100755 test/integration/targets/hosts_field/runme.sh create mode 100644 test/integration/targets/hosts_field/test_hosts_field.json create mode 100644 test/integration/targets/hosts_field/test_hosts_field.yml create mode 100644 test/integration/targets/ignore_errors/aliases create mode 100644 test/integration/targets/ignore_errors/meta/main.yml create mode 100644 test/integration/targets/ignore_errors/tasks/main.yml create mode 100644 test/integration/targets/ignore_unreachable/aliases create mode 100644 test/integration/targets/ignore_unreachable/fake_connectors/bad_exec.py create mode 100644 test/integration/targets/ignore_unreachable/fake_connectors/bad_put_file.py create mode 100644 test/integration/targets/ignore_unreachable/inventory create mode 100644 test/integration/targets/ignore_unreachable/meta/main.yml create mode 100755 test/integration/targets/ignore_unreachable/runme.sh create mode 100644 test/integration/targets/ignore_unreachable/test_base_cannot_connect.yml create mode 100644 test/integration/targets/ignore_unreachable/test_cannot_connect.yml create mode 100644 test/integration/targets/ignore_unreachable/test_with_bad_plugins.yml create mode 100644 test/integration/targets/import_tasks/aliases create mode 100644 test/integration/targets/import_tasks/inherit_notify.yml create mode 100755 test/integration/targets/import_tasks/runme.sh create mode 100644 test/integration/targets/import_tasks/tasks/trigger_change.yml create mode 100644 test/integration/targets/incidental_ios_file/aliases create mode 100644 test/integration/targets/incidental_ios_file/defaults/main.yaml create mode 100644 test/integration/targets/incidental_ios_file/ios1.cfg create mode 100644 test/integration/targets/incidental_ios_file/nonascii.bin create mode 100644 test/integration/targets/incidental_ios_file/tasks/cli.yaml create mode 100644 test/integration/targets/incidental_ios_file/tasks/main.yaml create mode 100644 test/integration/targets/incidental_ios_file/tests/cli/net_get.yaml create mode 100644 test/integration/targets/incidental_ios_file/tests/cli/net_put.yaml create mode 100644 test/integration/targets/incidental_vyos_config/aliases create mode 100644 test/integration/targets/incidental_vyos_config/defaults/main.yaml create mode 100644 test/integration/targets/incidental_vyos_config/tasks/cli.yaml create mode 100644 test/integration/targets/incidental_vyos_config/tasks/cli_config.yaml create mode 100644 test/integration/targets/incidental_vyos_config/tasks/main.yaml create mode 100644 test/integration/targets/incidental_vyos_config/tests/cli/backup.yaml create mode 100644 test/integration/targets/incidental_vyos_config/tests/cli/check_config.yaml create mode 100644 test/integration/targets/incidental_vyos_config/tests/cli/comment.yaml create mode 100644 test/integration/targets/incidental_vyos_config/tests/cli/config.cfg create mode 100644 test/integration/targets/incidental_vyos_config/tests/cli/save.yaml create mode 100644 test/integration/targets/incidental_vyos_config/tests/cli/simple.yaml create mode 100644 test/integration/targets/incidental_vyos_config/tests/cli_config/cli_backup.yaml create mode 100644 test/integration/targets/incidental_vyos_config/tests/cli_config/cli_basic.yaml create mode 100644 test/integration/targets/incidental_vyos_config/tests/cli_config/cli_comment.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/aliases create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/defaults/main.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/meta/main.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/tasks/cli.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/tasks/main.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_populate.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_populate_intf.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/_remove_config.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/deleted.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/empty_config.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/merged.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/overridden.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/replaced.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/tests/cli/rtt.yaml create mode 100644 test/integration/targets/incidental_vyos_lldp_interfaces/vars/main.yaml create mode 100644 test/integration/targets/incidental_vyos_prepare_tests/aliases create mode 100644 test/integration/targets/incidental_vyos_prepare_tests/tasks/main.yaml create mode 100644 test/integration/targets/incidental_win_reboot/aliases create mode 100644 test/integration/targets/incidental_win_reboot/tasks/main.yml create mode 100644 test/integration/targets/incidental_win_reboot/templates/post_reboot.ps1 create mode 100644 test/integration/targets/include_import/aliases create mode 100644 test/integration/targets/include_import/apply/import_apply.yml create mode 100644 test/integration/targets/include_import/apply/include_apply.yml create mode 100644 test/integration/targets/include_import/apply/include_apply_65710.yml create mode 100644 test/integration/targets/include_import/apply/include_tasks.yml create mode 100644 test/integration/targets/include_import/apply/roles/include_role/tasks/main.yml create mode 100644 test/integration/targets/include_import/apply/roles/include_role2/tasks/main.yml create mode 100644 test/integration/targets/include_import/empty_group_warning/playbook.yml create mode 100644 test/integration/targets/include_import/empty_group_warning/tasks.yml create mode 100644 test/integration/targets/include_import/grandchild/block_include_tasks.yml create mode 100644 test/integration/targets/include_import/grandchild/import.yml create mode 100644 test/integration/targets/include_import/grandchild/import_include_include_tasks.yml create mode 100644 test/integration/targets/include_import/grandchild/include_level_1.yml create mode 100644 test/integration/targets/include_import/handler_addressing/playbook.yml create mode 100644 test/integration/targets/include_import/handler_addressing/roles/import_handler_test/handlers/main.yml create mode 100644 test/integration/targets/include_import/handler_addressing/roles/import_handler_test/tasks/handlers.yml create mode 100644 test/integration/targets/include_import/handler_addressing/roles/import_handler_test/tasks/main.yml create mode 100644 test/integration/targets/include_import/handler_addressing/roles/include_handler_test/handlers/main.yml create mode 100644 test/integration/targets/include_import/handler_addressing/roles/include_handler_test/tasks/handlers.yml create mode 100644 test/integration/targets/include_import/handler_addressing/roles/include_handler_test/tasks/main.yml create mode 100644 test/integration/targets/include_import/include_role_omit/playbook.yml create mode 100644 test/integration/targets/include_import/include_role_omit/roles/foo/tasks/main.yml create mode 100644 test/integration/targets/include_import/inventory create mode 100644 test/integration/targets/include_import/issue73657.yml create mode 100644 test/integration/targets/include_import/issue73657_tasks.yml create mode 100644 test/integration/targets/include_import/nestedtasks/nested/nested.yml create mode 100644 test/integration/targets/include_import/parent_templating/playbook.yml create mode 100644 test/integration/targets/include_import/parent_templating/roles/test/tasks/localhost.yml create mode 100644 test/integration/targets/include_import/parent_templating/roles/test/tasks/main.yml create mode 100644 test/integration/targets/include_import/parent_templating/roles/test/tasks/other.yml create mode 100644 test/integration/targets/include_import/playbook/group_vars/all.yml create mode 100644 test/integration/targets/include_import/playbook/playbook1.yml create mode 100644 test/integration/targets/include_import/playbook/playbook2.yml create mode 100644 test/integration/targets/include_import/playbook/playbook3.yml create mode 100644 test/integration/targets/include_import/playbook/playbook4.yml create mode 100644 test/integration/targets/include_import/playbook/playbook_needing_vars.yml create mode 100644 test/integration/targets/include_import/playbook/roles/import_playbook_role/tasks/main.yml create mode 100644 test/integration/targets/include_import/playbook/sub_playbook/library/helloworld.py create mode 100644 test/integration/targets/include_import/playbook/sub_playbook/sub_playbook.yml create mode 100644 test/integration/targets/include_import/playbook/test_import_playbook.yml create mode 100644 test/integration/targets/include_import/playbook/test_import_playbook_tags.yml create mode 100644 test/integration/targets/include_import/playbook/test_templated_filenames.yml create mode 100644 test/integration/targets/include_import/playbook/validate1.yml create mode 100644 test/integration/targets/include_import/playbook/validate2.yml create mode 100644 test/integration/targets/include_import/playbook/validate34.yml create mode 100644 test/integration/targets/include_import/playbook/validate_tags.yml create mode 100644 test/integration/targets/include_import/playbook/validate_templated_playbook.yml create mode 100644 test/integration/targets/include_import/playbook/validate_templated_tasks.yml create mode 100644 test/integration/targets/include_import/public_exposure/no_bleeding.yml create mode 100644 test/integration/targets/include_import/public_exposure/no_overwrite_roles.yml create mode 100644 test/integration/targets/include_import/public_exposure/playbook.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/call_import/tasks/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/dynamic/defaults/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/dynamic/tasks/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/dynamic/vars/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/dynamic_private/defaults/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/dynamic_private/tasks/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/dynamic_private/vars/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/from/defaults/from.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/from/tasks/from.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/from/vars/from.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/regular/defaults/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/regular/tasks/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/regular/vars/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/static/defaults/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/static/tasks/main.yml create mode 100644 test/integration/targets/include_import/public_exposure/roles/static/vars/main.yml create mode 100644 test/integration/targets/include_import/role/test_import_role.yml create mode 100644 test/integration/targets/include_import/role/test_include_role.yml create mode 100644 test/integration/targets/include_import/role/test_include_role_vars_from.yml create mode 100644 test/integration/targets/include_import/roles/delegated_handler/handlers/main.yml create mode 100644 test/integration/targets/include_import/roles/delegated_handler/tasks/main.yml create mode 100644 test/integration/targets/include_import/roles/dup_allowed_role/meta/main.yml create mode 100644 test/integration/targets/include_import/roles/dup_allowed_role/tasks/main.yml create mode 100644 test/integration/targets/include_import/roles/loop_name_assert/tasks/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/defaults/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/meta/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/tasks/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/tasks/rund.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2/vars/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/defaults/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/meta/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/tasks/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/tasks/rune.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2a/vars/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/defaults/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/meta/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/tasks/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/tasks/runf.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested/nested_dep_role2b/vars/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested_dep_role/defaults/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested_dep_role/meta/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested_dep_role/tasks/main.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested_dep_role/tasks/runc.yml create mode 100644 test/integration/targets/include_import/roles/nested/nested_dep_role/vars/main.yml create mode 100644 test/integration/targets/include_import/roles/nested_include_task/meta/main.yml create mode 100644 test/integration/targets/include_import/roles/nested_include_task/tasks/main.yml create mode 100644 test/integration/targets/include_import/roles/nested_include_task/tasks/runa.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/canary1.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/canary2.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/canary3.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/fail.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/main.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t01.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t02.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t03.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t04.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t05.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t06.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t07.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t08.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t09.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t10.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t11.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/r1t12.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/tasks.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/templated.yml create mode 100644 test/integration/targets/include_import/roles/role1/tasks/vartest.yml create mode 100644 test/integration/targets/include_import/roles/role1/vars/main.yml create mode 100644 test/integration/targets/include_import/roles/role1/vars/role1vars.yml create mode 100644 test/integration/targets/include_import/roles/role2/tasks/main.yml create mode 100644 test/integration/targets/include_import/roles/role3/defaults/main.yml create mode 100644 test/integration/targets/include_import/roles/role3/handlers/main.yml create mode 100644 test/integration/targets/include_import/roles/role3/tasks/main.yml create mode 100644 test/integration/targets/include_import/roles/role3/tasks/tasks.yml create mode 100644 test/integration/targets/include_import/roles/role3/tasks/vartest.yml create mode 100644 test/integration/targets/include_import/roles/role3/vars/main.yml create mode 100644 test/integration/targets/include_import/roles/role3/vars/role3vars.yml create mode 100644 test/integration/targets/include_import/roles/role_with_deps/meta/main.yml create mode 100644 test/integration/targets/include_import/roles/role_with_deps/tasks/main.yml create mode 100644 test/integration/targets/include_import/run_once/include_me.yml create mode 100644 test/integration/targets/include_import/run_once/playbook.yml create mode 100755 test/integration/targets/include_import/runme.sh create mode 100644 test/integration/targets/include_import/tasks/debug_item.yml create mode 100644 test/integration/targets/include_import/tasks/hello/.gitignore create mode 100644 test/integration/targets/include_import/tasks/hello/keep create mode 100644 test/integration/targets/include_import/tasks/nested/nested.yml create mode 100644 test/integration/targets/include_import/tasks/tasks1.yml create mode 100644 test/integration/targets/include_import/tasks/tasks2.yml create mode 100644 test/integration/targets/include_import/tasks/tasks3.yml create mode 100644 test/integration/targets/include_import/tasks/tasks4.yml create mode 100644 test/integration/targets/include_import/tasks/tasks5.yml create mode 100644 test/integration/targets/include_import/tasks/tasks6.yml create mode 100644 test/integration/targets/include_import/tasks/test_allow_single_role_dup.yml create mode 100644 test/integration/targets/include_import/tasks/test_import_tasks.yml create mode 100644 test/integration/targets/include_import/tasks/test_import_tasks_tags.yml create mode 100644 test/integration/targets/include_import/tasks/test_include_dupe_loop.yml create mode 100644 test/integration/targets/include_import/tasks/test_include_tasks.yml create mode 100644 test/integration/targets/include_import/tasks/test_include_tasks_tags.yml create mode 100644 test/integration/targets/include_import/tasks/test_recursion.yml create mode 100644 test/integration/targets/include_import/tasks/validate3.yml create mode 100644 test/integration/targets/include_import/tasks/validate_tags.yml create mode 100644 test/integration/targets/include_import/test_copious_include_tasks.yml create mode 100644 test/integration/targets/include_import/test_copious_include_tasks_fqcn.yml create mode 100644 test/integration/targets/include_import/test_grandparent_inheritance.yml create mode 100644 test/integration/targets/include_import/test_grandparent_inheritance_fqcn.yml create mode 100644 test/integration/targets/include_import/test_include_loop.yml create mode 100644 test/integration/targets/include_import/test_include_loop_fqcn.yml create mode 100644 test/integration/targets/include_import/test_loop_var_bleed.yaml create mode 100644 test/integration/targets/include_import/test_nested_tasks.yml create mode 100644 test/integration/targets/include_import/test_nested_tasks_fqcn.yml create mode 100644 test/integration/targets/include_import/test_role_recursion.yml create mode 100644 test/integration/targets/include_import/test_role_recursion_fqcn.yml create mode 100644 test/integration/targets/include_import/undefined_var/include_tasks.yml create mode 100644 test/integration/targets/include_import/undefined_var/include_that_defines_var.yml create mode 100644 test/integration/targets/include_import/undefined_var/playbook.yml create mode 100644 test/integration/targets/include_import/valid_include_keywords/include_me.yml create mode 100644 test/integration/targets/include_import/valid_include_keywords/include_me_listen.yml create mode 100644 test/integration/targets/include_import/valid_include_keywords/include_me_notify.yml create mode 100644 test/integration/targets/include_import/valid_include_keywords/playbook.yml create mode 100644 test/integration/targets/include_import_tasks_nested/aliases create mode 100644 test/integration/targets/include_import_tasks_nested/tasks/main.yml create mode 100644 test/integration/targets/include_import_tasks_nested/tasks/nested/nested_adjacent.yml create mode 100644 test/integration/targets/include_import_tasks_nested/tasks/nested/nested_import.yml create mode 100644 test/integration/targets/include_import_tasks_nested/tasks/nested/nested_include.yml create mode 100644 test/integration/targets/include_parent_role_vars/aliases create mode 100644 test/integration/targets/include_parent_role_vars/tasks/included_by_other_role.yml create mode 100644 test/integration/targets/include_parent_role_vars/tasks/included_by_ourselves.yml create mode 100644 test/integration/targets/include_parent_role_vars/tasks/main.yml create mode 100644 test/integration/targets/include_vars-ad-hoc/aliases create mode 100644 test/integration/targets/include_vars-ad-hoc/dir/inc.yml create mode 100755 test/integration/targets/include_vars-ad-hoc/runme.sh create mode 100644 test/integration/targets/include_vars/aliases create mode 100644 test/integration/targets/include_vars/defaults/main.yml create mode 100644 test/integration/targets/include_vars/tasks/main.yml create mode 100644 test/integration/targets/include_vars/vars/all/all.yml create mode 100644 test/integration/targets/include_vars/vars/environments/development/all.yml create mode 100644 test/integration/targets/include_vars/vars/environments/development/services/webapp.yml create mode 100644 test/integration/targets/include_vars/vars/no_auto_unsafe.yml create mode 100644 test/integration/targets/include_vars/vars/services/service_vars.yml create mode 100644 test/integration/targets/include_vars/vars/services/service_vars_fqcn.yml create mode 100644 test/integration/targets/include_vars/vars/services/webapp.yml create mode 100644 test/integration/targets/include_vars/vars/webapp/file_without_extension create mode 100644 test/integration/targets/include_vars/vars2/hashes/hash1.yml create mode 100644 test/integration/targets/include_vars/vars2/hashes/hash2.yml create mode 100644 test/integration/targets/include_when_parent_is_dynamic/aliases create mode 100644 test/integration/targets/include_when_parent_is_dynamic/playbook.yml create mode 100755 test/integration/targets/include_when_parent_is_dynamic/runme.sh create mode 100644 test/integration/targets/include_when_parent_is_dynamic/syntax_error.yml create mode 100644 test/integration/targets/include_when_parent_is_dynamic/tasks.yml create mode 100644 test/integration/targets/include_when_parent_is_static/aliases create mode 100644 test/integration/targets/include_when_parent_is_static/playbook.yml create mode 100755 test/integration/targets/include_when_parent_is_static/runme.sh create mode 100644 test/integration/targets/include_when_parent_is_static/syntax_error.yml create mode 100644 test/integration/targets/include_when_parent_is_static/tasks.yml create mode 100644 test/integration/targets/includes/aliases create mode 100644 test/integration/targets/includes/include_on_playbook_should_fail.yml create mode 100644 test/integration/targets/includes/includes_loop_rescue.yml create mode 100644 test/integration/targets/includes/inherit_notify.yml create mode 100644 test/integration/targets/includes/roles/test_includes/handlers/main.yml create mode 100644 test/integration/targets/includes/roles/test_includes/handlers/more_handlers.yml create mode 100644 test/integration/targets/includes/roles/test_includes/tasks/branch_toplevel.yml create mode 100644 test/integration/targets/includes/roles/test_includes/tasks/empty.yml create mode 100644 test/integration/targets/includes/roles/test_includes/tasks/included_task1.yml create mode 100644 test/integration/targets/includes/roles/test_includes/tasks/leaf_sublevel.yml create mode 100644 test/integration/targets/includes/roles/test_includes/tasks/main.yml create mode 100644 test/integration/targets/includes/roles/test_includes/tasks/not_a_role_task.yml create mode 100644 test/integration/targets/includes/roles/test_includes_free/tasks/inner.yml create mode 100644 test/integration/targets/includes/roles/test_includes_free/tasks/inner_fqcn.yml create mode 100644 test/integration/targets/includes/roles/test_includes_free/tasks/main.yml create mode 100644 test/integration/targets/includes/roles/test_includes_host_pinned/tasks/inner.yml create mode 100644 test/integration/targets/includes/roles/test_includes_host_pinned/tasks/main.yml create mode 100755 test/integration/targets/includes/runme.sh create mode 100644 test/integration/targets/includes/tasks/trigger_change.yml create mode 100644 test/integration/targets/includes/test_include_free.yml create mode 100644 test/integration/targets/includes/test_include_host_pinned.yml create mode 100644 test/integration/targets/includes/test_includes.yml create mode 100644 test/integration/targets/includes/test_includes2.yml create mode 100644 test/integration/targets/includes/test_includes3.yml create mode 100644 test/integration/targets/includes/test_includes4.yml create mode 100644 test/integration/targets/includes_race/aliases create mode 100644 test/integration/targets/includes_race/inventory create mode 100644 test/integration/targets/includes_race/roles/random_sleep/tasks/main.yml create mode 100644 test/integration/targets/includes_race/roles/set_a_fact/tasks/fact1.yml create mode 100644 test/integration/targets/includes_race/roles/set_a_fact/tasks/fact2.yml create mode 100755 test/integration/targets/includes_race/runme.sh create mode 100644 test/integration/targets/includes_race/test_includes_race.yml create mode 100644 test/integration/targets/infra/aliases create mode 100644 test/integration/targets/infra/inventory.local create mode 100644 test/integration/targets/infra/library/test.py create mode 100755 test/integration/targets/infra/runme.sh create mode 100644 test/integration/targets/infra/test_test_infra.yml create mode 100644 test/integration/targets/interpreter_discovery_python/aliases create mode 100644 test/integration/targets/interpreter_discovery_python/library/test_echo_module.py create mode 100644 test/integration/targets/interpreter_discovery_python/tasks/main.yml create mode 100644 test/integration/targets/interpreter_discovery_python_delegate_facts/aliases create mode 100644 test/integration/targets/interpreter_discovery_python_delegate_facts/delegate_facts.yml create mode 100644 test/integration/targets/interpreter_discovery_python_delegate_facts/inventory create mode 100755 test/integration/targets/interpreter_discovery_python_delegate_facts/runme.sh create mode 100644 test/integration/targets/inventory-invalid-group/aliases create mode 100644 test/integration/targets/inventory-invalid-group/inventory.ini create mode 100755 test/integration/targets/inventory-invalid-group/runme.sh create mode 100644 test/integration/targets/inventory-invalid-group/test.yml create mode 100644 test/integration/targets/inventory/1/2/3/extra_vars_relative.yml create mode 100644 test/integration/targets/inventory/1/2/inventory.yml create mode 100644 test/integration/targets/inventory/1/vars.yml create mode 100644 test/integration/targets/inventory/aliases create mode 100644 test/integration/targets/inventory/extra_vars_constructed.yml create mode 100644 test/integration/targets/inventory/host_vars_constructed.yml create mode 100644 test/integration/targets/inventory/inv_with_host_vars.yml create mode 100644 test/integration/targets/inventory/inv_with_int.yml create mode 100644 test/integration/targets/inventory/inventory_plugins/contructed_with_hostvars.py create mode 100644 test/integration/targets/inventory/playbook.yml create mode 100755 test/integration/targets/inventory/runme.sh create mode 100644 test/integration/targets/inventory/strategy.yml create mode 100644 test/integration/targets/inventory/test_empty.yml create mode 100644 test/integration/targets/inventory_advanced_host_list/aliases create mode 100755 test/integration/targets/inventory_advanced_host_list/runme.sh create mode 100644 test/integration/targets/inventory_advanced_host_list/test_advanced_host_list.yml create mode 100644 test/integration/targets/inventory_cache/aliases create mode 100644 test/integration/targets/inventory_cache/cache/.keep create mode 100644 test/integration/targets/inventory_cache/cache_host.yml create mode 100644 test/integration/targets/inventory_cache/exercise_cache.yml create mode 100644 test/integration/targets/inventory_cache/plugins/inventory/cache_host.py create mode 100644 test/integration/targets/inventory_cache/plugins/inventory/exercise_cache.py create mode 100755 test/integration/targets/inventory_cache/runme.sh create mode 100644 test/integration/targets/inventory_constructed/aliases create mode 100644 test/integration/targets/inventory_constructed/constructed.yml create mode 100644 test/integration/targets/inventory_constructed/invs/1/group_vars/stuff.yml create mode 100644 test/integration/targets/inventory_constructed/invs/1/host_vars/testing.yml create mode 100644 test/integration/targets/inventory_constructed/invs/1/one.yml create mode 100644 test/integration/targets/inventory_constructed/invs/2/constructed.yml create mode 100644 test/integration/targets/inventory_constructed/keyed_group_default_value.yml create mode 100644 test/integration/targets/inventory_constructed/keyed_group_list_default_value.yml create mode 100644 test/integration/targets/inventory_constructed/keyed_group_str_default_value.yml create mode 100644 test/integration/targets/inventory_constructed/keyed_group_trailing_separator.yml create mode 100644 test/integration/targets/inventory_constructed/no_leading_separator_constructed.yml create mode 100755 test/integration/targets/inventory_constructed/runme.sh create mode 100644 test/integration/targets/inventory_constructed/static_inventory.yml create mode 100644 test/integration/targets/inventory_constructed/tag_inventory.yml create mode 100644 test/integration/targets/inventory_ini/aliases create mode 100644 test/integration/targets/inventory_ini/inventory.ini create mode 100755 test/integration/targets/inventory_ini/runme.sh create mode 100644 test/integration/targets/inventory_ini/test_ansible_become.yml create mode 100644 test/integration/targets/inventory_script/aliases create mode 100644 test/integration/targets/inventory_script/inventory.json create mode 100755 test/integration/targets/inventory_script/inventory.sh create mode 100755 test/integration/targets/inventory_script/runme.sh create mode 100644 test/integration/targets/inventory_yaml/aliases create mode 100644 test/integration/targets/inventory_yaml/empty.json create mode 100755 test/integration/targets/inventory_yaml/runme.sh create mode 100644 test/integration/targets/inventory_yaml/success.json create mode 100644 test/integration/targets/inventory_yaml/test.yml create mode 100644 test/integration/targets/inventory_yaml/test_int_hostname.yml create mode 100644 test/integration/targets/iptables/aliases create mode 100644 test/integration/targets/iptables/tasks/chain_management.yml create mode 100644 test/integration/targets/iptables/tasks/main.yml create mode 100644 test/integration/targets/iptables/vars/alpine.yml create mode 100644 test/integration/targets/iptables/vars/centos.yml create mode 100644 test/integration/targets/iptables/vars/default.yml create mode 100644 test/integration/targets/iptables/vars/fedora.yml create mode 100644 test/integration/targets/iptables/vars/redhat.yml create mode 100644 test/integration/targets/iptables/vars/suse.yml create mode 100644 test/integration/targets/jinja2_native_types/aliases create mode 100644 test/integration/targets/jinja2_native_types/nested_undefined.yml create mode 100755 test/integration/targets/jinja2_native_types/runme.sh create mode 100644 test/integration/targets/jinja2_native_types/runtests.yml create mode 100644 test/integration/targets/jinja2_native_types/test_bool.yml create mode 100644 test/integration/targets/jinja2_native_types/test_casting.yml create mode 100644 test/integration/targets/jinja2_native_types/test_concatentation.yml create mode 100644 test/integration/targets/jinja2_native_types/test_dunder.yml create mode 100644 test/integration/targets/jinja2_native_types/test_hostvars.yml create mode 100644 test/integration/targets/jinja2_native_types/test_none.yml create mode 100644 test/integration/targets/jinja2_native_types/test_preserving_quotes.yml create mode 100644 test/integration/targets/jinja2_native_types/test_template.yml create mode 100644 test/integration/targets/jinja2_native_types/test_template_newlines.j2 create mode 100644 test/integration/targets/jinja2_native_types/test_types.yml create mode 100644 test/integration/targets/jinja2_native_types/test_vault.yml create mode 100644 test/integration/targets/jinja2_native_types/test_vault_pass create mode 100644 test/integration/targets/jinja_plugins/aliases create mode 100644 test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/bad_collection_filter.py create mode 100644 test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/bad_collection_filter2.py create mode 100644 test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/filter/good_collection_filter.py create mode 100644 test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/test/bad_collection_test.py create mode 100644 test/integration/targets/jinja_plugins/collections/ansible_collections/foo/bar/plugins/test/good_collection_test.py create mode 100644 test/integration/targets/jinja_plugins/filter_plugins/bad_filter.py create mode 100644 test/integration/targets/jinja_plugins/filter_plugins/good_filter.py create mode 100644 test/integration/targets/jinja_plugins/playbook.yml create mode 100644 test/integration/targets/jinja_plugins/tasks/main.yml create mode 100644 test/integration/targets/jinja_plugins/test_plugins/bad_test.py create mode 100644 test/integration/targets/jinja_plugins/test_plugins/good_test.py create mode 100644 test/integration/targets/json_cleanup/aliases create mode 100644 test/integration/targets/json_cleanup/library/bad_json create mode 100644 test/integration/targets/json_cleanup/module_output_cleaning.yml create mode 100755 test/integration/targets/json_cleanup/runme.sh create mode 100644 test/integration/targets/keyword_inheritance/aliases create mode 100644 test/integration/targets/keyword_inheritance/roles/whoami/tasks/main.yml create mode 100755 test/integration/targets/keyword_inheritance/runme.sh create mode 100644 test/integration/targets/keyword_inheritance/test.yml create mode 100644 test/integration/targets/known_hosts/aliases create mode 100644 test/integration/targets/known_hosts/defaults/main.yml create mode 100644 test/integration/targets/known_hosts/files/existing_known_hosts create mode 100644 test/integration/targets/known_hosts/meta/main.yml create mode 100644 test/integration/targets/known_hosts/tasks/main.yml create mode 100644 test/integration/targets/limit_inventory/aliases create mode 100644 test/integration/targets/limit_inventory/hosts.yml create mode 100755 test/integration/targets/limit_inventory/runme.sh create mode 100644 test/integration/targets/lineinfile/aliases create mode 100644 test/integration/targets/lineinfile/files/firstmatch.txt create mode 100644 test/integration/targets/lineinfile/files/test.conf create mode 100644 test/integration/targets/lineinfile/files/test.txt create mode 100644 test/integration/targets/lineinfile/files/test_58923.txt create mode 100644 test/integration/targets/lineinfile/files/testempty.txt create mode 100644 test/integration/targets/lineinfile/files/testmultiple.txt create mode 100644 test/integration/targets/lineinfile/files/testnoeof.txt create mode 100644 test/integration/targets/lineinfile/files/teststring.conf create mode 100644 test/integration/targets/lineinfile/files/teststring.txt create mode 100644 test/integration/targets/lineinfile/files/teststring_58923.txt create mode 100644 test/integration/targets/lineinfile/meta/main.yml create mode 100644 test/integration/targets/lineinfile/tasks/main.yml create mode 100644 test/integration/targets/lineinfile/tasks/test_string01.yml create mode 100644 test/integration/targets/lineinfile/tasks/test_string02.yml create mode 100644 test/integration/targets/lineinfile/vars/main.yml create mode 100644 test/integration/targets/lookup_config/aliases create mode 100644 test/integration/targets/lookup_config/tasks/main.yml create mode 100644 test/integration/targets/lookup_csvfile/aliases create mode 100644 test/integration/targets/lookup_csvfile/files/cool list of things.csv create mode 100644 test/integration/targets/lookup_csvfile/files/crlf.csv create mode 100644 test/integration/targets/lookup_csvfile/files/people.csv create mode 100644 test/integration/targets/lookup_csvfile/files/tabs.csv create mode 100644 test/integration/targets/lookup_csvfile/files/x1a.csv create mode 100644 test/integration/targets/lookup_csvfile/tasks/main.yml create mode 100644 test/integration/targets/lookup_dict/aliases create mode 100644 test/integration/targets/lookup_dict/tasks/main.yml create mode 100644 test/integration/targets/lookup_env/aliases create mode 100755 test/integration/targets/lookup_env/runme.sh create mode 100644 test/integration/targets/lookup_env/tasks/main.yml create mode 100644 test/integration/targets/lookup_file/aliases create mode 100644 test/integration/targets/lookup_file/tasks/main.yml create mode 100644 test/integration/targets/lookup_fileglob/aliases create mode 100644 test/integration/targets/lookup_fileglob/find_levels/files/play_adj_subdir.txt create mode 100644 test/integration/targets/lookup_fileglob/find_levels/files/somepath/play_adj_subsubdir.txt create mode 100644 test/integration/targets/lookup_fileglob/find_levels/play.yml create mode 100644 test/integration/targets/lookup_fileglob/find_levels/play_adj.txt create mode 100644 test/integration/targets/lookup_fileglob/find_levels/roles/get_file/files/in_role.txt create mode 100644 test/integration/targets/lookup_fileglob/find_levels/roles/get_file/files/otherpath/in_role_subdir.txt create mode 100644 test/integration/targets/lookup_fileglob/find_levels/roles/get_file/tasks/main.yml create mode 100644 test/integration/targets/lookup_fileglob/issue72873/test.yml create mode 100644 test/integration/targets/lookup_fileglob/non_existent/play.yml create mode 100755 test/integration/targets/lookup_fileglob/runme.sh create mode 100644 test/integration/targets/lookup_first_found/aliases create mode 100644 test/integration/targets/lookup_first_found/files/bar1 create mode 100644 test/integration/targets/lookup_first_found/files/foo1 create mode 100644 test/integration/targets/lookup_first_found/files/vars file spaces.yml create mode 100644 test/integration/targets/lookup_first_found/tasks/main.yml create mode 100644 test/integration/targets/lookup_indexed_items/aliases create mode 100644 test/integration/targets/lookup_indexed_items/tasks/main.yml create mode 100644 test/integration/targets/lookup_ini/aliases create mode 100644 test/integration/targets/lookup_ini/duplicate.ini create mode 100644 test/integration/targets/lookup_ini/duplicate_case_check.ini create mode 100644 test/integration/targets/lookup_ini/inventory create mode 100644 test/integration/targets/lookup_ini/lookup-8859-15.ini create mode 100644 test/integration/targets/lookup_ini/lookup.ini create mode 100644 test/integration/targets/lookup_ini/lookup.properties create mode 100644 test/integration/targets/lookup_ini/lookup_case_check.properties create mode 100644 test/integration/targets/lookup_ini/mysql.ini create mode 100755 test/integration/targets/lookup_ini/runme.sh create mode 100644 test/integration/targets/lookup_ini/test_allow_no_value.yml create mode 100644 test/integration/targets/lookup_ini/test_case_sensitive.yml create mode 100644 test/integration/targets/lookup_ini/test_errors.yml create mode 100644 test/integration/targets/lookup_ini/test_ini.yml create mode 100644 test/integration/targets/lookup_ini/test_lookup_properties.yml create mode 100644 test/integration/targets/lookup_inventory_hostnames/aliases create mode 100644 test/integration/targets/lookup_inventory_hostnames/inventory create mode 100644 test/integration/targets/lookup_inventory_hostnames/main.yml create mode 100755 test/integration/targets/lookup_inventory_hostnames/runme.sh create mode 100644 test/integration/targets/lookup_items/aliases create mode 100644 test/integration/targets/lookup_items/tasks/main.yml create mode 100644 test/integration/targets/lookup_lines/aliases create mode 100644 test/integration/targets/lookup_lines/tasks/main.yml create mode 100644 test/integration/targets/lookup_list/aliases create mode 100644 test/integration/targets/lookup_list/tasks/main.yml create mode 100644 test/integration/targets/lookup_nested/aliases create mode 100644 test/integration/targets/lookup_nested/tasks/main.yml create mode 100644 test/integration/targets/lookup_password/aliases create mode 100755 test/integration/targets/lookup_password/runme.sh create mode 100644 test/integration/targets/lookup_password/runme.yml create mode 100644 test/integration/targets/lookup_password/tasks/main.yml create mode 100644 test/integration/targets/lookup_pipe/aliases create mode 100644 test/integration/targets/lookup_pipe/tasks/main.yml create mode 100644 test/integration/targets/lookup_random_choice/aliases create mode 100644 test/integration/targets/lookup_random_choice/tasks/main.yml create mode 100644 test/integration/targets/lookup_sequence/aliases create mode 100644 test/integration/targets/lookup_sequence/tasks/main.yml create mode 100644 test/integration/targets/lookup_subelements/aliases create mode 100644 test/integration/targets/lookup_subelements/tasks/main.yml create mode 100644 test/integration/targets/lookup_subelements/vars/main.yml create mode 100644 test/integration/targets/lookup_template/aliases create mode 100644 test/integration/targets/lookup_template/tasks/main.yml create mode 100644 test/integration/targets/lookup_template/templates/dict.j2 create mode 100644 test/integration/targets/lookup_template/templates/hello.txt create mode 100644 test/integration/targets/lookup_template/templates/hello_comment.txt create mode 100644 test/integration/targets/lookup_template/templates/hello_string.txt create mode 100644 test/integration/targets/lookup_template/templates/world.txt create mode 100644 test/integration/targets/lookup_together/aliases create mode 100644 test/integration/targets/lookup_together/tasks/main.yml create mode 100644 test/integration/targets/lookup_unvault/aliases create mode 100644 test/integration/targets/lookup_unvault/files/foot.txt create mode 100644 test/integration/targets/lookup_unvault/files/foot.txt.vault create mode 100755 test/integration/targets/lookup_unvault/runme.sh create mode 100644 test/integration/targets/lookup_unvault/secret create mode 100644 test/integration/targets/lookup_unvault/unvault.yml create mode 100644 test/integration/targets/lookup_url/aliases create mode 100644 test/integration/targets/lookup_url/meta/main.yml create mode 100644 test/integration/targets/lookup_url/tasks/main.yml create mode 100644 test/integration/targets/lookup_url/tasks/use_netrc.yml create mode 100644 test/integration/targets/lookup_varnames/aliases create mode 100644 test/integration/targets/lookup_varnames/tasks/main.yml create mode 100644 test/integration/targets/lookup_vars/aliases create mode 100644 test/integration/targets/lookup_vars/tasks/main.yml create mode 100644 test/integration/targets/loop-connection/aliases create mode 100644 test/integration/targets/loop-connection/collections/ansible_collections/ns/name/meta/runtime.yml create mode 100644 test/integration/targets/loop-connection/collections/ansible_collections/ns/name/plugins/connection/dummy.py create mode 100644 test/integration/targets/loop-connection/main.yml create mode 100755 test/integration/targets/loop-connection/runme.sh create mode 100644 test/integration/targets/loop-until/aliases create mode 100644 test/integration/targets/loop-until/tasks/main.yml create mode 100644 test/integration/targets/loop_control/aliases create mode 100644 test/integration/targets/loop_control/extended.yml create mode 100644 test/integration/targets/loop_control/inner.yml create mode 100644 test/integration/targets/loop_control/label.yml create mode 100755 test/integration/targets/loop_control/runme.sh create mode 100644 test/integration/targets/loops/aliases create mode 100644 test/integration/targets/loops/files/data1.txt create mode 100644 test/integration/targets/loops/files/data2.txt create mode 100644 test/integration/targets/loops/tasks/index_var_tasks.yml create mode 100644 test/integration/targets/loops/tasks/main.yml create mode 100644 test/integration/targets/loops/tasks/templated_loop_var_tasks.yml create mode 100644 test/integration/targets/loops/vars/64169.yml create mode 100644 test/integration/targets/loops/vars/main.yml create mode 100644 test/integration/targets/meta_tasks/aliases create mode 100644 test/integration/targets/meta_tasks/inventory.yml create mode 100644 test/integration/targets/meta_tasks/inventory_new.yml create mode 100644 test/integration/targets/meta_tasks/inventory_old.yml create mode 100644 test/integration/targets/meta_tasks/inventory_refresh.yml create mode 100644 test/integration/targets/meta_tasks/refresh.yml create mode 100644 test/integration/targets/meta_tasks/refresh_preserve_dynamic.yml create mode 100755 test/integration/targets/meta_tasks/runme.sh create mode 100644 test/integration/targets/meta_tasks/test_end_batch.yml create mode 100644 test/integration/targets/meta_tasks/test_end_host.yml create mode 100644 test/integration/targets/meta_tasks/test_end_host_all.yml create mode 100644 test/integration/targets/meta_tasks/test_end_host_all_fqcn.yml create mode 100644 test/integration/targets/meta_tasks/test_end_host_fqcn.yml create mode 100644 test/integration/targets/meta_tasks/test_end_play.yml create mode 100644 test/integration/targets/meta_tasks/test_end_play_fqcn.yml create mode 100644 test/integration/targets/meta_tasks/test_end_play_multiple_plays.yml create mode 100644 test/integration/targets/meta_tasks/test_end_play_serial_one.yml create mode 100644 test/integration/targets/missing_required_lib/aliases create mode 100644 test/integration/targets/missing_required_lib/library/missing_required_lib.py create mode 100755 test/integration/targets/missing_required_lib/runme.sh create mode 100644 test/integration/targets/missing_required_lib/runme.yml create mode 100644 test/integration/targets/missing_required_lib/tasks/main.yml create mode 100644 test/integration/targets/module_defaults/action_plugins/debug.py create mode 100644 test/integration/targets/module_defaults/aliases create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/othercoll/plugins/action/other_echoaction.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/othercoll/plugins/modules/other_echo1.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/meta/runtime.yml create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/echoaction.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/eos.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/ios.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/action/vyos.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/module_utils/echo_impl.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/echo1.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/echo2.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/eosfacts.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ios_facts.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/metadata.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/module.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/ping.py create mode 100644 test/integration/targets/module_defaults/collections/ansible_collections/testns/testcoll/plugins/modules/vyosfacts.py create mode 100644 test/integration/targets/module_defaults/library/legacy_ping.py create mode 100644 test/integration/targets/module_defaults/library/test_module_defaults.py create mode 100755 test/integration/targets/module_defaults/runme.sh create mode 100644 test/integration/targets/module_defaults/tasks/main.yml create mode 100644 test/integration/targets/module_defaults/templates/test_metadata_warning.yml.j2 create mode 100644 test/integration/targets/module_defaults/test_action_group_metadata.yml create mode 100644 test/integration/targets/module_defaults/test_action_groups.yml create mode 100644 test/integration/targets/module_defaults/test_defaults.yml create mode 100644 test/integration/targets/module_no_log/aliases create mode 100644 test/integration/targets/module_no_log/library/module_that_logs.py create mode 100644 test/integration/targets/module_no_log/tasks/main.yml create mode 100644 test/integration/targets/module_precedence/aliases create mode 100644 test/integration/targets/module_precedence/lib_no_extension/ping create mode 100644 test/integration/targets/module_precedence/lib_with_extension/a.ini create mode 100644 test/integration/targets/module_precedence/lib_with_extension/a.py create mode 100644 test/integration/targets/module_precedence/lib_with_extension/ping.ini create mode 100644 test/integration/targets/module_precedence/lib_with_extension/ping.py create mode 100644 test/integration/targets/module_precedence/modules_test.yml create mode 100644 test/integration/targets/module_precedence/modules_test_envvar.yml create mode 100644 test/integration/targets/module_precedence/modules_test_envvar_ext.yml create mode 100644 test/integration/targets/module_precedence/modules_test_multiple_roles.yml create mode 100644 test/integration/targets/module_precedence/modules_test_multiple_roles_reverse_order.yml create mode 100644 test/integration/targets/module_precedence/modules_test_role.yml create mode 100644 test/integration/targets/module_precedence/modules_test_role_ext.yml create mode 100644 test/integration/targets/module_precedence/multiple_roles/bar/library/ping.py create mode 100644 test/integration/targets/module_precedence/multiple_roles/bar/tasks/main.yml create mode 100644 test/integration/targets/module_precedence/multiple_roles/foo/library/ping.py create mode 100644 test/integration/targets/module_precedence/multiple_roles/foo/tasks/main.yml create mode 100644 test/integration/targets/module_precedence/roles_no_extension/foo/library/ping create mode 100644 test/integration/targets/module_precedence/roles_no_extension/foo/tasks/main.yml create mode 100644 test/integration/targets/module_precedence/roles_with_extension/foo/library/a.ini create mode 100644 test/integration/targets/module_precedence/roles_with_extension/foo/library/a.py create mode 100644 test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.ini create mode 100644 test/integration/targets/module_precedence/roles_with_extension/foo/library/ping.py create mode 100644 test/integration/targets/module_precedence/roles_with_extension/foo/tasks/main.yml create mode 100755 test/integration/targets/module_precedence/runme.sh create mode 100644 test/integration/targets/module_tracebacks/aliases create mode 100644 test/integration/targets/module_tracebacks/inventory create mode 100755 test/integration/targets/module_tracebacks/runme.sh create mode 100644 test/integration/targets/module_tracebacks/traceback.yml create mode 100644 test/integration/targets/module_utils/aliases create mode 100644 test/integration/targets/module_utils/callback/pure_json.py create mode 100644 test/integration/targets/module_utils/collections/ansible_collections/testns/testcoll/plugins/module_utils/legit.py create mode 100644 test/integration/targets/module_utils/library/test.py create mode 100644 test/integration/targets/module_utils/library/test_alias_deprecation.py create mode 100644 test/integration/targets/module_utils/library/test_cwd_missing.py create mode 100644 test/integration/targets/module_utils/library/test_cwd_unreadable.py create mode 100644 test/integration/targets/module_utils/library/test_datetime.py create mode 100644 test/integration/targets/module_utils/library/test_env_override.py create mode 100644 test/integration/targets/module_utils/library/test_failure.py create mode 100644 test/integration/targets/module_utils/library/test_network.py create mode 100644 test/integration/targets/module_utils/library/test_no_log.py create mode 100644 test/integration/targets/module_utils/library/test_optional.py create mode 100644 test/integration/targets/module_utils/library/test_override.py create mode 100644 test/integration/targets/module_utils/library/test_recursive_diff.py create mode 100644 test/integration/targets/module_utils/module_utils/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/a/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/a/b/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/a/b/c/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/a/b/c/d/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/a/b/c/d/e/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/a/b/c/d/e/f/g/h/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/ansible_release.py create mode 100644 test/integration/targets/module_utils/module_utils/bar0/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/bar0/foo.py create mode 100644 test/integration/targets/module_utils/module_utils/bar1/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/bar2/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/baz1/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/baz1/one.py create mode 100644 test/integration/targets/module_utils/module_utils/baz2/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/baz2/one.py create mode 100644 test/integration/targets/module_utils/module_utils/foo.py create mode 100644 test/integration/targets/module_utils/module_utils/foo0.py create mode 100644 test/integration/targets/module_utils/module_utils/foo1.py create mode 100644 test/integration/targets/module_utils/module_utils/foo2.py create mode 100644 test/integration/targets/module_utils/module_utils/qux1/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/qux1/quux.py create mode 100644 test/integration/targets/module_utils/module_utils/qux2/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/qux2/quux.py create mode 100644 test/integration/targets/module_utils/module_utils/qux2/quuz.py create mode 100644 test/integration/targets/module_utils/module_utils/service.py create mode 100644 test/integration/targets/module_utils/module_utils/spam1/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam1/ham/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam1/ham/eggs/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam2/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam2/ham/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam2/ham/eggs/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam3/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam3/ham/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam3/ham/bacon.py create mode 100644 test/integration/targets/module_utils/module_utils/spam4/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam4/ham/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam4/ham/bacon.py create mode 100644 test/integration/targets/module_utils/module_utils/spam5/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam5/ham/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam5/ham/bacon.py create mode 100644 test/integration/targets/module_utils/module_utils/spam5/ham/eggs.py create mode 100644 test/integration/targets/module_utils/module_utils/spam6/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam6/ham/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam7/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam7/ham/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam7/ham/bacon.py create mode 100644 test/integration/targets/module_utils/module_utils/spam8/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam8/ham/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/spam8/ham/bacon.py create mode 100644 test/integration/targets/module_utils/module_utils/sub/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/sub/bam.py create mode 100644 test/integration/targets/module_utils/module_utils/sub/bam/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/sub/bam/bam.py create mode 100644 test/integration/targets/module_utils/module_utils/sub/bar/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/sub/bar/bam.py create mode 100644 test/integration/targets/module_utils/module_utils/sub/bar/bar.py create mode 100644 test/integration/targets/module_utils/module_utils/yak/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/yak/zebra/__init__.py create mode 100644 test/integration/targets/module_utils/module_utils/yak/zebra/foo.py create mode 100644 test/integration/targets/module_utils/module_utils_basic_setcwd.yml create mode 100644 test/integration/targets/module_utils/module_utils_common_dict_transformation.yml create mode 100644 test/integration/targets/module_utils/module_utils_common_network.yml create mode 100644 test/integration/targets/module_utils/module_utils_envvar.yml create mode 100644 test/integration/targets/module_utils/module_utils_test.yml create mode 100644 test/integration/targets/module_utils/module_utils_test_no_log.yml create mode 100644 test/integration/targets/module_utils/module_utils_vvvvv.yml create mode 100644 test/integration/targets/module_utils/other_mu_dir/__init__.py create mode 100644 test/integration/targets/module_utils/other_mu_dir/a/__init__.py create mode 100644 test/integration/targets/module_utils/other_mu_dir/a/b/__init__.py create mode 100644 test/integration/targets/module_utils/other_mu_dir/a/b/c/__init__.py create mode 100644 test/integration/targets/module_utils/other_mu_dir/a/b/c/d/__init__.py create mode 100644 test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/__init__.py create mode 100644 test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/__init__.py create mode 100644 test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/__init__.py create mode 100644 test/integration/targets/module_utils/other_mu_dir/a/b/c/d/e/f/g/h/__init__.py create mode 100644 test/integration/targets/module_utils/other_mu_dir/facts.py create mode 100644 test/integration/targets/module_utils/other_mu_dir/json_utils.py create mode 100644 test/integration/targets/module_utils/other_mu_dir/mork.py create mode 100755 test/integration/targets/module_utils/runme.sh create mode 100644 test/integration/targets/module_utils_Ansible.AccessToken/aliases create mode 100644 test/integration/targets/module_utils_Ansible.AccessToken/library/ansible_access_token_tests.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.AccessToken/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.Basic/aliases create mode 100644 test/integration/targets/module_utils_Ansible.Basic/library/ansible_basic_tests.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.Basic/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.Become/aliases create mode 100644 test/integration/targets/module_utils_Ansible.Become/library/ansible_become_tests.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.Become/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/aliases create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/library/add_type_test.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.AddType/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/aliases create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/library/argv_parser_test.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/meta/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.ArgvParser/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/aliases create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/library/backup_file_test.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.Backup/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/aliases create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/library/camel_conversion_test.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.CamelConversion/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/aliases create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/library/command_util_test.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/meta/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.CommandUtil/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/aliases create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/library/file_util_test.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.FileUtil/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/aliases create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testlist.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/library/testpath.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.Legacy/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/aliases create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/library/symbolic_link_test.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.LinkUtil/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/aliases create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/library/privilege_util_test.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.PrivilegeUtil/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.SID/aliases create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.SID/library/sid_utils_test.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.SID/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/aliases create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/library/web_request_test.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/meta/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.ModuleUtils.WebRequest/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.Privilege/aliases create mode 100644 test/integration/targets/module_utils_Ansible.Privilege/library/ansible_privilege_tests.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.Privilege/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.Process/aliases create mode 100644 test/integration/targets/module_utils_Ansible.Process/library/ansible_process_tests.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.Process/tasks/main.yml create mode 100644 test/integration/targets/module_utils_Ansible.Service/aliases create mode 100644 test/integration/targets/module_utils_Ansible.Service/library/ansible_service_tests.ps1 create mode 100644 test/integration/targets/module_utils_Ansible.Service/tasks/main.yml create mode 100644 test/integration/targets/module_utils_ansible_release/aliases create mode 100644 test/integration/targets/module_utils_ansible_release/library/ansible_release.py create mode 100644 test/integration/targets/module_utils_ansible_release/tasks/main.yml create mode 100644 test/integration/targets/module_utils_common.respawn/aliases create mode 100644 test/integration/targets/module_utils_common.respawn/library/respawnme.py create mode 100644 test/integration/targets/module_utils_common.respawn/tasks/main.yml create mode 100644 test/integration/targets/module_utils_distro/aliases create mode 100644 test/integration/targets/module_utils_distro/meta/main.yml create mode 100755 test/integration/targets/module_utils_distro/runme.sh create mode 100644 test/integration/targets/module_utils_facts.system.selinux/aliases create mode 100644 test/integration/targets/module_utils_facts.system.selinux/tasks/main.yml create mode 100644 test/integration/targets/module_utils_facts.system.selinux/tasks/selinux.yml create mode 100644 test/integration/targets/module_utils_urls/aliases create mode 100644 test/integration/targets/module_utils_urls/library/test_peercert.py create mode 100644 test/integration/targets/module_utils_urls/meta/main.yml create mode 100644 test/integration/targets/module_utils_urls/tasks/main.yml create mode 100644 test/integration/targets/network_cli/aliases create mode 100644 test/integration/targets/network_cli/passworded_user.yml create mode 100755 test/integration/targets/network_cli/runme.sh create mode 100644 test/integration/targets/network_cli/setup.yml create mode 100644 test/integration/targets/network_cli/teardown.yml create mode 100644 test/integration/targets/no_log/aliases create mode 100644 test/integration/targets/no_log/dynamic.yml create mode 100644 test/integration/targets/no_log/library/module.py create mode 100644 test/integration/targets/no_log/no_log_local.yml create mode 100644 test/integration/targets/no_log/no_log_suboptions.yml create mode 100644 test/integration/targets/no_log/no_log_suboptions_invalid.yml create mode 100755 test/integration/targets/no_log/runme.sh create mode 100644 test/integration/targets/noexec/aliases create mode 100644 test/integration/targets/noexec/inventory create mode 100755 test/integration/targets/noexec/runme.sh create mode 100644 test/integration/targets/noexec/test-noexec.yml create mode 100644 test/integration/targets/old_style_cache_plugins/aliases create mode 100644 test/integration/targets/old_style_cache_plugins/cleanup.yml create mode 100644 test/integration/targets/old_style_cache_plugins/inspect_cache.yml create mode 100644 test/integration/targets/old_style_cache_plugins/inventory_config create mode 100644 test/integration/targets/old_style_cache_plugins/plugins/cache/configurable_redis.py create mode 100644 test/integration/targets/old_style_cache_plugins/plugins/cache/legacy_redis.py create mode 100644 test/integration/targets/old_style_cache_plugins/plugins/inventory/test.py create mode 100755 test/integration/targets/old_style_cache_plugins/runme.sh create mode 100644 test/integration/targets/old_style_cache_plugins/setup_redis_cache.yml create mode 100644 test/integration/targets/old_style_cache_plugins/test_fact_gathering.yml create mode 100644 test/integration/targets/old_style_cache_plugins/test_inventory_cache.yml create mode 100644 test/integration/targets/old_style_modules_posix/aliases create mode 100644 test/integration/targets/old_style_modules_posix/library/helloworld.sh create mode 100644 test/integration/targets/old_style_modules_posix/meta/main.yml create mode 100644 test/integration/targets/old_style_modules_posix/tasks/main.yml create mode 100644 test/integration/targets/old_style_vars_plugins/aliases create mode 100644 test/integration/targets/old_style_vars_plugins/deprecation_warning/vars.py create mode 100755 test/integration/targets/old_style_vars_plugins/runme.sh create mode 100644 test/integration/targets/old_style_vars_plugins/vars_plugins/auto_enabled.py create mode 100644 test/integration/targets/old_style_vars_plugins/vars_plugins/implicitly_auto_enabled.py create mode 100644 test/integration/targets/old_style_vars_plugins/vars_plugins/require_enabled.py create mode 100644 test/integration/targets/omit/48673.yml create mode 100644 test/integration/targets/omit/75692.yml create mode 100644 test/integration/targets/omit/C75692.yml create mode 100644 test/integration/targets/omit/aliases create mode 100755 test/integration/targets/omit/runme.sh create mode 100644 test/integration/targets/order/aliases create mode 100644 test/integration/targets/order/inventory create mode 100644 test/integration/targets/order/order.yml create mode 100755 test/integration/targets/order/runme.sh create mode 100644 test/integration/targets/package/aliases create mode 100644 test/integration/targets/package/meta/main.yml create mode 100644 test/integration/targets/package/tasks/main.yml create mode 100644 test/integration/targets/package_facts/aliases create mode 100644 test/integration/targets/package_facts/tasks/main.yml create mode 100644 test/integration/targets/parsing/aliases create mode 100644 test/integration/targets/parsing/bad_parsing.yml create mode 100644 test/integration/targets/parsing/good_parsing.yml create mode 100644 test/integration/targets/parsing/roles/test_bad_parsing/tasks/main.yml create mode 100644 test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario1.yml create mode 100644 test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario2.yml create mode 100644 test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario3.yml create mode 100644 test/integration/targets/parsing/roles/test_bad_parsing/tasks/scenario4.yml create mode 100644 test/integration/targets/parsing/roles/test_bad_parsing/vars/main.yml create mode 100644 test/integration/targets/parsing/roles/test_good_parsing/tasks/main.yml create mode 100644 test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include.yml create mode 100644 test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_conditional.yml create mode 100644 test/integration/targets/parsing/roles/test_good_parsing/tasks/test_include_nested.yml create mode 100644 test/integration/targets/parsing/roles/test_good_parsing/vars/main.yml create mode 100755 test/integration/targets/parsing/runme.sh create mode 100644 test/integration/targets/path_lookups/aliases create mode 100644 test/integration/targets/path_lookups/play.yml create mode 100644 test/integration/targets/path_lookups/roles/showfile/tasks/main.yml create mode 100755 test/integration/targets/path_lookups/runme.sh create mode 100644 test/integration/targets/path_lookups/testplay.yml create mode 100644 test/integration/targets/path_with_comma_in_inventory/aliases create mode 100644 test/integration/targets/path_with_comma_in_inventory/playbook.yml create mode 100755 test/integration/targets/path_with_comma_in_inventory/runme.sh create mode 100644 test/integration/targets/path_with_comma_in_inventory/this,path,has,commas/group_vars/all.yml create mode 100644 test/integration/targets/path_with_comma_in_inventory/this,path,has,commas/hosts create mode 100644 test/integration/targets/pause/aliases create mode 100644 test/integration/targets/pause/pause-1.yml create mode 100644 test/integration/targets/pause/pause-2.yml create mode 100644 test/integration/targets/pause/pause-3.yml create mode 100644 test/integration/targets/pause/pause-4.yml create mode 100644 test/integration/targets/pause/pause-5.yml create mode 100755 test/integration/targets/pause/runme.sh create mode 100644 test/integration/targets/pause/setup.yml create mode 100644 test/integration/targets/pause/test-pause-background.yml create mode 100644 test/integration/targets/pause/test-pause-no-tty.yml create mode 100755 test/integration/targets/pause/test-pause.py create mode 100644 test/integration/targets/pause/test-pause.yml create mode 100644 test/integration/targets/ping/aliases create mode 100644 test/integration/targets/ping/tasks/main.yml create mode 100644 test/integration/targets/pip/aliases create mode 100644 test/integration/targets/pip/files/ansible_test_pip_chdir/__init__.py create mode 100755 test/integration/targets/pip/files/setup.py create mode 100644 test/integration/targets/pip/meta/main.yml create mode 100644 test/integration/targets/pip/tasks/default_cleanup.yml create mode 100644 test/integration/targets/pip/tasks/freebsd_cleanup.yml create mode 100644 test/integration/targets/pip/tasks/main.yml create mode 100644 test/integration/targets/pip/tasks/pip.yml create mode 100644 test/integration/targets/pip/vars/main.yml create mode 100644 test/integration/targets/pkg_resources/aliases create mode 100644 test/integration/targets/pkg_resources/lookup_plugins/check_pkg_resources.py create mode 100644 test/integration/targets/pkg_resources/tasks/main.yml create mode 100644 test/integration/targets/play_iterator/aliases create mode 100644 test/integration/targets/play_iterator/playbook.yml create mode 100755 test/integration/targets/play_iterator/runme.sh create mode 100644 test/integration/targets/playbook/aliases create mode 100644 test/integration/targets/playbook/empty.yml create mode 100644 test/integration/targets/playbook/empty_hosts.yml create mode 100644 test/integration/targets/playbook/malformed_post_tasks.yml create mode 100644 test/integration/targets/playbook/malformed_pre_tasks.yml create mode 100644 test/integration/targets/playbook/malformed_roles.yml create mode 100644 test/integration/targets/playbook/malformed_tasks.yml create mode 100644 test/integration/targets/playbook/malformed_vars_prompt.yml create mode 100644 test/integration/targets/playbook/old_style_role.yml create mode 100644 test/integration/targets/playbook/remote_user_and_user.yml create mode 100644 test/integration/targets/playbook/roles_null.yml create mode 100755 test/integration/targets/playbook/runme.sh create mode 100644 test/integration/targets/playbook/some_vars.yml create mode 100644 test/integration/targets/playbook/timeout.yml create mode 100644 test/integration/targets/playbook/types.yml create mode 100644 test/integration/targets/playbook/user.yml create mode 100644 test/integration/targets/playbook/vars_files_null.yml create mode 100644 test/integration/targets/playbook/vars_files_string.yml create mode 100644 test/integration/targets/playbook/vars_prompt_null.yml create mode 100644 test/integration/targets/plugin_config_for_inventory/aliases create mode 100644 test/integration/targets/plugin_config_for_inventory/cache_plugins/none.py create mode 100644 test/integration/targets/plugin_config_for_inventory/config_with_parameter.yml create mode 100644 test/integration/targets/plugin_config_for_inventory/config_without_parameter.yml create mode 100755 test/integration/targets/plugin_config_for_inventory/runme.sh create mode 100644 test/integration/targets/plugin_config_for_inventory/test_inventory.py create mode 100644 test/integration/targets/plugin_filtering/aliases create mode 100644 test/integration/targets/plugin_filtering/copy.yml create mode 100644 test/integration/targets/plugin_filtering/filter_lookup.ini create mode 100644 test/integration/targets/plugin_filtering/filter_lookup.yml create mode 100644 test/integration/targets/plugin_filtering/filter_modules.ini create mode 100644 test/integration/targets/plugin_filtering/filter_modules.yml create mode 100644 test/integration/targets/plugin_filtering/filter_ping.ini create mode 100644 test/integration/targets/plugin_filtering/filter_ping.yml create mode 100644 test/integration/targets/plugin_filtering/filter_stat.ini create mode 100644 test/integration/targets/plugin_filtering/filter_stat.yml create mode 100644 test/integration/targets/plugin_filtering/lookup.yml create mode 100644 test/integration/targets/plugin_filtering/no_blacklist_module.ini create mode 100644 test/integration/targets/plugin_filtering/no_blacklist_module.yml create mode 100644 test/integration/targets/plugin_filtering/no_filters.ini create mode 100644 test/integration/targets/plugin_filtering/pause.yml create mode 100644 test/integration/targets/plugin_filtering/ping.yml create mode 100755 test/integration/targets/plugin_filtering/runme.sh create mode 100644 test/integration/targets/plugin_filtering/stat.yml create mode 100644 test/integration/targets/plugin_filtering/tempfile.yml create mode 100644 test/integration/targets/plugin_loader/aliases create mode 100644 test/integration/targets/plugin_loader/normal/action_plugins/self_referential.py create mode 100644 test/integration/targets/plugin_loader/normal/filters.yml create mode 100644 test/integration/targets/plugin_loader/normal/library/_underscore.py create mode 100644 test/integration/targets/plugin_loader/normal/self_referential.yml create mode 100644 test/integration/targets/plugin_loader/normal/underscore.yml create mode 100644 test/integration/targets/plugin_loader/override/filter_plugins/core.py create mode 100644 test/integration/targets/plugin_loader/override/filters.yml create mode 100755 test/integration/targets/plugin_loader/runme.sh create mode 100644 test/integration/targets/plugin_loader/use_coll_name.yml create mode 100644 test/integration/targets/plugin_namespace/aliases create mode 100644 test/integration/targets/plugin_namespace/filter_plugins/test_filter.py create mode 100644 test/integration/targets/plugin_namespace/lookup_plugins/lookup_name.py create mode 100644 test/integration/targets/plugin_namespace/tasks/main.yml create mode 100644 test/integration/targets/plugin_namespace/test_plugins/test_test.py create mode 100644 test/integration/targets/preflight_encoding/aliases create mode 100644 test/integration/targets/preflight_encoding/tasks/main.yml create mode 100644 test/integration/targets/preflight_encoding/vars/main.yml create mode 100644 test/integration/targets/prepare_http_tests/defaults/main.yml create mode 100644 test/integration/targets/prepare_http_tests/handlers/main.yml create mode 100644 test/integration/targets/prepare_http_tests/library/httptester_kinit.py create mode 100644 test/integration/targets/prepare_http_tests/meta/main.yml create mode 100644 test/integration/targets/prepare_http_tests/tasks/default.yml create mode 100644 test/integration/targets/prepare_http_tests/tasks/kerberos.yml create mode 100644 test/integration/targets/prepare_http_tests/tasks/main.yml create mode 100644 test/integration/targets/prepare_http_tests/tasks/windows.yml create mode 100644 test/integration/targets/prepare_http_tests/templates/krb5.conf.j2 create mode 100644 test/integration/targets/prepare_http_tests/vars/Alpine.yml create mode 100644 test/integration/targets/prepare_http_tests/vars/Debian.yml create mode 100644 test/integration/targets/prepare_http_tests/vars/FreeBSD.yml create mode 100644 test/integration/targets/prepare_http_tests/vars/RedHat-9.yml create mode 100644 test/integration/targets/prepare_http_tests/vars/Suse.yml create mode 100644 test/integration/targets/prepare_http_tests/vars/default.yml create mode 100644 test/integration/targets/prepare_http_tests/vars/httptester.yml create mode 100644 test/integration/targets/prepare_tests/tasks/main.yml create mode 100644 test/integration/targets/pyyaml/aliases create mode 100755 test/integration/targets/pyyaml/runme.sh create mode 100644 test/integration/targets/raw/aliases create mode 100644 test/integration/targets/raw/meta/main.yml create mode 100755 test/integration/targets/raw/runme.sh create mode 100644 test/integration/targets/raw/runme.yml create mode 100644 test/integration/targets/raw/tasks/main.yml create mode 100644 test/integration/targets/reboot/aliases create mode 100644 test/integration/targets/reboot/handlers/main.yml create mode 100644 test/integration/targets/reboot/tasks/check_reboot.yml create mode 100644 test/integration/targets/reboot/tasks/get_boot_time.yml create mode 100644 test/integration/targets/reboot/tasks/main.yml create mode 100644 test/integration/targets/reboot/tasks/test_invalid_parameter.yml create mode 100644 test/integration/targets/reboot/tasks/test_invalid_test_command.yml create mode 100644 test/integration/targets/reboot/tasks/test_molly_guard.yml create mode 100644 test/integration/targets/reboot/tasks/test_reboot_command.yml create mode 100644 test/integration/targets/reboot/tasks/test_standard_scenarios.yml create mode 100644 test/integration/targets/reboot/vars/main.yml create mode 100644 test/integration/targets/register/aliases create mode 100644 test/integration/targets/register/can_register.yml create mode 100644 test/integration/targets/register/invalid.yml create mode 100644 test/integration/targets/register/invalid_skipped.yml create mode 100755 test/integration/targets/register/runme.sh create mode 100644 test/integration/targets/rel_plugin_loading/aliases create mode 100644 test/integration/targets/rel_plugin_loading/notyaml.yml create mode 100755 test/integration/targets/rel_plugin_loading/runme.sh create mode 100644 test/integration/targets/rel_plugin_loading/subdir/inventory_plugins/notyaml.py create mode 100644 test/integration/targets/rel_plugin_loading/subdir/play.yml create mode 100644 test/integration/targets/remote_tmp/aliases create mode 100644 test/integration/targets/remote_tmp/playbook.yml create mode 100755 test/integration/targets/remote_tmp/runme.sh create mode 100644 test/integration/targets/replace/aliases create mode 100644 test/integration/targets/replace/meta/main.yml create mode 100644 test/integration/targets/replace/tasks/main.yml create mode 100644 test/integration/targets/retry_task_name_in_callback/aliases create mode 100755 test/integration/targets/retry_task_name_in_callback/runme.sh create mode 100644 test/integration/targets/retry_task_name_in_callback/test.yml create mode 100644 test/integration/targets/roles/aliases create mode 100644 test/integration/targets/roles/allowed_dupes.yml create mode 100644 test/integration/targets/roles/data_integrity.yml create mode 100644 test/integration/targets/roles/no_dupes.yml create mode 100644 test/integration/targets/roles/no_outside.yml create mode 100644 test/integration/targets/roles/roles/a/tasks/main.yml create mode 100644 test/integration/targets/roles/roles/b/meta/main.yml create mode 100644 test/integration/targets/roles/roles/b/tasks/main.yml create mode 100644 test/integration/targets/roles/roles/c/meta/main.yml create mode 100644 test/integration/targets/roles/roles/c/tasks/main.yml create mode 100644 test/integration/targets/roles/roles/data/defaults/main/00.yml create mode 100644 test/integration/targets/roles/roles/data/defaults/main/01.yml create mode 100644 test/integration/targets/roles/roles/data/tasks/main.yml create mode 100755 test/integration/targets/roles/runme.sh create mode 100644 test/integration/targets/roles/tasks/dummy.yml create mode 100644 test/integration/targets/roles_arg_spec/aliases create mode 100644 test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/MANIFEST.json create mode 100644 test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/roles/blah/meta/argument_specs.yml create mode 100644 test/integration/targets/roles_arg_spec/collections/ansible_collections/foo/bar/roles/blah/tasks/main.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/a/meta/argument_specs.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/a/meta/main.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/a/tasks/alternate.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/a/tasks/main.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/a/tasks/no_spec_entrypoint.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/b/meta/argument_specs.yaml create mode 100644 test/integration/targets/roles_arg_spec/roles/b/tasks/main.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/c/meta/main.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/c/tasks/main.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/empty_argspec/meta/argument_specs.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/empty_argspec/tasks/main.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/empty_file/meta/argument_specs.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/empty_file/tasks/main.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/role_with_no_tasks/meta/argument_specs.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/test1/defaults/main.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/test1/meta/argument_specs.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/test1/tasks/main.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/test1/tasks/other.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/test1/tasks/test1_other.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/test1/vars/main.yml create mode 100644 test/integration/targets/roles_arg_spec/roles/test1/vars/other.yml create mode 100755 test/integration/targets/roles_arg_spec/runme.sh create mode 100644 test/integration/targets/roles_arg_spec/test.yml create mode 100644 test/integration/targets/roles_arg_spec/test_complex_role_fails.yml create mode 100644 test/integration/targets/roles_arg_spec/test_play_level_role_fails.yml create mode 100644 test/integration/targets/roles_arg_spec/test_tags.yml create mode 100644 test/integration/targets/roles_var_inheritance/aliases create mode 100644 test/integration/targets/roles_var_inheritance/play.yml create mode 100644 test/integration/targets/roles_var_inheritance/roles/A/meta/main.yml create mode 100644 test/integration/targets/roles_var_inheritance/roles/B/meta/main.yml create mode 100644 test/integration/targets/roles_var_inheritance/roles/child_nested_dep/vars/main.yml create mode 100644 test/integration/targets/roles_var_inheritance/roles/common_dep/meta/main.yml create mode 100644 test/integration/targets/roles_var_inheritance/roles/common_dep/vars/main.yml create mode 100644 test/integration/targets/roles_var_inheritance/roles/nested_dep/meta/main.yml create mode 100644 test/integration/targets/roles_var_inheritance/roles/nested_dep/tasks/main.yml create mode 100755 test/integration/targets/roles_var_inheritance/runme.sh create mode 100644 test/integration/targets/rpm_key/aliases create mode 100644 test/integration/targets/rpm_key/defaults/main.yaml create mode 100644 test/integration/targets/rpm_key/meta/main.yml create mode 100644 test/integration/targets/rpm_key/tasks/main.yaml create mode 100644 test/integration/targets/rpm_key/tasks/rpm_key.yaml create mode 100644 test/integration/targets/run_modules/aliases create mode 100644 test/integration/targets/run_modules/args.json create mode 100644 test/integration/targets/run_modules/library/test.py create mode 100755 test/integration/targets/run_modules/runme.sh create mode 100644 test/integration/targets/script/aliases create mode 100755 test/integration/targets/script/files/create_afile.sh create mode 100644 test/integration/targets/script/files/no_shebang.py create mode 100755 test/integration/targets/script/files/remove_afile.sh create mode 100755 test/integration/targets/script/files/space path/test.sh create mode 100755 test/integration/targets/script/files/test.sh create mode 100755 test/integration/targets/script/files/test_with_args.sh create mode 100644 test/integration/targets/script/meta/main.yml create mode 100644 test/integration/targets/script/tasks/main.yml create mode 100644 test/integration/targets/service/aliases create mode 100644 test/integration/targets/service/files/ansible-broken.upstart create mode 100644 test/integration/targets/service/files/ansible.rc create mode 100644 test/integration/targets/service/files/ansible.systemd create mode 100755 test/integration/targets/service/files/ansible.sysv create mode 100644 test/integration/targets/service/files/ansible.upstart create mode 100644 test/integration/targets/service/files/ansible_test_service.py create mode 100644 test/integration/targets/service/meta/main.yml create mode 100644 test/integration/targets/service/tasks/main.yml create mode 100644 test/integration/targets/service/tasks/rc_cleanup.yml create mode 100644 test/integration/targets/service/tasks/rc_setup.yml create mode 100644 test/integration/targets/service/tasks/systemd_cleanup.yml create mode 100644 test/integration/targets/service/tasks/systemd_setup.yml create mode 100644 test/integration/targets/service/tasks/sysv_cleanup.yml create mode 100644 test/integration/targets/service/tasks/sysv_setup.yml create mode 100644 test/integration/targets/service/tasks/tests.yml create mode 100644 test/integration/targets/service/tasks/upstart_cleanup.yml create mode 100644 test/integration/targets/service/tasks/upstart_setup.yml create mode 100644 test/integration/targets/service/templates/main.yml create mode 100644 test/integration/targets/service_facts/aliases create mode 100644 test/integration/targets/service_facts/files/ansible.systemd create mode 100644 test/integration/targets/service_facts/files/ansible_test_service.py create mode 100644 test/integration/targets/service_facts/tasks/main.yml create mode 100644 test/integration/targets/service_facts/tasks/systemd_cleanup.yml create mode 100644 test/integration/targets/service_facts/tasks/systemd_setup.yml create mode 100644 test/integration/targets/service_facts/tasks/tests.yml create mode 100644 test/integration/targets/set_fact/aliases create mode 100644 test/integration/targets/set_fact/incremental.yml create mode 100644 test/integration/targets/set_fact/inventory create mode 100644 test/integration/targets/set_fact/nowarn_clean_facts.yml create mode 100755 test/integration/targets/set_fact/runme.sh create mode 100644 test/integration/targets/set_fact/set_fact_auto_unsafe.yml create mode 100644 test/integration/targets/set_fact/set_fact_bool_conv.yml create mode 100644 test/integration/targets/set_fact/set_fact_bool_conv_jinja2_native.yml create mode 100644 test/integration/targets/set_fact/set_fact_cached_1.yml create mode 100644 test/integration/targets/set_fact/set_fact_cached_2.yml create mode 100644 test/integration/targets/set_fact/set_fact_empty_str_key.yml create mode 100644 test/integration/targets/set_fact/set_fact_no_cache.yml create mode 100644 test/integration/targets/set_stats/aliases create mode 100755 test/integration/targets/set_stats/runme.sh create mode 100644 test/integration/targets/set_stats/test_aggregate.yml create mode 100644 test/integration/targets/set_stats/test_simple.yml create mode 100644 test/integration/targets/setup_cron/defaults/main.yml create mode 100644 test/integration/targets/setup_cron/meta/main.yml create mode 100644 test/integration/targets/setup_cron/tasks/main.yml create mode 100644 test/integration/targets/setup_cron/vars/alpine.yml create mode 100644 test/integration/targets/setup_cron/vars/debian.yml create mode 100644 test/integration/targets/setup_cron/vars/default.yml create mode 100644 test/integration/targets/setup_cron/vars/fedora.yml create mode 100644 test/integration/targets/setup_cron/vars/freebsd.yml create mode 100644 test/integration/targets/setup_cron/vars/redhat.yml create mode 100644 test/integration/targets/setup_cron/vars/suse.yml create mode 100644 test/integration/targets/setup_deb_repo/files/package_specs/stable/foo-1.0.0 create mode 100644 test/integration/targets/setup_deb_repo/files/package_specs/stable/foo-1.0.1 create mode 100644 test/integration/targets/setup_deb_repo/files/package_specs/stable/foobar-1.0.0 create mode 100644 test/integration/targets/setup_deb_repo/files/package_specs/stable/foobar-1.0.1 create mode 100644 test/integration/targets/setup_deb_repo/files/package_specs/testing/foo-2.0.0 create mode 100644 test/integration/targets/setup_deb_repo/files/package_specs/testing/foo-2.0.1 create mode 100644 test/integration/targets/setup_deb_repo/meta/main.yml create mode 100644 test/integration/targets/setup_deb_repo/tasks/main.yml create mode 100644 test/integration/targets/setup_epel/tasks/main.yml create mode 100644 test/integration/targets/setup_gnutar/handlers/main.yml create mode 100644 test/integration/targets/setup_gnutar/tasks/main.yml create mode 100644 test/integration/targets/setup_nobody/handlers/main.yml create mode 100644 test/integration/targets/setup_nobody/tasks/main.yml create mode 100644 test/integration/targets/setup_paramiko/aliases create mode 100644 test/integration/targets/setup_paramiko/constraints.txt create mode 100644 test/integration/targets/setup_paramiko/install-Alpine-3-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/install-CentOS-6-python-2.yml create mode 100644 test/integration/targets/setup_paramiko/install-Darwin-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/install-Fedora-35-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/install-FreeBSD-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/install-RedHat-8-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/install-RedHat-9-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/install-Ubuntu-16-python-2.yml create mode 100644 test/integration/targets/setup_paramiko/install-fail.yml create mode 100644 test/integration/targets/setup_paramiko/install-python-2.yml create mode 100644 test/integration/targets/setup_paramiko/install-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/install.yml create mode 100644 test/integration/targets/setup_paramiko/inventory create mode 100644 test/integration/targets/setup_paramiko/library/detect_paramiko.py create mode 100644 test/integration/targets/setup_paramiko/setup-remote-constraints.yml create mode 100644 test/integration/targets/setup_paramiko/setup.sh create mode 100644 test/integration/targets/setup_paramiko/uninstall-Alpine-3-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-Darwin-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-Fedora-35-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-FreeBSD-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-RedHat-8-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-RedHat-9-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-apt-python-2.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-apt-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-dnf.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-fail.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-yum.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-zypper-python-2.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall-zypper-python-3.yml create mode 100644 test/integration/targets/setup_paramiko/uninstall.yml create mode 100644 test/integration/targets/setup_passlib/tasks/main.yml create mode 100644 test/integration/targets/setup_pexpect/files/constraints.txt create mode 100644 test/integration/targets/setup_pexpect/meta/main.yml create mode 100644 test/integration/targets/setup_pexpect/tasks/main.yml create mode 100644 test/integration/targets/setup_remote_constraints/aliases create mode 100644 test/integration/targets/setup_remote_constraints/meta/main.yml create mode 100644 test/integration/targets/setup_remote_constraints/tasks/main.yml create mode 100644 test/integration/targets/setup_remote_tmp_dir/defaults/main.yml create mode 100644 test/integration/targets/setup_remote_tmp_dir/handlers/main.yml create mode 100644 test/integration/targets/setup_remote_tmp_dir/tasks/default-cleanup.yml create mode 100644 test/integration/targets/setup_remote_tmp_dir/tasks/default.yml create mode 100644 test/integration/targets/setup_remote_tmp_dir/tasks/main.yml create mode 100644 test/integration/targets/setup_remote_tmp_dir/tasks/windows-cleanup.yml create mode 100644 test/integration/targets/setup_remote_tmp_dir/tasks/windows.yml create mode 100644 test/integration/targets/setup_rpm_repo/aliases create mode 100644 test/integration/targets/setup_rpm_repo/defaults/main.yml create mode 100644 test/integration/targets/setup_rpm_repo/files/comps.xml create mode 100644 test/integration/targets/setup_rpm_repo/handlers/main.yml create mode 100644 test/integration/targets/setup_rpm_repo/library/create_repo.py create mode 100644 test/integration/targets/setup_rpm_repo/meta/main.yml create mode 100644 test/integration/targets/setup_rpm_repo/tasks/main.yml create mode 100644 test/integration/targets/setup_rpm_repo/vars/Fedora.yml create mode 100644 test/integration/targets/setup_rpm_repo/vars/RedHat-6.yml create mode 100644 test/integration/targets/setup_rpm_repo/vars/RedHat-7.yml create mode 100644 test/integration/targets/setup_rpm_repo/vars/RedHat-8.yml create mode 100644 test/integration/targets/setup_rpm_repo/vars/RedHat-9.yml create mode 100644 test/integration/targets/setup_rpm_repo/vars/main.yml create mode 100644 test/integration/targets/setup_test_user/handlers/main.yml create mode 100644 test/integration/targets/setup_test_user/tasks/default.yml create mode 100644 test/integration/targets/setup_test_user/tasks/macosx.yml create mode 100644 test/integration/targets/setup_test_user/tasks/main.yml create mode 100644 test/integration/targets/setup_win_printargv/files/PrintArgv.cs create mode 100644 test/integration/targets/setup_win_printargv/meta/main.yml create mode 100644 test/integration/targets/setup_win_printargv/tasks/main.yml create mode 100644 test/integration/targets/shell/action_plugins/test_shell.py create mode 100644 test/integration/targets/shell/aliases create mode 100644 test/integration/targets/shell/connection_plugins/test_connection_default.py create mode 100644 test/integration/targets/shell/connection_plugins/test_connection_override.py create mode 100644 test/integration/targets/shell/tasks/main.yml create mode 100644 test/integration/targets/slurp/aliases create mode 100644 test/integration/targets/slurp/files/bar.bin create mode 100644 test/integration/targets/slurp/meta/main.yml create mode 100644 test/integration/targets/slurp/tasks/main.yml create mode 100644 test/integration/targets/slurp/tasks/test_unreadable.yml create mode 100644 test/integration/targets/special_vars/aliases create mode 100644 test/integration/targets/special_vars/meta/main.yml create mode 100644 test/integration/targets/special_vars/tasks/main.yml create mode 100644 test/integration/targets/special_vars/templates/foo.j2 create mode 100644 test/integration/targets/special_vars/vars/main.yml create mode 100644 test/integration/targets/special_vars_hosts/aliases create mode 100644 test/integration/targets/special_vars_hosts/inventory create mode 100644 test/integration/targets/special_vars_hosts/playbook.yml create mode 100755 test/integration/targets/special_vars_hosts/runme.sh create mode 100644 test/integration/targets/split/aliases create mode 100644 test/integration/targets/split/tasks/main.yml create mode 100644 test/integration/targets/stat/aliases create mode 100644 test/integration/targets/stat/files/foo.txt create mode 100644 test/integration/targets/stat/meta/main.yml create mode 100644 test/integration/targets/stat/tasks/main.yml create mode 100644 test/integration/targets/strategy_free/aliases create mode 100644 test/integration/targets/strategy_free/inventory create mode 100644 test/integration/targets/strategy_free/last_include_tasks.yml create mode 100755 test/integration/targets/strategy_free/runme.sh create mode 100644 test/integration/targets/strategy_free/test_last_include_in_always.yml create mode 100644 test/integration/targets/strategy_linear/aliases create mode 100644 test/integration/targets/strategy_linear/inventory create mode 100644 test/integration/targets/strategy_linear/roles/role1/tasks/main.yml create mode 100644 test/integration/targets/strategy_linear/roles/role1/tasks/tasks.yml create mode 100644 test/integration/targets/strategy_linear/roles/role2/tasks/main.yml create mode 100755 test/integration/targets/strategy_linear/runme.sh create mode 100644 test/integration/targets/strategy_linear/task_action_templating.yml create mode 100644 test/integration/targets/strategy_linear/test_include_file_noop.yml create mode 100644 test/integration/targets/subversion/aliases create mode 100644 test/integration/targets/subversion/roles/subversion/defaults/main.yml create mode 100644 test/integration/targets/subversion/roles/subversion/files/create_repo.sh create mode 100644 test/integration/targets/subversion/roles/subversion/tasks/cleanup.yml create mode 100644 test/integration/targets/subversion/roles/subversion/tasks/main.yml create mode 100644 test/integration/targets/subversion/roles/subversion/tasks/setup.yml create mode 100644 test/integration/targets/subversion/roles/subversion/tasks/setup_selinux.yml create mode 100644 test/integration/targets/subversion/roles/subversion/tasks/tests.yml create mode 100644 test/integration/targets/subversion/roles/subversion/tasks/warnings.yml create mode 100644 test/integration/targets/subversion/roles/subversion/templates/subversion.conf.j2 create mode 100755 test/integration/targets/subversion/runme.sh create mode 100644 test/integration/targets/subversion/runme.yml create mode 100644 test/integration/targets/subversion/vars/Alpine.yml create mode 100644 test/integration/targets/subversion/vars/Debian.yml create mode 100644 test/integration/targets/subversion/vars/FreeBSD.yml create mode 100644 test/integration/targets/subversion/vars/RedHat.yml create mode 100644 test/integration/targets/subversion/vars/Suse.yml create mode 100644 test/integration/targets/subversion/vars/Ubuntu-18.yml create mode 100644 test/integration/targets/subversion/vars/Ubuntu-20.yml create mode 100644 test/integration/targets/systemd/aliases create mode 100644 test/integration/targets/systemd/defaults/main.yml create mode 100644 test/integration/targets/systemd/handlers/main.yml create mode 100644 test/integration/targets/systemd/meta/main.yml create mode 100644 test/integration/targets/systemd/tasks/main.yml create mode 100644 test/integration/targets/systemd/tasks/test_indirect_service.yml create mode 100644 test/integration/targets/systemd/tasks/test_unit_template.yml create mode 100644 test/integration/targets/systemd/templates/dummy.service create mode 100644 test/integration/targets/systemd/templates/dummy.socket create mode 100644 test/integration/targets/systemd/templates/sleeper@.service create mode 100644 test/integration/targets/systemd/vars/Debian.yml create mode 100644 test/integration/targets/systemd/vars/default.yml create mode 100644 test/integration/targets/tags/aliases create mode 100644 test/integration/targets/tags/ansible_run_tags.yml create mode 100755 test/integration/targets/tags/runme.sh create mode 100644 test/integration/targets/tags/test_tags.yml create mode 100644 test/integration/targets/task_ordering/aliases create mode 100644 test/integration/targets/task_ordering/meta/main.yml create mode 100644 test/integration/targets/task_ordering/tasks/main.yml create mode 100644 test/integration/targets/task_ordering/tasks/taskorder-include.yml create mode 100644 test/integration/targets/tasks/aliases create mode 100644 test/integration/targets/tasks/playbook.yml create mode 100755 test/integration/targets/tasks/runme.sh create mode 100644 test/integration/targets/tempfile/aliases create mode 100644 test/integration/targets/tempfile/meta/main.yml create mode 100644 test/integration/targets/tempfile/tasks/main.yml create mode 100644 test/integration/targets/template/6653.yml create mode 100644 test/integration/targets/template/72262.yml create mode 100644 test/integration/targets/template/72615.yml create mode 100644 test/integration/targets/template/aliases create mode 100644 test/integration/targets/template/ansible_managed.cfg create mode 100644 test/integration/targets/template/ansible_managed.yml create mode 100644 test/integration/targets/template/badnull1.cfg create mode 100644 test/integration/targets/template/badnull2.cfg create mode 100644 test/integration/targets/template/badnull3.cfg create mode 100644 test/integration/targets/template/corner_cases.yml create mode 100644 test/integration/targets/template/custom_tasks/tasks/main.yml create mode 100644 test/integration/targets/template/custom_tasks/templates/test create mode 100644 test/integration/targets/template/custom_template.yml create mode 100644 test/integration/targets/template/files/custom_comment_string.expected create mode 100644 test/integration/targets/template/files/encoding_1252_utf-8.expected create mode 100644 test/integration/targets/template/files/encoding_1252_windows-1252.expected create mode 100644 test/integration/targets/template/files/foo-py26.txt create mode 100644 test/integration/targets/template/files/foo.dos.txt create mode 100644 test/integration/targets/template/files/foo.txt create mode 100644 test/integration/targets/template/files/foo.unix.txt create mode 100644 test/integration/targets/template/files/import_as.expected create mode 100644 test/integration/targets/template/files/import_as_with_context.expected create mode 100644 test/integration/targets/template/files/import_with_context.expected create mode 100644 test/integration/targets/template/files/lstrip_blocks_false.expected create mode 100644 test/integration/targets/template/files/lstrip_blocks_true.expected create mode 100644 test/integration/targets/template/files/override_colon_value.expected create mode 100644 test/integration/targets/template/files/string_type_filters.expected create mode 100644 test/integration/targets/template/files/trim_blocks_false.expected create mode 100644 test/integration/targets/template/files/trim_blocks_true.expected create mode 100644 test/integration/targets/template/filter_plugins.yml create mode 100644 test/integration/targets/template/in_template_overrides.j2 create mode 100644 test/integration/targets/template/in_template_overrides.yml create mode 100644 test/integration/targets/template/lazy_eval.yml create mode 100644 test/integration/targets/template/meta/main.yml create mode 100644 test/integration/targets/template/role_filter/filter_plugins/myplugin.py create mode 100644 test/integration/targets/template/role_filter/tasks/main.yml create mode 100755 test/integration/targets/template/runme.sh create mode 100644 test/integration/targets/template/tasks/backup_test.yml create mode 100644 test/integration/targets/template/tasks/main.yml create mode 100644 test/integration/targets/template/template.yml create mode 100644 test/integration/targets/template/templates/6653-include.j2 create mode 100644 test/integration/targets/template/templates/6653.j2 create mode 100644 test/integration/targets/template/templates/72262-included.j2 create mode 100644 test/integration/targets/template/templates/72262-vars.j2 create mode 100644 test/integration/targets/template/templates/72262.j2 create mode 100644 test/integration/targets/template/templates/72615-macro-nested.j2 create mode 100644 test/integration/targets/template/templates/72615-macro.j2 create mode 100644 test/integration/targets/template/templates/72615.j2 create mode 100644 test/integration/targets/template/templates/bar create mode 100644 "test/integration/targets/template/templates/caf\303\251.j2" create mode 100644 test/integration/targets/template/templates/custom_comment_string.j2 create mode 100644 test/integration/targets/template/templates/empty_template.j2 create mode 100644 test/integration/targets/template/templates/encoding_1252.j2 create mode 100644 test/integration/targets/template/templates/foo.j2 create mode 100644 test/integration/targets/template/templates/foo2.j2 create mode 100644 test/integration/targets/template/templates/foo3.j2 create mode 100644 test/integration/targets/template/templates/for_loop.j2 create mode 100644 test/integration/targets/template/templates/for_loop_include.j2 create mode 100644 test/integration/targets/template/templates/for_loop_include_nested.j2 create mode 100644 test/integration/targets/template/templates/import_as.j2 create mode 100644 test/integration/targets/template/templates/import_as_with_context.j2 create mode 100644 test/integration/targets/template/templates/import_with_context.j2 create mode 100644 test/integration/targets/template/templates/indirect_dict.j2 create mode 100644 test/integration/targets/template/templates/json_macro.j2 create mode 100644 test/integration/targets/template/templates/lstrip_blocks.j2 create mode 100644 test/integration/targets/template/templates/macro_using_globals.j2 create mode 100644 test/integration/targets/template/templates/override_colon_value.j2 create mode 100644 test/integration/targets/template/templates/override_separator.j2 create mode 100644 test/integration/targets/template/templates/parent.j2 create mode 100644 test/integration/targets/template/templates/qux create mode 100644 test/integration/targets/template/templates/short.j2 create mode 100644 test/integration/targets/template/templates/subtemplate.j2 create mode 100644 test/integration/targets/template/templates/template_destpath_test.j2 create mode 100644 test/integration/targets/template/templates/template_import_macro_globals.j2 create mode 100644 test/integration/targets/template/templates/trim_blocks.j2 create mode 100644 test/integration/targets/template/templates/unused_vars_include.j2 create mode 100644 test/integration/targets/template/templates/unused_vars_template.j2 create mode 100644 test/integration/targets/template/undefined_in_import-import.j2 create mode 100644 test/integration/targets/template/undefined_in_import.j2 create mode 100644 test/integration/targets/template/undefined_in_import.yml create mode 100644 test/integration/targets/template/undefined_var_info.yml create mode 100644 test/integration/targets/template/unsafe.yml create mode 100644 test/integration/targets/template/unused_vars_include.yml create mode 100644 test/integration/targets/template/vars/main.yml create mode 100644 test/integration/targets/template_jinja2_non_native/46169.yml create mode 100644 test/integration/targets/template_jinja2_non_native/aliases create mode 100755 test/integration/targets/template_jinja2_non_native/runme.sh create mode 100644 test/integration/targets/template_jinja2_non_native/templates/46169.json.j2 create mode 100644 test/integration/targets/templating/aliases create mode 100644 test/integration/targets/templating/tasks/main.yml create mode 100644 test/integration/targets/templating/templates/invalid_test_name.j2 create mode 100644 test/integration/targets/templating_lookups/aliases create mode 100755 test/integration/targets/templating_lookups/runme.sh create mode 100644 test/integration/targets/templating_lookups/runme.yml create mode 100644 test/integration/targets/templating_lookups/template_deepcopy/hosts create mode 100644 test/integration/targets/templating_lookups/template_deepcopy/playbook.yml create mode 100644 test/integration/targets/templating_lookups/template_deepcopy/template.in create mode 100644 test/integration/targets/templating_lookups/template_lookup_vaulted/playbook.yml create mode 100644 test/integration/targets/templating_lookups/template_lookup_vaulted/templates/vaulted_hello.j2 create mode 100644 test/integration/targets/templating_lookups/template_lookup_vaulted/test_vault_pass create mode 100644 test/integration/targets/templating_lookups/template_lookups/mock_lookup_plugins/77788.py create mode 100644 test/integration/targets/templating_lookups/template_lookups/tasks/errors.yml create mode 100644 test/integration/targets/templating_lookups/template_lookups/tasks/main.yml create mode 100644 test/integration/targets/templating_lookups/template_lookups/vars/main.yml create mode 100644 test/integration/targets/templating_settings/aliases create mode 100644 test/integration/targets/templating_settings/dont_warn_register.yml create mode 100755 test/integration/targets/templating_settings/runme.sh create mode 100644 test/integration/targets/templating_settings/test_templating_settings.yml create mode 100644 test/integration/targets/test_core/aliases create mode 100644 test/integration/targets/test_core/inventory create mode 100755 test/integration/targets/test_core/runme.sh create mode 100644 test/integration/targets/test_core/runme.yml create mode 100644 test/integration/targets/test_core/tasks/main.yml create mode 100644 test/integration/targets/test_core/vault-password create mode 100644 test/integration/targets/test_files/aliases create mode 100644 test/integration/targets/test_files/tasks/main.yml create mode 100644 test/integration/targets/test_mathstuff/aliases create mode 100644 test/integration/targets/test_mathstuff/tasks/main.yml create mode 100644 test/integration/targets/test_uri/aliases create mode 100644 test/integration/targets/test_uri/tasks/main.yml create mode 100644 test/integration/targets/throttle/aliases create mode 100644 test/integration/targets/throttle/group_vars/all.yml create mode 100644 test/integration/targets/throttle/inventory create mode 100755 test/integration/targets/throttle/runme.sh create mode 100755 test/integration/targets/throttle/test_throttle.py create mode 100644 test/integration/targets/throttle/test_throttle.yml create mode 100644 test/integration/targets/unarchive/aliases create mode 100644 test/integration/targets/unarchive/files/foo.txt create mode 100644 "test/integration/targets/unarchive/files/test-unarchive-nonascii-\343\201\217\343\202\211\343\201\250\343\201\277.tar.gz" create mode 100644 test/integration/targets/unarchive/handlers/main.yml create mode 100644 test/integration/targets/unarchive/meta/main.yml create mode 100644 test/integration/targets/unarchive/tasks/main.yml create mode 100644 test/integration/targets/unarchive/tasks/prepare_tests.yml create mode 100644 test/integration/targets/unarchive/tasks/test_different_language_var.yml create mode 100644 test/integration/targets/unarchive/tasks/test_download.yml create mode 100644 test/integration/targets/unarchive/tasks/test_exclude.yml create mode 100644 test/integration/targets/unarchive/tasks/test_include.yml create mode 100644 test/integration/targets/unarchive/tasks/test_invalid_options.yml create mode 100644 test/integration/targets/unarchive/tasks/test_missing_binaries.yml create mode 100644 test/integration/targets/unarchive/tasks/test_missing_files.yml create mode 100644 test/integration/targets/unarchive/tasks/test_mode.yml create mode 100644 test/integration/targets/unarchive/tasks/test_non_ascii_filename.yml create mode 100644 test/integration/targets/unarchive/tasks/test_owner_group.yml create mode 100644 test/integration/targets/unarchive/tasks/test_ownership_top_folder.yml create mode 100644 test/integration/targets/unarchive/tasks/test_parent_not_writeable.yml create mode 100644 test/integration/targets/unarchive/tasks/test_quotable_characters.yml create mode 100644 test/integration/targets/unarchive/tasks/test_symlink.yml create mode 100644 test/integration/targets/unarchive/tasks/test_tar.yml create mode 100644 test/integration/targets/unarchive/tasks/test_tar_gz.yml create mode 100644 test/integration/targets/unarchive/tasks/test_tar_gz_creates.yml create mode 100644 test/integration/targets/unarchive/tasks/test_tar_gz_keep_newer.yml create mode 100644 test/integration/targets/unarchive/tasks/test_tar_gz_owner_group.yml create mode 100644 test/integration/targets/unarchive/tasks/test_tar_zst.yml create mode 100644 test/integration/targets/unarchive/tasks/test_unprivileged_user.yml create mode 100644 test/integration/targets/unarchive/tasks/test_zip.yml create mode 100644 test/integration/targets/unarchive/vars/Darwin.yml create mode 100644 test/integration/targets/unarchive/vars/FreeBSD.yml create mode 100644 test/integration/targets/unarchive/vars/Linux.yml create mode 100644 test/integration/targets/undefined/aliases create mode 100644 test/integration/targets/undefined/tasks/main.yml create mode 100644 test/integration/targets/unexpected_executor_exception/action_plugins/unexpected.py create mode 100644 test/integration/targets/unexpected_executor_exception/aliases create mode 100644 test/integration/targets/unexpected_executor_exception/tasks/main.yml create mode 100644 test/integration/targets/unicode/aliases create mode 100644 test/integration/targets/unicode/inventory create mode 100644 "test/integration/targets/unicode/k\305\231\303\255\305\276ek-ansible-project/ansible.cfg" create mode 100755 test/integration/targets/unicode/runme.sh create mode 100755 test/integration/targets/unicode/unicode-test-script create mode 100644 test/integration/targets/unicode/unicode.yml create mode 100644 test/integration/targets/unsafe_writes/aliases create mode 100644 test/integration/targets/unsafe_writes/basic.yml create mode 100755 test/integration/targets/unsafe_writes/runme.sh create mode 100644 test/integration/targets/until/action_plugins/shell_no_failed.py create mode 100644 test/integration/targets/until/aliases create mode 100644 test/integration/targets/until/tasks/main.yml create mode 100644 test/integration/targets/unvault/aliases create mode 100644 test/integration/targets/unvault/main.yml create mode 100644 test/integration/targets/unvault/password create mode 100755 test/integration/targets/unvault/runme.sh create mode 100644 test/integration/targets/unvault/vault create mode 100644 test/integration/targets/uri/aliases create mode 100644 test/integration/targets/uri/files/README create mode 100644 test/integration/targets/uri/files/fail0.json create mode 100644 test/integration/targets/uri/files/fail1.json create mode 100644 test/integration/targets/uri/files/fail10.json create mode 100644 test/integration/targets/uri/files/fail11.json create mode 100644 test/integration/targets/uri/files/fail12.json create mode 100644 test/integration/targets/uri/files/fail13.json create mode 100644 test/integration/targets/uri/files/fail14.json create mode 100644 test/integration/targets/uri/files/fail15.json create mode 100644 test/integration/targets/uri/files/fail16.json create mode 100644 test/integration/targets/uri/files/fail17.json create mode 100644 test/integration/targets/uri/files/fail18.json create mode 100644 test/integration/targets/uri/files/fail19.json create mode 100644 test/integration/targets/uri/files/fail2.json create mode 100644 test/integration/targets/uri/files/fail20.json create mode 100644 test/integration/targets/uri/files/fail21.json create mode 100644 test/integration/targets/uri/files/fail22.json create mode 100644 test/integration/targets/uri/files/fail23.json create mode 100644 test/integration/targets/uri/files/fail24.json create mode 100644 test/integration/targets/uri/files/fail25.json create mode 100644 test/integration/targets/uri/files/fail26.json create mode 100644 test/integration/targets/uri/files/fail27.json create mode 100644 test/integration/targets/uri/files/fail28.json create mode 100644 test/integration/targets/uri/files/fail29.json create mode 100644 test/integration/targets/uri/files/fail3.json create mode 100644 test/integration/targets/uri/files/fail30.json create mode 100644 test/integration/targets/uri/files/fail4.json create mode 100644 test/integration/targets/uri/files/fail5.json create mode 100644 test/integration/targets/uri/files/fail6.json create mode 100644 test/integration/targets/uri/files/fail7.json create mode 100644 test/integration/targets/uri/files/fail8.json create mode 100644 test/integration/targets/uri/files/fail9.json create mode 100644 test/integration/targets/uri/files/formdata.txt create mode 100644 test/integration/targets/uri/files/pass0.json create mode 100644 test/integration/targets/uri/files/pass1.json create mode 100644 test/integration/targets/uri/files/pass2.json create mode 100644 test/integration/targets/uri/files/pass3.json create mode 100644 test/integration/targets/uri/files/pass4.json create mode 100644 test/integration/targets/uri/files/testserver.py create mode 100644 test/integration/targets/uri/meta/main.yml create mode 100644 test/integration/targets/uri/tasks/ciphers.yml create mode 100644 test/integration/targets/uri/tasks/main.yml create mode 100644 test/integration/targets/uri/tasks/redirect-all.yml create mode 100644 test/integration/targets/uri/tasks/redirect-none.yml create mode 100644 test/integration/targets/uri/tasks/redirect-safe.yml create mode 100644 test/integration/targets/uri/tasks/redirect-urllib2.yml create mode 100644 test/integration/targets/uri/tasks/return-content.yml create mode 100644 test/integration/targets/uri/tasks/unexpected-failures.yml create mode 100644 test/integration/targets/uri/tasks/use_gssapi.yml create mode 100644 test/integration/targets/uri/tasks/use_netrc.yml create mode 100644 test/integration/targets/uri/templates/netrc.j2 create mode 100644 test/integration/targets/uri/vars/main.yml create mode 100644 test/integration/targets/user/aliases create mode 100644 test/integration/targets/user/files/userlist.sh create mode 100644 test/integration/targets/user/meta/main.yml create mode 100644 test/integration/targets/user/tasks/main.yml create mode 100644 test/integration/targets/user/tasks/test_create_system_user.yml create mode 100644 test/integration/targets/user/tasks/test_create_user.yml create mode 100644 test/integration/targets/user/tasks/test_create_user_home.yml create mode 100644 test/integration/targets/user/tasks/test_create_user_password.yml create mode 100644 test/integration/targets/user/tasks/test_create_user_uid.yml create mode 100644 test/integration/targets/user/tasks/test_expires.yml create mode 100644 test/integration/targets/user/tasks/test_expires_min_max.yml create mode 100644 test/integration/targets/user/tasks/test_expires_new_account.yml create mode 100644 test/integration/targets/user/tasks/test_expires_new_account_epoch_negative.yml create mode 100644 test/integration/targets/user/tasks/test_local.yml create mode 100644 test/integration/targets/user/tasks/test_local_expires.yml create mode 100644 test/integration/targets/user/tasks/test_no_home_fallback.yml create mode 100644 test/integration/targets/user/tasks/test_password_lock.yml create mode 100644 test/integration/targets/user/tasks/test_password_lock_new_user.yml create mode 100644 test/integration/targets/user/tasks/test_remove_user.yml create mode 100644 test/integration/targets/user/tasks/test_shadow_backup.yml create mode 100644 test/integration/targets/user/tasks/test_ssh_key_passphrase.yml create mode 100644 test/integration/targets/user/tasks/test_umask.yml create mode 100644 test/integration/targets/user/vars/main.yml create mode 100644 test/integration/targets/var_blending/aliases create mode 100644 test/integration/targets/var_blending/group_vars/all create mode 100644 test/integration/targets/var_blending/group_vars/local create mode 100644 test/integration/targets/var_blending/host_vars/testhost create mode 100644 test/integration/targets/var_blending/inventory create mode 100644 test/integration/targets/var_blending/roles/test_var_blending/defaults/main.yml create mode 100644 test/integration/targets/var_blending/roles/test_var_blending/files/foo.txt create mode 100644 test/integration/targets/var_blending/roles/test_var_blending/tasks/main.yml create mode 100644 test/integration/targets/var_blending/roles/test_var_blending/templates/foo.j2 create mode 100644 test/integration/targets/var_blending/roles/test_var_blending/vars/main.yml create mode 100644 test/integration/targets/var_blending/roles/test_var_blending/vars/more_vars.yml create mode 100755 test/integration/targets/var_blending/runme.sh create mode 100644 test/integration/targets/var_blending/test_var_blending.yml create mode 100644 test/integration/targets/var_blending/test_vars.yml create mode 100644 test/integration/targets/var_blending/vars_file.yml create mode 100644 test/integration/targets/var_inheritance/aliases create mode 100644 test/integration/targets/var_inheritance/tasks/main.yml create mode 100644 test/integration/targets/var_precedence/aliases create mode 100755 test/integration/targets/var_precedence/ansible-var-precedence-check.py create mode 100644 test/integration/targets/var_precedence/host_vars/testhost create mode 100644 test/integration/targets/var_precedence/inventory create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence/meta/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence/tasks/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_dep/defaults/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_dep/tasks/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_dep/vars/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_inven_override/tasks/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_role1/defaults/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_role1/meta/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_role1/tasks/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_role1/vars/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_role2/defaults/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_role2/tasks/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_role2/vars/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_role3/defaults/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_role3/tasks/main.yml create mode 100644 test/integration/targets/var_precedence/roles/test_var_precedence_role3/vars/main.yml create mode 100755 test/integration/targets/var_precedence/runme.sh create mode 100644 test/integration/targets/var_precedence/test_var_precedence.yml create mode 100644 test/integration/targets/var_precedence/vars/test_var_precedence.yml create mode 100644 test/integration/targets/var_reserved/aliases create mode 100644 test/integration/targets/var_reserved/reserved_varname_warning.yml create mode 100755 test/integration/targets/var_reserved/runme.sh create mode 100644 test/integration/targets/var_templating/aliases create mode 100644 test/integration/targets/var_templating/ansible_debug_template.j2 create mode 100644 test/integration/targets/var_templating/group_vars/all.yml create mode 100755 test/integration/targets/var_templating/runme.sh create mode 100644 test/integration/targets/var_templating/task_vars_templating.yml create mode 100644 test/integration/targets/var_templating/test_connection_vars.yml create mode 100644 test/integration/targets/var_templating/test_vars_with_sources.yml create mode 100644 test/integration/targets/var_templating/undall.yml create mode 100644 test/integration/targets/var_templating/undefined.yml create mode 100644 test/integration/targets/var_templating/vars/connection.yml create mode 100644 test/integration/targets/wait_for/aliases create mode 100644 test/integration/targets/wait_for/files/testserver.py create mode 100644 test/integration/targets/wait_for/files/write_utf16.py create mode 100644 test/integration/targets/wait_for/files/zombie.py create mode 100644 test/integration/targets/wait_for/meta/main.yml create mode 100644 test/integration/targets/wait_for/tasks/main.yml create mode 100644 test/integration/targets/wait_for/vars/main.yml create mode 100644 test/integration/targets/wait_for_connection/aliases create mode 100644 test/integration/targets/wait_for_connection/tasks/main.yml create mode 100644 test/integration/targets/want_json_modules_posix/aliases create mode 100644 test/integration/targets/want_json_modules_posix/library/helloworld.py create mode 100644 test/integration/targets/want_json_modules_posix/meta/main.yml create mode 100644 test/integration/targets/want_json_modules_posix/tasks/main.yml create mode 100644 test/integration/targets/win_async_wrapper/aliases create mode 100644 test/integration/targets/win_async_wrapper/library/async_test.ps1 create mode 100644 test/integration/targets/win_async_wrapper/tasks/main.yml create mode 100644 test/integration/targets/win_become/aliases create mode 100644 test/integration/targets/win_become/tasks/main.yml create mode 100644 test/integration/targets/win_exec_wrapper/aliases create mode 100644 test/integration/targets/win_exec_wrapper/library/test_all_options.ps1 create mode 100644 test/integration/targets/win_exec_wrapper/library/test_common_functions.ps1 create mode 100644 test/integration/targets/win_exec_wrapper/library/test_fail.ps1 create mode 100644 test/integration/targets/win_exec_wrapper/library/test_invalid_requires.ps1 create mode 100644 test/integration/targets/win_exec_wrapper/library/test_min_os_version.ps1 create mode 100644 test/integration/targets/win_exec_wrapper/library/test_min_ps_version.ps1 create mode 100644 test/integration/targets/win_exec_wrapper/tasks/main.yml create mode 100644 test/integration/targets/win_fetch/aliases create mode 100644 test/integration/targets/win_fetch/meta/main.yml create mode 100644 test/integration/targets/win_fetch/tasks/main.yml create mode 100644 test/integration/targets/win_module_utils/aliases create mode 100644 test/integration/targets/win_module_utils/library/csharp_util.ps1 create mode 100644 test/integration/targets/win_module_utils/library/legacy_only_new_way.ps1 create mode 100644 test/integration/targets/win_module_utils/library/legacy_only_new_way_win_line_ending.ps1 create mode 100644 test/integration/targets/win_module_utils/library/legacy_only_old_way.ps1 create mode 100644 test/integration/targets/win_module_utils/library/legacy_only_old_way_win_line_ending.ps1 create mode 100644 test/integration/targets/win_module_utils/library/recursive_requires.ps1 create mode 100644 test/integration/targets/win_module_utils/library/uses_bogus_utils.ps1 create mode 100644 test/integration/targets/win_module_utils/library/uses_local_utils.ps1 create mode 100644 test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive1.psm1 create mode 100644 test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive2.psm1 create mode 100644 test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.Recursive3.psm1 create mode 100644 test/integration/targets/win_module_utils/module_utils/Ansible.ModuleUtils.ValidTestModule.psm1 create mode 100644 test/integration/targets/win_module_utils/module_utils/Ansible.Test.cs create mode 100644 test/integration/targets/win_module_utils/tasks/main.yml create mode 100644 test/integration/targets/win_raw/aliases create mode 100644 test/integration/targets/win_raw/tasks/main.yml create mode 100644 test/integration/targets/win_script/aliases create mode 100644 test/integration/targets/win_script/defaults/main.yml create mode 100644 test/integration/targets/win_script/files/fail.bat create mode 100644 test/integration/targets/win_script/files/space path/test_script.ps1 create mode 100644 test/integration/targets/win_script/files/test_script.bat create mode 100644 test/integration/targets/win_script/files/test_script.cmd create mode 100644 test/integration/targets/win_script/files/test_script.ps1 create mode 100644 test/integration/targets/win_script/files/test_script_bool.ps1 create mode 100644 test/integration/targets/win_script/files/test_script_creates_file.ps1 create mode 100644 test/integration/targets/win_script/files/test_script_removes_file.ps1 create mode 100644 test/integration/targets/win_script/files/test_script_whoami.ps1 create mode 100644 test/integration/targets/win_script/files/test_script_with_args.ps1 create mode 100644 test/integration/targets/win_script/files/test_script_with_env.ps1 create mode 100644 test/integration/targets/win_script/files/test_script_with_errors.ps1 create mode 100644 test/integration/targets/win_script/files/test_script_with_splatting.ps1 create mode 100644 test/integration/targets/win_script/tasks/main.yml create mode 100644 test/integration/targets/windows-minimal/aliases create mode 100644 test/integration/targets/windows-minimal/library/win_ping.ps1 create mode 100644 test/integration/targets/windows-minimal/library/win_ping.py create mode 100644 test/integration/targets/windows-minimal/library/win_ping_set_attr.ps1 create mode 100644 test/integration/targets/windows-minimal/library/win_ping_strict_mode_error.ps1 create mode 100644 test/integration/targets/windows-minimal/library/win_ping_syntax_error.ps1 create mode 100644 test/integration/targets/windows-minimal/library/win_ping_throw.ps1 create mode 100644 test/integration/targets/windows-minimal/library/win_ping_throw_string.ps1 create mode 100644 test/integration/targets/windows-minimal/tasks/main.yml create mode 100644 test/integration/targets/windows-paths/aliases create mode 100644 test/integration/targets/windows-paths/tasks/main.yml create mode 100644 test/integration/targets/yaml_parsing/aliases create mode 100644 test/integration/targets/yaml_parsing/playbook.yml create mode 100644 test/integration/targets/yaml_parsing/tasks/main.yml create mode 100644 test/integration/targets/yaml_parsing/tasks/unsafe.yml create mode 100644 test/integration/targets/yaml_parsing/vars/main.yml create mode 100644 test/integration/targets/yum/aliases create mode 100644 test/integration/targets/yum/files/yum.conf create mode 100644 test/integration/targets/yum/filter_plugins/filter_list_of_tuples_by_first_param.py create mode 100644 test/integration/targets/yum/meta/main.yml create mode 100644 test/integration/targets/yum/tasks/cacheonly.yml create mode 100644 test/integration/targets/yum/tasks/check_mode_consistency.yml create mode 100644 test/integration/targets/yum/tasks/lock.yml create mode 100644 test/integration/targets/yum/tasks/main.yml create mode 100644 test/integration/targets/yum/tasks/multiarch.yml create mode 100644 test/integration/targets/yum/tasks/proxy.yml create mode 100644 test/integration/targets/yum/tasks/repo.yml create mode 100644 test/integration/targets/yum/tasks/yum.yml create mode 100644 test/integration/targets/yum/tasks/yum_group_remove.yml create mode 100644 test/integration/targets/yum/tasks/yuminstallroot.yml create mode 100644 test/integration/targets/yum/vars/main.yml create mode 100644 test/integration/targets/yum_repository/aliases create mode 100644 test/integration/targets/yum_repository/defaults/main.yml create mode 100644 test/integration/targets/yum_repository/handlers/main.yml create mode 100644 test/integration/targets/yum_repository/meta/main.yml create mode 100644 test/integration/targets/yum_repository/tasks/main.yml (limited to 'test/integration') diff --git a/test/integration/network-integration.cfg b/test/integration/network-integration.cfg new file mode 100644 index 0000000..00764bc --- /dev/null +++ b/test/integration/network-integration.cfg @@ -0,0 +1,14 @@ +# NOTE: This file is used by ansible-test to override specific Ansible constants +# This file is used by `ansible-test network-integration` + +[defaults] +host_key_checking = False +timeout = 90 + +[ssh_connection] +ssh_args = '-o UserKnownHostsFile=/dev/null' + +[persistent_connection] +command_timeout = 100 +connect_timeout = 100 +connect_retry_timeout = 100 diff --git a/test/integration/network-integration.requirements.txt b/test/integration/network-integration.requirements.txt new file mode 100644 index 0000000..9c4d78d --- /dev/null +++ b/test/integration/network-integration.requirements.txt @@ -0,0 +1 @@ +scp # needed by incidental_ios_file 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 + +# 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 . + +# 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 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 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 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 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 < 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 .. 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 ." 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 ()" + 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 ()" + 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 +# (c) 2016, Toshio Kuratomi +# 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 - + "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 < 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 < ./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 < ./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 < ansible.cfg +[defaults] +roles_path = ../:../../:../roles:roles/ +EOF + cat < 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' ' if result.cleanup else '' + + if result.cgroup_dirs: + notes += f' ' + + 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 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 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 (<unknown>, 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 (<unknown>, 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 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 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 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 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 . +# +# 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 . +# +# 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\303\251.yml" "b/test/integration/targets/ansible-vault/vault-caf\303\251.yml" new file mode 100644 index 0000000..0d179ae --- /dev/null +++ "b/test/integration/targets/ansible-vault/vault-caf\303\251.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\303\251.cfg" "b/test/integration/targets/ansible/ansible-test\303\251.cfg" new file mode 100644 index 0000000..09af947 --- /dev/null +++ "b/test/integration/targets/ansible/ansible-test\303\251.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 + +# 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 . + +- 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 + +# 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 . + +- 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= +# +- import_tasks: 'cleanup.yml' + +- name: 'record apt cache mtime' + stat: path='/var/cache/apt/pkgcache.bin' + register: cache_before + +- 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= update_cache=no +# +- import_tasks: 'cleanup.yml' + +- name: 'record apt cache mtime' + stat: path='/var/cache/apt/pkgcache.bin' + register: cache_before + +- 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= update_cache=yes +# +- import_tasks: 'cleanup.yml' + +- name: 'record apt cache mtime' + stat: path='/var/cache/apt/pkgcache.bin' + register: cache_before + +- 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= +# +- 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= (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= 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= 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 + +# 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 . + +- 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 +# 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 + +# 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 . + +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 + +# 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 . + +- 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 + +# 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 . + +- 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 +# +# 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 . +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 . + +# 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 . + +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 + +# 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 . + +- 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: " " + 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: " " + 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: " " + 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_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_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_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_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: + stdout: foo + stdout_lines: + +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: + +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: + +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: +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: +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: + +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: + +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: + +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: + +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: + +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: + +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: + +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 + +# 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 . + +- 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 + +# 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 . 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 + +# 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 . + +- 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 + +# 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 . + +- 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 +# 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 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 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 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 . +############################################# +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 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 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 . +############################################# +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 . +############################################# +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 . +############################################# +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 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 +# 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 < {{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 +# 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 +# (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 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 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 +# (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 + +# 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 . + +## +## 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 + +# 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 . + +# 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 + +# 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 . +- 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 +# 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 + +# 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 . + +- 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 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 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 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 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 + +# 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 . + +- 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 + +# 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 . + +- 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 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 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 +# 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('(?Pgood)(?Pbye)', '\\g', '\\g') }}" + 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 + +# 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 . + +- 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 + +# 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 . + +- 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 + +# 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 . +- 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 + +# 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 . + +- 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 + +# 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 . + +- 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 + +# 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 . + +- 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 + +# 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 . + +- 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 + +# 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 . + +- 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 + +# 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 . + + +- 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 . + + +- 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 + +# 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 . + +- 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 + +# 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 . + +--- +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 + +# 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 . 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 + +# 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 . + +- 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 + +# 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 . + +--- +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 + +# 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 . + +- 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 Binary files /dev/null and b/test/integration/targets/incidental_ios_file/nonascii.bin 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 . + +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 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 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 + +# 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 . + + +- 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 +# (c) 2016, Toshio Kuratomi +# 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 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 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 + +# 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 . +--- +- 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 + +# 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 . +--- +- 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 +# 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 +# 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 +# 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 +# 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 +# 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 +# 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 +# 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 +# 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 +# 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 + +# 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 . + +- 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 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 + 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 + +# 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 . + +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 + +# 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 . + +- 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 +# and @samdoran : + +# 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: + 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 . +- 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 +# 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 +# Copyright 2016, Toshio Kuratomi +# +# 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 . +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 +# (c) 2016, Toshio Kuratomi +# 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 +# (c) 2016, Toshio Kuratomi +# 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 +# (c) 2016, Toshio Kuratomi +# +# 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 . + +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 +# (c) 2016, Toshio Kuratomi +# +# 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 . + +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 +# (c) 2016, Toshio Kuratomi +# +# 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 . + +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 +# (c) 2016, Toshio Kuratomi +# +# 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 . + +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 +# (c) 2016, Toshio Kuratomi +# +# 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 . + +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 +# (c) 2016, Toshio Kuratomi +# +# 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 . + +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 +# 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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) +# 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 . + +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 + +# 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 . + +# 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 + +# 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 . + +- 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 + +# 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 . + +# 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 + +# 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 . + +# 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 + +# 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 . + +- 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 + +# 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 . + +# 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 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 + +# 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 . + +- 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 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 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: "" + unicode_type_match: "" + string_type_match: "" + float_type_match: "" + list_type_match: "" + ansible_list_type_match: "" + dict_type_match: "" + ansible_dict_type_match: "" + ansible_unicode_class_match: "" + unicode_class_match: "" + string_class_match: "" + bytes_class_match: "" + float_class_match: "" + list_class_match: "" + ansible_list_class_match: "" + dict_class_match: "" + ansible_dict_class_match: "" + expected: + test1_1: + argument_errors: [ + "argument 'tidy_expected' is of type and we were unable to convert to list: cannot be converted to a list", + "argument 'bust_some_stuff' is of type and we were unable to convert to int: cannot be converted to an int", + "argument 'some_list' is of type and we were unable to convert to list: cannot be converted to a list", + "argument 'some_dict' is of type and we were unable to convert to dict: cannot be converted to a dict", + "argument 'some_int' is of type and we were unable to convert to int: cannot be converted to an int", + "argument 'some_float' is of type and we were unable to convert to float: cannot be converted to a float", + "argument 'some_bytes' is of type and we were unable to convert to bytes: cannot be converted to a Byte value", + "argument 'some_bits' is of type and we were unable to convert to bits: 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 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 found in 'multi_level_option -> second_level'. and we were unable to convert to int: cannot be converted to an int", + "argument 'some_more_dict_options' is of type 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, "" vs "". 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 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 + +# 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 . + +## +## 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 + +# 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 . + +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 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 +# Copyright: (c) 2020, Abhijeet Kasurde +# 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 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 +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 +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 +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 +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 +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 +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 @@ + + + + customgroup + Custom Group + + false + true + 1024 + + dinginessentail + + + + + customenvgroup + Custom Environment Group + + false + false + 1024 + + landsidescalping + + + + + customenvgroup-environment + Custom Environment Group + + 1024 + + customenvgroup + + + 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 Binary files /dev/null and b/test/integration/targets/slurp/files/bar.bin 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 + +# 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 . + +- 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 + +# 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 . + +- 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 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 + +# 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 . + +- 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 + +# 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 . + +# 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 + + + Require all denied + + +{% 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 + + + DAV svn + SVNParentPath {{ subversion_server_dir }} +{% if ansible_distribution == "CentOS" and ansible_distribution_version.startswith("6") %} + Allow from all +{% else %} + Require all granted +{% endif %} + + + + DAV svn + SVNParentPath {{ subversion_server_dir }} + AuthType Basic + AuthName "Subversion repositories" + AuthUserFile {{ subversion_server_dir }}/svn-auth-users + Require valid-user + 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 + +# 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 . + +## +## 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 + +# 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 . + +- 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\303\251.j2" "b/test/integration/targets/template/templates/caf\303\251.j2" new file mode 100644 index 0000000..ef7e08e --- /dev/null +++ "b/test/integration/targets/template/templates/caf\303\251.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 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¶m=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¶m=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-\343\201\217\343\202\211\343\201\250\343\201\277.tar.gz" "b/test/integration/targets/unarchive/files/test-unarchive-nonascii-\343\201\217\343\202\211\343\201\250\343\201\277.tar.gz" new file mode 100644 index 0000000..4882b92 Binary files /dev/null and "b/test/integration/targets/unarchive/files/test-unarchive-nonascii-\343\201\217\343\202\211\343\201\250\343\201\277.tar.gz" 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\305\231\303\255\305\276ek-ansible-project/ansible.cfg" "b/test/integration/targets/unicode/k\305\231\303\255\305\276ek-ansible-project/ansible.cfg" new file mode 100644 index 0000000..6775889 --- /dev/null +++ "b/test/integration/targets/unicode/k\305\231\303\255\305\276ek-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": "" \u0022 %22 0x22 034 "", + "\/\\\"\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 + +# 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 . + +- 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 + +# 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 . +# + +- 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 + +# 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 . + +- 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\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 . + +# 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, : 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 + +# 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 . + +- 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 obj = new Dictionary(); + 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 + +# 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 . + +- 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 + +# 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 . + +- 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 , 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 . + +# 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 . + +# 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 . + +# 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 . + +# 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 . + +# 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 + +# 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 . + +- 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 +# 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' }}" -- cgit v1.2.3