summaryrefslogtreecommitdiffstats
path: root/test/units
diff options
context:
space:
mode:
Diffstat (limited to 'test/units')
-rw-r--r--test/units/__init__.py0
-rw-r--r--test/units/_vendor/__init__.py0
-rw-r--r--test/units/_vendor/test_vendor.py65
-rw-r--r--test/units/ansible_test/__init__.py0
-rw-r--r--test/units/ansible_test/ci/__init__.py0
-rw-r--r--test/units/ansible_test/ci/test_azp.py31
-rw-r--r--test/units/ansible_test/ci/util.py51
-rw-r--r--test/units/ansible_test/conftest.py14
-rw-r--r--test/units/cli/__init__.py0
-rw-r--r--test/units/cli/arguments/test_optparse_helpers.py38
-rw-r--r--test/units/cli/galaxy/test_collection_extract_tar.py61
-rw-r--r--test/units/cli/galaxy/test_display_collection.py46
-rw-r--r--test/units/cli/galaxy/test_display_header.py41
-rw-r--r--test/units/cli/galaxy/test_display_role.py28
-rw-r--r--test/units/cli/galaxy/test_execute_list.py40
-rw-r--r--test/units/cli/galaxy/test_execute_list_collection.py284
-rw-r--r--test/units/cli/galaxy/test_get_collection_widths.py34
-rw-r--r--test/units/cli/test_adhoc.py116
-rw-r--r--test/units/cli/test_cli.py381
-rw-r--r--test/units/cli/test_console.py51
-rw-r--r--test/units/cli/test_data/collection_skeleton/README.md1
-rw-r--r--test/units/cli/test_data/collection_skeleton/docs/My Collection.md1
-rw-r--r--test/units/cli/test_data/collection_skeleton/galaxy.yml.j27
-rw-r--r--test/units/cli/test_data/collection_skeleton/playbooks/main.yml0
-rw-r--r--test/units/cli/test_data/collection_skeleton/playbooks/templates/subfolder/test.conf.j22
-rw-r--r--test/units/cli/test_data/collection_skeleton/playbooks/templates/test.conf.j22
-rw-r--r--test/units/cli/test_data/collection_skeleton/plugins/action/.git_keep0
-rw-r--r--test/units/cli/test_data/collection_skeleton/plugins/filter/.git_keep0
-rw-r--r--test/units/cli/test_data/collection_skeleton/plugins/inventory/.git_keep0
-rw-r--r--test/units/cli/test_data/collection_skeleton/plugins/lookup/.git_keep0
-rw-r--r--test/units/cli/test_data/collection_skeleton/plugins/module_utils/.git_keep0
-rw-r--r--test/units/cli/test_data/collection_skeleton/plugins/modules/.git_keep0
-rw-r--r--test/units/cli/test_data/collection_skeleton/roles/common/tasks/main.yml.j23
-rw-r--r--test/units/cli/test_data/collection_skeleton/roles/common/templates/subfolder/test.conf.j22
-rw-r--r--test/units/cli/test_data/collection_skeleton/roles/common/templates/test.conf.j22
-rw-r--r--test/units/cli/test_data/role_skeleton/README.md38
-rw-r--r--test/units/cli/test_data/role_skeleton/defaults/main.yml.j22
-rw-r--r--test/units/cli/test_data/role_skeleton/files/.git_keep0
-rw-r--r--test/units/cli/test_data/role_skeleton/handlers/main.yml.j22
-rw-r--r--test/units/cli/test_data/role_skeleton/inventory1
-rw-r--r--test/units/cli/test_data/role_skeleton/meta/main.yml.j262
-rw-r--r--test/units/cli/test_data/role_skeleton/tasks/main.yml.j22
-rw-r--r--test/units/cli/test_data/role_skeleton/templates/.git_keep0
-rw-r--r--test/units/cli/test_data/role_skeleton/templates/subfolder/test.conf.j22
-rw-r--r--test/units/cli/test_data/role_skeleton/templates/test.conf.j22
-rw-r--r--test/units/cli/test_data/role_skeleton/templates_extra/templates.txt.j21
-rw-r--r--test/units/cli/test_data/role_skeleton/tests/test.yml.j25
-rw-r--r--test/units/cli/test_data/role_skeleton/vars/main.yml.j22
-rw-r--r--test/units/cli/test_doc.py130
-rw-r--r--test/units/cli/test_galaxy.py1346
-rw-r--r--test/units/cli/test_playbook.py46
-rw-r--r--test/units/cli/test_vault.py230
-rw-r--r--test/units/compat/__init__.py0
-rw-r--r--test/units/compat/mock.py23
-rw-r--r--test/units/compat/unittest.py29
-rw-r--r--test/units/config/__init__.py0
-rw-r--r--test/units/config/manager/__init__.py0
-rw-r--r--test/units/config/manager/test_find_ini_config_file.py253
-rw-r--r--test/units/config/test.cfg4
-rw-r--r--test/units/config/test.yml55
-rw-r--r--test/units/config/test2.cfg4
-rw-r--r--test/units/config/test_manager.py144
-rw-r--r--test/units/errors/__init__.py0
-rw-r--r--test/units/errors/test_errors.py150
-rw-r--r--test/units/executor/__init__.py0
-rw-r--r--test/units/executor/module_common/test_modify_module.py43
-rw-r--r--test/units/executor/module_common/test_module_common.py200
-rw-r--r--test/units/executor/module_common/test_recursive_finder.py130
-rw-r--r--test/units/executor/test_interpreter_discovery.py86
-rw-r--r--test/units/executor/test_play_iterator.py462
-rw-r--r--test/units/executor/test_playbook_executor.py148
-rw-r--r--test/units/executor/test_task_executor.py489
-rw-r--r--test/units/executor/test_task_queue_manager_callbacks.py121
-rw-r--r--test/units/executor/test_task_result.py171
-rw-r--r--test/units/galaxy/__init__.py0
-rw-r--r--test/units/galaxy/test_api.py1362
-rw-r--r--test/units/galaxy/test_collection.py1217
-rw-r--r--test/units/galaxy/test_collection_install.py1081
-rw-r--r--test/units/galaxy/test_role_install.py152
-rw-r--r--test/units/galaxy/test_role_requirements.py88
-rw-r--r--test/units/galaxy/test_token.py98
-rw-r--r--test/units/galaxy/test_user_agent.py18
-rw-r--r--test/units/inventory/__init__.py0
-rw-r--r--test/units/inventory/test_group.py155
-rw-r--r--test/units/inventory/test_host.py112
-rw-r--r--test/units/inventory_test_data/group_vars/noparse/all.yml~2
-rw-r--r--test/units/inventory_test_data/group_vars/noparse/file.txt2
-rw-r--r--test/units/inventory_test_data/group_vars/parse/all.yml2
-rw-r--r--test/units/mock/__init__.py0
-rw-r--r--test/units/mock/loader.py117
-rw-r--r--test/units/mock/path.py8
-rw-r--r--test/units/mock/procenv.py90
-rw-r--r--test/units/mock/vault_helper.py39
-rw-r--r--test/units/mock/yaml_helper.py124
-rw-r--r--test/units/module_utils/__init__.py0
-rw-r--r--test/units/module_utils/basic/__init__.py0
-rw-r--r--test/units/module_utils/basic/test__log_invocation.py55
-rw-r--r--test/units/module_utils/basic/test__symbolic_mode_to_octal.py103
-rw-r--r--test/units/module_utils/basic/test_argument_spec.py724
-rw-r--r--test/units/module_utils/basic/test_atomic_move.py223
-rw-r--r--test/units/module_utils/basic/test_command_nonexisting.py31
-rw-r--r--test/units/module_utils/basic/test_deprecate_warn.py77
-rw-r--r--test/units/module_utils/basic/test_dict_converters.py31
-rw-r--r--test/units/module_utils/basic/test_exit_json.py173
-rw-r--r--test/units/module_utils/basic/test_filesystem.py160
-rw-r--r--test/units/module_utils/basic/test_get_file_attributes.py75
-rw-r--r--test/units/module_utils/basic/test_get_module_path.py22
-rw-r--r--test/units/module_utils/basic/test_heuristic_log_sanitize.py92
-rw-r--r--test/units/module_utils/basic/test_imports.py128
-rw-r--r--test/units/module_utils/basic/test_log.py152
-rw-r--r--test/units/module_utils/basic/test_no_log.py160
-rw-r--r--test/units/module_utils/basic/test_platform_distribution.py188
-rw-r--r--test/units/module_utils/basic/test_run_command.py278
-rw-r--r--test/units/module_utils/basic/test_safe_eval.py70
-rw-r--r--test/units/module_utils/basic/test_sanitize_keys.py98
-rw-r--r--test/units/module_utils/basic/test_selinux.py190
-rw-r--r--test/units/module_utils/basic/test_set_cwd.py195
-rw-r--r--test/units/module_utils/basic/test_set_mode_if_different.py190
-rw-r--r--test/units/module_utils/basic/test_tmpdir.py119
-rw-r--r--test/units/module_utils/common/__init__.py0
-rw-r--r--test/units/module_utils/common/arg_spec/__init__.py0
-rw-r--r--test/units/module_utils/common/arg_spec/test_aliases.py132
-rw-r--r--test/units/module_utils/common/arg_spec/test_module_validate.py58
-rw-r--r--test/units/module_utils/common/arg_spec/test_sub_spec.py106
-rw-r--r--test/units/module_utils/common/arg_spec/test_validate_invalid.py134
-rw-r--r--test/units/module_utils/common/arg_spec/test_validate_valid.py335
-rw-r--r--test/units/module_utils/common/parameters/test_check_arguments.py38
-rw-r--r--test/units/module_utils/common/parameters/test_handle_aliases.py74
-rw-r--r--test/units/module_utils/common/parameters/test_list_deprecations.py44
-rw-r--r--test/units/module_utils/common/parameters/test_list_no_log_values.py228
-rw-r--r--test/units/module_utils/common/process/test_get_bin_path.py39
-rw-r--r--test/units/module_utils/common/test_collections.py175
-rw-r--r--test/units/module_utils/common/test_dict_transformations.py153
-rw-r--r--test/units/module_utils/common/test_locale.py42
-rw-r--r--test/units/module_utils/common/test_network.py79
-rw-r--r--test/units/module_utils/common/test_sys_info.py168
-rw-r--r--test/units/module_utils/common/test_utils.py46
-rw-r--r--test/units/module_utils/common/text/converters/test_container_to_bytes.py95
-rw-r--r--test/units/module_utils/common/text/converters/test_container_to_text.py78
-rw-r--r--test/units/module_utils/common/text/converters/test_json_encode_fallback.py68
-rw-r--r--test/units/module_utils/common/text/converters/test_jsonify.py27
-rw-r--r--test/units/module_utils/common/text/converters/test_to_str.py50
-rw-r--r--test/units/module_utils/common/text/formatters/test_bytes_to_human.py116
-rw-r--r--test/units/module_utils/common/text/formatters/test_human_to_bytes.py185
-rw-r--r--test/units/module_utils/common/text/formatters/test_lenient_lowercase.py68
-rw-r--r--test/units/module_utils/common/validation/test_check_missing_parameters.py35
-rw-r--r--test/units/module_utils/common/validation/test_check_mutually_exclusive.py57
-rw-r--r--test/units/module_utils/common/validation/test_check_required_arguments.py88
-rw-r--r--test/units/module_utils/common/validation/test_check_required_by.py98
-rw-r--r--test/units/module_utils/common/validation/test_check_required_if.py79
-rw-r--r--test/units/module_utils/common/validation/test_check_required_one_of.py47
-rw-r--r--test/units/module_utils/common/validation/test_check_required_together.py57
-rw-r--r--test/units/module_utils/common/validation/test_check_type_bits.py43
-rw-r--r--test/units/module_utils/common/validation/test_check_type_bool.py49
-rw-r--r--test/units/module_utils/common/validation/test_check_type_bytes.py50
-rw-r--r--test/units/module_utils/common/validation/test_check_type_dict.py34
-rw-r--r--test/units/module_utils/common/validation/test_check_type_float.py38
-rw-r--r--test/units/module_utils/common/validation/test_check_type_int.py34
-rw-r--r--test/units/module_utils/common/validation/test_check_type_jsonarg.py36
-rw-r--r--test/units/module_utils/common/validation/test_check_type_list.py32
-rw-r--r--test/units/module_utils/common/validation/test_check_type_path.py28
-rw-r--r--test/units/module_utils/common/validation/test_check_type_raw.py23
-rw-r--r--test/units/module_utils/common/validation/test_check_type_str.py33
-rw-r--r--test/units/module_utils/common/validation/test_count_terms.py40
-rw-r--r--test/units/module_utils/common/warnings/test_deprecate.py101
-rw-r--r--test/units/module_utils/common/warnings/test_warn.py61
-rw-r--r--test/units/module_utils/conftest.py72
-rw-r--r--test/units/module_utils/facts/__init__.py0
-rw-r--r--test/units/module_utils/facts/base.py65
-rw-r--r--test/units/module_utils/facts/fixtures/cpuinfo/aarch64-4cpu-cpuinfo40
-rw-r--r--test/units/module_utils/facts/fixtures/cpuinfo/arm64-4cpu-cpuinfo32
-rw-r--r--test/units/module_utils/facts/fixtures/cpuinfo/armv6-rev7-1cpu-cpuinfo12
-rw-r--r--test/units/module_utils/facts/fixtures/cpuinfo/armv7-rev3-8cpu-cpuinfo75
-rw-r--r--test/units/module_utils/facts/fixtures/cpuinfo/armv7-rev4-4cpu-cpuinfo39
-rw-r--r--test/units/module_utils/facts/fixtures/cpuinfo/ppc64-power7-rhel7-8cpu-cpuinfo44
-rw-r--r--test/units/module_utils/facts/fixtures/cpuinfo/ppc64le-power8-24cpu-cpuinfo125
-rw-r--r--test/units/module_utils/facts/fixtures/cpuinfo/sparc-t5-debian-ldom-24vcpu61
-rw-r--r--test/units/module_utils/facts/fixtures/cpuinfo/x86_64-2cpu-cpuinfo56
-rw-r--r--test/units/module_utils/facts/fixtures/cpuinfo/x86_64-4cpu-cpuinfo104
-rw-r--r--test/units/module_utils/facts/fixtures/cpuinfo/x86_64-8cpu-cpuinfo216
-rw-r--r--test/units/module_utils/facts/fixtures/distribution_files/ClearLinux10
-rw-r--r--test/units/module_utils/facts/fixtures/distribution_files/CoreOS10
-rw-r--r--test/units/module_utils/facts/fixtures/distribution_files/LinuxMint12
-rw-r--r--test/units/module_utils/facts/fixtures/distribution_files/Slackware1
-rw-r--r--test/units/module_utils/facts/fixtures/distribution_files/SlackwareCurrent1
-rw-r--r--test/units/module_utils/facts/fixtures/findmount_output.txt40
-rw-r--r--test/units/module_utils/facts/hardware/__init__.py0
-rw-r--r--test/units/module_utils/facts/hardware/aix_data.py75
-rw-r--r--test/units/module_utils/facts/hardware/linux_data.py633
-rw-r--r--test/units/module_utils/facts/hardware/test_aix_processor.py24
-rw-r--r--test/units/module_utils/facts/hardware/test_linux.py198
-rw-r--r--test/units/module_utils/facts/hardware/test_linux_get_cpu_info.py62
-rw-r--r--test/units/module_utils/facts/hardware/test_sunos_get_uptime_facts.py20
-rw-r--r--test/units/module_utils/facts/network/__init__.py0
-rw-r--r--test/units/module_utils/facts/network/test_fc_wwn.py137
-rw-r--r--test/units/module_utils/facts/network/test_generic_bsd.py217
-rw-r--r--test/units/module_utils/facts/network/test_iscsi_get_initiator.py54
-rw-r--r--test/units/module_utils/facts/other/__init__.py0
-rw-r--r--test/units/module_utils/facts/other/test_facter.py228
-rw-r--r--test/units/module_utils/facts/other/test_ohai.py6768
-rw-r--r--test/units/module_utils/facts/system/__init__.py0
-rw-r--r--test/units/module_utils/facts/system/distribution/__init__.py0
-rw-r--r--test/units/module_utils/facts/system/distribution/conftest.py21
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/almalinux_8_3_beta.json53
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2.json39
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2016.03.json40
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2018.03.json40
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2_karoo.json34
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_release_2.json34
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/arch_linux_na.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/arch_linux_no_arch-release_na.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/archlinux_rolling.json31
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/centos_6.7.json31
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/centos_8_1.json54
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/centos_stream_8.json46
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/clearlinux_26580.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/clearlinux_28120.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/core_os_1911.5.0.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/core_os_976.0.0.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/cumulus_linux_2.5.4.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/cumulus_linux_3.7.3.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/debian_10.json42
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/debian_7.9.json39
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/debian_stretch_sid.json36
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/deepin_20.4.json29
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/devuan.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/dragonfly_5.2.2.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/dragonfly_5.6.2.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/eurolinux_8.5.json46
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/fedora_22.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/fedora_25.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/fedora_31.json55
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/flatcar_3139.2.0.json43
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/kali_2019.1.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/kde_neon_16.04.json42
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/kylin_linux_advanced_server_v10.json38
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/linux_mint_18.2.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/linux_mint_19.1.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/netbsd_8.2.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/nexenta_3.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/nexenta_4.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/omnios.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/openeuler_20.03.json28
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/openindiana.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/opensuse_13.2.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_15.0.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_15.1.json36
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_42.1.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/opensuse_tumbleweed_20160917.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/osmc.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/pardus_19.1.json41
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/parrot_4.8.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/pop_os_20.04.json29
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/redhat_6.7.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/redhat_7.2.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/redhat_7.7.json43
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/rockylinux_8_3.json46
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/sles_11.3.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/sles_11.4.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/sles_12_sp0.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/sles_12_sp1.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/smartos_global_zone.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/smartos_zone.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/smgl_na.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/solaris_10.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/solaris_11.3.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/solaris_11.4.json35
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/solaris_11.json26
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/steamos_2.0.json40
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/tencentos_3_1.json50
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/truenas_12.0rc1.json39
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/ubuntu_10.04_guess.json23
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/ubuntu_12.04.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/ubuntu_14.04.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/ubuntu_16.04.json24
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/ubuntu_18.04.json39
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/uos_20.json29
-rw-r--r--test/units/module_utils/facts/system/distribution/fixtures/virtuozzo_7.3.json25
-rw-r--r--test/units/module_utils/facts/system/distribution/test_distribution_sles4sap.py33
-rw-r--r--test/units/module_utils/facts/system/distribution/test_distribution_version.py158
-rw-r--r--test/units/module_utils/facts/system/distribution/test_parse_distribution_file_ClearLinux.py51
-rw-r--r--test/units/module_utils/facts/system/distribution/test_parse_distribution_file_Slackware.py37
-rw-r--r--test/units/module_utils/facts/system/test_cmdline.py67
-rw-r--r--test/units/module_utils/facts/system/test_lsb.py108
-rw-r--r--test/units/module_utils/facts/system/test_user.py40
-rw-r--r--test/units/module_utils/facts/test_ansible_collector.py524
-rw-r--r--test/units/module_utils/facts/test_collector.py563
-rw-r--r--test/units/module_utils/facts/test_collectors.py510
-rw-r--r--test/units/module_utils/facts/test_date_time.py106
-rw-r--r--test/units/module_utils/facts/test_facts.py646
-rw-r--r--test/units/module_utils/facts/test_sysctl.py251
-rw-r--r--test/units/module_utils/facts/test_timeout.py171
-rw-r--r--test/units/module_utils/facts/test_utils.py39
-rw-r--r--test/units/module_utils/facts/virtual/__init__.py0
-rw-r--r--test/units/module_utils/facts/virtual/test_linux.py52
-rw-r--r--test/units/module_utils/json_utils/__init__.py0
-rw-r--r--test/units/module_utils/json_utils/test_filter_non_json_lines.py88
-rw-r--r--test/units/module_utils/parsing/test_convert_bool.py60
-rw-r--r--test/units/module_utils/test_api.py121
-rw-r--r--test/units/module_utils/test_connection.py22
-rw-r--r--test/units/module_utils/test_distro.py39
-rw-r--r--test/units/module_utils/urls/__init__.py0
-rw-r--r--test/units/module_utils/urls/fixtures/cbt/ecdsa_sha256.pem12
-rw-r--r--test/units/module_utils/urls/fixtures/cbt/ecdsa_sha512.pem12
-rw-r--r--test/units/module_utils/urls/fixtures/cbt/rsa-pss_sha256.pem21
-rw-r--r--test/units/module_utils/urls/fixtures/cbt/rsa-pss_sha512.pem21
-rw-r--r--test/units/module_utils/urls/fixtures/cbt/rsa_md5.pem22
-rw-r--r--test/units/module_utils/urls/fixtures/cbt/rsa_sha.pem22
-rw-r--r--test/units/module_utils/urls/fixtures/cbt/rsa_sha1.pem22
-rw-r--r--test/units/module_utils/urls/fixtures/cbt/rsa_sha256.pem22
-rw-r--r--test/units/module_utils/urls/fixtures/cbt/rsa_sha384.pem22
-rw-r--r--test/units/module_utils/urls/fixtures/cbt/rsa_sha512.pem22
-rw-r--r--test/units/module_utils/urls/fixtures/client.key28
-rw-r--r--test/units/module_utils/urls/fixtures/client.pem81
-rw-r--r--test/units/module_utils/urls/fixtures/client.txt3
-rw-r--r--test/units/module_utils/urls/fixtures/multipart.txt166
-rw-r--r--test/units/module_utils/urls/fixtures/netrc3
-rw-r--r--test/units/module_utils/urls/test_RedirectHandlerFactory.py140
-rw-r--r--test/units/module_utils/urls/test_Request.py467
-rw-r--r--test/units/module_utils/urls/test_RequestWithMethod.py22
-rw-r--r--test/units/module_utils/urls/test_channel_binding.py74
-rw-r--r--test/units/module_utils/urls/test_fetch_file.py45
-rw-r--r--test/units/module_utils/urls/test_fetch_url.py230
-rw-r--r--test/units/module_utils/urls/test_generic_urlparse.py57
-rw-r--r--test/units/module_utils/urls/test_gzip.py152
-rw-r--r--test/units/module_utils/urls/test_prepare_multipart.py103
-rw-r--r--test/units/module_utils/urls/test_split.py77
-rw-r--r--test/units/module_utils/urls/test_urls.py109
-rw-r--r--test/units/modules/__init__.py0
-rw-r--r--test/units/modules/conftest.py31
-rw-r--r--test/units/modules/test_apt.py53
-rw-r--r--test/units/modules/test_apt_key.py32
-rw-r--r--test/units/modules/test_async_wrapper.py58
-rw-r--r--test/units/modules/test_copy.py215
-rw-r--r--test/units/modules/test_hostname.py147
-rw-r--r--test/units/modules/test_iptables.py1192
-rw-r--r--test/units/modules/test_known_hosts.py110
-rw-r--r--test/units/modules/test_pip.py40
-rw-r--r--test/units/modules/test_service.py70
-rw-r--r--test/units/modules/test_service_facts.py126
-rw-r--r--test/units/modules/test_systemd.py52
-rw-r--r--test/units/modules/test_unarchive.py93
-rw-r--r--test/units/modules/test_yum.py222
-rw-r--r--test/units/modules/utils.py50
-rw-r--r--test/units/parsing/__init__.py0
-rw-r--r--test/units/parsing/fixtures/ajson.json19
-rw-r--r--test/units/parsing/fixtures/vault.yml6
-rw-r--r--test/units/parsing/test_ajson.py186
-rw-r--r--test/units/parsing/test_dataloader.py239
-rw-r--r--test/units/parsing/test_mod_args.py137
-rw-r--r--test/units/parsing/test_splitter.py110
-rw-r--r--test/units/parsing/test_unquote.py51
-rw-r--r--test/units/parsing/utils/__init__.py0
-rw-r--r--test/units/parsing/utils/test_addresses.py98
-rw-r--r--test/units/parsing/utils/test_jsonify.py39
-rw-r--r--test/units/parsing/utils/test_yaml.py34
-rw-r--r--test/units/parsing/vault/__init__.py0
-rw-r--r--test/units/parsing/vault/test_vault.py870
-rw-r--r--test/units/parsing/vault/test_vault_editor.py521
-rw-r--r--test/units/parsing/yaml/__init__.py0
-rw-r--r--test/units/parsing/yaml/test_constructor.py84
-rw-r--r--test/units/parsing/yaml/test_dumper.py123
-rw-r--r--test/units/parsing/yaml/test_loader.py432
-rw-r--r--test/units/parsing/yaml/test_objects.py164
-rw-r--r--test/units/playbook/__init__.py0
-rw-r--r--test/units/playbook/role/__init__.py0
-rw-r--r--test/units/playbook/role/test_include_role.py251
-rw-r--r--test/units/playbook/role/test_role.py423
-rw-r--r--test/units/playbook/test_attribute.py57
-rw-r--r--test/units/playbook/test_base.py615
-rw-r--r--test/units/playbook/test_block.py82
-rw-r--r--test/units/playbook/test_collectionsearch.py78
-rw-r--r--test/units/playbook/test_conditional.py212
-rw-r--r--test/units/playbook/test_helpers.py373
-rw-r--r--test/units/playbook/test_included_file.py332
-rw-r--r--test/units/playbook/test_play.py291
-rw-r--r--test/units/playbook/test_play_context.py94
-rw-r--r--test/units/playbook/test_playbook.py61
-rw-r--r--test/units/playbook/test_taggable.py105
-rw-r--r--test/units/playbook/test_task.py114
-rw-r--r--test/units/plugins/__init__.py0
-rw-r--r--test/units/plugins/action/__init__.py0
-rw-r--r--test/units/plugins/action/test_action.py912
-rw-r--r--test/units/plugins/action/test_gather_facts.py98
-rw-r--r--test/units/plugins/action/test_pause.py89
-rw-r--r--test/units/plugins/action/test_raw.py105
-rw-r--r--test/units/plugins/become/__init__.py0
-rw-r--r--test/units/plugins/become/conftest.py37
-rw-r--r--test/units/plugins/become/test_su.py30
-rw-r--r--test/units/plugins/become/test_sudo.py67
-rw-r--r--test/units/plugins/cache/__init__.py0
-rw-r--r--test/units/plugins/cache/test_cache.py199
-rw-r--r--test/units/plugins/callback/__init__.py0
-rw-r--r--test/units/plugins/callback/test_callback.py416
-rw-r--r--test/units/plugins/connection/__init__.py0
-rw-r--r--test/units/plugins/connection/test_connection.py163
-rw-r--r--test/units/plugins/connection/test_local.py40
-rw-r--r--test/units/plugins/connection/test_paramiko.py56
-rw-r--r--test/units/plugins/connection/test_psrp.py233
-rw-r--r--test/units/plugins/connection/test_ssh.py696
-rw-r--r--test/units/plugins/connection/test_winrm.py443
-rw-r--r--test/units/plugins/filter/__init__.py0
-rw-r--r--test/units/plugins/filter/test_core.py43
-rw-r--r--test/units/plugins/filter/test_mathstuff.py162
-rw-r--r--test/units/plugins/inventory/__init__.py0
-rw-r--r--test/units/plugins/inventory/test_constructed.py337
-rw-r--r--test/units/plugins/inventory/test_inventory.py208
-rw-r--r--test/units/plugins/inventory/test_script.py105
-rw-r--r--test/units/plugins/loader_fixtures/__init__.py0
-rw-r--r--test/units/plugins/loader_fixtures/import_fixture.py9
-rw-r--r--test/units/plugins/lookup/__init__.py0
-rw-r--r--test/units/plugins/lookup/test_env.py35
-rw-r--r--test/units/plugins/lookup/test_ini.py64
-rw-r--r--test/units/plugins/lookup/test_password.py577
-rw-r--r--test/units/plugins/lookup/test_url.py26
-rw-r--r--test/units/plugins/shell/__init__.py0
-rw-r--r--test/units/plugins/shell/test_cmd.py19
-rw-r--r--test/units/plugins/shell/test_powershell.py83
-rw-r--r--test/units/plugins/strategy/__init__.py0
-rw-r--r--test/units/plugins/strategy/test_linear.py320
-rw-r--r--test/units/plugins/strategy/test_strategy.py492
-rw-r--r--test/units/plugins/test_plugins.py133
-rw-r--r--test/units/regex/test_invalid_var_names.py27
-rw-r--r--test/units/requirements.txt4
-rw-r--r--test/units/template/__init__.py0
-rw-r--r--test/units/template/test_native_concat.py25
-rw-r--r--test/units/template/test_templar.py470
-rw-r--r--test/units/template/test_template_utilities.py117
-rw-r--r--test/units/template/test_vars.py41
-rw-r--r--test/units/test_constants.py94
-rw-r--r--test/units/test_context.py27
-rw-r--r--test/units/test_no_tty.py7
-rw-r--r--test/units/utils/__init__.py0
-rw-r--r--test/units/utils/collection_loader/__init__.py0
-rw-r--r--test/units/utils/collection_loader/fixtures/collections/ansible_collections/ansible/builtin/plugins/modules/shouldnotload.py4
-rw-r--r--test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/meta/runtime.yml4
-rw-r--r--test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py8
-rw-r--r--test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/__init__.py0
-rw-r--r--test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_other_util.py4
-rw-r--r--test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_util.py6
-rw-r--r--test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/__init__.py5
-rw-r--r--test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/amodule.py6
-rw-r--r--test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/roles/some_role/.gitkeep0
-rw-r--r--test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/__init__.py5
-rw-r--r--test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/ansible/__init__.py5
-rw-r--r--test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/__init__.py5
-rw-r--r--test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/testcoll/__init__.py5
-rw-r--r--test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/ansible/playbook_adj_other/.gitkeep0
-rw-r--r--test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/freshns/playbook_adj_other/.gitkeep0
-rw-r--r--test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/testns/playbook_adj_other/.gitkeep0
-rw-r--r--test/units/utils/collection_loader/test_collection_loader.py868
-rw-r--r--test/units/utils/display/test_broken_cowsay.py27
-rw-r--r--test/units/utils/display/test_display.py20
-rw-r--r--test/units/utils/display/test_logger.py31
-rw-r--r--test/units/utils/display/test_warning.py42
-rw-r--r--test/units/utils/test_cleanup_tmp_file.py48
-rw-r--r--test/units/utils/test_context_objects.py70
-rw-r--r--test/units/utils/test_display.py135
-rw-r--r--test/units/utils/test_encrypt.py220
-rw-r--r--test/units/utils/test_helpers.py34
-rw-r--r--test/units/utils/test_isidentifier.py49
-rw-r--r--test/units/utils/test_plugin_docs.py333
-rw-r--r--test/units/utils/test_shlex.py41
-rw-r--r--test/units/utils/test_unsafe_proxy.py121
-rw-r--r--test/units/utils/test_vars.py284
-rw-r--r--test/units/utils/test_version.py335
-rw-r--r--test/units/vars/__init__.py0
-rw-r--r--test/units/vars/test_module_response_deepcopy.py60
-rw-r--r--test/units/vars/test_variable_manager.py307
469 files changed, 53650 insertions, 0 deletions
diff --git a/test/units/__init__.py b/test/units/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/__init__.py
diff --git a/test/units/_vendor/__init__.py b/test/units/_vendor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/_vendor/__init__.py
diff --git a/test/units/_vendor/test_vendor.py b/test/units/_vendor/test_vendor.py
new file mode 100644
index 0000000..84b850e
--- /dev/null
+++ b/test/units/_vendor/test_vendor.py
@@ -0,0 +1,65 @@
+# (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 os
+import pkgutil
+import pytest
+import sys
+
+from unittest.mock import MagicMock, NonCallableMagicMock, patch
+
+
+def reset_internal_vendor_package():
+ import ansible
+ ansible_vendor_path = os.path.join(os.path.dirname(ansible.__file__), '_vendor')
+
+ if ansible_vendor_path in sys.path:
+ sys.path.remove(ansible_vendor_path)
+
+ for pkg in ['ansible._vendor', 'ansible']:
+ if pkg in sys.modules:
+ del sys.modules[pkg]
+
+
+def test_package_path_masking():
+ from ansible import _vendor
+
+ assert hasattr(_vendor, '__path__') and _vendor.__path__ == []
+
+
+def test_no_vendored():
+ reset_internal_vendor_package()
+ with patch.object(pkgutil, 'iter_modules', return_value=[]):
+ previous_path = list(sys.path)
+ import ansible
+ ansible_vendor_path = os.path.join(os.path.dirname(ansible.__file__), '_vendor')
+
+ assert ansible_vendor_path not in sys.path
+ assert sys.path == previous_path
+
+
+def test_vendored(vendored_pkg_names=None):
+ if not vendored_pkg_names:
+ vendored_pkg_names = ['boguspkg']
+ reset_internal_vendor_package()
+ with patch.object(pkgutil, 'iter_modules', return_value=list((None, p, None) for p in vendored_pkg_names)):
+ previous_path = list(sys.path)
+ import ansible
+ ansible_vendor_path = os.path.join(os.path.dirname(ansible.__file__), '_vendor')
+ assert sys.path[0] == ansible_vendor_path
+
+ if ansible_vendor_path in previous_path:
+ previous_path.remove(ansible_vendor_path)
+
+ assert sys.path[1:] == previous_path
+
+
+def test_vendored_conflict():
+ with pytest.warns(UserWarning) as w:
+ import pkgutil
+ import sys
+ test_vendored(vendored_pkg_names=['sys', 'pkgutil']) # pass a real package we know is already loaded
+ assert any('pkgutil, sys' in str(msg.message) for msg in w) # ensure both conflicting modules are listed and sorted
diff --git a/test/units/ansible_test/__init__.py b/test/units/ansible_test/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/ansible_test/__init__.py
diff --git a/test/units/ansible_test/ci/__init__.py b/test/units/ansible_test/ci/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/ansible_test/ci/__init__.py
diff --git a/test/units/ansible_test/ci/test_azp.py b/test/units/ansible_test/ci/test_azp.py
new file mode 100644
index 0000000..69c4fa4
--- /dev/null
+++ b/test/units/ansible_test/ci/test_azp.py
@@ -0,0 +1,31 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from .util import common_auth_test
+
+
+def test_auth():
+ # noinspection PyProtectedMember
+ from ansible_test._internal.ci.azp import (
+ AzurePipelinesAuthHelper,
+ )
+
+ class TestAzurePipelinesAuthHelper(AzurePipelinesAuthHelper):
+ def __init__(self):
+ self.public_key_pem = None
+ self.private_key_pem = None
+
+ def publish_public_key(self, public_key_pem):
+ # avoid publishing key
+ self.public_key_pem = public_key_pem
+
+ def initialize_private_key(self):
+ # cache in memory instead of on disk
+ if not self.private_key_pem:
+ self.private_key_pem = self.generate_private_key()
+
+ return self.private_key_pem
+
+ auth = TestAzurePipelinesAuthHelper()
+
+ common_auth_test(auth)
diff --git a/test/units/ansible_test/ci/util.py b/test/units/ansible_test/ci/util.py
new file mode 100644
index 0000000..2273f0a
--- /dev/null
+++ b/test/units/ansible_test/ci/util.py
@@ -0,0 +1,51 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import base64
+import json
+import re
+
+
+def common_auth_test(auth):
+ private_key_pem = auth.initialize_private_key()
+ public_key_pem = auth.public_key_pem
+
+ extract_pem_key(private_key_pem, private=True)
+ extract_pem_key(public_key_pem, private=False)
+
+ request = dict(hello='World')
+ auth.sign_request(request)
+
+ verify_signature(request, public_key_pem)
+
+
+def extract_pem_key(value, private):
+ assert isinstance(value, type(u''))
+
+ key_type = '(EC )?PRIVATE' if private else 'PUBLIC'
+ pattern = r'^-----BEGIN ' + key_type + r' KEY-----\n(?P<key>.*?)\n-----END ' + key_type + r' KEY-----\n$'
+ match = re.search(pattern, value, flags=re.DOTALL)
+
+ assert match, 'key "%s" does not match pattern "%s"' % (value, pattern)
+
+ base64.b64decode(match.group('key')) # make sure the key can be decoded
+
+
+def verify_signature(request, public_key_pem):
+ signature = request.pop('signature')
+ payload_bytes = json.dumps(request, sort_keys=True).encode()
+
+ assert isinstance(signature, type(u''))
+
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives import hashes
+ from cryptography.hazmat.primitives.asymmetric import ec
+ from cryptography.hazmat.primitives.serialization import load_pem_public_key
+
+ public_key = load_pem_public_key(public_key_pem.encode(), default_backend())
+
+ public_key.verify(
+ base64.b64decode(signature.encode()),
+ payload_bytes,
+ ec.ECDSA(hashes.SHA256()),
+ )
diff --git a/test/units/ansible_test/conftest.py b/test/units/ansible_test/conftest.py
new file mode 100644
index 0000000..9ec9a02
--- /dev/null
+++ b/test/units/ansible_test/conftest.py
@@ -0,0 +1,14 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+import os
+import pytest
+import sys
+
+
+@pytest.fixture(autouse=True, scope='session')
+def ansible_test():
+ """Make ansible_test available on sys.path for unit testing ansible-test."""
+ test_lib = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'lib')
+ sys.path.insert(0, test_lib)
diff --git a/test/units/cli/__init__.py b/test/units/cli/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/cli/__init__.py
diff --git a/test/units/cli/arguments/test_optparse_helpers.py b/test/units/cli/arguments/test_optparse_helpers.py
new file mode 100644
index 0000000..082c9be
--- /dev/null
+++ b/test/units/cli/arguments/test_optparse_helpers.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# 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
+
+import sys
+
+import pytest
+
+from ansible import constants as C
+from ansible.cli.arguments import option_helpers as opt_help
+from ansible import __path__ as ansible_path
+from ansible.release import __version__ as ansible_version
+
+if C.DEFAULT_MODULE_PATH is None:
+ cpath = u'Default w/o overrides'
+else:
+ cpath = C.DEFAULT_MODULE_PATH
+
+FAKE_PROG = u'ansible-cli-test'
+VERSION_OUTPUT = opt_help.version(prog=FAKE_PROG)
+
+
+@pytest.mark.parametrize(
+ 'must_have', [
+ FAKE_PROG + u' [core %s]' % ansible_version,
+ u'config file = %s' % C.CONFIG_FILE,
+ u'configured module search path = %s' % cpath,
+ u'ansible python module location = %s' % ':'.join(ansible_path),
+ u'ansible collection location = %s' % ':'.join(C.COLLECTIONS_PATHS),
+ u'executable location = ',
+ u'python version = %s' % ''.join(sys.version.splitlines()),
+ ]
+)
+def test_option_helper_version(must_have):
+ assert must_have in VERSION_OUTPUT
diff --git a/test/units/cli/galaxy/test_collection_extract_tar.py b/test/units/cli/galaxy/test_collection_extract_tar.py
new file mode 100644
index 0000000..526442c
--- /dev/null
+++ b/test/units/cli/galaxy/test_collection_extract_tar.py
@@ -0,0 +1,61 @@
+# -*- 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
+
+import pytest
+
+from ansible.errors import AnsibleError
+from ansible.galaxy.collection import _extract_tar_dir
+
+
+@pytest.fixture
+def fake_tar_obj(mocker):
+ m_tarfile = mocker.Mock()
+ m_tarfile.type = mocker.Mock(return_value=b'99')
+ m_tarfile.SYMTYPE = mocker.Mock(return_value=b'22')
+
+ return m_tarfile
+
+
+def test_extract_tar_member_trailing_sep(mocker):
+ m_tarfile = mocker.Mock()
+ m_tarfile.getmember = mocker.Mock(side_effect=KeyError)
+
+ with pytest.raises(AnsibleError, match='Unable to extract'):
+ _extract_tar_dir(m_tarfile, '/some/dir/', b'/some/dest')
+
+ assert m_tarfile.getmember.call_count == 1
+
+
+def test_extract_tar_member_no_trailing_sep(mocker):
+ m_tarfile = mocker.Mock()
+ m_tarfile.getmember = mocker.Mock(side_effect=KeyError)
+
+ with pytest.raises(AnsibleError, match='Unable to extract'):
+ _extract_tar_dir(m_tarfile, '/some/dir', b'/some/dest')
+
+ assert m_tarfile.getmember.call_count == 2
+
+
+def test_extract_tar_dir_exists(mocker, fake_tar_obj):
+ mocker.patch('os.makedirs', return_value=None)
+ m_makedir = mocker.patch('os.mkdir', return_value=None)
+ mocker.patch('os.path.isdir', return_value=True)
+
+ _extract_tar_dir(fake_tar_obj, '/some/dir', b'/some/dest')
+
+ assert not m_makedir.called
+
+
+def test_extract_tar_dir_does_not_exist(mocker, fake_tar_obj):
+ mocker.patch('os.makedirs', return_value=None)
+ m_makedir = mocker.patch('os.mkdir', return_value=None)
+ mocker.patch('os.path.isdir', return_value=False)
+
+ _extract_tar_dir(fake_tar_obj, '/some/dir', b'/some/dest')
+
+ assert m_makedir.called
+ assert m_makedir.call_args[0] == (b'/some/dir', 0o0755)
diff --git a/test/units/cli/galaxy/test_display_collection.py b/test/units/cli/galaxy/test_display_collection.py
new file mode 100644
index 0000000..c86227b
--- /dev/null
+++ b/test/units/cli/galaxy/test_display_collection.py
@@ -0,0 +1,46 @@
+# -*- 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
+
+import pytest
+
+from ansible.cli.galaxy import _display_collection
+from ansible.galaxy.dependency_resolution.dataclasses import Requirement
+
+
+@pytest.fixture
+def collection_object():
+ def _cobj(fqcn='sandwiches.ham'):
+ return Requirement(fqcn, '1.5.0', None, 'galaxy', None)
+ return _cobj
+
+
+def test_display_collection(capsys, collection_object):
+ _display_collection(collection_object())
+ out, err = capsys.readouterr()
+
+ assert out == 'sandwiches.ham 1.5.0 \n'
+
+
+def test_display_collections_small_max_widths(capsys, collection_object):
+ _display_collection(collection_object(), 1, 1)
+ out, err = capsys.readouterr()
+
+ assert out == 'sandwiches.ham 1.5.0 \n'
+
+
+def test_display_collections_large_max_widths(capsys, collection_object):
+ _display_collection(collection_object(), 20, 20)
+ out, err = capsys.readouterr()
+
+ assert out == 'sandwiches.ham 1.5.0 \n'
+
+
+def test_display_collection_small_minimum_widths(capsys, collection_object):
+ _display_collection(collection_object('a.b'), min_cwidth=0, min_vwidth=0)
+ out, err = capsys.readouterr()
+
+ assert out == 'a.b 1.5.0 \n'
diff --git a/test/units/cli/galaxy/test_display_header.py b/test/units/cli/galaxy/test_display_header.py
new file mode 100644
index 0000000..ae926b0
--- /dev/null
+++ b/test/units/cli/galaxy/test_display_header.py
@@ -0,0 +1,41 @@
+# -*- 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
+
+from ansible.cli.galaxy import _display_header
+
+
+def test_display_header_default(capsys):
+ _display_header('/collections/path', 'h1', 'h2')
+ out, err = capsys.readouterr()
+ out_lines = out.splitlines()
+
+ assert out_lines[0] == ''
+ assert out_lines[1] == '# /collections/path'
+ assert out_lines[2] == 'h1 h2 '
+ assert out_lines[3] == '---------- -------'
+
+
+def test_display_header_widths(capsys):
+ _display_header('/collections/path', 'Collection', 'Version', 18, 18)
+ out, err = capsys.readouterr()
+ out_lines = out.splitlines()
+
+ assert out_lines[0] == ''
+ assert out_lines[1] == '# /collections/path'
+ assert out_lines[2] == 'Collection Version '
+ assert out_lines[3] == '------------------ ------------------'
+
+
+def test_display_header_small_widths(capsys):
+ _display_header('/collections/path', 'Col', 'Ver', 1, 1)
+ out, err = capsys.readouterr()
+ out_lines = out.splitlines()
+
+ assert out_lines[0] == ''
+ assert out_lines[1] == '# /collections/path'
+ assert out_lines[2] == 'Col Ver'
+ assert out_lines[3] == '--- ---'
diff --git a/test/units/cli/galaxy/test_display_role.py b/test/units/cli/galaxy/test_display_role.py
new file mode 100644
index 0000000..e23a772
--- /dev/null
+++ b/test/units/cli/galaxy/test_display_role.py
@@ -0,0 +1,28 @@
+# -*- 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
+
+from ansible.cli.galaxy import _display_role
+
+
+def test_display_role(mocker, capsys):
+ mocked_galaxy_role = mocker.Mock(install_info=None)
+ mocked_galaxy_role.name = 'testrole'
+ _display_role(mocked_galaxy_role)
+ out, err = capsys.readouterr()
+ out_lines = out.splitlines()
+
+ assert out_lines[0] == '- testrole, (unknown version)'
+
+
+def test_display_role_known_version(mocker, capsys):
+ mocked_galaxy_role = mocker.Mock(install_info={'version': '1.0.0'})
+ mocked_galaxy_role.name = 'testrole'
+ _display_role(mocked_galaxy_role)
+ out, err = capsys.readouterr()
+ out_lines = out.splitlines()
+
+ assert out_lines[0] == '- testrole, 1.0.0'
diff --git a/test/units/cli/galaxy/test_execute_list.py b/test/units/cli/galaxy/test_execute_list.py
new file mode 100644
index 0000000..41fee0b
--- /dev/null
+++ b/test/units/cli/galaxy/test_execute_list.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+# -*- 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
+
+import pytest
+
+from ansible import context
+from ansible.cli.galaxy import GalaxyCLI
+
+
+def test_execute_list_role_called(mocker):
+ """Make sure the correct method is called for a role"""
+
+ gc = GalaxyCLI(['ansible-galaxy', 'role', 'list'])
+ context.CLIARGS._store = {'type': 'role'}
+ execute_list_role_mock = mocker.patch('ansible.cli.galaxy.GalaxyCLI.execute_list_role', side_effect=AttributeError('raised intentionally'))
+ execute_list_collection_mock = mocker.patch('ansible.cli.galaxy.GalaxyCLI.execute_list_collection', side_effect=AttributeError('raised intentionally'))
+ with pytest.raises(AttributeError):
+ gc.execute_list()
+
+ assert execute_list_role_mock.call_count == 1
+ assert execute_list_collection_mock.call_count == 0
+
+
+def test_execute_list_collection_called(mocker):
+ """Make sure the correct method is called for a collection"""
+
+ gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list'])
+ context.CLIARGS._store = {'type': 'collection'}
+ execute_list_role_mock = mocker.patch('ansible.cli.galaxy.GalaxyCLI.execute_list_role', side_effect=AttributeError('raised intentionally'))
+ execute_list_collection_mock = mocker.patch('ansible.cli.galaxy.GalaxyCLI.execute_list_collection', side_effect=AttributeError('raised intentionally'))
+ with pytest.raises(AttributeError):
+ gc.execute_list()
+
+ assert execute_list_role_mock.call_count == 0
+ assert execute_list_collection_mock.call_count == 1
diff --git a/test/units/cli/galaxy/test_execute_list_collection.py b/test/units/cli/galaxy/test_execute_list_collection.py
new file mode 100644
index 0000000..e8a834d
--- /dev/null
+++ b/test/units/cli/galaxy/test_execute_list_collection.py
@@ -0,0 +1,284 @@
+# -*- 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
+
+import pytest
+
+from ansible import context
+from ansible.cli.galaxy import GalaxyCLI
+from ansible.errors import AnsibleError, AnsibleOptionsError
+from ansible.galaxy import collection
+from ansible.galaxy.dependency_resolution.dataclasses import Requirement
+from ansible.module_utils._text import to_native
+
+
+def path_exists(path):
+ if to_native(path) == '/root/.ansible/collections/ansible_collections/sandwiches/ham':
+ return False
+ elif to_native(path) == '/usr/share/ansible/collections/ansible_collections/sandwiches/reuben':
+ return False
+ elif to_native(path) == 'nope':
+ return False
+ else:
+ return True
+
+
+def isdir(path):
+ if to_native(path) == 'nope':
+ return False
+ else:
+ return True
+
+
+def cliargs(collections_paths=None, collection_name=None):
+ if collections_paths is None:
+ collections_paths = ['~/root/.ansible/collections', '/usr/share/ansible/collections']
+
+ context.CLIARGS._store = {
+ 'collections_path': collections_paths,
+ 'collection': collection_name,
+ 'type': 'collection',
+ 'output_format': 'human'
+ }
+
+
+@pytest.fixture
+def mock_collection_objects(mocker):
+ mocker.patch('ansible.cli.galaxy.GalaxyCLI._resolve_path', side_effect=['/root/.ansible/collections', '/usr/share/ansible/collections'])
+ mocker.patch('ansible.cli.galaxy.validate_collection_path',
+ side_effect=['/root/.ansible/collections/ansible_collections', '/usr/share/ansible/collections/ansible_collections'])
+
+ collection_args_1 = (
+ (
+ 'sandwiches.pbj',
+ '1.5.0',
+ None,
+ 'dir',
+ None,
+ ),
+ (
+ 'sandwiches.reuben',
+ '2.5.0',
+ None,
+ 'dir',
+ None,
+ ),
+ )
+
+ collection_args_2 = (
+ (
+ 'sandwiches.pbj',
+ '1.0.0',
+ None,
+ 'dir',
+ None,
+ ),
+ (
+ 'sandwiches.ham',
+ '1.0.0',
+ None,
+ 'dir',
+ None,
+ ),
+ )
+
+ collections_path_1 = [Requirement(*cargs) for cargs in collection_args_1]
+ collections_path_2 = [Requirement(*cargs) for cargs in collection_args_2]
+
+ mocker.patch('ansible.cli.galaxy.find_existing_collections', side_effect=[collections_path_1, collections_path_2])
+
+
+@pytest.fixture
+def mock_from_path(mocker):
+ def _from_path(collection_name='pbj'):
+ collection_args = {
+ 'sandwiches.pbj': (
+ (
+ 'sandwiches.pbj',
+ '1.5.0',
+ None,
+ 'dir',
+ None,
+ ),
+ (
+ 'sandwiches.pbj',
+ '1.0.0',
+ None,
+ 'dir',
+ None,
+ ),
+ ),
+ 'sandwiches.ham': (
+ (
+ 'sandwiches.ham',
+ '1.0.0',
+ None,
+ 'dir',
+ None,
+ ),
+ ),
+ }
+
+ from_path_objects = [Requirement(*args) for args in collection_args[collection_name]]
+ mocker.patch('ansible.cli.galaxy.Requirement.from_dir_path_as_unknown', side_effect=from_path_objects)
+
+ return _from_path
+
+
+def test_execute_list_collection_all(mocker, capsys, mock_collection_objects, tmp_path_factory):
+ """Test listing all collections from multiple paths"""
+
+ cliargs()
+
+ mocker.patch('os.path.exists', return_value=True)
+ mocker.patch('os.path.isdir', return_value=True)
+ gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list'])
+ tmp_path = tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+ gc.execute_list_collection(artifacts_manager=concrete_artifact_cm)
+
+ out, err = capsys.readouterr()
+ out_lines = out.splitlines()
+
+ assert len(out_lines) == 12
+ assert out_lines[0] == ''
+ assert out_lines[1] == '# /root/.ansible/collections/ansible_collections'
+ assert out_lines[2] == 'Collection Version'
+ assert out_lines[3] == '----------------- -------'
+ assert out_lines[4] == 'sandwiches.pbj 1.5.0 '
+ assert out_lines[5] == 'sandwiches.reuben 2.5.0 '
+ assert out_lines[6] == ''
+ assert out_lines[7] == '# /usr/share/ansible/collections/ansible_collections'
+ assert out_lines[8] == 'Collection Version'
+ assert out_lines[9] == '-------------- -------'
+ assert out_lines[10] == 'sandwiches.ham 1.0.0 '
+ assert out_lines[11] == 'sandwiches.pbj 1.0.0 '
+
+
+def test_execute_list_collection_specific(mocker, capsys, mock_collection_objects, mock_from_path, tmp_path_factory):
+ """Test listing a specific collection"""
+
+ collection_name = 'sandwiches.ham'
+ mock_from_path(collection_name)
+
+ cliargs(collection_name=collection_name)
+ mocker.patch('os.path.exists', path_exists)
+ mocker.patch('os.path.isdir', return_value=True)
+ mocker.patch('ansible.galaxy.collection.validate_collection_name', collection_name)
+ mocker.patch('ansible.cli.galaxy._get_collection_widths', return_value=(14, 5))
+
+ gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', collection_name])
+ tmp_path = tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+ gc.execute_list_collection(artifacts_manager=concrete_artifact_cm)
+
+ out, err = capsys.readouterr()
+ out_lines = out.splitlines()
+
+ assert len(out_lines) == 5
+ assert out_lines[0] == ''
+ assert out_lines[1] == '# /usr/share/ansible/collections/ansible_collections'
+ assert out_lines[2] == 'Collection Version'
+ assert out_lines[3] == '-------------- -------'
+ assert out_lines[4] == 'sandwiches.ham 1.0.0 '
+
+
+def test_execute_list_collection_specific_duplicate(mocker, capsys, mock_collection_objects, mock_from_path, tmp_path_factory):
+ """Test listing a specific collection that exists at multiple paths"""
+
+ collection_name = 'sandwiches.pbj'
+ mock_from_path(collection_name)
+
+ cliargs(collection_name=collection_name)
+ mocker.patch('os.path.exists', path_exists)
+ mocker.patch('os.path.isdir', return_value=True)
+ mocker.patch('ansible.galaxy.collection.validate_collection_name', collection_name)
+
+ gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', collection_name])
+ tmp_path = tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+ gc.execute_list_collection(artifacts_manager=concrete_artifact_cm)
+
+ out, err = capsys.readouterr()
+ out_lines = out.splitlines()
+
+ assert len(out_lines) == 10
+ assert out_lines[0] == ''
+ assert out_lines[1] == '# /root/.ansible/collections/ansible_collections'
+ assert out_lines[2] == 'Collection Version'
+ assert out_lines[3] == '-------------- -------'
+ assert out_lines[4] == 'sandwiches.pbj 1.5.0 '
+ assert out_lines[5] == ''
+ assert out_lines[6] == '# /usr/share/ansible/collections/ansible_collections'
+ assert out_lines[7] == 'Collection Version'
+ assert out_lines[8] == '-------------- -------'
+ assert out_lines[9] == 'sandwiches.pbj 1.0.0 '
+
+
+def test_execute_list_collection_specific_invalid_fqcn(mocker, tmp_path_factory):
+ """Test an invalid fully qualified collection name (FQCN)"""
+
+ collection_name = 'no.good.name'
+
+ cliargs(collection_name=collection_name)
+ mocker.patch('os.path.exists', return_value=True)
+ mocker.patch('os.path.isdir', return_value=True)
+
+ gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', collection_name])
+ tmp_path = tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+ with pytest.raises(AnsibleError, match='Invalid collection name'):
+ gc.execute_list_collection(artifacts_manager=concrete_artifact_cm)
+
+
+def test_execute_list_collection_no_valid_paths(mocker, capsys, tmp_path_factory):
+ """Test listing collections when no valid paths are given"""
+
+ cliargs()
+
+ mocker.patch('os.path.exists', return_value=True)
+ mocker.patch('os.path.isdir', return_value=False)
+ mocker.patch('ansible.utils.color.ANSIBLE_COLOR', False)
+ mocker.patch('ansible.cli.galaxy.display.columns', 79)
+ gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list'])
+
+ tmp_path = tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+
+ with pytest.raises(AnsibleOptionsError, match=r'None of the provided paths were usable.'):
+ gc.execute_list_collection(artifacts_manager=concrete_artifact_cm)
+
+ out, err = capsys.readouterr()
+
+ assert '[WARNING]: - the configured path' in err
+ assert 'exists, but it\nis not a directory.' in err
+
+
+def test_execute_list_collection_one_invalid_path(mocker, capsys, mock_collection_objects, tmp_path_factory):
+ """Test listing all collections when one invalid path is given"""
+
+ cliargs()
+ mocker.patch('os.path.exists', return_value=True)
+ mocker.patch('os.path.isdir', isdir)
+ mocker.patch('ansible.cli.galaxy.GalaxyCLI._resolve_path', side_effect=['/root/.ansible/collections', 'nope'])
+ mocker.patch('ansible.utils.color.ANSIBLE_COLOR', False)
+
+ gc = GalaxyCLI(['ansible-galaxy', 'collection', 'list', '-p', 'nope'])
+ tmp_path = tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+ gc.execute_list_collection(artifacts_manager=concrete_artifact_cm)
+
+ out, err = capsys.readouterr()
+ out_lines = out.splitlines()
+
+ assert out_lines[0] == ''
+ assert out_lines[1] == '# /root/.ansible/collections/ansible_collections'
+ assert out_lines[2] == 'Collection Version'
+ assert out_lines[3] == '----------------- -------'
+ assert out_lines[4] == 'sandwiches.pbj 1.5.0 '
+ # Only a partial test of the output
+
+ assert err == '[WARNING]: - the configured path nope, exists, but it is not a directory.\n'
diff --git a/test/units/cli/galaxy/test_get_collection_widths.py b/test/units/cli/galaxy/test_get_collection_widths.py
new file mode 100644
index 0000000..6e1cbf5
--- /dev/null
+++ b/test/units/cli/galaxy/test_get_collection_widths.py
@@ -0,0 +1,34 @@
+# -*- 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
+
+import pytest
+
+from ansible.cli.galaxy import _get_collection_widths
+from ansible.galaxy.dependency_resolution.dataclasses import Requirement
+
+
+@pytest.fixture
+def collection_objects():
+ collection_ham = Requirement('sandwiches.ham', '1.5.0', None, 'galaxy', None)
+
+ collection_pbj = Requirement('sandwiches.pbj', '2.5', None, 'galaxy', None)
+
+ collection_reuben = Requirement('sandwiches.reuben', '4', None, 'galaxy', None)
+
+ return [collection_ham, collection_pbj, collection_reuben]
+
+
+def test_get_collection_widths(collection_objects):
+ assert _get_collection_widths(collection_objects) == (17, 5)
+
+
+def test_get_collection_widths_single_collection(mocker):
+ mocked_collection = Requirement('sandwiches.club', '3.0.0', None, 'galaxy', None)
+ # Make this look like it is not iterable
+ mocker.patch('ansible.cli.galaxy.is_iterable', return_value=False)
+
+ assert _get_collection_widths(mocked_collection) == (15, 5)
diff --git a/test/units/cli/test_adhoc.py b/test/units/cli/test_adhoc.py
new file mode 100644
index 0000000..18775f5
--- /dev/null
+++ b/test/units/cli/test_adhoc.py
@@ -0,0 +1,116 @@
+# Copyright: (c) 2018, Abhijeet Kasurde <akasurde@redhat.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+import re
+
+from ansible import context
+from ansible.cli.adhoc import AdHocCLI, display
+from ansible.errors import AnsibleOptionsError
+
+
+def test_parse():
+ """ Test adhoc parse"""
+ with pytest.raises(ValueError, match='A non-empty list for args is required'):
+ adhoc_cli = AdHocCLI([])
+
+ adhoc_cli = AdHocCLI(['ansibletest'])
+ with pytest.raises(SystemExit):
+ adhoc_cli.parse()
+
+
+def test_with_command():
+ """ Test simple adhoc command"""
+ module_name = 'command'
+ adhoc_cli = AdHocCLI(args=['ansible', '-m', module_name, '-vv', 'localhost'])
+ adhoc_cli.parse()
+ assert context.CLIARGS['module_name'] == module_name
+ assert display.verbosity == 2
+
+
+def test_simple_command():
+ """ Test valid command and its run"""
+ adhoc_cli = AdHocCLI(['/bin/ansible', '-m', 'command', 'localhost', '-a', 'echo "hi"'])
+ adhoc_cli.parse()
+ ret = adhoc_cli.run()
+ assert ret == 0
+
+
+def test_no_argument():
+ """ Test no argument command"""
+ adhoc_cli = AdHocCLI(['/bin/ansible', '-m', 'command', 'localhost'])
+ adhoc_cli.parse()
+ with pytest.raises(AnsibleOptionsError) as exec_info:
+ adhoc_cli.run()
+ assert 'No argument passed to command module' == str(exec_info.value)
+
+
+def test_did_you_mean_playbook():
+ """ Test adhoc with yml file as argument parameter"""
+ adhoc_cli = AdHocCLI(['/bin/ansible', '-m', 'command', 'localhost.yml'])
+ adhoc_cli.parse()
+ with pytest.raises(AnsibleOptionsError) as exec_info:
+ adhoc_cli.run()
+ assert 'No argument passed to command module (did you mean to run ansible-playbook?)' == str(exec_info.value)
+
+
+def test_play_ds_positive():
+ """ Test _play_ds"""
+ adhoc_cli = AdHocCLI(args=['/bin/ansible', 'localhost', '-m', 'command'])
+ adhoc_cli.parse()
+ ret = adhoc_cli._play_ds('command', 10, 2)
+ assert ret['name'] == 'Ansible Ad-Hoc'
+ assert ret['tasks'] == [{'action': {'module': 'command', 'args': {}}, 'async_val': 10, 'poll': 2, 'timeout': 0}]
+
+
+def test_play_ds_with_include_role():
+ """ Test include_role command with poll"""
+ adhoc_cli = AdHocCLI(args=['/bin/ansible', 'localhost', '-m', 'include_role'])
+ adhoc_cli.parse()
+ ret = adhoc_cli._play_ds('include_role', None, 2)
+ assert ret['name'] == 'Ansible Ad-Hoc'
+ assert ret['gather_facts'] == 'no'
+
+
+def test_run_import_playbook():
+ """ Test import_playbook which is not allowed with ad-hoc command"""
+ import_playbook = 'import_playbook'
+ adhoc_cli = AdHocCLI(args=['/bin/ansible', '-m', import_playbook, 'localhost'])
+ adhoc_cli.parse()
+ with pytest.raises(AnsibleOptionsError) as exec_info:
+ adhoc_cli.run()
+ assert context.CLIARGS['module_name'] == import_playbook
+ assert "'%s' is not a valid action for ad-hoc commands" % import_playbook == str(exec_info.value)
+
+
+def test_run_no_extra_vars():
+ adhoc_cli = AdHocCLI(args=['/bin/ansible', 'localhost', '-e'])
+ with pytest.raises(SystemExit) as exec_info:
+ adhoc_cli.parse()
+ assert exec_info.value.code == 2
+
+
+def test_ansible_version(capsys, mocker):
+ adhoc_cli = AdHocCLI(args=['/bin/ansible', '--version'])
+ with pytest.raises(SystemExit):
+ adhoc_cli.run()
+ version = capsys.readouterr()
+ try:
+ version_lines = version.out.splitlines()
+ except AttributeError:
+ # Python 2.6 does return a named tuple, so get the first item
+ version_lines = version[0].splitlines()
+
+ assert len(version_lines) == 9, 'Incorrect number of lines in "ansible --version" output'
+ assert re.match(r'ansible \[core [0-9.a-z]+\]$', version_lines[0]), 'Incorrect ansible version line in "ansible --version" output'
+ assert re.match(' config file = .*$', version_lines[1]), 'Incorrect config file line in "ansible --version" output'
+ assert re.match(' configured module search path = .*$', version_lines[2]), 'Incorrect module search path in "ansible --version" output'
+ assert re.match(' ansible python module location = .*$', version_lines[3]), 'Incorrect python module location in "ansible --version" output'
+ assert re.match(' ansible collection location = .*$', version_lines[4]), 'Incorrect collection location in "ansible --version" output'
+ assert re.match(' executable location = .*$', version_lines[5]), 'Incorrect executable locaction in "ansible --version" output'
+ assert re.match(' python version = .*$', version_lines[6]), 'Incorrect python version in "ansible --version" output'
+ assert re.match(' jinja version = .*$', version_lines[7]), 'Incorrect jinja version in "ansible --version" output'
+ assert re.match(' libyaml = .*$', version_lines[8]), 'Missing libyaml in "ansible --version" output'
diff --git a/test/units/cli/test_cli.py b/test/units/cli/test_cli.py
new file mode 100644
index 0000000..79c2b8f
--- /dev/null
+++ b/test/units/cli/test_cli.py
@@ -0,0 +1,381 @@
+# (c) 2017, Adrian Likins <alikins@redhat.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from unittest.mock import patch, MagicMock
+
+from units.mock.loader import DictDataLoader
+
+from ansible.release import __version__
+from ansible.parsing import vault
+from ansible import cli
+
+
+class TestCliVersion(unittest.TestCase):
+
+ def test_version_info(self):
+ version_info = cli.CLI.version_info()
+ self.assertEqual(version_info['string'], __version__)
+
+ def test_version_info_gitinfo(self):
+ version_info = cli.CLI.version_info(gitinfo=True)
+ self.assertIn('python version', version_info['string'])
+
+
+class TestCliBuildVaultIds(unittest.TestCase):
+ def setUp(self):
+ self.tty_patcher = patch('ansible.cli.sys.stdin.isatty', return_value=True)
+ self.mock_isatty = self.tty_patcher.start()
+
+ def tearDown(self):
+ self.tty_patcher.stop()
+
+ def test(self):
+ res = cli.CLI.build_vault_ids(['foo@bar'])
+ self.assertEqual(res, ['foo@bar'])
+
+ def test_create_new_password_no_vault_id(self):
+ res = cli.CLI.build_vault_ids([], create_new_password=True)
+ self.assertEqual(res, ['default@prompt_ask_vault_pass'])
+
+ def test_create_new_password_no_vault_id_no_auto_prompt(self):
+ res = cli.CLI.build_vault_ids([], auto_prompt=False, create_new_password=True)
+ self.assertEqual(res, [])
+
+ def test_no_vault_id_no_auto_prompt(self):
+ # simulate 'ansible-playbook site.yml' with out --ask-vault-pass, should not prompt
+ res = cli.CLI.build_vault_ids([], auto_prompt=False)
+ self.assertEqual(res, [])
+
+ def test_no_vault_ids_auto_prompt(self):
+ # create_new_password=False
+ # simulate 'ansible-vault edit encrypted.yml'
+ res = cli.CLI.build_vault_ids([], auto_prompt=True)
+ self.assertEqual(res, ['default@prompt_ask_vault_pass'])
+
+ def test_no_vault_ids_auto_prompt_ask_vault_pass(self):
+ # create_new_password=False
+ # simulate 'ansible-vault edit --ask-vault-pass encrypted.yml'
+ res = cli.CLI.build_vault_ids([], auto_prompt=True, ask_vault_pass=True)
+ self.assertEqual(res, ['default@prompt_ask_vault_pass'])
+
+ def test_create_new_password_auto_prompt(self):
+ # simulate 'ansible-vault encrypt somefile.yml'
+ res = cli.CLI.build_vault_ids([], auto_prompt=True, create_new_password=True)
+ self.assertEqual(res, ['default@prompt_ask_vault_pass'])
+
+ def test_create_new_password_no_vault_id_ask_vault_pass(self):
+ res = cli.CLI.build_vault_ids([], ask_vault_pass=True,
+ create_new_password=True)
+ self.assertEqual(res, ['default@prompt_ask_vault_pass'])
+
+ def test_create_new_password_with_vault_ids(self):
+ res = cli.CLI.build_vault_ids(['foo@bar'], create_new_password=True)
+ self.assertEqual(res, ['foo@bar'])
+
+ def test_create_new_password_no_vault_ids_password_files(self):
+ res = cli.CLI.build_vault_ids([], vault_password_files=['some-password-file'],
+ create_new_password=True)
+ self.assertEqual(res, ['default@some-password-file'])
+
+ def test_everything(self):
+ res = cli.CLI.build_vault_ids(['blip@prompt', 'baz@prompt_ask_vault_pass',
+ 'some-password-file', 'qux@another-password-file'],
+ vault_password_files=['yet-another-password-file',
+ 'one-more-password-file'],
+ ask_vault_pass=True,
+ create_new_password=True,
+ auto_prompt=False)
+
+ self.assertEqual(set(res), set(['blip@prompt', 'baz@prompt_ask_vault_pass',
+ 'default@prompt_ask_vault_pass',
+ 'some-password-file', 'qux@another-password-file',
+ 'default@yet-another-password-file',
+ 'default@one-more-password-file']))
+
+
+class TestCliSetupVaultSecrets(unittest.TestCase):
+ def setUp(self):
+ self.fake_loader = DictDataLoader({})
+ self.tty_patcher = patch('ansible.cli.sys.stdin.isatty', return_value=True)
+ self.mock_isatty = self.tty_patcher.start()
+
+ self.display_v_patcher = patch('ansible.cli.display.verbosity', return_value=6)
+ self.mock_display_v = self.display_v_patcher.start()
+ cli.display.verbosity = 5
+
+ def tearDown(self):
+ self.tty_patcher.stop()
+ self.display_v_patcher.stop()
+ cli.display.verbosity = 0
+
+ def test(self):
+ res = cli.CLI.setup_vault_secrets(None, None, auto_prompt=False)
+ self.assertIsInstance(res, list)
+
+ @patch('ansible.cli.get_file_vault_secret')
+ def test_password_file(self, mock_file_secret):
+ filename = '/dev/null/secret'
+ mock_file_secret.return_value = MagicMock(bytes=b'file1_password',
+ vault_id='file1',
+ filename=filename)
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=['secret1@%s' % filename, 'secret2'],
+ vault_password_files=[filename])
+ self.assertIsInstance(res, list)
+ matches = vault.match_secrets(res, ['secret1'])
+ self.assertIn('secret1', [x[0] for x in matches])
+ match = matches[0][1]
+ self.assertEqual(match.bytes, b'file1_password')
+
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_prompt(self, mock_prompt_secret):
+ mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
+ vault_id='prompt1')
+
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=['prompt1@prompt'],
+ ask_vault_pass=True,
+ auto_prompt=False)
+
+ self.assertIsInstance(res, list)
+ matches = vault.match_secrets(res, ['prompt1'])
+ self.assertIn('prompt1', [x[0] for x in matches])
+ match = matches[0][1]
+ self.assertEqual(match.bytes, b'prompt1_password')
+
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_prompt_no_tty(self, mock_prompt_secret):
+ self.mock_isatty.return_value = False
+ mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
+ vault_id='prompt1',
+ name='bytes_should_be_prompt1_password',
+ spec=vault.PromptVaultSecret)
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=['prompt1@prompt'],
+ ask_vault_pass=True,
+ auto_prompt=False)
+
+ self.assertIsInstance(res, list)
+ self.assertEqual(len(res), 2)
+ matches = vault.match_secrets(res, ['prompt1'])
+ self.assertIn('prompt1', [x[0] for x in matches])
+ self.assertEqual(len(matches), 1)
+
+ @patch('ansible.cli.get_file_vault_secret')
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_prompt_no_tty_and_password_file(self, mock_prompt_secret, mock_file_secret):
+ self.mock_isatty.return_value = False
+ mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
+ vault_id='prompt1')
+ filename = '/dev/null/secret'
+ mock_file_secret.return_value = MagicMock(bytes=b'file1_password',
+ vault_id='file1',
+ filename=filename)
+
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=['prompt1@prompt', 'file1@/dev/null/secret'],
+ ask_vault_pass=True)
+
+ self.assertIsInstance(res, list)
+ matches = vault.match_secrets(res, ['file1'])
+ self.assertIn('file1', [x[0] for x in matches])
+ self.assertNotIn('prompt1', [x[0] for x in matches])
+ match = matches[0][1]
+ self.assertEqual(match.bytes, b'file1_password')
+
+ def _assert_ids(self, vault_id_names, res, password=b'prompt1_password'):
+ self.assertIsInstance(res, list)
+ len_ids = len(vault_id_names)
+ matches = vault.match_secrets(res, vault_id_names)
+ self.assertEqual(len(res), len_ids, 'len(res):%s does not match len_ids:%s' % (len(res), len_ids))
+ self.assertEqual(len(matches), len_ids)
+ for index, prompt in enumerate(vault_id_names):
+ self.assertIn(prompt, [x[0] for x in matches])
+ # simple mock, same password/prompt for each mock_prompt_secret
+ self.assertEqual(matches[index][1].bytes, password)
+
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_multiple_prompts(self, mock_prompt_secret):
+ mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
+ vault_id='prompt1')
+
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=['prompt1@prompt',
+ 'prompt2@prompt'],
+ ask_vault_pass=False)
+
+ vault_id_names = ['prompt1', 'prompt2']
+ self._assert_ids(vault_id_names, res)
+
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_multiple_prompts_and_ask_vault_pass(self, mock_prompt_secret):
+ self.mock_isatty.return_value = False
+ mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
+ vault_id='prompt1')
+
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=['prompt1@prompt',
+ 'prompt2@prompt',
+ 'prompt3@prompt_ask_vault_pass'],
+ ask_vault_pass=True)
+
+ # We provide some vault-ids and secrets, so auto_prompt shouldn't get triggered,
+ # so there is
+ vault_id_names = ['prompt1', 'prompt2', 'prompt3', 'default']
+ self._assert_ids(vault_id_names, res)
+
+ @patch('ansible.cli.C')
+ @patch('ansible.cli.get_file_vault_secret')
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_default_file_vault(self, mock_prompt_secret,
+ mock_file_secret,
+ mock_config):
+ mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
+ vault_id='default')
+ mock_file_secret.return_value = MagicMock(bytes=b'file1_password',
+ vault_id='default')
+ mock_config.DEFAULT_VAULT_PASSWORD_FILE = '/dev/null/faux/vault_password_file'
+ mock_config.DEFAULT_VAULT_IDENTITY = 'default'
+
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=[],
+ create_new_password=False,
+ ask_vault_pass=False)
+
+ self.assertIsInstance(res, list)
+ matches = vault.match_secrets(res, ['default'])
+ # --vault-password-file/DEFAULT_VAULT_PASSWORD_FILE is higher precendce than prompts
+ # if the same vault-id ('default') regardless of cli order since it didn't matter in 2.3
+
+ self.assertEqual(matches[0][1].bytes, b'file1_password')
+ self.assertEqual(len(matches), 1)
+
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=[],
+ create_new_password=False,
+ ask_vault_pass=True,
+ auto_prompt=True)
+
+ self.assertIsInstance(res, list)
+ matches = vault.match_secrets(res, ['default'])
+ self.assertEqual(matches[0][1].bytes, b'file1_password')
+ self.assertEqual(matches[1][1].bytes, b'prompt1_password')
+ self.assertEqual(len(matches), 2)
+
+ @patch('ansible.cli.get_file_vault_secret')
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_default_file_vault_identity_list(self, mock_prompt_secret,
+ mock_file_secret):
+ default_vault_ids = ['some_prompt@prompt',
+ 'some_file@/dev/null/secret']
+
+ mock_prompt_secret.return_value = MagicMock(bytes=b'some_prompt_password',
+ vault_id='some_prompt')
+
+ filename = '/dev/null/secret'
+ mock_file_secret.return_value = MagicMock(bytes=b'some_file_password',
+ vault_id='some_file',
+ filename=filename)
+
+ vault_ids = default_vault_ids
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=vault_ids,
+ create_new_password=False,
+ ask_vault_pass=True)
+
+ self.assertIsInstance(res, list)
+ matches = vault.match_secrets(res, ['some_file'])
+ # --vault-password-file/DEFAULT_VAULT_PASSWORD_FILE is higher precendce than prompts
+ # if the same vault-id ('default') regardless of cli order since it didn't matter in 2.3
+ self.assertEqual(matches[0][1].bytes, b'some_file_password')
+ matches = vault.match_secrets(res, ['some_prompt'])
+ self.assertEqual(matches[0][1].bytes, b'some_prompt_password')
+
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_prompt_just_ask_vault_pass(self, mock_prompt_secret):
+ mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
+ vault_id='default')
+
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=[],
+ create_new_password=False,
+ ask_vault_pass=True)
+
+ self.assertIsInstance(res, list)
+ match = vault.match_secrets(res, ['default'])[0][1]
+ self.assertEqual(match.bytes, b'prompt1_password')
+
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_prompt_new_password_ask_vault_pass(self, mock_prompt_secret):
+ mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
+ vault_id='default')
+
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=[],
+ create_new_password=True,
+ ask_vault_pass=True)
+
+ self.assertIsInstance(res, list)
+ match = vault.match_secrets(res, ['default'])[0][1]
+ self.assertEqual(match.bytes, b'prompt1_password')
+
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_prompt_new_password_vault_id_prompt(self, mock_prompt_secret):
+ mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
+ vault_id='some_vault_id')
+
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=['some_vault_id@prompt'],
+ create_new_password=True,
+ ask_vault_pass=False)
+
+ self.assertIsInstance(res, list)
+ match = vault.match_secrets(res, ['some_vault_id'])[0][1]
+ self.assertEqual(match.bytes, b'prompt1_password')
+
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_prompt_new_password_vault_id_prompt_ask_vault_pass(self, mock_prompt_secret):
+ mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
+ vault_id='default')
+
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=['some_vault_id@prompt_ask_vault_pass'],
+ create_new_password=True,
+ ask_vault_pass=False)
+
+ self.assertIsInstance(res, list)
+ match = vault.match_secrets(res, ['some_vault_id'])[0][1]
+ self.assertEqual(match.bytes, b'prompt1_password')
+
+ @patch('ansible.cli.PromptVaultSecret')
+ def test_prompt_new_password_vault_id_prompt_ask_vault_pass_ask_vault_pass(self, mock_prompt_secret):
+ mock_prompt_secret.return_value = MagicMock(bytes=b'prompt1_password',
+ vault_id='default')
+
+ res = cli.CLI.setup_vault_secrets(loader=self.fake_loader,
+ vault_ids=['some_vault_id@prompt_ask_vault_pass'],
+ create_new_password=True,
+ ask_vault_pass=True)
+
+ self.assertIsInstance(res, list)
+ match = vault.match_secrets(res, ['some_vault_id'])[0][1]
+ self.assertEqual(match.bytes, b'prompt1_password')
diff --git a/test/units/cli/test_console.py b/test/units/cli/test_console.py
new file mode 100644
index 0000000..4fc05dd
--- /dev/null
+++ b/test/units/cli/test_console.py
@@ -0,0 +1,51 @@
+# (c) 2016, Thilo Uttendorfer <tlo@sengaya.de>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from unittest.mock import patch
+
+from ansible.cli.console import ConsoleCLI
+
+
+class TestConsoleCLI(unittest.TestCase):
+ def test_parse(self):
+ cli = ConsoleCLI(['ansible test'])
+ cli.parse()
+ self.assertTrue(cli.parser is not None)
+
+ def test_module_args(self):
+ cli = ConsoleCLI(['ansible test'])
+ cli.parse()
+ res = cli.module_args('copy')
+ self.assertTrue(cli.parser is not None)
+ self.assertIn('src', res)
+ self.assertIn('backup', res)
+ self.assertIsInstance(res, list)
+
+ @patch('ansible.utils.display.Display.display')
+ def test_helpdefault(self, mock_display):
+ cli = ConsoleCLI(['ansible test'])
+ cli.parse()
+ cli.modules = set(['copy'])
+ cli.helpdefault('copy')
+ self.assertTrue(cli.parser is not None)
+ self.assertTrue(len(mock_display.call_args_list) > 0,
+ "display.display should have been called but was not")
diff --git a/test/units/cli/test_data/collection_skeleton/README.md b/test/units/cli/test_data/collection_skeleton/README.md
new file mode 100644
index 0000000..4cfd8af
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/README.md
@@ -0,0 +1 @@
+A readme \ No newline at end of file
diff --git a/test/units/cli/test_data/collection_skeleton/docs/My Collection.md b/test/units/cli/test_data/collection_skeleton/docs/My Collection.md
new file mode 100644
index 0000000..6fa917f
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/docs/My Collection.md
@@ -0,0 +1 @@
+Welcome to my test collection doc for {{ namespace }}. \ No newline at end of file
diff --git a/test/units/cli/test_data/collection_skeleton/galaxy.yml.j2 b/test/units/cli/test_data/collection_skeleton/galaxy.yml.j2
new file mode 100644
index 0000000..b1da267
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/galaxy.yml.j2
@@ -0,0 +1,7 @@
+namespace: '{{ namespace }}'
+name: '{{ collection_name }}'
+version: 0.1.0
+readme: README.md
+authors:
+- Ansible Cow <acow@bovineuniversity.edu>
+- Tu Cow <tucow@bovineuniversity.edu>
diff --git a/test/units/cli/test_data/collection_skeleton/playbooks/main.yml b/test/units/cli/test_data/collection_skeleton/playbooks/main.yml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/playbooks/main.yml
diff --git a/test/units/cli/test_data/collection_skeleton/playbooks/templates/subfolder/test.conf.j2 b/test/units/cli/test_data/collection_skeleton/playbooks/templates/subfolder/test.conf.j2
new file mode 100644
index 0000000..b4e3364
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/playbooks/templates/subfolder/test.conf.j2
@@ -0,0 +1,2 @@
+[defaults]
+test_key = {{ test_variable }}
diff --git a/test/units/cli/test_data/collection_skeleton/playbooks/templates/test.conf.j2 b/test/units/cli/test_data/collection_skeleton/playbooks/templates/test.conf.j2
new file mode 100644
index 0000000..b4e3364
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/playbooks/templates/test.conf.j2
@@ -0,0 +1,2 @@
+[defaults]
+test_key = {{ test_variable }}
diff --git a/test/units/cli/test_data/collection_skeleton/plugins/action/.git_keep b/test/units/cli/test_data/collection_skeleton/plugins/action/.git_keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/plugins/action/.git_keep
diff --git a/test/units/cli/test_data/collection_skeleton/plugins/filter/.git_keep b/test/units/cli/test_data/collection_skeleton/plugins/filter/.git_keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/plugins/filter/.git_keep
diff --git a/test/units/cli/test_data/collection_skeleton/plugins/inventory/.git_keep b/test/units/cli/test_data/collection_skeleton/plugins/inventory/.git_keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/plugins/inventory/.git_keep
diff --git a/test/units/cli/test_data/collection_skeleton/plugins/lookup/.git_keep b/test/units/cli/test_data/collection_skeleton/plugins/lookup/.git_keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/plugins/lookup/.git_keep
diff --git a/test/units/cli/test_data/collection_skeleton/plugins/module_utils/.git_keep b/test/units/cli/test_data/collection_skeleton/plugins/module_utils/.git_keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/plugins/module_utils/.git_keep
diff --git a/test/units/cli/test_data/collection_skeleton/plugins/modules/.git_keep b/test/units/cli/test_data/collection_skeleton/plugins/modules/.git_keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/plugins/modules/.git_keep
diff --git a/test/units/cli/test_data/collection_skeleton/roles/common/tasks/main.yml.j2 b/test/units/cli/test_data/collection_skeleton/roles/common/tasks/main.yml.j2
new file mode 100644
index 0000000..77adf2e
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/roles/common/tasks/main.yml.j2
@@ -0,0 +1,3 @@
+- name: test collection skeleton
+ debug:
+ msg: "Namespace: {{ namespace }}" \ No newline at end of file
diff --git a/test/units/cli/test_data/collection_skeleton/roles/common/templates/subfolder/test.conf.j2 b/test/units/cli/test_data/collection_skeleton/roles/common/templates/subfolder/test.conf.j2
new file mode 100644
index 0000000..b4e3364
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/roles/common/templates/subfolder/test.conf.j2
@@ -0,0 +1,2 @@
+[defaults]
+test_key = {{ test_variable }}
diff --git a/test/units/cli/test_data/collection_skeleton/roles/common/templates/test.conf.j2 b/test/units/cli/test_data/collection_skeleton/roles/common/templates/test.conf.j2
new file mode 100644
index 0000000..b4e3364
--- /dev/null
+++ b/test/units/cli/test_data/collection_skeleton/roles/common/templates/test.conf.j2
@@ -0,0 +1,2 @@
+[defaults]
+test_key = {{ test_variable }}
diff --git a/test/units/cli/test_data/role_skeleton/README.md b/test/units/cli/test_data/role_skeleton/README.md
new file mode 100644
index 0000000..225dd44
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/README.md
@@ -0,0 +1,38 @@
+Role Name
+=========
+
+A brief description of the role goes here.
+
+Requirements
+------------
+
+Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required.
+
+Role Variables
+--------------
+
+A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well.
+
+Dependencies
+------------
+
+A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles.
+
+Example Playbook
+----------------
+
+Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too:
+
+ - hosts: servers
+ roles:
+ - { role: username.rolename, x: 42 }
+
+License
+-------
+
+BSD
+
+Author Information
+------------------
+
+An optional section for the role authors to include contact information, or a website (HTML is not allowed).
diff --git a/test/units/cli/test_data/role_skeleton/defaults/main.yml.j2 b/test/units/cli/test_data/role_skeleton/defaults/main.yml.j2
new file mode 100644
index 0000000..3818e64
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/defaults/main.yml.j2
@@ -0,0 +1,2 @@
+---
+# defaults file for {{ role_name }}
diff --git a/test/units/cli/test_data/role_skeleton/files/.git_keep b/test/units/cli/test_data/role_skeleton/files/.git_keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/files/.git_keep
diff --git a/test/units/cli/test_data/role_skeleton/handlers/main.yml.j2 b/test/units/cli/test_data/role_skeleton/handlers/main.yml.j2
new file mode 100644
index 0000000..3f4c496
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/handlers/main.yml.j2
@@ -0,0 +1,2 @@
+---
+# handlers file for {{ role_name }}
diff --git a/test/units/cli/test_data/role_skeleton/inventory b/test/units/cli/test_data/role_skeleton/inventory
new file mode 100644
index 0000000..2fbb50c
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/inventory
@@ -0,0 +1 @@
+localhost
diff --git a/test/units/cli/test_data/role_skeleton/meta/main.yml.j2 b/test/units/cli/test_data/role_skeleton/meta/main.yml.j2
new file mode 100644
index 0000000..2fc53cb
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/meta/main.yml.j2
@@ -0,0 +1,62 @@
+galaxy_info:
+ author: {{ author }}
+ description: {{ description }}
+ company: {{ company }}
+
+ # If the issue tracker for your role is not on github, uncomment the
+ # next line and provide a value
+ # issue_tracker_url: {{ issue_tracker_url }}
+
+ # Some suggested licenses:
+ # - BSD (default)
+ # - MIT
+ # - GPLv2
+ # - GPLv3
+ # - Apache
+ # - CC-BY
+ license: {{ license }}
+
+ min_ansible_version: {{ min_ansible_version }}
+
+ # Optionally specify the branch Galaxy will use when accessing the GitHub
+ # repo for this role. During role install, if no tags are available,
+ # Galaxy will use this branch. During import Galaxy will access files on
+ # this branch. If travis integration is configured, only notification for this
+ # branch will be accepted. Otherwise, in all cases, the repo's default branch
+ # (usually master) will be used.
+ #github_branch:
+
+ #
+ # Provide a list of supported platforms, and for each platform a list of versions.
+ # If you don't wish to enumerate all versions for a particular platform, use 'all'.
+ # To view available platforms and versions (or releases), visit:
+ # https://galaxy.ansible.com/api/v1/platforms/
+ #
+ # platforms:
+ # - name: Fedora
+ # versions:
+ # - all
+ # - 25
+ # - name: SomePlatform
+ # versions:
+ # - all
+ # - 1.0
+ # - 7
+ # - 99.99
+
+ galaxy_tags: []
+ # List tags for your role here, one per line. A tag is
+ # a keyword that describes and categorizes the role.
+ # Users find roles by searching for tags. Be sure to
+ # remove the '[]' above if you add tags to this list.
+ #
+ # NOTE: A tag is limited to a single word comprised of
+ # alphanumeric characters. Maximum 20 tags per role.
+
+dependencies: []
+ # List your role dependencies here, one per line.
+ # Be sure to remove the '[]' above if you add dependencies
+ # to this list.
+{%- for dependency in dependencies %}
+ #- {{ dependency }}
+{%- endfor %}
diff --git a/test/units/cli/test_data/role_skeleton/tasks/main.yml.j2 b/test/units/cli/test_data/role_skeleton/tasks/main.yml.j2
new file mode 100644
index 0000000..a988065
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/tasks/main.yml.j2
@@ -0,0 +1,2 @@
+---
+# tasks file for {{ role_name }}
diff --git a/test/units/cli/test_data/role_skeleton/templates/.git_keep b/test/units/cli/test_data/role_skeleton/templates/.git_keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/templates/.git_keep
diff --git a/test/units/cli/test_data/role_skeleton/templates/subfolder/test.conf.j2 b/test/units/cli/test_data/role_skeleton/templates/subfolder/test.conf.j2
new file mode 100644
index 0000000..b4e3364
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/templates/subfolder/test.conf.j2
@@ -0,0 +1,2 @@
+[defaults]
+test_key = {{ test_variable }}
diff --git a/test/units/cli/test_data/role_skeleton/templates/test.conf.j2 b/test/units/cli/test_data/role_skeleton/templates/test.conf.j2
new file mode 100644
index 0000000..b4e3364
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/templates/test.conf.j2
@@ -0,0 +1,2 @@
+[defaults]
+test_key = {{ test_variable }}
diff --git a/test/units/cli/test_data/role_skeleton/templates_extra/templates.txt.j2 b/test/units/cli/test_data/role_skeleton/templates_extra/templates.txt.j2
new file mode 100644
index 0000000..143d630
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/templates_extra/templates.txt.j2
@@ -0,0 +1 @@
+{{ role_name }}
diff --git a/test/units/cli/test_data/role_skeleton/tests/test.yml.j2 b/test/units/cli/test_data/role_skeleton/tests/test.yml.j2
new file mode 100644
index 0000000..0c40f95
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/tests/test.yml.j2
@@ -0,0 +1,5 @@
+---
+- hosts: localhost
+ remote_user: root
+ roles:
+ - {{ role_name }}
diff --git a/test/units/cli/test_data/role_skeleton/vars/main.yml.j2 b/test/units/cli/test_data/role_skeleton/vars/main.yml.j2
new file mode 100644
index 0000000..092d511
--- /dev/null
+++ b/test/units/cli/test_data/role_skeleton/vars/main.yml.j2
@@ -0,0 +1,2 @@
+---
+# vars file for {{ role_name }}
diff --git a/test/units/cli/test_doc.py b/test/units/cli/test_doc.py
new file mode 100644
index 0000000..b10f088
--- /dev/null
+++ b/test/units/cli/test_doc.py
@@ -0,0 +1,130 @@
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible.cli.doc import DocCLI, RoleMixin
+from ansible.plugins.loader import module_loader
+
+
+TTY_IFY_DATA = {
+ # No substitutions
+ 'no-op': 'no-op',
+ 'no-op Z(test)': 'no-op Z(test)',
+ # Simple cases of all substitutions
+ 'I(italic)': "`italic'",
+ 'B(bold)': '*bold*',
+ 'M(ansible.builtin.module)': '[ansible.builtin.module]',
+ 'U(https://docs.ansible.com)': 'https://docs.ansible.com',
+ 'L(the user guide,https://docs.ansible.com/user-guide.html)': 'the user guide <https://docs.ansible.com/user-guide.html>',
+ 'R(the user guide,user-guide)': 'the user guide',
+ 'C(/usr/bin/file)': "`/usr/bin/file'",
+ 'HORIZONTALLINE': '\n{0}\n'.format('-' * 13),
+ # Multiple substitutions
+ 'The M(ansible.builtin.yum) module B(MUST) be given the C(package) parameter. See the R(looping docs,using-loops) for more info':
+ "The [ansible.builtin.yum] module *MUST* be given the `package' parameter. See the looping docs for more info",
+ # Problem cases
+ 'IBM(International Business Machines)': 'IBM(International Business Machines)',
+ 'L(the user guide, https://docs.ansible.com/)': 'the user guide <https://docs.ansible.com/>',
+ 'R(the user guide, user-guide)': 'the user guide',
+ # de-rsty refs and anchors
+ 'yolo :ref:`my boy` does stuff': 'yolo `my boy` does stuff',
+ '.. seealso:: Something amazing': 'See also: Something amazing',
+ '.. seealso:: Troublesome multiline\n Stuff goes htere': 'See also: Troublesome multiline\n Stuff goes htere',
+ '.. note:: boring stuff': 'Note: boring stuff',
+}
+
+
+@pytest.mark.parametrize('text, expected', sorted(TTY_IFY_DATA.items()))
+def test_ttyify(text, expected):
+ assert DocCLI.tty_ify(text) == expected
+
+
+def test_rolemixin__build_summary():
+ obj = RoleMixin()
+ role_name = 'test_role'
+ collection_name = 'test.units'
+ argspec = {
+ 'main': {'short_description': 'main short description'},
+ 'alternate': {'short_description': 'alternate short description'},
+ }
+ expected = {
+ 'collection': collection_name,
+ 'entry_points': {
+ 'main': argspec['main']['short_description'],
+ 'alternate': argspec['alternate']['short_description'],
+ }
+ }
+
+ fqcn, summary = obj._build_summary(role_name, collection_name, argspec)
+ assert fqcn == '.'.join([collection_name, role_name])
+ assert summary == expected
+
+
+def test_rolemixin__build_summary_empty_argspec():
+ obj = RoleMixin()
+ role_name = 'test_role'
+ collection_name = 'test.units'
+ argspec = {}
+ expected = {
+ 'collection': collection_name,
+ 'entry_points': {}
+ }
+
+ fqcn, summary = obj._build_summary(role_name, collection_name, argspec)
+ assert fqcn == '.'.join([collection_name, role_name])
+ assert summary == expected
+
+
+def test_rolemixin__build_doc():
+ obj = RoleMixin()
+ role_name = 'test_role'
+ path = '/a/b/c'
+ collection_name = 'test.units'
+ entrypoint_filter = 'main'
+ argspec = {
+ 'main': {'short_description': 'main short description'},
+ 'alternate': {'short_description': 'alternate short description'},
+ }
+ expected = {
+ 'path': path,
+ 'collection': collection_name,
+ 'entry_points': {
+ 'main': argspec['main'],
+ }
+ }
+ fqcn, doc = obj._build_doc(role_name, path, collection_name, argspec, entrypoint_filter)
+ assert fqcn == '.'.join([collection_name, role_name])
+ assert doc == expected
+
+
+def test_rolemixin__build_doc_no_filter_match():
+ obj = RoleMixin()
+ role_name = 'test_role'
+ path = '/a/b/c'
+ collection_name = 'test.units'
+ entrypoint_filter = 'doesNotExist'
+ argspec = {
+ 'main': {'short_description': 'main short description'},
+ 'alternate': {'short_description': 'alternate short description'},
+ }
+ fqcn, doc = obj._build_doc(role_name, path, collection_name, argspec, entrypoint_filter)
+ assert fqcn == '.'.join([collection_name, role_name])
+ assert doc is None
+
+
+def test_builtin_modules_list():
+ args = ['ansible-doc', '-l', 'ansible.builtin', '-t', 'module']
+ obj = DocCLI(args=args)
+ obj.parse()
+ result = obj._list_plugins('module', module_loader)
+ assert len(result) > 0
+
+
+def test_legacy_modules_list():
+ args = ['ansible-doc', '-l', 'ansible.legacy', '-t', 'module']
+ obj = DocCLI(args=args)
+ obj.parse()
+ result = obj._list_plugins('module', module_loader)
+ assert len(result) > 0
diff --git a/test/units/cli/test_galaxy.py b/test/units/cli/test_galaxy.py
new file mode 100644
index 0000000..8ff5640
--- /dev/null
+++ b/test/units/cli/test_galaxy.py
@@ -0,0 +1,1346 @@
+# -*- coding: utf-8 -*-
+# (c) 2016, Adrian Likins <alikins@redhat.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import ansible
+from io import BytesIO
+import json
+import os
+import pytest
+import shutil
+import stat
+import tarfile
+import tempfile
+import yaml
+
+import ansible.constants as C
+from ansible import context
+from ansible.cli.galaxy import GalaxyCLI
+from ansible.galaxy import collection
+from ansible.galaxy.api import GalaxyAPI
+from ansible.errors import AnsibleError
+from ansible.module_utils._text import to_bytes, to_native, to_text
+from ansible.utils import context_objects as co
+from ansible.utils.display import Display
+from units.compat import unittest
+from unittest.mock import patch, MagicMock
+
+
+@pytest.fixture(autouse='function')
+def reset_cli_args():
+ co.GlobalCLIArgs._Singleton__instance = None
+ yield
+ co.GlobalCLIArgs._Singleton__instance = None
+
+
+class TestGalaxy(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ '''creating prerequisites for installing a role; setUpClass occurs ONCE whereas setUp occurs with every method tested.'''
+ # class data for easy viewing: role_dir, role_tar, role_name, role_req, role_path
+
+ cls.temp_dir = tempfile.mkdtemp(prefix='ansible-test_galaxy-')
+ os.chdir(cls.temp_dir)
+
+ if os.path.exists("./delete_me"):
+ shutil.rmtree("./delete_me")
+
+ # creating framework for a role
+ gc = GalaxyCLI(args=["ansible-galaxy", "init", "--offline", "delete_me"])
+ gc.run()
+ cls.role_dir = "./delete_me"
+ cls.role_name = "delete_me"
+
+ # making a temp dir for role installation
+ cls.role_path = os.path.join(tempfile.mkdtemp(), "roles")
+ if not os.path.isdir(cls.role_path):
+ os.makedirs(cls.role_path)
+
+ # creating a tar file name for class data
+ cls.role_tar = './delete_me.tar.gz'
+ cls.makeTar(cls.role_tar, cls.role_dir)
+
+ # creating a temp file with installation requirements
+ cls.role_req = './delete_me_requirements.yml'
+ fd = open(cls.role_req, "w")
+ fd.write("- 'src': '%s'\n 'name': '%s'\n 'path': '%s'" % (cls.role_tar, cls.role_name, cls.role_path))
+ fd.close()
+
+ @classmethod
+ def makeTar(cls, output_file, source_dir):
+ ''' used for making a tarfile from a role directory '''
+ # adding directory into a tar file
+ try:
+ tar = tarfile.open(output_file, "w:gz")
+ tar.add(source_dir, arcname=os.path.basename(source_dir))
+ except AttributeError: # tarfile obj. has no attribute __exit__ prior to python 2. 7
+ pass
+ finally: # ensuring closure of tarfile obj
+ tar.close()
+
+ @classmethod
+ def tearDownClass(cls):
+ '''After tests are finished removes things created in setUpClass'''
+ # deleting the temp role directory
+ if os.path.exists(cls.role_dir):
+ shutil.rmtree(cls.role_dir)
+ if os.path.exists(cls.role_req):
+ os.remove(cls.role_req)
+ if os.path.exists(cls.role_tar):
+ os.remove(cls.role_tar)
+ if os.path.isdir(cls.role_path):
+ shutil.rmtree(cls.role_path)
+
+ os.chdir('/')
+ shutil.rmtree(cls.temp_dir)
+
+ def setUp(self):
+ # Reset the stored command line args
+ co.GlobalCLIArgs._Singleton__instance = None
+ self.default_args = ['ansible-galaxy']
+
+ def tearDown(self):
+ # Reset the stored command line args
+ co.GlobalCLIArgs._Singleton__instance = None
+
+ def test_init(self):
+ galaxy_cli = GalaxyCLI(args=self.default_args)
+ self.assertTrue(isinstance(galaxy_cli, GalaxyCLI))
+
+ def test_display_min(self):
+ gc = GalaxyCLI(args=self.default_args)
+ role_info = {'name': 'some_role_name'}
+ display_result = gc._display_role_info(role_info)
+ self.assertTrue(display_result.find('some_role_name') > -1)
+
+ def test_display_galaxy_info(self):
+ gc = GalaxyCLI(args=self.default_args)
+ galaxy_info = {}
+ role_info = {'name': 'some_role_name',
+ 'galaxy_info': galaxy_info}
+ display_result = gc._display_role_info(role_info)
+ if display_result.find('\n\tgalaxy_info:') == -1:
+ self.fail('Expected galaxy_info to be indented once')
+
+ def test_run(self):
+ ''' verifies that the GalaxyCLI object's api is created and that execute() is called. '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "install", "--ignore-errors", "imaginary_role"])
+ gc.parse()
+ with patch.object(ansible.cli.CLI, "run", return_value=None) as mock_run:
+ gc.run()
+ # testing
+ self.assertIsInstance(gc.galaxy, ansible.galaxy.Galaxy)
+ self.assertEqual(mock_run.call_count, 1)
+ self.assertTrue(isinstance(gc.api, ansible.galaxy.api.GalaxyAPI))
+
+ def test_execute_remove(self):
+ # installing role
+ gc = GalaxyCLI(args=["ansible-galaxy", "install", "-p", self.role_path, "-r", self.role_req, '--force'])
+ gc.run()
+
+ # location where the role was installed
+ role_file = os.path.join(self.role_path, self.role_name)
+
+ # removing role
+ # Have to reset the arguments in the context object manually since we're doing the
+ # equivalent of running the command line program twice
+ co.GlobalCLIArgs._Singleton__instance = None
+ gc = GalaxyCLI(args=["ansible-galaxy", "remove", role_file, self.role_name])
+ gc.run()
+
+ # testing role was removed
+ removed_role = not os.path.exists(role_file)
+ self.assertTrue(removed_role)
+
+ def test_exit_without_ignore_without_flag(self):
+ ''' tests that GalaxyCLI exits with the error specified if the --ignore-errors flag is not used '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "install", "--server=None", "fake_role_name"])
+ with patch.object(ansible.utils.display.Display, "display", return_value=None) as mocked_display:
+ # testing that error expected is raised
+ self.assertRaises(AnsibleError, gc.run)
+ self.assertTrue(mocked_display.called_once_with("- downloading role 'fake_role_name', owned by "))
+
+ def test_exit_without_ignore_with_flag(self):
+ ''' tests that GalaxyCLI exits without the error specified if the --ignore-errors flag is used '''
+ # testing with --ignore-errors flag
+ gc = GalaxyCLI(args=["ansible-galaxy", "install", "--server=None", "fake_role_name", "--ignore-errors"])
+ with patch.object(ansible.utils.display.Display, "display", return_value=None) as mocked_display:
+ gc.run()
+ self.assertTrue(mocked_display.called_once_with("- downloading role 'fake_role_name', owned by "))
+
+ def test_parse_no_action(self):
+ ''' testing the options parser when no action is given '''
+ gc = GalaxyCLI(args=["ansible-galaxy", ""])
+ self.assertRaises(SystemExit, gc.parse)
+
+ def test_parse_invalid_action(self):
+ ''' testing the options parser when an invalid action is given '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "NOT_ACTION"])
+ self.assertRaises(SystemExit, gc.parse)
+
+ def test_parse_delete(self):
+ ''' testing the options parser when the action 'delete' is given '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "delete", "foo", "bar"])
+ gc.parse()
+ self.assertEqual(context.CLIARGS['verbosity'], 0)
+
+ def test_parse_import(self):
+ ''' testing the options parser when the action 'import' is given '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "import", "foo", "bar"])
+ gc.parse()
+ self.assertEqual(context.CLIARGS['wait'], True)
+ self.assertEqual(context.CLIARGS['reference'], None)
+ self.assertEqual(context.CLIARGS['check_status'], False)
+ self.assertEqual(context.CLIARGS['verbosity'], 0)
+
+ def test_parse_info(self):
+ ''' testing the options parser when the action 'info' is given '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "info", "foo", "bar"])
+ gc.parse()
+ self.assertEqual(context.CLIARGS['offline'], False)
+
+ def test_parse_init(self):
+ ''' testing the options parser when the action 'init' is given '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "init", "foo"])
+ gc.parse()
+ self.assertEqual(context.CLIARGS['offline'], False)
+ self.assertEqual(context.CLIARGS['force'], False)
+
+ def test_parse_install(self):
+ ''' testing the options parser when the action 'install' is given '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "install"])
+ gc.parse()
+ self.assertEqual(context.CLIARGS['ignore_errors'], False)
+ self.assertEqual(context.CLIARGS['no_deps'], False)
+ self.assertEqual(context.CLIARGS['requirements'], None)
+ self.assertEqual(context.CLIARGS['force'], False)
+
+ def test_parse_list(self):
+ ''' testing the options parser when the action 'list' is given '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "list"])
+ gc.parse()
+ self.assertEqual(context.CLIARGS['verbosity'], 0)
+
+ def test_parse_remove(self):
+ ''' testing the options parser when the action 'remove' is given '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "remove", "foo"])
+ gc.parse()
+ self.assertEqual(context.CLIARGS['verbosity'], 0)
+
+ def test_parse_search(self):
+ ''' testing the options parswer when the action 'search' is given '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "search"])
+ gc.parse()
+ self.assertEqual(context.CLIARGS['platforms'], None)
+ self.assertEqual(context.CLIARGS['galaxy_tags'], None)
+ self.assertEqual(context.CLIARGS['author'], None)
+
+ def test_parse_setup(self):
+ ''' testing the options parser when the action 'setup' is given '''
+ gc = GalaxyCLI(args=["ansible-galaxy", "setup", "source", "github_user", "github_repo", "secret"])
+ gc.parse()
+ self.assertEqual(context.CLIARGS['verbosity'], 0)
+ self.assertEqual(context.CLIARGS['remove_id'], None)
+ self.assertEqual(context.CLIARGS['setup_list'], False)
+
+
+class ValidRoleTests(object):
+
+ expected_role_dirs = ('defaults', 'files', 'handlers', 'meta', 'tasks', 'templates', 'vars', 'tests')
+
+ @classmethod
+ def setUpRole(cls, role_name, galaxy_args=None, skeleton_path=None, use_explicit_type=False):
+ if galaxy_args is None:
+ galaxy_args = []
+
+ if skeleton_path is not None:
+ cls.role_skeleton_path = skeleton_path
+ galaxy_args += ['--role-skeleton', skeleton_path]
+
+ # Make temp directory for testing
+ cls.test_dir = tempfile.mkdtemp()
+ if not os.path.isdir(cls.test_dir):
+ os.makedirs(cls.test_dir)
+
+ cls.role_dir = os.path.join(cls.test_dir, role_name)
+ cls.role_name = role_name
+
+ # create role using default skeleton
+ args = ['ansible-galaxy']
+ if use_explicit_type:
+ args += ['role']
+ args += ['init', '-c', '--offline'] + galaxy_args + ['--init-path', cls.test_dir, cls.role_name]
+
+ gc = GalaxyCLI(args=args)
+ gc.run()
+ cls.gc = gc
+
+ if skeleton_path is None:
+ cls.role_skeleton_path = gc.galaxy.default_role_skeleton_path
+
+ @classmethod
+ def tearDownClass(cls):
+ if os.path.isdir(cls.test_dir):
+ shutil.rmtree(cls.test_dir)
+
+ def test_metadata(self):
+ with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf:
+ metadata = yaml.safe_load(mf)
+ self.assertIn('galaxy_info', metadata, msg='unable to find galaxy_info in metadata')
+ self.assertIn('dependencies', metadata, msg='unable to find dependencies in metadata')
+
+ def test_readme(self):
+ readme_path = os.path.join(self.role_dir, 'README.md')
+ self.assertTrue(os.path.exists(readme_path), msg='Readme doesn\'t exist')
+
+ def test_main_ymls(self):
+ need_main_ymls = set(self.expected_role_dirs) - set(['meta', 'tests', 'files', 'templates'])
+ for d in need_main_ymls:
+ main_yml = os.path.join(self.role_dir, d, 'main.yml')
+ self.assertTrue(os.path.exists(main_yml))
+ expected_string = "---\n# {0} file for {1}".format(d, self.role_name)
+ with open(main_yml, 'r') as f:
+ self.assertEqual(expected_string, f.read().strip())
+
+ def test_role_dirs(self):
+ for d in self.expected_role_dirs:
+ self.assertTrue(os.path.isdir(os.path.join(self.role_dir, d)), msg="Expected role subdirectory {0} doesn't exist".format(d))
+
+ def test_readme_contents(self):
+ with open(os.path.join(self.role_dir, 'README.md'), 'r') as readme:
+ contents = readme.read()
+
+ with open(os.path.join(self.role_skeleton_path, 'README.md'), 'r') as f:
+ expected_contents = f.read()
+
+ self.assertEqual(expected_contents, contents, msg='README.md does not match expected')
+
+ def test_test_yml(self):
+ with open(os.path.join(self.role_dir, 'tests', 'test.yml'), 'r') as f:
+ test_playbook = yaml.safe_load(f)
+ print(test_playbook)
+ self.assertEqual(len(test_playbook), 1)
+ self.assertEqual(test_playbook[0]['hosts'], 'localhost')
+ self.assertEqual(test_playbook[0]['remote_user'], 'root')
+ self.assertListEqual(test_playbook[0]['roles'], [self.role_name], msg='The list of roles included in the test play doesn\'t match')
+
+
+class TestGalaxyInitDefault(unittest.TestCase, ValidRoleTests):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.setUpRole(role_name='delete_me')
+
+ def test_metadata_contents(self):
+ with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf:
+ metadata = yaml.safe_load(mf)
+ self.assertEqual(metadata.get('galaxy_info', dict()).get('author'), 'your name', msg='author was not set properly in metadata')
+
+
+class TestGalaxyInitAPB(unittest.TestCase, ValidRoleTests):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.setUpRole('delete_me_apb', galaxy_args=['--type=apb'])
+
+ def test_metadata_apb_tag(self):
+ with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf:
+ metadata = yaml.safe_load(mf)
+ self.assertIn('apb', metadata.get('galaxy_info', dict()).get('galaxy_tags', []), msg='apb tag not set in role metadata')
+
+ def test_metadata_contents(self):
+ with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf:
+ metadata = yaml.safe_load(mf)
+ self.assertEqual(metadata.get('galaxy_info', dict()).get('author'), 'your name', msg='author was not set properly in metadata')
+
+ def test_apb_yml(self):
+ self.assertTrue(os.path.exists(os.path.join(self.role_dir, 'apb.yml')), msg='apb.yml was not created')
+
+ def test_test_yml(self):
+ with open(os.path.join(self.role_dir, 'tests', 'test.yml'), 'r') as f:
+ test_playbook = yaml.safe_load(f)
+ print(test_playbook)
+ self.assertEqual(len(test_playbook), 1)
+ self.assertEqual(test_playbook[0]['hosts'], 'localhost')
+ self.assertFalse(test_playbook[0]['gather_facts'])
+ self.assertEqual(test_playbook[0]['connection'], 'local')
+ self.assertIsNone(test_playbook[0]['tasks'], msg='We\'re expecting an unset list of tasks in test.yml')
+
+
+class TestGalaxyInitContainer(unittest.TestCase, ValidRoleTests):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.setUpRole('delete_me_container', galaxy_args=['--type=container'])
+
+ def test_metadata_container_tag(self):
+ with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf:
+ metadata = yaml.safe_load(mf)
+ self.assertIn('container', metadata.get('galaxy_info', dict()).get('galaxy_tags', []), msg='container tag not set in role metadata')
+
+ def test_metadata_contents(self):
+ with open(os.path.join(self.role_dir, 'meta', 'main.yml'), 'r') as mf:
+ metadata = yaml.safe_load(mf)
+ self.assertEqual(metadata.get('galaxy_info', dict()).get('author'), 'your name', msg='author was not set properly in metadata')
+
+ def test_meta_container_yml(self):
+ self.assertTrue(os.path.exists(os.path.join(self.role_dir, 'meta', 'container.yml')), msg='container.yml was not created')
+
+ def test_test_yml(self):
+ with open(os.path.join(self.role_dir, 'tests', 'test.yml'), 'r') as f:
+ test_playbook = yaml.safe_load(f)
+ print(test_playbook)
+ self.assertEqual(len(test_playbook), 1)
+ self.assertEqual(test_playbook[0]['hosts'], 'localhost')
+ self.assertFalse(test_playbook[0]['gather_facts'])
+ self.assertEqual(test_playbook[0]['connection'], 'local')
+ self.assertIsNone(test_playbook[0]['tasks'], msg='We\'re expecting an unset list of tasks in test.yml')
+
+
+class TestGalaxyInitSkeleton(unittest.TestCase, ValidRoleTests):
+
+ @classmethod
+ def setUpClass(cls):
+ role_skeleton_path = os.path.join(os.path.split(__file__)[0], 'test_data', 'role_skeleton')
+ cls.setUpRole('delete_me_skeleton', skeleton_path=role_skeleton_path, use_explicit_type=True)
+
+ def test_empty_files_dir(self):
+ files_dir = os.path.join(self.role_dir, 'files')
+ self.assertTrue(os.path.isdir(files_dir))
+ self.assertListEqual(os.listdir(files_dir), [], msg='we expect the files directory to be empty, is ignore working?')
+
+ def test_template_ignore_jinja(self):
+ test_conf_j2 = os.path.join(self.role_dir, 'templates', 'test.conf.j2')
+ self.assertTrue(os.path.exists(test_conf_j2), msg="The test.conf.j2 template doesn't seem to exist, is it being rendered as test.conf?")
+ with open(test_conf_j2, 'r') as f:
+ contents = f.read()
+ expected_contents = '[defaults]\ntest_key = {{ test_variable }}'
+ self.assertEqual(expected_contents, contents.strip(), msg="test.conf.j2 doesn't contain what it should, is it being rendered?")
+
+ def test_template_ignore_jinja_subfolder(self):
+ test_conf_j2 = os.path.join(self.role_dir, 'templates', 'subfolder', 'test.conf.j2')
+ self.assertTrue(os.path.exists(test_conf_j2), msg="The test.conf.j2 template doesn't seem to exist, is it being rendered as test.conf?")
+ with open(test_conf_j2, 'r') as f:
+ contents = f.read()
+ expected_contents = '[defaults]\ntest_key = {{ test_variable }}'
+ self.assertEqual(expected_contents, contents.strip(), msg="test.conf.j2 doesn't contain what it should, is it being rendered?")
+
+ def test_template_ignore_similar_folder(self):
+ self.assertTrue(os.path.exists(os.path.join(self.role_dir, 'templates_extra', 'templates.txt')))
+
+ def test_skeleton_option(self):
+ self.assertEqual(self.role_skeleton_path, context.CLIARGS['role_skeleton'], msg='Skeleton path was not parsed properly from the command line')
+
+
+@pytest.mark.parametrize('cli_args, expected', [
+ (['ansible-galaxy', 'collection', 'init', 'abc._def'], 0),
+ (['ansible-galaxy', 'collection', 'init', 'abc._def', '-vvv'], 3),
+ (['ansible-galaxy', 'collection', 'init', 'abc._def', '-vv'], 2),
+])
+def test_verbosity_arguments(cli_args, expected, monkeypatch):
+ # Mock out the functions so we don't actually execute anything
+ for func_name in [f for f in dir(GalaxyCLI) if f.startswith("execute_")]:
+ monkeypatch.setattr(GalaxyCLI, func_name, MagicMock())
+
+ cli = GalaxyCLI(args=cli_args)
+ cli.run()
+
+ assert context.CLIARGS['verbosity'] == expected
+
+
+@pytest.fixture()
+def collection_skeleton(request, tmp_path_factory):
+ name, skeleton_path = request.param
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'init', '-c']
+
+ if skeleton_path is not None:
+ galaxy_args += ['--collection-skeleton', skeleton_path]
+
+ test_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections'))
+ galaxy_args += ['--init-path', test_dir, name]
+
+ GalaxyCLI(args=galaxy_args).run()
+ namespace_name, collection_name = name.split('.', 1)
+ collection_dir = os.path.join(test_dir, namespace_name, collection_name)
+
+ return collection_dir
+
+
+@pytest.mark.parametrize('collection_skeleton', [
+ ('ansible_test.my_collection', None),
+], indirect=True)
+def test_collection_default(collection_skeleton):
+ meta_path = os.path.join(collection_skeleton, 'galaxy.yml')
+
+ with open(meta_path, 'r') as galaxy_meta:
+ metadata = yaml.safe_load(galaxy_meta)
+
+ assert metadata['namespace'] == 'ansible_test'
+ assert metadata['name'] == 'my_collection'
+ assert metadata['authors'] == ['your name <example@domain.com>']
+ assert metadata['readme'] == 'README.md'
+ assert metadata['version'] == '1.0.0'
+ assert metadata['description'] == 'your collection description'
+ assert metadata['license'] == ['GPL-2.0-or-later']
+ assert metadata['tags'] == []
+ assert metadata['dependencies'] == {}
+ assert metadata['documentation'] == 'http://docs.example.com'
+ assert metadata['repository'] == 'http://example.com/repository'
+ assert metadata['homepage'] == 'http://example.com'
+ assert metadata['issues'] == 'http://example.com/issue/tracker'
+
+ for d in ['docs', 'plugins', 'roles']:
+ assert os.path.isdir(os.path.join(collection_skeleton, d)), \
+ "Expected collection subdirectory {0} doesn't exist".format(d)
+
+
+@pytest.mark.parametrize('collection_skeleton', [
+ ('ansible_test.delete_me_skeleton', os.path.join(os.path.split(__file__)[0], 'test_data', 'collection_skeleton')),
+], indirect=True)
+def test_collection_skeleton(collection_skeleton):
+ meta_path = os.path.join(collection_skeleton, 'galaxy.yml')
+
+ with open(meta_path, 'r') as galaxy_meta:
+ metadata = yaml.safe_load(galaxy_meta)
+
+ assert metadata['namespace'] == 'ansible_test'
+ assert metadata['name'] == 'delete_me_skeleton'
+ assert metadata['authors'] == ['Ansible Cow <acow@bovineuniversity.edu>', 'Tu Cow <tucow@bovineuniversity.edu>']
+ assert metadata['version'] == '0.1.0'
+ assert metadata['readme'] == 'README.md'
+ assert len(metadata) == 5
+
+ assert os.path.exists(os.path.join(collection_skeleton, 'README.md'))
+
+ # Test empty directories exist and are empty
+ for empty_dir in ['plugins/action', 'plugins/filter', 'plugins/inventory', 'plugins/lookup',
+ 'plugins/module_utils', 'plugins/modules']:
+
+ assert os.listdir(os.path.join(collection_skeleton, empty_dir)) == []
+
+ # Test files that don't end with .j2 were not templated
+ doc_file = os.path.join(collection_skeleton, 'docs', 'My Collection.md')
+ with open(doc_file, 'r') as f:
+ doc_contents = f.read()
+ assert doc_contents.strip() == 'Welcome to my test collection doc for {{ namespace }}.'
+
+ # Test files that end with .j2 but are in the templates directory were not templated
+ for template_dir in ['playbooks/templates', 'playbooks/templates/subfolder',
+ 'roles/common/templates', 'roles/common/templates/subfolder']:
+ test_conf_j2 = os.path.join(collection_skeleton, template_dir, 'test.conf.j2')
+ assert os.path.exists(test_conf_j2)
+
+ with open(test_conf_j2, 'r') as f:
+ contents = f.read()
+ expected_contents = '[defaults]\ntest_key = {{ test_variable }}'
+
+ assert expected_contents == contents.strip()
+
+
+@pytest.fixture()
+def collection_artifact(collection_skeleton, tmp_path_factory):
+ ''' Creates a collection artifact tarball that is ready to be published and installed '''
+ output_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Output'))
+
+ # Create a file with +x in the collection so we can test the permissions
+ execute_path = os.path.join(collection_skeleton, 'runme.sh')
+ with open(execute_path, mode='wb') as fd:
+ fd.write(b"echo hi")
+
+ # S_ISUID should not be present on extraction.
+ os.chmod(execute_path, os.stat(execute_path).st_mode | stat.S_ISUID | stat.S_IEXEC)
+
+ # Because we call GalaxyCLI in collection_skeleton we need to reset the singleton back to None so it uses the new
+ # args, we reset the original args once it is done.
+ orig_cli_args = co.GlobalCLIArgs._Singleton__instance
+ try:
+ co.GlobalCLIArgs._Singleton__instance = None
+ galaxy_args = ['ansible-galaxy', 'collection', 'build', collection_skeleton, '--output-path', output_dir]
+ gc = GalaxyCLI(args=galaxy_args)
+ gc.run()
+
+ yield output_dir
+ finally:
+ co.GlobalCLIArgs._Singleton__instance = orig_cli_args
+
+
+def test_invalid_skeleton_path():
+ expected = "- the skeleton path '/fake/path' does not exist, cannot init collection"
+
+ gc = GalaxyCLI(args=['ansible-galaxy', 'collection', 'init', 'my.collection', '--collection-skeleton',
+ '/fake/path'])
+ with pytest.raises(AnsibleError, match=expected):
+ gc.run()
+
+
+@pytest.mark.parametrize("name", [
+ "",
+ "invalid",
+ "hypen-ns.collection",
+ "ns.hyphen-collection",
+ "ns.collection.weird",
+])
+def test_invalid_collection_name_init(name):
+ expected = "Invalid collection name '%s', name must be in the format <namespace>.<collection>" % name
+
+ gc = GalaxyCLI(args=['ansible-galaxy', 'collection', 'init', name])
+ with pytest.raises(AnsibleError, match=expected):
+ gc.run()
+
+
+@pytest.mark.parametrize("name, expected", [
+ ("", ""),
+ ("invalid", "invalid"),
+ ("invalid:1.0.0", "invalid"),
+ ("hypen-ns.collection", "hypen-ns.collection"),
+ ("ns.hyphen-collection", "ns.hyphen-collection"),
+ ("ns.collection.weird", "ns.collection.weird"),
+])
+def test_invalid_collection_name_install(name, expected, tmp_path_factory):
+ install_path = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections'))
+
+ # FIXME: we should add the collection name in the error message
+ # Used to be: expected = "Invalid collection name '%s', name must be in the format <namespace>.<collection>" % expected
+ expected = "Neither the collection requirement entry key 'name', nor 'source' point to a concrete resolvable collection artifact. "
+ expected += r"Also 'name' is not an FQCN\. A valid collection name must be in the format <namespace>\.<collection>\. "
+ expected += r"Please make sure that the namespace and the collection name contain characters from \[a\-zA\-Z0\-9_\] only\."
+
+ gc = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', name, '-p', os.path.join(install_path, 'install')])
+ with pytest.raises(AnsibleError, match=expected):
+ gc.run()
+
+
+@pytest.mark.parametrize('collection_skeleton', [
+ ('ansible_test.build_collection', None),
+], indirect=True)
+def test_collection_build(collection_artifact):
+ tar_path = os.path.join(collection_artifact, 'ansible_test-build_collection-1.0.0.tar.gz')
+ assert tarfile.is_tarfile(tar_path)
+
+ with tarfile.open(tar_path, mode='r') as tar:
+ tar_members = tar.getmembers()
+
+ valid_files = ['MANIFEST.json', 'FILES.json', 'roles', 'docs', 'plugins', 'plugins/README.md', 'README.md',
+ 'runme.sh', 'meta', 'meta/runtime.yml']
+ assert len(tar_members) == len(valid_files)
+
+ # Verify the uid and gid is 0 and the correct perms are set
+ for member in tar_members:
+ assert member.name in valid_files
+
+ assert member.gid == 0
+ assert member.gname == ''
+ assert member.uid == 0
+ assert member.uname == ''
+ if member.isdir() or member.name == 'runme.sh':
+ assert member.mode == 0o0755
+ else:
+ assert member.mode == 0o0644
+
+ manifest_file = tar.extractfile(tar_members[0])
+ try:
+ manifest = json.loads(to_text(manifest_file.read()))
+ finally:
+ manifest_file.close()
+
+ coll_info = manifest['collection_info']
+ file_manifest = manifest['file_manifest_file']
+ assert manifest['format'] == 1
+ assert len(manifest.keys()) == 3
+
+ assert coll_info['namespace'] == 'ansible_test'
+ assert coll_info['name'] == 'build_collection'
+ assert coll_info['version'] == '1.0.0'
+ assert coll_info['authors'] == ['your name <example@domain.com>']
+ assert coll_info['readme'] == 'README.md'
+ assert coll_info['tags'] == []
+ assert coll_info['description'] == 'your collection description'
+ assert coll_info['license'] == ['GPL-2.0-or-later']
+ assert coll_info['license_file'] is None
+ assert coll_info['dependencies'] == {}
+ assert coll_info['repository'] == 'http://example.com/repository'
+ assert coll_info['documentation'] == 'http://docs.example.com'
+ assert coll_info['homepage'] == 'http://example.com'
+ assert coll_info['issues'] == 'http://example.com/issue/tracker'
+ assert len(coll_info.keys()) == 14
+
+ assert file_manifest['name'] == 'FILES.json'
+ assert file_manifest['ftype'] == 'file'
+ assert file_manifest['chksum_type'] == 'sha256'
+ assert file_manifest['chksum_sha256'] is not None # Order of keys makes it hard to verify the checksum
+ assert file_manifest['format'] == 1
+ assert len(file_manifest.keys()) == 5
+
+ files_file = tar.extractfile(tar_members[1])
+ try:
+ files = json.loads(to_text(files_file.read()))
+ finally:
+ files_file.close()
+
+ assert len(files['files']) == 9
+ assert files['format'] == 1
+ assert len(files.keys()) == 2
+
+ valid_files_entries = ['.', 'roles', 'docs', 'plugins', 'plugins/README.md', 'README.md', 'runme.sh', 'meta', 'meta/runtime.yml']
+ for file_entry in files['files']:
+ assert file_entry['name'] in valid_files_entries
+ assert file_entry['format'] == 1
+
+ if file_entry['name'] in ['plugins/README.md', 'runme.sh', 'meta/runtime.yml']:
+ assert file_entry['ftype'] == 'file'
+ assert file_entry['chksum_type'] == 'sha256'
+ # Can't test the actual checksum as the html link changes based on the version or the file contents
+ # don't matter
+ assert file_entry['chksum_sha256'] is not None
+ elif file_entry['name'] == 'README.md':
+ assert file_entry['ftype'] == 'file'
+ assert file_entry['chksum_type'] == 'sha256'
+ assert file_entry['chksum_sha256'] == '6d8b5f9b5d53d346a8cd7638a0ec26e75e8d9773d952162779a49d25da6ef4f5'
+ else:
+ assert file_entry['ftype'] == 'dir'
+ assert file_entry['chksum_type'] is None
+ assert file_entry['chksum_sha256'] is None
+
+ assert len(file_entry.keys()) == 5
+
+
+@pytest.fixture()
+def collection_install(reset_cli_args, tmp_path_factory, monkeypatch):
+ mock_install = MagicMock()
+ monkeypatch.setattr(ansible.cli.galaxy, 'install_collections', mock_install)
+
+ mock_warning = MagicMock()
+ monkeypatch.setattr(ansible.utils.display.Display, 'warning', mock_warning)
+
+ output_dir = to_text((tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Output')))
+ yield mock_install, mock_warning, output_dir
+
+
+def test_collection_install_with_names(collection_install):
+ mock_install, mock_warning, output_dir = collection_install
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', 'namespace.collection', 'namespace2.collection:1.0.1',
+ '--collections-path', output_dir]
+ GalaxyCLI(args=galaxy_args).run()
+
+ collection_path = os.path.join(output_dir, 'ansible_collections')
+ assert os.path.isdir(collection_path)
+
+ assert mock_warning.call_count == 1
+ assert "The specified collections path '%s' is not part of the configured Ansible collections path" % output_dir \
+ in mock_warning.call_args[0][0]
+
+ assert mock_install.call_count == 1
+ requirements = [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type,) for r in mock_install.call_args[0][0]]
+ assert requirements == [('namespace.collection', '*', None, 'galaxy'),
+ ('namespace2.collection', '1.0.1', None, 'galaxy')]
+ assert mock_install.call_args[0][1] == collection_path
+ assert len(mock_install.call_args[0][2]) == 1
+ assert mock_install.call_args[0][2][0].api_server == 'https://galaxy.ansible.com'
+ assert mock_install.call_args[0][2][0].validate_certs is True
+ assert mock_install.call_args[0][3] is False # ignore_errors
+ assert mock_install.call_args[0][4] is False # no_deps
+ assert mock_install.call_args[0][5] is False # force
+ assert mock_install.call_args[0][6] is False # force_deps
+
+
+def test_collection_install_with_requirements_file(collection_install):
+ mock_install, mock_warning, output_dir = collection_install
+
+ requirements_file = os.path.join(output_dir, 'requirements.yml')
+ with open(requirements_file, 'wb') as req_obj:
+ req_obj.write(b'''---
+collections:
+- namespace.coll
+- name: namespace2.coll
+ version: '>2.0.1'
+''')
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', '--requirements-file', requirements_file,
+ '--collections-path', output_dir]
+ GalaxyCLI(args=galaxy_args).run()
+
+ collection_path = os.path.join(output_dir, 'ansible_collections')
+ assert os.path.isdir(collection_path)
+
+ assert mock_warning.call_count == 1
+ assert "The specified collections path '%s' is not part of the configured Ansible collections path" % output_dir \
+ in mock_warning.call_args[0][0]
+
+ assert mock_install.call_count == 1
+ requirements = [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type,) for r in mock_install.call_args[0][0]]
+ assert requirements == [('namespace.coll', '*', None, 'galaxy'),
+ ('namespace2.coll', '>2.0.1', None, 'galaxy')]
+ assert mock_install.call_args[0][1] == collection_path
+ assert mock_install.call_args[0][2][0].api_server == 'https://galaxy.ansible.com'
+ assert mock_install.call_args[0][2][0].validate_certs is True
+ assert mock_install.call_args[0][3] is False # ignore_errors
+ assert mock_install.call_args[0][4] is False # no_deps
+ assert mock_install.call_args[0][5] is False # force
+ assert mock_install.call_args[0][6] is False # force_deps
+
+
+def test_collection_install_with_relative_path(collection_install, monkeypatch):
+ mock_install = collection_install[0]
+
+ mock_req = MagicMock()
+ mock_req.return_value = {'collections': [('namespace.coll', '*', None, None)], 'roles': []}
+ monkeypatch.setattr(ansible.cli.galaxy.GalaxyCLI, '_parse_requirements_file', mock_req)
+
+ monkeypatch.setattr(os, 'makedirs', MagicMock())
+
+ requirements_file = './requirements.myl'
+ collections_path = './ansible_collections'
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', '--requirements-file', requirements_file,
+ '--collections-path', collections_path]
+ GalaxyCLI(args=galaxy_args).run()
+
+ assert mock_install.call_count == 1
+ assert mock_install.call_args[0][0] == [('namespace.coll', '*', None, None)]
+ assert mock_install.call_args[0][1] == os.path.abspath(collections_path)
+ assert len(mock_install.call_args[0][2]) == 1
+ assert mock_install.call_args[0][2][0].api_server == 'https://galaxy.ansible.com'
+ assert mock_install.call_args[0][2][0].validate_certs is True
+ assert mock_install.call_args[0][3] is False # ignore_errors
+ assert mock_install.call_args[0][4] is False # no_deps
+ assert mock_install.call_args[0][5] is False # force
+ assert mock_install.call_args[0][6] is False # force_deps
+
+ assert mock_req.call_count == 1
+ assert mock_req.call_args[0][0] == os.path.abspath(requirements_file)
+
+
+def test_collection_install_with_unexpanded_path(collection_install, monkeypatch):
+ mock_install = collection_install[0]
+
+ mock_req = MagicMock()
+ mock_req.return_value = {'collections': [('namespace.coll', '*', None, None)], 'roles': []}
+ monkeypatch.setattr(ansible.cli.galaxy.GalaxyCLI, '_parse_requirements_file', mock_req)
+
+ monkeypatch.setattr(os, 'makedirs', MagicMock())
+
+ requirements_file = '~/requirements.myl'
+ collections_path = '~/ansible_collections'
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', '--requirements-file', requirements_file,
+ '--collections-path', collections_path]
+ GalaxyCLI(args=galaxy_args).run()
+
+ assert mock_install.call_count == 1
+ assert mock_install.call_args[0][0] == [('namespace.coll', '*', None, None)]
+ assert mock_install.call_args[0][1] == os.path.expanduser(os.path.expandvars(collections_path))
+ assert len(mock_install.call_args[0][2]) == 1
+ assert mock_install.call_args[0][2][0].api_server == 'https://galaxy.ansible.com'
+ assert mock_install.call_args[0][2][0].validate_certs is True
+ assert mock_install.call_args[0][3] is False # ignore_errors
+ assert mock_install.call_args[0][4] is False # no_deps
+ assert mock_install.call_args[0][5] is False # force
+ assert mock_install.call_args[0][6] is False # force_deps
+
+ assert mock_req.call_count == 1
+ assert mock_req.call_args[0][0] == os.path.expanduser(os.path.expandvars(requirements_file))
+
+
+def test_collection_install_in_collection_dir(collection_install, monkeypatch):
+ mock_install, mock_warning, output_dir = collection_install
+
+ collections_path = C.COLLECTIONS_PATHS[0]
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', 'namespace.collection', 'namespace2.collection:1.0.1',
+ '--collections-path', collections_path]
+ GalaxyCLI(args=galaxy_args).run()
+
+ assert mock_warning.call_count == 0
+
+ assert mock_install.call_count == 1
+ requirements = [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type,) for r in mock_install.call_args[0][0]]
+ assert requirements == [('namespace.collection', '*', None, 'galaxy'),
+ ('namespace2.collection', '1.0.1', None, 'galaxy')]
+ assert mock_install.call_args[0][1] == os.path.join(collections_path, 'ansible_collections')
+ assert len(mock_install.call_args[0][2]) == 1
+ assert mock_install.call_args[0][2][0].api_server == 'https://galaxy.ansible.com'
+ assert mock_install.call_args[0][2][0].validate_certs is True
+ assert mock_install.call_args[0][3] is False # ignore_errors
+ assert mock_install.call_args[0][4] is False # no_deps
+ assert mock_install.call_args[0][5] is False # force
+ assert mock_install.call_args[0][6] is False # force_deps
+
+
+def test_collection_install_with_url(monkeypatch, collection_install):
+ mock_install, dummy, output_dir = collection_install
+
+ mock_open = MagicMock(return_value=BytesIO())
+ monkeypatch.setattr(collection.concrete_artifact_manager, 'open_url', mock_open)
+
+ mock_metadata = MagicMock(return_value={'namespace': 'foo', 'name': 'bar', 'version': 'v1.0.0'})
+ monkeypatch.setattr(collection.concrete_artifact_manager, '_get_meta_from_tar', mock_metadata)
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', 'https://foo/bar/foo-bar-v1.0.0.tar.gz',
+ '--collections-path', output_dir]
+ GalaxyCLI(args=galaxy_args).run()
+
+ collection_path = os.path.join(output_dir, 'ansible_collections')
+ assert os.path.isdir(collection_path)
+
+ assert mock_install.call_count == 1
+ requirements = [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type,) for r in mock_install.call_args[0][0]]
+ assert requirements == [('foo.bar', 'v1.0.0', 'https://foo/bar/foo-bar-v1.0.0.tar.gz', 'url')]
+ assert mock_install.call_args[0][1] == collection_path
+ assert len(mock_install.call_args[0][2]) == 1
+ assert mock_install.call_args[0][2][0].api_server == 'https://galaxy.ansible.com'
+ assert mock_install.call_args[0][2][0].validate_certs is True
+ assert mock_install.call_args[0][3] is False # ignore_errors
+ assert mock_install.call_args[0][4] is False # no_deps
+ assert mock_install.call_args[0][5] is False # force
+ assert mock_install.call_args[0][6] is False # force_deps
+
+
+def test_collection_install_name_and_requirements_fail(collection_install):
+ test_path = collection_install[2]
+ expected = 'The positional collection_name arg and --requirements-file are mutually exclusive.'
+
+ with pytest.raises(AnsibleError, match=expected):
+ GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection', '--collections-path',
+ test_path, '--requirements-file', test_path]).run()
+
+
+def test_collection_install_no_name_and_requirements_fail(collection_install):
+ test_path = collection_install[2]
+ expected = 'You must specify a collection name or a requirements file.'
+
+ with pytest.raises(AnsibleError, match=expected):
+ GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', '--collections-path', test_path]).run()
+
+
+def test_collection_install_path_with_ansible_collections(collection_install):
+ mock_install, mock_warning, output_dir = collection_install
+
+ collection_path = os.path.join(output_dir, 'ansible_collections')
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', 'namespace.collection', 'namespace2.collection:1.0.1',
+ '--collections-path', collection_path]
+ GalaxyCLI(args=galaxy_args).run()
+
+ assert os.path.isdir(collection_path)
+
+ assert mock_warning.call_count == 1
+ assert "The specified collections path '%s' is not part of the configured Ansible collections path" \
+ % collection_path in mock_warning.call_args[0][0]
+
+ assert mock_install.call_count == 1
+ requirements = [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type,) for r in mock_install.call_args[0][0]]
+ assert requirements == [('namespace.collection', '*', None, 'galaxy'),
+ ('namespace2.collection', '1.0.1', None, 'galaxy')]
+ assert mock_install.call_args[0][1] == collection_path
+ assert len(mock_install.call_args[0][2]) == 1
+ assert mock_install.call_args[0][2][0].api_server == 'https://galaxy.ansible.com'
+ assert mock_install.call_args[0][2][0].validate_certs is True
+ assert mock_install.call_args[0][3] is False # ignore_errors
+ assert mock_install.call_args[0][4] is False # no_deps
+ assert mock_install.call_args[0][5] is False # force
+ assert mock_install.call_args[0][6] is False # force_deps
+
+
+def test_collection_install_ignore_certs(collection_install):
+ mock_install, mock_warning, output_dir = collection_install
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', 'namespace.collection', '--collections-path', output_dir,
+ '--ignore-certs']
+ GalaxyCLI(args=galaxy_args).run()
+
+ assert mock_install.call_args[0][3] is False
+
+
+def test_collection_install_force(collection_install):
+ mock_install, mock_warning, output_dir = collection_install
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', 'namespace.collection', '--collections-path', output_dir,
+ '--force']
+ GalaxyCLI(args=galaxy_args).run()
+
+ # mock_install args: collections, output_path, apis, ignore_errors, no_deps, force, force_deps
+ assert mock_install.call_args[0][5] is True
+
+
+def test_collection_install_force_deps(collection_install):
+ mock_install, mock_warning, output_dir = collection_install
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', 'namespace.collection', '--collections-path', output_dir,
+ '--force-with-deps']
+ GalaxyCLI(args=galaxy_args).run()
+
+ # mock_install args: collections, output_path, apis, ignore_errors, no_deps, force, force_deps
+ assert mock_install.call_args[0][6] is True
+
+
+def test_collection_install_no_deps(collection_install):
+ mock_install, mock_warning, output_dir = collection_install
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', 'namespace.collection', '--collections-path', output_dir,
+ '--no-deps']
+ GalaxyCLI(args=galaxy_args).run()
+
+ # mock_install args: collections, output_path, apis, ignore_errors, no_deps, force, force_deps
+ assert mock_install.call_args[0][4] is True
+
+
+def test_collection_install_ignore(collection_install):
+ mock_install, mock_warning, output_dir = collection_install
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', 'namespace.collection', '--collections-path', output_dir,
+ '--ignore-errors']
+ GalaxyCLI(args=galaxy_args).run()
+
+ # mock_install args: collections, output_path, apis, ignore_errors, no_deps, force, force_deps
+ assert mock_install.call_args[0][3] is True
+
+
+def test_collection_install_custom_server(collection_install):
+ mock_install, mock_warning, output_dir = collection_install
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'install', 'namespace.collection', '--collections-path', output_dir,
+ '--server', 'https://galaxy-dev.ansible.com']
+ GalaxyCLI(args=galaxy_args).run()
+
+ assert len(mock_install.call_args[0][2]) == 1
+ assert mock_install.call_args[0][2][0].api_server == 'https://galaxy-dev.ansible.com'
+ assert mock_install.call_args[0][2][0].validate_certs is True
+
+
+@pytest.fixture()
+def requirements_file(request, tmp_path_factory):
+ content = request.param
+
+ test_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Requirements'))
+ requirements_file = os.path.join(test_dir, 'requirements.yml')
+
+ if content:
+ with open(requirements_file, 'wb') as req_obj:
+ req_obj.write(to_bytes(content))
+
+ yield requirements_file
+
+
+@pytest.fixture()
+def requirements_cli(monkeypatch):
+ monkeypatch.setattr(GalaxyCLI, 'execute_install', MagicMock())
+ cli = GalaxyCLI(args=['ansible-galaxy', 'install'])
+ cli.run()
+ return cli
+
+
+@pytest.mark.parametrize('requirements_file', [None], indirect=True)
+def test_parse_requirements_file_that_doesnt_exist(requirements_cli, requirements_file):
+ expected = "The requirements file '%s' does not exist." % to_native(requirements_file)
+ with pytest.raises(AnsibleError, match=expected):
+ requirements_cli._parse_requirements_file(requirements_file)
+
+
+@pytest.mark.parametrize('requirements_file', ['not a valid yml file: hi: world'], indirect=True)
+def test_parse_requirements_file_that_isnt_yaml(requirements_cli, requirements_file):
+ expected = "Failed to parse the requirements yml at '%s' with the following error" % to_native(requirements_file)
+ with pytest.raises(AnsibleError, match=expected):
+ requirements_cli._parse_requirements_file(requirements_file)
+
+
+@pytest.mark.parametrize('requirements_file', [('''
+# Older role based requirements.yml
+- galaxy.role
+- anotherrole
+''')], indirect=True)
+def test_parse_requirements_in_older_format_illega(requirements_cli, requirements_file):
+ expected = "Expecting requirements file to be a dict with the key 'collections' that contains a list of " \
+ "collections to install"
+
+ with pytest.raises(AnsibleError, match=expected):
+ requirements_cli._parse_requirements_file(requirements_file, allow_old_format=False)
+
+
+@pytest.mark.parametrize('requirements_file', ['''
+collections:
+- version: 1.0.0
+'''], indirect=True)
+def test_parse_requirements_without_mandatory_name_key(requirements_cli, requirements_file):
+ # Used to be "Collections requirement entry should contain the key name."
+ # Should we check that either source or name is provided before using the dep resolver?
+
+ expected = "Neither the collection requirement entry key 'name', nor 'source' point to a concrete resolvable collection artifact. "
+ expected += r"Also 'name' is not an FQCN\. A valid collection name must be in the format <namespace>\.<collection>\. "
+ expected += r"Please make sure that the namespace and the collection name contain characters from \[a\-zA\-Z0\-9_\] only\."
+
+ with pytest.raises(AnsibleError, match=expected):
+ requirements_cli._parse_requirements_file(requirements_file)
+
+
+@pytest.mark.parametrize('requirements_file', [('''
+collections:
+- namespace.collection1
+- namespace.collection2
+'''), ('''
+collections:
+- name: namespace.collection1
+- name: namespace.collection2
+''')], indirect=True)
+def test_parse_requirements(requirements_cli, requirements_file):
+ expected = {
+ 'roles': [],
+ 'collections': [('namespace.collection1', '*', None, 'galaxy'), ('namespace.collection2', '*', None, 'galaxy')]
+ }
+ actual = requirements_cli._parse_requirements_file(requirements_file)
+ actual['collections'] = [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type,) for r in actual.get('collections', [])]
+
+ assert actual == expected
+
+
+@pytest.mark.parametrize('requirements_file', ['''
+collections:
+- name: namespace.collection1
+ version: ">=1.0.0,<=2.0.0"
+ source: https://galaxy-dev.ansible.com
+- namespace.collection2'''], indirect=True)
+def test_parse_requirements_with_extra_info(requirements_cli, requirements_file):
+ actual = requirements_cli._parse_requirements_file(requirements_file)
+ actual['collections'] = [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type,) for r in actual.get('collections', [])]
+
+ assert len(actual['roles']) == 0
+ assert len(actual['collections']) == 2
+ assert actual['collections'][0][0] == 'namespace.collection1'
+ assert actual['collections'][0][1] == '>=1.0.0,<=2.0.0'
+ assert actual['collections'][0][2].api_server == 'https://galaxy-dev.ansible.com'
+
+ assert actual['collections'][1] == ('namespace.collection2', '*', None, 'galaxy')
+
+
+@pytest.mark.parametrize('requirements_file', ['''
+roles:
+- username.role_name
+- src: username2.role_name2
+- src: ssh://github.com/user/repo
+ scm: git
+
+collections:
+- namespace.collection2
+'''], indirect=True)
+def test_parse_requirements_with_roles_and_collections(requirements_cli, requirements_file):
+ actual = requirements_cli._parse_requirements_file(requirements_file)
+ actual['collections'] = [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type,) for r in actual.get('collections', [])]
+
+ assert len(actual['roles']) == 3
+ assert actual['roles'][0].name == 'username.role_name'
+ assert actual['roles'][1].name == 'username2.role_name2'
+ assert actual['roles'][2].name == 'repo'
+ assert actual['roles'][2].src == 'ssh://github.com/user/repo'
+
+ assert len(actual['collections']) == 1
+ assert actual['collections'][0] == ('namespace.collection2', '*', None, 'galaxy')
+
+
+@pytest.mark.parametrize('requirements_file', ['''
+collections:
+- name: namespace.collection
+- name: namespace2.collection2
+ source: https://galaxy-dev.ansible.com/
+- name: namespace3.collection3
+ source: server
+'''], indirect=True)
+def test_parse_requirements_with_collection_source(requirements_cli, requirements_file):
+ galaxy_api = GalaxyAPI(requirements_cli.api, 'server', 'https://config-server')
+ requirements_cli.api_servers.append(galaxy_api)
+
+ actual = requirements_cli._parse_requirements_file(requirements_file)
+ actual['collections'] = [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type,) for r in actual.get('collections', [])]
+
+ assert actual['roles'] == []
+ assert len(actual['collections']) == 3
+ assert actual['collections'][0] == ('namespace.collection', '*', None, 'galaxy')
+
+ assert actual['collections'][1][0] == 'namespace2.collection2'
+ assert actual['collections'][1][1] == '*'
+ assert actual['collections'][1][2].api_server == 'https://galaxy-dev.ansible.com/'
+
+ assert actual['collections'][2][0] == 'namespace3.collection3'
+ assert actual['collections'][2][1] == '*'
+ assert actual['collections'][2][2].api_server == 'https://config-server'
+
+
+@pytest.mark.parametrize('requirements_file', ['''
+- username.included_role
+- src: https://github.com/user/repo
+'''], indirect=True)
+def test_parse_requirements_roles_with_include(requirements_cli, requirements_file):
+ reqs = [
+ 'ansible.role',
+ {'include': requirements_file},
+ ]
+ parent_requirements = os.path.join(os.path.dirname(requirements_file), 'parent.yaml')
+ with open(to_bytes(parent_requirements), 'wb') as req_fd:
+ req_fd.write(to_bytes(yaml.safe_dump(reqs)))
+
+ actual = requirements_cli._parse_requirements_file(parent_requirements)
+
+ assert len(actual['roles']) == 3
+ assert actual['collections'] == []
+ assert actual['roles'][0].name == 'ansible.role'
+ assert actual['roles'][1].name == 'username.included_role'
+ assert actual['roles'][2].name == 'repo'
+ assert actual['roles'][2].src == 'https://github.com/user/repo'
+
+
+@pytest.mark.parametrize('requirements_file', ['''
+- username.role
+- include: missing.yml
+'''], indirect=True)
+def test_parse_requirements_roles_with_include_missing(requirements_cli, requirements_file):
+ expected = "Failed to find include requirements file 'missing.yml' in '%s'" % to_native(requirements_file)
+
+ with pytest.raises(AnsibleError, match=expected):
+ requirements_cli._parse_requirements_file(requirements_file)
+
+
+@pytest.mark.parametrize('requirements_file', ['''
+collections:
+- namespace.name
+roles:
+- namespace.name
+'''], indirect=True)
+def test_install_implicit_role_with_collections(requirements_file, monkeypatch):
+ mock_collection_install = MagicMock()
+ monkeypatch.setattr(GalaxyCLI, '_execute_install_collection', mock_collection_install)
+ mock_role_install = MagicMock()
+ monkeypatch.setattr(GalaxyCLI, '_execute_install_role', mock_role_install)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'install', '-r', requirements_file])
+ cli.run()
+
+ assert mock_collection_install.call_count == 1
+ requirements = [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type,) for r in mock_collection_install.call_args[0][0]]
+ assert requirements == [('namespace.name', '*', None, 'galaxy')]
+ assert mock_collection_install.call_args[0][1] == cli._get_default_collection_path()
+
+ assert mock_role_install.call_count == 1
+ assert len(mock_role_install.call_args[0][0]) == 1
+ assert str(mock_role_install.call_args[0][0][0]) == 'namespace.name'
+
+ found = False
+ for mock_call in mock_display.mock_calls:
+ if 'contains collections which will be ignored' in mock_call[1][0]:
+ found = True
+ break
+ assert not found
+
+
+@pytest.mark.parametrize('requirements_file', ['''
+collections:
+- namespace.name
+roles:
+- namespace.name
+'''], indirect=True)
+def test_install_explicit_role_with_collections(requirements_file, monkeypatch):
+ mock_collection_install = MagicMock()
+ monkeypatch.setattr(GalaxyCLI, '_execute_install_collection', mock_collection_install)
+ mock_role_install = MagicMock()
+ monkeypatch.setattr(GalaxyCLI, '_execute_install_role', mock_role_install)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'vvv', mock_display)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'role', 'install', '-r', requirements_file])
+ cli.run()
+
+ assert mock_collection_install.call_count == 0
+
+ assert mock_role_install.call_count == 1
+ assert len(mock_role_install.call_args[0][0]) == 1
+ assert str(mock_role_install.call_args[0][0][0]) == 'namespace.name'
+
+ found = False
+ for mock_call in mock_display.mock_calls:
+ if 'contains collections which will be ignored' in mock_call[1][0]:
+ found = True
+ break
+ assert found
+
+
+@pytest.mark.parametrize('requirements_file', ['''
+collections:
+- namespace.name
+roles:
+- namespace.name
+'''], indirect=True)
+def test_install_role_with_collections_and_path(requirements_file, monkeypatch):
+ mock_collection_install = MagicMock()
+ monkeypatch.setattr(GalaxyCLI, '_execute_install_collection', mock_collection_install)
+ mock_role_install = MagicMock()
+ monkeypatch.setattr(GalaxyCLI, '_execute_install_role', mock_role_install)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'warning', mock_display)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'install', '-p', 'path', '-r', requirements_file])
+ cli.run()
+
+ assert mock_collection_install.call_count == 0
+
+ assert mock_role_install.call_count == 1
+ assert len(mock_role_install.call_args[0][0]) == 1
+ assert str(mock_role_install.call_args[0][0][0]) == 'namespace.name'
+
+ found = False
+ for mock_call in mock_display.mock_calls:
+ if 'contains collections which will be ignored' in mock_call[1][0]:
+ found = True
+ break
+ assert found
+
+
+@pytest.mark.parametrize('requirements_file', ['''
+collections:
+- namespace.name
+roles:
+- namespace.name
+'''], indirect=True)
+def test_install_collection_with_roles(requirements_file, monkeypatch):
+ mock_collection_install = MagicMock()
+ monkeypatch.setattr(GalaxyCLI, '_execute_install_collection', mock_collection_install)
+ mock_role_install = MagicMock()
+ monkeypatch.setattr(GalaxyCLI, '_execute_install_role', mock_role_install)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'vvv', mock_display)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', '-r', requirements_file])
+ cli.run()
+
+ assert mock_collection_install.call_count == 1
+ requirements = [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type,) for r in mock_collection_install.call_args[0][0]]
+ assert requirements == [('namespace.name', '*', None, 'galaxy')]
+
+ assert mock_role_install.call_count == 0
+
+ found = False
+ for mock_call in mock_display.mock_calls:
+ if 'contains roles which will be ignored' in mock_call[1][0]:
+ found = True
+ break
+ assert found
diff --git a/test/units/cli/test_playbook.py b/test/units/cli/test_playbook.py
new file mode 100644
index 0000000..f25e54d
--- /dev/null
+++ b/test/units/cli/test_playbook.py
@@ -0,0 +1,46 @@
+# (c) 2016, Adrian Likins <alikins@redhat.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from units.mock.loader import DictDataLoader
+
+from ansible import context
+from ansible.inventory.manager import InventoryManager
+from ansible.vars.manager import VariableManager
+
+from ansible.cli.playbook import PlaybookCLI
+
+
+class TestPlaybookCLI(unittest.TestCase):
+ def test_flush_cache(self):
+ cli = PlaybookCLI(args=["ansible-playbook", "--flush-cache", "foobar.yml"])
+ cli.parse()
+ self.assertTrue(context.CLIARGS['flush_cache'])
+
+ variable_manager = VariableManager()
+ fake_loader = DictDataLoader({'foobar.yml': ""})
+ inventory = InventoryManager(loader=fake_loader, sources='testhost,')
+
+ variable_manager.set_host_facts('testhost', {'canary': True})
+ self.assertTrue('testhost' in variable_manager._fact_cache)
+
+ cli._flush_cache(inventory, variable_manager)
+ self.assertFalse('testhost' in variable_manager._fact_cache)
diff --git a/test/units/cli/test_vault.py b/test/units/cli/test_vault.py
new file mode 100644
index 0000000..2304f4d
--- /dev/null
+++ b/test/units/cli/test_vault.py
@@ -0,0 +1,230 @@
+# -*- coding: utf-8 -*-
+# (c) 2017, Adrian Likins <alikins@redhat.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import pytest
+
+from units.compat import unittest
+from unittest.mock import patch, MagicMock
+from units.mock.vault_helper import TextVaultSecret
+
+from ansible import context, errors
+from ansible.cli.vault import VaultCLI
+from ansible.module_utils._text import to_text
+from ansible.utils import context_objects as co
+
+
+# TODO: make these tests assert something, likely by verifing
+# mock calls
+
+
+@pytest.fixture(autouse='function')
+def reset_cli_args():
+ co.GlobalCLIArgs._Singleton__instance = None
+ yield
+ co.GlobalCLIArgs._Singleton__instance = None
+
+
+class TestVaultCli(unittest.TestCase):
+ def setUp(self):
+ self.tty_patcher = patch('ansible.cli.sys.stdin.isatty', return_value=False)
+ self.mock_isatty = self.tty_patcher.start()
+
+ def tearDown(self):
+ self.tty_patcher.stop()
+
+ def test_parse_empty(self):
+ cli = VaultCLI(['vaultcli'])
+ self.assertRaises(SystemExit,
+ cli.parse)
+
+ # FIXME: something weird seems to be afoot when parsing actions
+ # cli = VaultCLI(args=['view', '/dev/null/foo', 'mysecret3'])
+ # will skip '/dev/null/foo'. something in cli.CLI.set_action() ?
+ # maybe we self.args gets modified in a loop?
+ def test_parse_view_file(self):
+ cli = VaultCLI(args=['ansible-vault', 'view', '/dev/null/foo'])
+ cli.parse()
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ def test_view_missing_file_no_secret(self, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = []
+ cli = VaultCLI(args=['ansible-vault', 'view', '/dev/null/foo'])
+ cli.parse()
+ self.assertRaisesRegex(errors.AnsibleOptionsError,
+ "A vault password is required to use Ansible's Vault",
+ cli.run)
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ def test_encrypt_missing_file_no_secret(self, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = []
+ cli = VaultCLI(args=['ansible-vault', 'encrypt', '/dev/null/foo'])
+ cli.parse()
+ self.assertRaisesRegex(errors.AnsibleOptionsError,
+ "A vault password is required to use Ansible's Vault",
+ cli.run)
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ def test_encrypt(self, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault', 'encrypt', '/dev/null/foo'])
+ cli.parse()
+ cli.run()
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ def test_encrypt_string(self, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault', 'encrypt_string',
+ 'some string to encrypt'])
+ cli.parse()
+ cli.run()
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ @patch('ansible.cli.vault.display.prompt', return_value='a_prompt')
+ def test_encrypt_string_prompt(self, mock_display, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault',
+ 'encrypt_string',
+ '--prompt',
+ '--show-input',
+ 'some string to encrypt'])
+ cli.parse()
+ cli.run()
+ args, kwargs = mock_display.call_args
+ assert kwargs["private"] is False
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ @patch('ansible.cli.vault.display.prompt', return_value='a_prompt')
+ def test_shadowed_encrypt_string_prompt(self, mock_display, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault',
+ 'encrypt_string',
+ '--prompt',
+ 'some string to encrypt'])
+ cli.parse()
+ cli.run()
+ args, kwargs = mock_display.call_args
+ assert kwargs["private"]
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ @patch('ansible.cli.vault.sys.stdin.read', return_value='This is data from stdin')
+ def test_encrypt_string_stdin(self, mock_stdin_read, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault',
+ 'encrypt_string',
+ '--stdin-name',
+ 'the_var_from_stdin',
+ '-'])
+ cli.parse()
+ cli.run()
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ def test_encrypt_string_names(self, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault', 'encrypt_string',
+ '--name', 'foo1',
+ '--name', 'foo2',
+ 'some string to encrypt'])
+ cli.parse()
+ cli.run()
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ def test_encrypt_string_more_args_than_names(self, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault', 'encrypt_string',
+ '--name', 'foo1',
+ 'some string to encrypt',
+ 'other strings',
+ 'a few more string args'])
+ cli.parse()
+ cli.run()
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ def test_create(self, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault', 'create', '/dev/null/foo'])
+ cli.parse()
+ cli.run()
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ def test_edit(self, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault', 'edit', '/dev/null/foo'])
+ cli.parse()
+ cli.run()
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ def test_decrypt(self, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault', 'decrypt', '/dev/null/foo'])
+ cli.parse()
+ cli.run()
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ def test_view(self, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault', 'view', '/dev/null/foo'])
+ cli.parse()
+ cli.run()
+
+ @patch('ansible.cli.vault.VaultCLI.setup_vault_secrets')
+ @patch('ansible.cli.vault.VaultEditor')
+ def test_rekey(self, mock_vault_editor, mock_setup_vault_secrets):
+ mock_setup_vault_secrets.return_value = [('default', TextVaultSecret('password'))]
+ cli = VaultCLI(args=['ansible-vault', 'rekey', '/dev/null/foo'])
+ cli.parse()
+ cli.run()
+
+
+@pytest.mark.parametrize('cli_args, expected', [
+ (['ansible-vault', 'view', 'vault.txt'], 0),
+ (['ansible-vault', 'view', 'vault.txt', '-vvv'], 3),
+ (['ansible-vault', 'view', 'vault.txt', '-vv'], 2),
+])
+def test_verbosity_arguments(cli_args, expected, tmp_path_factory, monkeypatch):
+ # Add a password file so we don't get a prompt in the test
+ test_dir = to_text(tmp_path_factory.mktemp('test-ansible-vault'))
+ pass_file = os.path.join(test_dir, 'pass.txt')
+ with open(pass_file, 'w') as pass_fd:
+ pass_fd.write('password')
+
+ cli_args.extend(['--vault-id', pass_file])
+
+ # Mock out the functions so we don't actually execute anything
+ for func_name in [f for f in dir(VaultCLI) if f.startswith("execute_")]:
+ monkeypatch.setattr(VaultCLI, func_name, MagicMock())
+
+ cli = VaultCLI(args=cli_args)
+ cli.run()
+
+ assert context.CLIARGS['verbosity'] == expected
diff --git a/test/units/compat/__init__.py b/test/units/compat/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/compat/__init__.py
diff --git a/test/units/compat/mock.py b/test/units/compat/mock.py
new file mode 100644
index 0000000..58dc78e
--- /dev/null
+++ b/test/units/compat/mock.py
@@ -0,0 +1,23 @@
+"""
+Compatibility shim for mock imports in modules and module_utils.
+This can be removed once support for Python 2.7 is dropped.
+"""
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+try:
+ from unittest.mock import (
+ call,
+ patch,
+ mock_open,
+ MagicMock,
+ Mock,
+ )
+except ImportError:
+ from mock import (
+ call,
+ patch,
+ mock_open,
+ MagicMock,
+ Mock,
+ )
diff --git a/test/units/compat/unittest.py b/test/units/compat/unittest.py
new file mode 100644
index 0000000..b416774
--- /dev/null
+++ b/test/units/compat/unittest.py
@@ -0,0 +1,29 @@
+# (c) 2014, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+# Allow wildcard import because we really do want to import all of
+# unittests's symbols into this compat shim
+# pylint: disable=wildcard-import,unused-wildcard-import
+from unittest import *
+
+if not hasattr(TestCase, 'assertRaisesRegex'):
+ # added in Python 3.2
+ TestCase.assertRaisesRegex = TestCase.assertRaisesRegexp
diff --git a/test/units/config/__init__.py b/test/units/config/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/config/__init__.py
diff --git a/test/units/config/manager/__init__.py b/test/units/config/manager/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/config/manager/__init__.py
diff --git a/test/units/config/manager/test_find_ini_config_file.py b/test/units/config/manager/test_find_ini_config_file.py
new file mode 100644
index 0000000..df41138
--- /dev/null
+++ b/test/units/config/manager/test_find_ini_config_file.py
@@ -0,0 +1,253 @@
+# -*- coding: utf-8 -*-
+# Copyright: (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
+
+import os
+import os.path
+import stat
+
+import pytest
+
+from ansible.config.manager import find_ini_config_file
+from ansible.module_utils._text import to_text
+
+real_exists = os.path.exists
+real_isdir = os.path.isdir
+
+working_dir = os.path.dirname(__file__)
+cfg_in_cwd = os.path.join(working_dir, 'ansible.cfg')
+
+cfg_dir = os.path.join(working_dir, 'data')
+cfg_file = os.path.join(cfg_dir, 'ansible.cfg')
+alt_cfg_file = os.path.join(cfg_dir, 'test.cfg')
+cfg_in_homedir = os.path.expanduser('~/.ansible.cfg')
+
+
+@pytest.fixture
+def setup_env(request):
+ cur_config = os.environ.get('ANSIBLE_CONFIG', None)
+ cfg_path = request.param[0]
+
+ if cfg_path is None and cur_config:
+ del os.environ['ANSIBLE_CONFIG']
+ else:
+ os.environ['ANSIBLE_CONFIG'] = request.param[0]
+
+ yield
+
+ if cur_config is None and cfg_path:
+ del os.environ['ANSIBLE_CONFIG']
+ else:
+ os.environ['ANSIBLE_CONFIG'] = cur_config
+
+
+@pytest.fixture
+def setup_existing_files(request, monkeypatch):
+ def _os_path_exists(path):
+ if to_text(path) in (request.param[0]):
+ return True
+ else:
+ return False
+
+ def _os_access(path, access):
+ if to_text(path) in (request.param[0]):
+ return True
+ else:
+ return False
+
+ # Enable user and system dirs so that we know cwd takes precedence
+ monkeypatch.setattr("os.path.exists", _os_path_exists)
+ monkeypatch.setattr("os.access", _os_access)
+ monkeypatch.setattr("os.getcwd", lambda: os.path.dirname(cfg_dir))
+ monkeypatch.setattr("os.path.isdir", lambda path: True if to_text(path) == cfg_dir else real_isdir(path))
+
+
+class TestFindIniFile:
+ # This tells us to run twice, once with a file specified and once with a directory
+ @pytest.mark.parametrize('setup_env, expected', (([alt_cfg_file], alt_cfg_file), ([cfg_dir], cfg_file)), indirect=['setup_env'])
+ # This just passes the list of files that exist to the fixture
+ @pytest.mark.parametrize('setup_existing_files',
+ [[('/etc/ansible/ansible.cfg', cfg_in_homedir, cfg_in_cwd, alt_cfg_file, cfg_file)]],
+ indirect=['setup_existing_files'])
+ def test_env_has_cfg_file(self, setup_env, setup_existing_files, expected):
+ """ANSIBLE_CONFIG is specified, use it"""
+ warnings = set()
+ assert find_ini_config_file(warnings) == expected
+ assert warnings == set()
+
+ @pytest.mark.parametrize('setup_env', ([alt_cfg_file], [cfg_dir]), indirect=['setup_env'])
+ @pytest.mark.parametrize('setup_existing_files',
+ [[('/etc/ansible/ansible.cfg', cfg_in_homedir, cfg_in_cwd)]],
+ indirect=['setup_existing_files'])
+ def test_env_has_no_cfg_file(self, setup_env, setup_existing_files):
+ """ANSIBLE_CONFIG is specified but the file does not exist"""
+
+ warnings = set()
+ # since the cfg file specified by ANSIBLE_CONFIG doesn't exist, the one at cwd that does
+ # exist should be returned
+ assert find_ini_config_file(warnings) == cfg_in_cwd
+ assert warnings == set()
+
+ # ANSIBLE_CONFIG not specified
+ @pytest.mark.parametrize('setup_env', [[None]], indirect=['setup_env'])
+ # All config files are present
+ @pytest.mark.parametrize('setup_existing_files',
+ [[('/etc/ansible/ansible.cfg', cfg_in_homedir, cfg_in_cwd, cfg_file, alt_cfg_file)]],
+ indirect=['setup_existing_files'])
+ def test_ini_in_cwd(self, setup_env, setup_existing_files):
+ """ANSIBLE_CONFIG not specified. Use the cwd cfg"""
+ warnings = set()
+ assert find_ini_config_file(warnings) == cfg_in_cwd
+ assert warnings == set()
+
+ # ANSIBLE_CONFIG not specified
+ @pytest.mark.parametrize('setup_env', [[None]], indirect=['setup_env'])
+ # No config in cwd
+ @pytest.mark.parametrize('setup_existing_files',
+ [[('/etc/ansible/ansible.cfg', cfg_in_homedir, cfg_file, alt_cfg_file)]],
+ indirect=['setup_existing_files'])
+ def test_ini_in_homedir(self, setup_env, setup_existing_files):
+ """First config found is in the homedir"""
+ warnings = set()
+ assert find_ini_config_file(warnings) == cfg_in_homedir
+ assert warnings == set()
+
+ # ANSIBLE_CONFIG not specified
+ @pytest.mark.parametrize('setup_env', [[None]], indirect=['setup_env'])
+ # No config in cwd
+ @pytest.mark.parametrize('setup_existing_files', [[('/etc/ansible/ansible.cfg', cfg_file, alt_cfg_file)]], indirect=['setup_existing_files'])
+ def test_ini_in_systemdir(self, setup_env, setup_existing_files):
+ """First config found is the system config"""
+ warnings = set()
+ assert find_ini_config_file(warnings) == '/etc/ansible/ansible.cfg'
+ assert warnings == set()
+
+ # ANSIBLE_CONFIG not specified
+ @pytest.mark.parametrize('setup_env', [[None]], indirect=['setup_env'])
+ # No config in cwd
+ @pytest.mark.parametrize('setup_existing_files',
+ [[('/etc/ansible/ansible.cfg', cfg_in_homedir, cfg_file, alt_cfg_file)]],
+ indirect=['setup_existing_files'])
+ def test_cwd_does_not_exist(self, setup_env, setup_existing_files, monkeypatch):
+ """Smoketest current working directory doesn't exist"""
+ def _os_stat(path):
+ raise OSError('%s does not exist' % path)
+ monkeypatch.setattr('os.stat', _os_stat)
+
+ warnings = set()
+ assert find_ini_config_file(warnings) == cfg_in_homedir
+ assert warnings == set()
+
+ @pytest.mark.parametrize('setup_env', [[None]], indirect=['setup_env'])
+ # No config in cwd
+ @pytest.mark.parametrize('setup_existing_files', [[list()]], indirect=['setup_existing_files'])
+ def test_no_config(self, setup_env, setup_existing_files):
+ """No config present, no config found"""
+ warnings = set()
+ assert find_ini_config_file(warnings) is None
+ assert warnings == set()
+
+ # ANSIBLE_CONFIG not specified
+ @pytest.mark.parametrize('setup_env', [[None]], indirect=['setup_env'])
+ # All config files are present except in cwd
+ @pytest.mark.parametrize('setup_existing_files',
+ [[('/etc/ansible/ansible.cfg', cfg_in_homedir, cfg_file, alt_cfg_file)]],
+ indirect=['setup_existing_files'])
+ def test_no_cwd_cfg_no_warning_on_writable(self, setup_env, setup_existing_files, monkeypatch):
+ """If the cwd is writable but there is no config file there, move on with no warning"""
+ real_stat = os.stat
+
+ def _os_stat(path):
+ if path == working_dir:
+ from posix import stat_result
+ stat_info = list(real_stat(path))
+ stat_info[stat.ST_MODE] |= stat.S_IWOTH
+ return stat_result(stat_info)
+ else:
+ return real_stat(path)
+
+ monkeypatch.setattr('os.stat', _os_stat)
+
+ warnings = set()
+ assert find_ini_config_file(warnings) == cfg_in_homedir
+ assert len(warnings) == 0
+
+ # ANSIBLE_CONFIG not specified
+ @pytest.mark.parametrize('setup_env', [[None]], indirect=['setup_env'])
+ # All config files are present
+ @pytest.mark.parametrize('setup_existing_files',
+ [[('/etc/ansible/ansible.cfg', cfg_in_homedir, cfg_in_cwd, cfg_file, alt_cfg_file)]],
+ indirect=['setup_existing_files'])
+ def test_cwd_warning_on_writable(self, setup_env, setup_existing_files, monkeypatch):
+ """If the cwd is writable, warn and skip it """
+ real_stat = os.stat
+
+ def _os_stat(path):
+ if path == working_dir:
+ from posix import stat_result
+ stat_info = list(real_stat(path))
+ stat_info[stat.ST_MODE] |= stat.S_IWOTH
+ return stat_result(stat_info)
+ else:
+ return real_stat(path)
+
+ monkeypatch.setattr('os.stat', _os_stat)
+
+ warnings = set()
+ assert find_ini_config_file(warnings) == cfg_in_homedir
+ assert len(warnings) == 1
+ warning = warnings.pop()
+ assert u'Ansible is being run in a world writable directory' in warning
+ assert u'ignoring it as an ansible.cfg source' in warning
+
+ # ANSIBLE_CONFIG is sepcified
+ @pytest.mark.parametrize('setup_env, expected', (([alt_cfg_file], alt_cfg_file), ([cfg_in_cwd], cfg_in_cwd)), indirect=['setup_env'])
+ # All config files are present
+ @pytest.mark.parametrize('setup_existing_files',
+ [[('/etc/ansible/ansible.cfg', cfg_in_homedir, cfg_in_cwd, cfg_file, alt_cfg_file)]],
+ indirect=['setup_existing_files'])
+ def test_no_warning_on_writable_if_env_used(self, setup_env, setup_existing_files, monkeypatch, expected):
+ """If the cwd is writable but ANSIBLE_CONFIG was used, no warning should be issued"""
+ real_stat = os.stat
+
+ def _os_stat(path):
+ if path == working_dir:
+ from posix import stat_result
+ stat_info = list(real_stat(path))
+ stat_info[stat.ST_MODE] |= stat.S_IWOTH
+ return stat_result(stat_info)
+ else:
+ return real_stat(path)
+
+ monkeypatch.setattr('os.stat', _os_stat)
+
+ warnings = set()
+ assert find_ini_config_file(warnings) == expected
+ assert warnings == set()
+
+ # ANSIBLE_CONFIG not specified
+ @pytest.mark.parametrize('setup_env', [[None]], indirect=['setup_env'])
+ # All config files are present
+ @pytest.mark.parametrize('setup_existing_files',
+ [[('/etc/ansible/ansible.cfg', cfg_in_homedir, cfg_in_cwd, cfg_file, alt_cfg_file)]],
+ indirect=['setup_existing_files'])
+ def test_cwd_warning_on_writable_no_warning_set(self, setup_env, setup_existing_files, monkeypatch):
+ """Smoketest that the function succeeds even though no warning set was passed in"""
+ real_stat = os.stat
+
+ def _os_stat(path):
+ if path == working_dir:
+ from posix import stat_result
+ stat_info = list(real_stat(path))
+ stat_info[stat.ST_MODE] |= stat.S_IWOTH
+ return stat_result(stat_info)
+ else:
+ return real_stat(path)
+
+ monkeypatch.setattr('os.stat', _os_stat)
+
+ assert find_ini_config_file() == cfg_in_homedir
diff --git a/test/units/config/test.cfg b/test/units/config/test.cfg
new file mode 100644
index 0000000..57958d8
--- /dev/null
+++ b/test/units/config/test.cfg
@@ -0,0 +1,4 @@
+[defaults]
+inikey=fromini
+matterless=lessfromini
+mattermore=morefromini
diff --git a/test/units/config/test.yml b/test/units/config/test.yml
new file mode 100644
index 0000000..384a055
--- /dev/null
+++ b/test/units/config/test.yml
@@ -0,0 +1,55 @@
+# mock config defs with diff use cases
+config_entry: &entry
+ name: test config
+ default: DEFAULT
+ description:
+ - This does nothing, its for testing
+ env:
+ - name: ENVVAR
+ ini:
+ - section: defaults
+ key: inikey
+ type: string
+config_entry_multi: &entry_multi
+ name: has more than one entry per config source
+ default: DEFAULT
+ description:
+ - This does nothing, its for testing
+ env:
+ - name: MATTERLESS
+ - name: MATTERMORE
+ ini:
+ - section: defaults
+ key: matterless
+ - section: defaults
+ key: mattermore
+ type: string
+config_entry_bool:
+ <<: *entry
+ type: bool
+ default: False
+config_entry_list:
+ <<: *entry
+ type: list
+ default: [DEFAULT]
+config_entry_deprecated:
+ <<: *entry
+ deprecated: &dep
+ why: 'cause i wanna'
+ version: 9.2
+ alternative: 'none whatso ever'
+config_entry_multi_deprecated:
+ <<: *entry_multi
+ deprecated: *dep
+config_entry_multi_deprecated_source:
+ <<: *entry_multi
+ env:
+ - name: MATTERLESS
+ deprecated: *dep
+ - name: MATTERMORE
+ ini:
+ - section: defaults
+ key: matterless
+ deprecated: *dep
+ - section: defaults
+ key: mattermore
diff --git a/test/units/config/test2.cfg b/test/units/config/test2.cfg
new file mode 100644
index 0000000..da2d77b
--- /dev/null
+++ b/test/units/config/test2.cfg
@@ -0,0 +1,4 @@
+[defaults]
+inikey=fromini2
+matterless=lessfromini2
+mattermore=morefromini2
diff --git a/test/units/config/test_manager.py b/test/units/config/test_manager.py
new file mode 100644
index 0000000..8ef4043
--- /dev/null
+++ b/test/units/config/test_manager.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+# Copyright: (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
+
+import os
+import os.path
+import pytest
+
+from ansible.config.manager import ConfigManager, Setting, ensure_type, resolve_path, get_config_type
+from ansible.errors import AnsibleOptionsError, AnsibleError
+from ansible.module_utils.six import integer_types, string_types
+from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
+
+curdir = os.path.dirname(__file__)
+cfg_file = os.path.join(curdir, 'test.cfg')
+cfg_file2 = os.path.join(curdir, 'test2.cfg')
+
+ensure_test_data = [
+ ('a,b', 'list', list),
+ (['a', 'b'], 'list', list),
+ ('y', 'bool', bool),
+ ('yes', 'bool', bool),
+ ('on', 'bool', bool),
+ ('1', 'bool', bool),
+ ('true', 'bool', bool),
+ ('t', 'bool', bool),
+ (1, 'bool', bool),
+ (1.0, 'bool', bool),
+ (True, 'bool', bool),
+ ('n', 'bool', bool),
+ ('no', 'bool', bool),
+ ('off', 'bool', bool),
+ ('0', 'bool', bool),
+ ('false', 'bool', bool),
+ ('f', 'bool', bool),
+ (0, 'bool', bool),
+ (0.0, 'bool', bool),
+ (False, 'bool', bool),
+ ('10', 'int', integer_types),
+ (20, 'int', integer_types),
+ ('0.10', 'float', float),
+ (0.2, 'float', float),
+ ('/tmp/test.yml', 'pathspec', list),
+ ('/tmp/test.yml,/home/test2.yml', 'pathlist', list),
+ ('a', 'str', string_types),
+ ('a', 'string', string_types),
+ ('Café', 'string', string_types),
+ ('', 'string', string_types),
+ ('29', 'str', string_types),
+ ('13.37', 'str', string_types),
+ ('123j', 'string', string_types),
+ ('0x123', 'string', string_types),
+ ('true', 'string', string_types),
+ ('True', 'string', string_types),
+ (0, 'str', string_types),
+ (29, 'str', string_types),
+ (13.37, 'str', string_types),
+ (123j, 'string', string_types),
+ (0x123, 'string', string_types),
+ (True, 'string', string_types),
+ ('None', 'none', type(None))
+]
+
+
+class TestConfigManager:
+ @classmethod
+ def setup_class(cls):
+ cls.manager = ConfigManager(cfg_file, os.path.join(curdir, 'test.yml'))
+
+ @classmethod
+ def teardown_class(cls):
+ cls.manager = None
+
+ @pytest.mark.parametrize("value, expected_type, python_type", ensure_test_data)
+ def test_ensure_type(self, value, expected_type, python_type):
+ assert isinstance(ensure_type(value, expected_type), python_type)
+
+ def test_resolve_path(self):
+ assert os.path.join(curdir, 'test.yml') == resolve_path('./test.yml', cfg_file)
+
+ def test_resolve_path_cwd(self):
+ assert os.path.join(os.getcwd(), 'test.yml') == resolve_path('{{CWD}}/test.yml')
+ assert os.path.join(os.getcwd(), 'test.yml') == resolve_path('./test.yml')
+
+ def test_value_and_origin_from_ini(self):
+ assert self.manager.get_config_value_and_origin('config_entry') == ('fromini', cfg_file)
+
+ def test_value_from_ini(self):
+ assert self.manager.get_config_value('config_entry') == 'fromini'
+
+ def test_value_and_origin_from_alt_ini(self):
+ assert self.manager.get_config_value_and_origin('config_entry', cfile=cfg_file2) == ('fromini2', cfg_file2)
+
+ def test_value_from_alt_ini(self):
+ assert self.manager.get_config_value('config_entry', cfile=cfg_file2) == 'fromini2'
+
+ def test_config_types(self):
+ assert get_config_type('/tmp/ansible.ini') == 'ini'
+ assert get_config_type('/tmp/ansible.cfg') == 'ini'
+ assert get_config_type('/tmp/ansible.yaml') == 'yaml'
+ assert get_config_type('/tmp/ansible.yml') == 'yaml'
+
+ def test_config_types_negative(self):
+ with pytest.raises(AnsibleOptionsError) as exec_info:
+ get_config_type('/tmp/ansible.txt')
+ assert "Unsupported configuration file extension for" in str(exec_info.value)
+
+ def test_read_config_yaml_file(self):
+ assert isinstance(self.manager._read_config_yaml_file(os.path.join(curdir, 'test.yml')), dict)
+
+ def test_read_config_yaml_file_negative(self):
+ with pytest.raises(AnsibleError) as exec_info:
+ self.manager._read_config_yaml_file(os.path.join(curdir, 'test_non_existent.yml'))
+
+ assert "Missing base YAML definition file (bad install?)" in str(exec_info.value)
+
+ def test_entry_as_vault_var(self):
+ class MockVault:
+
+ def decrypt(self, value, filename=None, obj=None):
+ return value
+
+ vault_var = AnsibleVaultEncryptedUnicode(b"vault text")
+ vault_var.vault = MockVault()
+
+ actual_value, actual_origin = self.manager._loop_entries({'name': vault_var}, [{'name': 'name'}])
+ assert actual_value == "vault text"
+ assert actual_origin == "name"
+
+ @pytest.mark.parametrize("value_type", ("str", "string", None))
+ def test_ensure_type_with_vaulted_str(self, value_type):
+ class MockVault:
+ def decrypt(self, value, filename=None, obj=None):
+ return value
+
+ vault_var = AnsibleVaultEncryptedUnicode(b"vault text")
+ vault_var.vault = MockVault()
+
+ actual_value = ensure_type(vault_var, value_type)
+ assert actual_value == "vault text"
diff --git a/test/units/errors/__init__.py b/test/units/errors/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/errors/__init__.py
diff --git a/test/units/errors/test_errors.py b/test/units/errors/test_errors.py
new file mode 100644
index 0000000..7a1de3d
--- /dev/null
+++ b/test/units/errors/test_errors.py
@@ -0,0 +1,150 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+from units.compat import unittest
+from unittest.mock import mock_open, patch
+from ansible.errors import AnsibleError
+from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject
+
+
+class TestErrors(unittest.TestCase):
+
+ def setUp(self):
+ self.message = 'This is the error message'
+ self.unicode_message = 'This is an error with \xf0\x9f\x98\xa8 in it'
+
+ self.obj = AnsibleBaseYAMLObject()
+
+ def test_basic_error(self):
+ e = AnsibleError(self.message)
+ self.assertEqual(e.message, self.message)
+ self.assertEqual(repr(e), self.message)
+
+ def test_basic_unicode_error(self):
+ e = AnsibleError(self.unicode_message)
+ self.assertEqual(e.message, self.unicode_message)
+ self.assertEqual(repr(e), self.unicode_message)
+
+ @patch.object(AnsibleError, '_get_error_lines_from_file')
+ def test_error_with_kv(self, mock_method):
+ ''' This tests a task with both YAML and k=v syntax
+
+ - lineinfile: line=foo path=bar
+ line: foo
+
+ An accurate error message and position indicator are expected.
+
+ _get_error_lines_from_file() returns (target_line, prev_line)
+ '''
+
+ self.obj.ansible_pos = ('foo.yml', 2, 1)
+
+ mock_method.return_value = [' line: foo\n', '- lineinfile: line=foo path=bar\n']
+
+ e = AnsibleError(self.message, self.obj)
+ self.assertEqual(
+ e.message,
+ ("This is the error message\n\nThe error appears to be in 'foo.yml': line 1, column 19, but may\nbe elsewhere in the "
+ "file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n- lineinfile: line=foo path=bar\n"
+ " ^ here\n\n"
+ "There appears to be both 'k=v' shorthand syntax and YAML in this task. Only one syntax may be used.\n")
+ )
+
+ @patch.object(AnsibleError, '_get_error_lines_from_file')
+ def test_error_with_object(self, mock_method):
+ self.obj.ansible_pos = ('foo.yml', 1, 1)
+
+ mock_method.return_value = ('this is line 1\n', '')
+ e = AnsibleError(self.message, self.obj)
+
+ self.assertEqual(
+ e.message,
+ ("This is the error message\n\nThe error appears to be in 'foo.yml': line 1, column 1, but may\nbe elsewhere in the file depending on the "
+ "exact syntax problem.\n\nThe offending line appears to be:\n\n\nthis is line 1\n^ here\n")
+ )
+
+ def test_get_error_lines_from_file(self):
+ m = mock_open()
+ m.return_value.readlines.return_value = ['this is line 1\n']
+
+ with patch('builtins.open', m):
+ # this line will be found in the file
+ self.obj.ansible_pos = ('foo.yml', 1, 1)
+ e = AnsibleError(self.message, self.obj)
+ self.assertEqual(
+ e.message,
+ ("This is the error message\n\nThe error appears to be in 'foo.yml': line 1, column 1, but may\nbe elsewhere in the file depending on "
+ "the exact syntax problem.\n\nThe offending line appears to be:\n\n\nthis is line 1\n^ here\n")
+ )
+
+ with patch('ansible.errors.to_text', side_effect=IndexError('Raised intentionally')):
+ # raise an IndexError
+ self.obj.ansible_pos = ('foo.yml', 2, 1)
+ e = AnsibleError(self.message, self.obj)
+ self.assertEqual(
+ e.message,
+ ("This is the error message\n\nThe error appears to be in 'foo.yml': line 2, column 1, but may\nbe elsewhere in the file depending on "
+ "the exact syntax problem.\n\n(specified line no longer in file, maybe it changed?)")
+ )
+
+ m = mock_open()
+ m.return_value.readlines.return_value = ['this line has unicode \xf0\x9f\x98\xa8 in it!\n']
+
+ with patch('builtins.open', m):
+ # this line will be found in the file
+ self.obj.ansible_pos = ('foo.yml', 1, 1)
+ e = AnsibleError(self.unicode_message, self.obj)
+ self.assertEqual(
+ e.message,
+ ("This is an error with \xf0\x9f\x98\xa8 in it\n\nThe error appears to be in 'foo.yml': line 1, column 1, but may\nbe elsewhere in the "
+ "file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\nthis line has unicode \xf0\x9f\x98\xa8 in it!\n^ "
+ "here\n")
+ )
+
+ def test_get_error_lines_error_in_last_line(self):
+ m = mock_open()
+ m.return_value.readlines.return_value = ['this is line 1\n', 'this is line 2\n', 'this is line 3\n']
+
+ with patch('builtins.open', m):
+ # If the error occurs in the last line of the file, use the correct index to get the line
+ # and avoid the IndexError
+ self.obj.ansible_pos = ('foo.yml', 4, 1)
+ e = AnsibleError(self.message, self.obj)
+ self.assertEqual(
+ e.message,
+ ("This is the error message\n\nThe error appears to be in 'foo.yml': line 4, column 1, but may\nbe elsewhere in the file depending on "
+ "the exact syntax problem.\n\nThe offending line appears to be:\n\nthis is line 2\nthis is line 3\n^ here\n")
+ )
+
+ def test_get_error_lines_error_empty_lines_around_error(self):
+ """Test that trailing whitespace after the error is removed"""
+ m = mock_open()
+ m.return_value.readlines.return_value = ['this is line 1\n', 'this is line 2\n', 'this is line 3\n', ' \n', ' \n', ' ']
+
+ with patch('builtins.open', m):
+ self.obj.ansible_pos = ('foo.yml', 5, 1)
+ e = AnsibleError(self.message, self.obj)
+ self.assertEqual(
+ e.message,
+ ("This is the error message\n\nThe error appears to be in 'foo.yml': line 5, column 1, but may\nbe elsewhere in the file depending on "
+ "the exact syntax problem.\n\nThe offending line appears to be:\n\nthis is line 2\nthis is line 3\n^ here\n")
+ )
diff --git a/test/units/executor/__init__.py b/test/units/executor/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/executor/__init__.py
diff --git a/test/units/executor/module_common/test_modify_module.py b/test/units/executor/module_common/test_modify_module.py
new file mode 100644
index 0000000..dceef76
--- /dev/null
+++ b/test/units/executor/module_common/test_modify_module.py
@@ -0,0 +1,43 @@
+# Copyright (c) 2018 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+# -*- coding: utf-8 -*-
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible.executor.module_common import modify_module
+from ansible.module_utils.six import PY2
+
+from test_module_common import templar
+
+
+FAKE_OLD_MODULE = b'''#!/usr/bin/python
+import sys
+print('{"result": "%s"}' % sys.executable)
+'''
+
+
+@pytest.fixture
+def fake_old_module_open(mocker):
+ m = mocker.mock_open(read_data=FAKE_OLD_MODULE)
+ if PY2:
+ mocker.patch('__builtin__.open', m)
+ else:
+ mocker.patch('builtins.open', m)
+
+# this test no longer makes sense, since a Python module will always either have interpreter discovery run or
+# an explicit interpreter passed (so we'll never default to the module shebang)
+# def test_shebang(fake_old_module_open, templar):
+# (data, style, shebang) = modify_module('fake_module', 'fake_path', {}, templar)
+# assert shebang == '#!/usr/bin/python'
+
+
+def test_shebang_task_vars(fake_old_module_open, templar):
+ task_vars = {
+ 'ansible_python_interpreter': '/usr/bin/python3'
+ }
+
+ (data, style, shebang) = modify_module('fake_module', 'fake_path', {}, templar, task_vars=task_vars)
+ assert shebang == '#!/usr/bin/python3'
diff --git a/test/units/executor/module_common/test_module_common.py b/test/units/executor/module_common/test_module_common.py
new file mode 100644
index 0000000..fa6add8
--- /dev/null
+++ b/test/units/executor/module_common/test_module_common.py
@@ -0,0 +1,200 @@
+# (c) 2017, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os.path
+
+import pytest
+
+import ansible.errors
+
+from ansible.executor import module_common as amc
+from ansible.executor.interpreter_discovery import InterpreterDiscoveryRequiredError
+from ansible.module_utils.six import PY2
+
+
+class TestStripComments:
+ def test_no_changes(self):
+ no_comments = u"""def some_code():
+ return False"""
+ assert amc._strip_comments(no_comments) == no_comments
+
+ def test_all_comments(self):
+ all_comments = u"""# This is a test
+ # Being as it is
+ # To be
+ """
+ assert amc._strip_comments(all_comments) == u""
+
+ def test_all_whitespace(self):
+ # Note: Do not remove the spaces on the blank lines below. They're
+ # test data to show that the lines get removed despite having spaces
+ # on them
+ all_whitespace = u"""
+
+
+
+\t\t\r\n
+ """ # nopep8
+ assert amc._strip_comments(all_whitespace) == u""
+
+ def test_somewhat_normal(self):
+ mixed = u"""#!/usr/bin/python
+
+# here we go
+def test(arg):
+ # this is a thing
+ thing = '# test'
+ return thing
+# End
+"""
+ mixed_results = u"""def test(arg):
+ thing = '# test'
+ return thing"""
+ assert amc._strip_comments(mixed) == mixed_results
+
+
+class TestSlurp:
+ def test_slurp_nonexistent(self, mocker):
+ mocker.patch('os.path.exists', side_effect=lambda x: False)
+ with pytest.raises(ansible.errors.AnsibleError):
+ amc._slurp('no_file')
+
+ def test_slurp_file(self, mocker):
+ mocker.patch('os.path.exists', side_effect=lambda x: True)
+ m = mocker.mock_open(read_data='This is a test')
+ if PY2:
+ mocker.patch('__builtin__.open', m)
+ else:
+ mocker.patch('builtins.open', m)
+ assert amc._slurp('some_file') == 'This is a test'
+
+ def test_slurp_file_with_newlines(self, mocker):
+ mocker.patch('os.path.exists', side_effect=lambda x: True)
+ m = mocker.mock_open(read_data='#!/usr/bin/python\ndef test(args):\nprint("hi")\n')
+ if PY2:
+ mocker.patch('__builtin__.open', m)
+ else:
+ mocker.patch('builtins.open', m)
+ assert amc._slurp('some_file') == '#!/usr/bin/python\ndef test(args):\nprint("hi")\n'
+
+
+@pytest.fixture
+def templar():
+ class FakeTemplar:
+ def template(self, template_string, *args, **kwargs):
+ return template_string
+
+ return FakeTemplar()
+
+
+class TestGetShebang:
+ """Note: We may want to change the API of this function in the future. It isn't a great API"""
+ def test_no_interpreter_set(self, templar):
+ # normally this would return /usr/bin/python, but so long as we're defaulting to auto python discovery, we'll get
+ # an InterpreterDiscoveryRequiredError here instead
+ with pytest.raises(InterpreterDiscoveryRequiredError):
+ amc._get_shebang(u'/usr/bin/python', {}, templar)
+
+ def test_python_interpreter(self, templar):
+ assert amc._get_shebang(u'/usr/bin/python3.8', {}, templar) == ('#!/usr/bin/python3.8', u'/usr/bin/python3.8')
+
+ def test_non_python_interpreter(self, templar):
+ assert amc._get_shebang(u'/usr/bin/ruby', {}, templar) == ('#!/usr/bin/ruby', u'/usr/bin/ruby')
+
+ def test_interpreter_set_in_task_vars(self, templar):
+ assert amc._get_shebang(u'/usr/bin/python', {u'ansible_python_interpreter': u'/usr/bin/pypy'}, templar) == \
+ (u'#!/usr/bin/pypy', u'/usr/bin/pypy')
+
+ def test_non_python_interpreter_in_task_vars(self, templar):
+ assert amc._get_shebang(u'/usr/bin/ruby', {u'ansible_ruby_interpreter': u'/usr/local/bin/ruby'}, templar) == \
+ (u'#!/usr/local/bin/ruby', u'/usr/local/bin/ruby')
+
+ def test_with_args(self, templar):
+ assert amc._get_shebang(u'/usr/bin/python', {u'ansible_python_interpreter': u'/usr/bin/python3'}, templar, args=('-tt', '-OO')) == \
+ (u'#!/usr/bin/python3 -tt -OO', u'/usr/bin/python3')
+
+ def test_python_via_env(self, templar):
+ assert amc._get_shebang(u'/usr/bin/python', {u'ansible_python_interpreter': u'/usr/bin/env python'}, templar) == \
+ (u'#!/usr/bin/env python', u'/usr/bin/env python')
+
+
+class TestDetectionRegexes:
+ ANSIBLE_MODULE_UTIL_STRINGS = (
+ # Absolute collection imports
+ b'import ansible_collections.my_ns.my_col.plugins.module_utils.my_util',
+ b'from ansible_collections.my_ns.my_col.plugins.module_utils import my_util',
+ b'from ansible_collections.my_ns.my_col.plugins.module_utils.my_util import my_func',
+ # Absolute core imports
+ b'import ansible.module_utils.basic',
+ b'from ansible.module_utils import basic',
+ b'from ansible.module_utils.basic import AnsibleModule',
+ # Relative imports
+ b'from ..module_utils import basic',
+ b'from .. module_utils import basic',
+ b'from ....module_utils import basic',
+ b'from ..module_utils.basic import AnsibleModule',
+ )
+ NOT_ANSIBLE_MODULE_UTIL_STRINGS = (
+ b'from ansible import release',
+ b'from ..release import __version__',
+ b'from .. import release',
+ b'from ansible.modules.system import ping',
+ b'from ansible_collecitons.my_ns.my_col.plugins.modules import function',
+ )
+
+ OFFSET = os.path.dirname(os.path.dirname(amc.__file__))
+ CORE_PATHS = (
+ ('%s/modules/from_role.py' % OFFSET, 'ansible/modules/from_role'),
+ ('%s/modules/system/ping.py' % OFFSET, 'ansible/modules/system/ping'),
+ ('%s/modules/cloud/amazon/s3.py' % OFFSET, 'ansible/modules/cloud/amazon/s3'),
+ )
+
+ COLLECTION_PATHS = (
+ ('/root/ansible_collections/ns/col/plugins/modules/ping.py',
+ 'ansible_collections/ns/col/plugins/modules/ping'),
+ ('/root/ansible_collections/ns/col/plugins/modules/subdir/ping.py',
+ 'ansible_collections/ns/col/plugins/modules/subdir/ping'),
+ )
+
+ @pytest.mark.parametrize('testcase', ANSIBLE_MODULE_UTIL_STRINGS)
+ def test_detect_new_style_python_module_re(self, testcase):
+ assert amc.NEW_STYLE_PYTHON_MODULE_RE.search(testcase)
+
+ @pytest.mark.parametrize('testcase', NOT_ANSIBLE_MODULE_UTIL_STRINGS)
+ def test_no_detect_new_style_python_module_re(self, testcase):
+ assert not amc.NEW_STYLE_PYTHON_MODULE_RE.search(testcase)
+
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ @pytest.mark.parametrize('testcase, result', CORE_PATHS) # pylint: disable=undefined-variable
+ def test_detect_core_library_path_re(self, testcase, result):
+ assert amc.CORE_LIBRARY_PATH_RE.search(testcase).group('path') == result
+
+ @pytest.mark.parametrize('testcase', (p[0] for p in COLLECTION_PATHS)) # pylint: disable=undefined-variable
+ def test_no_detect_core_library_path_re(self, testcase):
+ assert not amc.CORE_LIBRARY_PATH_RE.search(testcase)
+
+ @pytest.mark.parametrize('testcase, result', COLLECTION_PATHS) # pylint: disable=undefined-variable
+ def test_detect_collection_path_re(self, testcase, result):
+ assert amc.COLLECTION_PATH_RE.search(testcase).group('path') == result
+
+ @pytest.mark.parametrize('testcase', (p[0] for p in CORE_PATHS)) # pylint: disable=undefined-variable
+ def test_no_detect_collection_path_re(self, testcase):
+ assert not amc.COLLECTION_PATH_RE.search(testcase)
diff --git a/test/units/executor/module_common/test_recursive_finder.py b/test/units/executor/module_common/test_recursive_finder.py
new file mode 100644
index 0000000..8136a00
--- /dev/null
+++ b/test/units/executor/module_common/test_recursive_finder.py
@@ -0,0 +1,130 @@
+# (c) 2017, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import pytest
+import zipfile
+
+from collections import namedtuple
+from io import BytesIO
+
+import ansible.errors
+
+from ansible.executor.module_common import recursive_finder
+
+
+# These are the modules that are brought in by module_utils/basic.py This may need to be updated
+# when basic.py gains new imports
+# We will remove these when we modify AnsiBallZ to store its args in a separate file instead of in
+# basic.py
+
+MODULE_UTILS_BASIC_FILES = frozenset(('ansible/__init__.py',
+ 'ansible/module_utils/__init__.py',
+ 'ansible/module_utils/_text.py',
+ 'ansible/module_utils/basic.py',
+ 'ansible/module_utils/six/__init__.py',
+ 'ansible/module_utils/_text.py',
+ 'ansible/module_utils/common/_collections_compat.py',
+ 'ansible/module_utils/common/_json_compat.py',
+ 'ansible/module_utils/common/collections.py',
+ 'ansible/module_utils/common/parameters.py',
+ 'ansible/module_utils/common/warnings.py',
+ 'ansible/module_utils/parsing/convert_bool.py',
+ 'ansible/module_utils/common/__init__.py',
+ 'ansible/module_utils/common/file.py',
+ 'ansible/module_utils/common/locale.py',
+ 'ansible/module_utils/common/process.py',
+ 'ansible/module_utils/common/sys_info.py',
+ 'ansible/module_utils/common/text/__init__.py',
+ 'ansible/module_utils/common/text/converters.py',
+ 'ansible/module_utils/common/text/formatters.py',
+ 'ansible/module_utils/common/validation.py',
+ 'ansible/module_utils/common/_utils.py',
+ 'ansible/module_utils/common/arg_spec.py',
+ 'ansible/module_utils/compat/__init__.py',
+ 'ansible/module_utils/compat/_selectors2.py',
+ 'ansible/module_utils/compat/selectors.py',
+ 'ansible/module_utils/compat/selinux.py',
+ 'ansible/module_utils/distro/__init__.py',
+ 'ansible/module_utils/distro/_distro.py',
+ 'ansible/module_utils/errors.py',
+ 'ansible/module_utils/parsing/__init__.py',
+ 'ansible/module_utils/parsing/convert_bool.py',
+ 'ansible/module_utils/pycompat24.py',
+ 'ansible/module_utils/six/__init__.py',
+ ))
+
+ONLY_BASIC_FILE = frozenset(('ansible/module_utils/basic.py',))
+
+ANSIBLE_LIB = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), 'lib', 'ansible')
+
+
+@pytest.fixture
+def finder_containers():
+ FinderContainers = namedtuple('FinderContainers', ['zf'])
+
+ zipoutput = BytesIO()
+ zf = zipfile.ZipFile(zipoutput, mode='w', compression=zipfile.ZIP_STORED)
+
+ return FinderContainers(zf)
+
+
+class TestRecursiveFinder(object):
+ def test_no_module_utils(self, finder_containers):
+ name = 'ping'
+ data = b'#!/usr/bin/python\nreturn \'{\"changed\": false}\''
+ recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'ping.py'), data, *finder_containers)
+ assert frozenset(finder_containers.zf.namelist()) == MODULE_UTILS_BASIC_FILES
+
+ def test_module_utils_with_syntax_error(self, finder_containers):
+ name = 'fake_module'
+ data = b'#!/usr/bin/python\ndef something(:\n pass\n'
+ with pytest.raises(ansible.errors.AnsibleError) as exec_info:
+ recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'fake_module.py'), data, *finder_containers)
+ assert 'Unable to import fake_module due to invalid syntax' in str(exec_info.value)
+
+ def test_module_utils_with_identation_error(self, finder_containers):
+ name = 'fake_module'
+ data = b'#!/usr/bin/python\n def something():\n pass\n'
+ with pytest.raises(ansible.errors.AnsibleError) as exec_info:
+ recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'fake_module.py'), data, *finder_containers)
+ assert 'Unable to import fake_module due to unexpected indent' in str(exec_info.value)
+
+ #
+ # Test importing six with many permutations because it is not a normal module
+ #
+ def test_from_import_six(self, finder_containers):
+ name = 'ping'
+ data = b'#!/usr/bin/python\nfrom ansible.module_utils import six'
+ recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'ping.py'), data, *finder_containers)
+ assert frozenset(finder_containers.zf.namelist()) == frozenset(('ansible/module_utils/six/__init__.py', )).union(MODULE_UTILS_BASIC_FILES)
+
+ def test_import_six(self, finder_containers):
+ name = 'ping'
+ data = b'#!/usr/bin/python\nimport ansible.module_utils.six'
+ recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'ping.py'), data, *finder_containers)
+ assert frozenset(finder_containers.zf.namelist()) == frozenset(('ansible/module_utils/six/__init__.py', )).union(MODULE_UTILS_BASIC_FILES)
+
+ def test_import_six_from_many_submodules(self, finder_containers):
+ name = 'ping'
+ data = b'#!/usr/bin/python\nfrom ansible.module_utils.six.moves.urllib.parse import urlparse'
+ recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'ping.py'), data, *finder_containers)
+ assert frozenset(finder_containers.zf.namelist()) == frozenset(('ansible/module_utils/six/__init__.py',)).union(MODULE_UTILS_BASIC_FILES)
diff --git a/test/units/executor/test_interpreter_discovery.py b/test/units/executor/test_interpreter_discovery.py
new file mode 100644
index 0000000..43db595
--- /dev/null
+++ b/test/units/executor/test_interpreter_discovery.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+# (c) 2019, Jordan Borean <jborean@redhat.com>
+# 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 unittest.mock import MagicMock
+
+from ansible.executor.interpreter_discovery import discover_interpreter
+from ansible.module_utils._text import to_text
+
+mock_ubuntu_platform_res = to_text(
+ r'{"osrelease_content": "NAME=\"Ubuntu\"\nVERSION=\"16.04.5 LTS (Xenial Xerus)\"\nID=ubuntu\nID_LIKE=debian\n'
+ r'PRETTY_NAME=\"Ubuntu 16.04.5 LTS\"\nVERSION_ID=\"16.04\"\nHOME_URL=\"http://www.ubuntu.com/\"\n'
+ r'SUPPORT_URL=\"http://help.ubuntu.com/\"\nBUG_REPORT_URL=\"http://bugs.launchpad.net/ubuntu/\"\n'
+ r'VERSION_CODENAME=xenial\nUBUNTU_CODENAME=xenial\n", "platform_dist_result": ["Ubuntu", "16.04", "xenial"]}'
+)
+
+
+def test_discovery_interpreter_linux_auto_legacy():
+ res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3.5\n/usr/bin/python3\nENDFOUND'
+
+ mock_action = MagicMock()
+ mock_action._low_level_execute_command.side_effect = [{'stdout': res1}, {'stdout': mock_ubuntu_platform_res}]
+
+ actual = discover_interpreter(mock_action, 'python', 'auto_legacy', {'inventory_hostname': u'host-fóöbär'})
+
+ assert actual == u'/usr/bin/python'
+ assert len(mock_action.method_calls) == 3
+ assert mock_action.method_calls[2][0] == '_discovery_warnings.append'
+ assert u'Distribution Ubuntu 16.04 on host host-fóöbär should use /usr/bin/python3, but is using /usr/bin/python' \
+ u' for backward compatibility' in mock_action.method_calls[2][1][0]
+
+
+def test_discovery_interpreter_linux_auto_legacy_silent():
+ res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3.5\n/usr/bin/python3\nENDFOUND'
+
+ mock_action = MagicMock()
+ mock_action._low_level_execute_command.side_effect = [{'stdout': res1}, {'stdout': mock_ubuntu_platform_res}]
+
+ actual = discover_interpreter(mock_action, 'python', 'auto_legacy_silent', {'inventory_hostname': u'host-fóöbär'})
+
+ assert actual == u'/usr/bin/python'
+ assert len(mock_action.method_calls) == 2
+
+
+def test_discovery_interpreter_linux_auto():
+ res1 = u'PLATFORM\nLinux\nFOUND\n/usr/bin/python\n/usr/bin/python3.5\n/usr/bin/python3\nENDFOUND'
+
+ mock_action = MagicMock()
+ mock_action._low_level_execute_command.side_effect = [{'stdout': res1}, {'stdout': mock_ubuntu_platform_res}]
+
+ actual = discover_interpreter(mock_action, 'python', 'auto', {'inventory_hostname': u'host-fóöbär'})
+
+ assert actual == u'/usr/bin/python3'
+ assert len(mock_action.method_calls) == 2
+
+
+def test_discovery_interpreter_non_linux():
+ mock_action = MagicMock()
+ mock_action._low_level_execute_command.return_value = \
+ {'stdout': u'PLATFORM\nDarwin\nFOUND\n/usr/bin/python\nENDFOUND'}
+
+ actual = discover_interpreter(mock_action, 'python', 'auto_legacy', {'inventory_hostname': u'host-fóöbär'})
+
+ assert actual == u'/usr/bin/python'
+ assert len(mock_action.method_calls) == 2
+ assert mock_action.method_calls[1][0] == '_discovery_warnings.append'
+ assert u'Platform darwin on host host-fóöbär is using the discovered Python interpreter at /usr/bin/python, ' \
+ u'but future installation of another Python interpreter could change the meaning of that path' \
+ in mock_action.method_calls[1][1][0]
+
+
+def test_no_interpreters_found():
+ mock_action = MagicMock()
+ mock_action._low_level_execute_command.return_value = {'stdout': u'PLATFORM\nWindows\nFOUND\nENDFOUND'}
+
+ actual = discover_interpreter(mock_action, 'python', 'auto_legacy', {'inventory_hostname': u'host-fóöbär'})
+
+ assert actual == u'/usr/bin/python'
+ assert len(mock_action.method_calls) == 2
+ assert mock_action.method_calls[1][0] == '_discovery_warnings.append'
+ assert u'No python interpreters found for host host-fóöbär (tried' \
+ in mock_action.method_calls[1][1][0]
diff --git a/test/units/executor/test_play_iterator.py b/test/units/executor/test_play_iterator.py
new file mode 100644
index 0000000..6670888
--- /dev/null
+++ b/test/units/executor/test_play_iterator.py
@@ -0,0 +1,462 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from unittest.mock import patch, MagicMock
+
+from ansible.executor.play_iterator import HostState, PlayIterator, IteratingStates, FailedStates
+from ansible.playbook import Playbook
+from ansible.playbook.play_context import PlayContext
+
+from units.mock.loader import DictDataLoader
+from units.mock.path import mock_unfrackpath_noop
+
+
+class TestPlayIterator(unittest.TestCase):
+
+ def test_host_state(self):
+ hs = HostState(blocks=list(range(0, 10)))
+ hs.tasks_child_state = HostState(blocks=[0])
+ hs.rescue_child_state = HostState(blocks=[1])
+ hs.always_child_state = HostState(blocks=[2])
+ repr(hs)
+ hs.run_state = 100
+ repr(hs)
+ hs.fail_state = 15
+ repr(hs)
+
+ for i in range(0, 10):
+ hs.cur_block = i
+ self.assertEqual(hs.get_current_block(), i)
+
+ new_hs = hs.copy()
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_play_iterator(self):
+ fake_loader = DictDataLoader({
+ "test_play.yml": """
+ - hosts: all
+ gather_facts: false
+ roles:
+ - test_role
+ pre_tasks:
+ - debug: msg="this is a pre_task"
+ tasks:
+ - debug: msg="this is a regular task"
+ - block:
+ - debug: msg="this is a block task"
+ - block:
+ - debug: msg="this is a sub-block in a block"
+ rescue:
+ - debug: msg="this is a rescue task"
+ - block:
+ - debug: msg="this is a sub-block in a rescue"
+ always:
+ - debug: msg="this is an always task"
+ - block:
+ - debug: msg="this is a sub-block in an always"
+ post_tasks:
+ - debug: msg="this is a post_task"
+ """,
+ '/etc/ansible/roles/test_role/tasks/main.yml': """
+ - name: role task
+ debug: msg="this is a role task"
+ - block:
+ - name: role block task
+ debug: msg="inside block in role"
+ always:
+ - name: role always task
+ debug: msg="always task in block in role"
+ - include: foo.yml
+ - name: role task after include
+ debug: msg="after include in role"
+ - block:
+ - name: starting role nested block 1
+ debug:
+ - block:
+ - name: role nested block 1 task 1
+ debug:
+ - name: role nested block 1 task 2
+ debug:
+ - name: role nested block 1 task 3
+ debug:
+ - name: end of role nested block 1
+ debug:
+ - name: starting role nested block 2
+ debug:
+ - block:
+ - name: role nested block 2 task 1
+ debug:
+ - name: role nested block 2 task 2
+ debug:
+ - name: role nested block 2 task 3
+ debug:
+ - name: end of role nested block 2
+ debug:
+ """,
+ '/etc/ansible/roles/test_role/tasks/foo.yml': """
+ - name: role included task
+ debug: msg="this is task in an include from a role"
+ """
+ })
+
+ mock_var_manager = MagicMock()
+ mock_var_manager._fact_cache = dict()
+ mock_var_manager.get_vars.return_value = dict()
+
+ p = Playbook.load('test_play.yml', loader=fake_loader, variable_manager=mock_var_manager)
+
+ hosts = []
+ for i in range(0, 10):
+ host = MagicMock()
+ host.name = host.get_name.return_value = 'host%02d' % i
+ hosts.append(host)
+
+ mock_var_manager._fact_cache['host00'] = dict()
+
+ inventory = MagicMock()
+ inventory.get_hosts.return_value = hosts
+ inventory.filter_hosts.return_value = hosts
+
+ play_context = PlayContext(play=p._entries[0])
+
+ itr = PlayIterator(
+ inventory=inventory,
+ play=p._entries[0],
+ play_context=play_context,
+ variable_manager=mock_var_manager,
+ all_vars=dict(),
+ )
+
+ # pre task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ # implicit meta: flush_handlers
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'meta')
+ # role task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ self.assertEqual(task.name, "role task")
+ self.assertIsNotNone(task._role)
+ # role block task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "role block task")
+ self.assertIsNotNone(task._role)
+ # role block always task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "role always task")
+ self.assertIsNotNone(task._role)
+ # role include task
+ # (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ # self.assertIsNotNone(task)
+ # self.assertEqual(task.action, 'debug')
+ # self.assertEqual(task.name, "role included task")
+ # self.assertIsNotNone(task._role)
+ # role task after include
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "role task after include")
+ self.assertIsNotNone(task._role)
+ # role nested block tasks
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "starting role nested block 1")
+ self.assertIsNotNone(task._role)
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "role nested block 1 task 1")
+ self.assertIsNotNone(task._role)
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "role nested block 1 task 2")
+ self.assertIsNotNone(task._role)
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "role nested block 1 task 3")
+ self.assertIsNotNone(task._role)
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "end of role nested block 1")
+ self.assertIsNotNone(task._role)
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "starting role nested block 2")
+ self.assertIsNotNone(task._role)
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "role nested block 2 task 1")
+ self.assertIsNotNone(task._role)
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "role nested block 2 task 2")
+ self.assertIsNotNone(task._role)
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "role nested block 2 task 3")
+ self.assertIsNotNone(task._role)
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.name, "end of role nested block 2")
+ self.assertIsNotNone(task._role)
+ # implicit meta: role_complete
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'meta')
+ self.assertIsNotNone(task._role)
+ # regular play task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ self.assertIsNone(task._role)
+ # block task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ self.assertEqual(task.args, dict(msg="this is a block task"))
+ # sub-block task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ self.assertEqual(task.args, dict(msg="this is a sub-block in a block"))
+ # mark the host failed
+ itr.mark_host_failed(hosts[0])
+ # block rescue task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ self.assertEqual(task.args, dict(msg="this is a rescue task"))
+ # sub-block rescue task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ self.assertEqual(task.args, dict(msg="this is a sub-block in a rescue"))
+ # block always task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ self.assertEqual(task.args, dict(msg="this is an always task"))
+ # sub-block always task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ self.assertEqual(task.args, dict(msg="this is a sub-block in an always"))
+ # implicit meta: flush_handlers
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'meta')
+ # post task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ # implicit meta: flush_handlers
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'meta')
+ # end of iteration
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNone(task)
+
+ # host 0 shouldn't be in the failed hosts, as the error
+ # was handled by a rescue block
+ failed_hosts = itr.get_failed_hosts()
+ self.assertNotIn(hosts[0], failed_hosts)
+
+ def test_play_iterator_nested_blocks(self):
+ fake_loader = DictDataLoader({
+ "test_play.yml": """
+ - hosts: all
+ gather_facts: false
+ tasks:
+ - block:
+ - block:
+ - block:
+ - block:
+ - block:
+ - debug: msg="this is the first task"
+ - ping:
+ rescue:
+ - block:
+ - block:
+ - block:
+ - block:
+ - debug: msg="this is the rescue task"
+ always:
+ - block:
+ - block:
+ - block:
+ - block:
+ - debug: msg="this is the always task"
+ """,
+ })
+
+ mock_var_manager = MagicMock()
+ mock_var_manager._fact_cache = dict()
+ mock_var_manager.get_vars.return_value = dict()
+
+ p = Playbook.load('test_play.yml', loader=fake_loader, variable_manager=mock_var_manager)
+
+ hosts = []
+ for i in range(0, 10):
+ host = MagicMock()
+ host.name = host.get_name.return_value = 'host%02d' % i
+ hosts.append(host)
+
+ inventory = MagicMock()
+ inventory.get_hosts.return_value = hosts
+ inventory.filter_hosts.return_value = hosts
+
+ play_context = PlayContext(play=p._entries[0])
+
+ itr = PlayIterator(
+ inventory=inventory,
+ play=p._entries[0],
+ play_context=play_context,
+ variable_manager=mock_var_manager,
+ all_vars=dict(),
+ )
+
+ # implicit meta: flush_handlers
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'meta')
+ self.assertEqual(task.args, dict(_raw_params='flush_handlers'))
+ # get the first task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ self.assertEqual(task.args, dict(msg='this is the first task'))
+ # fail the host
+ itr.mark_host_failed(hosts[0])
+ # get the resuce task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ self.assertEqual(task.args, dict(msg='this is the rescue task'))
+ # get the always task
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'debug')
+ self.assertEqual(task.args, dict(msg='this is the always task'))
+ # implicit meta: flush_handlers
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'meta')
+ self.assertEqual(task.args, dict(_raw_params='flush_handlers'))
+ # implicit meta: flush_handlers
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNotNone(task)
+ self.assertEqual(task.action, 'meta')
+ self.assertEqual(task.args, dict(_raw_params='flush_handlers'))
+ # end of iteration
+ (host_state, task) = itr.get_next_task_for_host(hosts[0])
+ self.assertIsNone(task)
+
+ def test_play_iterator_add_tasks(self):
+ fake_loader = DictDataLoader({
+ 'test_play.yml': """
+ - hosts: all
+ gather_facts: no
+ tasks:
+ - debug: msg="dummy task"
+ """,
+ })
+
+ mock_var_manager = MagicMock()
+ mock_var_manager._fact_cache = dict()
+ mock_var_manager.get_vars.return_value = dict()
+
+ p = Playbook.load('test_play.yml', loader=fake_loader, variable_manager=mock_var_manager)
+
+ hosts = []
+ for i in range(0, 10):
+ host = MagicMock()
+ host.name = host.get_name.return_value = 'host%02d' % i
+ hosts.append(host)
+
+ inventory = MagicMock()
+ inventory.get_hosts.return_value = hosts
+ inventory.filter_hosts.return_value = hosts
+
+ play_context = PlayContext(play=p._entries[0])
+
+ itr = PlayIterator(
+ inventory=inventory,
+ play=p._entries[0],
+ play_context=play_context,
+ variable_manager=mock_var_manager,
+ all_vars=dict(),
+ )
+
+ # test the high-level add_tasks() method
+ s = HostState(blocks=[0, 1, 2])
+ itr._insert_tasks_into_state = MagicMock(return_value=s)
+ itr.add_tasks(hosts[0], [MagicMock(), MagicMock(), MagicMock()])
+ self.assertEqual(itr._host_states[hosts[0].name], s)
+
+ # now actually test the lower-level method that does the work
+ itr = PlayIterator(
+ inventory=inventory,
+ play=p._entries[0],
+ play_context=play_context,
+ variable_manager=mock_var_manager,
+ all_vars=dict(),
+ )
+
+ # iterate past first task
+ _, task = itr.get_next_task_for_host(hosts[0])
+ while (task and task.action != 'debug'):
+ _, task = itr.get_next_task_for_host(hosts[0])
+
+ if task is None:
+ raise Exception("iterated past end of play while looking for place to insert tasks")
+
+ # get the current host state and copy it so we can mutate it
+ s = itr.get_host_state(hosts[0])
+ s_copy = s.copy()
+
+ # assert with an empty task list, or if we're in a failed state, we simply return the state as-is
+ res_state = itr._insert_tasks_into_state(s_copy, task_list=[])
+ self.assertEqual(res_state, s_copy)
+
+ s_copy.fail_state = FailedStates.TASKS
+ res_state = itr._insert_tasks_into_state(s_copy, task_list=[MagicMock()])
+ self.assertEqual(res_state, s_copy)
+
+ # but if we've failed with a rescue/always block
+ mock_task = MagicMock()
+ s_copy.run_state = IteratingStates.RESCUE
+ res_state = itr._insert_tasks_into_state(s_copy, task_list=[mock_task])
+ self.assertEqual(res_state, s_copy)
+ self.assertIn(mock_task, res_state._blocks[res_state.cur_block].rescue)
+ itr.set_state_for_host(hosts[0].name, res_state)
+ (next_state, next_task) = itr.get_next_task_for_host(hosts[0], peek=True)
+ self.assertEqual(next_task, mock_task)
+ itr.set_state_for_host(hosts[0].name, s)
+
+ # test a regular insertion
+ s_copy = s.copy()
+ res_state = itr._insert_tasks_into_state(s_copy, task_list=[MagicMock()])
diff --git a/test/units/executor/test_playbook_executor.py b/test/units/executor/test_playbook_executor.py
new file mode 100644
index 0000000..6032dbb
--- /dev/null
+++ b/test/units/executor/test_playbook_executor.py
@@ -0,0 +1,148 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from unittest.mock import MagicMock
+
+from ansible.executor.playbook_executor import PlaybookExecutor
+from ansible.playbook import Playbook
+from ansible.template import Templar
+from ansible.utils import context_objects as co
+
+from units.mock.loader import DictDataLoader
+
+
+class TestPlaybookExecutor(unittest.TestCase):
+
+ def setUp(self):
+ # Reset command line args for every test
+ co.GlobalCLIArgs._Singleton__instance = None
+
+ def tearDown(self):
+ # And cleanup after ourselves too
+ co.GlobalCLIArgs._Singleton__instance = None
+
+ def test_get_serialized_batches(self):
+ fake_loader = DictDataLoader({
+ 'no_serial.yml': '''
+ - hosts: all
+ gather_facts: no
+ tasks:
+ - debug: var=inventory_hostname
+ ''',
+ 'serial_int.yml': '''
+ - hosts: all
+ gather_facts: no
+ serial: 2
+ tasks:
+ - debug: var=inventory_hostname
+ ''',
+ 'serial_pct.yml': '''
+ - hosts: all
+ gather_facts: no
+ serial: 20%
+ tasks:
+ - debug: var=inventory_hostname
+ ''',
+ 'serial_list.yml': '''
+ - hosts: all
+ gather_facts: no
+ serial: [1, 2, 3]
+ tasks:
+ - debug: var=inventory_hostname
+ ''',
+ 'serial_list_mixed.yml': '''
+ - hosts: all
+ gather_facts: no
+ serial: [1, "20%", -1]
+ tasks:
+ - debug: var=inventory_hostname
+ ''',
+ })
+
+ mock_inventory = MagicMock()
+ mock_var_manager = MagicMock()
+
+ templar = Templar(loader=fake_loader)
+
+ pbe = PlaybookExecutor(
+ playbooks=['no_serial.yml', 'serial_int.yml', 'serial_pct.yml', 'serial_list.yml', 'serial_list_mixed.yml'],
+ inventory=mock_inventory,
+ variable_manager=mock_var_manager,
+ loader=fake_loader,
+ passwords=[],
+ )
+
+ playbook = Playbook.load(pbe._playbooks[0], variable_manager=mock_var_manager, loader=fake_loader)
+ play = playbook.get_plays()[0]
+ play.post_validate(templar)
+ mock_inventory.get_hosts.return_value = ['host0', 'host1', 'host2', 'host3', 'host4', 'host5', 'host6', 'host7', 'host8', 'host9']
+ self.assertEqual(pbe._get_serialized_batches(play), [['host0', 'host1', 'host2', 'host3', 'host4', 'host5', 'host6', 'host7', 'host8', 'host9']])
+
+ playbook = Playbook.load(pbe._playbooks[1], variable_manager=mock_var_manager, loader=fake_loader)
+ play = playbook.get_plays()[0]
+ play.post_validate(templar)
+ mock_inventory.get_hosts.return_value = ['host0', 'host1', 'host2', 'host3', 'host4', 'host5', 'host6', 'host7', 'host8', 'host9']
+ self.assertEqual(
+ pbe._get_serialized_batches(play),
+ [['host0', 'host1'], ['host2', 'host3'], ['host4', 'host5'], ['host6', 'host7'], ['host8', 'host9']]
+ )
+
+ playbook = Playbook.load(pbe._playbooks[2], variable_manager=mock_var_manager, loader=fake_loader)
+ play = playbook.get_plays()[0]
+ play.post_validate(templar)
+ mock_inventory.get_hosts.return_value = ['host0', 'host1', 'host2', 'host3', 'host4', 'host5', 'host6', 'host7', 'host8', 'host9']
+ self.assertEqual(
+ pbe._get_serialized_batches(play),
+ [['host0', 'host1'], ['host2', 'host3'], ['host4', 'host5'], ['host6', 'host7'], ['host8', 'host9']]
+ )
+
+ playbook = Playbook.load(pbe._playbooks[3], variable_manager=mock_var_manager, loader=fake_loader)
+ play = playbook.get_plays()[0]
+ play.post_validate(templar)
+ mock_inventory.get_hosts.return_value = ['host0', 'host1', 'host2', 'host3', 'host4', 'host5', 'host6', 'host7', 'host8', 'host9']
+ self.assertEqual(
+ pbe._get_serialized_batches(play),
+ [['host0'], ['host1', 'host2'], ['host3', 'host4', 'host5'], ['host6', 'host7', 'host8'], ['host9']]
+ )
+
+ playbook = Playbook.load(pbe._playbooks[4], variable_manager=mock_var_manager, loader=fake_loader)
+ play = playbook.get_plays()[0]
+ play.post_validate(templar)
+ mock_inventory.get_hosts.return_value = ['host0', 'host1', 'host2', 'host3', 'host4', 'host5', 'host6', 'host7', 'host8', 'host9']
+ self.assertEqual(pbe._get_serialized_batches(play), [['host0'], ['host1', 'host2'], ['host3', 'host4', 'host5', 'host6', 'host7', 'host8', 'host9']])
+
+ # Test when serial percent is under 1.0
+ playbook = Playbook.load(pbe._playbooks[2], variable_manager=mock_var_manager, loader=fake_loader)
+ play = playbook.get_plays()[0]
+ play.post_validate(templar)
+ mock_inventory.get_hosts.return_value = ['host0', 'host1', 'host2']
+ self.assertEqual(pbe._get_serialized_batches(play), [['host0'], ['host1'], ['host2']])
+
+ # Test when there is a remainder for serial as a percent
+ playbook = Playbook.load(pbe._playbooks[2], variable_manager=mock_var_manager, loader=fake_loader)
+ play = playbook.get_plays()[0]
+ play.post_validate(templar)
+ mock_inventory.get_hosts.return_value = ['host0', 'host1', 'host2', 'host3', 'host4', 'host5', 'host6', 'host7', 'host8', 'host9', 'host10']
+ self.assertEqual(
+ pbe._get_serialized_batches(play),
+ [['host0', 'host1'], ['host2', 'host3'], ['host4', 'host5'], ['host6', 'host7'], ['host8', 'host9'], ['host10']]
+ )
diff --git a/test/units/executor/test_task_executor.py b/test/units/executor/test_task_executor.py
new file mode 100644
index 0000000..315d26a
--- /dev/null
+++ b/test/units/executor/test_task_executor.py
@@ -0,0 +1,489 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from unittest import mock
+
+from units.compat import unittest
+from unittest.mock import patch, MagicMock
+from ansible.errors import AnsibleError
+from ansible.executor.task_executor import TaskExecutor, remove_omit
+from ansible.plugins.loader import action_loader, lookup_loader, module_loader
+from ansible.parsing.yaml.objects import AnsibleUnicode
+from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes
+from ansible.module_utils.six import text_type
+
+from collections import namedtuple
+from units.mock.loader import DictDataLoader
+
+
+get_with_context_result = namedtuple('get_with_context_result', ['object', 'plugin_load_context'])
+
+
+class TestTaskExecutor(unittest.TestCase):
+
+ def test_task_executor_init(self):
+ fake_loader = DictDataLoader({})
+ mock_host = MagicMock()
+ mock_task = MagicMock()
+ mock_play_context = MagicMock()
+ mock_shared_loader = MagicMock()
+ new_stdin = None
+ job_vars = dict()
+ mock_queue = MagicMock()
+ te = TaskExecutor(
+ host=mock_host,
+ task=mock_task,
+ job_vars=job_vars,
+ play_context=mock_play_context,
+ new_stdin=new_stdin,
+ loader=fake_loader,
+ shared_loader_obj=mock_shared_loader,
+ final_q=mock_queue,
+ )
+
+ def test_task_executor_run(self):
+ fake_loader = DictDataLoader({})
+
+ mock_host = MagicMock()
+
+ mock_task = MagicMock()
+ mock_task._role._role_path = '/path/to/role/foo'
+
+ mock_play_context = MagicMock()
+
+ mock_shared_loader = MagicMock()
+ mock_queue = MagicMock()
+
+ new_stdin = None
+ job_vars = dict()
+
+ te = TaskExecutor(
+ host=mock_host,
+ task=mock_task,
+ job_vars=job_vars,
+ play_context=mock_play_context,
+ new_stdin=new_stdin,
+ loader=fake_loader,
+ shared_loader_obj=mock_shared_loader,
+ final_q=mock_queue,
+ )
+
+ te._get_loop_items = MagicMock(return_value=None)
+ te._execute = MagicMock(return_value=dict())
+ res = te.run()
+
+ te._get_loop_items = MagicMock(return_value=[])
+ res = te.run()
+
+ te._get_loop_items = MagicMock(return_value=['a', 'b', 'c'])
+ te._run_loop = MagicMock(return_value=[dict(item='a', changed=True), dict(item='b', failed=True), dict(item='c')])
+ res = te.run()
+
+ te._get_loop_items = MagicMock(side_effect=AnsibleError(""))
+ res = te.run()
+ self.assertIn("failed", res)
+
+ def test_task_executor_run_clean_res(self):
+ te = TaskExecutor(None, MagicMock(), None, None, None, None, None, None)
+ te._get_loop_items = MagicMock(return_value=[1])
+ te._run_loop = MagicMock(
+ return_value=[
+ {
+ 'unsafe_bytes': AnsibleUnsafeBytes(b'{{ $bar }}'),
+ 'unsafe_text': AnsibleUnsafeText(u'{{ $bar }}'),
+ 'bytes': b'bytes',
+ 'text': u'text',
+ 'int': 1,
+ }
+ ]
+ )
+ res = te.run()
+ data = res['results'][0]
+ self.assertIsInstance(data['unsafe_bytes'], AnsibleUnsafeText)
+ self.assertIsInstance(data['unsafe_text'], AnsibleUnsafeText)
+ self.assertIsInstance(data['bytes'], text_type)
+ self.assertIsInstance(data['text'], text_type)
+ self.assertIsInstance(data['int'], int)
+
+ def test_task_executor_get_loop_items(self):
+ fake_loader = DictDataLoader({})
+
+ mock_host = MagicMock()
+
+ mock_task = MagicMock()
+ mock_task.loop_with = 'items'
+ mock_task.loop = ['a', 'b', 'c']
+
+ mock_play_context = MagicMock()
+
+ mock_shared_loader = MagicMock()
+ mock_shared_loader.lookup_loader = lookup_loader
+
+ new_stdin = None
+ job_vars = dict()
+ mock_queue = MagicMock()
+
+ te = TaskExecutor(
+ host=mock_host,
+ task=mock_task,
+ job_vars=job_vars,
+ play_context=mock_play_context,
+ new_stdin=new_stdin,
+ loader=fake_loader,
+ shared_loader_obj=mock_shared_loader,
+ final_q=mock_queue,
+ )
+
+ items = te._get_loop_items()
+ self.assertEqual(items, ['a', 'b', 'c'])
+
+ def test_task_executor_run_loop(self):
+ items = ['a', 'b', 'c']
+
+ fake_loader = DictDataLoader({})
+
+ mock_host = MagicMock()
+
+ def _copy(exclude_parent=False, exclude_tasks=False):
+ new_item = MagicMock()
+ return new_item
+
+ mock_task = MagicMock()
+ mock_task.copy.side_effect = _copy
+
+ mock_play_context = MagicMock()
+
+ mock_shared_loader = MagicMock()
+ mock_queue = MagicMock()
+
+ new_stdin = None
+ job_vars = dict()
+
+ te = TaskExecutor(
+ host=mock_host,
+ task=mock_task,
+ job_vars=job_vars,
+ play_context=mock_play_context,
+ new_stdin=new_stdin,
+ loader=fake_loader,
+ shared_loader_obj=mock_shared_loader,
+ final_q=mock_queue,
+ )
+
+ def _execute(variables):
+ return dict(item=variables.get('item'))
+
+ te._execute = MagicMock(side_effect=_execute)
+
+ res = te._run_loop(items)
+ self.assertEqual(len(res), 3)
+
+ def test_task_executor_get_action_handler(self):
+ te = TaskExecutor(
+ host=MagicMock(),
+ task=MagicMock(),
+ job_vars={},
+ play_context=MagicMock(),
+ new_stdin=None,
+ loader=DictDataLoader({}),
+ shared_loader_obj=MagicMock(),
+ final_q=MagicMock(),
+ )
+
+ context = MagicMock(resolved=False)
+ te._shared_loader_obj.module_loader.find_plugin_with_context.return_value = context
+ action_loader = te._shared_loader_obj.action_loader
+ action_loader.has_plugin.return_value = True
+ action_loader.get.return_value = mock.sentinel.handler
+
+ mock_connection = MagicMock()
+ mock_templar = MagicMock()
+ action = 'namespace.prefix_suffix'
+ te._task.action = action
+
+ handler = te._get_action_handler(mock_connection, mock_templar)
+
+ self.assertIs(mock.sentinel.handler, handler)
+
+ action_loader.has_plugin.assert_called_once_with(
+ action, collection_list=te._task.collections)
+
+ action_loader.get.assert_called_once_with(
+ te._task.action, task=te._task, connection=mock_connection,
+ play_context=te._play_context, loader=te._loader,
+ templar=mock_templar, shared_loader_obj=te._shared_loader_obj,
+ collection_list=te._task.collections)
+
+ def test_task_executor_get_handler_prefix(self):
+ te = TaskExecutor(
+ host=MagicMock(),
+ task=MagicMock(),
+ job_vars={},
+ play_context=MagicMock(),
+ new_stdin=None,
+ loader=DictDataLoader({}),
+ shared_loader_obj=MagicMock(),
+ final_q=MagicMock(),
+ )
+
+ context = MagicMock(resolved=False)
+ te._shared_loader_obj.module_loader.find_plugin_with_context.return_value = context
+ action_loader = te._shared_loader_obj.action_loader
+ action_loader.has_plugin.side_effect = [False, True]
+ action_loader.get.return_value = mock.sentinel.handler
+ action_loader.__contains__.return_value = True
+
+ mock_connection = MagicMock()
+ mock_templar = MagicMock()
+ action = 'namespace.netconf_suffix'
+ module_prefix = action.split('_', 1)[0]
+ te._task.action = action
+
+ handler = te._get_action_handler(mock_connection, mock_templar)
+
+ self.assertIs(mock.sentinel.handler, handler)
+ action_loader.has_plugin.assert_has_calls([mock.call(action, collection_list=te._task.collections), # called twice
+ mock.call(module_prefix, collection_list=te._task.collections)])
+
+ action_loader.get.assert_called_once_with(
+ module_prefix, task=te._task, connection=mock_connection,
+ play_context=te._play_context, loader=te._loader,
+ templar=mock_templar, shared_loader_obj=te._shared_loader_obj,
+ collection_list=te._task.collections)
+
+ def test_task_executor_get_handler_normal(self):
+ te = TaskExecutor(
+ host=MagicMock(),
+ task=MagicMock(),
+ job_vars={},
+ play_context=MagicMock(),
+ new_stdin=None,
+ loader=DictDataLoader({}),
+ shared_loader_obj=MagicMock(),
+ final_q=MagicMock(),
+ )
+
+ action_loader = te._shared_loader_obj.action_loader
+ action_loader.has_plugin.return_value = False
+ action_loader.get.return_value = mock.sentinel.handler
+ action_loader.__contains__.return_value = False
+ module_loader = te._shared_loader_obj.module_loader
+ context = MagicMock(resolved=False)
+ module_loader.find_plugin_with_context.return_value = context
+
+ mock_connection = MagicMock()
+ mock_templar = MagicMock()
+ action = 'namespace.prefix_suffix'
+ module_prefix = action.split('_', 1)[0]
+ te._task.action = action
+ handler = te._get_action_handler(mock_connection, mock_templar)
+
+ self.assertIs(mock.sentinel.handler, handler)
+
+ action_loader.has_plugin.assert_has_calls([mock.call(action, collection_list=te._task.collections),
+ mock.call(module_prefix, collection_list=te._task.collections)])
+
+ action_loader.get.assert_called_once_with(
+ 'ansible.legacy.normal', task=te._task, connection=mock_connection,
+ play_context=te._play_context, loader=te._loader,
+ templar=mock_templar, shared_loader_obj=te._shared_loader_obj,
+ collection_list=None)
+
+ def test_task_executor_execute(self):
+ fake_loader = DictDataLoader({})
+
+ mock_host = MagicMock()
+
+ mock_task = MagicMock()
+ mock_task.action = 'mock.action'
+ mock_task.args = dict()
+ mock_task.become = False
+ mock_task.retries = 0
+ mock_task.delay = -1
+ mock_task.register = 'foo'
+ mock_task.until = None
+ mock_task.changed_when = None
+ mock_task.failed_when = None
+ mock_task.post_validate.return_value = None
+ # mock_task.async_val cannot be left unset, because on Python 3 MagicMock()
+ # > 0 raises a TypeError There are two reasons for using the value 1
+ # here: on Python 2 comparing MagicMock() > 0 returns True, and the
+ # other reason is that if I specify 0 here, the test fails. ;)
+ mock_task.async_val = 1
+ mock_task.poll = 0
+
+ mock_play_context = MagicMock()
+ mock_play_context.post_validate.return_value = None
+ mock_play_context.update_vars.return_value = None
+
+ mock_connection = MagicMock()
+ mock_connection.force_persistence = False
+ mock_connection.supports_persistence = False
+ mock_connection.set_host_overrides.return_value = None
+ mock_connection._connect.return_value = None
+
+ mock_action = MagicMock()
+ mock_queue = MagicMock()
+
+ shared_loader = MagicMock()
+ new_stdin = None
+ job_vars = dict(omit="XXXXXXXXXXXXXXXXXXX")
+
+ te = TaskExecutor(
+ host=mock_host,
+ task=mock_task,
+ job_vars=job_vars,
+ play_context=mock_play_context,
+ new_stdin=new_stdin,
+ loader=fake_loader,
+ shared_loader_obj=shared_loader,
+ final_q=mock_queue,
+ )
+
+ te._get_connection = MagicMock(return_value=mock_connection)
+ context = MagicMock()
+ te._get_action_handler_with_context = MagicMock(return_value=get_with_context_result(mock_action, context))
+
+ mock_action.run.return_value = dict(ansible_facts=dict())
+ res = te._execute()
+
+ mock_task.changed_when = MagicMock(return_value=AnsibleUnicode("1 == 1"))
+ res = te._execute()
+
+ mock_task.changed_when = None
+ mock_task.failed_when = MagicMock(return_value=AnsibleUnicode("1 == 1"))
+ res = te._execute()
+
+ mock_task.failed_when = None
+ mock_task.evaluate_conditional.return_value = False
+ res = te._execute()
+
+ mock_task.evaluate_conditional.return_value = True
+ mock_task.args = dict(_raw_params='foo.yml', a='foo', b='bar')
+ mock_task.action = 'include'
+ res = te._execute()
+
+ def test_task_executor_poll_async_result(self):
+ fake_loader = DictDataLoader({})
+
+ mock_host = MagicMock()
+
+ mock_task = MagicMock()
+ mock_task.async_val = 0.1
+ mock_task.poll = 0.05
+
+ mock_play_context = MagicMock()
+
+ mock_connection = MagicMock()
+
+ mock_action = MagicMock()
+ mock_queue = MagicMock()
+
+ shared_loader = MagicMock()
+ shared_loader.action_loader = action_loader
+
+ new_stdin = None
+ job_vars = dict(omit="XXXXXXXXXXXXXXXXXXX")
+
+ te = TaskExecutor(
+ host=mock_host,
+ task=mock_task,
+ job_vars=job_vars,
+ play_context=mock_play_context,
+ new_stdin=new_stdin,
+ loader=fake_loader,
+ shared_loader_obj=shared_loader,
+ final_q=mock_queue,
+ )
+
+ te._connection = MagicMock()
+
+ def _get(*args, **kwargs):
+ mock_action = MagicMock()
+ mock_action.run.return_value = dict(stdout='')
+ return mock_action
+
+ # testing with some bad values in the result passed to poll async,
+ # and with a bad value returned from the mock action
+ with patch.object(action_loader, 'get', _get):
+ mock_templar = MagicMock()
+ res = te._poll_async_result(result=dict(), templar=mock_templar)
+ self.assertIn('failed', res)
+ res = te._poll_async_result(result=dict(ansible_job_id=1), templar=mock_templar)
+ self.assertIn('failed', res)
+
+ def _get(*args, **kwargs):
+ mock_action = MagicMock()
+ mock_action.run.return_value = dict(finished=1)
+ return mock_action
+
+ # now testing with good values
+ with patch.object(action_loader, 'get', _get):
+ mock_templar = MagicMock()
+ res = te._poll_async_result(result=dict(ansible_job_id=1), templar=mock_templar)
+ self.assertEqual(res, dict(finished=1))
+
+ def test_recursive_remove_omit(self):
+ omit_token = 'POPCORN'
+
+ data = {
+ 'foo': 'bar',
+ 'baz': 1,
+ 'qux': ['one', 'two', 'three'],
+ 'subdict': {
+ 'remove': 'POPCORN',
+ 'keep': 'not_popcorn',
+ 'subsubdict': {
+ 'remove': 'POPCORN',
+ 'keep': 'not_popcorn',
+ },
+ 'a_list': ['POPCORN'],
+ },
+ 'a_list': ['POPCORN'],
+ 'list_of_lists': [
+ ['some', 'thing'],
+ ],
+ 'list_of_dicts': [
+ {
+ 'remove': 'POPCORN',
+ }
+ ],
+ }
+
+ expected = {
+ 'foo': 'bar',
+ 'baz': 1,
+ 'qux': ['one', 'two', 'three'],
+ 'subdict': {
+ 'keep': 'not_popcorn',
+ 'subsubdict': {
+ 'keep': 'not_popcorn',
+ },
+ 'a_list': ['POPCORN'],
+ },
+ 'a_list': ['POPCORN'],
+ 'list_of_lists': [
+ ['some', 'thing'],
+ ],
+ 'list_of_dicts': [{}],
+ }
+
+ self.assertEqual(remove_omit(data, omit_token), expected)
diff --git a/test/units/executor/test_task_queue_manager_callbacks.py b/test/units/executor/test_task_queue_manager_callbacks.py
new file mode 100644
index 0000000..c63385d
--- /dev/null
+++ b/test/units/executor/test_task_queue_manager_callbacks.py
@@ -0,0 +1,121 @@
+# (c) 2016, Steve Kuznetsov <skuznets@redhat.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+
+from units.compat import unittest
+from unittest.mock import MagicMock
+
+from ansible.executor.task_queue_manager import TaskQueueManager
+from ansible.playbook import Playbook
+from ansible.plugins.callback import CallbackBase
+from ansible.utils import context_objects as co
+
+__metaclass__ = type
+
+
+class TestTaskQueueManagerCallbacks(unittest.TestCase):
+ def setUp(self):
+ inventory = MagicMock()
+ variable_manager = MagicMock()
+ loader = MagicMock()
+ passwords = []
+
+ # Reset the stored command line args
+ co.GlobalCLIArgs._Singleton__instance = None
+ self._tqm = TaskQueueManager(inventory, variable_manager, loader, passwords)
+ self._playbook = Playbook(loader)
+
+ # we use a MagicMock to register the result of the call we
+ # expect to `v2_playbook_on_call`. We don't mock out the
+ # method since we're testing code that uses `inspect` to
+ # look at that method's argspec and we want to ensure this
+ # test is easy to reason about.
+ self._register = MagicMock()
+
+ def tearDown(self):
+ # Reset the stored command line args
+ co.GlobalCLIArgs._Singleton__instance = None
+
+ def test_task_queue_manager_callbacks_v2_playbook_on_start(self):
+ """
+ Assert that no exceptions are raised when sending a Playbook
+ start callback to a current callback module plugin.
+ """
+ register = self._register
+
+ class CallbackModule(CallbackBase):
+ """
+ This is a callback module with the current
+ method signature for `v2_playbook_on_start`.
+ """
+ CALLBACK_VERSION = 2.0
+ CALLBACK_TYPE = 'notification'
+ CALLBACK_NAME = 'current_module'
+
+ def v2_playbook_on_start(self, playbook):
+ register(self, playbook)
+
+ callback_module = CallbackModule()
+ self._tqm._callback_plugins.append(callback_module)
+ self._tqm.send_callback('v2_playbook_on_start', self._playbook)
+ register.assert_called_once_with(callback_module, self._playbook)
+
+ def test_task_queue_manager_callbacks_v2_playbook_on_start_wrapped(self):
+ """
+ Assert that no exceptions are raised when sending a Playbook
+ start callback to a wrapped current callback module plugin.
+ """
+ register = self._register
+
+ def wrap_callback(func):
+ """
+ This wrapper changes the exposed argument
+ names for a method from the original names
+ to (*args, **kwargs). This is used in order
+ to validate that wrappers which change par-
+ ameter names do not break the TQM callback
+ system.
+
+ :param func: function to decorate
+ :return: decorated function
+ """
+
+ def wrapper(*args, **kwargs):
+ return func(*args, **kwargs)
+
+ return wrapper
+
+ class WrappedCallbackModule(CallbackBase):
+ """
+ This is a callback module with the current
+ method signature for `v2_playbook_on_start`
+ wrapped in order to change the signature.
+ """
+ CALLBACK_VERSION = 2.0
+ CALLBACK_TYPE = 'notification'
+ CALLBACK_NAME = 'current_module'
+
+ @wrap_callback
+ def v2_playbook_on_start(self, playbook):
+ register(self, playbook)
+
+ callback_module = WrappedCallbackModule()
+ self._tqm._callback_plugins.append(callback_module)
+ self._tqm.send_callback('v2_playbook_on_start', self._playbook)
+ register.assert_called_once_with(callback_module, self._playbook)
diff --git a/test/units/executor/test_task_result.py b/test/units/executor/test_task_result.py
new file mode 100644
index 0000000..8b79571
--- /dev/null
+++ b/test/units/executor/test_task_result.py
@@ -0,0 +1,171 @@
+# (c) 2016, James Cammarata <jimi@sngx.net>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from unittest.mock import patch, MagicMock
+
+from ansible.executor.task_result import TaskResult
+
+
+class TestTaskResult(unittest.TestCase):
+ def test_task_result_basic(self):
+ mock_host = MagicMock()
+ mock_task = MagicMock()
+
+ # test loading a result with a dict
+ tr = TaskResult(mock_host, mock_task, dict())
+
+ # test loading a result with a JSON string
+ with patch('ansible.parsing.dataloader.DataLoader.load') as p:
+ tr = TaskResult(mock_host, mock_task, '{}')
+
+ def test_task_result_is_changed(self):
+ mock_host = MagicMock()
+ mock_task = MagicMock()
+
+ # test with no changed in result
+ tr = TaskResult(mock_host, mock_task, dict())
+ self.assertFalse(tr.is_changed())
+
+ # test with changed in the result
+ tr = TaskResult(mock_host, mock_task, dict(changed=True))
+ self.assertTrue(tr.is_changed())
+
+ # test with multiple results but none changed
+ mock_task.loop = 'foo'
+ tr = TaskResult(mock_host, mock_task, dict(results=[dict(foo='bar'), dict(bam='baz'), True]))
+ self.assertFalse(tr.is_changed())
+
+ # test with multiple results and one changed
+ mock_task.loop = 'foo'
+ tr = TaskResult(mock_host, mock_task, dict(results=[dict(changed=False), dict(changed=True), dict(some_key=False)]))
+ self.assertTrue(tr.is_changed())
+
+ def test_task_result_is_skipped(self):
+ mock_host = MagicMock()
+ mock_task = MagicMock()
+
+ # test with no skipped in result
+ tr = TaskResult(mock_host, mock_task, dict())
+ self.assertFalse(tr.is_skipped())
+
+ # test with skipped in the result
+ tr = TaskResult(mock_host, mock_task, dict(skipped=True))
+ self.assertTrue(tr.is_skipped())
+
+ # test with multiple results but none skipped
+ mock_task.loop = 'foo'
+ tr = TaskResult(mock_host, mock_task, dict(results=[dict(foo='bar'), dict(bam='baz'), True]))
+ self.assertFalse(tr.is_skipped())
+
+ # test with multiple results and one skipped
+ mock_task.loop = 'foo'
+ tr = TaskResult(mock_host, mock_task, dict(results=[dict(skipped=False), dict(skipped=True), dict(some_key=False)]))
+ self.assertFalse(tr.is_skipped())
+
+ # test with multiple results and all skipped
+ mock_task.loop = 'foo'
+ tr = TaskResult(mock_host, mock_task, dict(results=[dict(skipped=True), dict(skipped=True), dict(skipped=True)]))
+ self.assertTrue(tr.is_skipped())
+
+ # test with multiple squashed results (list of strings)
+ # first with the main result having skipped=False
+ mock_task.loop = 'foo'
+ tr = TaskResult(mock_host, mock_task, dict(results=["a", "b", "c"], skipped=False))
+ self.assertFalse(tr.is_skipped())
+ # then with the main result having skipped=True
+ tr = TaskResult(mock_host, mock_task, dict(results=["a", "b", "c"], skipped=True))
+ self.assertTrue(tr.is_skipped())
+
+ def test_task_result_is_unreachable(self):
+ mock_host = MagicMock()
+ mock_task = MagicMock()
+
+ # test with no unreachable in result
+ tr = TaskResult(mock_host, mock_task, dict())
+ self.assertFalse(tr.is_unreachable())
+
+ # test with unreachable in the result
+ tr = TaskResult(mock_host, mock_task, dict(unreachable=True))
+ self.assertTrue(tr.is_unreachable())
+
+ # test with multiple results but none unreachable
+ mock_task.loop = 'foo'
+ tr = TaskResult(mock_host, mock_task, dict(results=[dict(foo='bar'), dict(bam='baz'), True]))
+ self.assertFalse(tr.is_unreachable())
+
+ # test with multiple results and one unreachable
+ mock_task.loop = 'foo'
+ tr = TaskResult(mock_host, mock_task, dict(results=[dict(unreachable=False), dict(unreachable=True), dict(some_key=False)]))
+ self.assertTrue(tr.is_unreachable())
+
+ def test_task_result_is_failed(self):
+ mock_host = MagicMock()
+ mock_task = MagicMock()
+
+ # test with no failed in result
+ tr = TaskResult(mock_host, mock_task, dict())
+ self.assertFalse(tr.is_failed())
+
+ # test failed result with rc values (should not matter)
+ tr = TaskResult(mock_host, mock_task, dict(rc=0))
+ self.assertFalse(tr.is_failed())
+ tr = TaskResult(mock_host, mock_task, dict(rc=1))
+ self.assertFalse(tr.is_failed())
+
+ # test with failed in result
+ tr = TaskResult(mock_host, mock_task, dict(failed=True))
+ self.assertTrue(tr.is_failed())
+
+ # test with failed_when in result
+ tr = TaskResult(mock_host, mock_task, dict(failed_when_result=True))
+ self.assertTrue(tr.is_failed())
+
+ def test_task_result_no_log(self):
+ mock_host = MagicMock()
+ mock_task = MagicMock()
+
+ # no_log should remove secrets
+ tr = TaskResult(mock_host, mock_task, dict(_ansible_no_log=True, secret='DONTSHOWME'))
+ clean = tr.clean_copy()
+ self.assertTrue('secret' not in clean._result)
+
+ def test_task_result_no_log_preserve(self):
+ mock_host = MagicMock()
+ mock_task = MagicMock()
+
+ # no_log should not remove presrved keys
+ tr = TaskResult(
+ mock_host,
+ mock_task,
+ dict(
+ _ansible_no_log=True,
+ retries=5,
+ attempts=5,
+ changed=False,
+ foo='bar',
+ )
+ )
+ clean = tr.clean_copy()
+ self.assertTrue('retries' in clean._result)
+ self.assertTrue('attempts' in clean._result)
+ self.assertTrue('changed' in clean._result)
+ self.assertTrue('foo' not in clean._result)
diff --git a/test/units/galaxy/__init__.py b/test/units/galaxy/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/galaxy/__init__.py
diff --git a/test/units/galaxy/test_api.py b/test/units/galaxy/test_api.py
new file mode 100644
index 0000000..064aff2
--- /dev/null
+++ b/test/units/galaxy/test_api.py
@@ -0,0 +1,1362 @@
+# -*- 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)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import os
+import re
+import pytest
+import stat
+import tarfile
+import tempfile
+import time
+
+from io import BytesIO, StringIO
+from unittest.mock import MagicMock
+
+import ansible.constants as C
+from ansible import context
+from ansible.errors import AnsibleError
+from ansible.galaxy import api as galaxy_api
+from ansible.galaxy.api import CollectionVersionMetadata, GalaxyAPI, GalaxyError
+from ansible.galaxy.token import BasicAuthToken, GalaxyToken, KeycloakToken
+from ansible.module_utils._text import to_native, to_text
+from ansible.module_utils.six.moves.urllib import error as urllib_error
+from ansible.utils import context_objects as co
+from ansible.utils.display import Display
+
+
+@pytest.fixture(autouse='function')
+def reset_cli_args():
+ co.GlobalCLIArgs._Singleton__instance = None
+ # Required to initialise the GalaxyAPI object
+ context.CLIARGS._store = {'ignore_certs': False}
+ yield
+ co.GlobalCLIArgs._Singleton__instance = None
+
+
+@pytest.fixture()
+def collection_artifact(tmp_path_factory):
+ ''' Creates a collection artifact tarball that is ready to be published '''
+ output_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Output'))
+
+ tar_path = os.path.join(output_dir, 'namespace-collection-v1.0.0.tar.gz')
+ with tarfile.open(tar_path, 'w:gz') as tfile:
+ b_io = BytesIO(b"\x00\x01\x02\x03")
+ tar_info = tarfile.TarInfo('test')
+ tar_info.size = 4
+ tar_info.mode = 0o0644
+ tfile.addfile(tarinfo=tar_info, fileobj=b_io)
+
+ yield tar_path
+
+
+@pytest.fixture()
+def cache_dir(tmp_path_factory, monkeypatch):
+ cache_dir = to_text(tmp_path_factory.mktemp('Test ÅÑŚÌβŁÈ Galaxy Cache'))
+ monkeypatch.setattr(C, 'GALAXY_CACHE_DIR', cache_dir)
+
+ yield cache_dir
+
+
+def get_test_galaxy_api(url, version, token_ins=None, token_value=None, no_cache=True):
+ token_value = token_value or "my token"
+ token_ins = token_ins or GalaxyToken(token_value)
+ api = GalaxyAPI(None, "test", url, no_cache=no_cache)
+ # Warning, this doesn't test g_connect() because _availabe_api_versions is set here. That means
+ # that urls for v2 servers have to append '/api/' themselves in the input data.
+ api._available_api_versions = {version: '%s' % version}
+ api.token = token_ins
+
+ return api
+
+
+def get_v3_collection_versions(namespace='namespace', name='collection'):
+ pagination_path = f"/api/galaxy/content/community/v3/plugin/{namespace}/content/community/collections/index/{namespace}/{name}/versions"
+ page_versions = (('1.0.0', '1.0.1',), ('1.0.2', '1.0.3',), ('1.0.4', '1.0.5'),)
+ responses = [
+ {}, # TODO: initial response
+ ]
+
+ first = f"{pagination_path}/?limit=100"
+ last = f"{pagination_path}/?limit=100&offset=200"
+ page_versions = [
+ {
+ "versions": ('1.0.0', '1.0.1',),
+ "url": first,
+ },
+ {
+ "versions": ('1.0.2', '1.0.3',),
+ "url": f"{pagination_path}/?limit=100&offset=100",
+ },
+ {
+ "versions": ('1.0.4', '1.0.5'),
+ "url": last,
+ },
+ ]
+
+ previous = None
+ for page in range(0, len(page_versions)):
+ data = []
+
+ if page_versions[page]["url"] == last:
+ next_page = None
+ else:
+ next_page = page_versions[page + 1]["url"]
+ links = {"first": first, "last": last, "next": next_page, "previous": previous}
+
+ for version in page_versions[page]["versions"]:
+ data.append(
+ {
+ "version": f"{version}",
+ "href": f"{pagination_path}/{version}/",
+ "created_at": "2022-05-13T15:55:58.913107Z",
+ "updated_at": "2022-05-13T15:55:58.913121Z",
+ "requires_ansible": ">=2.9.10"
+ }
+ )
+
+ responses.append({"meta": {"count": 6}, "links": links, "data": data})
+
+ previous = page_versions[page]["url"]
+ return responses
+
+
+def get_collection_versions(namespace='namespace', name='collection'):
+ base_url = 'https://galaxy.server.com/api/v2/collections/{0}/{1}/'.format(namespace, name)
+ versions_url = base_url + 'versions/'
+
+ # Response for collection info
+ responses = [
+ {
+ "id": 1000,
+ "href": base_url,
+ "name": name,
+ "namespace": {
+ "id": 30000,
+ "href": "https://galaxy.ansible.com/api/v1/namespaces/30000/",
+ "name": namespace,
+ },
+ "versions_url": versions_url,
+ "latest_version": {
+ "version": "1.0.5",
+ "href": versions_url + "1.0.5/"
+ },
+ "deprecated": False,
+ "created": "2021-02-09T16:55:42.749915-05:00",
+ "modified": "2021-02-09T16:55:42.749915-05:00",
+ }
+ ]
+
+ # Paginated responses for versions
+ page_versions = (('1.0.0', '1.0.1',), ('1.0.2', '1.0.3',), ('1.0.4', '1.0.5'),)
+ last_page = None
+ for page in range(1, len(page_versions) + 1):
+ if page < len(page_versions):
+ next_page = versions_url + '?page={0}'.format(page + 1)
+ else:
+ next_page = None
+
+ version_results = []
+ for version in page_versions[int(page - 1)]:
+ version_results.append(
+ {'version': version, 'href': versions_url + '{0}/'.format(version)}
+ )
+
+ responses.append(
+ {
+ 'count': 6,
+ 'next': next_page,
+ 'previous': last_page,
+ 'results': version_results,
+ }
+ )
+ last_page = page
+
+ return responses
+
+
+def test_api_no_auth():
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/")
+ actual = {}
+ api._add_auth_token(actual, "")
+ assert actual == {}
+
+
+def test_api_no_auth_but_required():
+ expected = "No access token or username set. A token can be set with --api-key or at "
+ with pytest.raises(AnsibleError, match=expected):
+ GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/")._add_auth_token({}, "", required=True)
+
+
+def test_api_token_auth():
+ token = GalaxyToken(token=u"my_token")
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
+ actual = {}
+ api._add_auth_token(actual, "", required=True)
+ assert actual == {'Authorization': 'Token my_token'}
+
+
+def test_api_token_auth_with_token_type(monkeypatch):
+ token = KeycloakToken(auth_url='https://api.test/')
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my_token'
+ monkeypatch.setattr(token, 'get', mock_token_get)
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
+ actual = {}
+ api._add_auth_token(actual, "", token_type="Bearer", required=True)
+ assert actual == {'Authorization': 'Bearer my_token'}
+
+
+def test_api_token_auth_with_v3_url(monkeypatch):
+ token = KeycloakToken(auth_url='https://api.test/')
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my_token'
+ monkeypatch.setattr(token, 'get', mock_token_get)
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
+ actual = {}
+ api._add_auth_token(actual, "https://galaxy.ansible.com/api/v3/resource/name", required=True)
+ assert actual == {'Authorization': 'Bearer my_token'}
+
+
+def test_api_token_auth_with_v2_url():
+ token = GalaxyToken(token=u"my_token")
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
+ actual = {}
+ # Add v3 to random part of URL but response should only see the v2 as the full URI path segment.
+ api._add_auth_token(actual, "https://galaxy.ansible.com/api/v2/resourcev3/name", required=True)
+ assert actual == {'Authorization': 'Token my_token'}
+
+
+def test_api_basic_auth_password():
+ token = BasicAuthToken(username=u"user", password=u"pass")
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
+ actual = {}
+ api._add_auth_token(actual, "", required=True)
+ assert actual == {'Authorization': 'Basic dXNlcjpwYXNz'}
+
+
+def test_api_basic_auth_no_password():
+ token = BasicAuthToken(username=u"user")
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
+ actual = {}
+ api._add_auth_token(actual, "", required=True)
+ assert actual == {'Authorization': 'Basic dXNlcjo='}
+
+
+def test_api_dont_override_auth_header():
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/")
+ actual = {'Authorization': 'Custom token'}
+ api._add_auth_token(actual, "", required=True)
+ assert actual == {'Authorization': 'Custom token'}
+
+
+def test_initialise_galaxy(monkeypatch):
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ StringIO(u'{"available_versions":{"v1":"v1/"}}'),
+ StringIO(u'{"token":"my token"}'),
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/")
+ actual = api.authenticate("github_token")
+
+ assert len(api.available_api_versions) == 2
+ assert api.available_api_versions['v1'] == u'v1/'
+ assert api.available_api_versions['v2'] == u'v2/'
+ assert actual == {u'token': u'my token'}
+ assert mock_open.call_count == 2
+ assert mock_open.mock_calls[0][1][0] == 'https://galaxy.ansible.com/api/'
+ assert 'ansible-galaxy' in mock_open.mock_calls[0][2]['http_agent']
+ assert mock_open.mock_calls[1][1][0] == 'https://galaxy.ansible.com/api/v1/tokens/'
+ assert 'ansible-galaxy' in mock_open.mock_calls[1][2]['http_agent']
+ assert mock_open.mock_calls[1][2]['data'] == 'github_token=github_token'
+
+
+def test_initialise_galaxy_with_auth(monkeypatch):
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ StringIO(u'{"available_versions":{"v1":"v1/"}}'),
+ StringIO(u'{"token":"my token"}'),
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=GalaxyToken(token='my_token'))
+ actual = api.authenticate("github_token")
+
+ assert len(api.available_api_versions) == 2
+ assert api.available_api_versions['v1'] == u'v1/'
+ assert api.available_api_versions['v2'] == u'v2/'
+ assert actual == {u'token': u'my token'}
+ assert mock_open.call_count == 2
+ assert mock_open.mock_calls[0][1][0] == 'https://galaxy.ansible.com/api/'
+ assert 'ansible-galaxy' in mock_open.mock_calls[0][2]['http_agent']
+ assert mock_open.mock_calls[1][1][0] == 'https://galaxy.ansible.com/api/v1/tokens/'
+ assert 'ansible-galaxy' in mock_open.mock_calls[1][2]['http_agent']
+ assert mock_open.mock_calls[1][2]['data'] == 'github_token=github_token'
+
+
+def test_initialise_automation_hub(monkeypatch):
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ StringIO(u'{"available_versions":{"v2": "v2/", "v3":"v3/"}}'),
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+ token = KeycloakToken(auth_url='https://api.test/')
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my_token'
+ monkeypatch.setattr(token, 'get', mock_token_get)
+
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
+
+ assert len(api.available_api_versions) == 2
+ assert api.available_api_versions['v2'] == u'v2/'
+ assert api.available_api_versions['v3'] == u'v3/'
+
+ assert mock_open.mock_calls[0][1][0] == 'https://galaxy.ansible.com/api/'
+ assert 'ansible-galaxy' in mock_open.mock_calls[0][2]['http_agent']
+ assert mock_open.mock_calls[0][2]['headers'] == {'Authorization': 'Bearer my_token'}
+
+
+def test_initialise_unknown(monkeypatch):
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ urllib_error.HTTPError('https://galaxy.ansible.com/api/', 500, 'msg', {}, StringIO(u'{"msg":"raw error"}')),
+ urllib_error.HTTPError('https://galaxy.ansible.com/api/api/', 500, 'msg', {}, StringIO(u'{"msg":"raw error"}')),
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=GalaxyToken(token='my_token'))
+
+ expected = "Error when finding available api versions from test (%s) (HTTP Code: 500, Message: msg)" \
+ % api.api_server
+ with pytest.raises(AnsibleError, match=re.escape(expected)):
+ api.authenticate("github_token")
+
+
+def test_get_available_api_versions(monkeypatch):
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ StringIO(u'{"available_versions":{"v1":"v1/","v2":"v2/"}}'),
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/")
+ actual = api.available_api_versions
+ assert len(actual) == 2
+ assert actual['v1'] == u'v1/'
+ assert actual['v2'] == u'v2/'
+
+ assert mock_open.call_count == 1
+ assert mock_open.mock_calls[0][1][0] == 'https://galaxy.ansible.com/api/'
+ assert 'ansible-galaxy' in mock_open.mock_calls[0][2]['http_agent']
+
+
+def test_publish_collection_missing_file():
+ fake_path = u'/fake/ÅÑŚÌβŁÈ/path'
+ expected = to_native("The collection path specified '%s' does not exist." % fake_path)
+
+ api = get_test_galaxy_api("https://galaxy.ansible.com/api/", "v2")
+ with pytest.raises(AnsibleError, match=expected):
+ api.publish_collection(fake_path)
+
+
+def test_publish_collection_not_a_tarball():
+ expected = "The collection path specified '{0}' is not a tarball, use 'ansible-galaxy collection build' to " \
+ "create a proper release artifact."
+
+ api = get_test_galaxy_api("https://galaxy.ansible.com/api/", "v2")
+ with tempfile.NamedTemporaryFile(prefix=u'ÅÑŚÌβŁÈ') as temp_file:
+ temp_file.write(b"\x00")
+ temp_file.flush()
+ with pytest.raises(AnsibleError, match=expected.format(to_native(temp_file.name))):
+ api.publish_collection(temp_file.name)
+
+
+def test_publish_collection_unsupported_version():
+ expected = "Galaxy action publish_collection requires API versions 'v2, v3' but only 'v1' are available on test " \
+ "https://galaxy.ansible.com/api/"
+
+ api = get_test_galaxy_api("https://galaxy.ansible.com/api/", "v1")
+ with pytest.raises(AnsibleError, match=expected):
+ api.publish_collection("path")
+
+
+@pytest.mark.parametrize('api_version, collection_url', [
+ ('v2', 'collections'),
+ ('v3', 'artifacts/collections'),
+])
+def test_publish_collection(api_version, collection_url, collection_artifact, monkeypatch):
+ api = get_test_galaxy_api("https://galaxy.ansible.com/api/", api_version)
+
+ mock_call = MagicMock()
+ mock_call.return_value = {'task': 'http://task.url/'}
+ monkeypatch.setattr(api, '_call_galaxy', mock_call)
+
+ actual = api.publish_collection(collection_artifact)
+ assert actual == 'http://task.url/'
+ assert mock_call.call_count == 1
+ assert mock_call.mock_calls[0][1][0] == 'https://galaxy.ansible.com/api/%s/%s/' % (api_version, collection_url)
+ assert mock_call.mock_calls[0][2]['headers']['Content-length'] == len(mock_call.mock_calls[0][2]['args'])
+ assert mock_call.mock_calls[0][2]['headers']['Content-type'].startswith(
+ 'multipart/form-data; boundary=')
+ assert mock_call.mock_calls[0][2]['args'].startswith(b'--')
+ assert mock_call.mock_calls[0][2]['method'] == 'POST'
+ assert mock_call.mock_calls[0][2]['auth_required'] is True
+
+
+@pytest.mark.parametrize('api_version, collection_url, response, expected', [
+ ('v2', 'collections', {},
+ 'Error when publishing collection to test (%s) (HTTP Code: 500, Message: msg Code: Unknown)'),
+ ('v2', 'collections', {
+ 'message': u'Galaxy error messäge',
+ 'code': 'GWE002',
+ }, u'Error when publishing collection to test (%s) (HTTP Code: 500, Message: Galaxy error messäge Code: GWE002)'),
+ ('v3', 'artifact/collections', {},
+ 'Error when publishing collection to test (%s) (HTTP Code: 500, Message: msg Code: Unknown)'),
+ ('v3', 'artifact/collections', {
+ 'errors': [
+ {
+ 'code': 'conflict.collection_exists',
+ 'detail': 'Collection "mynamespace-mycollection-4.1.1" already exists.',
+ 'title': 'Conflict.',
+ 'status': '400',
+ },
+ {
+ 'code': 'quantum_improbability',
+ 'title': u'Rändom(?) quantum improbability.',
+ 'source': {'parameter': 'the_arrow_of_time'},
+ 'meta': {'remediation': 'Try again before'},
+ },
+ ],
+ }, u'Error when publishing collection to test (%s) (HTTP Code: 500, Message: Collection '
+ u'"mynamespace-mycollection-4.1.1" already exists. Code: conflict.collection_exists), (HTTP Code: 500, '
+ u'Message: Rändom(?) quantum improbability. Code: quantum_improbability)')
+])
+def test_publish_failure(api_version, collection_url, response, expected, collection_artifact, monkeypatch):
+ api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version)
+
+ expected_url = '%s/api/%s/%s' % (api.api_server, api_version, collection_url)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = urllib_error.HTTPError(expected_url, 500, 'msg', {},
+ StringIO(to_text(json.dumps(response))))
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ with pytest.raises(GalaxyError, match=re.escape(to_native(expected % api.api_server))):
+ api.publish_collection(collection_artifact)
+
+
+@pytest.mark.parametrize('server_url, api_version, token_type, token_ins, import_uri, full_import_uri', [
+ ('https://galaxy.server.com/api', 'v2', 'Token', GalaxyToken('my token'),
+ '1234',
+ 'https://galaxy.server.com/api/v2/collection-imports/1234/'),
+ ('https://galaxy.server.com/api/automation-hub/', 'v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
+ '1234',
+ 'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
+])
+def test_wait_import_task(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
+ api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
+
+ if token_ins:
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my token'
+ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.return_value = StringIO(u'{"state":"success","finished_at":"time"}')
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ api.wait_import_task(import_uri)
+
+ assert mock_open.call_count == 1
+ assert mock_open.mock_calls[0][1][0] == full_import_uri
+ assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
+
+ assert mock_display.call_count == 1
+ assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
+
+
+@pytest.mark.parametrize('server_url, api_version, token_type, token_ins, import_uri, full_import_uri', [
+ ('https://galaxy.server.com/api/', 'v2', 'Token', GalaxyToken('my token'),
+ '1234',
+ 'https://galaxy.server.com/api/v2/collection-imports/1234/'),
+ ('https://galaxy.server.com/api/automation-hub', 'v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
+ '1234',
+ 'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
+])
+def test_wait_import_task_multiple_requests(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
+ api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
+
+ if token_ins:
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my token'
+ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ StringIO(u'{"state":"test"}'),
+ StringIO(u'{"state":"success","finished_at":"time"}'),
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ mock_vvv = MagicMock()
+ monkeypatch.setattr(Display, 'vvv', mock_vvv)
+
+ monkeypatch.setattr(time, 'sleep', MagicMock())
+
+ api.wait_import_task(import_uri)
+
+ assert mock_open.call_count == 2
+ assert mock_open.mock_calls[0][1][0] == full_import_uri
+ assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
+ assert mock_open.mock_calls[1][1][0] == full_import_uri
+ assert mock_open.mock_calls[1][2]['headers']['Authorization'] == '%s my token' % token_type
+
+ assert mock_display.call_count == 1
+ assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
+
+ assert mock_vvv.call_count == 1
+ assert mock_vvv.mock_calls[0][1][0] == \
+ 'Galaxy import process has a status of test, wait 2 seconds before trying again'
+
+
+@pytest.mark.parametrize('server_url, api_version, token_type, token_ins, import_uri, full_import_uri,', [
+ ('https://galaxy.server.com/api/', 'v2', 'Token', GalaxyToken('my token'),
+ '1234',
+ 'https://galaxy.server.com/api/v2/collection-imports/1234/'),
+ ('https://galaxy.server.com/api/automation-hub/', 'v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
+ '1234',
+ 'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
+])
+def test_wait_import_task_with_failure(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
+ api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
+
+ if token_ins:
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my token'
+ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ StringIO(to_text(json.dumps({
+ 'finished_at': 'some_time',
+ 'state': 'failed',
+ 'error': {
+ 'code': 'GW001',
+ 'description': u'Becäuse I said so!',
+
+ },
+ 'messages': [
+ {
+ 'level': 'ERrOR',
+ 'message': u'Somé error',
+ },
+ {
+ 'level': 'WARNiNG',
+ 'message': u'Some wärning',
+ },
+ {
+ 'level': 'INFO',
+ 'message': u'Somé info',
+ },
+ ],
+ }))),
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ mock_vvv = MagicMock()
+ monkeypatch.setattr(Display, 'vvv', mock_vvv)
+
+ mock_warn = MagicMock()
+ monkeypatch.setattr(Display, 'warning', mock_warn)
+
+ mock_err = MagicMock()
+ monkeypatch.setattr(Display, 'error', mock_err)
+
+ expected = to_native(u'Galaxy import process failed: Becäuse I said so! (Code: GW001)')
+ with pytest.raises(AnsibleError, match=re.escape(expected)):
+ api.wait_import_task(import_uri)
+
+ assert mock_open.call_count == 1
+ assert mock_open.mock_calls[0][1][0] == full_import_uri
+ assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
+
+ assert mock_display.call_count == 1
+ assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
+
+ assert mock_vvv.call_count == 1
+ assert mock_vvv.mock_calls[0][1][0] == u'Galaxy import message: INFO - Somé info'
+
+ assert mock_warn.call_count == 1
+ assert mock_warn.mock_calls[0][1][0] == u'Galaxy import warning message: Some wärning'
+
+ assert mock_err.call_count == 1
+ assert mock_err.mock_calls[0][1][0] == u'Galaxy import error message: Somé error'
+
+
+@pytest.mark.parametrize('server_url, api_version, token_type, token_ins, import_uri, full_import_uri', [
+ ('https://galaxy.server.com/api/', 'v2', 'Token', GalaxyToken('my_token'),
+ '1234',
+ 'https://galaxy.server.com/api/v2/collection-imports/1234/'),
+ ('https://galaxy.server.com/api/automation-hub/', 'v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
+ '1234',
+ 'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
+])
+def test_wait_import_task_with_failure_no_error(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
+ api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
+
+ if token_ins:
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my token'
+ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ StringIO(to_text(json.dumps({
+ 'finished_at': 'some_time',
+ 'state': 'failed',
+ 'error': {},
+ 'messages': [
+ {
+ 'level': 'ERROR',
+ 'message': u'Somé error',
+ },
+ {
+ 'level': 'WARNING',
+ 'message': u'Some wärning',
+ },
+ {
+ 'level': 'INFO',
+ 'message': u'Somé info',
+ },
+ ],
+ }))),
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ mock_vvv = MagicMock()
+ monkeypatch.setattr(Display, 'vvv', mock_vvv)
+
+ mock_warn = MagicMock()
+ monkeypatch.setattr(Display, 'warning', mock_warn)
+
+ mock_err = MagicMock()
+ monkeypatch.setattr(Display, 'error', mock_err)
+
+ expected = 'Galaxy import process failed: Unknown error, see %s for more details \\(Code: UNKNOWN\\)' % full_import_uri
+ with pytest.raises(AnsibleError, match=expected):
+ api.wait_import_task(import_uri)
+
+ assert mock_open.call_count == 1
+ assert mock_open.mock_calls[0][1][0] == full_import_uri
+ assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
+
+ assert mock_display.call_count == 1
+ assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
+
+ assert mock_vvv.call_count == 1
+ assert mock_vvv.mock_calls[0][1][0] == u'Galaxy import message: INFO - Somé info'
+
+ assert mock_warn.call_count == 1
+ assert mock_warn.mock_calls[0][1][0] == u'Galaxy import warning message: Some wärning'
+
+ assert mock_err.call_count == 1
+ assert mock_err.mock_calls[0][1][0] == u'Galaxy import error message: Somé error'
+
+
+@pytest.mark.parametrize('server_url, api_version, token_type, token_ins, import_uri, full_import_uri', [
+ ('https://galaxy.server.com/api', 'v2', 'Token', GalaxyToken('my token'),
+ '1234',
+ 'https://galaxy.server.com/api/v2/collection-imports/1234/'),
+ ('https://galaxy.server.com/api/automation-hub', 'v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
+ '1234',
+ 'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
+])
+def test_wait_import_task_timeout(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
+ api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
+
+ if token_ins:
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my token'
+ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ def return_response(*args, **kwargs):
+ return StringIO(u'{"state":"waiting"}')
+
+ mock_open = MagicMock()
+ mock_open.side_effect = return_response
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ mock_vvv = MagicMock()
+ monkeypatch.setattr(Display, 'vvv', mock_vvv)
+
+ monkeypatch.setattr(time, 'sleep', MagicMock())
+
+ expected = "Timeout while waiting for the Galaxy import process to finish, check progress at '%s'" % full_import_uri
+ with pytest.raises(AnsibleError, match=expected):
+ api.wait_import_task(import_uri, 1)
+
+ assert mock_open.call_count > 1
+ assert mock_open.mock_calls[0][1][0] == full_import_uri
+ assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
+ assert mock_open.mock_calls[1][1][0] == full_import_uri
+ assert mock_open.mock_calls[1][2]['headers']['Authorization'] == '%s my token' % token_type
+
+ assert mock_display.call_count == 1
+ assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
+
+ # expected_wait_msg = 'Galaxy import process has a status of waiting, wait {0} seconds before trying again'
+ assert mock_vvv.call_count > 9 # 1st is opening Galaxy token file.
+
+ # FIXME:
+ # assert mock_vvv.mock_calls[1][1][0] == expected_wait_msg.format(2)
+ # assert mock_vvv.mock_calls[2][1][0] == expected_wait_msg.format(3)
+ # assert mock_vvv.mock_calls[3][1][0] == expected_wait_msg.format(4)
+ # assert mock_vvv.mock_calls[4][1][0] == expected_wait_msg.format(6)
+ # assert mock_vvv.mock_calls[5][1][0] == expected_wait_msg.format(10)
+ # assert mock_vvv.mock_calls[6][1][0] == expected_wait_msg.format(15)
+ # assert mock_vvv.mock_calls[7][1][0] == expected_wait_msg.format(22)
+ # assert mock_vvv.mock_calls[8][1][0] == expected_wait_msg.format(30)
+
+
+@pytest.mark.parametrize('api_version, token_type, version, token_ins', [
+ ('v2', None, 'v2.1.13', None),
+ ('v3', 'Bearer', 'v1.0.0', KeycloakToken(auth_url='https://api.test/api/automation-hub/')),
+])
+def test_get_collection_version_metadata_no_version(api_version, token_type, version, token_ins, monkeypatch):
+ api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
+
+ if token_ins:
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my token'
+ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ StringIO(to_text(json.dumps({
+ 'href': 'https://galaxy.server.com/api/{api}/namespace/name/versions/{version}/'.format(api=api_version, version=version),
+ 'download_url': 'https://downloadme.com',
+ 'artifact': {
+ 'sha256': 'ac47b6fac117d7c171812750dacda655b04533cf56b31080b82d1c0db3c9d80f',
+ },
+ 'namespace': {
+ 'name': 'namespace',
+ },
+ 'collection': {
+ 'name': 'collection',
+ },
+ 'version': version,
+ 'metadata': {
+ 'dependencies': {},
+ }
+ }))),
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ actual = api.get_collection_version_metadata('namespace', 'collection', version)
+
+ assert isinstance(actual, CollectionVersionMetadata)
+ assert actual.namespace == u'namespace'
+ assert actual.name == u'collection'
+ assert actual.download_url == u'https://downloadme.com'
+ assert actual.artifact_sha256 == u'ac47b6fac117d7c171812750dacda655b04533cf56b31080b82d1c0db3c9d80f'
+ assert actual.version == version
+ assert actual.dependencies == {}
+
+ assert mock_open.call_count == 1
+ assert mock_open.mock_calls[0][1][0] == '%s%s/collections/namespace/collection/versions/%s/' \
+ % (api.api_server, api_version, version)
+
+ # v2 calls dont need auth, so no authz header or token_type
+ if token_type:
+ assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
+
+
+@pytest.mark.parametrize('api_version, token_type, token_ins, version', [
+ ('v2', None, None, '2.1.13'),
+ ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/api/automation-hub/'), '1.0.0'),
+])
+def test_get_collection_signatures_backwards_compat(api_version, token_type, token_ins, version, monkeypatch):
+ api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
+
+ if token_ins:
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my token'
+ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ StringIO("{}")
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ actual = api.get_collection_signatures('namespace', 'collection', version)
+ assert actual == []
+
+ assert mock_open.call_count == 1
+ assert mock_open.mock_calls[0][1][0] == '%s%s/collections/namespace/collection/versions/%s/' \
+ % (api.api_server, api_version, version)
+
+ # v2 calls dont need auth, so no authz header or token_type
+ if token_type:
+ assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
+
+
+@pytest.mark.parametrize('api_version, token_type, token_ins, version', [
+ ('v2', None, None, '2.1.13'),
+ ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/api/automation-hub/'), '1.0.0'),
+])
+def test_get_collection_signatures(api_version, token_type, token_ins, version, monkeypatch):
+ api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
+
+ if token_ins:
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my token'
+ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ StringIO(to_text(json.dumps({
+ 'signatures': [
+ {
+ "signature": "-----BEGIN PGP SIGNATURE-----\nSIGNATURE1\n-----END PGP SIGNATURE-----\n",
+ "pubkey_fingerprint": "FINGERPRINT",
+ "signing_service": "ansible-default",
+ "pulp_created": "2022-01-14T14:05:53.835605Z",
+ },
+ {
+ "signature": "-----BEGIN PGP SIGNATURE-----\nSIGNATURE2\n-----END PGP SIGNATURE-----\n",
+ "pubkey_fingerprint": "FINGERPRINT",
+ "signing_service": "ansible-default",
+ "pulp_created": "2022-01-14T14:05:53.835605Z",
+ },
+ ],
+ }))),
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ actual = api.get_collection_signatures('namespace', 'collection', version)
+
+ assert actual == [
+ "-----BEGIN PGP SIGNATURE-----\nSIGNATURE1\n-----END PGP SIGNATURE-----\n",
+ "-----BEGIN PGP SIGNATURE-----\nSIGNATURE2\n-----END PGP SIGNATURE-----\n"
+ ]
+
+ assert mock_open.call_count == 1
+ assert mock_open.mock_calls[0][1][0] == '%s%s/collections/namespace/collection/versions/%s/' \
+ % (api.api_server, api_version, version)
+
+ # v2 calls dont need auth, so no authz header or token_type
+ if token_type:
+ assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
+
+
+@pytest.mark.parametrize('api_version, token_type, token_ins, response', [
+ ('v2', None, None, {
+ 'count': 2,
+ 'next': None,
+ 'previous': None,
+ 'results': [
+ {
+ 'version': '1.0.0',
+ 'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.0',
+ },
+ {
+ 'version': '1.0.1',
+ 'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.1',
+ },
+ ],
+ }),
+ # TODO: Verify this once Automation Hub is actually out
+ ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'), {
+ 'count': 2,
+ 'next': None,
+ 'previous': None,
+ 'data': [
+ {
+ 'version': '1.0.0',
+ 'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.0',
+ },
+ {
+ 'version': '1.0.1',
+ 'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.1',
+ },
+ ],
+ }),
+])
+def test_get_collection_versions(api_version, token_type, token_ins, response, monkeypatch):
+ api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
+
+ if token_ins:
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my token'
+ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [
+ StringIO(to_text(json.dumps(response))),
+ ]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ actual = api.get_collection_versions('namespace', 'collection')
+ assert actual == [u'1.0.0', u'1.0.1']
+
+ page_query = '?limit=100' if api_version == 'v3' else '?page_size=100'
+ assert mock_open.call_count == 1
+ assert mock_open.mock_calls[0][1][0] == 'https://galaxy.server.com/api/%s/collections/namespace/collection/' \
+ 'versions/%s' % (api_version, page_query)
+ if token_ins:
+ assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
+
+
+@pytest.mark.parametrize('api_version, token_type, token_ins, responses', [
+ ('v2', None, None, [
+ {
+ 'count': 6,
+ 'next': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/?page=2&page_size=100',
+ 'previous': None,
+ 'results': [ # Pay no mind, using more manageable results than page_size would indicate
+ {
+ 'version': '1.0.0',
+ 'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.0',
+ },
+ {
+ 'version': '1.0.1',
+ 'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.1',
+ },
+ ],
+ },
+ {
+ 'count': 6,
+ 'next': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/?page=3&page_size=100',
+ 'previous': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions',
+ 'results': [
+ {
+ 'version': '1.0.2',
+ 'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.2',
+ },
+ {
+ 'version': '1.0.3',
+ 'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.3',
+ },
+ ],
+ },
+ {
+ 'count': 6,
+ 'next': None,
+ 'previous': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/?page=2&page_size=100',
+ 'results': [
+ {
+ 'version': '1.0.4',
+ 'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.4',
+ },
+ {
+ 'version': '1.0.5',
+ 'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.5',
+ },
+ ],
+ },
+ ]),
+ ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'), [
+ {
+ 'count': 6,
+ 'links': {
+ # v3 links are relative and the limit is included during pagination
+ 'next': '/api/v3/collections/namespace/collection/versions/?limit=100&offset=100',
+ 'previous': None,
+ },
+ 'data': [
+ {
+ 'version': '1.0.0',
+ 'href': '/api/v3/collections/namespace/collection/versions/1.0.0',
+ },
+ {
+ 'version': '1.0.1',
+ 'href': '/api/v3/collections/namespace/collection/versions/1.0.1',
+ },
+ ],
+ },
+ {
+ 'count': 6,
+ 'links': {
+ 'next': '/api/v3/collections/namespace/collection/versions/?limit=100&offset=200',
+ 'previous': '/api/v3/collections/namespace/collection/versions',
+ },
+ 'data': [
+ {
+ 'version': '1.0.2',
+ 'href': '/api/v3/collections/namespace/collection/versions/1.0.2',
+ },
+ {
+ 'version': '1.0.3',
+ 'href': '/api/v3/collections/namespace/collection/versions/1.0.3',
+ },
+ ],
+ },
+ {
+ 'count': 6,
+ 'links': {
+ 'next': None,
+ 'previous': '/api/v3/collections/namespace/collection/versions/?limit=100&offset=100',
+ },
+ 'data': [
+ {
+ 'version': '1.0.4',
+ 'href': '/api/v3/collections/namespace/collection/versions/1.0.4',
+ },
+ {
+ 'version': '1.0.5',
+ 'href': '/api/v3/collections/namespace/collection/versions/1.0.5',
+ },
+ ],
+ },
+ ]),
+])
+def test_get_collection_versions_pagination(api_version, token_type, token_ins, responses, monkeypatch):
+ api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
+
+ if token_ins:
+ mock_token_get = MagicMock()
+ mock_token_get.return_value = 'my token'
+ monkeypatch.setattr(token_ins, 'get', mock_token_get)
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [StringIO(to_text(json.dumps(r))) for r in responses]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ actual = api.get_collection_versions('namespace', 'collection')
+ assert actual == [u'1.0.0', u'1.0.1', u'1.0.2', u'1.0.3', u'1.0.4', u'1.0.5']
+
+ assert mock_open.call_count == 3
+
+ if api_version == 'v3':
+ query_1 = 'limit=100'
+ query_2 = 'limit=100&offset=100'
+ query_3 = 'limit=100&offset=200'
+ else:
+ query_1 = 'page_size=100'
+ query_2 = 'page=2&page_size=100'
+ query_3 = 'page=3&page_size=100'
+
+ assert mock_open.mock_calls[0][1][0] == 'https://galaxy.server.com/api/%s/collections/namespace/collection/' \
+ 'versions/?%s' % (api_version, query_1)
+ assert mock_open.mock_calls[1][1][0] == 'https://galaxy.server.com/api/%s/collections/namespace/collection/' \
+ 'versions/?%s' % (api_version, query_2)
+ assert mock_open.mock_calls[2][1][0] == 'https://galaxy.server.com/api/%s/collections/namespace/collection/' \
+ 'versions/?%s' % (api_version, query_3)
+
+ if token_type:
+ assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
+ assert mock_open.mock_calls[1][2]['headers']['Authorization'] == '%s my token' % token_type
+ assert mock_open.mock_calls[2][2]['headers']['Authorization'] == '%s my token' % token_type
+
+
+@pytest.mark.parametrize('responses', [
+ [
+ {
+ 'count': 2,
+ 'results': [{'name': '3.5.1', }, {'name': '3.5.2'}],
+ 'next_link': None,
+ 'next': None,
+ 'previous_link': None,
+ 'previous': None
+ },
+ ],
+ [
+ {
+ 'count': 2,
+ 'results': [{'name': '3.5.1'}],
+ 'next_link': '/api/v1/roles/432/versions/?page=2&page_size=50',
+ 'next': '/roles/432/versions/?page=2&page_size=50',
+ 'previous_link': None,
+ 'previous': None
+ },
+ {
+ 'count': 2,
+ 'results': [{'name': '3.5.2'}],
+ 'next_link': None,
+ 'next': None,
+ 'previous_link': '/api/v1/roles/432/versions/?&page_size=50',
+ 'previous': '/roles/432/versions/?page_size=50',
+ },
+ ]
+])
+def test_get_role_versions_pagination(monkeypatch, responses):
+ api = get_test_galaxy_api('https://galaxy.com/api/', 'v1')
+
+ mock_open = MagicMock()
+ mock_open.side_effect = [StringIO(to_text(json.dumps(r))) for r in responses]
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ actual = api.fetch_role_related('versions', 432)
+ assert actual == [{'name': '3.5.1'}, {'name': '3.5.2'}]
+
+ assert mock_open.call_count == len(responses)
+
+ assert mock_open.mock_calls[0][1][0] == 'https://galaxy.com/api/v1/roles/432/versions/?page_size=50'
+ if len(responses) == 2:
+ assert mock_open.mock_calls[1][1][0] == 'https://galaxy.com/api/v1/roles/432/versions/?page=2&page_size=50'
+
+
+def test_missing_cache_dir(cache_dir):
+ os.rmdir(cache_dir)
+ GalaxyAPI(None, "test", 'https://galaxy.ansible.com/', no_cache=False)
+
+ assert os.path.isdir(cache_dir)
+ assert stat.S_IMODE(os.stat(cache_dir).st_mode) == 0o700
+
+ cache_file = os.path.join(cache_dir, 'api.json')
+ with open(cache_file) as fd:
+ actual_cache = fd.read()
+ assert actual_cache == '{"version": 1}'
+ assert stat.S_IMODE(os.stat(cache_file).st_mode) == 0o600
+
+
+def test_existing_cache(cache_dir):
+ cache_file = os.path.join(cache_dir, 'api.json')
+ cache_file_contents = '{"version": 1, "test": "json"}'
+ with open(cache_file, mode='w') as fd:
+ fd.write(cache_file_contents)
+ os.chmod(cache_file, 0o655)
+
+ GalaxyAPI(None, "test", 'https://galaxy.ansible.com/', no_cache=False)
+
+ assert os.path.isdir(cache_dir)
+ with open(cache_file) as fd:
+ actual_cache = fd.read()
+ assert actual_cache == cache_file_contents
+ assert stat.S_IMODE(os.stat(cache_file).st_mode) == 0o655
+
+
+@pytest.mark.parametrize('content', [
+ '',
+ 'value',
+ '{"de" "finit" "ely" [\'invalid"]}',
+ '[]',
+ '{"version": 2, "test": "json"}',
+ '{"version": 2, "key": "ÅÑŚÌβŁÈ"}',
+])
+def test_cache_invalid_cache_content(content, cache_dir):
+ cache_file = os.path.join(cache_dir, 'api.json')
+ with open(cache_file, mode='w') as fd:
+ fd.write(content)
+ os.chmod(cache_file, 0o664)
+
+ GalaxyAPI(None, "test", 'https://galaxy.ansible.com/', no_cache=False)
+
+ with open(cache_file) as fd:
+ actual_cache = fd.read()
+ assert actual_cache == '{"version": 1}'
+ assert stat.S_IMODE(os.stat(cache_file).st_mode) == 0o664
+
+
+def test_cache_complete_pagination(cache_dir, monkeypatch):
+
+ responses = get_collection_versions()
+ cache_file = os.path.join(cache_dir, 'api.json')
+
+ api = get_test_galaxy_api('https://galaxy.server.com/api/', 'v2', no_cache=False)
+
+ mock_open = MagicMock(
+ side_effect=[
+ StringIO(to_text(json.dumps(r)))
+ for r in responses
+ ]
+ )
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ actual_versions = api.get_collection_versions('namespace', 'collection')
+ assert actual_versions == [u'1.0.0', u'1.0.1', u'1.0.2', u'1.0.3', u'1.0.4', u'1.0.5']
+
+ with open(cache_file) as fd:
+ final_cache = json.loads(fd.read())
+
+ cached_server = final_cache['galaxy.server.com:']
+ cached_collection = cached_server['/api/v2/collections/namespace/collection/versions/']
+ cached_versions = [r['version'] for r in cached_collection['results']]
+
+ assert final_cache == api._cache
+ assert cached_versions == actual_versions
+
+
+def test_cache_complete_pagination_v3(cache_dir, monkeypatch):
+
+ responses = get_v3_collection_versions()
+ cache_file = os.path.join(cache_dir, 'api.json')
+
+ api = get_test_galaxy_api('https://galaxy.server.com/api/', 'v3', no_cache=False)
+
+ mock_open = MagicMock(
+ side_effect=[
+ StringIO(to_text(json.dumps(r)))
+ for r in responses
+ ]
+ )
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ actual_versions = api.get_collection_versions('namespace', 'collection')
+ assert actual_versions == [u'1.0.0', u'1.0.1', u'1.0.2', u'1.0.3', u'1.0.4', u'1.0.5']
+
+ with open(cache_file) as fd:
+ final_cache = json.loads(fd.read())
+
+ cached_server = final_cache['galaxy.server.com:']
+ cached_collection = cached_server['/api/v3/collections/namespace/collection/versions/']
+ cached_versions = [r['version'] for r in cached_collection['results']]
+
+ assert final_cache == api._cache
+ assert cached_versions == actual_versions
+
+
+def test_cache_flaky_pagination(cache_dir, monkeypatch):
+
+ responses = get_collection_versions()
+ cache_file = os.path.join(cache_dir, 'api.json')
+
+ api = get_test_galaxy_api('https://galaxy.server.com/api/', 'v2', no_cache=False)
+
+ # First attempt, fail midway through
+ mock_open = MagicMock(
+ side_effect=[
+ StringIO(to_text(json.dumps(responses[0]))),
+ StringIO(to_text(json.dumps(responses[1]))),
+ urllib_error.HTTPError(responses[1]['next'], 500, 'Error', {}, StringIO()),
+ StringIO(to_text(json.dumps(responses[3]))),
+ ]
+ )
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ expected = (
+ r'Error when getting available collection versions for namespace\.collection '
+ r'from test \(https://galaxy\.server\.com/api/\) '
+ r'\(HTTP Code: 500, Message: Error Code: Unknown\)'
+ )
+ with pytest.raises(GalaxyError, match=expected):
+ api.get_collection_versions('namespace', 'collection')
+
+ with open(cache_file) as fd:
+ final_cache = json.loads(fd.read())
+
+ assert final_cache == {
+ 'version': 1,
+ 'galaxy.server.com:': {
+ 'modified': {
+ 'namespace.collection': responses[0]['modified']
+ }
+ }
+ }
+
+ # Reset API
+ api = get_test_galaxy_api('https://galaxy.server.com/api/', 'v2', no_cache=False)
+
+ # Second attempt is successful so cache should be populated
+ mock_open = MagicMock(
+ side_effect=[
+ StringIO(to_text(json.dumps(r)))
+ for r in responses
+ ]
+ )
+ monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
+
+ actual_versions = api.get_collection_versions('namespace', 'collection')
+ assert actual_versions == [u'1.0.0', u'1.0.1', u'1.0.2', u'1.0.3', u'1.0.4', u'1.0.5']
+
+ with open(cache_file) as fd:
+ final_cache = json.loads(fd.read())
+
+ cached_server = final_cache['galaxy.server.com:']
+ cached_collection = cached_server['/api/v2/collections/namespace/collection/versions/']
+ cached_versions = [r['version'] for r in cached_collection['results']]
+
+ assert cached_versions == actual_versions
+
+
+def test_world_writable_cache(cache_dir, monkeypatch):
+ mock_warning = MagicMock()
+ monkeypatch.setattr(Display, 'warning', mock_warning)
+
+ cache_file = os.path.join(cache_dir, 'api.json')
+ with open(cache_file, mode='w') as fd:
+ fd.write('{"version": 2}')
+ os.chmod(cache_file, 0o666)
+
+ api = GalaxyAPI(None, "test", 'https://galaxy.ansible.com/', no_cache=False)
+ assert api._cache is None
+
+ with open(cache_file) as fd:
+ actual_cache = fd.read()
+ assert actual_cache == '{"version": 2}'
+ assert stat.S_IMODE(os.stat(cache_file).st_mode) == 0o666
+
+ assert mock_warning.call_count == 1
+ assert mock_warning.call_args[0][0] == \
+ 'Galaxy cache has world writable access (%s), ignoring it as a cache source.' % cache_file
+
+
+def test_no_cache(cache_dir):
+ cache_file = os.path.join(cache_dir, 'api.json')
+ with open(cache_file, mode='w') as fd:
+ fd.write('random')
+
+ api = GalaxyAPI(None, "test", 'https://galaxy.ansible.com/')
+ assert api._cache is None
+
+ with open(cache_file) as fd:
+ actual_cache = fd.read()
+ assert actual_cache == 'random'
+
+
+def test_clear_cache_with_no_cache(cache_dir):
+ cache_file = os.path.join(cache_dir, 'api.json')
+ with open(cache_file, mode='w') as fd:
+ fd.write('{"version": 1, "key": "value"}')
+
+ GalaxyAPI(None, "test", 'https://galaxy.ansible.com/', clear_response_cache=True)
+ assert not os.path.exists(cache_file)
+
+
+def test_clear_cache(cache_dir):
+ cache_file = os.path.join(cache_dir, 'api.json')
+ with open(cache_file, mode='w') as fd:
+ fd.write('{"version": 1, "key": "value"}')
+
+ GalaxyAPI(None, "test", 'https://galaxy.ansible.com/', clear_response_cache=True, no_cache=False)
+
+ with open(cache_file) as fd:
+ actual_cache = fd.read()
+ assert actual_cache == '{"version": 1}'
+ assert stat.S_IMODE(os.stat(cache_file).st_mode) == 0o600
+
+
+@pytest.mark.parametrize(['url', 'expected'], [
+ ('http://hostname/path', 'hostname:'),
+ ('http://hostname:80/path', 'hostname:80'),
+ ('https://testing.com:invalid', 'testing.com:'),
+ ('https://testing.com:1234', 'testing.com:1234'),
+ ('https://username:password@testing.com/path', 'testing.com:'),
+ ('https://username:password@testing.com:443/path', 'testing.com:443'),
+])
+def test_cache_id(url, expected):
+ actual = galaxy_api.get_cache_id(url)
+ assert actual == expected
diff --git a/test/units/galaxy/test_collection.py b/test/units/galaxy/test_collection.py
new file mode 100644
index 0000000..106251c
--- /dev/null
+++ b/test/units/galaxy/test_collection.py
@@ -0,0 +1,1217 @@
+# -*- 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)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import os
+import pytest
+import re
+import tarfile
+import tempfile
+import uuid
+
+from hashlib import sha256
+from io import BytesIO
+from unittest.mock import MagicMock, mock_open, patch
+
+import ansible.constants as C
+from ansible import context
+from ansible.cli.galaxy import GalaxyCLI, SERVER_DEF
+from ansible.errors import AnsibleError
+from ansible.galaxy import api, collection, token
+from ansible.module_utils._text import to_bytes, to_native, to_text
+from ansible.module_utils.six.moves import builtins
+from ansible.utils import context_objects as co
+from ansible.utils.display import Display
+from ansible.utils.hashing import secure_hash_s
+from ansible.utils.sentinel import Sentinel
+
+
+@pytest.fixture(autouse='function')
+def reset_cli_args():
+ co.GlobalCLIArgs._Singleton__instance = None
+ yield
+ co.GlobalCLIArgs._Singleton__instance = None
+
+
+@pytest.fixture()
+def collection_input(tmp_path_factory):
+ ''' Creates a collection skeleton directory for build tests '''
+ test_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ namespace = 'ansible_namespace'
+ collection = 'collection'
+ skeleton = os.path.join(os.path.dirname(os.path.split(__file__)[0]), 'cli', 'test_data', 'collection_skeleton')
+
+ galaxy_args = ['ansible-galaxy', 'collection', 'init', '%s.%s' % (namespace, collection),
+ '-c', '--init-path', test_dir, '--collection-skeleton', skeleton]
+ GalaxyCLI(args=galaxy_args).run()
+ collection_dir = os.path.join(test_dir, namespace, collection)
+ output_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Output'))
+
+ return collection_dir, output_dir
+
+
+@pytest.fixture()
+def collection_artifact(monkeypatch, tmp_path_factory):
+ ''' Creates a temp collection artifact and mocked open_url instance for publishing tests '''
+ mock_open = MagicMock()
+ monkeypatch.setattr(collection.concrete_artifact_manager, 'open_url', mock_open)
+
+ mock_uuid = MagicMock()
+ mock_uuid.return_value.hex = 'uuid'
+ monkeypatch.setattr(uuid, 'uuid4', mock_uuid)
+
+ tmp_path = tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections')
+ input_file = to_text(tmp_path / 'collection.tar.gz')
+
+ with tarfile.open(input_file, 'w:gz') as tfile:
+ b_io = BytesIO(b"\x00\x01\x02\x03")
+ tar_info = tarfile.TarInfo('test')
+ tar_info.size = 4
+ tar_info.mode = 0o0644
+ tfile.addfile(tarinfo=tar_info, fileobj=b_io)
+
+ return input_file, mock_open
+
+
+@pytest.fixture()
+def galaxy_yml_dir(request, tmp_path_factory):
+ b_test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections'))
+ b_galaxy_yml = os.path.join(b_test_dir, b'galaxy.yml')
+ with open(b_galaxy_yml, 'wb') as galaxy_obj:
+ galaxy_obj.write(to_bytes(request.param))
+
+ yield b_test_dir
+
+
+@pytest.fixture()
+def tmp_tarfile(tmp_path_factory, manifest_info):
+ ''' Creates a temporary tar file for _extract_tar_file tests '''
+ filename = u'ÅÑŚÌβŁÈ'
+ temp_dir = to_bytes(tmp_path_factory.mktemp('test-%s Collections' % to_native(filename)))
+ tar_file = os.path.join(temp_dir, to_bytes('%s.tar.gz' % filename))
+ data = os.urandom(8)
+
+ with tarfile.open(tar_file, 'w:gz') as tfile:
+ b_io = BytesIO(data)
+ tar_info = tarfile.TarInfo(filename)
+ tar_info.size = len(data)
+ tar_info.mode = 0o0644
+ tfile.addfile(tarinfo=tar_info, fileobj=b_io)
+
+ b_data = to_bytes(json.dumps(manifest_info, indent=True), errors='surrogate_or_strict')
+ b_io = BytesIO(b_data)
+ tar_info = tarfile.TarInfo('MANIFEST.json')
+ tar_info.size = len(b_data)
+ tar_info.mode = 0o0644
+ tfile.addfile(tarinfo=tar_info, fileobj=b_io)
+
+ sha256_hash = sha256()
+ sha256_hash.update(data)
+
+ with tarfile.open(tar_file, 'r') as tfile:
+ yield temp_dir, tfile, filename, sha256_hash.hexdigest()
+
+
+@pytest.fixture()
+def galaxy_server():
+ context.CLIARGS._store = {'ignore_certs': False}
+ galaxy_api = api.GalaxyAPI(None, 'test_server', 'https://galaxy.ansible.com',
+ token=token.GalaxyToken(token='key'))
+ return galaxy_api
+
+
+@pytest.fixture()
+def manifest_template():
+ def get_manifest_info(namespace='ansible_namespace', name='collection', version='0.1.0'):
+ return {
+ "collection_info": {
+ "namespace": namespace,
+ "name": name,
+ "version": version,
+ "authors": [
+ "shertel"
+ ],
+ "readme": "README.md",
+ "tags": [
+ "test",
+ "collection"
+ ],
+ "description": "Test",
+ "license": [
+ "MIT"
+ ],
+ "license_file": None,
+ "dependencies": {},
+ "repository": "https://github.com/{0}/{1}".format(namespace, name),
+ "documentation": None,
+ "homepage": None,
+ "issues": None
+ },
+ "file_manifest_file": {
+ "name": "FILES.json",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "files_manifest_checksum",
+ "format": 1
+ },
+ "format": 1
+ }
+
+ return get_manifest_info
+
+
+@pytest.fixture()
+def manifest_info(manifest_template):
+ return manifest_template()
+
+
+@pytest.fixture()
+def files_manifest_info():
+ return {
+ "files": [
+ {
+ "name": ".",
+ "ftype": "dir",
+ "chksum_type": None,
+ "chksum_sha256": None,
+ "format": 1
+ },
+ {
+ "name": "README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "individual_file_checksum",
+ "format": 1
+ }
+ ],
+ "format": 1}
+
+
+@pytest.fixture()
+def manifest(manifest_info):
+ b_data = to_bytes(json.dumps(manifest_info))
+
+ with patch.object(builtins, 'open', mock_open(read_data=b_data)) as m:
+ with open('MANIFEST.json', mode='rb') as fake_file:
+ yield fake_file, sha256(b_data).hexdigest()
+
+
+@pytest.mark.parametrize(
+ 'required_signature_count,valid',
+ [
+ ("1", True),
+ ("+1", True),
+ ("all", True),
+ ("+all", True),
+ ("-1", False),
+ ("invalid", False),
+ ("1.5", False),
+ ("+", False),
+ ]
+)
+def test_cli_options(required_signature_count, valid, monkeypatch):
+ cli_args = [
+ 'ansible-galaxy',
+ 'collection',
+ 'install',
+ 'namespace.collection:1.0.0',
+ '--keyring',
+ '~/.ansible/pubring.kbx',
+ '--required-valid-signature-count',
+ required_signature_count
+ ]
+
+ galaxy_cli = GalaxyCLI(args=cli_args)
+ mock_execute_install = MagicMock()
+ monkeypatch.setattr(galaxy_cli, '_execute_install_collection', mock_execute_install)
+
+ if valid:
+ galaxy_cli.run()
+ else:
+ with pytest.raises(SystemExit, match='2') as error:
+ galaxy_cli.run()
+
+
+@pytest.mark.parametrize(
+ "config,server",
+ [
+ (
+ # Options to create ini config
+ {
+ 'url': 'https://galaxy.ansible.com',
+ 'validate_certs': 'False',
+ 'v3': 'False',
+ },
+ # Expected server attributes
+ {
+ 'validate_certs': False,
+ '_available_api_versions': {},
+ },
+ ),
+ (
+ {
+ 'url': 'https://galaxy.ansible.com',
+ 'validate_certs': 'True',
+ 'v3': 'True',
+ },
+ {
+ 'validate_certs': True,
+ '_available_api_versions': {'v3': '/v3'},
+ },
+ ),
+ ],
+)
+def test_bool_type_server_config_options(config, server, monkeypatch):
+ cli_args = [
+ 'ansible-galaxy',
+ 'collection',
+ 'install',
+ 'namespace.collection:1.0.0',
+ ]
+
+ config_lines = [
+ "[galaxy]",
+ "server_list=server1\n",
+ "[galaxy_server.server1]",
+ "url=%s" % config['url'],
+ "v3=%s" % config['v3'],
+ "validate_certs=%s\n" % config['validate_certs'],
+ ]
+
+ with tempfile.NamedTemporaryFile(suffix='.cfg') as tmp_file:
+ tmp_file.write(
+ to_bytes('\n'.join(config_lines))
+ )
+ tmp_file.flush()
+
+ with patch.object(C, 'GALAXY_SERVER_LIST', ['server1']):
+ with patch.object(C.config, '_config_file', tmp_file.name):
+ C.config._parse_config_file()
+ galaxy_cli = GalaxyCLI(args=cli_args)
+ mock_execute_install = MagicMock()
+ monkeypatch.setattr(galaxy_cli, '_execute_install_collection', mock_execute_install)
+ galaxy_cli.run()
+
+ assert galaxy_cli.api_servers[0].name == 'server1'
+ assert galaxy_cli.api_servers[0].validate_certs == server['validate_certs']
+ assert galaxy_cli.api_servers[0]._available_api_versions == server['_available_api_versions']
+
+
+@pytest.mark.parametrize('global_ignore_certs', [True, False])
+def test_validate_certs(global_ignore_certs, monkeypatch):
+ cli_args = [
+ 'ansible-galaxy',
+ 'collection',
+ 'install',
+ 'namespace.collection:1.0.0',
+ ]
+ if global_ignore_certs:
+ cli_args.append('--ignore-certs')
+
+ galaxy_cli = GalaxyCLI(args=cli_args)
+ mock_execute_install = MagicMock()
+ monkeypatch.setattr(galaxy_cli, '_execute_install_collection', mock_execute_install)
+ galaxy_cli.run()
+
+ assert len(galaxy_cli.api_servers) == 1
+ assert galaxy_cli.api_servers[0].validate_certs is not global_ignore_certs
+
+
+@pytest.mark.parametrize(
+ ["ignore_certs_cli", "ignore_certs_cfg", "expected_validate_certs"],
+ [
+ (None, None, True),
+ (None, True, False),
+ (None, False, True),
+ (True, None, False),
+ (True, True, False),
+ (True, False, False),
+ ]
+)
+def test_validate_certs_with_server_url(ignore_certs_cli, ignore_certs_cfg, expected_validate_certs, monkeypatch):
+ cli_args = [
+ 'ansible-galaxy',
+ 'collection',
+ 'install',
+ 'namespace.collection:1.0.0',
+ '-s',
+ 'https://galaxy.ansible.com'
+ ]
+ if ignore_certs_cli:
+ cli_args.append('--ignore-certs')
+ if ignore_certs_cfg is not None:
+ monkeypatch.setattr(C, 'GALAXY_IGNORE_CERTS', ignore_certs_cfg)
+
+ galaxy_cli = GalaxyCLI(args=cli_args)
+ mock_execute_install = MagicMock()
+ monkeypatch.setattr(galaxy_cli, '_execute_install_collection', mock_execute_install)
+ galaxy_cli.run()
+
+ assert len(galaxy_cli.api_servers) == 1
+ assert galaxy_cli.api_servers[0].validate_certs == expected_validate_certs
+
+
+@pytest.mark.parametrize(
+ ["ignore_certs_cli", "ignore_certs_cfg", "expected_server2_validate_certs", "expected_server3_validate_certs"],
+ [
+ (None, None, True, True),
+ (None, True, True, False),
+ (None, False, True, True),
+ (True, None, False, False),
+ (True, True, False, False),
+ (True, False, False, False),
+ ]
+)
+def test_validate_certs_server_config(ignore_certs_cfg, ignore_certs_cli, expected_server2_validate_certs, expected_server3_validate_certs, monkeypatch):
+ server_names = ['server1', 'server2', 'server3']
+ cfg_lines = [
+ "[galaxy]",
+ "server_list=server1,server2,server3",
+ "[galaxy_server.server1]",
+ "url=https://galaxy.ansible.com/api/",
+ "validate_certs=False",
+ "[galaxy_server.server2]",
+ "url=https://galaxy.ansible.com/api/",
+ "validate_certs=True",
+ "[galaxy_server.server3]",
+ "url=https://galaxy.ansible.com/api/",
+ ]
+ cli_args = [
+ 'ansible-galaxy',
+ 'collection',
+ 'install',
+ 'namespace.collection:1.0.0',
+ ]
+ if ignore_certs_cli:
+ cli_args.append('--ignore-certs')
+ if ignore_certs_cfg is not None:
+ monkeypatch.setattr(C, 'GALAXY_IGNORE_CERTS', ignore_certs_cfg)
+
+ monkeypatch.setattr(C, 'GALAXY_SERVER_LIST', server_names)
+
+ with tempfile.NamedTemporaryFile(suffix='.cfg') as tmp_file:
+ tmp_file.write(to_bytes('\n'.join(cfg_lines), errors='surrogate_or_strict'))
+ tmp_file.flush()
+
+ monkeypatch.setattr(C.config, '_config_file', tmp_file.name)
+ C.config._parse_config_file()
+ galaxy_cli = GalaxyCLI(args=cli_args)
+ mock_execute_install = MagicMock()
+ monkeypatch.setattr(galaxy_cli, '_execute_install_collection', mock_execute_install)
+ galaxy_cli.run()
+
+ # (not) --ignore-certs > server's validate_certs > (not) GALAXY_IGNORE_CERTS > True
+ assert galaxy_cli.api_servers[0].validate_certs is False
+ assert galaxy_cli.api_servers[1].validate_certs is expected_server2_validate_certs
+ assert galaxy_cli.api_servers[2].validate_certs is expected_server3_validate_certs
+
+
+def test_build_collection_no_galaxy_yaml():
+ fake_path = u'/fake/ÅÑŚÌβŁÈ/path'
+ expected = to_native("The collection galaxy.yml path '%s/galaxy.yml' does not exist." % fake_path)
+
+ with pytest.raises(AnsibleError, match=expected):
+ collection.build_collection(fake_path, u'output', False)
+
+
+def test_build_existing_output_file(collection_input):
+ input_dir, output_dir = collection_input
+
+ existing_output_dir = os.path.join(output_dir, 'ansible_namespace-collection-0.1.0.tar.gz')
+ os.makedirs(existing_output_dir)
+
+ expected = "The output collection artifact '%s' already exists, but is a directory - aborting" \
+ % to_native(existing_output_dir)
+ with pytest.raises(AnsibleError, match=expected):
+ collection.build_collection(to_text(input_dir, errors='surrogate_or_strict'), to_text(output_dir, errors='surrogate_or_strict'), False)
+
+
+def test_build_existing_output_without_force(collection_input):
+ input_dir, output_dir = collection_input
+
+ existing_output = os.path.join(output_dir, 'ansible_namespace-collection-0.1.0.tar.gz')
+ with open(existing_output, 'w+') as out_file:
+ out_file.write("random garbage")
+ out_file.flush()
+
+ expected = "The file '%s' already exists. You can use --force to re-create the collection artifact." \
+ % to_native(existing_output)
+ with pytest.raises(AnsibleError, match=expected):
+ collection.build_collection(to_text(input_dir, errors='surrogate_or_strict'), to_text(output_dir, errors='surrogate_or_strict'), False)
+
+
+def test_build_existing_output_with_force(collection_input):
+ input_dir, output_dir = collection_input
+
+ existing_output = os.path.join(output_dir, 'ansible_namespace-collection-0.1.0.tar.gz')
+ with open(existing_output, 'w+') as out_file:
+ out_file.write("random garbage")
+ out_file.flush()
+
+ collection.build_collection(to_text(input_dir, errors='surrogate_or_strict'), to_text(output_dir, errors='surrogate_or_strict'), True)
+
+ # Verify the file was replaced with an actual tar file
+ assert tarfile.is_tarfile(existing_output)
+
+
+def test_build_with_existing_files_and_manifest(collection_input):
+ input_dir, output_dir = collection_input
+
+ with open(os.path.join(input_dir, 'MANIFEST.json'), "wb") as fd:
+ fd.write(b'{"collection_info": {"version": "6.6.6"}, "version": 1}')
+
+ with open(os.path.join(input_dir, 'FILES.json'), "wb") as fd:
+ fd.write(b'{"files": [], "format": 1}')
+
+ with open(os.path.join(input_dir, "plugins", "MANIFEST.json"), "wb") as fd:
+ fd.write(b"test data that should be in build")
+
+ collection.build_collection(to_text(input_dir, errors='surrogate_or_strict'), to_text(output_dir, errors='surrogate_or_strict'), False)
+
+ output_artifact = os.path.join(output_dir, 'ansible_namespace-collection-0.1.0.tar.gz')
+ assert tarfile.is_tarfile(output_artifact)
+
+ with tarfile.open(output_artifact, mode='r') as actual:
+ members = actual.getmembers()
+
+ manifest_file = next(m for m in members if m.path == "MANIFEST.json")
+ manifest_file_obj = actual.extractfile(manifest_file.name)
+ manifest_file_text = manifest_file_obj.read()
+ manifest_file_obj.close()
+ assert manifest_file_text != b'{"collection_info": {"version": "6.6.6"}, "version": 1}'
+
+ json_file = next(m for m in members if m.path == "MANIFEST.json")
+ json_file_obj = actual.extractfile(json_file.name)
+ json_file_text = json_file_obj.read()
+ json_file_obj.close()
+ assert json_file_text != b'{"files": [], "format": 1}'
+
+ sub_manifest_file = next(m for m in members if m.path == "plugins/MANIFEST.json")
+ sub_manifest_file_obj = actual.extractfile(sub_manifest_file.name)
+ sub_manifest_file_text = sub_manifest_file_obj.read()
+ sub_manifest_file_obj.close()
+ assert sub_manifest_file_text == b"test data that should be in build"
+
+
+@pytest.mark.parametrize('galaxy_yml_dir', [b'namespace: value: broken'], indirect=True)
+def test_invalid_yaml_galaxy_file(galaxy_yml_dir):
+ galaxy_file = os.path.join(galaxy_yml_dir, b'galaxy.yml')
+ expected = to_native(b"Failed to parse the galaxy.yml at '%s' with the following error:" % galaxy_file)
+
+ with pytest.raises(AnsibleError, match=expected):
+ collection.concrete_artifact_manager._get_meta_from_src_dir(galaxy_yml_dir)
+
+
+@pytest.mark.parametrize('galaxy_yml_dir', [b'namespace: test_namespace'], indirect=True)
+def test_missing_required_galaxy_key(galaxy_yml_dir):
+ galaxy_file = os.path.join(galaxy_yml_dir, b'galaxy.yml')
+ expected = "The collection galaxy.yml at '%s' is missing the following mandatory keys: authors, name, " \
+ "readme, version" % to_native(galaxy_file)
+
+ with pytest.raises(AnsibleError, match=expected):
+ collection.concrete_artifact_manager._get_meta_from_src_dir(galaxy_yml_dir)
+
+
+@pytest.mark.parametrize('galaxy_yml_dir', [b'namespace: test_namespace'], indirect=True)
+def test_galaxy_yaml_no_mandatory_keys(galaxy_yml_dir):
+ expected = "The collection galaxy.yml at '%s/galaxy.yml' is missing the " \
+ "following mandatory keys: authors, name, readme, version" % to_native(galaxy_yml_dir)
+
+ with pytest.raises(ValueError, match=expected):
+ assert collection.concrete_artifact_manager._get_meta_from_src_dir(galaxy_yml_dir, require_build_metadata=False) == expected
+
+
+@pytest.mark.parametrize('galaxy_yml_dir', [b'My life story is so very interesting'], indirect=True)
+def test_galaxy_yaml_no_mandatory_keys_bad_yaml(galaxy_yml_dir):
+ expected = "The collection galaxy.yml at '%s/galaxy.yml' is incorrectly formatted." % to_native(galaxy_yml_dir)
+
+ with pytest.raises(AnsibleError, match=expected):
+ collection.concrete_artifact_manager._get_meta_from_src_dir(galaxy_yml_dir)
+
+
+@pytest.mark.parametrize('galaxy_yml_dir', [b"""
+namespace: namespace
+name: collection
+authors: Jordan
+version: 0.1.0
+readme: README.md
+invalid: value"""], indirect=True)
+def test_warning_extra_keys(galaxy_yml_dir, monkeypatch):
+ display_mock = MagicMock()
+ monkeypatch.setattr(Display, 'warning', display_mock)
+
+ collection.concrete_artifact_manager._get_meta_from_src_dir(galaxy_yml_dir)
+
+ assert display_mock.call_count == 1
+ assert display_mock.call_args[0][0] == "Found unknown keys in collection galaxy.yml at '%s/galaxy.yml': invalid"\
+ % to_text(galaxy_yml_dir)
+
+
+@pytest.mark.parametrize('galaxy_yml_dir', [b"""
+namespace: namespace
+name: collection
+authors: Jordan
+version: 0.1.0
+readme: README.md"""], indirect=True)
+def test_defaults_galaxy_yml(galaxy_yml_dir):
+ actual = collection.concrete_artifact_manager._get_meta_from_src_dir(galaxy_yml_dir)
+
+ assert actual['namespace'] == 'namespace'
+ assert actual['name'] == 'collection'
+ assert actual['authors'] == ['Jordan']
+ assert actual['version'] == '0.1.0'
+ assert actual['readme'] == 'README.md'
+ assert actual['description'] is None
+ assert actual['repository'] is None
+ assert actual['documentation'] is None
+ assert actual['homepage'] is None
+ assert actual['issues'] is None
+ assert actual['tags'] == []
+ assert actual['dependencies'] == {}
+ assert actual['license'] == []
+
+
+@pytest.mark.parametrize('galaxy_yml_dir', [(b"""
+namespace: namespace
+name: collection
+authors: Jordan
+version: 0.1.0
+readme: README.md
+license: MIT"""), (b"""
+namespace: namespace
+name: collection
+authors: Jordan
+version: 0.1.0
+readme: README.md
+license:
+- MIT""")], indirect=True)
+def test_galaxy_yml_list_value(galaxy_yml_dir):
+ actual = collection.concrete_artifact_manager._get_meta_from_src_dir(galaxy_yml_dir)
+ assert actual['license'] == ['MIT']
+
+
+def test_build_ignore_files_and_folders(collection_input, monkeypatch):
+ input_dir = collection_input[0]
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'vvv', mock_display)
+
+ git_folder = os.path.join(input_dir, '.git')
+ retry_file = os.path.join(input_dir, 'ansible.retry')
+
+ tests_folder = os.path.join(input_dir, 'tests', 'output')
+ tests_output_file = os.path.join(tests_folder, 'result.txt')
+
+ os.makedirs(git_folder)
+ os.makedirs(tests_folder)
+
+ with open(retry_file, 'w+') as ignore_file:
+ ignore_file.write('random')
+ ignore_file.flush()
+
+ with open(tests_output_file, 'w+') as tests_file:
+ tests_file.write('random')
+ tests_file.flush()
+
+ actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel)
+
+ assert actual['format'] == 1
+ for manifest_entry in actual['files']:
+ assert manifest_entry['name'] not in ['.git', 'ansible.retry', 'galaxy.yml', 'tests/output', 'tests/output/result.txt']
+
+ expected_msgs = [
+ "Skipping '%s/galaxy.yml' for collection build" % to_text(input_dir),
+ "Skipping '%s' for collection build" % to_text(retry_file),
+ "Skipping '%s' for collection build" % to_text(git_folder),
+ "Skipping '%s' for collection build" % to_text(tests_folder),
+ ]
+ assert mock_display.call_count == 4
+ assert mock_display.mock_calls[0][1][0] in expected_msgs
+ assert mock_display.mock_calls[1][1][0] in expected_msgs
+ assert mock_display.mock_calls[2][1][0] in expected_msgs
+ assert mock_display.mock_calls[3][1][0] in expected_msgs
+
+
+def test_build_ignore_older_release_in_root(collection_input, monkeypatch):
+ input_dir = collection_input[0]
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'vvv', mock_display)
+
+ # This is expected to be ignored because it is in the root collection dir.
+ release_file = os.path.join(input_dir, 'namespace-collection-0.0.0.tar.gz')
+
+ # This is not expected to be ignored because it is not in the root collection dir.
+ fake_release_file = os.path.join(input_dir, 'plugins', 'namespace-collection-0.0.0.tar.gz')
+
+ for filename in [release_file, fake_release_file]:
+ with open(filename, 'w+') as file_obj:
+ file_obj.write('random')
+ file_obj.flush()
+
+ actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel)
+ assert actual['format'] == 1
+
+ plugin_release_found = False
+ for manifest_entry in actual['files']:
+ assert manifest_entry['name'] != 'namespace-collection-0.0.0.tar.gz'
+ if manifest_entry['name'] == 'plugins/namespace-collection-0.0.0.tar.gz':
+ plugin_release_found = True
+
+ assert plugin_release_found
+
+ expected_msgs = [
+ "Skipping '%s/galaxy.yml' for collection build" % to_text(input_dir),
+ "Skipping '%s' for collection build" % to_text(release_file)
+ ]
+ assert mock_display.call_count == 2
+ assert mock_display.mock_calls[0][1][0] in expected_msgs
+ assert mock_display.mock_calls[1][1][0] in expected_msgs
+
+
+def test_build_ignore_patterns(collection_input, monkeypatch):
+ input_dir = collection_input[0]
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'vvv', mock_display)
+
+ actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection',
+ ['*.md', 'plugins/action', 'playbooks/*.j2'],
+ Sentinel)
+ assert actual['format'] == 1
+
+ expected_missing = [
+ 'README.md',
+ 'docs/My Collection.md',
+ 'plugins/action',
+ 'playbooks/templates/test.conf.j2',
+ 'playbooks/templates/subfolder/test.conf.j2',
+ ]
+
+ # Files or dirs that are close to a match but are not, make sure they are present
+ expected_present = [
+ 'docs',
+ 'roles/common/templates/test.conf.j2',
+ 'roles/common/templates/subfolder/test.conf.j2',
+ ]
+
+ actual_files = [e['name'] for e in actual['files']]
+ for m in expected_missing:
+ assert m not in actual_files
+
+ for p in expected_present:
+ assert p in actual_files
+
+ expected_msgs = [
+ "Skipping '%s/galaxy.yml' for collection build" % to_text(input_dir),
+ "Skipping '%s/README.md' for collection build" % to_text(input_dir),
+ "Skipping '%s/docs/My Collection.md' for collection build" % to_text(input_dir),
+ "Skipping '%s/plugins/action' for collection build" % to_text(input_dir),
+ "Skipping '%s/playbooks/templates/test.conf.j2' for collection build" % to_text(input_dir),
+ "Skipping '%s/playbooks/templates/subfolder/test.conf.j2' for collection build" % to_text(input_dir),
+ ]
+ assert mock_display.call_count == len(expected_msgs)
+ assert mock_display.mock_calls[0][1][0] in expected_msgs
+ assert mock_display.mock_calls[1][1][0] in expected_msgs
+ assert mock_display.mock_calls[2][1][0] in expected_msgs
+ assert mock_display.mock_calls[3][1][0] in expected_msgs
+ assert mock_display.mock_calls[4][1][0] in expected_msgs
+ assert mock_display.mock_calls[5][1][0] in expected_msgs
+
+
+def test_build_ignore_symlink_target_outside_collection(collection_input, monkeypatch):
+ input_dir, outside_dir = collection_input
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'warning', mock_display)
+
+ link_path = os.path.join(input_dir, 'plugins', 'connection')
+ os.symlink(outside_dir, link_path)
+
+ actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel)
+ for manifest_entry in actual['files']:
+ assert manifest_entry['name'] != 'plugins/connection'
+
+ assert mock_display.call_count == 1
+ assert mock_display.mock_calls[0][1][0] == "Skipping '%s' as it is a symbolic link to a directory outside " \
+ "the collection" % to_text(link_path)
+
+
+def test_build_copy_symlink_target_inside_collection(collection_input):
+ input_dir = collection_input[0]
+
+ os.makedirs(os.path.join(input_dir, 'playbooks', 'roles'))
+ roles_link = os.path.join(input_dir, 'playbooks', 'roles', 'linked')
+
+ roles_target = os.path.join(input_dir, 'roles', 'linked')
+ roles_target_tasks = os.path.join(roles_target, 'tasks')
+ os.makedirs(roles_target_tasks)
+ with open(os.path.join(roles_target_tasks, 'main.yml'), 'w+') as tasks_main:
+ tasks_main.write("---\n- hosts: localhost\n tasks:\n - ping:")
+ tasks_main.flush()
+
+ os.symlink(roles_target, roles_link)
+
+ actual = collection._build_files_manifest(to_bytes(input_dir), 'namespace', 'collection', [], Sentinel)
+
+ linked_entries = [e for e in actual['files'] if e['name'].startswith('playbooks/roles/linked')]
+ assert len(linked_entries) == 1
+ assert linked_entries[0]['name'] == 'playbooks/roles/linked'
+ assert linked_entries[0]['ftype'] == 'dir'
+
+
+def test_build_with_symlink_inside_collection(collection_input):
+ input_dir, output_dir = collection_input
+
+ os.makedirs(os.path.join(input_dir, 'playbooks', 'roles'))
+ roles_link = os.path.join(input_dir, 'playbooks', 'roles', 'linked')
+ file_link = os.path.join(input_dir, 'docs', 'README.md')
+
+ roles_target = os.path.join(input_dir, 'roles', 'linked')
+ roles_target_tasks = os.path.join(roles_target, 'tasks')
+ os.makedirs(roles_target_tasks)
+ with open(os.path.join(roles_target_tasks, 'main.yml'), 'w+') as tasks_main:
+ tasks_main.write("---\n- hosts: localhost\n tasks:\n - ping:")
+ tasks_main.flush()
+
+ os.symlink(roles_target, roles_link)
+ os.symlink(os.path.join(input_dir, 'README.md'), file_link)
+
+ collection.build_collection(to_text(input_dir, errors='surrogate_or_strict'), to_text(output_dir, errors='surrogate_or_strict'), False)
+
+ output_artifact = os.path.join(output_dir, 'ansible_namespace-collection-0.1.0.tar.gz')
+ assert tarfile.is_tarfile(output_artifact)
+
+ with tarfile.open(output_artifact, mode='r') as actual:
+ members = actual.getmembers()
+
+ linked_folder = next(m for m in members if m.path == 'playbooks/roles/linked')
+ assert linked_folder.type == tarfile.SYMTYPE
+ assert linked_folder.linkname == '../../roles/linked'
+
+ linked_file = next(m for m in members if m.path == 'docs/README.md')
+ assert linked_file.type == tarfile.SYMTYPE
+ assert linked_file.linkname == '../README.md'
+
+ linked_file_obj = actual.extractfile(linked_file.name)
+ actual_file = secure_hash_s(linked_file_obj.read())
+ linked_file_obj.close()
+
+ assert actual_file == '63444bfc766154e1bc7557ef6280de20d03fcd81'
+
+
+def test_publish_no_wait(galaxy_server, collection_artifact, monkeypatch):
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ artifact_path, mock_open = collection_artifact
+ fake_import_uri = 'https://galaxy.server.com/api/v2/import/1234'
+
+ mock_publish = MagicMock()
+ mock_publish.return_value = fake_import_uri
+ monkeypatch.setattr(galaxy_server, 'publish_collection', mock_publish)
+
+ collection.publish_collection(artifact_path, galaxy_server, False, 0)
+
+ assert mock_publish.call_count == 1
+ assert mock_publish.mock_calls[0][1][0] == artifact_path
+
+ assert mock_display.call_count == 1
+ assert mock_display.mock_calls[0][1][0] == \
+ "Collection has been pushed to the Galaxy server %s %s, not waiting until import has completed due to " \
+ "--no-wait being set. Import task results can be found at %s" % (galaxy_server.name, galaxy_server.api_server,
+ fake_import_uri)
+
+
+def test_publish_with_wait(galaxy_server, collection_artifact, monkeypatch):
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ artifact_path, mock_open = collection_artifact
+ fake_import_uri = 'https://galaxy.server.com/api/v2/import/1234'
+
+ mock_publish = MagicMock()
+ mock_publish.return_value = fake_import_uri
+ monkeypatch.setattr(galaxy_server, 'publish_collection', mock_publish)
+
+ mock_wait = MagicMock()
+ monkeypatch.setattr(galaxy_server, 'wait_import_task', mock_wait)
+
+ collection.publish_collection(artifact_path, galaxy_server, True, 0)
+
+ assert mock_publish.call_count == 1
+ assert mock_publish.mock_calls[0][1][0] == artifact_path
+
+ assert mock_wait.call_count == 1
+ assert mock_wait.mock_calls[0][1][0] == '1234'
+
+ assert mock_display.mock_calls[0][1][0] == "Collection has been published to the Galaxy server test_server %s" \
+ % galaxy_server.api_server
+
+
+def test_find_existing_collections(tmp_path_factory, monkeypatch):
+ test_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+ collection1 = os.path.join(test_dir, 'namespace1', 'collection1')
+ collection2 = os.path.join(test_dir, 'namespace2', 'collection2')
+ fake_collection1 = os.path.join(test_dir, 'namespace3', 'collection3')
+ fake_collection2 = os.path.join(test_dir, 'namespace4')
+ os.makedirs(collection1)
+ os.makedirs(collection2)
+ os.makedirs(os.path.split(fake_collection1)[0])
+
+ open(fake_collection1, 'wb+').close()
+ open(fake_collection2, 'wb+').close()
+
+ collection1_manifest = json.dumps({
+ 'collection_info': {
+ 'namespace': 'namespace1',
+ 'name': 'collection1',
+ 'version': '1.2.3',
+ 'authors': ['Jordan Borean'],
+ 'readme': 'README.md',
+ 'dependencies': {},
+ },
+ 'format': 1,
+ })
+ with open(os.path.join(collection1, 'MANIFEST.json'), 'wb') as manifest_obj:
+ manifest_obj.write(to_bytes(collection1_manifest))
+
+ mock_warning = MagicMock()
+ monkeypatch.setattr(Display, 'warning', mock_warning)
+
+ actual = list(collection.find_existing_collections(test_dir, artifacts_manager=concrete_artifact_cm))
+
+ assert len(actual) == 2
+ for actual_collection in actual:
+ if '%s.%s' % (actual_collection.namespace, actual_collection.name) == 'namespace1.collection1':
+ assert actual_collection.namespace == 'namespace1'
+ assert actual_collection.name == 'collection1'
+ assert actual_collection.ver == '1.2.3'
+ assert to_text(actual_collection.src) == collection1
+ else:
+ assert actual_collection.namespace == 'namespace2'
+ assert actual_collection.name == 'collection2'
+ assert actual_collection.ver == '*'
+ assert to_text(actual_collection.src) == collection2
+
+ assert mock_warning.call_count == 1
+ assert mock_warning.mock_calls[0][1][0] == "Collection at '%s' does not have a MANIFEST.json file, nor has it galaxy.yml: " \
+ "cannot detect version." % to_text(collection2)
+
+
+def test_download_file(tmp_path_factory, monkeypatch):
+ temp_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections'))
+
+ data = b"\x00\x01\x02\x03"
+ sha256_hash = sha256()
+ sha256_hash.update(data)
+
+ mock_open = MagicMock()
+ mock_open.return_value = BytesIO(data)
+ monkeypatch.setattr(collection.concrete_artifact_manager, 'open_url', mock_open)
+
+ expected = temp_dir
+ actual = collection._download_file('http://google.com/file', temp_dir, sha256_hash.hexdigest(), True)
+
+ assert actual.startswith(expected)
+ assert os.path.isfile(actual)
+ with open(actual, 'rb') as file_obj:
+ assert file_obj.read() == data
+
+ assert mock_open.call_count == 1
+ assert mock_open.mock_calls[0][1][0] == 'http://google.com/file'
+
+
+def test_download_file_hash_mismatch(tmp_path_factory, monkeypatch):
+ temp_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections'))
+
+ data = b"\x00\x01\x02\x03"
+
+ mock_open = MagicMock()
+ mock_open.return_value = BytesIO(data)
+ monkeypatch.setattr(collection.concrete_artifact_manager, 'open_url', mock_open)
+
+ expected = "Mismatch artifact hash with downloaded file"
+ with pytest.raises(AnsibleError, match=expected):
+ collection._download_file('http://google.com/file', temp_dir, 'bad', True)
+
+
+def test_extract_tar_file_invalid_hash(tmp_tarfile):
+ temp_dir, tfile, filename, dummy = tmp_tarfile
+
+ expected = "Checksum mismatch for '%s' inside collection at '%s'" % (to_native(filename), to_native(tfile.name))
+ with pytest.raises(AnsibleError, match=expected):
+ collection._extract_tar_file(tfile, filename, temp_dir, temp_dir, "fakehash")
+
+
+def test_extract_tar_file_missing_member(tmp_tarfile):
+ temp_dir, tfile, dummy, dummy = tmp_tarfile
+
+ expected = "Collection tar at '%s' does not contain the expected file 'missing'." % to_native(tfile.name)
+ with pytest.raises(AnsibleError, match=expected):
+ collection._extract_tar_file(tfile, 'missing', temp_dir, temp_dir)
+
+
+def test_extract_tar_file_missing_parent_dir(tmp_tarfile):
+ temp_dir, tfile, filename, checksum = tmp_tarfile
+ output_dir = os.path.join(temp_dir, b'output')
+ output_file = os.path.join(output_dir, to_bytes(filename))
+
+ collection._extract_tar_file(tfile, filename, output_dir, temp_dir, checksum)
+ os.path.isfile(output_file)
+
+
+def test_extract_tar_file_outside_dir(tmp_path_factory):
+ filename = u'ÅÑŚÌβŁÈ'
+ temp_dir = to_bytes(tmp_path_factory.mktemp('test-%s Collections' % to_native(filename)))
+ tar_file = os.path.join(temp_dir, to_bytes('%s.tar.gz' % filename))
+ data = os.urandom(8)
+
+ tar_filename = '../%s.sh' % filename
+ with tarfile.open(tar_file, 'w:gz') as tfile:
+ b_io = BytesIO(data)
+ tar_info = tarfile.TarInfo(tar_filename)
+ tar_info.size = len(data)
+ tar_info.mode = 0o0644
+ tfile.addfile(tarinfo=tar_info, fileobj=b_io)
+
+ expected = re.escape("Cannot extract tar entry '%s' as it will be placed outside the collection directory"
+ % to_native(tar_filename))
+ with tarfile.open(tar_file, 'r') as tfile:
+ with pytest.raises(AnsibleError, match=expected):
+ collection._extract_tar_file(tfile, tar_filename, os.path.join(temp_dir, to_bytes(filename)), temp_dir)
+
+
+def test_require_one_of_collections_requirements_with_both():
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'verify', 'namespace.collection', '-r', 'requirements.yml'])
+
+ with pytest.raises(AnsibleError) as req_err:
+ cli._require_one_of_collections_requirements(('namespace.collection',), 'requirements.yml')
+
+ with pytest.raises(AnsibleError) as cli_err:
+ cli.run()
+
+ assert req_err.value.message == cli_err.value.message == 'The positional collection_name arg and --requirements-file are mutually exclusive.'
+
+
+def test_require_one_of_collections_requirements_with_neither():
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'verify'])
+
+ with pytest.raises(AnsibleError) as req_err:
+ cli._require_one_of_collections_requirements((), '')
+
+ with pytest.raises(AnsibleError) as cli_err:
+ cli.run()
+
+ assert req_err.value.message == cli_err.value.message == 'You must specify a collection name or a requirements file.'
+
+
+def test_require_one_of_collections_requirements_with_collections():
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'verify', 'namespace1.collection1', 'namespace2.collection1:1.0.0'])
+ collections = ('namespace1.collection1', 'namespace2.collection1:1.0.0',)
+
+ requirements = cli._require_one_of_collections_requirements(collections, '')['collections']
+
+ req_tuples = [('%s.%s' % (req.namespace, req.name), req.ver, req.src, req.type,) for req in requirements]
+ assert req_tuples == [('namespace1.collection1', '*', None, 'galaxy'), ('namespace2.collection1', '1.0.0', None, 'galaxy')]
+
+
+@patch('ansible.cli.galaxy.GalaxyCLI._parse_requirements_file')
+def test_require_one_of_collections_requirements_with_requirements(mock_parse_requirements_file, galaxy_server):
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'verify', '-r', 'requirements.yml', 'namespace.collection'])
+ mock_parse_requirements_file.return_value = {'collections': [('namespace.collection', '1.0.5', galaxy_server)]}
+ requirements = cli._require_one_of_collections_requirements((), 'requirements.yml')['collections']
+
+ assert mock_parse_requirements_file.call_count == 1
+ assert requirements == [('namespace.collection', '1.0.5', galaxy_server)]
+
+
+@patch('ansible.cli.galaxy.GalaxyCLI.execute_verify', spec=True)
+def test_call_GalaxyCLI(execute_verify):
+ galaxy_args = ['ansible-galaxy', 'collection', 'verify', 'namespace.collection']
+
+ GalaxyCLI(args=galaxy_args).run()
+
+ assert execute_verify.call_count == 1
+
+
+@patch('ansible.cli.galaxy.GalaxyCLI.execute_verify')
+def test_call_GalaxyCLI_with_implicit_role(execute_verify):
+ galaxy_args = ['ansible-galaxy', 'verify', 'namespace.implicit_role']
+
+ with pytest.raises(SystemExit):
+ GalaxyCLI(args=galaxy_args).run()
+
+ assert not execute_verify.called
+
+
+@patch('ansible.cli.galaxy.GalaxyCLI.execute_verify')
+def test_call_GalaxyCLI_with_role(execute_verify):
+ galaxy_args = ['ansible-galaxy', 'role', 'verify', 'namespace.role']
+
+ with pytest.raises(SystemExit):
+ GalaxyCLI(args=galaxy_args).run()
+
+ assert not execute_verify.called
+
+
+@patch('ansible.cli.galaxy.verify_collections', spec=True)
+def test_execute_verify_with_defaults(mock_verify_collections):
+ galaxy_args = ['ansible-galaxy', 'collection', 'verify', 'namespace.collection:1.0.4']
+ GalaxyCLI(args=galaxy_args).run()
+
+ assert mock_verify_collections.call_count == 1
+
+ print("Call args {0}".format(mock_verify_collections.call_args[0]))
+ requirements, search_paths, galaxy_apis, ignore_errors = mock_verify_collections.call_args[0]
+
+ assert [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type) for r in requirements] == [('namespace.collection', '1.0.4', None, 'galaxy')]
+ for install_path in search_paths:
+ assert install_path.endswith('ansible_collections')
+ assert galaxy_apis[0].api_server == 'https://galaxy.ansible.com'
+ assert ignore_errors is False
+
+
+@patch('ansible.cli.galaxy.verify_collections', spec=True)
+def test_execute_verify(mock_verify_collections):
+ GalaxyCLI(args=[
+ 'ansible-galaxy', 'collection', 'verify', 'namespace.collection:1.0.4', '--ignore-certs',
+ '-p', '~/.ansible', '--ignore-errors', '--server', 'http://galaxy-dev.com',
+ ]).run()
+
+ assert mock_verify_collections.call_count == 1
+
+ requirements, search_paths, galaxy_apis, ignore_errors = mock_verify_collections.call_args[0]
+
+ assert [('%s.%s' % (r.namespace, r.name), r.ver, r.src, r.type) for r in requirements] == [('namespace.collection', '1.0.4', None, 'galaxy')]
+ for install_path in search_paths:
+ assert install_path.endswith('ansible_collections')
+ assert galaxy_apis[0].api_server == 'http://galaxy-dev.com'
+ assert ignore_errors is True
+
+
+def test_verify_file_hash_deleted_file(manifest_info):
+ data = to_bytes(json.dumps(manifest_info))
+ digest = sha256(data).hexdigest()
+
+ namespace = manifest_info['collection_info']['namespace']
+ name = manifest_info['collection_info']['name']
+ version = manifest_info['collection_info']['version']
+ server = 'http://galaxy.ansible.com'
+
+ error_queue = []
+
+ with patch.object(builtins, 'open', mock_open(read_data=data)) as m:
+ with patch.object(collection.os.path, 'isfile', MagicMock(return_value=False)) as mock_isfile:
+ collection._verify_file_hash(b'path/', 'file', digest, error_queue)
+
+ assert mock_isfile.called_once
+
+ assert len(error_queue) == 1
+ assert error_queue[0].installed is None
+ assert error_queue[0].expected == digest
+
+
+def test_verify_file_hash_matching_hash(manifest_info):
+
+ data = to_bytes(json.dumps(manifest_info))
+ digest = sha256(data).hexdigest()
+
+ namespace = manifest_info['collection_info']['namespace']
+ name = manifest_info['collection_info']['name']
+ version = manifest_info['collection_info']['version']
+ server = 'http://galaxy.ansible.com'
+
+ error_queue = []
+
+ with patch.object(builtins, 'open', mock_open(read_data=data)) as m:
+ with patch.object(collection.os.path, 'isfile', MagicMock(return_value=True)) as mock_isfile:
+ collection._verify_file_hash(b'path/', 'file', digest, error_queue)
+
+ assert mock_isfile.called_once
+
+ assert error_queue == []
+
+
+def test_verify_file_hash_mismatching_hash(manifest_info):
+
+ data = to_bytes(json.dumps(manifest_info))
+ digest = sha256(data).hexdigest()
+ different_digest = 'not_{0}'.format(digest)
+
+ namespace = manifest_info['collection_info']['namespace']
+ name = manifest_info['collection_info']['name']
+ version = manifest_info['collection_info']['version']
+ server = 'http://galaxy.ansible.com'
+
+ error_queue = []
+
+ with patch.object(builtins, 'open', mock_open(read_data=data)) as m:
+ with patch.object(collection.os.path, 'isfile', MagicMock(return_value=True)) as mock_isfile:
+ collection._verify_file_hash(b'path/', 'file', different_digest, error_queue)
+
+ assert mock_isfile.called_once
+
+ assert len(error_queue) == 1
+ assert error_queue[0].installed == digest
+ assert error_queue[0].expected == different_digest
+
+
+def test_consume_file(manifest):
+
+ manifest_file, checksum = manifest
+ assert checksum == collection._consume_file(manifest_file)
+
+
+def test_consume_file_and_write_contents(manifest, manifest_info):
+
+ manifest_file, checksum = manifest
+
+ write_to = BytesIO()
+ actual_hash = collection._consume_file(manifest_file, write_to)
+
+ write_to.seek(0)
+ assert to_bytes(json.dumps(manifest_info)) == write_to.read()
+ assert actual_hash == checksum
+
+
+def test_get_tar_file_member(tmp_tarfile):
+
+ temp_dir, tfile, filename, checksum = tmp_tarfile
+
+ with collection._get_tar_file_member(tfile, filename) as (tar_file_member, tar_file_obj):
+ assert isinstance(tar_file_member, tarfile.TarInfo)
+ assert isinstance(tar_file_obj, tarfile.ExFileObject)
+
+
+def test_get_nonexistent_tar_file_member(tmp_tarfile):
+ temp_dir, tfile, filename, checksum = tmp_tarfile
+
+ file_does_not_exist = filename + 'nonexistent'
+
+ with pytest.raises(AnsibleError) as err:
+ collection._get_tar_file_member(tfile, file_does_not_exist)
+
+ assert to_text(err.value.message) == "Collection tar at '%s' does not contain the expected file '%s'." % (to_text(tfile.name), file_does_not_exist)
+
+
+def test_get_tar_file_hash(tmp_tarfile):
+ temp_dir, tfile, filename, checksum = tmp_tarfile
+
+ assert checksum == collection._get_tar_file_hash(tfile.name, filename)
+
+
+def test_get_json_from_tar_file(tmp_tarfile):
+ temp_dir, tfile, filename, checksum = tmp_tarfile
+
+ assert 'MANIFEST.json' in tfile.getnames()
+
+ data = collection._get_json_from_tar_file(tfile.name, 'MANIFEST.json')
+
+ assert isinstance(data, dict)
diff --git a/test/units/galaxy/test_collection_install.py b/test/units/galaxy/test_collection_install.py
new file mode 100644
index 0000000..2118f0e
--- /dev/null
+++ b/test/units/galaxy/test_collection_install.py
@@ -0,0 +1,1081 @@
+# -*- 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)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import copy
+import json
+import os
+import pytest
+import re
+import shutil
+import stat
+import tarfile
+import yaml
+
+from io import BytesIO, StringIO
+from unittest.mock import MagicMock, patch
+from unittest import mock
+
+import ansible.module_utils.six.moves.urllib.error as urllib_error
+
+from ansible import context
+from ansible.cli.galaxy import GalaxyCLI
+from ansible.errors import AnsibleError
+from ansible.galaxy import collection, api, dependency_resolution
+from ansible.galaxy.dependency_resolution.dataclasses import Candidate, Requirement
+from ansible.module_utils._text import to_bytes, to_native, to_text
+from ansible.module_utils.common.process import get_bin_path
+from ansible.utils import context_objects as co
+from ansible.utils.display import Display
+
+
+class RequirementCandidates():
+ def __init__(self):
+ self.candidates = []
+
+ def func_wrapper(self, func):
+ def run(*args, **kwargs):
+ self.candidates = func(*args, **kwargs)
+ return self.candidates
+ return run
+
+
+def call_galaxy_cli(args):
+ orig = co.GlobalCLIArgs._Singleton__instance
+ co.GlobalCLIArgs._Singleton__instance = None
+ try:
+ GalaxyCLI(args=['ansible-galaxy', 'collection'] + args).run()
+ finally:
+ co.GlobalCLIArgs._Singleton__instance = orig
+
+
+def artifact_json(namespace, name, version, dependencies, server):
+ json_str = json.dumps({
+ 'artifact': {
+ 'filename': '%s-%s-%s.tar.gz' % (namespace, name, version),
+ 'sha256': '2d76f3b8c4bab1072848107fb3914c345f71a12a1722f25c08f5d3f51f4ab5fd',
+ 'size': 1234,
+ },
+ 'download_url': '%s/download/%s-%s-%s.tar.gz' % (server, namespace, name, version),
+ 'metadata': {
+ 'namespace': namespace,
+ 'name': name,
+ 'dependencies': dependencies,
+ },
+ 'version': version
+ })
+ return to_text(json_str)
+
+
+def artifact_versions_json(namespace, name, versions, galaxy_api, available_api_versions=None):
+ results = []
+ available_api_versions = available_api_versions or {}
+ api_version = 'v2'
+ if 'v3' in available_api_versions:
+ api_version = 'v3'
+ for version in versions:
+ results.append({
+ 'href': '%s/api/%s/%s/%s/versions/%s/' % (galaxy_api.api_server, api_version, namespace, name, version),
+ 'version': version,
+ })
+
+ if api_version == 'v2':
+ json_str = json.dumps({
+ 'count': len(versions),
+ 'next': None,
+ 'previous': None,
+ 'results': results
+ })
+
+ if api_version == 'v3':
+ response = {'meta': {'count': len(versions)},
+ 'data': results,
+ 'links': {'first': None,
+ 'last': None,
+ 'next': None,
+ 'previous': None},
+ }
+ json_str = json.dumps(response)
+ return to_text(json_str)
+
+
+def error_json(galaxy_api, errors_to_return=None, available_api_versions=None):
+ errors_to_return = errors_to_return or []
+ available_api_versions = available_api_versions or {}
+
+ response = {}
+
+ api_version = 'v2'
+ if 'v3' in available_api_versions:
+ api_version = 'v3'
+
+ if api_version == 'v2':
+ assert len(errors_to_return) <= 1
+ if errors_to_return:
+ response = errors_to_return[0]
+
+ if api_version == 'v3':
+ response['errors'] = errors_to_return
+
+ json_str = json.dumps(response)
+ return to_text(json_str)
+
+
+@pytest.fixture(autouse='function')
+def reset_cli_args():
+ co.GlobalCLIArgs._Singleton__instance = None
+ yield
+ co.GlobalCLIArgs._Singleton__instance = None
+
+
+@pytest.fixture()
+def collection_artifact(request, tmp_path_factory):
+ test_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ namespace = 'ansible_namespace'
+ collection = 'collection'
+
+ skeleton_path = os.path.join(os.path.dirname(os.path.split(__file__)[0]), 'cli', 'test_data', 'collection_skeleton')
+ collection_path = os.path.join(test_dir, namespace, collection)
+
+ call_galaxy_cli(['init', '%s.%s' % (namespace, collection), '-c', '--init-path', test_dir,
+ '--collection-skeleton', skeleton_path])
+ dependencies = getattr(request, 'param', {})
+
+ galaxy_yml = os.path.join(collection_path, 'galaxy.yml')
+ with open(galaxy_yml, 'rb+') as galaxy_obj:
+ existing_yaml = yaml.safe_load(galaxy_obj)
+ existing_yaml['dependencies'] = dependencies
+
+ galaxy_obj.seek(0)
+ galaxy_obj.write(to_bytes(yaml.safe_dump(existing_yaml)))
+ galaxy_obj.truncate()
+
+ # Create a file with +x in the collection so we can test the permissions
+ execute_path = os.path.join(collection_path, 'runme.sh')
+ with open(execute_path, mode='wb') as fd:
+ fd.write(b"echo hi")
+ os.chmod(execute_path, os.stat(execute_path).st_mode | stat.S_IEXEC)
+
+ call_galaxy_cli(['build', collection_path, '--output-path', test_dir])
+
+ collection_tar = os.path.join(test_dir, '%s-%s-0.1.0.tar.gz' % (namespace, collection))
+ return to_bytes(collection_path), to_bytes(collection_tar)
+
+
+@pytest.fixture()
+def galaxy_server():
+ context.CLIARGS._store = {'ignore_certs': False}
+ galaxy_api = api.GalaxyAPI(None, 'test_server', 'https://galaxy.ansible.com')
+ galaxy_api.get_collection_signatures = MagicMock(return_value=[])
+ return galaxy_api
+
+
+def test_concrete_artifact_manager_scm_no_executable(monkeypatch):
+ url = 'https://github.com/org/repo'
+ version = 'commitish'
+ mock_subprocess_check_call = MagicMock()
+ monkeypatch.setattr(collection.concrete_artifact_manager.subprocess, 'check_call', mock_subprocess_check_call)
+ mock_mkdtemp = MagicMock(return_value='')
+ monkeypatch.setattr(collection.concrete_artifact_manager, 'mkdtemp', mock_mkdtemp)
+ mock_get_bin_path = MagicMock(side_effect=[ValueError('Failed to find required executable')])
+ monkeypatch.setattr(collection.concrete_artifact_manager, 'get_bin_path', mock_get_bin_path)
+
+ error = re.escape(
+ "Could not find git executable to extract the collection from the Git repository `https://github.com/org/repo`"
+ )
+ with pytest.raises(AnsibleError, match=error):
+ collection.concrete_artifact_manager._extract_collection_from_git(url, version, b'path')
+
+
+@pytest.mark.parametrize(
+ 'url,version,trailing_slash',
+ [
+ ('https://github.com/org/repo', 'commitish', False),
+ ('https://github.com/org/repo,commitish', None, False),
+ ('https://github.com/org/repo/,commitish', None, True),
+ ('https://github.com/org/repo#,commitish', None, False),
+ ]
+)
+def test_concrete_artifact_manager_scm_cmd(url, version, trailing_slash, monkeypatch):
+ mock_subprocess_check_call = MagicMock()
+ monkeypatch.setattr(collection.concrete_artifact_manager.subprocess, 'check_call', mock_subprocess_check_call)
+ mock_mkdtemp = MagicMock(return_value='')
+ monkeypatch.setattr(collection.concrete_artifact_manager, 'mkdtemp', mock_mkdtemp)
+
+ collection.concrete_artifact_manager._extract_collection_from_git(url, version, b'path')
+
+ assert mock_subprocess_check_call.call_count == 2
+
+ repo = 'https://github.com/org/repo'
+ if trailing_slash:
+ repo += '/'
+
+ git_executable = get_bin_path('git')
+ clone_cmd = (git_executable, 'clone', repo, '')
+
+ assert mock_subprocess_check_call.call_args_list[0].args[0] == clone_cmd
+ assert mock_subprocess_check_call.call_args_list[1].args[0] == (git_executable, 'checkout', 'commitish')
+
+
+@pytest.mark.parametrize(
+ 'url,version,trailing_slash',
+ [
+ ('https://github.com/org/repo', 'HEAD', False),
+ ('https://github.com/org/repo,HEAD', None, False),
+ ('https://github.com/org/repo/,HEAD', None, True),
+ ('https://github.com/org/repo#,HEAD', None, False),
+ ('https://github.com/org/repo', None, False),
+ ]
+)
+def test_concrete_artifact_manager_scm_cmd_shallow(url, version, trailing_slash, monkeypatch):
+ mock_subprocess_check_call = MagicMock()
+ monkeypatch.setattr(collection.concrete_artifact_manager.subprocess, 'check_call', mock_subprocess_check_call)
+ mock_mkdtemp = MagicMock(return_value='')
+ monkeypatch.setattr(collection.concrete_artifact_manager, 'mkdtemp', mock_mkdtemp)
+
+ collection.concrete_artifact_manager._extract_collection_from_git(url, version, b'path')
+
+ assert mock_subprocess_check_call.call_count == 2
+
+ repo = 'https://github.com/org/repo'
+ if trailing_slash:
+ repo += '/'
+ git_executable = get_bin_path('git')
+ shallow_clone_cmd = (git_executable, 'clone', '--depth=1', repo, '')
+
+ assert mock_subprocess_check_call.call_args_list[0].args[0] == shallow_clone_cmd
+ assert mock_subprocess_check_call.call_args_list[1].args[0] == (git_executable, 'checkout', 'HEAD')
+
+
+def test_build_requirement_from_path(collection_artifact):
+ tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+ actual = Requirement.from_dir_path_as_unknown(collection_artifact[0], concrete_artifact_cm)
+
+ assert actual.namespace == u'ansible_namespace'
+ assert actual.name == u'collection'
+ assert actual.src == collection_artifact[0]
+ assert actual.ver == u'0.1.0'
+
+
+@pytest.mark.parametrize('version', ['1.1.1', '1.1.0', '1.0.0'])
+def test_build_requirement_from_path_with_manifest(version, collection_artifact):
+ manifest_path = os.path.join(collection_artifact[0], b'MANIFEST.json')
+ manifest_value = json.dumps({
+ 'collection_info': {
+ 'namespace': 'namespace',
+ 'name': 'name',
+ 'version': version,
+ 'dependencies': {
+ 'ansible_namespace.collection': '*'
+ }
+ }
+ })
+ with open(manifest_path, 'wb') as manifest_obj:
+ manifest_obj.write(to_bytes(manifest_value))
+
+ tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+ actual = Requirement.from_dir_path_as_unknown(collection_artifact[0], concrete_artifact_cm)
+
+ # While the folder name suggests a different collection, we treat MANIFEST.json as the source of truth.
+ assert actual.namespace == u'namespace'
+ assert actual.name == u'name'
+ assert actual.src == collection_artifact[0]
+ assert actual.ver == to_text(version)
+
+
+def test_build_requirement_from_path_invalid_manifest(collection_artifact):
+ manifest_path = os.path.join(collection_artifact[0], b'MANIFEST.json')
+ with open(manifest_path, 'wb') as manifest_obj:
+ manifest_obj.write(b"not json")
+
+ tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+
+ expected = "Collection tar file member MANIFEST.json does not contain a valid json string."
+ with pytest.raises(AnsibleError, match=expected):
+ Requirement.from_dir_path_as_unknown(collection_artifact[0], concrete_artifact_cm)
+
+
+def test_build_artifact_from_path_no_version(collection_artifact, monkeypatch):
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ # a collection artifact should always contain a valid version
+ manifest_path = os.path.join(collection_artifact[0], b'MANIFEST.json')
+ manifest_value = json.dumps({
+ 'collection_info': {
+ 'namespace': 'namespace',
+ 'name': 'name',
+ 'version': '',
+ 'dependencies': {}
+ }
+ })
+ with open(manifest_path, 'wb') as manifest_obj:
+ manifest_obj.write(to_bytes(manifest_value))
+
+ tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+
+ expected = (
+ '^Collection metadata file `.*` at `.*` is expected to have a valid SemVer '
+ 'version value but got {empty_unicode_string!r}$'.
+ format(empty_unicode_string=u'')
+ )
+ with pytest.raises(AnsibleError, match=expected):
+ Requirement.from_dir_path_as_unknown(collection_artifact[0], concrete_artifact_cm)
+
+
+def test_build_requirement_from_path_no_version(collection_artifact, monkeypatch):
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ # version may be falsey/arbitrary strings for collections in development
+ manifest_path = os.path.join(collection_artifact[0], b'galaxy.yml')
+ metadata = {
+ 'authors': ['Ansible'],
+ 'readme': 'README.md',
+ 'namespace': 'namespace',
+ 'name': 'name',
+ 'version': '',
+ 'dependencies': {},
+ }
+ with open(manifest_path, 'wb') as manifest_obj:
+ manifest_obj.write(to_bytes(yaml.safe_dump(metadata)))
+
+ tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+ actual = Requirement.from_dir_path_as_unknown(collection_artifact[0], concrete_artifact_cm)
+
+ # While the folder name suggests a different collection, we treat MANIFEST.json as the source of truth.
+ assert actual.namespace == u'namespace'
+ assert actual.name == u'name'
+ assert actual.src == collection_artifact[0]
+ assert actual.ver == u'*'
+
+
+def test_build_requirement_from_tar(collection_artifact):
+ tmp_path = os.path.join(os.path.split(collection_artifact[1])[0], b'temp')
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(tmp_path, validate_certs=False)
+
+ actual = Requirement.from_requirement_dict({'name': to_text(collection_artifact[1])}, concrete_artifact_cm)
+
+ assert actual.namespace == u'ansible_namespace'
+ assert actual.name == u'collection'
+ assert actual.src == to_text(collection_artifact[1])
+ assert actual.ver == u'0.1.0'
+
+
+def test_build_requirement_from_tar_fail_not_tar(tmp_path_factory):
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ test_file = os.path.join(test_dir, b'fake.tar.gz')
+ with open(test_file, 'wb') as test_obj:
+ test_obj.write(b"\x00\x01\x02\x03")
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ expected = "Collection artifact at '%s' is not a valid tar file." % to_native(test_file)
+ with pytest.raises(AnsibleError, match=expected):
+ Requirement.from_requirement_dict({'name': to_text(test_file)}, concrete_artifact_cm)
+
+
+def test_build_requirement_from_tar_no_manifest(tmp_path_factory):
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+
+ json_data = to_bytes(json.dumps(
+ {
+ 'files': [],
+ 'format': 1,
+ }
+ ))
+
+ tar_path = os.path.join(test_dir, b'ansible-collections.tar.gz')
+ with tarfile.open(tar_path, 'w:gz') as tfile:
+ b_io = BytesIO(json_data)
+ tar_info = tarfile.TarInfo('FILES.json')
+ tar_info.size = len(json_data)
+ tar_info.mode = 0o0644
+ tfile.addfile(tarinfo=tar_info, fileobj=b_io)
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ expected = "Collection at '%s' does not contain the required file MANIFEST.json." % to_native(tar_path)
+ with pytest.raises(AnsibleError, match=expected):
+ Requirement.from_requirement_dict({'name': to_text(tar_path)}, concrete_artifact_cm)
+
+
+def test_build_requirement_from_tar_no_files(tmp_path_factory):
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+
+ json_data = to_bytes(json.dumps(
+ {
+ 'collection_info': {},
+ }
+ ))
+
+ tar_path = os.path.join(test_dir, b'ansible-collections.tar.gz')
+ with tarfile.open(tar_path, 'w:gz') as tfile:
+ b_io = BytesIO(json_data)
+ tar_info = tarfile.TarInfo('MANIFEST.json')
+ tar_info.size = len(json_data)
+ tar_info.mode = 0o0644
+ tfile.addfile(tarinfo=tar_info, fileobj=b_io)
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+ with pytest.raises(KeyError, match='namespace'):
+ Requirement.from_requirement_dict({'name': to_text(tar_path)}, concrete_artifact_cm)
+
+
+def test_build_requirement_from_tar_invalid_manifest(tmp_path_factory):
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+
+ json_data = b"not a json"
+
+ tar_path = os.path.join(test_dir, b'ansible-collections.tar.gz')
+ with tarfile.open(tar_path, 'w:gz') as tfile:
+ b_io = BytesIO(json_data)
+ tar_info = tarfile.TarInfo('MANIFEST.json')
+ tar_info.size = len(json_data)
+ tar_info.mode = 0o0644
+ tfile.addfile(tarinfo=tar_info, fileobj=b_io)
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ expected = "Collection tar file member MANIFEST.json does not contain a valid json string."
+ with pytest.raises(AnsibleError, match=expected):
+ Requirement.from_requirement_dict({'name': to_text(tar_path)}, concrete_artifact_cm)
+
+
+def test_build_requirement_from_name(galaxy_server, monkeypatch, tmp_path_factory):
+ mock_get_versions = MagicMock()
+ mock_get_versions.return_value = ['2.1.9', '2.1.10']
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions)
+
+ mock_version_metadata = MagicMock(
+ namespace='namespace', name='collection',
+ version='2.1.10', artifact_sha256='', dependencies={}
+ )
+ monkeypatch.setattr(api.GalaxyAPI, 'get_collection_version_metadata', mock_version_metadata)
+
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ collections = ['namespace.collection']
+ requirements_file = None
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', collections[0]])
+ requirements = cli._require_one_of_collections_requirements(
+ collections, requirements_file, artifacts_manager=concrete_artifact_cm
+ )['collections']
+ actual = collection._resolve_depenency_map(
+ requirements, [galaxy_server], concrete_artifact_cm, None, True, False, False, False, False
+ )['namespace.collection']
+
+ assert actual.namespace == u'namespace'
+ assert actual.name == u'collection'
+ assert actual.ver == u'2.1.10'
+ assert actual.src == galaxy_server
+
+ assert mock_get_versions.call_count == 1
+ assert mock_get_versions.mock_calls[0][1] == ('namespace', 'collection')
+
+
+def test_build_requirement_from_name_with_prerelease(galaxy_server, monkeypatch, tmp_path_factory):
+ mock_get_versions = MagicMock()
+ mock_get_versions.return_value = ['1.0.1', '2.0.1-beta.1', '2.0.1']
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions)
+
+ mock_get_info = MagicMock()
+ mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '2.0.1', None, None, {}, None, None)
+ monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info)
+
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection'])
+ requirements = cli._require_one_of_collections_requirements(
+ ['namespace.collection'], None, artifacts_manager=concrete_artifact_cm
+ )['collections']
+ actual = collection._resolve_depenency_map(
+ requirements, [galaxy_server], concrete_artifact_cm, None, True, False, False, False, False
+ )['namespace.collection']
+
+ assert actual.namespace == u'namespace'
+ assert actual.name == u'collection'
+ assert actual.src == galaxy_server
+ assert actual.ver == u'2.0.1'
+
+ assert mock_get_versions.call_count == 1
+ assert mock_get_versions.mock_calls[0][1] == ('namespace', 'collection')
+
+
+def test_build_requirment_from_name_with_prerelease_explicit(galaxy_server, monkeypatch, tmp_path_factory):
+ mock_get_versions = MagicMock()
+ mock_get_versions.return_value = ['1.0.1', '2.0.1-beta.1', '2.0.1']
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions)
+
+ mock_get_info = MagicMock()
+ mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '2.0.1-beta.1', None, None,
+ {}, None, None)
+ monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info)
+
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:2.0.1-beta.1'])
+ requirements = cli._require_one_of_collections_requirements(
+ ['namespace.collection:2.0.1-beta.1'], None, artifacts_manager=concrete_artifact_cm
+ )['collections']
+ actual = collection._resolve_depenency_map(
+ requirements, [galaxy_server], concrete_artifact_cm, None, True, False, False, False, False
+ )['namespace.collection']
+
+ assert actual.namespace == u'namespace'
+ assert actual.name == u'collection'
+ assert actual.src == galaxy_server
+ assert actual.ver == u'2.0.1-beta.1'
+
+ assert mock_get_info.call_count == 1
+ assert mock_get_info.mock_calls[0][1] == ('namespace', 'collection', '2.0.1-beta.1')
+
+
+def test_build_requirement_from_name_second_server(galaxy_server, monkeypatch, tmp_path_factory):
+ mock_get_versions = MagicMock()
+ mock_get_versions.return_value = ['1.0.1', '1.0.2', '1.0.3']
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions)
+
+ mock_get_info = MagicMock()
+ mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '1.0.3', None, None, {}, None, None)
+ monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info)
+
+ broken_server = copy.copy(galaxy_server)
+ broken_server.api_server = 'https://broken.com/'
+ mock_version_list = MagicMock()
+ mock_version_list.return_value = []
+ monkeypatch.setattr(broken_server, 'get_collection_versions', mock_version_list)
+
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:>1.0.1'])
+ requirements = cli._require_one_of_collections_requirements(
+ ['namespace.collection:>1.0.1'], None, artifacts_manager=concrete_artifact_cm
+ )['collections']
+ actual = collection._resolve_depenency_map(
+ requirements, [broken_server, galaxy_server], concrete_artifact_cm, None, True, False, False, False, False
+ )['namespace.collection']
+
+ assert actual.namespace == u'namespace'
+ assert actual.name == u'collection'
+ assert actual.src == galaxy_server
+ assert actual.ver == u'1.0.3'
+
+ assert mock_version_list.call_count == 1
+ assert mock_version_list.mock_calls[0][1] == ('namespace', 'collection')
+
+ assert mock_get_versions.call_count == 1
+ assert mock_get_versions.mock_calls[0][1] == ('namespace', 'collection')
+
+
+def test_build_requirement_from_name_missing(galaxy_server, monkeypatch, tmp_path_factory):
+ mock_open = MagicMock()
+ mock_open.return_value = []
+
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_open)
+
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:>1.0.1'])
+ requirements = cli._require_one_of_collections_requirements(
+ ['namespace.collection'], None, artifacts_manager=concrete_artifact_cm
+ )['collections']
+
+ expected = "Failed to resolve the requested dependencies map. Could not satisfy the following requirements:\n* namespace.collection:* (direct request)"
+ with pytest.raises(AnsibleError, match=re.escape(expected)):
+ collection._resolve_depenency_map(requirements, [galaxy_server, galaxy_server], concrete_artifact_cm, None, False, True, False, False, False)
+
+
+def test_build_requirement_from_name_401_unauthorized(galaxy_server, monkeypatch, tmp_path_factory):
+ mock_open = MagicMock()
+ mock_open.side_effect = api.GalaxyError(urllib_error.HTTPError('https://galaxy.server.com', 401, 'msg', {},
+ StringIO()), "error")
+
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_open)
+
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:>1.0.1'])
+ requirements = cli._require_one_of_collections_requirements(
+ ['namespace.collection'], None, artifacts_manager=concrete_artifact_cm
+ )['collections']
+
+ expected = "error (HTTP Code: 401, Message: msg)"
+ with pytest.raises(api.GalaxyError, match=re.escape(expected)):
+ collection._resolve_depenency_map(requirements, [galaxy_server, galaxy_server], concrete_artifact_cm, None, False, False, False, False, False)
+
+
+def test_build_requirement_from_name_single_version(galaxy_server, monkeypatch, tmp_path_factory):
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+ multi_api_proxy = collection.galaxy_api_proxy.MultiGalaxyAPIProxy([galaxy_server], concrete_artifact_cm)
+ dep_provider = dependency_resolution.providers.CollectionDependencyProvider(apis=multi_api_proxy, concrete_artifacts_manager=concrete_artifact_cm)
+
+ matches = RequirementCandidates()
+ mock_find_matches = MagicMock(side_effect=matches.func_wrapper(dep_provider.find_matches), autospec=True)
+ monkeypatch.setattr(dependency_resolution.providers.CollectionDependencyProvider, 'find_matches', mock_find_matches)
+
+ mock_get_versions = MagicMock()
+ mock_get_versions.return_value = ['2.0.0']
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions)
+
+ mock_get_info = MagicMock()
+ mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '2.0.0', None, None,
+ {}, None, None)
+ monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:==2.0.0'])
+ requirements = cli._require_one_of_collections_requirements(
+ ['namespace.collection:==2.0.0'], None, artifacts_manager=concrete_artifact_cm
+ )['collections']
+
+ actual = collection._resolve_depenency_map(
+ requirements, [galaxy_server], concrete_artifact_cm, None, False, True, False, False, False)['namespace.collection']
+
+ assert actual.namespace == u'namespace'
+ assert actual.name == u'collection'
+ assert actual.src == galaxy_server
+ assert actual.ver == u'2.0.0'
+ assert [c.ver for c in matches.candidates] == [u'2.0.0']
+
+ assert mock_get_info.call_count == 1
+ assert mock_get_info.mock_calls[0][1] == ('namespace', 'collection', '2.0.0')
+
+
+def test_build_requirement_from_name_multiple_versions_one_match(galaxy_server, monkeypatch, tmp_path_factory):
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+ multi_api_proxy = collection.galaxy_api_proxy.MultiGalaxyAPIProxy([galaxy_server], concrete_artifact_cm)
+ dep_provider = dependency_resolution.providers.CollectionDependencyProvider(apis=multi_api_proxy, concrete_artifacts_manager=concrete_artifact_cm)
+
+ matches = RequirementCandidates()
+ mock_find_matches = MagicMock(side_effect=matches.func_wrapper(dep_provider.find_matches), autospec=True)
+ monkeypatch.setattr(dependency_resolution.providers.CollectionDependencyProvider, 'find_matches', mock_find_matches)
+
+ mock_get_versions = MagicMock()
+ mock_get_versions.return_value = ['2.0.0', '2.0.1', '2.0.2']
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions)
+
+ mock_get_info = MagicMock()
+ mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '2.0.1', None, None,
+ {}, None, None)
+ monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:>=2.0.1,<2.0.2'])
+ requirements = cli._require_one_of_collections_requirements(
+ ['namespace.collection:>=2.0.1,<2.0.2'], None, artifacts_manager=concrete_artifact_cm
+ )['collections']
+
+ actual = collection._resolve_depenency_map(
+ requirements, [galaxy_server], concrete_artifact_cm, None, False, True, False, False, False)['namespace.collection']
+
+ assert actual.namespace == u'namespace'
+ assert actual.name == u'collection'
+ assert actual.src == galaxy_server
+ assert actual.ver == u'2.0.1'
+ assert [c.ver for c in matches.candidates] == [u'2.0.1']
+
+ assert mock_get_versions.call_count == 1
+ assert mock_get_versions.mock_calls[0][1] == ('namespace', 'collection')
+
+ assert mock_get_info.call_count == 1
+ assert mock_get_info.mock_calls[0][1] == ('namespace', 'collection', '2.0.1')
+
+
+def test_build_requirement_from_name_multiple_version_results(galaxy_server, monkeypatch, tmp_path_factory):
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+ multi_api_proxy = collection.galaxy_api_proxy.MultiGalaxyAPIProxy([galaxy_server], concrete_artifact_cm)
+ dep_provider = dependency_resolution.providers.CollectionDependencyProvider(apis=multi_api_proxy, concrete_artifacts_manager=concrete_artifact_cm)
+
+ matches = RequirementCandidates()
+ mock_find_matches = MagicMock(side_effect=matches.func_wrapper(dep_provider.find_matches), autospec=True)
+ monkeypatch.setattr(dependency_resolution.providers.CollectionDependencyProvider, 'find_matches', mock_find_matches)
+
+ mock_get_info = MagicMock()
+ mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '2.0.5', None, None, {}, None, None)
+ monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info)
+
+ mock_get_versions = MagicMock()
+ mock_get_versions.return_value = ['1.0.1', '1.0.2', '1.0.3']
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions)
+
+ mock_get_versions.return_value = ['2.0.0', '2.0.1', '2.0.2', '2.0.3', '2.0.4', '2.0.5']
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:!=2.0.2'])
+ requirements = cli._require_one_of_collections_requirements(
+ ['namespace.collection:!=2.0.2'], None, artifacts_manager=concrete_artifact_cm
+ )['collections']
+
+ actual = collection._resolve_depenency_map(
+ requirements, [galaxy_server], concrete_artifact_cm, None, False, True, False, False, False)['namespace.collection']
+
+ assert actual.namespace == u'namespace'
+ assert actual.name == u'collection'
+ assert actual.src == galaxy_server
+ assert actual.ver == u'2.0.5'
+ # should be ordered latest to earliest
+ assert [c.ver for c in matches.candidates] == [u'2.0.5', u'2.0.4', u'2.0.3', u'2.0.1', u'2.0.0']
+
+ assert mock_get_versions.call_count == 1
+ assert mock_get_versions.mock_calls[0][1] == ('namespace', 'collection')
+
+
+def test_candidate_with_conflict(monkeypatch, tmp_path_factory, galaxy_server):
+
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ mock_get_info = MagicMock()
+ mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '2.0.5', None, None, {}, None, None)
+ monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info)
+
+ mock_get_versions = MagicMock()
+ mock_get_versions.return_value = ['2.0.5']
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection:!=2.0.5'])
+ requirements = cli._require_one_of_collections_requirements(
+ ['namespace.collection:!=2.0.5'], None, artifacts_manager=concrete_artifact_cm
+ )['collections']
+
+ expected = "Failed to resolve the requested dependencies map. Could not satisfy the following requirements:\n"
+ expected += "* namespace.collection:!=2.0.5 (direct request)"
+ with pytest.raises(AnsibleError, match=re.escape(expected)):
+ collection._resolve_depenency_map(requirements, [galaxy_server], concrete_artifact_cm, None, False, True, False, False, False)
+
+
+def test_dep_candidate_with_conflict(monkeypatch, tmp_path_factory, galaxy_server):
+ test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections Input'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ mock_get_info_return = [
+ api.CollectionVersionMetadata('parent', 'collection', '2.0.5', None, None, {'namespace.collection': '!=1.0.0'}, None, None),
+ api.CollectionVersionMetadata('namespace', 'collection', '1.0.0', None, None, {}, None, None),
+ ]
+ mock_get_info = MagicMock(side_effect=mock_get_info_return)
+ monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info)
+
+ mock_get_versions = MagicMock(side_effect=[['2.0.5'], ['1.0.0']])
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'parent.collection:2.0.5'])
+ requirements = cli._require_one_of_collections_requirements(
+ ['parent.collection:2.0.5'], None, artifacts_manager=concrete_artifact_cm
+ )['collections']
+
+ expected = "Failed to resolve the requested dependencies map. Could not satisfy the following requirements:\n"
+ expected += "* namespace.collection:!=1.0.0 (dependency of parent.collection:2.0.5)"
+ with pytest.raises(AnsibleError, match=re.escape(expected)):
+ collection._resolve_depenency_map(requirements, [galaxy_server], concrete_artifact_cm, None, False, True, False, False, False)
+
+
+def test_install_installed_collection(monkeypatch, tmp_path_factory, galaxy_server):
+
+ mock_installed_collections = MagicMock(return_value=[Candidate('namespace.collection', '1.2.3', None, 'dir', None)])
+
+ monkeypatch.setattr(collection, 'find_existing_collections', mock_installed_collections)
+
+ test_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Collections'))
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(test_dir, validate_certs=False)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ mock_get_info = MagicMock()
+ mock_get_info.return_value = api.CollectionVersionMetadata('namespace', 'collection', '1.2.3', None, None, {}, None, None)
+ monkeypatch.setattr(galaxy_server, 'get_collection_version_metadata', mock_get_info)
+
+ mock_get_versions = MagicMock(return_value=['1.2.3', '1.3.0'])
+ monkeypatch.setattr(galaxy_server, 'get_collection_versions', mock_get_versions)
+
+ cli = GalaxyCLI(args=['ansible-galaxy', 'collection', 'install', 'namespace.collection'])
+ cli.run()
+
+ expected = "Nothing to do. All requested collections are already installed. If you want to reinstall them, consider using `--force`."
+ assert mock_display.mock_calls[1][1][0] == expected
+
+
+def test_install_collection(collection_artifact, monkeypatch):
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ collection_tar = collection_artifact[1]
+
+ temp_path = os.path.join(os.path.split(collection_tar)[0], b'temp')
+ os.makedirs(temp_path)
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
+
+ output_path = os.path.join(os.path.split(collection_tar)[0])
+ collection_path = os.path.join(output_path, b'ansible_namespace', b'collection')
+ os.makedirs(os.path.join(collection_path, b'delete_me')) # Create a folder to verify the install cleans out the dir
+
+ candidate = Candidate('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)
+ collection.install(candidate, to_text(output_path), concrete_artifact_cm)
+
+ # Ensure the temp directory is empty, nothing is left behind
+ assert os.listdir(temp_path) == []
+
+ actual_files = os.listdir(collection_path)
+ actual_files.sort()
+ assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles',
+ b'runme.sh']
+
+ assert stat.S_IMODE(os.stat(os.path.join(collection_path, b'plugins')).st_mode) == 0o0755
+ assert stat.S_IMODE(os.stat(os.path.join(collection_path, b'README.md')).st_mode) == 0o0644
+ assert stat.S_IMODE(os.stat(os.path.join(collection_path, b'runme.sh')).st_mode) == 0o0755
+
+ assert mock_display.call_count == 2
+ assert mock_display.mock_calls[0][1][0] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" \
+ % to_text(collection_path)
+ assert mock_display.mock_calls[1][1][0] == "ansible_namespace.collection:0.1.0 was installed successfully"
+
+
+def test_install_collection_with_download(galaxy_server, collection_artifact, monkeypatch):
+ collection_path, collection_tar = collection_artifact
+ shutil.rmtree(collection_path)
+
+ collections_dir = ('%s' % os.path.sep).join(to_text(collection_path).split('%s' % os.path.sep)[:-2])
+
+ temp_path = os.path.join(os.path.split(collection_tar)[0], b'temp')
+ os.makedirs(temp_path)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
+
+ mock_download = MagicMock()
+ mock_download.return_value = collection_tar
+ monkeypatch.setattr(concrete_artifact_cm, 'get_galaxy_artifact_path', mock_download)
+
+ req = Candidate('ansible_namespace.collection', '0.1.0', 'https://downloadme.com', 'galaxy', None)
+ collection.install(req, to_text(collections_dir), concrete_artifact_cm)
+
+ actual_files = os.listdir(collection_path)
+ actual_files.sort()
+ assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles',
+ b'runme.sh']
+
+ assert mock_display.call_count == 2
+ assert mock_display.mock_calls[0][1][0] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" \
+ % to_text(collection_path)
+ assert mock_display.mock_calls[1][1][0] == "ansible_namespace.collection:0.1.0 was installed successfully"
+
+ assert mock_download.call_count == 1
+ assert mock_download.mock_calls[0][1][0].src == 'https://downloadme.com'
+ assert mock_download.mock_calls[0][1][0].type == 'galaxy'
+
+
+def test_install_collections_from_tar(collection_artifact, monkeypatch):
+ collection_path, collection_tar = collection_artifact
+ temp_path = os.path.split(collection_tar)[0]
+ shutil.rmtree(collection_path)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
+
+ requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)]
+ collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False)
+
+ assert os.path.isdir(collection_path)
+
+ actual_files = os.listdir(collection_path)
+ actual_files.sort()
+ assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles',
+ b'runme.sh']
+
+ with open(os.path.join(collection_path, b'MANIFEST.json'), 'rb') as manifest_obj:
+ actual_manifest = json.loads(to_text(manifest_obj.read()))
+
+ assert actual_manifest['collection_info']['namespace'] == 'ansible_namespace'
+ assert actual_manifest['collection_info']['name'] == 'collection'
+ assert actual_manifest['collection_info']['version'] == '0.1.0'
+
+ # Filter out the progress cursor display calls.
+ display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1]
+ assert len(display_msgs) == 4
+ assert display_msgs[0] == "Process install dependency map"
+ assert display_msgs[1] == "Starting collection install process"
+ assert display_msgs[2] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" % to_text(collection_path)
+
+
+def test_install_collections_existing_without_force(collection_artifact, monkeypatch):
+ collection_path, collection_tar = collection_artifact
+ temp_path = os.path.split(collection_tar)[0]
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
+
+ assert os.path.isdir(collection_path)
+
+ requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)]
+ collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False)
+
+ assert os.path.isdir(collection_path)
+
+ actual_files = os.listdir(collection_path)
+ actual_files.sort()
+ assert actual_files == [b'README.md', b'docs', b'galaxy.yml', b'playbooks', b'plugins', b'roles', b'runme.sh']
+
+ # Filter out the progress cursor display calls.
+ display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1]
+ assert len(display_msgs) == 1
+
+ assert display_msgs[0] == 'Nothing to do. All requested collections are already installed. If you want to reinstall them, consider using `--force`.'
+
+ for msg in display_msgs:
+ assert 'WARNING' not in msg
+
+
+def test_install_missing_metadata_warning(collection_artifact, monkeypatch):
+ collection_path, collection_tar = collection_artifact
+ temp_path = os.path.split(collection_tar)[0]
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ for file in [b'MANIFEST.json', b'galaxy.yml']:
+ b_path = os.path.join(collection_path, file)
+ if os.path.isfile(b_path):
+ os.unlink(b_path)
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
+ requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)]
+ collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False)
+
+ display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1]
+
+ assert 'WARNING' in display_msgs[0]
+
+
+# Makes sure we don't get stuck in some recursive loop
+@pytest.mark.parametrize('collection_artifact', [
+ {'ansible_namespace.collection': '>=0.0.1'},
+], indirect=True)
+def test_install_collection_with_circular_dependency(collection_artifact, monkeypatch):
+ collection_path, collection_tar = collection_artifact
+ temp_path = os.path.split(collection_tar)[0]
+ shutil.rmtree(collection_path)
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, 'display', mock_display)
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
+ requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)]
+ collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False)
+
+ assert os.path.isdir(collection_path)
+
+ actual_files = os.listdir(collection_path)
+ actual_files.sort()
+ assert actual_files == [b'FILES.json', b'MANIFEST.json', b'README.md', b'docs', b'playbooks', b'plugins', b'roles',
+ b'runme.sh']
+
+ with open(os.path.join(collection_path, b'MANIFEST.json'), 'rb') as manifest_obj:
+ actual_manifest = json.loads(to_text(manifest_obj.read()))
+
+ assert actual_manifest['collection_info']['namespace'] == 'ansible_namespace'
+ assert actual_manifest['collection_info']['name'] == 'collection'
+ assert actual_manifest['collection_info']['version'] == '0.1.0'
+ assert actual_manifest['collection_info']['dependencies'] == {'ansible_namespace.collection': '>=0.0.1'}
+
+ # Filter out the progress cursor display calls.
+ display_msgs = [m[1][0] for m in mock_display.mock_calls if 'newline' not in m[2] and len(m[1]) == 1]
+ assert len(display_msgs) == 4
+ assert display_msgs[0] == "Process install dependency map"
+ assert display_msgs[1] == "Starting collection install process"
+ assert display_msgs[2] == "Installing 'ansible_namespace.collection:0.1.0' to '%s'" % to_text(collection_path)
+ assert display_msgs[3] == "ansible_namespace.collection:0.1.0 was installed successfully"
+
+
+@pytest.mark.parametrize('collection_artifact', [
+ None,
+ {},
+], indirect=True)
+def test_install_collection_with_no_dependency(collection_artifact, monkeypatch):
+ collection_path, collection_tar = collection_artifact
+ temp_path = os.path.split(collection_tar)[0]
+ shutil.rmtree(collection_path)
+
+ concrete_artifact_cm = collection.concrete_artifact_manager.ConcreteArtifactsManager(temp_path, validate_certs=False)
+ requirements = [Requirement('ansible_namespace.collection', '0.1.0', to_text(collection_tar), 'file', None)]
+ collection.install_collections(requirements, to_text(temp_path), [], False, False, False, False, False, False, concrete_artifact_cm, True, False)
+
+ assert os.path.isdir(collection_path)
+
+ with open(os.path.join(collection_path, b'MANIFEST.json'), 'rb') as manifest_obj:
+ actual_manifest = json.loads(to_text(manifest_obj.read()))
+
+ assert not actual_manifest['collection_info']['dependencies']
+ assert actual_manifest['collection_info']['namespace'] == 'ansible_namespace'
+ assert actual_manifest['collection_info']['name'] == 'collection'
+ assert actual_manifest['collection_info']['version'] == '0.1.0'
+
+
+@pytest.mark.parametrize(
+ "signatures,required_successful_count,ignore_errors,expected_success",
+ [
+ ([], 'all', [], True),
+ (["good_signature"], 'all', [], True),
+ (["good_signature", collection.gpg.GpgBadArmor(status='failed')], 'all', [], False),
+ ([collection.gpg.GpgBadArmor(status='failed')], 'all', [], False),
+ # This is expected to succeed because ignored does not increment failed signatures.
+ # "all" signatures is not a specific number, so all == no (non-ignored) signatures in this case.
+ ([collection.gpg.GpgBadArmor(status='failed')], 'all', ["BADARMOR"], True),
+ ([collection.gpg.GpgBadArmor(status='failed'), "good_signature"], 'all', ["BADARMOR"], True),
+ ([], '+all', [], False),
+ ([collection.gpg.GpgBadArmor(status='failed')], '+all', ["BADARMOR"], False),
+ ([], '1', [], True),
+ ([], '+1', [], False),
+ (["good_signature"], '2', [], False),
+ (["good_signature", collection.gpg.GpgBadArmor(status='failed')], '2', [], False),
+ # This is expected to fail because ignored does not increment successful signatures.
+ # 2 signatures are required, but only 1 is successful.
+ (["good_signature", collection.gpg.GpgBadArmor(status='failed')], '2', ["BADARMOR"], False),
+ (["good_signature", "good_signature"], '2', [], True),
+ ]
+)
+def test_verify_file_signatures(signatures, required_successful_count, ignore_errors, expected_success):
+ # type: (List[bool], int, bool, bool) -> None
+
+ def gpg_error_generator(results):
+ for result in results:
+ if isinstance(result, collection.gpg.GpgBaseError):
+ yield result
+
+ fqcn = 'ns.coll'
+ manifest_file = 'MANIFEST.json'
+ keyring = '~/.ansible/pubring.kbx'
+
+ with patch.object(collection, 'run_gpg_verify', MagicMock(return_value=("somestdout", 0,))):
+ with patch.object(collection, 'parse_gpg_errors', MagicMock(return_value=gpg_error_generator(signatures))):
+ assert collection.verify_file_signatures(
+ fqcn,
+ manifest_file,
+ signatures,
+ keyring,
+ required_successful_count,
+ ignore_errors
+ ) == expected_success
diff --git a/test/units/galaxy/test_role_install.py b/test/units/galaxy/test_role_install.py
new file mode 100644
index 0000000..687fcac
--- /dev/null
+++ b/test/units/galaxy/test_role_install.py
@@ -0,0 +1,152 @@
+# -*- 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)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+import os
+import functools
+import pytest
+import tempfile
+
+from io import StringIO
+from ansible import context
+from ansible.cli.galaxy import GalaxyCLI
+from ansible.galaxy import api, role, Galaxy
+from ansible.module_utils._text import to_text
+from ansible.utils import context_objects as co
+
+
+def call_galaxy_cli(args):
+ orig = co.GlobalCLIArgs._Singleton__instance
+ co.GlobalCLIArgs._Singleton__instance = None
+ try:
+ GalaxyCLI(args=['ansible-galaxy', 'role'] + args).run()
+ finally:
+ co.GlobalCLIArgs._Singleton__instance = orig
+
+
+@pytest.fixture(autouse='function')
+def reset_cli_args():
+ co.GlobalCLIArgs._Singleton__instance = None
+ yield
+ co.GlobalCLIArgs._Singleton__instance = None
+
+
+@pytest.fixture(autouse=True)
+def galaxy_server():
+ context.CLIARGS._store = {'ignore_certs': False}
+ galaxy_api = api.GalaxyAPI(None, 'test_server', 'https://galaxy.ansible.com')
+ return galaxy_api
+
+
+@pytest.fixture(autouse=True)
+def init_role_dir(tmp_path_factory):
+ test_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Roles Input'))
+ namespace = 'ansible_namespace'
+ role = 'role'
+ skeleton_path = os.path.join(os.path.dirname(os.path.split(__file__)[0]), 'cli', 'test_data', 'role_skeleton')
+ call_galaxy_cli(['init', '%s.%s' % (namespace, role), '-c', '--init-path', test_dir, '--role-skeleton', skeleton_path])
+
+
+def mock_NamedTemporaryFile(mocker, **args):
+ mock_ntf = mocker.MagicMock()
+ mock_ntf.write = mocker.MagicMock()
+ mock_ntf.close = mocker.MagicMock()
+ mock_ntf.name = None
+ return mock_ntf
+
+
+@pytest.fixture
+def init_mock_temp_file(mocker, monkeypatch):
+ monkeypatch.setattr(tempfile, 'NamedTemporaryFile', functools.partial(mock_NamedTemporaryFile, mocker))
+
+
+@pytest.fixture(autouse=True)
+def mock_role_download_api(mocker, monkeypatch):
+ mock_role_api = mocker.MagicMock()
+ mock_role_api.side_effect = [
+ StringIO(u''),
+ ]
+ monkeypatch.setattr(role, 'open_url', mock_role_api)
+ return mock_role_api
+
+
+def test_role_download_github(init_mock_temp_file, mocker, galaxy_server, mock_role_download_api, monkeypatch):
+ mock_api = mocker.MagicMock()
+ mock_api.side_effect = [
+ StringIO(u'{"available_versions":{"v1":"v1/"}}'),
+ StringIO(u'{"results":[{"id":"123","github_user":"test_owner","github_repo": "test_role"}]}'),
+ StringIO(u'{"results":[{"name": "0.0.1"},{"name": "0.0.2"}]}'),
+ ]
+ monkeypatch.setattr(api, 'open_url', mock_api)
+
+ role.GalaxyRole(Galaxy(), galaxy_server, 'test_owner.test_role', version="0.0.1").install()
+
+ assert mock_role_download_api.call_count == 1
+ assert mock_role_download_api.mock_calls[0][1][0] == 'https://github.com/test_owner/test_role/archive/0.0.1.tar.gz'
+
+
+def test_role_download_github_default_version(init_mock_temp_file, mocker, galaxy_server, mock_role_download_api, monkeypatch):
+ mock_api = mocker.MagicMock()
+ mock_api.side_effect = [
+ StringIO(u'{"available_versions":{"v1":"v1/"}}'),
+ StringIO(u'{"results":[{"id":"123","github_user":"test_owner","github_repo": "test_role"}]}'),
+ StringIO(u'{"results":[{"name": "0.0.1"},{"name": "0.0.2"}]}'),
+ ]
+ monkeypatch.setattr(api, 'open_url', mock_api)
+
+ role.GalaxyRole(Galaxy(), galaxy_server, 'test_owner.test_role').install()
+
+ assert mock_role_download_api.call_count == 1
+ assert mock_role_download_api.mock_calls[0][1][0] == 'https://github.com/test_owner/test_role/archive/0.0.2.tar.gz'
+
+
+def test_role_download_github_no_download_url_for_version(init_mock_temp_file, mocker, galaxy_server, mock_role_download_api, monkeypatch):
+ mock_api = mocker.MagicMock()
+ mock_api.side_effect = [
+ StringIO(u'{"available_versions":{"v1":"v1/"}}'),
+ StringIO(u'{"results":[{"id":"123","github_user":"test_owner","github_repo": "test_role"}]}'),
+ StringIO(u'{"results":[{"name": "0.0.1"},{"name": "0.0.2","download_url":"http://localhost:8080/test_owner/test_role/0.0.2.tar.gz"}]}'),
+ ]
+ monkeypatch.setattr(api, 'open_url', mock_api)
+
+ role.GalaxyRole(Galaxy(), galaxy_server, 'test_owner.test_role', version="0.0.1").install()
+
+ assert mock_role_download_api.call_count == 1
+ assert mock_role_download_api.mock_calls[0][1][0] == 'https://github.com/test_owner/test_role/archive/0.0.1.tar.gz'
+
+
+def test_role_download_url(init_mock_temp_file, mocker, galaxy_server, mock_role_download_api, monkeypatch):
+ mock_api = mocker.MagicMock()
+ mock_api.side_effect = [
+ StringIO(u'{"available_versions":{"v1":"v1/"}}'),
+ StringIO(u'{"results":[{"id":"123","github_user":"test_owner","github_repo": "test_role"}]}'),
+ StringIO(u'{"results":[{"name": "0.0.1","download_url":"http://localhost:8080/test_owner/test_role/0.0.1.tar.gz"},'
+ u'{"name": "0.0.2","download_url":"http://localhost:8080/test_owner/test_role/0.0.2.tar.gz"}]}'),
+ ]
+ monkeypatch.setattr(api, 'open_url', mock_api)
+
+ role.GalaxyRole(Galaxy(), galaxy_server, 'test_owner.test_role', version="0.0.1").install()
+
+ assert mock_role_download_api.call_count == 1
+ assert mock_role_download_api.mock_calls[0][1][0] == 'http://localhost:8080/test_owner/test_role/0.0.1.tar.gz'
+
+
+def test_role_download_url_default_version(init_mock_temp_file, mocker, galaxy_server, mock_role_download_api, monkeypatch):
+ mock_api = mocker.MagicMock()
+ mock_api.side_effect = [
+ StringIO(u'{"available_versions":{"v1":"v1/"}}'),
+ StringIO(u'{"results":[{"id":"123","github_user":"test_owner","github_repo": "test_role"}]}'),
+ StringIO(u'{"results":[{"name": "0.0.1","download_url":"http://localhost:8080/test_owner/test_role/0.0.1.tar.gz"},'
+ u'{"name": "0.0.2","download_url":"http://localhost:8080/test_owner/test_role/0.0.2.tar.gz"}]}'),
+ ]
+ monkeypatch.setattr(api, 'open_url', mock_api)
+
+ role.GalaxyRole(Galaxy(), galaxy_server, 'test_owner.test_role').install()
+
+ assert mock_role_download_api.call_count == 1
+ assert mock_role_download_api.mock_calls[0][1][0] == 'http://localhost:8080/test_owner/test_role/0.0.2.tar.gz'
diff --git a/test/units/galaxy/test_role_requirements.py b/test/units/galaxy/test_role_requirements.py
new file mode 100644
index 0000000..a84bbb5
--- /dev/null
+++ b/test/units/galaxy/test_role_requirements.py
@@ -0,0 +1,88 @@
+# -*- 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)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+import pytest
+from ansible.playbook.role.requirement import RoleRequirement
+
+
+def test_null_role_url():
+ role = RoleRequirement.role_yaml_parse('')
+ assert role['src'] == ''
+ assert role['name'] == ''
+ assert role['scm'] is None
+ assert role['version'] is None
+
+
+def test_git_file_role_url():
+ role = RoleRequirement.role_yaml_parse('git+file:///home/bennojoy/nginx')
+ assert role['src'] == 'file:///home/bennojoy/nginx'
+ assert role['name'] == 'nginx'
+ assert role['scm'] == 'git'
+ assert role['version'] is None
+
+
+def test_https_role_url():
+ role = RoleRequirement.role_yaml_parse('https://github.com/bennojoy/nginx')
+ assert role['src'] == 'https://github.com/bennojoy/nginx'
+ assert role['name'] == 'nginx'
+ assert role['scm'] is None
+ assert role['version'] is None
+
+
+def test_git_https_role_url():
+ role = RoleRequirement.role_yaml_parse('git+https://github.com/geerlingguy/ansible-role-composer.git')
+ assert role['src'] == 'https://github.com/geerlingguy/ansible-role-composer.git'
+ assert role['name'] == 'ansible-role-composer'
+ assert role['scm'] == 'git'
+ assert role['version'] is None
+
+
+def test_git_version_role_url():
+ role = RoleRequirement.role_yaml_parse('git+https://github.com/geerlingguy/ansible-role-composer.git,main')
+ assert role['src'] == 'https://github.com/geerlingguy/ansible-role-composer.git'
+ assert role['name'] == 'ansible-role-composer'
+ assert role['scm'] == 'git'
+ assert role['version'] == 'main'
+
+
+@pytest.mark.parametrize("url", [
+ ('https://some.webserver.example.com/files/main.tar.gz'),
+ ('https://some.webserver.example.com/files/main.tar.bz2'),
+ ('https://some.webserver.example.com/files/main.tar.xz'),
+])
+def test_tar_role_url(url):
+ role = RoleRequirement.role_yaml_parse(url)
+ assert role['src'] == url
+ assert role['name'].startswith('main')
+ assert role['scm'] is None
+ assert role['version'] is None
+
+
+def test_git_ssh_role_url():
+ role = RoleRequirement.role_yaml_parse('git@gitlab.company.com:mygroup/ansible-base.git')
+ assert role['src'] == 'git@gitlab.company.com:mygroup/ansible-base.git'
+ assert role['name'].startswith('ansible-base')
+ assert role['scm'] is None
+ assert role['version'] is None
+
+
+def test_token_role_url():
+ role = RoleRequirement.role_yaml_parse('git+https://gitlab+deploy-token-312644:_aJQ9c3HWzmRR4knBNyx@gitlab.com/akasurde/ansible-demo')
+ assert role['src'] == 'https://gitlab+deploy-token-312644:_aJQ9c3HWzmRR4knBNyx@gitlab.com/akasurde/ansible-demo'
+ assert role['name'].startswith('ansible-demo')
+ assert role['scm'] == 'git'
+ assert role['version'] is None
+
+
+def test_token_new_style_role_url():
+ role = RoleRequirement.role_yaml_parse({"src": "git+https://gitlab+deploy-token-312644:_aJQ9c3HWzmRR4knBNyx@gitlab.com/akasurde/ansible-demo"})
+ assert role['src'] == 'https://gitlab+deploy-token-312644:_aJQ9c3HWzmRR4knBNyx@gitlab.com/akasurde/ansible-demo'
+ assert role['name'].startswith('ansible-demo')
+ assert role['scm'] == 'git'
+ assert role['version'] == ''
diff --git a/test/units/galaxy/test_token.py b/test/units/galaxy/test_token.py
new file mode 100644
index 0000000..24af386
--- /dev/null
+++ b/test/units/galaxy/test_token.py
@@ -0,0 +1,98 @@
+# -*- 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)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import pytest
+from unittest.mock import MagicMock
+
+import ansible.constants as C
+from ansible.cli.galaxy import GalaxyCLI, SERVER_DEF
+from ansible.galaxy.token import GalaxyToken, NoTokenSentinel
+from ansible.module_utils._text import to_bytes, to_text
+
+
+@pytest.fixture()
+def b_token_file(request, tmp_path_factory):
+ b_test_dir = to_bytes(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Token'))
+ b_token_path = os.path.join(b_test_dir, b"token.yml")
+
+ token = getattr(request, 'param', None)
+ if token:
+ with open(b_token_path, 'wb') as token_fd:
+ token_fd.write(b"token: %s" % to_bytes(token))
+
+ orig_token_path = C.GALAXY_TOKEN_PATH
+ C.GALAXY_TOKEN_PATH = to_text(b_token_path)
+ try:
+ yield b_token_path
+ finally:
+ C.GALAXY_TOKEN_PATH = orig_token_path
+
+
+def test_client_id(monkeypatch):
+ monkeypatch.setattr(C, 'GALAXY_SERVER_LIST', ['server1', 'server2'])
+
+ test_server_config = {option[0]: None for option in SERVER_DEF}
+ test_server_config.update(
+ {
+ 'url': 'http://my_galaxy_ng:8000/api/automation-hub/',
+ 'auth_url': 'http://my_keycloak:8080/auth/realms/myco/protocol/openid-connect/token',
+ 'client_id': 'galaxy-ng',
+ 'token': 'access_token',
+ }
+ )
+
+ test_server_default = {option[0]: None for option in SERVER_DEF}
+ test_server_default.update(
+ {
+ 'url': 'https://cloud.redhat.com/api/automation-hub/',
+ 'auth_url': 'https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token',
+ 'token': 'access_token',
+ }
+ )
+
+ get_plugin_options = MagicMock(side_effect=[test_server_config, test_server_default])
+ monkeypatch.setattr(C.config, 'get_plugin_options', get_plugin_options)
+
+ cli_args = [
+ 'ansible-galaxy',
+ 'collection',
+ 'install',
+ 'namespace.collection:1.0.0',
+ ]
+
+ galaxy_cli = GalaxyCLI(args=cli_args)
+ mock_execute_install = MagicMock()
+ monkeypatch.setattr(galaxy_cli, '_execute_install_collection', mock_execute_install)
+ galaxy_cli.run()
+
+ assert galaxy_cli.api_servers[0].token.client_id == 'galaxy-ng'
+ assert galaxy_cli.api_servers[1].token.client_id == 'cloud-services'
+
+
+def test_token_explicit(b_token_file):
+ assert GalaxyToken(token="explicit").get() == "explicit"
+
+
+@pytest.mark.parametrize('b_token_file', ['file'], indirect=True)
+def test_token_explicit_override_file(b_token_file):
+ assert GalaxyToken(token="explicit").get() == "explicit"
+
+
+@pytest.mark.parametrize('b_token_file', ['file'], indirect=True)
+def test_token_from_file(b_token_file):
+ assert GalaxyToken().get() == "file"
+
+
+def test_token_from_file_missing(b_token_file):
+ assert GalaxyToken().get() is None
+
+
+@pytest.mark.parametrize('b_token_file', ['file'], indirect=True)
+def test_token_none(b_token_file):
+ assert GalaxyToken(token=NoTokenSentinel).get() is None
diff --git a/test/units/galaxy/test_user_agent.py b/test/units/galaxy/test_user_agent.py
new file mode 100644
index 0000000..da0103f
--- /dev/null
+++ b/test/units/galaxy/test_user_agent.py
@@ -0,0 +1,18 @@
+# -*- 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
+
+import platform
+
+from ansible.galaxy import user_agent
+from ansible.module_utils.ansible_release import __version__ as ansible_version
+
+
+def test_user_agent():
+ res = user_agent.user_agent()
+ assert res.startswith('ansible-galaxy/%s' % ansible_version)
+ assert platform.system() in res
+ assert 'python:' in res
diff --git a/test/units/inventory/__init__.py b/test/units/inventory/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/inventory/__init__.py
diff --git a/test/units/inventory/test_group.py b/test/units/inventory/test_group.py
new file mode 100644
index 0000000..e8f1c0b
--- /dev/null
+++ b/test/units/inventory/test_group.py
@@ -0,0 +1,155 @@
+# Copyright 2018 Alan Rominger <arominge@redhat.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+
+from ansible.inventory.group import Group
+from ansible.inventory.host import Host
+from ansible.errors import AnsibleError
+
+
+class TestGroup(unittest.TestCase):
+
+ def test_depth_update(self):
+ A = Group('A')
+ B = Group('B')
+ Z = Group('Z')
+ A.add_child_group(B)
+ A.add_child_group(Z)
+ self.assertEqual(A.depth, 0)
+ self.assertEqual(Z.depth, 1)
+ self.assertEqual(B.depth, 1)
+
+ def test_depth_update_dual_branches(self):
+ alpha = Group('alpha')
+ A = Group('A')
+ alpha.add_child_group(A)
+ B = Group('B')
+ A.add_child_group(B)
+ Z = Group('Z')
+ alpha.add_child_group(Z)
+ beta = Group('beta')
+ B.add_child_group(beta)
+ Z.add_child_group(beta)
+
+ self.assertEqual(alpha.depth, 0) # apex
+ self.assertEqual(beta.depth, 3) # alpha -> A -> B -> beta
+
+ omega = Group('omega')
+ omega.add_child_group(alpha)
+
+ # verify that both paths are traversed to get the max depth value
+ self.assertEqual(B.depth, 3) # omega -> alpha -> A -> B
+ self.assertEqual(beta.depth, 4) # B -> beta
+
+ def test_depth_recursion(self):
+ A = Group('A')
+ B = Group('B')
+ A.add_child_group(B)
+ # hypothetical of adding B as child group to A
+ A.parent_groups.append(B)
+ B.child_groups.append(A)
+ # can't update depths of groups, because of loop
+ with self.assertRaises(AnsibleError):
+ B._check_children_depth()
+
+ def test_loop_detection(self):
+ A = Group('A')
+ B = Group('B')
+ C = Group('C')
+ A.add_child_group(B)
+ B.add_child_group(C)
+ with self.assertRaises(AnsibleError):
+ C.add_child_group(A)
+
+ def test_direct_host_ordering(self):
+ """Hosts are returned in order they are added
+ """
+ group = Group('A')
+ # host names not added in alphabetical order
+ host_name_list = ['z', 'b', 'c', 'a', 'p', 'q']
+ expected_hosts = []
+ for host_name in host_name_list:
+ h = Host(host_name)
+ group.add_host(h)
+ expected_hosts.append(h)
+ assert group.get_hosts() == expected_hosts
+
+ def test_sub_group_host_ordering(self):
+ """With multiple nested groups, asserts that hosts are returned
+ in deterministic order
+ """
+ top_group = Group('A')
+ expected_hosts = []
+ for name in ['z', 'b', 'c', 'a', 'p', 'q']:
+ child = Group('group_{0}'.format(name))
+ top_group.add_child_group(child)
+ host = Host('host_{0}'.format(name))
+ child.add_host(host)
+ expected_hosts.append(host)
+ assert top_group.get_hosts() == expected_hosts
+
+ def test_populates_descendant_hosts(self):
+ A = Group('A')
+ B = Group('B')
+ C = Group('C')
+ h = Host('h')
+ C.add_host(h)
+ A.add_child_group(B) # B is child of A
+ B.add_child_group(C) # C is descendant of A
+ A.add_child_group(B)
+ self.assertEqual(set(h.groups), set([C, B, A]))
+ h2 = Host('h2')
+ C.add_host(h2)
+ self.assertEqual(set(h2.groups), set([C, B, A]))
+
+ def test_ancestor_example(self):
+ # see docstring for Group._walk_relationship
+ groups = {}
+ for name in ['A', 'B', 'C', 'D', 'E', 'F']:
+ groups[name] = Group(name)
+ # first row
+ groups['A'].add_child_group(groups['D'])
+ groups['B'].add_child_group(groups['D'])
+ groups['B'].add_child_group(groups['E'])
+ groups['C'].add_child_group(groups['D'])
+ # second row
+ groups['D'].add_child_group(groups['E'])
+ groups['D'].add_child_group(groups['F'])
+ groups['E'].add_child_group(groups['F'])
+
+ self.assertEqual(
+ set(groups['F'].get_ancestors()),
+ set([
+ groups['A'], groups['B'], groups['C'], groups['D'], groups['E']
+ ])
+ )
+
+ def test_ancestors_recursive_loop_safe(self):
+ '''
+ The get_ancestors method may be referenced before circular parenting
+ checks, so the method is expected to be stable even with loops
+ '''
+ A = Group('A')
+ B = Group('B')
+ A.parent_groups.append(B)
+ B.parent_groups.append(A)
+ # finishes in finite time
+ self.assertEqual(A.get_ancestors(), set([A, B]))
diff --git a/test/units/inventory/test_host.py b/test/units/inventory/test_host.py
new file mode 100644
index 0000000..c8f4771
--- /dev/null
+++ b/test/units/inventory/test_host.py
@@ -0,0 +1,112 @@
+# Copyright 2015 Marius Gedminas <marius@gedmin.as>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# for __setstate__/__getstate__ tests
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pickle
+
+from units.compat import unittest
+
+from ansible.inventory.group import Group
+from ansible.inventory.host import Host
+from ansible.module_utils.six import string_types
+
+
+class TestHost(unittest.TestCase):
+ ansible_port = 22
+
+ def setUp(self):
+ self.hostA = Host('a')
+ self.hostB = Host('b')
+
+ def test_equality(self):
+ self.assertEqual(self.hostA, self.hostA)
+ self.assertNotEqual(self.hostA, self.hostB)
+ self.assertNotEqual(self.hostA, Host('a'))
+
+ def test_hashability(self):
+ # equality implies the hash values are the same
+ self.assertEqual(hash(self.hostA), hash(Host('a')))
+
+ def test_get_vars(self):
+ host_vars = self.hostA.get_vars()
+ self.assertIsInstance(host_vars, dict)
+
+ def test_repr(self):
+ host_repr = repr(self.hostA)
+ self.assertIsInstance(host_repr, string_types)
+
+ def test_add_group(self):
+ group = Group('some_group')
+ group_len = len(self.hostA.groups)
+ self.hostA.add_group(group)
+ self.assertEqual(len(self.hostA.groups), group_len + 1)
+
+ def test_get_groups(self):
+ group = Group('some_group')
+ self.hostA.add_group(group)
+ groups = self.hostA.get_groups()
+ self.assertEqual(len(groups), 1)
+ for _group in groups:
+ self.assertIsInstance(_group, Group)
+
+ def test_equals_none(self):
+ other = None
+ self.hostA == other
+ other == self.hostA
+ self.hostA != other
+ other != self.hostA
+ self.assertNotEqual(self.hostA, other)
+
+ def test_serialize(self):
+ group = Group('some_group')
+ self.hostA.add_group(group)
+ data = self.hostA.serialize()
+ self.assertIsInstance(data, dict)
+
+ def test_serialize_then_deserialize(self):
+ group = Group('some_group')
+ self.hostA.add_group(group)
+ hostA_data = self.hostA.serialize()
+
+ hostA_clone = Host()
+ hostA_clone.deserialize(hostA_data)
+ self.assertEqual(self.hostA, hostA_clone)
+
+ def test_set_state(self):
+ group = Group('some_group')
+ self.hostA.add_group(group)
+
+ pickled_hostA = pickle.dumps(self.hostA)
+
+ hostA_clone = pickle.loads(pickled_hostA)
+ self.assertEqual(self.hostA, hostA_clone)
+
+
+class TestHostWithPort(TestHost):
+ ansible_port = 8822
+
+ def setUp(self):
+ self.hostA = Host(name='a', port=self.ansible_port)
+ self.hostB = Host(name='b', port=self.ansible_port)
+
+ def test_get_vars_ansible_port(self):
+ host_vars = self.hostA.get_vars()
+ self.assertEqual(host_vars['ansible_port'], self.ansible_port)
diff --git a/test/units/inventory_test_data/group_vars/noparse/all.yml~ b/test/units/inventory_test_data/group_vars/noparse/all.yml~
new file mode 100644
index 0000000..6f52f11
--- /dev/null
+++ b/test/units/inventory_test_data/group_vars/noparse/all.yml~
@@ -0,0 +1,2 @@
+---
+YAML_FILENAME_EXTENSIONS_TEST: False
diff --git a/test/units/inventory_test_data/group_vars/noparse/file.txt b/test/units/inventory_test_data/group_vars/noparse/file.txt
new file mode 100644
index 0000000..6f52f11
--- /dev/null
+++ b/test/units/inventory_test_data/group_vars/noparse/file.txt
@@ -0,0 +1,2 @@
+---
+YAML_FILENAME_EXTENSIONS_TEST: False
diff --git a/test/units/inventory_test_data/group_vars/parse/all.yml b/test/units/inventory_test_data/group_vars/parse/all.yml
new file mode 100644
index 0000000..8687c86
--- /dev/null
+++ b/test/units/inventory_test_data/group_vars/parse/all.yml
@@ -0,0 +1,2 @@
+---
+YAML_FILENAME_EXTENSIONS_TEST: True
diff --git a/test/units/mock/__init__.py b/test/units/mock/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/mock/__init__.py
diff --git a/test/units/mock/loader.py b/test/units/mock/loader.py
new file mode 100644
index 0000000..f6ceb37
--- /dev/null
+++ b/test/units/mock/loader.py
@@ -0,0 +1,117 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+from ansible.errors import AnsibleParserError
+from ansible.parsing.dataloader import DataLoader
+from ansible.module_utils._text import to_bytes, to_text
+
+
+class DictDataLoader(DataLoader):
+
+ def __init__(self, file_mapping=None):
+ file_mapping = {} if file_mapping is None else file_mapping
+ assert type(file_mapping) == dict
+
+ super(DictDataLoader, self).__init__()
+
+ self._file_mapping = file_mapping
+ self._build_known_directories()
+ self._vault_secrets = None
+
+ def load_from_file(self, path, cache=True, unsafe=False):
+ data = None
+ path = to_text(path)
+ if path in self._file_mapping:
+ data = self.load(self._file_mapping[path], path)
+ return data
+
+ # TODO: the real _get_file_contents returns a bytestring, so we actually convert the
+ # unicode/text it's created with to utf-8
+ def _get_file_contents(self, file_name):
+ path = to_text(file_name)
+ if path in self._file_mapping:
+ return to_bytes(self._file_mapping[file_name]), False
+ else:
+ raise AnsibleParserError("file not found: %s" % file_name)
+
+ def path_exists(self, path):
+ path = to_text(path)
+ return path in self._file_mapping or path in self._known_directories
+
+ def is_file(self, path):
+ path = to_text(path)
+ return path in self._file_mapping
+
+ def is_directory(self, path):
+ path = to_text(path)
+ return path in self._known_directories
+
+ def list_directory(self, path):
+ ret = []
+ path = to_text(path)
+ for x in (list(self._file_mapping.keys()) + self._known_directories):
+ if x.startswith(path):
+ if os.path.dirname(x) == path:
+ ret.append(os.path.basename(x))
+ return ret
+
+ def is_executable(self, path):
+ # FIXME: figure out a way to make paths return true for this
+ return False
+
+ def _add_known_directory(self, directory):
+ if directory not in self._known_directories:
+ self._known_directories.append(directory)
+
+ def _build_known_directories(self):
+ self._known_directories = []
+ for path in self._file_mapping:
+ dirname = os.path.dirname(path)
+ while dirname not in ('/', ''):
+ self._add_known_directory(dirname)
+ dirname = os.path.dirname(dirname)
+
+ def push(self, path, content):
+ rebuild_dirs = False
+ if path not in self._file_mapping:
+ rebuild_dirs = True
+
+ self._file_mapping[path] = content
+
+ if rebuild_dirs:
+ self._build_known_directories()
+
+ def pop(self, path):
+ if path in self._file_mapping:
+ del self._file_mapping[path]
+ self._build_known_directories()
+
+ def clear(self):
+ self._file_mapping = dict()
+ self._known_directories = []
+
+ def get_basedir(self):
+ return os.getcwd()
+
+ def set_vault_secrets(self, vault_secrets):
+ self._vault_secrets = vault_secrets
diff --git a/test/units/mock/path.py b/test/units/mock/path.py
new file mode 100644
index 0000000..c24ddf4
--- /dev/null
+++ b/test/units/mock/path.py
@@ -0,0 +1,8 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from unittest.mock import MagicMock
+from ansible.utils.path import unfrackpath
+
+
+mock_unfrackpath_noop = MagicMock(spec_set=unfrackpath, side_effect=lambda x, *args, **kwargs: x)
diff --git a/test/units/mock/procenv.py b/test/units/mock/procenv.py
new file mode 100644
index 0000000..271a207
--- /dev/null
+++ b/test/units/mock/procenv.py
@@ -0,0 +1,90 @@
+# (c) 2016, Matt Davis <mdavis@ansible.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+import json
+
+from contextlib import contextmanager
+from io import BytesIO, StringIO
+from units.compat import unittest
+from ansible.module_utils.six import PY3
+from ansible.module_utils._text import to_bytes
+
+
+@contextmanager
+def swap_stdin_and_argv(stdin_data='', argv_data=tuple()):
+ """
+ context manager that temporarily masks the test runner's values for stdin and argv
+ """
+ real_stdin = sys.stdin
+ real_argv = sys.argv
+
+ if PY3:
+ fake_stream = StringIO(stdin_data)
+ fake_stream.buffer = BytesIO(to_bytes(stdin_data))
+ else:
+ fake_stream = BytesIO(to_bytes(stdin_data))
+
+ try:
+ sys.stdin = fake_stream
+ sys.argv = argv_data
+
+ yield
+ finally:
+ sys.stdin = real_stdin
+ sys.argv = real_argv
+
+
+@contextmanager
+def swap_stdout():
+ """
+ context manager that temporarily replaces stdout for tests that need to verify output
+ """
+ old_stdout = sys.stdout
+
+ if PY3:
+ fake_stream = StringIO()
+ else:
+ fake_stream = BytesIO()
+
+ try:
+ sys.stdout = fake_stream
+
+ yield fake_stream
+ finally:
+ sys.stdout = old_stdout
+
+
+class ModuleTestCase(unittest.TestCase):
+ def setUp(self, module_args=None):
+ if module_args is None:
+ module_args = {'_ansible_remote_tmp': '/tmp', '_ansible_keep_remote_files': False}
+
+ args = json.dumps(dict(ANSIBLE_MODULE_ARGS=module_args))
+
+ # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually
+ self.stdin_swap = swap_stdin_and_argv(stdin_data=args)
+ self.stdin_swap.__enter__()
+
+ def tearDown(self):
+ # unittest doesn't have a clean place to use a context manager, so we have to enter/exit manually
+ self.stdin_swap.__exit__(None, None, None)
diff --git a/test/units/mock/vault_helper.py b/test/units/mock/vault_helper.py
new file mode 100644
index 0000000..dcce9c7
--- /dev/null
+++ b/test/units/mock/vault_helper.py
@@ -0,0 +1,39 @@
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils._text import to_bytes
+
+from ansible.parsing.vault import VaultSecret
+
+
+class TextVaultSecret(VaultSecret):
+ '''A secret piece of text. ie, a password. Tracks text encoding.
+
+ The text encoding of the text may not be the default text encoding so
+ we keep track of the encoding so we encode it to the same bytes.'''
+
+ def __init__(self, text, encoding=None, errors=None, _bytes=None):
+ super(TextVaultSecret, self).__init__()
+ self.text = text
+ self.encoding = encoding or 'utf-8'
+ self._bytes = _bytes
+ self.errors = errors or 'strict'
+
+ @property
+ def bytes(self):
+ '''The text encoded with encoding, unless we specifically set _bytes.'''
+ return self._bytes or to_bytes(self.text, encoding=self.encoding, errors=self.errors)
diff --git a/test/units/mock/yaml_helper.py b/test/units/mock/yaml_helper.py
new file mode 100644
index 0000000..1ef1721
--- /dev/null
+++ b/test/units/mock/yaml_helper.py
@@ -0,0 +1,124 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import io
+import yaml
+
+from ansible.module_utils.six import PY3
+from ansible.parsing.yaml.loader import AnsibleLoader
+from ansible.parsing.yaml.dumper import AnsibleDumper
+
+
+class YamlTestUtils(object):
+ """Mixin class to combine with a unittest.TestCase subclass."""
+ def _loader(self, stream):
+ """Vault related tests will want to override this.
+
+ Vault cases should setup a AnsibleLoader that has the vault password."""
+ return AnsibleLoader(stream)
+
+ def _dump_stream(self, obj, stream, dumper=None):
+ """Dump to a py2-unicode or py3-string stream."""
+ if PY3:
+ return yaml.dump(obj, stream, Dumper=dumper)
+ else:
+ return yaml.dump(obj, stream, Dumper=dumper, encoding=None)
+
+ def _dump_string(self, obj, dumper=None):
+ """Dump to a py2-unicode or py3-string"""
+ if PY3:
+ return yaml.dump(obj, Dumper=dumper)
+ else:
+ return yaml.dump(obj, Dumper=dumper, encoding=None)
+
+ def _dump_load_cycle(self, obj):
+ # Each pass though a dump or load revs the 'generation'
+ # obj to yaml string
+ string_from_object_dump = self._dump_string(obj, dumper=AnsibleDumper)
+
+ # wrap a stream/file like StringIO around that yaml
+ stream_from_object_dump = io.StringIO(string_from_object_dump)
+ loader = self._loader(stream_from_object_dump)
+ # load the yaml stream to create a new instance of the object (gen 2)
+ obj_2 = loader.get_data()
+
+ # dump the gen 2 objects directory to strings
+ string_from_object_dump_2 = self._dump_string(obj_2,
+ dumper=AnsibleDumper)
+
+ # The gen 1 and gen 2 yaml strings
+ self.assertEqual(string_from_object_dump, string_from_object_dump_2)
+ # the gen 1 (orig) and gen 2 py object
+ self.assertEqual(obj, obj_2)
+
+ # again! gen 3... load strings into py objects
+ stream_3 = io.StringIO(string_from_object_dump_2)
+ loader_3 = self._loader(stream_3)
+ obj_3 = loader_3.get_data()
+
+ string_from_object_dump_3 = self._dump_string(obj_3, dumper=AnsibleDumper)
+
+ self.assertEqual(obj, obj_3)
+ # should be transitive, but...
+ self.assertEqual(obj_2, obj_3)
+ self.assertEqual(string_from_object_dump, string_from_object_dump_3)
+
+ def _old_dump_load_cycle(self, obj):
+ '''Dump the passed in object to yaml, load it back up, dump again, compare.'''
+ stream = io.StringIO()
+
+ yaml_string = self._dump_string(obj, dumper=AnsibleDumper)
+ self._dump_stream(obj, stream, dumper=AnsibleDumper)
+
+ yaml_string_from_stream = stream.getvalue()
+
+ # reset stream
+ stream.seek(0)
+
+ loader = self._loader(stream)
+ # loader = AnsibleLoader(stream, vault_password=self.vault_password)
+ obj_from_stream = loader.get_data()
+
+ stream_from_string = io.StringIO(yaml_string)
+ loader2 = self._loader(stream_from_string)
+ # loader2 = AnsibleLoader(stream_from_string, vault_password=self.vault_password)
+ obj_from_string = loader2.get_data()
+
+ stream_obj_from_stream = io.StringIO()
+ stream_obj_from_string = io.StringIO()
+
+ if PY3:
+ yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper)
+ yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper)
+ else:
+ yaml.dump(obj_from_stream, stream_obj_from_stream, Dumper=AnsibleDumper, encoding=None)
+ yaml.dump(obj_from_stream, stream_obj_from_string, Dumper=AnsibleDumper, encoding=None)
+
+ yaml_string_stream_obj_from_stream = stream_obj_from_stream.getvalue()
+ yaml_string_stream_obj_from_string = stream_obj_from_string.getvalue()
+
+ stream_obj_from_stream.seek(0)
+ stream_obj_from_string.seek(0)
+
+ if PY3:
+ yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper)
+ yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper)
+ else:
+ yaml_string_obj_from_stream = yaml.dump(obj_from_stream, Dumper=AnsibleDumper, encoding=None)
+ yaml_string_obj_from_string = yaml.dump(obj_from_string, Dumper=AnsibleDumper, encoding=None)
+
+ assert yaml_string == yaml_string_obj_from_stream
+ assert yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string
+ assert (yaml_string == yaml_string_obj_from_stream == yaml_string_obj_from_string == yaml_string_stream_obj_from_stream ==
+ yaml_string_stream_obj_from_string)
+ assert obj == obj_from_stream
+ assert obj == obj_from_string
+ assert obj == yaml_string_obj_from_stream
+ assert obj == yaml_string_obj_from_string
+ assert obj == obj_from_stream == obj_from_string == yaml_string_obj_from_stream == yaml_string_obj_from_string
+ return {'obj': obj,
+ 'yaml_string': yaml_string,
+ 'yaml_string_from_stream': yaml_string_from_stream,
+ 'obj_from_stream': obj_from_stream,
+ 'obj_from_string': obj_from_string,
+ 'yaml_string_obj_from_string': yaml_string_obj_from_string}
diff --git a/test/units/module_utils/__init__.py b/test/units/module_utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/__init__.py
diff --git a/test/units/module_utils/basic/__init__.py b/test/units/module_utils/basic/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/basic/__init__.py
diff --git a/test/units/module_utils/basic/test__log_invocation.py b/test/units/module_utils/basic/test__log_invocation.py
new file mode 100644
index 0000000..3beda8b
--- /dev/null
+++ b/test/units/module_utils/basic/test__log_invocation.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# (c) 2016, James Cammarata <jimi@sngx.net>
+# (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
+
+import pytest
+
+
+ARGS = dict(foo=False, bar=[1, 2, 3], bam="bam", baz=u'baz')
+ARGUMENT_SPEC = dict(
+ foo=dict(default=True, type='bool'),
+ bar=dict(default=[], type='list'),
+ bam=dict(default="bam"),
+ baz=dict(default=u"baz"),
+ password=dict(default=True),
+ no_log=dict(default="you shouldn't see me", no_log=True),
+)
+
+
+@pytest.mark.parametrize('am, stdin', [(ARGUMENT_SPEC, ARGS)], indirect=['am', 'stdin'])
+def test_module_utils_basic__log_invocation(am, mocker):
+
+ am.log = mocker.MagicMock()
+ am._log_invocation()
+
+ # Message is generated from a dict so it will be in an unknown order.
+ # have to check this manually rather than with assert_called_with()
+ args = am.log.call_args[0]
+ assert len(args) == 1
+ message = args[0]
+
+ assert len(message) == \
+ len('Invoked with bam=bam bar=[1, 2, 3] foo=False baz=baz no_log=NOT_LOGGING_PARAMETER password=NOT_LOGGING_PASSWORD')
+
+ assert message.startswith('Invoked with ')
+ assert ' bam=bam' in message
+ assert ' bar=[1, 2, 3]' in message
+ assert ' foo=False' in message
+ assert ' baz=baz' in message
+ assert ' no_log=NOT_LOGGING_PARAMETER' in message
+ assert ' password=NOT_LOGGING_PASSWORD' in message
+
+ kwargs = am.log.call_args[1]
+ assert kwargs == \
+ dict(log_args={
+ 'foo': 'False',
+ 'bar': '[1, 2, 3]',
+ 'bam': 'bam',
+ 'baz': 'baz',
+ 'password': 'NOT_LOGGING_PASSWORD',
+ 'no_log': 'NOT_LOGGING_PARAMETER',
+ })
diff --git a/test/units/module_utils/basic/test__symbolic_mode_to_octal.py b/test/units/module_utils/basic/test__symbolic_mode_to_octal.py
new file mode 100644
index 0000000..7793b34
--- /dev/null
+++ b/test/units/module_utils/basic/test__symbolic_mode_to_octal.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# Copyright:
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016-2017 Ansible Project
+# License: 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 pytest
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+#
+# Info helpful for making new test cases:
+#
+# base_mode = {'dir no perms': 0o040000,
+# 'file no perms': 0o100000,
+# 'dir all perms': 0o400000 | 0o777,
+# 'file all perms': 0o100000, | 0o777}
+#
+# perm_bits = {'x': 0b001,
+# 'w': 0b010,
+# 'r': 0b100}
+#
+# role_shift = {'u': 6,
+# 'g': 3,
+# 'o': 0}
+
+DATA = ( # Going from no permissions to setting all for user, group, and/or other
+ (0o040000, u'a+rwx', 0o0777),
+ (0o040000, u'u+rwx,g+rwx,o+rwx', 0o0777),
+ (0o040000, u'o+rwx', 0o0007),
+ (0o040000, u'g+rwx', 0o0070),
+ (0o040000, u'u+rwx', 0o0700),
+
+ # Going from all permissions to none for user, group, and/or other
+ (0o040777, u'a-rwx', 0o0000),
+ (0o040777, u'u-rwx,g-rwx,o-rwx', 0o0000),
+ (0o040777, u'o-rwx', 0o0770),
+ (0o040777, u'g-rwx', 0o0707),
+ (0o040777, u'u-rwx', 0o0077),
+
+ # now using absolute assignment from None to a set of perms
+ (0o040000, u'a=rwx', 0o0777),
+ (0o040000, u'u=rwx,g=rwx,o=rwx', 0o0777),
+ (0o040000, u'o=rwx', 0o0007),
+ (0o040000, u'g=rwx', 0o0070),
+ (0o040000, u'u=rwx', 0o0700),
+
+ # X effect on files and dirs
+ (0o040000, u'a+X', 0o0111),
+ (0o100000, u'a+X', 0),
+ (0o040000, u'a=X', 0o0111),
+ (0o100000, u'a=X', 0),
+ (0o040777, u'a-X', 0o0666),
+ # Same as chmod but is it a bug?
+ # chmod a-X statfile <== removes execute from statfile
+ (0o100777, u'a-X', 0o0666),
+
+ # Multiple permissions
+ (0o040000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0755),
+ (0o100000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0644),
+)
+
+UMASK_DATA = (
+ (0o100000, '+rwx', 0o770),
+ (0o100777, '-rwx', 0o007),
+)
+
+INVALID_DATA = (
+ (0o040000, u'a=foo', "bad symbolic permission for mode: a=foo"),
+ (0o040000, u'f=rwx', "bad symbolic permission for mode: f=rwx"),
+)
+
+
+@pytest.mark.parametrize('stat_info, mode_string, expected', DATA)
+def test_good_symbolic_modes(mocker, stat_info, mode_string, expected):
+ mock_stat = mocker.MagicMock()
+ mock_stat.st_mode = stat_info
+ assert AnsibleModule._symbolic_mode_to_octal(mock_stat, mode_string) == expected
+
+
+@pytest.mark.parametrize('stat_info, mode_string, expected', UMASK_DATA)
+def test_umask_with_symbolic_modes(mocker, stat_info, mode_string, expected):
+ mock_umask = mocker.patch('os.umask')
+ mock_umask.return_value = 0o7
+
+ mock_stat = mocker.MagicMock()
+ mock_stat.st_mode = stat_info
+
+ assert AnsibleModule._symbolic_mode_to_octal(mock_stat, mode_string) == expected
+
+
+@pytest.mark.parametrize('stat_info, mode_string, expected', INVALID_DATA)
+def test_invalid_symbolic_modes(mocker, stat_info, mode_string, expected):
+ mock_stat = mocker.MagicMock()
+ mock_stat.st_mode = stat_info
+ with pytest.raises(ValueError) as exc:
+ assert AnsibleModule._symbolic_mode_to_octal(mock_stat, mode_string) == 'blah'
+ assert exc.match(expected)
diff --git a/test/units/module_utils/basic/test_argument_spec.py b/test/units/module_utils/basic/test_argument_spec.py
new file mode 100644
index 0000000..211d65a
--- /dev/null
+++ b/test/units/module_utils/basic/test_argument_spec.py
@@ -0,0 +1,724 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# Copyright: 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 json
+import os
+
+import pytest
+
+from units.compat.mock import MagicMock
+from ansible.module_utils import basic
+from ansible.module_utils.api import basic_auth_argument_spec, rate_limit_argument_spec, retry_argument_spec
+from ansible.module_utils.common import warnings
+from ansible.module_utils.common.warnings import get_deprecation_messages, get_warning_messages
+from ansible.module_utils.six import integer_types, string_types
+from ansible.module_utils.six.moves import builtins
+
+
+MOCK_VALIDATOR_FAIL = MagicMock(side_effect=TypeError("bad conversion"))
+# Data is argspec, argument, expected
+VALID_SPECS = (
+ # Simple type=int
+ ({'arg': {'type': 'int'}}, {'arg': 42}, 42),
+ # Simple type=int with a large value (will be of type long under Python 2)
+ ({'arg': {'type': 'int'}}, {'arg': 18765432109876543210}, 18765432109876543210),
+ # Simple type=list, elements=int
+ ({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': [42, 32]}, [42, 32]),
+ # Type=int with conversion from string
+ ({'arg': {'type': 'int'}}, {'arg': '42'}, 42),
+ # Type=list elements=int with conversion from string
+ ({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': ['42', '32']}, [42, 32]),
+ # Simple type=float
+ ({'arg': {'type': 'float'}}, {'arg': 42.0}, 42.0),
+ # Simple type=list, elements=float
+ ({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': [42.1, 32.2]}, [42.1, 32.2]),
+ # Type=float conversion from int
+ ({'arg': {'type': 'float'}}, {'arg': 42}, 42.0),
+ # type=list, elements=float conversion from int
+ ({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': [42, 32]}, [42.0, 32.0]),
+ # Type=float conversion from string
+ ({'arg': {'type': 'float'}}, {'arg': '42.0'}, 42.0),
+ # type=list, elements=float conversion from string
+ ({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': ['42.1', '32.2']}, [42.1, 32.2]),
+ # Type=float conversion from string without decimal point
+ ({'arg': {'type': 'float'}}, {'arg': '42'}, 42.0),
+ # Type=list elements=float conversion from string without decimal point
+ ({'arg': {'type': 'list', 'elements': 'float'}}, {'arg': ['42', '32.2']}, [42.0, 32.2]),
+ # Simple type=bool
+ ({'arg': {'type': 'bool'}}, {'arg': True}, True),
+ # Simple type=list elements=bool
+ ({'arg': {'type': 'list', 'elements': 'bool'}}, {'arg': [True, 'true', 1, 'yes', False, 'false', 'no', 0]},
+ [True, True, True, True, False, False, False, False]),
+ # Type=int with conversion from string
+ ({'arg': {'type': 'bool'}}, {'arg': 'yes'}, True),
+ # Type=str converts to string
+ ({'arg': {'type': 'str'}}, {'arg': 42}, '42'),
+ # Type=list elements=str simple converts to string
+ ({'arg': {'type': 'list', 'elements': 'str'}}, {'arg': ['42', '32']}, ['42', '32']),
+ # Type is implicit, converts to string
+ ({'arg': {'type': 'str'}}, {'arg': 42}, '42'),
+ # Type=list elements=str implicit converts to string
+ ({'arg': {'type': 'list', 'elements': 'str'}}, {'arg': [42, 32]}, ['42', '32']),
+ # parameter is required
+ ({'arg': {'required': True}}, {'arg': 42}, '42'),
+)
+
+INVALID_SPECS = (
+ # Type is int; unable to convert this string
+ ({'arg': {'type': 'int'}}, {'arg': "wolf"}, "is of type {0} and we were unable to convert to int: {0} cannot be converted to an int".format(type('bad'))),
+ # Type is list elements is int; unable to convert this string
+ ({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': [1, "bad"]}, "is of type {0} and we were unable to convert to int: {0} cannot be converted to "
+ "an int".format(type('int'))),
+ # Type is int; unable to convert float
+ ({'arg': {'type': 'int'}}, {'arg': 42.1}, "'float'> cannot be converted to an int"),
+ # Type is list, elements is int; unable to convert float
+ ({'arg': {'type': 'list', 'elements': 'int'}}, {'arg': [42.1, 32, 2]}, "'float'> cannot be converted to an int"),
+ # type is a callable that fails to convert
+ ({'arg': {'type': MOCK_VALIDATOR_FAIL}}, {'arg': "bad"}, "bad conversion"),
+ # type is a list, elements is callable that fails to convert
+ ({'arg': {'type': 'list', 'elements': MOCK_VALIDATOR_FAIL}}, {'arg': [1, "bad"]}, "bad conversion"),
+ # unknown parameter
+ ({'arg': {'type': 'int'}}, {'other': 'bad', '_ansible_module_name': 'ansible_unittest'},
+ 'Unsupported parameters for (ansible_unittest) module: other. Supported parameters include: arg.'),
+ ({'arg': {'type': 'int', 'aliases': ['argument']}}, {'other': 'bad', '_ansible_module_name': 'ansible_unittest'},
+ 'Unsupported parameters for (ansible_unittest) module: other. Supported parameters include: arg (argument).'),
+ # parameter is required
+ ({'arg': {'required': True}}, {}, 'missing required arguments: arg'),
+)
+
+BASIC_AUTH_VALID_ARGS = [
+ {'api_username': 'user1', 'api_password': 'password1', 'api_url': 'http://example.com', 'validate_certs': False},
+ {'api_username': 'user1', 'api_password': 'password1', 'api_url': 'http://example.com', 'validate_certs': True},
+]
+
+RATE_LIMIT_VALID_ARGS = [
+ {'rate': 1, 'rate_limit': 1},
+ {'rate': '1', 'rate_limit': 1},
+ {'rate': 1, 'rate_limit': '1'},
+ {'rate': '1', 'rate_limit': '1'},
+]
+
+RETRY_VALID_ARGS = [
+ {'retries': 1, 'retry_pause': 1.5},
+ {'retries': '1', 'retry_pause': '1.5'},
+ {'retries': 1, 'retry_pause': '1.5'},
+ {'retries': '1', 'retry_pause': 1.5},
+]
+
+
+@pytest.fixture
+def complex_argspec():
+ arg_spec = dict(
+ foo=dict(required=True, aliases=['dup']),
+ bar=dict(),
+ bam=dict(),
+ bing=dict(),
+ bang=dict(),
+ bong=dict(),
+ baz=dict(fallback=(basic.env_fallback, ['BAZ'])),
+ bar1=dict(type='bool'),
+ bar3=dict(type='list', elements='path'),
+ bar_str=dict(type='list', elements=str),
+ zardoz=dict(choices=['one', 'two']),
+ zardoz2=dict(type='list', choices=['one', 'two', 'three']),
+ zardoz3=dict(type='str', aliases=['zodraz'], deprecated_aliases=[dict(name='zodraz', version='9.99')]),
+ )
+ mut_ex = (('bar', 'bam'), ('bing', 'bang', 'bong'))
+ req_to = (('bam', 'baz'),)
+
+ kwargs = dict(
+ argument_spec=arg_spec,
+ mutually_exclusive=mut_ex,
+ required_together=req_to,
+ no_log=True,
+ add_file_common_args=True,
+ supports_check_mode=True,
+ )
+ return kwargs
+
+
+@pytest.fixture
+def options_argspec_list():
+ options_spec = dict(
+ foo=dict(required=True, aliases=['dup']),
+ bar=dict(),
+ bar1=dict(type='list', elements='str'),
+ bar2=dict(type='list', elements='int'),
+ bar3=dict(type='list', elements='float'),
+ bar4=dict(type='list', elements='path'),
+ bam=dict(),
+ baz=dict(fallback=(basic.env_fallback, ['BAZ'])),
+ bam1=dict(),
+ bam2=dict(default='test'),
+ bam3=dict(type='bool'),
+ bam4=dict(type='str'),
+ )
+
+ arg_spec = dict(
+ foobar=dict(
+ type='list',
+ elements='dict',
+ options=options_spec,
+ mutually_exclusive=[
+ ['bam', 'bam1'],
+ ],
+ required_if=[
+ ['foo', 'hello', ['bam']],
+ ['foo', 'bam2', ['bam2']]
+ ],
+ required_one_of=[
+ ['bar', 'bam']
+ ],
+ required_together=[
+ ['bam1', 'baz']
+ ],
+ required_by={
+ 'bam4': ('bam1', 'bam3'),
+ },
+ )
+ )
+
+ kwargs = dict(
+ argument_spec=arg_spec,
+ no_log=True,
+ add_file_common_args=True,
+ supports_check_mode=True
+ )
+ return kwargs
+
+
+@pytest.fixture
+def options_argspec_dict(options_argspec_list):
+ # should test ok, for options in dict format.
+ kwargs = options_argspec_list
+ kwargs['argument_spec']['foobar']['type'] = 'dict'
+ kwargs['argument_spec']['foobar']['elements'] = None
+
+ return kwargs
+
+
+#
+# Tests for one aspect of arg_spec
+#
+
+@pytest.mark.parametrize('argspec, expected, stdin', [(s[0], s[2], s[1]) for s in VALID_SPECS],
+ indirect=['stdin'])
+def test_validator_basic_types(argspec, expected, stdin):
+
+ am = basic.AnsibleModule(argspec)
+
+ if 'type' in argspec['arg']:
+ if argspec['arg']['type'] == 'int':
+ type_ = integer_types
+ else:
+ type_ = getattr(builtins, argspec['arg']['type'])
+ else:
+ type_ = str
+
+ assert isinstance(am.params['arg'], type_)
+ assert am.params['arg'] == expected
+
+
+@pytest.mark.parametrize('stdin', [{'arg': 42}, {'arg': 18765432109876543210}], indirect=['stdin'])
+def test_validator_function(mocker, stdin):
+ # Type is a callable
+ MOCK_VALIDATOR_SUCCESS = mocker.MagicMock(return_value=27)
+ argspec = {'arg': {'type': MOCK_VALIDATOR_SUCCESS}}
+ am = basic.AnsibleModule(argspec)
+
+ assert isinstance(am.params['arg'], integer_types)
+ assert am.params['arg'] == 27
+
+
+@pytest.mark.parametrize('stdin', BASIC_AUTH_VALID_ARGS, indirect=['stdin'])
+def test_validate_basic_auth_arg(mocker, stdin):
+ kwargs = dict(
+ argument_spec=basic_auth_argument_spec()
+ )
+ am = basic.AnsibleModule(**kwargs)
+ assert isinstance(am.params['api_username'], string_types)
+ assert isinstance(am.params['api_password'], string_types)
+ assert isinstance(am.params['api_url'], string_types)
+ assert isinstance(am.params['validate_certs'], bool)
+
+
+@pytest.mark.parametrize('stdin', RATE_LIMIT_VALID_ARGS, indirect=['stdin'])
+def test_validate_rate_limit_argument_spec(mocker, stdin):
+ kwargs = dict(
+ argument_spec=rate_limit_argument_spec()
+ )
+ am = basic.AnsibleModule(**kwargs)
+ assert isinstance(am.params['rate'], integer_types)
+ assert isinstance(am.params['rate_limit'], integer_types)
+
+
+@pytest.mark.parametrize('stdin', RETRY_VALID_ARGS, indirect=['stdin'])
+def test_validate_retry_argument_spec(mocker, stdin):
+ kwargs = dict(
+ argument_spec=retry_argument_spec()
+ )
+ am = basic.AnsibleModule(**kwargs)
+ assert isinstance(am.params['retries'], integer_types)
+ assert isinstance(am.params['retry_pause'], float)
+
+
+@pytest.mark.parametrize('stdin', [{'arg': '123'}, {'arg': 123}], indirect=['stdin'])
+def test_validator_string_type(mocker, stdin):
+ # Custom callable that is 'str'
+ argspec = {'arg': {'type': str}}
+ am = basic.AnsibleModule(argspec)
+
+ assert isinstance(am.params['arg'], string_types)
+ assert am.params['arg'] == '123'
+
+
+@pytest.mark.parametrize('argspec, expected, stdin', [(s[0], s[2], s[1]) for s in INVALID_SPECS],
+ indirect=['stdin'])
+def test_validator_fail(stdin, capfd, argspec, expected):
+ with pytest.raises(SystemExit):
+ basic.AnsibleModule(argument_spec=argspec)
+
+ out, err = capfd.readouterr()
+ assert not err
+ assert expected in json.loads(out)['msg']
+ assert json.loads(out)['failed']
+
+
+class TestComplexArgSpecs:
+ """Test with a more complex arg_spec"""
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello'}, {'dup': 'hello'}], indirect=['stdin'])
+ def test_complex_required(self, stdin, complex_argspec):
+ """Test that the complex argspec works if we give it its required param as either the canonical or aliased name"""
+ am = basic.AnsibleModule(**complex_argspec)
+ assert isinstance(am.params['foo'], str)
+ assert am.params['foo'] == 'hello'
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello1', 'dup': 'hello2'}], indirect=['stdin'])
+ def test_complex_duplicate_warning(self, stdin, complex_argspec):
+ """Test that the complex argspec issues a warning if we specify an option both with its canonical name and its alias"""
+ am = basic.AnsibleModule(**complex_argspec)
+ assert isinstance(am.params['foo'], str)
+ assert 'Both option foo and its alias dup are set.' in get_warning_messages()
+ assert am.params['foo'] == 'hello2'
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bam': 'test'}], indirect=['stdin'])
+ def test_complex_type_fallback(self, mocker, stdin, complex_argspec):
+ """Test that the complex argspec works if we get a required parameter via fallback"""
+ environ = os.environ.copy()
+ environ['BAZ'] = 'test data'
+ mocker.patch('ansible.module_utils.basic.os.environ', environ)
+
+ am = basic.AnsibleModule(**complex_argspec)
+
+ assert isinstance(am.params['baz'], str)
+ assert am.params['baz'] == 'test data'
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bar': 'bad', 'bam': 'bad2', 'bing': 'a', 'bang': 'b', 'bong': 'c'}], indirect=['stdin'])
+ def test_fail_mutually_exclusive(self, capfd, stdin, complex_argspec):
+ """Fail because of mutually exclusive parameters"""
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**complex_argspec)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert results['msg'] == "parameters are mutually exclusive: bar|bam, bing|bang|bong"
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bam': 'bad2'}], indirect=['stdin'])
+ def test_fail_required_together(self, capfd, stdin, complex_argspec):
+ """Fail because only one of a required_together pair of parameters was specified"""
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**complex_argspec)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert results['msg'] == "parameters are required together: bam, baz"
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bar': 'hi'}], indirect=['stdin'])
+ def test_fail_required_together_and_default(self, capfd, stdin, complex_argspec):
+ """Fail because one of a required_together pair of parameters has a default and the other was not specified"""
+ complex_argspec['argument_spec']['baz'] = {'default': 42}
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**complex_argspec)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert results['msg'] == "parameters are required together: bam, baz"
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello'}], indirect=['stdin'])
+ def test_fail_required_together_and_fallback(self, capfd, mocker, stdin, complex_argspec):
+ """Fail because one of a required_together pair of parameters has a fallback and the other was not specified"""
+ environ = os.environ.copy()
+ environ['BAZ'] = 'test data'
+ mocker.patch('ansible.module_utils.basic.os.environ', environ)
+
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**complex_argspec)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert results['msg'] == "parameters are required together: bam, baz"
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'zardoz2': ['one', 'four', 'five']}], indirect=['stdin'])
+ def test_fail_list_with_choices(self, capfd, mocker, stdin, complex_argspec):
+ """Fail because one of the items is not in the choice"""
+ with pytest.raises(SystemExit):
+ basic.AnsibleModule(**complex_argspec)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert results['msg'] == "value of zardoz2 must be one or more of: one, two, three. Got no match for: four, five"
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'zardoz2': ['one', 'three']}], indirect=['stdin'])
+ def test_list_with_choices(self, capfd, mocker, stdin, complex_argspec):
+ """Test choices with list"""
+ am = basic.AnsibleModule(**complex_argspec)
+ assert isinstance(am.params['zardoz2'], list)
+ assert am.params['zardoz2'] == ['one', 'three']
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bar3': ['~/test', 'test/']}], indirect=['stdin'])
+ def test_list_with_elements_path(self, capfd, mocker, stdin, complex_argspec):
+ """Test choices with list"""
+ am = basic.AnsibleModule(**complex_argspec)
+ assert isinstance(am.params['bar3'], list)
+ assert am.params['bar3'][0].startswith('/')
+ assert am.params['bar3'][1] == 'test/'
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'zodraz': 'one'}], indirect=['stdin'])
+ def test_deprecated_alias(self, capfd, mocker, stdin, complex_argspec, monkeypatch):
+ """Test a deprecated alias"""
+ monkeypatch.setattr(warnings, '_global_deprecations', [])
+
+ am = basic.AnsibleModule(**complex_argspec)
+
+ assert "Alias 'zodraz' is deprecated." in get_deprecation_messages()[0]['msg']
+ assert get_deprecation_messages()[0]['version'] == '9.99'
+
+ @pytest.mark.parametrize('stdin', [{'foo': 'hello', 'bar_str': [867, '5309']}], indirect=['stdin'])
+ def test_list_with_elements_callable_str(self, capfd, mocker, stdin, complex_argspec):
+ """Test choices with list"""
+ am = basic.AnsibleModule(**complex_argspec)
+ assert isinstance(am.params['bar_str'], list)
+ assert isinstance(am.params['bar_str'][0], string_types)
+ assert isinstance(am.params['bar_str'][1], string_types)
+ assert am.params['bar_str'][0] == '867'
+ assert am.params['bar_str'][1] == '5309'
+
+
+class TestComplexOptions:
+ """Test arg spec options"""
+
+ # (Parameters, expected value of module.params['foobar'])
+ OPTIONS_PARAMS_LIST = (
+ ({'foobar': [{"foo": "hello", "bam": "good"}, {"foo": "test", "bar": "good"}]},
+ [{'foo': 'hello', 'bam': 'good', 'bam2': 'test', 'bar': None, 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None},
+ {'foo': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
+ ),
+ # Alias for required param
+ ({'foobar': [{"dup": "test", "bar": "good"}]},
+ [{'foo': 'test', 'dup': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
+ ),
+ # Required_if utilizing default value of the requirement
+ ({'foobar': [{"foo": "bam2", "bar": "required_one_of"}]},
+ [{'bam': None, 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': 'required_one_of', 'baz': None, 'foo': 'bam2',
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
+ ),
+ # Check that a bool option is converted
+ ({"foobar": [{"foo": "required", "bam": "good", "bam3": "yes"}]},
+ [{'bam': 'good', 'bam1': None, 'bam2': 'test', 'bam3': True, 'bam4': None, 'bar': None, 'baz': None, 'foo': 'required',
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
+ ),
+ # Check required_by options
+ ({"foobar": [{"foo": "required", "bar": "good", "baz": "good", "bam4": "required_by", "bam1": "ok", "bam3": "yes"}]},
+ [{'bar': 'good', 'baz': 'good', 'bam1': 'ok', 'bam2': 'test', 'bam3': True, 'bam4': 'required_by', 'bam': None, 'foo': 'required',
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}]
+ ),
+ # Check for elements in sub-options
+ ({"foobar": [{"foo": "good", "bam": "required_one_of", "bar1": [1, "good", "yes"], "bar2": ['1', 1], "bar3":['1.3', 1.3, 1]}]},
+ [{'foo': 'good', 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': None, 'baz': None, 'bam': 'required_one_of',
+ 'bar1': ["1", "good", "yes"], 'bar2': [1, 1], 'bar3': [1.3, 1.3, 1.0], 'bar4': None}]
+ ),
+ )
+
+ # (Parameters, expected value of module.params['foobar'])
+ OPTIONS_PARAMS_DICT = (
+ ({'foobar': {"foo": "hello", "bam": "good"}},
+ {'foo': 'hello', 'bam': 'good', 'bam2': 'test', 'bar': None, 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
+ ),
+ # Alias for required param
+ ({'foobar': {"dup": "test", "bar": "good"}},
+ {'foo': 'test', 'dup': 'test', 'bam': None, 'bam2': 'test', 'bar': 'good', 'baz': None, 'bam1': None, 'bam3': None, 'bam4': None,
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
+ ),
+ # Required_if utilizing default value of the requirement
+ ({'foobar': {"foo": "bam2", "bar": "required_one_of"}},
+ {'bam': None, 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': 'required_one_of', 'baz': None, 'foo': 'bam2',
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
+ ),
+ # Check that a bool option is converted
+ ({"foobar": {"foo": "required", "bam": "good", "bam3": "yes"}},
+ {'bam': 'good', 'bam1': None, 'bam2': 'test', 'bam3': True, 'bam4': None, 'bar': None, 'baz': None, 'foo': 'required',
+ 'bar1': None, 'bar2': None, 'bar3': None, 'bar4': None}
+ ),
+ # Check required_by options
+ ({"foobar": {"foo": "required", "bar": "good", "baz": "good", "bam4": "required_by", "bam1": "ok", "bam3": "yes"}},
+ {'bar': 'good', 'baz': 'good', 'bam1': 'ok', 'bam2': 'test', 'bam3': True, 'bam4': 'required_by', 'bam': None,
+ 'foo': 'required', 'bar1': None, 'bar3': None, 'bar2': None, 'bar4': None}
+ ),
+ # Check for elements in sub-options
+ ({"foobar": {"foo": "good", "bam": "required_one_of", "bar1": [1, "good", "yes"],
+ "bar2": ['1', 1], "bar3": ['1.3', 1.3, 1]}},
+ {'foo': 'good', 'bam1': None, 'bam2': 'test', 'bam3': None, 'bam4': None, 'bar': None,
+ 'baz': None, 'bam': 'required_one_of',
+ 'bar1': ["1", "good", "yes"], 'bar2': [1, 1], 'bar3': [1.3, 1.3, 1.0], 'bar4': None}
+ ),
+ )
+
+ # (Parameters, failure message)
+ FAILING_PARAMS_LIST = (
+ # Missing required option
+ ({'foobar': [{}]}, 'missing required arguments: foo found in foobar'),
+ # Invalid option
+ ({'foobar': [{"foo": "hello", "bam": "good", "invalid": "bad"}]}, 'module: foobar.invalid. Supported parameters include'),
+ # Mutually exclusive options found
+ ({'foobar': [{"foo": "test", "bam": "bad", "bam1": "bad", "baz": "req_to"}]},
+ 'parameters are mutually exclusive: bam|bam1 found in foobar'),
+ # required_if fails
+ ({'foobar': [{"foo": "hello", "bar": "bad"}]},
+ 'foo is hello but all of the following are missing: bam found in foobar'),
+ # Missing required_one_of option
+ ({'foobar': [{"foo": "test"}]},
+ 'one of the following is required: bar, bam found in foobar'),
+ # Missing required_together option
+ ({'foobar': [{"foo": "test", "bar": "required_one_of", "bam1": "bad"}]},
+ 'parameters are required together: bam1, baz found in foobar'),
+ # Missing required_by options
+ ({'foobar': [{"foo": "test", "bar": "required_one_of", "bam4": "required_by"}]},
+ "missing parameter(s) required by 'bam4': bam1, bam3"),
+ )
+
+ # (Parameters, failure message)
+ FAILING_PARAMS_DICT = (
+ # Missing required option
+ ({'foobar': {}}, 'missing required arguments: foo found in foobar'),
+ # Invalid option
+ ({'foobar': {"foo": "hello", "bam": "good", "invalid": "bad"}},
+ 'module: foobar.invalid. Supported parameters include'),
+ # Mutually exclusive options found
+ ({'foobar': {"foo": "test", "bam": "bad", "bam1": "bad", "baz": "req_to"}},
+ 'parameters are mutually exclusive: bam|bam1 found in foobar'),
+ # required_if fails
+ ({'foobar': {"foo": "hello", "bar": "bad"}},
+ 'foo is hello but all of the following are missing: bam found in foobar'),
+ # Missing required_one_of option
+ ({'foobar': {"foo": "test"}},
+ 'one of the following is required: bar, bam found in foobar'),
+ # Missing required_together option
+ ({'foobar': {"foo": "test", "bar": "required_one_of", "bam1": "bad"}},
+ 'parameters are required together: bam1, baz found in foobar'),
+ # Missing required_by options
+ ({'foobar': {"foo": "test", "bar": "required_one_of", "bam4": "required_by"}},
+ "missing parameter(s) required by 'bam4': bam1, bam3"),
+ )
+
+ @pytest.mark.parametrize('stdin, expected', OPTIONS_PARAMS_DICT, indirect=['stdin'])
+ def test_options_type_dict(self, stdin, options_argspec_dict, expected):
+ """Test that a basic creation with required and required_if works"""
+ # should test ok, tests basic foo requirement and required_if
+ am = basic.AnsibleModule(**options_argspec_dict)
+
+ assert isinstance(am.params['foobar'], dict)
+ assert am.params['foobar'] == expected
+
+ @pytest.mark.parametrize('stdin, expected', OPTIONS_PARAMS_LIST, indirect=['stdin'])
+ def test_options_type_list(self, stdin, options_argspec_list, expected):
+ """Test that a basic creation with required and required_if works"""
+ # should test ok, tests basic foo requirement and required_if
+ am = basic.AnsibleModule(**options_argspec_list)
+
+ assert isinstance(am.params['foobar'], list)
+ assert am.params['foobar'] == expected
+
+ @pytest.mark.parametrize('stdin, expected', FAILING_PARAMS_DICT, indirect=['stdin'])
+ def test_fail_validate_options_dict(self, capfd, stdin, options_argspec_dict, expected):
+ """Fail because one of a required_together pair of parameters has a default and the other was not specified"""
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**options_argspec_dict)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert expected in results['msg']
+
+ @pytest.mark.parametrize('stdin, expected', FAILING_PARAMS_LIST, indirect=['stdin'])
+ def test_fail_validate_options_list(self, capfd, stdin, options_argspec_list, expected):
+ """Fail because one of a required_together pair of parameters has a default and the other was not specified"""
+ with pytest.raises(SystemExit):
+ am = basic.AnsibleModule(**options_argspec_list)
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert results['failed']
+ assert expected in results['msg']
+
+ @pytest.mark.parametrize('stdin', [{'foobar': {'foo': 'required', 'bam1': 'test', 'bar': 'case'}}], indirect=['stdin'])
+ def test_fallback_in_option(self, mocker, stdin, options_argspec_dict):
+ """Test that the complex argspec works if we get a required parameter via fallback"""
+ environ = os.environ.copy()
+ environ['BAZ'] = 'test data'
+ mocker.patch('ansible.module_utils.basic.os.environ', environ)
+
+ am = basic.AnsibleModule(**options_argspec_dict)
+
+ assert isinstance(am.params['foobar']['baz'], str)
+ assert am.params['foobar']['baz'] == 'test data'
+
+ @pytest.mark.parametrize('stdin',
+ [{'foobar': {'foo': 'required', 'bam1': 'test', 'baz': 'data', 'bar': 'case', 'bar4': '~/test'}}],
+ indirect=['stdin'])
+ def test_elements_path_in_option(self, mocker, stdin, options_argspec_dict):
+ """Test that the complex argspec works with elements path type"""
+
+ am = basic.AnsibleModule(**options_argspec_dict)
+
+ assert isinstance(am.params['foobar']['bar4'][0], str)
+ assert am.params['foobar']['bar4'][0].startswith('/')
+
+ @pytest.mark.parametrize('stdin,spec,expected', [
+ ({},
+ {'one': {'type': 'dict', 'apply_defaults': True, 'options': {'two': {'default': True, 'type': 'bool'}}}},
+ {'two': True}),
+ ({},
+ {'one': {'type': 'dict', 'options': {'two': {'default': True, 'type': 'bool'}}}},
+ None),
+ ], indirect=['stdin'])
+ def test_subspec_not_required_defaults(self, stdin, spec, expected):
+ # Check that top level not required, processed subspec defaults
+ am = basic.AnsibleModule(spec)
+ assert am.params['one'] == expected
+
+
+class TestLoadFileCommonArguments:
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_smoketest_load_file_common_args(self, am):
+ """With no file arguments, an empty dict is returned"""
+ am.selinux_mls_enabled = MagicMock()
+ am.selinux_mls_enabled.return_value = True
+ am.selinux_default_context = MagicMock()
+ am.selinux_default_context.return_value = 'unconfined_u:object_r:default_t:s0'.split(':', 3)
+
+ assert am.load_file_common_arguments(params={}) == {}
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_load_file_common_args(self, am, mocker):
+ am.selinux_mls_enabled = MagicMock()
+ am.selinux_mls_enabled.return_value = True
+ am.selinux_default_context = MagicMock()
+ am.selinux_default_context.return_value = 'unconfined_u:object_r:default_t:s0'.split(':', 3)
+
+ base_params = dict(
+ path='/path/to/file',
+ mode=0o600,
+ owner='root',
+ group='root',
+ seuser='_default',
+ serole='_default',
+ setype='_default',
+ selevel='_default',
+ )
+
+ extended_params = base_params.copy()
+ extended_params.update(dict(
+ follow=True,
+ foo='bar',
+ ))
+
+ final_params = base_params.copy()
+ final_params.update(dict(
+ path='/path/to/real_file',
+ secontext=['unconfined_u', 'object_r', 'default_t', 's0'],
+ attributes=None,
+ ))
+
+ # with the proper params specified, the returned dictionary should represent
+ # only those params which have something to do with the file arguments, excluding
+ # other params and updated as required with proper values which may have been
+ # massaged by the method
+ mocker.patch('os.path.islink', return_value=True)
+ mocker.patch('os.path.realpath', return_value='/path/to/real_file')
+
+ res = am.load_file_common_arguments(params=extended_params)
+
+ assert res == final_params
+
+
+@pytest.mark.parametrize("stdin", [{"arg_pass": "testing"}], indirect=["stdin"])
+def test_no_log_true(stdin, capfd):
+ """Explicitly mask an argument (no_log=True)."""
+ arg_spec = {
+ "arg_pass": {"no_log": True}
+ }
+ am = basic.AnsibleModule(arg_spec)
+ # no_log=True is picked up by both am._log_invocation and list_no_log_values
+ # (called by am._handle_no_log_values). As a result, we can check for the
+ # value in am.no_log_values.
+ assert "testing" in am.no_log_values
+
+
+@pytest.mark.parametrize("stdin", [{"arg_pass": "testing"}], indirect=["stdin"])
+def test_no_log_false(stdin, capfd):
+ """Explicitly log and display an argument (no_log=False)."""
+ arg_spec = {
+ "arg_pass": {"no_log": False}
+ }
+ am = basic.AnsibleModule(arg_spec)
+ assert "testing" not in am.no_log_values and not get_warning_messages()
+
+
+@pytest.mark.parametrize("stdin", [{"arg_pass": "testing"}], indirect=["stdin"])
+def test_no_log_none(stdin, capfd):
+ """Allow Ansible to make the decision by matching the argument name
+ against PASSWORD_MATCH."""
+ arg_spec = {
+ "arg_pass": {}
+ }
+ am = basic.AnsibleModule(arg_spec)
+ # Omitting no_log is only picked up by _log_invocation, so the value never
+ # makes it into am.no_log_values. Instead we can check for the warning
+ # emitted by am._log_invocation.
+ assert len(get_warning_messages()) > 0
+
+
+@pytest.mark.parametrize("stdin", [{"pass": "testing"}], indirect=["stdin"])
+def test_no_log_alias(stdin, capfd):
+ """Given module parameters that use an alias for a parameter that matches
+ PASSWORD_MATCH and has no_log=True set, a warning should not be issued.
+ """
+ arg_spec = {
+ "other_pass": {"no_log": True, "aliases": ["pass"]},
+ }
+ am = basic.AnsibleModule(arg_spec)
+
+ assert len(get_warning_messages()) == 0
diff --git a/test/units/module_utils/basic/test_atomic_move.py b/test/units/module_utils/basic/test_atomic_move.py
new file mode 100644
index 0000000..bbdb051
--- /dev/null
+++ b/test/units/module_utils/basic/test_atomic_move.py
@@ -0,0 +1,223 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# (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
+
+import os
+import errno
+import json
+from itertools import product
+
+import pytest
+
+from ansible.module_utils import basic
+
+
+@pytest.fixture
+def atomic_am(am, mocker):
+ am.selinux_enabled = mocker.MagicMock()
+ am.selinux_context = mocker.MagicMock()
+ am.selinux_default_context = mocker.MagicMock()
+ am.set_context_if_different = mocker.MagicMock()
+ am._unsafe_writes = mocker.MagicMock()
+
+ yield am
+
+
+@pytest.fixture
+def atomic_mocks(mocker, monkeypatch):
+ environ = dict()
+ mocks = {
+ 'chmod': mocker.patch('os.chmod'),
+ 'chown': mocker.patch('os.chown'),
+ 'close': mocker.patch('os.close'),
+ 'environ': mocker.patch('os.environ', environ),
+ 'getlogin': mocker.patch('os.getlogin'),
+ 'getuid': mocker.patch('os.getuid'),
+ 'path_exists': mocker.patch('os.path.exists'),
+ 'rename': mocker.patch('os.rename'),
+ 'stat': mocker.patch('os.stat'),
+ 'umask': mocker.patch('os.umask'),
+ 'getpwuid': mocker.patch('pwd.getpwuid'),
+ 'copy2': mocker.patch('shutil.copy2'),
+ 'copyfileobj': mocker.patch('shutil.copyfileobj'),
+ 'move': mocker.patch('shutil.move'),
+ 'mkstemp': mocker.patch('tempfile.mkstemp'),
+ }
+
+ mocks['getlogin'].return_value = 'root'
+ mocks['getuid'].return_value = 0
+ mocks['getpwuid'].return_value = ('root', '', 0, 0, '', '', '')
+ mocks['umask'].side_effect = [18, 0]
+ mocks['rename'].return_value = None
+
+ # normalize OS specific features
+ monkeypatch.delattr(os, 'chflags', raising=False)
+
+ yield mocks
+
+
+@pytest.fixture
+def fake_stat(mocker):
+ stat1 = mocker.MagicMock()
+ stat1.st_mode = 0o0644
+ stat1.st_uid = 0
+ stat1.st_gid = 0
+ stat1.st_flags = 0
+ yield stat1
+
+
+@pytest.mark.parametrize('stdin, selinux', product([{}], (True, False)), indirect=['stdin'])
+def test_new_file(atomic_am, atomic_mocks, mocker, selinux):
+ # test destination does not exist, login name = 'root', no environment, os.rename() succeeds
+ mock_context = atomic_am.selinux_default_context.return_value
+ atomic_mocks['path_exists'].return_value = False
+ atomic_am.selinux_enabled.return_value = selinux
+
+ atomic_am.atomic_move('/path/to/src', '/path/to/dest')
+
+ atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/path/to/dest')
+ assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/dest', basic.DEFAULT_PERM & ~18)]
+
+ if selinux:
+ assert atomic_am.selinux_default_context.call_args_list == [mocker.call('/path/to/dest')]
+ assert atomic_am.set_context_if_different.call_args_list == [mocker.call('/path/to/dest', mock_context, False)]
+ else:
+ assert not atomic_am.selinux_default_context.called
+ assert not atomic_am.set_context_if_different.called
+
+
+@pytest.mark.parametrize('stdin, selinux', product([{}], (True, False)), indirect=['stdin'])
+def test_existing_file(atomic_am, atomic_mocks, fake_stat, mocker, selinux):
+ # Test destination already present
+ mock_context = atomic_am.selinux_context.return_value
+ atomic_mocks['stat'].return_value = fake_stat
+ atomic_mocks['path_exists'].return_value = True
+ atomic_am.selinux_enabled.return_value = selinux
+
+ atomic_am.atomic_move('/path/to/src', '/path/to/dest')
+
+ atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/path/to/dest')
+ assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/src', basic.DEFAULT_PERM & ~18)]
+
+ if selinux:
+ assert atomic_am.set_context_if_different.call_args_list == [mocker.call('/path/to/dest', mock_context, False)]
+ assert atomic_am.selinux_context.call_args_list == [mocker.call('/path/to/dest')]
+ else:
+ assert not atomic_am.selinux_default_context.called
+ assert not atomic_am.set_context_if_different.called
+
+
+@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+def test_no_tty_fallback(atomic_am, atomic_mocks, fake_stat, mocker):
+ """Raise OSError when using getlogin() to simulate no tty cornercase"""
+ mock_context = atomic_am.selinux_context.return_value
+ atomic_mocks['stat'].return_value = fake_stat
+ atomic_mocks['path_exists'].return_value = True
+ atomic_am.selinux_enabled.return_value = True
+ atomic_mocks['getlogin'].side_effect = OSError()
+ atomic_mocks['environ']['LOGNAME'] = 'root'
+
+ atomic_am.atomic_move('/path/to/src', '/path/to/dest')
+
+ atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/path/to/dest')
+ assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/src', basic.DEFAULT_PERM & ~18)]
+
+ assert atomic_am.set_context_if_different.call_args_list == [mocker.call('/path/to/dest', mock_context, False)]
+ assert atomic_am.selinux_context.call_args_list == [mocker.call('/path/to/dest')]
+
+
+@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+def test_existing_file_stat_failure(atomic_am, atomic_mocks, mocker):
+ """Failure to stat an existing file in order to copy permissions propogates the error (unless EPERM)"""
+ atomic_mocks['stat'].side_effect = OSError()
+ atomic_mocks['path_exists'].return_value = True
+
+ with pytest.raises(OSError):
+ atomic_am.atomic_move('/path/to/src', '/path/to/dest')
+
+
+@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+def test_existing_file_stat_perms_failure(atomic_am, atomic_mocks, mocker):
+ """Failure to stat an existing file to copy the permissions due to permissions passes fine"""
+ # and now have os.stat return EPERM, which should not fail
+ mock_context = atomic_am.selinux_context.return_value
+ atomic_mocks['stat'].side_effect = OSError(errno.EPERM, 'testing os stat with EPERM')
+ atomic_mocks['path_exists'].return_value = True
+ atomic_am.selinux_enabled.return_value = True
+
+ atomic_am.atomic_move('/path/to/src', '/path/to/dest')
+
+ atomic_mocks['rename'].assert_called_with(b'/path/to/src', b'/path/to/dest')
+ # FIXME: Should atomic_move() set a default permission value when it cannot retrieve the
+ # existing file's permissions? (Right now it's up to the calling code.
+ # assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/src', basic.DEFAULT_PERM & ~18)]
+ assert atomic_am.set_context_if_different.call_args_list == [mocker.call('/path/to/dest', mock_context, False)]
+ assert atomic_am.selinux_context.call_args_list == [mocker.call('/path/to/dest')]
+
+
+@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+def test_rename_failure(atomic_am, atomic_mocks, mocker, capfd):
+ """Test os.rename fails with EIO, causing it to bail out"""
+ atomic_mocks['path_exists'].side_effect = [False, False]
+ atomic_mocks['rename'].side_effect = OSError(errno.EIO, 'failing with EIO')
+
+ with pytest.raises(SystemExit):
+ atomic_am.atomic_move('/path/to/src', '/path/to/dest')
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert 'Could not replace file' in results['msg']
+ assert 'failing with EIO' in results['msg']
+ assert results['failed']
+
+
+@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+def test_rename_perms_fail_temp_creation_fails(atomic_am, atomic_mocks, mocker, capfd):
+ """Test os.rename fails with EPERM working but failure in mkstemp"""
+ atomic_mocks['path_exists'].return_value = False
+ atomic_mocks['close'].return_value = None
+ atomic_mocks['rename'].side_effect = [OSError(errno.EPERM, 'failing with EPERM'), None]
+ atomic_mocks['mkstemp'].return_value = None
+ atomic_mocks['mkstemp'].side_effect = OSError()
+ atomic_am.selinux_enabled.return_value = False
+
+ with pytest.raises(SystemExit):
+ atomic_am.atomic_move('/path/to/src', '/path/to/dest')
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+
+ assert 'is not writable by the current user' in results['msg']
+ assert results['failed']
+
+
+@pytest.mark.parametrize('stdin, selinux', product([{}], (True, False)), indirect=['stdin'])
+def test_rename_perms_fail_temp_succeeds(atomic_am, atomic_mocks, fake_stat, mocker, selinux):
+ """Test os.rename raising an error but fallback to using mkstemp works"""
+ mock_context = atomic_am.selinux_default_context.return_value
+ atomic_mocks['path_exists'].return_value = False
+ atomic_mocks['rename'].side_effect = [OSError(errno.EPERM, 'failing with EPERM'), None]
+ atomic_mocks['stat'].return_value = fake_stat
+ atomic_mocks['stat'].side_effect = None
+ atomic_mocks['mkstemp'].return_value = (None, '/path/to/tempfile')
+ atomic_mocks['mkstemp'].side_effect = None
+ atomic_am.selinux_enabled.return_value = selinux
+
+ atomic_am.atomic_move('/path/to/src', '/path/to/dest')
+ assert atomic_mocks['rename'].call_args_list == [mocker.call(b'/path/to/src', b'/path/to/dest'),
+ mocker.call(b'/path/to/tempfile', b'/path/to/dest')]
+ assert atomic_mocks['chmod'].call_args_list == [mocker.call(b'/path/to/dest', basic.DEFAULT_PERM & ~18)]
+
+ if selinux:
+ assert atomic_am.selinux_default_context.call_args_list == [mocker.call('/path/to/dest')]
+ assert atomic_am.set_context_if_different.call_args_list == [mocker.call(b'/path/to/tempfile', mock_context, False),
+ mocker.call('/path/to/dest', mock_context, False)]
+ else:
+ assert not atomic_am.selinux_default_context.called
+ assert not atomic_am.set_context_if_different.called
diff --git a/test/units/module_utils/basic/test_command_nonexisting.py b/test/units/module_utils/basic/test_command_nonexisting.py
new file mode 100644
index 0000000..6ed7f91
--- /dev/null
+++ b/test/units/module_utils/basic/test_command_nonexisting.py
@@ -0,0 +1,31 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+import pytest
+import json
+import sys
+import pytest
+import subprocess
+import ansible.module_utils.basic
+from ansible.module_utils._text import to_bytes
+from ansible.module_utils import basic
+
+
+def test_run_non_existent_command(monkeypatch):
+ """ Test that `command` returns std{out,err} even if the executable is not found """
+ def fail_json(msg, **kwargs):
+ assert kwargs["stderr"] == b''
+ assert kwargs["stdout"] == b''
+ sys.exit(1)
+
+ def popen(*args, **kwargs):
+ raise OSError()
+
+ monkeypatch.setattr(basic, '_ANSIBLE_ARGS', to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': {}})))
+ monkeypatch.setattr(subprocess, 'Popen', popen)
+
+ am = basic.AnsibleModule(argument_spec={})
+ monkeypatch.setattr(am, 'fail_json', fail_json)
+ with pytest.raises(SystemExit):
+ am.run_command("lecho", "whatever")
diff --git a/test/units/module_utils/basic/test_deprecate_warn.py b/test/units/module_utils/basic/test_deprecate_warn.py
new file mode 100644
index 0000000..581ba6d
--- /dev/null
+++ b/test/units/module_utils/basic/test_deprecate_warn.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+#
+# 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
+
+import json
+
+import pytest
+
+from ansible.module_utils.common import warnings
+
+
+@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+def test_warn(am, capfd):
+
+ am.warn('warning1')
+
+ with pytest.raises(SystemExit):
+ am.exit_json(warnings=['warning2'])
+ out, err = capfd.readouterr()
+ assert json.loads(out)['warnings'] == ['warning1', 'warning2']
+
+
+@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+def test_deprecate(am, capfd, monkeypatch):
+ monkeypatch.setattr(warnings, '_global_deprecations', [])
+
+ am.deprecate('deprecation1')
+ am.deprecate('deprecation2', '2.3') # pylint: disable=ansible-deprecated-no-collection-name
+ am.deprecate('deprecation3', version='2.4') # pylint: disable=ansible-deprecated-no-collection-name
+ am.deprecate('deprecation4', date='2020-03-10') # pylint: disable=ansible-deprecated-no-collection-name
+ am.deprecate('deprecation5', collection_name='ansible.builtin')
+ am.deprecate('deprecation6', '2.3', collection_name='ansible.builtin')
+ am.deprecate('deprecation7', version='2.4', collection_name='ansible.builtin')
+ am.deprecate('deprecation8', date='2020-03-10', collection_name='ansible.builtin')
+
+ with pytest.raises(SystemExit):
+ am.exit_json(deprecations=['deprecation9', ('deprecation10', '2.4')])
+
+ out, err = capfd.readouterr()
+ output = json.loads(out)
+ assert ('warnings' not in output or output['warnings'] == [])
+ assert output['deprecations'] == [
+ {u'msg': u'deprecation1', u'version': None, u'collection_name': None},
+ {u'msg': u'deprecation2', u'version': '2.3', u'collection_name': None},
+ {u'msg': u'deprecation3', u'version': '2.4', u'collection_name': None},
+ {u'msg': u'deprecation4', u'date': '2020-03-10', u'collection_name': None},
+ {u'msg': u'deprecation5', u'version': None, u'collection_name': 'ansible.builtin'},
+ {u'msg': u'deprecation6', u'version': '2.3', u'collection_name': 'ansible.builtin'},
+ {u'msg': u'deprecation7', u'version': '2.4', u'collection_name': 'ansible.builtin'},
+ {u'msg': u'deprecation8', u'date': '2020-03-10', u'collection_name': 'ansible.builtin'},
+ {u'msg': u'deprecation9', u'version': None, u'collection_name': None},
+ {u'msg': u'deprecation10', u'version': '2.4', u'collection_name': None},
+ ]
+
+
+@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+def test_deprecate_without_list(am, capfd):
+ with pytest.raises(SystemExit):
+ am.exit_json(deprecations='Simple deprecation warning')
+
+ out, err = capfd.readouterr()
+ output = json.loads(out)
+ assert ('warnings' not in output or output['warnings'] == [])
+ assert output['deprecations'] == [
+ {u'msg': u'Simple deprecation warning', u'version': None, u'collection_name': None},
+ ]
+
+
+@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+def test_deprecate_without_list_version_date_not_set(am, capfd):
+ with pytest.raises(AssertionError) as ctx:
+ am.deprecate('Simple deprecation warning', date='', version='')
+ assert ctx.value.args[0] == "implementation error -- version and date must not both be set"
diff --git a/test/units/module_utils/basic/test_dict_converters.py b/test/units/module_utils/basic/test_dict_converters.py
new file mode 100644
index 0000000..f63ed9c
--- /dev/null
+++ b/test/units/module_utils/basic/test_dict_converters.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# (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 units.mock.procenv import ModuleTestCase
+
+from ansible.module_utils.six.moves import builtins
+
+realimport = builtins.__import__
+
+
+class TestTextifyContainers(ModuleTestCase):
+ def test_module_utils_basic_json_dict_converters(self):
+ from ansible.module_utils.basic import json_dict_unicode_to_bytes, json_dict_bytes_to_unicode
+
+ test_data = dict(
+ item1=u"Fóo",
+ item2=[u"Bár", u"Bam"],
+ item3=dict(sub1=u"Súb"),
+ item4=(u"föo", u"bär", u"©"),
+ item5=42,
+ )
+ res = json_dict_unicode_to_bytes(test_data)
+ res2 = json_dict_bytes_to_unicode(res)
+
+ self.assertEqual(test_data, res2)
diff --git a/test/units/module_utils/basic/test_exit_json.py b/test/units/module_utils/basic/test_exit_json.py
new file mode 100644
index 0000000..4afcb27
--- /dev/null
+++ b/test/units/module_utils/basic/test_exit_json.py
@@ -0,0 +1,173 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2015-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
+
+import json
+import sys
+import datetime
+
+import pytest
+
+from ansible.module_utils.common import warnings
+
+EMPTY_INVOCATION = {u'module_args': {}}
+DATETIME = datetime.datetime.strptime('2020-07-13 12:50:00', '%Y-%m-%d %H:%M:%S')
+
+
+class TestAnsibleModuleExitJson:
+ """
+ Test that various means of calling exitJson and FailJson return the messages they've been given
+ """
+ DATA = (
+ ({}, {'invocation': EMPTY_INVOCATION}),
+ ({'msg': 'message'}, {'msg': 'message', 'invocation': EMPTY_INVOCATION}),
+ ({'msg': 'success', 'changed': True},
+ {'msg': 'success', 'changed': True, 'invocation': EMPTY_INVOCATION}),
+ ({'msg': 'nochange', 'changed': False},
+ {'msg': 'nochange', 'changed': False, 'invocation': EMPTY_INVOCATION}),
+ ({'msg': 'message', 'date': DATETIME.date()},
+ {'msg': 'message', 'date': DATETIME.date().isoformat(), 'invocation': EMPTY_INVOCATION}),
+ ({'msg': 'message', 'datetime': DATETIME},
+ {'msg': 'message', 'datetime': DATETIME.isoformat(), 'invocation': EMPTY_INVOCATION}),
+ )
+
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ # pylint: disable=undefined-variable
+ @pytest.mark.parametrize('args, expected, stdin', ((a, e, {}) for a, e in DATA), indirect=['stdin'])
+ def test_exit_json_exits(self, am, capfd, args, expected, monkeypatch):
+ monkeypatch.setattr(warnings, '_global_deprecations', [])
+
+ with pytest.raises(SystemExit) as ctx:
+ am.exit_json(**args)
+ assert ctx.value.code == 0
+
+ out, err = capfd.readouterr()
+ return_val = json.loads(out)
+ assert return_val == expected
+
+ # Fail_json is only legal if it's called with a message
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ @pytest.mark.parametrize('args, expected, stdin',
+ ((a, e, {}) for a, e in DATA if 'msg' in a), # pylint: disable=undefined-variable
+ indirect=['stdin'])
+ def test_fail_json_exits(self, am, capfd, args, expected, monkeypatch):
+ monkeypatch.setattr(warnings, '_global_deprecations', [])
+
+ with pytest.raises(SystemExit) as ctx:
+ am.fail_json(**args)
+ assert ctx.value.code == 1
+
+ out, err = capfd.readouterr()
+ return_val = json.loads(out)
+ # Fail_json should add failed=True
+ expected['failed'] = True
+ assert return_val == expected
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_fail_json_msg_positional(self, am, capfd, monkeypatch):
+ monkeypatch.setattr(warnings, '_global_deprecations', [])
+
+ with pytest.raises(SystemExit) as ctx:
+ am.fail_json('This is the msg')
+ assert ctx.value.code == 1
+
+ out, err = capfd.readouterr()
+ return_val = json.loads(out)
+ # Fail_json should add failed=True
+ assert return_val == {'msg': 'This is the msg', 'failed': True,
+ 'invocation': EMPTY_INVOCATION}
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_fail_json_msg_as_kwarg_after(self, am, capfd, monkeypatch):
+ """Test that msg as a kwarg after other kwargs works"""
+ monkeypatch.setattr(warnings, '_global_deprecations', [])
+
+ with pytest.raises(SystemExit) as ctx:
+ am.fail_json(arbitrary=42, msg='This is the msg')
+ assert ctx.value.code == 1
+
+ out, err = capfd.readouterr()
+ return_val = json.loads(out)
+ # Fail_json should add failed=True
+ assert return_val == {'msg': 'This is the msg', 'failed': True,
+ 'arbitrary': 42,
+ 'invocation': EMPTY_INVOCATION}
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_fail_json_no_msg(self, am):
+ with pytest.raises(TypeError) as ctx:
+ am.fail_json()
+
+ if sys.version_info < (3,):
+ error_msg = "fail_json() takes exactly 2 arguments (1 given)"
+ elif sys.version_info >= (3, 10):
+ error_msg = "AnsibleModule.fail_json() missing 1 required positional argument: 'msg'"
+ else:
+ error_msg = "fail_json() missing 1 required positional argument: 'msg'"
+
+ assert ctx.value.args[0] == error_msg
+
+
+class TestAnsibleModuleExitValuesRemoved:
+ """
+ Test that ExitJson and FailJson remove password-like values
+ """
+ OMIT = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+
+ DATA = (
+ (
+ dict(username='person', password='$ecret k3y'),
+ dict(one=1, pwd='$ecret k3y', url='https://username:password12345@foo.com/login/',
+ not_secret='following the leader', msg='here'),
+ dict(one=1, pwd=OMIT, url='https://username:password12345@foo.com/login/',
+ not_secret='following the leader', msg='here',
+ invocation=dict(module_args=dict(password=OMIT, token=None, username='person'))),
+ ),
+ (
+ dict(username='person', password='password12345'),
+ dict(one=1, pwd='$ecret k3y', url='https://username:password12345@foo.com/login/',
+ not_secret='following the leader', msg='here'),
+ dict(one=1, pwd='$ecret k3y', url='https://username:********@foo.com/login/',
+ not_secret='following the leader', msg='here',
+ invocation=dict(module_args=dict(password=OMIT, token=None, username='person'))),
+ ),
+ (
+ dict(username='person', password='$ecret k3y'),
+ dict(one=1, pwd='$ecret k3y', url='https://username:$ecret k3y@foo.com/login/',
+ not_secret='following the leader', msg='here'),
+ dict(one=1, pwd=OMIT, url='https://username:********@foo.com/login/',
+ not_secret='following the leader', msg='here',
+ invocation=dict(module_args=dict(password=OMIT, token=None, username='person'))),
+ ),
+ )
+
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ @pytest.mark.parametrize('am, stdin, return_val, expected',
+ (({'username': {}, 'password': {'no_log': True}, 'token': {'no_log': True}}, s, r, e)
+ for s, r, e in DATA), # pylint: disable=undefined-variable
+ indirect=['am', 'stdin'])
+ def test_exit_json_removes_values(self, am, capfd, return_val, expected, monkeypatch):
+ monkeypatch.setattr(warnings, '_global_deprecations', [])
+ with pytest.raises(SystemExit):
+ am.exit_json(**return_val)
+ out, err = capfd.readouterr()
+
+ assert json.loads(out) == expected
+
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ @pytest.mark.parametrize('am, stdin, return_val, expected',
+ (({'username': {}, 'password': {'no_log': True}, 'token': {'no_log': True}}, s, r, e)
+ for s, r, e in DATA), # pylint: disable=undefined-variable
+ indirect=['am', 'stdin'])
+ def test_fail_json_removes_values(self, am, capfd, return_val, expected, monkeypatch):
+ monkeypatch.setattr(warnings, '_global_deprecations', [])
+ expected['failed'] = True
+ with pytest.raises(SystemExit):
+ am.fail_json(**return_val) == expected
+ out, err = capfd.readouterr()
+
+ assert json.loads(out) == expected
diff --git a/test/units/module_utils/basic/test_filesystem.py b/test/units/module_utils/basic/test_filesystem.py
new file mode 100644
index 0000000..f09cecf
--- /dev/null
+++ b/test/units/module_utils/basic/test_filesystem.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# (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 units.mock.procenv import ModuleTestCase
+
+from units.compat.mock import patch, MagicMock
+from ansible.module_utils.six.moves import builtins
+
+realimport = builtins.__import__
+
+
+class TestOtherFilesystem(ModuleTestCase):
+ def test_module_utils_basic_ansible_module_user_and_group(self):
+ from ansible.module_utils import basic
+ basic._ANSIBLE_ARGS = None
+
+ am = basic.AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ mock_stat = MagicMock()
+ mock_stat.st_uid = 0
+ mock_stat.st_gid = 0
+
+ with patch('os.lstat', return_value=mock_stat):
+ self.assertEqual(am.user_and_group('/path/to/file'), (0, 0))
+
+ def test_module_utils_basic_ansible_module_find_mount_point(self):
+ from ansible.module_utils import basic
+ basic._ANSIBLE_ARGS = None
+
+ am = basic.AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ def _mock_ismount(path):
+ if path == b'/':
+ return True
+ return False
+
+ with patch('os.path.ismount', side_effect=_mock_ismount):
+ self.assertEqual(am.find_mount_point('/root/fs/../mounted/path/to/whatever'), '/')
+
+ def _mock_ismount(path):
+ if path == b'/subdir/mount':
+ return True
+ if path == b'/':
+ return True
+ return False
+
+ with patch('os.path.ismount', side_effect=_mock_ismount):
+ self.assertEqual(am.find_mount_point('/subdir/mount/path/to/whatever'), '/subdir/mount')
+
+ def test_module_utils_basic_ansible_module_set_owner_if_different(self):
+ from ansible.module_utils import basic
+ basic._ANSIBLE_ARGS = None
+
+ am = basic.AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ self.assertEqual(am.set_owner_if_different('/path/to/file', None, True), True)
+ self.assertEqual(am.set_owner_if_different('/path/to/file', None, False), False)
+
+ am.user_and_group = MagicMock(return_value=(500, 500))
+
+ with patch('os.lchown', return_value=None) as m:
+ self.assertEqual(am.set_owner_if_different('/path/to/file', 0, False), True)
+ m.assert_called_with(b'/path/to/file', 0, -1)
+
+ def _mock_getpwnam(*args, **kwargs):
+ mock_pw = MagicMock()
+ mock_pw.pw_uid = 0
+ return mock_pw
+
+ m.reset_mock()
+ with patch('pwd.getpwnam', side_effect=_mock_getpwnam):
+ self.assertEqual(am.set_owner_if_different('/path/to/file', 'root', False), True)
+ m.assert_called_with(b'/path/to/file', 0, -1)
+
+ with patch('pwd.getpwnam', side_effect=KeyError):
+ self.assertRaises(SystemExit, am.set_owner_if_different, '/path/to/file', 'root', False)
+
+ m.reset_mock()
+ am.check_mode = True
+ self.assertEqual(am.set_owner_if_different('/path/to/file', 0, False), True)
+ self.assertEqual(m.called, False)
+ am.check_mode = False
+
+ with patch('os.lchown', side_effect=OSError) as m:
+ self.assertRaises(SystemExit, am.set_owner_if_different, '/path/to/file', 'root', False)
+
+ def test_module_utils_basic_ansible_module_set_group_if_different(self):
+ from ansible.module_utils import basic
+ basic._ANSIBLE_ARGS = None
+
+ am = basic.AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ self.assertEqual(am.set_group_if_different('/path/to/file', None, True), True)
+ self.assertEqual(am.set_group_if_different('/path/to/file', None, False), False)
+
+ am.user_and_group = MagicMock(return_value=(500, 500))
+
+ with patch('os.lchown', return_value=None) as m:
+ self.assertEqual(am.set_group_if_different('/path/to/file', 0, False), True)
+ m.assert_called_with(b'/path/to/file', -1, 0)
+
+ def _mock_getgrnam(*args, **kwargs):
+ mock_gr = MagicMock()
+ mock_gr.gr_gid = 0
+ return mock_gr
+
+ m.reset_mock()
+ with patch('grp.getgrnam', side_effect=_mock_getgrnam):
+ self.assertEqual(am.set_group_if_different('/path/to/file', 'root', False), True)
+ m.assert_called_with(b'/path/to/file', -1, 0)
+
+ with patch('grp.getgrnam', side_effect=KeyError):
+ self.assertRaises(SystemExit, am.set_group_if_different, '/path/to/file', 'root', False)
+
+ m.reset_mock()
+ am.check_mode = True
+ self.assertEqual(am.set_group_if_different('/path/to/file', 0, False), True)
+ self.assertEqual(m.called, False)
+ am.check_mode = False
+
+ with patch('os.lchown', side_effect=OSError) as m:
+ self.assertRaises(SystemExit, am.set_group_if_different, '/path/to/file', 'root', False)
+
+ def test_module_utils_basic_ansible_module_set_directory_attributes_if_different(self):
+ from ansible.module_utils import basic
+ basic._ANSIBLE_ARGS = None
+
+ am = basic.AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ file_args = {
+ 'path': '/path/to/file',
+ 'mode': None,
+ 'owner': None,
+ 'group': None,
+ 'seuser': None,
+ 'serole': None,
+ 'setype': None,
+ 'selevel': None,
+ 'secontext': [None, None, None],
+ 'attributes': None,
+ }
+
+ self.assertEqual(am.set_directory_attributes_if_different(file_args, True), True)
+ self.assertEqual(am.set_directory_attributes_if_different(file_args, False), False)
diff --git a/test/units/module_utils/basic/test_get_file_attributes.py b/test/units/module_utils/basic/test_get_file_attributes.py
new file mode 100644
index 0000000..836529c
--- /dev/null
+++ b/test/units/module_utils/basic/test_get_file_attributes.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# Copyright:
+# (c) 2017, Pierre-Louis Bonicoli <pierre-louis@libregerbil.fr>
+# License: 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 itertools import product
+
+from ansible.module_utils.basic import AnsibleModule
+
+import pytest
+
+
+DATA = (
+ (
+ '3353595900 --------------e---- /usr/lib32',
+ {'attr_flags': 'e', 'version': '3353595900', 'attributes': ['extents']}
+ ),
+ # with e2fsprogs < 1.43, output isn't aligned
+ (
+ '78053594 -----------I--e---- /usr/lib',
+ {'attr_flags': 'Ie', 'version': '78053594', 'attributes': ['indexed', 'extents']}
+ ),
+ (
+ '15711607 -------A------e---- /tmp/test',
+ {'attr_flags': 'Ae', 'version': '15711607', 'attributes': ['noatime', 'extents']}
+ ),
+ # with e2fsprogs >= 1.43, output is aligned
+ (
+ '78053594 -----------I--e---- /usr/lib',
+ {'attr_flags': 'Ie', 'version': '78053594', 'attributes': ['indexed', 'extents']}
+ ),
+ (
+ '15711607 -------A------e---- /tmp/test',
+ {'attr_flags': 'Ae', 'version': '15711607', 'attributes': ['noatime', 'extents']}
+ ),
+)
+
+NO_VERSION_DATA = (
+ (
+ '--------------e---- /usr/lib32',
+ {'attr_flags': 'e', 'attributes': ['extents']}
+ ),
+ (
+ '-----------I--e---- /usr/lib',
+ {'attr_flags': 'Ie', 'attributes': ['indexed', 'extents']}
+ ),
+ (
+ '-------A------e---- /tmp/test',
+ {'attr_flags': 'Ae', 'attributes': ['noatime', 'extents']}
+ ),
+)
+
+
+@pytest.mark.parametrize('stdin, data', product(({},), DATA), indirect=['stdin'])
+def test_get_file_attributes(am, stdin, mocker, data):
+ # Test #18731
+ mocker.patch.object(AnsibleModule, 'get_bin_path', return_value=(0, '/usr/bin/lsattr', ''))
+ mocker.patch.object(AnsibleModule, 'run_command', return_value=(0, data[0], ''))
+ result = am.get_file_attributes('/path/to/file')
+ for key, value in data[1].items():
+ assert key in result and result[key] == value
+
+
+@pytest.mark.parametrize('stdin, data', product(({},), NO_VERSION_DATA), indirect=['stdin'])
+def test_get_file_attributes_no_version(am, stdin, mocker, data):
+ # Test #18731
+ mocker.patch.object(AnsibleModule, 'get_bin_path', return_value=(0, '/usr/bin/lsattr', ''))
+ mocker.patch.object(AnsibleModule, 'run_command', return_value=(0, data[0], ''))
+ result = am.get_file_attributes('/path/to/file', include_version=False)
+ for key, value in data[1].items():
+ assert key in result and result[key] == value
diff --git a/test/units/module_utils/basic/test_get_module_path.py b/test/units/module_utils/basic/test_get_module_path.py
new file mode 100644
index 0000000..6ff4a3b
--- /dev/null
+++ b/test/units/module_utils/basic/test_get_module_path.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# (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 units.mock.procenv import ModuleTestCase
+
+from units.compat.mock import patch
+from ansible.module_utils.six.moves import builtins
+
+realimport = builtins.__import__
+
+
+class TestGetModulePath(ModuleTestCase):
+ def test_module_utils_basic_get_module_path(self):
+ from ansible.module_utils.basic import get_module_path
+ with patch('os.path.realpath', return_value='/path/to/foo/'):
+ self.assertEqual(get_module_path(), '/path/to/foo')
diff --git a/test/units/module_utils/basic/test_heuristic_log_sanitize.py b/test/units/module_utils/basic/test_heuristic_log_sanitize.py
new file mode 100644
index 0000000..664b8a5
--- /dev/null
+++ b/test/units/module_utils/basic/test_heuristic_log_sanitize.py
@@ -0,0 +1,92 @@
+# -*- coding: utf-8 -*-
+# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from ansible.module_utils.basic import heuristic_log_sanitize
+
+
+class TestHeuristicLogSanitize(unittest.TestCase):
+ def setUp(self):
+ self.URL_SECRET = 'http://username:pas:word@foo.com/data'
+ self.SSH_SECRET = 'username:pas:word@foo.com/data'
+ self.clean_data = repr(self._gen_data(3, True, True, 'no_secret_here'))
+ self.url_data = repr(self._gen_data(3, True, True, self.URL_SECRET))
+ self.ssh_data = repr(self._gen_data(3, True, True, self.SSH_SECRET))
+
+ def _gen_data(self, records, per_rec, top_level, secret_text):
+ hostvars = {'hostvars': {}}
+ for i in range(1, records, 1):
+ host_facts = {
+ 'host%s' % i: {
+ 'pstack': {
+ 'running': '875.1',
+ 'symlinked': '880.0',
+ 'tars': [],
+ 'versions': ['885.0']
+ },
+ }
+ }
+ if per_rec:
+ host_facts['host%s' % i]['secret'] = secret_text
+ hostvars['hostvars'].update(host_facts)
+ if top_level:
+ hostvars['secret'] = secret_text
+ return hostvars
+
+ def test_did_not_hide_too_much(self):
+ self.assertEqual(heuristic_log_sanitize(self.clean_data), self.clean_data)
+
+ def test_hides_url_secrets(self):
+ url_output = heuristic_log_sanitize(self.url_data)
+ # Basic functionality: Successfully hid the password
+ self.assertNotIn('pas:word', url_output)
+
+ # Slightly more advanced, we hid all of the password despite the ":"
+ self.assertNotIn('pas', url_output)
+
+ # In this implementation we replace the password with 8 "*" which is
+ # also the length of our password. The url fields should be able to
+ # accurately detect where the password ends so the length should be
+ # the same:
+ self.assertEqual(len(url_output), len(self.url_data))
+
+ def test_hides_ssh_secrets(self):
+ ssh_output = heuristic_log_sanitize(self.ssh_data)
+ self.assertNotIn('pas:word', ssh_output)
+
+ # Slightly more advanced, we hid all of the password despite the ":"
+ self.assertNotIn('pas', ssh_output)
+
+ # ssh checking is harder as the heuristic is overzealous in many
+ # cases. Since the input will have at least one ":" present before
+ # the password we can tell some things about the beginning and end of
+ # the data, though:
+ self.assertTrue(ssh_output.startswith("{'"))
+ self.assertTrue(ssh_output.endswith("}"))
+ self.assertIn(":********@foo.com/data'", ssh_output)
+
+ def test_hides_parameter_secrets(self):
+ output = heuristic_log_sanitize('token="secret", user="person", token_entry="test=secret"', frozenset(['secret']))
+ self.assertNotIn('secret', output)
+
+ def test_no_password(self):
+ self.assertEqual(heuristic_log_sanitize('foo@bar'), 'foo@bar')
diff --git a/test/units/module_utils/basic/test_imports.py b/test/units/module_utils/basic/test_imports.py
new file mode 100644
index 0000000..d1a5f37
--- /dev/null
+++ b/test/units/module_utils/basic/test_imports.py
@@ -0,0 +1,128 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# (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
+
+import sys
+
+from units.mock.procenv import ModuleTestCase
+
+from units.compat import unittest
+from units.compat.mock import patch
+from ansible.module_utils.six.moves import builtins
+
+realimport = builtins.__import__
+
+
+class TestImports(ModuleTestCase):
+
+ def clear_modules(self, mods):
+ for mod in mods:
+ if mod in sys.modules:
+ del sys.modules[mod]
+
+ @patch.object(builtins, '__import__')
+ def test_module_utils_basic_import_syslog(self, mock_import):
+ def _mock_import(name, *args, **kwargs):
+ if name == 'syslog':
+ raise ImportError
+ return realimport(name, *args, **kwargs)
+
+ self.clear_modules(['syslog', 'ansible.module_utils.basic'])
+ mod = builtins.__import__('ansible.module_utils.basic')
+ self.assertTrue(mod.module_utils.basic.HAS_SYSLOG)
+
+ self.clear_modules(['syslog', 'ansible.module_utils.basic'])
+ mock_import.side_effect = _mock_import
+ mod = builtins.__import__('ansible.module_utils.basic')
+ self.assertFalse(mod.module_utils.basic.HAS_SYSLOG)
+
+ @patch.object(builtins, '__import__')
+ def test_module_utils_basic_import_selinux(self, mock_import):
+ def _mock_import(name, globals=None, locals=None, fromlist=tuple(), level=0, **kwargs):
+ if name == 'ansible.module_utils.compat' and fromlist == ('selinux',):
+ raise ImportError
+ return realimport(name, globals=globals, locals=locals, fromlist=fromlist, level=level, **kwargs)
+
+ try:
+ self.clear_modules(['ansible.module_utils.compat.selinux', 'ansible.module_utils.basic'])
+ mod = builtins.__import__('ansible.module_utils.basic')
+ self.assertTrue(mod.module_utils.basic.HAVE_SELINUX)
+ except ImportError:
+ # no selinux on test system, so skip
+ pass
+
+ self.clear_modules(['ansible.module_utils.compat.selinux', 'ansible.module_utils.basic'])
+ mock_import.side_effect = _mock_import
+ mod = builtins.__import__('ansible.module_utils.basic')
+ self.assertFalse(mod.module_utils.basic.HAVE_SELINUX)
+
+ @patch.object(builtins, '__import__')
+ def test_module_utils_basic_import_json(self, mock_import):
+ def _mock_import(name, *args, **kwargs):
+ if name == 'ansible.module_utils.common._json_compat':
+ raise ImportError
+ return realimport(name, *args, **kwargs)
+
+ self.clear_modules(['json', 'ansible.module_utils.basic'])
+ builtins.__import__('ansible.module_utils.basic')
+ self.clear_modules(['json', 'ansible.module_utils.basic'])
+ mock_import.side_effect = _mock_import
+ with self.assertRaises(SystemExit):
+ builtins.__import__('ansible.module_utils.basic')
+
+ # FIXME: doesn't work yet
+ # @patch.object(builtins, 'bytes')
+ # def test_module_utils_basic_bytes(self, mock_bytes):
+ # mock_bytes.side_effect = NameError()
+ # from ansible.module_utils import basic
+
+ @patch.object(builtins, '__import__')
+ @unittest.skipIf(sys.version_info[0] >= 3, "literal_eval is available in every version of Python3")
+ def test_module_utils_basic_import_literal_eval(self, mock_import):
+ def _mock_import(name, *args, **kwargs):
+ try:
+ fromlist = kwargs.get('fromlist', args[2])
+ except IndexError:
+ fromlist = []
+ if name == 'ast' and 'literal_eval' in fromlist:
+ raise ImportError
+ return realimport(name, *args, **kwargs)
+
+ mock_import.side_effect = _mock_import
+ self.clear_modules(['ast', 'ansible.module_utils.basic'])
+ mod = builtins.__import__('ansible.module_utils.basic')
+ self.assertEqual(mod.module_utils.basic.literal_eval("'1'"), "1")
+ self.assertEqual(mod.module_utils.basic.literal_eval("1"), 1)
+ self.assertEqual(mod.module_utils.basic.literal_eval("-1"), -1)
+ self.assertEqual(mod.module_utils.basic.literal_eval("(1,2,3)"), (1, 2, 3))
+ self.assertEqual(mod.module_utils.basic.literal_eval("[1]"), [1])
+ self.assertEqual(mod.module_utils.basic.literal_eval("True"), True)
+ self.assertEqual(mod.module_utils.basic.literal_eval("False"), False)
+ self.assertEqual(mod.module_utils.basic.literal_eval("None"), None)
+ # self.assertEqual(mod.module_utils.basic.literal_eval('{"a": 1}'), dict(a=1))
+ self.assertRaises(ValueError, mod.module_utils.basic.literal_eval, "asdfasdfasdf")
+
+ @patch.object(builtins, '__import__')
+ def test_module_utils_basic_import_systemd_journal(self, mock_import):
+ def _mock_import(name, *args, **kwargs):
+ try:
+ fromlist = kwargs.get('fromlist', args[2])
+ except IndexError:
+ fromlist = []
+ if name == 'systemd' and 'journal' in fromlist:
+ raise ImportError
+ return realimport(name, *args, **kwargs)
+
+ self.clear_modules(['systemd', 'ansible.module_utils.basic'])
+ mod = builtins.__import__('ansible.module_utils.basic')
+ self.assertTrue(mod.module_utils.basic.has_journal)
+
+ self.clear_modules(['systemd', 'ansible.module_utils.basic'])
+ mock_import.side_effect = _mock_import
+ mod = builtins.__import__('ansible.module_utils.basic')
+ self.assertFalse(mod.module_utils.basic.has_journal)
diff --git a/test/units/module_utils/basic/test_log.py b/test/units/module_utils/basic/test_log.py
new file mode 100644
index 0000000..f3f764f
--- /dev/null
+++ b/test/units/module_utils/basic/test_log.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (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
+
+import syslog
+from itertools import product
+
+import pytest
+
+import ansible.module_utils.basic
+from ansible.module_utils.six import PY3
+
+
+class TestAnsibleModuleLogSmokeTest:
+ DATA = [u'Text string', u'Toshio くらとみ non-ascii test']
+ DATA = DATA + [d.encode('utf-8') for d in DATA]
+ DATA += [b'non-utf8 :\xff: test']
+
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ @pytest.mark.parametrize('msg, stdin', ((m, {}) for m in DATA), indirect=['stdin']) # pylint: disable=undefined-variable
+ def test_smoketest_syslog(self, am, mocker, msg):
+ # These talk to the live daemons on the system. Need to do this to
+ # show that what we send doesn't cause an issue once it gets to the
+ # daemon. These are just smoketests to test that we don't fail.
+ mocker.patch('ansible.module_utils.basic.has_journal', False)
+
+ am.log(u'Text string')
+ am.log(u'Toshio くらとみ non-ascii test')
+
+ am.log(b'Byte string')
+ am.log(u'Toshio くらとみ non-ascii test'.encode('utf-8'))
+ am.log(b'non-utf8 :\xff: test')
+
+ @pytest.mark.skipif(not ansible.module_utils.basic.has_journal, reason='python systemd bindings not installed')
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ @pytest.mark.parametrize('msg, stdin', ((m, {}) for m in DATA), indirect=['stdin']) # pylint: disable=undefined-variable
+ def test_smoketest_journal(self, am, mocker, msg):
+ # These talk to the live daemons on the system. Need to do this to
+ # show that what we send doesn't cause an issue once it gets to the
+ # daemon. These are just smoketests to test that we don't fail.
+ mocker.patch('ansible.module_utils.basic.has_journal', True)
+
+ am.log(u'Text string')
+ am.log(u'Toshio くらとみ non-ascii test')
+
+ am.log(b'Byte string')
+ am.log(u'Toshio くらとみ non-ascii test'.encode('utf-8'))
+ am.log(b'non-utf8 :\xff: test')
+
+
+class TestAnsibleModuleLogSyslog:
+ """Test the AnsibleModule Log Method"""
+
+ PY2_OUTPUT_DATA = [
+ (u'Text string', b'Text string'),
+ (u'Toshio くらとみ non-ascii test', u'Toshio くらとみ non-ascii test'.encode('utf-8')),
+ (b'Byte string', b'Byte string'),
+ (u'Toshio くらとみ non-ascii test'.encode('utf-8'), u'Toshio くらとみ non-ascii test'.encode('utf-8')),
+ (b'non-utf8 :\xff: test', b'non-utf8 :\xff: test'.decode('utf-8', 'replace').encode('utf-8')),
+ ]
+
+ PY3_OUTPUT_DATA = [
+ (u'Text string', u'Text string'),
+ (u'Toshio くらとみ non-ascii test', u'Toshio くらとみ non-ascii test'),
+ (b'Byte string', u'Byte string'),
+ (u'Toshio くらとみ non-ascii test'.encode('utf-8'), u'Toshio くらとみ non-ascii test'),
+ (b'non-utf8 :\xff: test', b'non-utf8 :\xff: test'.decode('utf-8', 'replace')),
+ ]
+
+ OUTPUT_DATA = PY3_OUTPUT_DATA if PY3 else PY2_OUTPUT_DATA
+
+ @pytest.mark.parametrize('no_log, stdin', (product((True, False), [{}])), indirect=['stdin'])
+ def test_no_log(self, am, mocker, no_log):
+ """Test that when no_log is set, logging does not occur"""
+ mock_syslog = mocker.patch('syslog.syslog', autospec=True)
+ mocker.patch('ansible.module_utils.basic.has_journal', False)
+ am.no_log = no_log
+ am.log('unittest no_log')
+ if no_log:
+ assert not mock_syslog.called
+ else:
+ mock_syslog.assert_called_once_with(syslog.LOG_INFO, 'unittest no_log')
+
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ @pytest.mark.parametrize('msg, param, stdin',
+ ((m, p, {}) for m, p in OUTPUT_DATA), # pylint: disable=undefined-variable
+ indirect=['stdin'])
+ def test_output_matches(self, am, mocker, msg, param):
+ """Check that log messages are sent correctly"""
+ mocker.patch('ansible.module_utils.basic.has_journal', False)
+ mock_syslog = mocker.patch('syslog.syslog', autospec=True)
+
+ am.log(msg)
+ mock_syslog.assert_called_once_with(syslog.LOG_INFO, param)
+
+
+@pytest.mark.skipif(not ansible.module_utils.basic.has_journal, reason='python systemd bindings not installed')
+class TestAnsibleModuleLogJournal:
+ """Test the AnsibleModule Log Method"""
+
+ OUTPUT_DATA = [
+ (u'Text string', u'Text string'),
+ (u'Toshio くらとみ non-ascii test', u'Toshio くらとみ non-ascii test'),
+ (b'Byte string', u'Byte string'),
+ (u'Toshio くらとみ non-ascii test'.encode('utf-8'), u'Toshio くらとみ non-ascii test'),
+ (b'non-utf8 :\xff: test', b'non-utf8 :\xff: test'.decode('utf-8', 'replace')),
+ ]
+
+ @pytest.mark.parametrize('no_log, stdin', (product((True, False), [{}])), indirect=['stdin'])
+ def test_no_log(self, am, mocker, no_log):
+ journal_send = mocker.patch('systemd.journal.send')
+ am.no_log = no_log
+ am.log('unittest no_log')
+ if no_log:
+ assert not journal_send.called
+ else:
+ assert journal_send.called == 1
+ # Message
+ # call_args is a 2-tuple of (arg_list, kwarg_dict)
+ assert journal_send.call_args[1]['MESSAGE'].endswith('unittest no_log'), 'Message was not sent to log'
+ # log adds this journal field
+ assert 'MODULE' in journal_send.call_args[1]
+ assert 'basic.py' in journal_send.call_args[1]['MODULE']
+
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ @pytest.mark.parametrize('msg, param, stdin',
+ ((m, p, {}) for m, p in OUTPUT_DATA), # pylint: disable=undefined-variable
+ indirect=['stdin'])
+ def test_output_matches(self, am, mocker, msg, param):
+ journal_send = mocker.patch('systemd.journal.send')
+ am.log(msg)
+ assert journal_send.call_count == 1, 'journal.send not called exactly once'
+ assert journal_send.call_args[1]['MESSAGE'].endswith(param)
+
+ @pytest.mark.parametrize('stdin', ({},), indirect=['stdin'])
+ def test_log_args(self, am, mocker):
+ journal_send = mocker.patch('systemd.journal.send')
+ am.log('unittest log_args', log_args=dict(TEST='log unittest'))
+ assert journal_send.called == 1
+ assert journal_send.call_args[1]['MESSAGE'].endswith('unittest log_args'), 'Message was not sent to log'
+
+ # log adds this journal field
+ assert 'MODULE' in journal_send.call_args[1]
+ assert 'basic.py' in journal_send.call_args[1]['MODULE']
+
+ # We added this journal field
+ assert 'TEST' in journal_send.call_args[1]
+ assert 'log unittest' in journal_send.call_args[1]['TEST']
diff --git a/test/units/module_utils/basic/test_no_log.py b/test/units/module_utils/basic/test_no_log.py
new file mode 100644
index 0000000..c479702
--- /dev/null
+++ b/test/units/module_utils/basic/test_no_log.py
@@ -0,0 +1,160 @@
+# -*- coding: utf-8 -*-
+# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
+# (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 units.compat import unittest
+
+from ansible.module_utils.basic import remove_values
+from ansible.module_utils.common.parameters import _return_datastructure_name
+
+
+class TestReturnValues(unittest.TestCase):
+ dataset = (
+ ('string', frozenset(['string'])),
+ ('', frozenset()),
+ (1, frozenset(['1'])),
+ (1.0, frozenset(['1.0'])),
+ (False, frozenset()),
+ (['1', '2', '3'], frozenset(['1', '2', '3'])),
+ (('1', '2', '3'), frozenset(['1', '2', '3'])),
+ ({'one': 1, 'two': 'dos'}, frozenset(['1', 'dos'])),
+ (
+ {
+ 'one': 1,
+ 'two': 'dos',
+ 'three': [
+ 'amigos', 'musketeers', None, {
+ 'ping': 'pong',
+ 'base': (
+ 'balls', 'raquets'
+ )
+ }
+ ]
+ },
+ frozenset(['1', 'dos', 'amigos', 'musketeers', 'pong', 'balls', 'raquets'])
+ ),
+ (u'Toshio くらとみ', frozenset(['Toshio くらとみ'])),
+ ('Toshio くらとみ', frozenset(['Toshio くらとみ'])),
+ )
+
+ def test_return_datastructure_name(self):
+ for data, expected in self.dataset:
+ self.assertEqual(frozenset(_return_datastructure_name(data)), expected)
+
+ def test_unknown_type(self):
+ self.assertRaises(TypeError, frozenset, _return_datastructure_name(object()))
+
+
+class TestRemoveValues(unittest.TestCase):
+ OMIT = 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
+ dataset_no_remove = (
+ ('string', frozenset(['nope'])),
+ (1234, frozenset(['4321'])),
+ (False, frozenset(['4321'])),
+ (1.0, frozenset(['4321'])),
+ (['string', 'strang', 'strung'], frozenset(['nope'])),
+ ({'one': 1, 'two': 'dos', 'secret': 'key'}, frozenset(['nope'])),
+ (
+ {
+ 'one': 1,
+ 'two': 'dos',
+ 'three': [
+ 'amigos', 'musketeers', None, {
+ 'ping': 'pong', 'base': ['balls', 'raquets']
+ }
+ ]
+ },
+ frozenset(['nope'])
+ ),
+ (u'Toshio くら'.encode('utf-8'), frozenset([u'とみ'.encode('utf-8')])),
+ (u'Toshio くら', frozenset([u'とみ'])),
+ )
+ dataset_remove = (
+ ('string', frozenset(['string']), OMIT),
+ (1234, frozenset(['1234']), OMIT),
+ (1234, frozenset(['23']), OMIT),
+ (1.0, frozenset(['1.0']), OMIT),
+ (['string', 'strang', 'strung'], frozenset(['strang']), ['string', OMIT, 'strung']),
+ (['string', 'strang', 'strung'], frozenset(['strang', 'string', 'strung']), [OMIT, OMIT, OMIT]),
+ (('string', 'strang', 'strung'), frozenset(['string', 'strung']), [OMIT, 'strang', OMIT]),
+ ((1234567890, 345678, 987654321), frozenset(['1234567890']), [OMIT, 345678, 987654321]),
+ ((1234567890, 345678, 987654321), frozenset(['345678']), [OMIT, OMIT, 987654321]),
+ ({'one': 1, 'two': 'dos', 'secret': 'key'}, frozenset(['key']), {'one': 1, 'two': 'dos', 'secret': OMIT}),
+ ({'one': 1, 'two': 'dos', 'secret': 'key'}, frozenset(['key', 'dos', '1']), {'one': OMIT, 'two': OMIT, 'secret': OMIT}),
+ ({'one': 1, 'two': 'dos', 'secret': 'key'}, frozenset(['key', 'dos', '1']), {'one': OMIT, 'two': OMIT, 'secret': OMIT}),
+ (
+ {
+ 'one': 1,
+ 'two': 'dos',
+ 'three': [
+ 'amigos', 'musketeers', None, {
+ 'ping': 'pong', 'base': [
+ 'balls', 'raquets'
+ ]
+ }
+ ]
+ },
+ frozenset(['balls', 'base', 'pong', 'amigos']),
+ {
+ 'one': 1,
+ 'two': 'dos',
+ 'three': [
+ OMIT, 'musketeers', None, {
+ 'ping': OMIT,
+ 'base': [
+ OMIT, 'raquets'
+ ]
+ }
+ ]
+ }
+ ),
+ (
+ 'This sentence has an enigma wrapped in a mystery inside of a secret. - mr mystery',
+ frozenset(['enigma', 'mystery', 'secret']),
+ 'This sentence has an ******** wrapped in a ******** inside of a ********. - mr ********'
+ ),
+ (u'Toshio くらとみ'.encode('utf-8'), frozenset([u'くらとみ'.encode('utf-8')]), u'Toshio ********'.encode('utf-8')),
+ (u'Toshio くらとみ', frozenset([u'くらとみ']), u'Toshio ********'),
+ )
+
+ def test_no_removal(self):
+ for value, no_log_strings in self.dataset_no_remove:
+ self.assertEqual(remove_values(value, no_log_strings), value)
+
+ def test_strings_to_remove(self):
+ for value, no_log_strings, expected in self.dataset_remove:
+ self.assertEqual(remove_values(value, no_log_strings), expected)
+
+ def test_unknown_type(self):
+ self.assertRaises(TypeError, remove_values, object(), frozenset())
+
+ def test_hit_recursion_limit(self):
+ """ Check that we do not hit a recursion limit"""
+ data_list = []
+ inner_list = data_list
+ for i in range(0, 10000):
+ new_list = []
+ inner_list.append(new_list)
+ inner_list = new_list
+ inner_list.append('secret')
+
+ # Check that this does not hit a recursion limit
+ actual_data_list = remove_values(data_list, frozenset(('secret',)))
+
+ levels = 0
+ inner_list = actual_data_list
+ while inner_list:
+ if isinstance(inner_list, list):
+ self.assertEqual(len(inner_list), 1)
+ else:
+ levels -= 1
+ break
+ inner_list = inner_list[0]
+ levels += 1
+
+ self.assertEqual(inner_list, self.OMIT)
+ self.assertEqual(levels, 10000)
diff --git a/test/units/module_utils/basic/test_platform_distribution.py b/test/units/module_utils/basic/test_platform_distribution.py
new file mode 100644
index 0000000..3c1afb7
--- /dev/null
+++ b/test/units/module_utils/basic/test_platform_distribution.py
@@ -0,0 +1,188 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# (c) 2017-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
+
+import pytest
+
+from units.compat.mock import patch
+
+from ansible.module_utils.six.moves import builtins
+
+# Functions being tested
+from ansible.module_utils.basic import get_platform
+from ansible.module_utils.basic import get_all_subclasses
+from ansible.module_utils.basic import get_distribution
+from ansible.module_utils.basic import get_distribution_version
+from ansible.module_utils.basic import load_platform_subclass
+
+
+realimport = builtins.__import__
+
+
+@pytest.fixture
+def platform_linux(mocker):
+ mocker.patch('platform.system', return_value='Linux')
+
+
+#
+# get_platform tests
+#
+
+def test_get_platform():
+ with patch('platform.system', return_value='foo'):
+ assert get_platform() == 'foo'
+
+
+#
+# get_distribution tests
+#
+
+@pytest.mark.usefixtures("platform_linux")
+class TestGetDistribution:
+ """Tests for get_distribution that have to find something"""
+ def test_distro_known(self):
+ with patch('ansible.module_utils.distro.id', return_value="alpine"):
+ assert get_distribution() == "Alpine"
+
+ with patch('ansible.module_utils.distro.id', return_value="arch"):
+ assert get_distribution() == "Arch"
+
+ with patch('ansible.module_utils.distro.id', return_value="centos"):
+ assert get_distribution() == "Centos"
+
+ with patch('ansible.module_utils.distro.id', return_value="clear-linux-os"):
+ assert get_distribution() == "Clear-linux-os"
+
+ with patch('ansible.module_utils.distro.id', return_value="coreos"):
+ assert get_distribution() == "Coreos"
+
+ with patch('ansible.module_utils.distro.id', return_value="debian"):
+ assert get_distribution() == "Debian"
+
+ with patch('ansible.module_utils.distro.id', return_value="flatcar"):
+ assert get_distribution() == "Flatcar"
+
+ with patch('ansible.module_utils.distro.id', return_value="linuxmint"):
+ assert get_distribution() == "Linuxmint"
+
+ with patch('ansible.module_utils.distro.id', return_value="opensuse"):
+ assert get_distribution() == "Opensuse"
+
+ with patch('ansible.module_utils.distro.id', return_value="oracle"):
+ assert get_distribution() == "Oracle"
+
+ with patch('ansible.module_utils.distro.id', return_value="raspian"):
+ assert get_distribution() == "Raspian"
+
+ with patch('ansible.module_utils.distro.id', return_value="rhel"):
+ assert get_distribution() == "Redhat"
+
+ with patch('ansible.module_utils.distro.id', return_value="ubuntu"):
+ assert get_distribution() == "Ubuntu"
+
+ with patch('ansible.module_utils.distro.id', return_value="virtuozzo"):
+ assert get_distribution() == "Virtuozzo"
+
+ with patch('ansible.module_utils.distro.id', return_value="foo"):
+ assert get_distribution() == "Foo"
+
+ def test_distro_unknown(self):
+ with patch('ansible.module_utils.distro.id', return_value=""):
+ assert get_distribution() == "OtherLinux"
+
+ def test_distro_amazon_linux_short(self):
+ with patch('ansible.module_utils.distro.id', return_value="amzn"):
+ assert get_distribution() == "Amazon"
+
+ def test_distro_amazon_linux_long(self):
+ with patch('ansible.module_utils.distro.id', return_value="amazon"):
+ assert get_distribution() == "Amazon"
+
+
+#
+# get_distribution_version tests
+#
+
+
+@pytest.mark.usefixtures("platform_linux")
+def test_distro_found():
+ with patch('ansible.module_utils.distro.version', return_value="1"):
+ assert get_distribution_version() == "1"
+
+
+#
+# Tests for LoadPlatformSubclass
+#
+
+class TestLoadPlatformSubclass:
+ class LinuxTest:
+ pass
+
+ class Foo(LinuxTest):
+ platform = "Linux"
+ distribution = None
+
+ class Bar(LinuxTest):
+ platform = "Linux"
+ distribution = "Bar"
+
+ def test_not_linux(self):
+ # if neither match, the fallback should be the top-level class
+ with patch('platform.system', return_value="Foo"):
+ with patch('ansible.module_utils.common.sys_info.get_distribution', return_value=None):
+ assert isinstance(load_platform_subclass(self.LinuxTest), self.LinuxTest)
+
+ @pytest.mark.usefixtures("platform_linux")
+ def test_get_distribution_none(self):
+ # match just the platform class, not a specific distribution
+ with patch('ansible.module_utils.common.sys_info.get_distribution', return_value=None):
+ assert isinstance(load_platform_subclass(self.LinuxTest), self.Foo)
+
+ @pytest.mark.usefixtures("platform_linux")
+ def test_get_distribution_found(self):
+ # match both the distribution and platform class
+ with patch('ansible.module_utils.common.sys_info.get_distribution', return_value="Bar"):
+ assert isinstance(load_platform_subclass(self.LinuxTest), self.Bar)
+
+
+#
+# Tests for get_all_subclasses
+#
+
+class TestGetAllSubclasses:
+ class Base:
+ pass
+
+ class BranchI(Base):
+ pass
+
+ class BranchII(Base):
+ pass
+
+ class BranchIA(BranchI):
+ pass
+
+ class BranchIB(BranchI):
+ pass
+
+ class BranchIIA(BranchII):
+ pass
+
+ class BranchIIB(BranchII):
+ pass
+
+ def test_bottom_level(self):
+ assert get_all_subclasses(self.BranchIIB) == []
+
+ def test_one_inheritance(self):
+ assert set(get_all_subclasses(self.BranchII)) == set([self.BranchIIA, self.BranchIIB])
+
+ def test_toplevel(self):
+ assert set(get_all_subclasses(self.Base)) == set([self.BranchI, self.BranchII,
+ self.BranchIA, self.BranchIB,
+ self.BranchIIA, self.BranchIIB])
diff --git a/test/units/module_utils/basic/test_run_command.py b/test/units/module_utils/basic/test_run_command.py
new file mode 100644
index 0000000..04211e2
--- /dev/null
+++ b/test/units/module_utils/basic/test_run_command.py
@@ -0,0 +1,278 @@
+# -*- coding: utf-8 -*-
+# Copyright (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
+
+import errno
+from itertools import product
+from io import BytesIO
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.six import PY2
+from ansible.module_utils.compat import selectors
+
+
+class OpenBytesIO(BytesIO):
+ """BytesIO with dummy close() method
+
+ So that you can inspect the content after close() was called.
+ """
+
+ def close(self):
+ pass
+
+
+@pytest.fixture
+def mock_os(mocker):
+ def mock_os_chdir(path):
+ if path == '/inaccessible':
+ raise OSError(errno.EPERM, "Permission denied: '/inaccessible'")
+
+ def mock_os_abspath(path):
+ if path.startswith('/'):
+ return path
+ else:
+ return os.getcwd.return_value + '/' + path
+
+ os = mocker.patch('ansible.module_utils.basic.os')
+
+ os.path.expandvars.side_effect = lambda x: x
+ os.path.expanduser.side_effect = lambda x: x
+ os.environ = {'PATH': '/bin'}
+ os.getcwd.return_value = '/home/foo'
+ os.path.isdir.return_value = True
+ os.chdir.side_effect = mock_os_chdir
+ os.path.abspath.side_effect = mock_os_abspath
+
+ yield os
+
+
+class DummyFileObj():
+ def __init__(self, fileobj):
+ self.fileobj = fileobj
+
+
+class SpecialBytesIO(BytesIO):
+ def __init__(self, *args, **kwargs):
+ fh = kwargs.pop('fh', None)
+ super(SpecialBytesIO, self).__init__(*args, **kwargs)
+ self.fh = fh
+
+ def fileno(self):
+ return self.fh
+
+ # We need to do this because some of our tests create a new value for stdout and stderr
+ # The new value is able to affect the string that is returned by the subprocess stdout and
+ # stderr but by the time the test gets it, it is too late to change the SpecialBytesIO that
+ # subprocess.Popen returns for stdout and stderr. If we could figure out how to change those as
+ # well, then we wouldn't need this.
+ def __eq__(self, other):
+ if id(self) == id(other) or self.fh == other.fileno():
+ return True
+ return False
+
+
+class DummyKey:
+ def __init__(self, fileobj):
+ self.fileobj = fileobj
+
+
+@pytest.fixture
+def mock_subprocess(mocker):
+
+ class MockSelector(selectors.BaseSelector):
+ def __init__(self):
+ super(MockSelector, self).__init__()
+ self._file_objs = []
+
+ def register(self, fileobj, events, data=None):
+ self._file_objs.append(fileobj)
+
+ def unregister(self, fileobj):
+ self._file_objs.remove(fileobj)
+
+ def select(self, timeout=None):
+ ready = []
+ for file_obj in self._file_objs:
+ ready.append((DummyKey(subprocess._output[file_obj.fileno()]), selectors.EVENT_READ))
+ return ready
+
+ def get_map(self):
+ return self._file_objs
+
+ def close(self):
+ super(MockSelector, self).close()
+ self._file_objs = []
+
+ selectors.DefaultSelector = MockSelector
+
+ subprocess = mocker.patch('ansible.module_utils.basic.subprocess')
+ subprocess._output = {mocker.sentinel.stdout: SpecialBytesIO(b'', fh=mocker.sentinel.stdout),
+ mocker.sentinel.stderr: SpecialBytesIO(b'', fh=mocker.sentinel.stderr)}
+
+ cmd = mocker.MagicMock()
+ cmd.returncode = 0
+ cmd.stdin = OpenBytesIO()
+ cmd.stdout = subprocess._output[mocker.sentinel.stdout]
+ cmd.stderr = subprocess._output[mocker.sentinel.stderr]
+ subprocess.Popen.return_value = cmd
+
+ yield subprocess
+
+
+@pytest.fixture()
+def rc_am(mocker, am, mock_os, mock_subprocess):
+ am.fail_json = mocker.MagicMock(side_effect=SystemExit)
+ am._os = mock_os
+ am._subprocess = mock_subprocess
+ yield am
+
+
+class TestRunCommandArgs:
+ # Format is command as passed to run_command, command to Popen as list, command to Popen as string
+ ARGS_DATA = (
+ (['/bin/ls', 'a', 'b', 'c'], [b'/bin/ls', b'a', b'b', b'c'], b'/bin/ls a b c'),
+ ('/bin/ls a " b" "c "', [b'/bin/ls', b'a', b' b', b'c '], b'/bin/ls a " b" "c "'),
+ )
+
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ # pylint: disable=undefined-variable
+ @pytest.mark.parametrize('cmd, expected, shell, stdin',
+ ((arg, cmd_str if sh else cmd_lst, sh, {})
+ for (arg, cmd_lst, cmd_str), sh in product(ARGS_DATA, (True, False))),
+ indirect=['stdin'])
+ def test_args(self, cmd, expected, shell, rc_am):
+ rc_am.run_command(cmd, use_unsafe_shell=shell)
+ assert rc_am._subprocess.Popen.called
+ args, kwargs = rc_am._subprocess.Popen.call_args
+ assert args == (expected, )
+ assert kwargs['shell'] == shell
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_tuple_as_args(self, rc_am):
+ with pytest.raises(SystemExit):
+ rc_am.run_command(('ls', '/'))
+ assert rc_am.fail_json.called
+
+
+class TestRunCommandCwd:
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_cwd(self, mocker, rc_am):
+ rc_am.run_command('/bin/ls', cwd='/new')
+ assert rc_am._subprocess.Popen.mock_calls[0][2]['cwd'] == b'/new'
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_cwd_relative_path(self, mocker, rc_am):
+ rc_am.run_command('/bin/ls', cwd='sub-dir')
+ assert rc_am._subprocess.Popen.mock_calls[0][2]['cwd'] == b'/home/foo/sub-dir'
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_cwd_not_a_dir(self, mocker, rc_am):
+ rc_am.run_command('/bin/ls', cwd='/not-a-dir')
+ assert rc_am._subprocess.Popen.mock_calls[0][2]['cwd'] == b'/not-a-dir'
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_cwd_not_a_dir_noignore(self, rc_am):
+ rc_am._os.path.isdir.side_effect = lambda d: d != b'/not-a-dir'
+ with pytest.raises(SystemExit):
+ rc_am.run_command('/bin/ls', cwd='/not-a-dir', ignore_invalid_cwd=False)
+ assert rc_am.fail_json.called
+
+
+class TestRunCommandPrompt:
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_prompt_bad_regex(self, rc_am):
+ with pytest.raises(SystemExit):
+ rc_am.run_command('foo', prompt_regex='[pP)assword:')
+ assert rc_am.fail_json.called
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_prompt_no_match(self, mocker, rc_am):
+ rc_am._os._cmd_out[mocker.sentinel.stdout] = BytesIO(b'hello')
+ (rc, _, _) = rc_am.run_command('foo', prompt_regex='[pP]assword:')
+ assert rc == 0
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_prompt_match_wo_data(self, mocker, rc_am):
+ rc_am._subprocess._output = {mocker.sentinel.stdout:
+ SpecialBytesIO(b'Authentication required!\nEnter password: ',
+ fh=mocker.sentinel.stdout),
+ mocker.sentinel.stderr:
+ SpecialBytesIO(b'', fh=mocker.sentinel.stderr)}
+ (rc, _, _) = rc_am.run_command('foo', prompt_regex=r'[pP]assword:', data=None)
+ assert rc == 257
+
+
+class TestRunCommandRc:
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_check_rc_false(self, rc_am):
+ rc_am._subprocess.Popen.return_value.returncode = 1
+ (rc, _, _) = rc_am.run_command('/bin/false', check_rc=False)
+ assert rc == 1
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_check_rc_true(self, rc_am):
+ rc_am._subprocess.Popen.return_value.returncode = 1
+ with pytest.raises(SystemExit):
+ rc_am.run_command('/bin/false', check_rc=True)
+ assert rc_am.fail_json.called
+ args, kwargs = rc_am.fail_json.call_args
+ assert kwargs['rc'] == 1
+
+
+class TestRunCommandOutput:
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_text_stdin(self, rc_am):
+ (rc, stdout, stderr) = rc_am.run_command('/bin/foo', data='hello world')
+ assert rc_am._subprocess.Popen.return_value.stdin.getvalue() == b'hello world\n'
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_ascii_stdout(self, mocker, rc_am):
+ rc_am._subprocess._output = {mocker.sentinel.stdout:
+ SpecialBytesIO(b'hello', fh=mocker.sentinel.stdout),
+ mocker.sentinel.stderr:
+ SpecialBytesIO(b'', fh=mocker.sentinel.stderr)}
+ (rc, stdout, stderr) = rc_am.run_command('/bin/cat hello.txt')
+ assert rc == 0
+ # module_utils function. On py3 it returns text and py2 it returns
+ # bytes because it's returning native strings
+ assert stdout == 'hello'
+
+ @pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+ def test_utf8_output(self, mocker, rc_am):
+ rc_am._subprocess._output = {mocker.sentinel.stdout:
+ SpecialBytesIO(u'Žarn§'.encode('utf-8'),
+ fh=mocker.sentinel.stdout),
+ mocker.sentinel.stderr:
+ SpecialBytesIO(u'لرئيسية'.encode('utf-8'),
+ fh=mocker.sentinel.stderr)}
+ (rc, stdout, stderr) = rc_am.run_command('/bin/something_ugly')
+ assert rc == 0
+ # module_utils function. On py3 it returns text and py2 it returns
+ # bytes because it's returning native strings
+ assert stdout == to_native(u'Žarn§')
+ assert stderr == to_native(u'لرئيسية')
+
+
+@pytest.mark.parametrize('stdin', [{}], indirect=['stdin'])
+def test_run_command_fds(mocker, rc_am):
+ subprocess_mock = mocker.patch('ansible.module_utils.basic.subprocess')
+ subprocess_mock.Popen.side_effect = AssertionError
+
+ try:
+ rc_am.run_command('synchronize', pass_fds=(101, 42))
+ except SystemExit:
+ pass
+
+ if PY2:
+ assert subprocess_mock.Popen.call_args[1]['close_fds'] is False
+ assert 'pass_fds' not in subprocess_mock.Popen.call_args[1]
+
+ else:
+ assert subprocess_mock.Popen.call_args[1]['pass_fds'] == (101, 42)
+ assert subprocess_mock.Popen.call_args[1]['close_fds'] is True
diff --git a/test/units/module_utils/basic/test_safe_eval.py b/test/units/module_utils/basic/test_safe_eval.py
new file mode 100644
index 0000000..e8538ca
--- /dev/null
+++ b/test/units/module_utils/basic/test_safe_eval.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+# (c) 2015-2017, Toshio Kuratomi <tkuratomi@ansible.com>
+# 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 itertools import chain
+import pytest
+
+
+# Strings that should be converted into a typed value
+VALID_STRINGS = (
+ ("'a'", 'a'),
+ ("'1'", '1'),
+ ("1", 1),
+ ("True", True),
+ ("False", False),
+ ("{}", {}),
+)
+
+# Passing things that aren't strings should just return the object
+NONSTRINGS = (
+ ({'a': 1}, {'a': 1}),
+)
+
+# These strings are not basic types. For security, these should not be
+# executed. We return the same string and get an exception for some
+INVALID_STRINGS = (
+ ("a=1", "a=1", SyntaxError),
+ ("a.foo()", "a.foo()", None),
+ ("import foo", "import foo", None),
+ ("__import__('foo')", "__import__('foo')", ValueError),
+)
+
+
+@pytest.mark.parametrize('code, expected, stdin',
+ ((c, e, {}) for c, e in chain(VALID_STRINGS, NONSTRINGS)),
+ indirect=['stdin'])
+def test_simple_types(am, code, expected):
+ # test some basic usage for various types
+ assert am.safe_eval(code) == expected
+
+
+@pytest.mark.parametrize('code, expected, stdin',
+ ((c, e, {}) for c, e in chain(VALID_STRINGS, NONSTRINGS)),
+ indirect=['stdin'])
+def test_simple_types_with_exceptions(am, code, expected):
+ # Test simple types with exceptions requested
+ assert am.safe_eval(code, include_exceptions=True), (expected, None)
+
+
+@pytest.mark.parametrize('code, expected, stdin',
+ ((c, e, {}) for c, e, dummy in INVALID_STRINGS),
+ indirect=['stdin'])
+def test_invalid_strings(am, code, expected):
+ assert am.safe_eval(code) == expected
+
+
+@pytest.mark.parametrize('code, expected, exception, stdin',
+ ((c, e, ex, {}) for c, e, ex in INVALID_STRINGS),
+ indirect=['stdin'])
+def test_invalid_strings_with_exceptions(am, code, expected, exception):
+ res = am.safe_eval(code, include_exceptions=True)
+ assert res[0] == expected
+ if exception is None:
+ assert res[1] == exception
+ else:
+ assert type(res[1]) == exception
diff --git a/test/units/module_utils/basic/test_sanitize_keys.py b/test/units/module_utils/basic/test_sanitize_keys.py
new file mode 100644
index 0000000..180f866
--- /dev/null
+++ b/test/units/module_utils/basic/test_sanitize_keys.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# (c) 2020, Red Hat
+# 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 pytest
+from ansible.module_utils.basic import sanitize_keys
+
+
+def test_sanitize_keys_non_dict_types():
+ """ Test that non-dict-like objects return the same data. """
+
+ type_exception = 'Unsupported type for key sanitization.'
+ no_log_strings = set()
+
+ assert 'string value' == sanitize_keys('string value', no_log_strings)
+
+ assert sanitize_keys(None, no_log_strings) is None
+
+ assert set(['x', 'y']) == sanitize_keys(set(['x', 'y']), no_log_strings)
+
+ assert not sanitize_keys(False, no_log_strings)
+
+
+def _run_comparison(obj):
+ no_log_strings = set(['secret', 'password'])
+
+ ret = sanitize_keys(obj, no_log_strings)
+
+ expected = [
+ None,
+ True,
+ 100,
+ "some string",
+ set([1, 2]),
+ [1, 2],
+
+ {'key1': ['value1a', 'value1b'],
+ 'some-********': 'value-for-some-password',
+ 'key2': {'key3': set(['value3a', 'value3b']),
+ 'i-have-a-********': {'********-********': 'value-for-secret-password', 'key4': 'value4'}
+ }
+ },
+
+ {'foo': [{'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER': 1}]}
+ ]
+
+ assert ret == expected
+
+
+def test_sanitize_keys_dict():
+ """ Test that santize_keys works with a dict. """
+
+ d = [
+ None,
+ True,
+ 100,
+ "some string",
+ set([1, 2]),
+ [1, 2],
+
+ {'key1': ['value1a', 'value1b'],
+ 'some-password': 'value-for-some-password',
+ 'key2': {'key3': set(['value3a', 'value3b']),
+ 'i-have-a-secret': {'secret-password': 'value-for-secret-password', 'key4': 'value4'}
+ }
+ },
+
+ {'foo': [{'secret': 1}]}
+ ]
+
+ _run_comparison(d)
+
+
+def test_sanitize_keys_with_ignores():
+ """ Test that we can actually ignore keys. """
+
+ no_log_strings = set(['secret', 'rc'])
+ ignore_keys = set(['changed', 'rc', 'status'])
+
+ value = {'changed': True,
+ 'rc': 0,
+ 'test-rc': 1,
+ 'another-secret': 2,
+ 'status': 'okie dokie'}
+
+ # We expect to change 'test-rc' but NOT 'rc'.
+ expected = {'changed': True,
+ 'rc': 0,
+ 'test-********': 1,
+ 'another-********': 2,
+ 'status': 'okie dokie'}
+
+ ret = sanitize_keys(value, no_log_strings, ignore_keys)
+ assert ret == expected
diff --git a/test/units/module_utils/basic/test_selinux.py b/test/units/module_utils/basic/test_selinux.py
new file mode 100644
index 0000000..d855768
--- /dev/null
+++ b/test/units/module_utils/basic/test_selinux.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# (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
+
+import errno
+import json
+import pytest
+
+from units.compat.mock import mock_open, patch
+
+from ansible.module_utils import basic
+from ansible.module_utils.common.text.converters import to_bytes
+from ansible.module_utils.six.moves import builtins
+
+
+@pytest.fixture
+def no_args_module_exec():
+ with patch.object(basic, '_ANSIBLE_ARGS', b'{"ANSIBLE_MODULE_ARGS": {}}'):
+ yield # we're patching the global module object, so nothing to yield
+
+
+def no_args_module(selinux_enabled=None, selinux_mls_enabled=None):
+ am = basic.AnsibleModule(argument_spec={})
+ # just dirty-patch the wrappers on the object instance since it's short-lived
+ if isinstance(selinux_enabled, bool):
+ patch.object(am, 'selinux_enabled', return_value=selinux_enabled).start()
+ if isinstance(selinux_mls_enabled, bool):
+ patch.object(am, 'selinux_mls_enabled', return_value=selinux_mls_enabled).start()
+ return am
+
+
+# test AnsibleModule selinux wrapper methods
+@pytest.mark.usefixtures('no_args_module_exec')
+class TestSELinuxMU:
+ def test_selinux_enabled(self):
+ # test selinux unavailable
+ # selinux unavailable, should return false
+ with patch.object(basic, 'HAVE_SELINUX', False):
+ assert no_args_module().selinux_enabled() is False
+
+ # test selinux present/not-enabled
+ disabled_mod = no_args_module()
+ with patch('ansible.module_utils.compat.selinux.is_selinux_enabled', return_value=0):
+ assert disabled_mod.selinux_enabled() is False
+ # ensure value is cached (same answer after unpatching)
+ assert disabled_mod.selinux_enabled() is False
+ # and present / enabled
+ enabled_mod = no_args_module()
+ with patch('ansible.module_utils.compat.selinux.is_selinux_enabled', return_value=1):
+ assert enabled_mod.selinux_enabled() is True
+ # ensure value is cached (same answer after unpatching)
+ assert enabled_mod.selinux_enabled() is True
+
+ def test_selinux_mls_enabled(self):
+ # selinux unavailable, should return false
+ with patch.object(basic, 'HAVE_SELINUX', False):
+ assert no_args_module().selinux_mls_enabled() is False
+ # selinux disabled, should return false
+ with patch('ansible.module_utils.compat.selinux.is_selinux_mls_enabled', return_value=0):
+ assert no_args_module(selinux_enabled=False).selinux_mls_enabled() is False
+ # selinux enabled, should pass through the value of is_selinux_mls_enabled
+ with patch('ansible.module_utils.compat.selinux.is_selinux_mls_enabled', return_value=1):
+ assert no_args_module(selinux_enabled=True).selinux_mls_enabled() is True
+
+ def test_selinux_initial_context(self):
+ # selinux missing/disabled/enabled sans MLS is 3-element None
+ assert no_args_module(selinux_enabled=False, selinux_mls_enabled=False).selinux_initial_context() == [None, None, None]
+ assert no_args_module(selinux_enabled=True, selinux_mls_enabled=False).selinux_initial_context() == [None, None, None]
+ # selinux enabled with MLS is 4-element None
+ assert no_args_module(selinux_enabled=True, selinux_mls_enabled=True).selinux_initial_context() == [None, None, None, None]
+
+ def test_selinux_default_context(self):
+ # selinux unavailable
+ with patch.object(basic, 'HAVE_SELINUX', False):
+ assert no_args_module().selinux_default_context(path='/foo/bar') == [None, None, None]
+
+ am = no_args_module(selinux_enabled=True, selinux_mls_enabled=True)
+ # matchpathcon success
+ with patch('ansible.module_utils.compat.selinux.matchpathcon', return_value=[0, 'unconfined_u:object_r:default_t:s0']):
+ assert am.selinux_default_context(path='/foo/bar') == ['unconfined_u', 'object_r', 'default_t', 's0']
+
+ # matchpathcon fail (return initial context value)
+ with patch('ansible.module_utils.compat.selinux.matchpathcon', return_value=[-1, '']):
+ assert am.selinux_default_context(path='/foo/bar') == [None, None, None, None]
+
+ # matchpathcon OSError
+ with patch('ansible.module_utils.compat.selinux.matchpathcon', side_effect=OSError):
+ assert am.selinux_default_context(path='/foo/bar') == [None, None, None, None]
+
+ def test_selinux_context(self):
+ # selinux unavailable
+ with patch.object(basic, 'HAVE_SELINUX', False):
+ assert no_args_module().selinux_context(path='/foo/bar') == [None, None, None]
+
+ am = no_args_module(selinux_enabled=True, selinux_mls_enabled=True)
+ # lgetfilecon_raw passthru
+ with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', return_value=[0, 'unconfined_u:object_r:default_t:s0']):
+ assert am.selinux_context(path='/foo/bar') == ['unconfined_u', 'object_r', 'default_t', 's0']
+
+ # lgetfilecon_raw returned a failure
+ with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', return_value=[-1, '']):
+ assert am.selinux_context(path='/foo/bar') == [None, None, None, None]
+
+ # lgetfilecon_raw OSError (should bomb the module)
+ with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', side_effect=OSError(errno.ENOENT, 'NotFound')):
+ with pytest.raises(SystemExit):
+ am.selinux_context(path='/foo/bar')
+
+ with patch('ansible.module_utils.compat.selinux.lgetfilecon_raw', side_effect=OSError()):
+ with pytest.raises(SystemExit):
+ am.selinux_context(path='/foo/bar')
+
+ def test_is_special_selinux_path(self):
+ args = to_bytes(json.dumps(dict(ANSIBLE_MODULE_ARGS={'_ansible_selinux_special_fs': "nfs,nfsd,foos",
+ '_ansible_remote_tmp': "/tmp",
+ '_ansible_keep_remote_files': False})))
+
+ with patch.object(basic, '_ANSIBLE_ARGS', args):
+ am = basic.AnsibleModule(
+ argument_spec=dict(),
+ )
+
+ def _mock_find_mount_point(path):
+ if path.startswith('/some/path'):
+ return '/some/path'
+ elif path.startswith('/weird/random/fstype'):
+ return '/weird/random/fstype'
+ return '/'
+
+ am.find_mount_point = _mock_find_mount_point
+ am.selinux_context = lambda path: ['foo_u', 'foo_r', 'foo_t', 's0']
+
+ m = mock_open()
+ m.side_effect = OSError
+
+ with patch.object(builtins, 'open', m, create=True):
+ assert am.is_special_selinux_path('/some/path/that/should/be/nfs') == (False, None)
+
+ mount_data = [
+ '/dev/disk1 / ext4 rw,seclabel,relatime,data=ordered 0 0\n',
+ '10.1.1.1:/path/to/nfs /some/path nfs ro 0 0\n',
+ 'whatever /weird/random/fstype foos rw 0 0\n',
+ ]
+
+ # mock_open has a broken readlines() implementation apparently...
+ # this should work by default but doesn't, so we fix it
+ m = mock_open(read_data=''.join(mount_data))
+ m.return_value.readlines.return_value = mount_data
+
+ with patch.object(builtins, 'open', m, create=True):
+ assert am.is_special_selinux_path('/some/random/path') == (False, None)
+ assert am.is_special_selinux_path('/some/path/that/should/be/nfs') == (True, ['foo_u', 'foo_r', 'foo_t', 's0'])
+ assert am.is_special_selinux_path('/weird/random/fstype/path') == (True, ['foo_u', 'foo_r', 'foo_t', 's0'])
+
+ def test_set_context_if_different(self):
+ am = no_args_module(selinux_enabled=False)
+ assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], True) is True
+ assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is False
+
+ am = no_args_module(selinux_enabled=True, selinux_mls_enabled=True)
+ am.selinux_context = lambda path: ['bar_u', 'bar_r', None, None]
+ am.is_special_selinux_path = lambda path: (False, None)
+
+ with patch('ansible.module_utils.compat.selinux.lsetfilecon', return_value=0) as m:
+ assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True
+ m.assert_called_with('/path/to/file', 'foo_u:foo_r:foo_t:s0')
+ m.reset_mock()
+ am.check_mode = True
+ assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True
+ assert not m.called
+ am.check_mode = False
+
+ with patch('ansible.module_utils.compat.selinux.lsetfilecon', return_value=1):
+ with pytest.raises(SystemExit):
+ am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], True)
+
+ with patch('ansible.module_utils.compat.selinux.lsetfilecon', side_effect=OSError):
+ with pytest.raises(SystemExit):
+ am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], True)
+
+ am.is_special_selinux_path = lambda path: (True, ['sp_u', 'sp_r', 'sp_t', 's0'])
+
+ with patch('ansible.module_utils.compat.selinux.lsetfilecon', return_value=0) as m:
+ assert am.set_context_if_different('/path/to/file', ['foo_u', 'foo_r', 'foo_t', 's0'], False) is True
+ m.assert_called_with('/path/to/file', 'sp_u:sp_r:sp_t:s0')
diff --git a/test/units/module_utils/basic/test_set_cwd.py b/test/units/module_utils/basic/test_set_cwd.py
new file mode 100644
index 0000000..159236b
--- /dev/null
+++ b/test/units/module_utils/basic/test_set_cwd.py
@@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018 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
+
+import json
+import os
+import shutil
+import tempfile
+
+import pytest
+
+from units.compat.mock import patch, MagicMock
+from ansible.module_utils._text import to_bytes
+
+from ansible.module_utils import basic
+
+
+class TestAnsibleModuleSetCwd:
+
+ def test_set_cwd(self, monkeypatch):
+
+ '''make sure /tmp is used'''
+
+ def mock_getcwd():
+ return '/tmp'
+
+ def mock_access(path, perm):
+ return True
+
+ def mock_chdir(path):
+ pass
+
+ monkeypatch.setattr(os, 'getcwd', mock_getcwd)
+ monkeypatch.setattr(os, 'access', mock_access)
+ monkeypatch.setattr(basic, '_ANSIBLE_ARGS', to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': {}})))
+ with patch('time.time', return_value=42):
+ am = basic.AnsibleModule(argument_spec={})
+
+ result = am._set_cwd()
+ assert result == '/tmp'
+
+ def test_set_cwd_unreadable_use_self_tmpdir(self, monkeypatch):
+
+ '''pwd is not readable, use instance's tmpdir property'''
+
+ def mock_getcwd():
+ return '/tmp'
+
+ def mock_access(path, perm):
+ if path == '/tmp' and perm == 4:
+ return False
+ return True
+
+ def mock_expandvars(var):
+ if var == '$HOME':
+ return '/home/foobar'
+ return var
+
+ def mock_gettempdir():
+ return '/tmp/testdir'
+
+ def mock_chdir(path):
+ if path == '/tmp':
+ raise Exception()
+ return
+
+ monkeypatch.setattr(os, 'getcwd', mock_getcwd)
+ monkeypatch.setattr(os, 'chdir', mock_chdir)
+ monkeypatch.setattr(os, 'access', mock_access)
+ monkeypatch.setattr(os.path, 'expandvars', mock_expandvars)
+ monkeypatch.setattr(basic, '_ANSIBLE_ARGS', to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': {}})))
+ with patch('time.time', return_value=42):
+ am = basic.AnsibleModule(argument_spec={})
+
+ am._tmpdir = '/tmp2'
+ result = am._set_cwd()
+ assert result == am._tmpdir
+
+ def test_set_cwd_unreadable_use_home(self, monkeypatch):
+
+ '''cwd and instance tmpdir are unreadable, use home'''
+
+ def mock_getcwd():
+ return '/tmp'
+
+ def mock_access(path, perm):
+ if path in ['/tmp', '/tmp2'] and perm == 4:
+ return False
+ return True
+
+ def mock_expandvars(var):
+ if var == '$HOME':
+ return '/home/foobar'
+ return var
+
+ def mock_gettempdir():
+ return '/tmp/testdir'
+
+ def mock_chdir(path):
+ if path == '/tmp':
+ raise Exception()
+ return
+
+ monkeypatch.setattr(os, 'getcwd', mock_getcwd)
+ monkeypatch.setattr(os, 'chdir', mock_chdir)
+ monkeypatch.setattr(os, 'access', mock_access)
+ monkeypatch.setattr(os.path, 'expandvars', mock_expandvars)
+ monkeypatch.setattr(basic, '_ANSIBLE_ARGS', to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': {}})))
+ with patch('time.time', return_value=42):
+ am = basic.AnsibleModule(argument_spec={})
+
+ am._tmpdir = '/tmp2'
+ result = am._set_cwd()
+ assert result == '/home/foobar'
+
+ def test_set_cwd_unreadable_use_gettempdir(self, monkeypatch):
+
+ '''fallback to tempfile.gettempdir'''
+
+ thisdir = None
+
+ def mock_getcwd():
+ return '/tmp'
+
+ def mock_access(path, perm):
+ if path in ['/tmp', '/tmp2', '/home/foobar'] and perm == 4:
+ return False
+ return True
+
+ def mock_expandvars(var):
+ if var == '$HOME':
+ return '/home/foobar'
+ return var
+
+ def mock_gettempdir():
+ return '/tmp3'
+
+ def mock_chdir(path):
+ if path == '/tmp':
+ raise Exception()
+ thisdir = path
+
+ monkeypatch.setattr(os, 'getcwd', mock_getcwd)
+ monkeypatch.setattr(os, 'chdir', mock_chdir)
+ monkeypatch.setattr(os, 'access', mock_access)
+ monkeypatch.setattr(os.path, 'expandvars', mock_expandvars)
+ monkeypatch.setattr(basic, '_ANSIBLE_ARGS', to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': {}})))
+ with patch('time.time', return_value=42):
+ am = basic.AnsibleModule(argument_spec={})
+
+ am._tmpdir = '/tmp2'
+ monkeypatch.setattr(tempfile, 'gettempdir', mock_gettempdir)
+ result = am._set_cwd()
+ assert result == '/tmp3'
+
+ def test_set_cwd_unreadable_use_None(self, monkeypatch):
+
+ '''all paths are unreable, should return None and not an exception'''
+
+ def mock_getcwd():
+ return '/tmp'
+
+ def mock_access(path, perm):
+ if path in ['/tmp', '/tmp2', '/tmp3', '/home/foobar'] and perm == 4:
+ return False
+ return True
+
+ def mock_expandvars(var):
+ if var == '$HOME':
+ return '/home/foobar'
+ return var
+
+ def mock_gettempdir():
+ return '/tmp3'
+
+ def mock_chdir(path):
+ if path == '/tmp':
+ raise Exception()
+
+ monkeypatch.setattr(os, 'getcwd', mock_getcwd)
+ monkeypatch.setattr(os, 'chdir', mock_chdir)
+ monkeypatch.setattr(os, 'access', mock_access)
+ monkeypatch.setattr(os.path, 'expandvars', mock_expandvars)
+ monkeypatch.setattr(basic, '_ANSIBLE_ARGS', to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': {}})))
+ with patch('time.time', return_value=42):
+ am = basic.AnsibleModule(argument_spec={})
+
+ am._tmpdir = '/tmp2'
+ monkeypatch.setattr(tempfile, 'gettempdir', mock_gettempdir)
+ result = am._set_cwd()
+ assert result is None
diff --git a/test/units/module_utils/basic/test_set_mode_if_different.py b/test/units/module_utils/basic/test_set_mode_if_different.py
new file mode 100644
index 0000000..5fec331
--- /dev/null
+++ b/test/units/module_utils/basic/test_set_mode_if_different.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+# Copyright (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
+
+import errno
+import os
+
+from itertools import product
+
+try:
+ import builtins
+except ImportError:
+ import __builtin__ as builtins
+
+import pytest
+
+
+SYNONYMS_0660 = (
+ 0o660,
+ '0o660',
+ '660',
+ 'u+rw-x,g+rw-x,o-rwx',
+ 'u=rw,g=rw,o-rwx',
+)
+
+
+@pytest.fixture
+def mock_stats(mocker):
+ mock_stat1 = mocker.MagicMock()
+ mock_stat1.st_mode = 0o444
+ mock_stat2 = mocker.MagicMock()
+ mock_stat2.st_mode = 0o660
+ yield {"before": mock_stat1, "after": mock_stat2}
+
+
+@pytest.fixture
+def am_check_mode(am):
+ am.check_mode = True
+ yield am
+ am.check_mode = False
+
+
+@pytest.fixture
+def mock_lchmod(mocker):
+ m_lchmod = mocker.patch('ansible.module_utils.basic.os.lchmod', return_value=None, create=True)
+ yield m_lchmod
+
+
+@pytest.mark.parametrize('previous_changes, check_mode, exists, stdin',
+ product((True, False), (True, False), (True, False), ({},)),
+ indirect=['stdin'])
+def test_no_mode_given_returns_previous_changes(am, mock_stats, mock_lchmod, mocker, previous_changes, check_mode, exists):
+ am.check_mode = check_mode
+ mocker.patch('os.lstat', side_effect=[mock_stats['before']])
+ m_lchmod = mocker.patch('os.lchmod', return_value=None, create=True)
+ m_path_exists = mocker.patch('os.path.exists', return_value=exists)
+
+ assert am.set_mode_if_different('/path/to/file', None, previous_changes) == previous_changes
+ assert not m_lchmod.called
+ assert not m_path_exists.called
+
+
+@pytest.mark.parametrize('mode, check_mode, stdin',
+ product(SYNONYMS_0660, (True, False), ({},)),
+ indirect=['stdin'])
+def test_mode_changed_to_0660(am, mock_stats, mocker, mode, check_mode):
+ # Note: This is for checking that all the different ways of specifying
+ # 0660 mode work. It cannot be used to check that setting a mode that is
+ # not equivalent to 0660 works.
+ am.check_mode = check_mode
+ mocker.patch('os.lstat', side_effect=[mock_stats['before'], mock_stats['after'], mock_stats['after']])
+ m_lchmod = mocker.patch('os.lchmod', return_value=None, create=True)
+ mocker.patch('os.path.exists', return_value=True)
+
+ assert am.set_mode_if_different('/path/to/file', mode, False)
+ if check_mode:
+ assert not m_lchmod.called
+ else:
+ m_lchmod.assert_called_with(b'/path/to/file', 0o660)
+
+
+@pytest.mark.parametrize('mode, check_mode, stdin',
+ product(SYNONYMS_0660, (True, False), ({},)),
+ indirect=['stdin'])
+def test_mode_unchanged_when_already_0660(am, mock_stats, mocker, mode, check_mode):
+ # Note: This is for checking that all the different ways of specifying
+ # 0660 mode work. It cannot be used to check that setting a mode that is
+ # not equivalent to 0660 works.
+ am.check_mode = check_mode
+ mocker.patch('os.lstat', side_effect=[mock_stats['after'], mock_stats['after'], mock_stats['after']])
+ m_lchmod = mocker.patch('os.lchmod', return_value=None, create=True)
+ mocker.patch('os.path.exists', return_value=True)
+
+ assert not am.set_mode_if_different('/path/to/file', mode, False)
+ assert not m_lchmod.called
+
+
+@pytest.mark.parametrize('mode, stdin', product(SYNONYMS_0660, ({},)), indirect=['stdin'])
+def test_mode_changed_to_0660_check_mode_no_file(am, mocker, mode):
+ am.check_mode = True
+ mocker.patch('os.path.exists', return_value=False)
+ assert am.set_mode_if_different('/path/to/file', mode, False)
+
+
+@pytest.mark.parametrize('check_mode, stdin',
+ product((True, False), ({},)),
+ indirect=['stdin'])
+def test_missing_lchmod_is_not_link(am, mock_stats, mocker, monkeypatch, check_mode):
+ """Some platforms have lchmod (*BSD) others do not (Linux)"""
+
+ am.check_mode = check_mode
+ original_hasattr = hasattr
+
+ monkeypatch.delattr(os, 'lchmod', raising=False)
+
+ mocker.patch('os.lstat', side_effect=[mock_stats['before'], mock_stats['after']])
+ mocker.patch('os.path.islink', return_value=False)
+ mocker.patch('os.path.exists', return_value=True)
+ m_chmod = mocker.patch('os.chmod', return_value=None)
+
+ assert am.set_mode_if_different('/path/to/file/no_lchmod', 0o660, False)
+ if check_mode:
+ assert not m_chmod.called
+ else:
+ m_chmod.assert_called_with(b'/path/to/file/no_lchmod', 0o660)
+
+
+@pytest.mark.parametrize('check_mode, stdin',
+ product((True, False), ({},)),
+ indirect=['stdin'])
+def test_missing_lchmod_is_link(am, mock_stats, mocker, monkeypatch, check_mode):
+ """Some platforms have lchmod (*BSD) others do not (Linux)"""
+
+ am.check_mode = check_mode
+ original_hasattr = hasattr
+
+ monkeypatch.delattr(os, 'lchmod', raising=False)
+
+ mocker.patch('os.lstat', side_effect=[mock_stats['before'], mock_stats['after']])
+ mocker.patch('os.path.islink', return_value=True)
+ mocker.patch('os.path.exists', return_value=True)
+ m_chmod = mocker.patch('os.chmod', return_value=None)
+ mocker.patch('os.stat', return_value=mock_stats['after'])
+
+ assert am.set_mode_if_different('/path/to/file/no_lchmod', 0o660, False)
+ if check_mode:
+ assert not m_chmod.called
+ else:
+ m_chmod.assert_called_with(b'/path/to/file/no_lchmod', 0o660)
+
+ mocker.resetall()
+ mocker.stopall()
+
+
+@pytest.mark.parametrize('stdin,',
+ ({},),
+ indirect=['stdin'])
+def test_missing_lchmod_is_link_in_sticky_dir(am, mock_stats, mocker):
+ """Some platforms have lchmod (*BSD) others do not (Linux)"""
+
+ am.check_mode = False
+ original_hasattr = hasattr
+
+ def _hasattr(obj, name):
+ if obj == os and name == 'lchmod':
+ return False
+ return original_hasattr(obj, name)
+
+ mock_lstat = mocker.MagicMock()
+ mock_lstat.st_mode = 0o777
+
+ mocker.patch('os.lstat', side_effect=[mock_lstat, mock_lstat])
+ mocker.patch.object(builtins, 'hasattr', side_effect=_hasattr)
+ mocker.patch('os.path.islink', return_value=True)
+ mocker.patch('os.path.exists', return_value=True)
+ m_stat = mocker.patch('os.stat', side_effect=OSError(errno.EACCES, 'Permission denied'))
+ m_chmod = mocker.patch('os.chmod', return_value=None)
+
+ # not changed: can't set mode on symbolic links
+ assert not am.set_mode_if_different('/path/to/file/no_lchmod', 0o660, False)
+ m_stat.assert_called_with(b'/path/to/file/no_lchmod')
+ m_chmod.assert_not_called()
+
+ mocker.resetall()
+ mocker.stopall()
diff --git a/test/units/module_utils/basic/test_tmpdir.py b/test/units/module_utils/basic/test_tmpdir.py
new file mode 100644
index 0000000..818cb9b
--- /dev/null
+++ b/test/units/module_utils/basic/test_tmpdir.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018 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
+
+import json
+import os
+import shutil
+import tempfile
+
+import pytest
+
+from units.compat.mock import patch, MagicMock
+from ansible.module_utils._text import to_bytes
+
+from ansible.module_utils import basic
+
+
+class TestAnsibleModuleTmpDir:
+
+ DATA = (
+ (
+ {
+ "_ansible_tmpdir": "/path/to/dir",
+ "_ansible_remote_tmp": "/path/tmpdir",
+ "_ansible_keep_remote_files": False,
+ },
+ True,
+ "/path/to/dir"
+ ),
+ (
+ {
+ "_ansible_tmpdir": None,
+ "_ansible_remote_tmp": "/path/tmpdir",
+ "_ansible_keep_remote_files": False
+ },
+ False,
+ "/path/tmpdir/ansible-moduletmp-42-"
+ ),
+ (
+ {
+ "_ansible_tmpdir": None,
+ "_ansible_remote_tmp": "/path/tmpdir",
+ "_ansible_keep_remote_files": False
+ },
+ True,
+ "/path/tmpdir/ansible-moduletmp-42-"
+ ),
+ (
+ {
+ "_ansible_tmpdir": None,
+ "_ansible_remote_tmp": "$HOME/.test",
+ "_ansible_keep_remote_files": False
+ },
+ False,
+ os.path.join(os.environ['HOME'], ".test/ansible-moduletmp-42-")
+ ),
+ )
+
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ # pylint: disable=undefined-variable
+ @pytest.mark.parametrize('args, expected, stat_exists', ((s, e, t) for s, t, e in DATA))
+ def test_tmpdir_property(self, monkeypatch, args, expected, stat_exists):
+ makedirs = {'called': False}
+
+ def mock_mkdtemp(prefix, dir):
+ return os.path.join(dir, prefix)
+
+ def mock_makedirs(path, mode):
+ makedirs['called'] = True
+ makedirs['path'] = path
+ makedirs['mode'] = mode
+ return
+
+ monkeypatch.setattr(tempfile, 'mkdtemp', mock_mkdtemp)
+ monkeypatch.setattr(os.path, 'exists', lambda x: stat_exists)
+ monkeypatch.setattr(os, 'makedirs', mock_makedirs)
+ monkeypatch.setattr(shutil, 'rmtree', lambda x: None)
+ monkeypatch.setattr(basic, '_ANSIBLE_ARGS', to_bytes(json.dumps({'ANSIBLE_MODULE_ARGS': args})))
+
+ with patch('time.time', return_value=42):
+ am = basic.AnsibleModule(argument_spec={})
+ actual_tmpdir = am.tmpdir
+
+ assert actual_tmpdir == expected
+
+ # verify subsequent calls always produces the same tmpdir
+ assert am.tmpdir == actual_tmpdir
+
+ if not stat_exists:
+ assert makedirs['called']
+ expected = os.path.expanduser(os.path.expandvars(am._remote_tmp))
+ assert makedirs['path'] == expected
+ assert makedirs['mode'] == 0o700
+
+ @pytest.mark.parametrize('stdin', ({"_ansible_tmpdir": None,
+ "_ansible_remote_tmp": "$HOME/.test",
+ "_ansible_keep_remote_files": True},),
+ indirect=['stdin'])
+ def test_tmpdir_makedirs_failure(self, am, monkeypatch):
+
+ mock_mkdtemp = MagicMock(return_value="/tmp/path")
+ mock_makedirs = MagicMock(side_effect=OSError("Some OS Error here"))
+
+ monkeypatch.setattr(tempfile, 'mkdtemp', mock_mkdtemp)
+ monkeypatch.setattr(os.path, 'exists', lambda x: False)
+ monkeypatch.setattr(os, 'makedirs', mock_makedirs)
+
+ actual = am.tmpdir
+ assert actual == "/tmp/path"
+ assert mock_makedirs.call_args[0] == (os.path.expanduser(os.path.expandvars("$HOME/.test")),)
+ assert mock_makedirs.call_args[1] == {"mode": 0o700}
+
+ # because makedirs failed the dir should be None so it uses the System tmp
+ assert mock_mkdtemp.call_args[1]['dir'] is None
+ assert mock_mkdtemp.call_args[1]['prefix'].startswith("ansible-moduletmp-")
diff --git a/test/units/module_utils/common/__init__.py b/test/units/module_utils/common/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/common/__init__.py
diff --git a/test/units/module_utils/common/arg_spec/__init__.py b/test/units/module_utils/common/arg_spec/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/common/arg_spec/__init__.py
diff --git a/test/units/module_utils/common/arg_spec/test_aliases.py b/test/units/module_utils/common/arg_spec/test_aliases.py
new file mode 100644
index 0000000..7d30fb0
--- /dev/null
+++ b/test/units/module_utils/common/arg_spec/test_aliases.py
@@ -0,0 +1,132 @@
+# -*- coding: utf-8 -*-
+# 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 pytest
+
+from ansible.module_utils.errors import AnsibleValidationError, AnsibleValidationErrorMultiple
+from ansible.module_utils.common.arg_spec import ArgumentSpecValidator, ValidationResult
+from ansible.module_utils.common.warnings import get_deprecation_messages, get_warning_messages
+
+# id, argument spec, parameters, expected parameters, deprecation, warning
+ALIAS_TEST_CASES = [
+ (
+ "alias",
+ {'path': {'aliases': ['dir', 'directory']}},
+ {'dir': '/tmp'},
+ {
+ 'dir': '/tmp',
+ 'path': '/tmp',
+ },
+ "",
+ "",
+ ),
+ (
+ "alias-duplicate-warning",
+ {'path': {'aliases': ['dir', 'directory']}},
+ {
+ 'dir': '/tmp',
+ 'directory': '/tmp',
+ },
+ {
+ 'dir': '/tmp',
+ 'directory': '/tmp',
+ 'path': '/tmp',
+ },
+ "",
+ {'alias': 'directory', 'option': 'path'},
+ ),
+ (
+ "deprecated-alias",
+ {
+ 'path': {
+ 'aliases': ['not_yo_path'],
+ 'deprecated_aliases': [
+ {
+ 'name': 'not_yo_path',
+ 'version': '1.7',
+ }
+ ]
+ }
+ },
+ {'not_yo_path': '/tmp'},
+ {
+ 'path': '/tmp',
+ 'not_yo_path': '/tmp',
+ },
+ {
+ 'version': '1.7',
+ 'date': None,
+ 'collection_name': None,
+ 'msg': "Alias 'not_yo_path' is deprecated. See the module docs for more information",
+ },
+ "",
+ )
+]
+
+
+# id, argument spec, parameters, expected parameters, error
+ALIAS_TEST_CASES_INVALID = [
+ (
+ "alias-invalid",
+ {'path': {'aliases': 'bad'}},
+ {},
+ {'path': None},
+ "internal error: aliases must be a list or tuple",
+ ),
+ (
+ # This isn't related to aliases, but it exists in the alias handling code
+ "default-and-required",
+ {'name': {'default': 'ray', 'required': True}},
+ {},
+ {'name': 'ray'},
+ "internal error: required and default are mutually exclusive for name",
+ ),
+]
+
+
+@pytest.mark.parametrize(
+ ('arg_spec', 'parameters', 'expected', 'deprecation', 'warning'),
+ ((i[1:]) for i in ALIAS_TEST_CASES),
+ ids=[i[0] for i in ALIAS_TEST_CASES]
+)
+def test_aliases(arg_spec, parameters, expected, deprecation, warning):
+ v = ArgumentSpecValidator(arg_spec)
+ result = v.validate(parameters)
+
+ assert isinstance(result, ValidationResult)
+ assert result.validated_parameters == expected
+ assert result.error_messages == []
+ assert result._aliases == {
+ alias: param
+ for param, value in arg_spec.items()
+ for alias in value.get("aliases", [])
+ }
+
+ if deprecation:
+ assert deprecation == result._deprecations[0]
+ else:
+ assert result._deprecations == []
+
+ if warning:
+ assert warning == result._warnings[0]
+ else:
+ assert result._warnings == []
+
+
+@pytest.mark.parametrize(
+ ('arg_spec', 'parameters', 'expected', 'error'),
+ ((i[1:]) for i in ALIAS_TEST_CASES_INVALID),
+ ids=[i[0] for i in ALIAS_TEST_CASES_INVALID]
+)
+def test_aliases_invalid(arg_spec, parameters, expected, error):
+ v = ArgumentSpecValidator(arg_spec)
+ result = v.validate(parameters)
+
+ assert isinstance(result, ValidationResult)
+ assert error in result.error_messages
+ assert isinstance(result.errors.errors[0], AnsibleValidationError)
+ assert isinstance(result.errors, AnsibleValidationErrorMultiple)
diff --git a/test/units/module_utils/common/arg_spec/test_module_validate.py b/test/units/module_utils/common/arg_spec/test_module_validate.py
new file mode 100644
index 0000000..2c2211c
--- /dev/null
+++ b/test/units/module_utils/common/arg_spec/test_module_validate.py
@@ -0,0 +1,58 @@
+# -*- coding: utf-8 -*-
+# 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.common import warnings
+
+from ansible.module_utils.common.arg_spec import ModuleArgumentSpecValidator, ValidationResult
+
+
+def test_module_validate():
+ arg_spec = {'name': {}}
+ parameters = {'name': 'larry'}
+ expected = {'name': 'larry'}
+
+ v = ModuleArgumentSpecValidator(arg_spec)
+ result = v.validate(parameters)
+
+ assert isinstance(result, ValidationResult)
+ assert result.error_messages == []
+ assert result._deprecations == []
+ assert result._warnings == []
+ assert result.validated_parameters == expected
+
+
+def test_module_alias_deprecations_warnings(monkeypatch):
+ monkeypatch.setattr(warnings, '_global_deprecations', [])
+
+ arg_spec = {
+ 'path': {
+ 'aliases': ['source', 'src', 'flamethrower'],
+ 'deprecated_aliases': [{'name': 'flamethrower', 'date': '2020-03-04'}],
+ },
+ }
+ parameters = {'flamethrower': '/tmp', 'source': '/tmp'}
+ expected = {
+ 'path': '/tmp',
+ 'flamethrower': '/tmp',
+ 'source': '/tmp',
+ }
+
+ v = ModuleArgumentSpecValidator(arg_spec)
+ result = v.validate(parameters)
+
+ assert result.validated_parameters == expected
+ assert result._deprecations == [
+ {
+ 'collection_name': None,
+ 'date': '2020-03-04',
+ 'msg': "Alias 'flamethrower' is deprecated. See the module docs for more information",
+ 'version': None,
+ }
+ ]
+ assert "Alias 'flamethrower' is deprecated" in warnings._global_deprecations[0]['msg']
+ assert result._warnings == [{'alias': 'flamethrower', 'option': 'path'}]
+ assert "Both option path and its alias flamethrower are set" in warnings._global_warnings[0]
diff --git a/test/units/module_utils/common/arg_spec/test_sub_spec.py b/test/units/module_utils/common/arg_spec/test_sub_spec.py
new file mode 100644
index 0000000..a6e7575
--- /dev/null
+++ b/test/units/module_utils/common/arg_spec/test_sub_spec.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+# 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.common.arg_spec import ArgumentSpecValidator, ValidationResult
+
+
+def test_sub_spec():
+ arg_spec = {
+ 'state': {},
+ 'user': {
+ 'type': 'dict',
+ 'options': {
+ 'first': {'no_log': True},
+ 'last': {},
+ 'age': {'type': 'int'},
+ }
+ }
+ }
+
+ parameters = {
+ 'state': 'present',
+ 'user': {
+ 'first': 'Rey',
+ 'last': 'Skywalker',
+ 'age': '19',
+ }
+ }
+
+ expected = {
+ 'state': 'present',
+ 'user': {
+ 'first': 'Rey',
+ 'last': 'Skywalker',
+ 'age': 19,
+ }
+ }
+
+ v = ArgumentSpecValidator(arg_spec)
+ result = v.validate(parameters)
+
+ assert isinstance(result, ValidationResult)
+ assert result.validated_parameters == expected
+ assert result.error_messages == []
+
+
+def test_nested_sub_spec():
+ arg_spec = {
+ 'type': {},
+ 'car': {
+ 'type': 'dict',
+ 'options': {
+ 'make': {},
+ 'model': {},
+ 'customizations': {
+ 'type': 'dict',
+ 'options': {
+ 'engine': {},
+ 'transmission': {},
+ 'color': {},
+ 'max_rpm': {'type': 'int'},
+ }
+ }
+ }
+ }
+ }
+
+ parameters = {
+ 'type': 'endurance',
+ 'car': {
+ 'make': 'Ford',
+ 'model': 'GT-40',
+ 'customizations': {
+ 'engine': '7.0 L',
+ 'transmission': '5-speed',
+ 'color': 'Ford blue',
+ 'max_rpm': '6000',
+ }
+
+ }
+ }
+
+ expected = {
+ 'type': 'endurance',
+ 'car': {
+ 'make': 'Ford',
+ 'model': 'GT-40',
+ 'customizations': {
+ 'engine': '7.0 L',
+ 'transmission': '5-speed',
+ 'color': 'Ford blue',
+ 'max_rpm': 6000,
+ }
+
+ }
+ }
+
+ v = ArgumentSpecValidator(arg_spec)
+ result = v.validate(parameters)
+
+ assert isinstance(result, ValidationResult)
+ assert result.validated_parameters == expected
+ assert result.error_messages == []
diff --git a/test/units/module_utils/common/arg_spec/test_validate_invalid.py b/test/units/module_utils/common/arg_spec/test_validate_invalid.py
new file mode 100644
index 0000000..7302e8a
--- /dev/null
+++ b/test/units/module_utils/common/arg_spec/test_validate_invalid.py
@@ -0,0 +1,134 @@
+# -*- coding: utf-8 -*-
+# 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 pytest
+
+from ansible.module_utils.common.arg_spec import ArgumentSpecValidator, ValidationResult
+from ansible.module_utils.errors import AnsibleValidationErrorMultiple
+from ansible.module_utils.six import PY2
+
+
+# Each item is id, argument_spec, parameters, expected, unsupported parameters, error test string
+INVALID_SPECS = [
+ (
+ 'invalid-list',
+ {'packages': {'type': 'list'}},
+ {'packages': {'key': 'value'}},
+ {'packages': {'key': 'value'}},
+ set(),
+ "unable to convert to list: <class 'dict'> cannot be converted to a list",
+ ),
+ (
+ 'invalid-dict',
+ {'users': {'type': 'dict'}},
+ {'users': ['one', 'two']},
+ {'users': ['one', 'two']},
+ set(),
+ "unable to convert to dict: <class 'list'> cannot be converted to a dict",
+ ),
+ (
+ 'invalid-bool',
+ {'bool': {'type': 'bool'}},
+ {'bool': {'k': 'v'}},
+ {'bool': {'k': 'v'}},
+ set(),
+ "unable to convert to bool: <class 'dict'> cannot be converted to a bool",
+ ),
+ (
+ 'invalid-float',
+ {'float': {'type': 'float'}},
+ {'float': 'hello'},
+ {'float': 'hello'},
+ set(),
+ "unable to convert to float: <class 'str'> cannot be converted to a float",
+ ),
+ (
+ 'invalid-bytes',
+ {'bytes': {'type': 'bytes'}},
+ {'bytes': 'one'},
+ {'bytes': 'one'},
+ set(),
+ "unable to convert to bytes: <class 'str'> cannot be converted to a Byte value",
+ ),
+ (
+ 'invalid-bits',
+ {'bits': {'type': 'bits'}},
+ {'bits': 'one'},
+ {'bits': 'one'},
+ set(),
+ "unable to convert to bits: <class 'str'> cannot be converted to a Bit value",
+ ),
+ (
+ 'invalid-jsonargs',
+ {'some_json': {'type': 'jsonarg'}},
+ {'some_json': set()},
+ {'some_json': set()},
+ set(),
+ "unable to convert to jsonarg: <class 'set'> cannot be converted to a json string",
+ ),
+ (
+ 'invalid-parameter',
+ {'name': {}},
+ {
+ 'badparam': '',
+ 'another': '',
+ },
+ {
+ 'name': None,
+ 'badparam': '',
+ 'another': '',
+ },
+ set(('another', 'badparam')),
+ "another, badparam. Supported parameters include: name.",
+ ),
+ (
+ 'invalid-elements',
+ {'numbers': {'type': 'list', 'elements': 'int'}},
+ {'numbers': [55, 33, 34, {'key': 'value'}]},
+ {'numbers': [55, 33, 34]},
+ set(),
+ "Elements value for option 'numbers' is of type <class 'dict'> and we were unable to convert to int: <class 'dict'> cannot be converted to an int"
+ ),
+ (
+ 'required',
+ {'req': {'required': True}},
+ {},
+ {'req': None},
+ set(),
+ "missing required arguments: req"
+ ),
+ (
+ 'blank_values',
+ {'ch_param': {'elements': 'str', 'type': 'list', 'choices': ['a', 'b']}},
+ {'ch_param': ['']},
+ {'ch_param': ['']},
+ set(),
+ "value of ch_param must be one or more of"
+ )
+]
+
+
+@pytest.mark.parametrize(
+ ('arg_spec', 'parameters', 'expected', 'unsupported', 'error'),
+ (i[1:] for i in INVALID_SPECS),
+ ids=[i[0] for i in INVALID_SPECS]
+)
+def test_invalid_spec(arg_spec, parameters, expected, unsupported, error):
+ v = ArgumentSpecValidator(arg_spec)
+ result = v.validate(parameters)
+
+ with pytest.raises(AnsibleValidationErrorMultiple) as exc_info:
+ raise result.errors
+
+ if PY2:
+ error = error.replace('class', 'type')
+
+ assert isinstance(result, ValidationResult)
+ assert error in exc_info.value.msg
+ assert error in result.error_messages[0]
+ assert result.unsupported_parameters == unsupported
+ assert result.validated_parameters == expected
diff --git a/test/units/module_utils/common/arg_spec/test_validate_valid.py b/test/units/module_utils/common/arg_spec/test_validate_valid.py
new file mode 100644
index 0000000..7e41127
--- /dev/null
+++ b/test/units/module_utils/common/arg_spec/test_validate_valid.py
@@ -0,0 +1,335 @@
+# -*- coding: utf-8 -*-
+# 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 pytest
+
+from ansible.module_utils.common.arg_spec import ArgumentSpecValidator, ValidationResult
+
+# Each item is id, argument_spec, parameters, expected, valid parameter names
+VALID_SPECS = [
+ (
+ 'str-no-type-specified',
+ {'name': {}},
+ {'name': 'rey'},
+ {'name': 'rey'},
+ set(('name',)),
+ ),
+ (
+ 'str',
+ {'name': {'type': 'str'}},
+ {'name': 'rey'},
+ {'name': 'rey'},
+ set(('name',)),
+ ),
+ (
+ 'str-convert',
+ {'name': {'type': 'str'}},
+ {'name': 5},
+ {'name': '5'},
+ set(('name',)),
+ ),
+ (
+ 'list',
+ {'packages': {'type': 'list'}},
+ {'packages': ['vim', 'python']},
+ {'packages': ['vim', 'python']},
+ set(('packages',)),
+ ),
+ (
+ 'list-comma-string',
+ {'packages': {'type': 'list'}},
+ {'packages': 'vim,python'},
+ {'packages': ['vim', 'python']},
+ set(('packages',)),
+ ),
+ (
+ 'list-comma-string-space',
+ {'packages': {'type': 'list'}},
+ {'packages': 'vim, python'},
+ {'packages': ['vim', ' python']},
+ set(('packages',)),
+ ),
+ (
+ 'dict',
+ {'user': {'type': 'dict'}},
+ {
+ 'user':
+ {
+ 'first': 'rey',
+ 'last': 'skywalker',
+ }
+ },
+ {
+ 'user':
+ {
+ 'first': 'rey',
+ 'last': 'skywalker',
+ }
+ },
+ set(('user',)),
+ ),
+ (
+ 'dict-k=v',
+ {'user': {'type': 'dict'}},
+ {'user': 'first=rey,last=skywalker'},
+ {
+ 'user':
+ {
+ 'first': 'rey',
+ 'last': 'skywalker',
+ }
+ },
+ set(('user',)),
+ ),
+ (
+ 'dict-k=v-spaces',
+ {'user': {'type': 'dict'}},
+ {'user': 'first=rey, last=skywalker'},
+ {
+ 'user':
+ {
+ 'first': 'rey',
+ 'last': 'skywalker',
+ }
+ },
+ set(('user',)),
+ ),
+ (
+ 'bool',
+ {
+ 'enabled': {'type': 'bool'},
+ 'disabled': {'type': 'bool'},
+ },
+ {
+ 'enabled': True,
+ 'disabled': False,
+ },
+ {
+ 'enabled': True,
+ 'disabled': False,
+ },
+ set(('enabled', 'disabled')),
+ ),
+ (
+ 'bool-ints',
+ {
+ 'enabled': {'type': 'bool'},
+ 'disabled': {'type': 'bool'},
+ },
+ {
+ 'enabled': 1,
+ 'disabled': 0,
+ },
+ {
+ 'enabled': True,
+ 'disabled': False,
+ },
+ set(('enabled', 'disabled')),
+ ),
+ (
+ 'bool-true-false',
+ {
+ 'enabled': {'type': 'bool'},
+ 'disabled': {'type': 'bool'},
+ },
+ {
+ 'enabled': 'true',
+ 'disabled': 'false',
+ },
+ {
+ 'enabled': True,
+ 'disabled': False,
+ },
+ set(('enabled', 'disabled')),
+ ),
+ (
+ 'bool-yes-no',
+ {
+ 'enabled': {'type': 'bool'},
+ 'disabled': {'type': 'bool'},
+ },
+ {
+ 'enabled': 'yes',
+ 'disabled': 'no',
+ },
+ {
+ 'enabled': True,
+ 'disabled': False,
+ },
+ set(('enabled', 'disabled')),
+ ),
+ (
+ 'bool-y-n',
+ {
+ 'enabled': {'type': 'bool'},
+ 'disabled': {'type': 'bool'},
+ },
+ {
+ 'enabled': 'y',
+ 'disabled': 'n',
+ },
+ {
+ 'enabled': True,
+ 'disabled': False,
+ },
+ set(('enabled', 'disabled')),
+ ),
+ (
+ 'bool-on-off',
+ {
+ 'enabled': {'type': 'bool'},
+ 'disabled': {'type': 'bool'},
+ },
+ {
+ 'enabled': 'on',
+ 'disabled': 'off',
+ },
+ {
+ 'enabled': True,
+ 'disabled': False,
+ },
+ set(('enabled', 'disabled')),
+ ),
+ (
+ 'bool-1-0',
+ {
+ 'enabled': {'type': 'bool'},
+ 'disabled': {'type': 'bool'},
+ },
+ {
+ 'enabled': '1',
+ 'disabled': '0',
+ },
+ {
+ 'enabled': True,
+ 'disabled': False,
+ },
+ set(('enabled', 'disabled')),
+ ),
+ (
+ 'bool-float',
+ {
+ 'enabled': {'type': 'bool'},
+ 'disabled': {'type': 'bool'},
+ },
+ {
+ 'enabled': 1.0,
+ 'disabled': 0.0,
+ },
+ {
+ 'enabled': True,
+ 'disabled': False,
+ },
+ set(('enabled', 'disabled')),
+ ),
+ (
+ 'float',
+ {'digit': {'type': 'float'}},
+ {'digit': 3.14159},
+ {'digit': 3.14159},
+ set(('digit',)),
+ ),
+ (
+ 'float-str',
+ {'digit': {'type': 'float'}},
+ {'digit': '3.14159'},
+ {'digit': 3.14159},
+ set(('digit',)),
+ ),
+ (
+ 'path',
+ {'path': {'type': 'path'}},
+ {'path': '~/bin'},
+ {'path': '/home/ansible/bin'},
+ set(('path',)),
+ ),
+ (
+ 'raw',
+ {'raw': {'type': 'raw'}},
+ {'raw': 0x644},
+ {'raw': 0x644},
+ set(('raw',)),
+ ),
+ (
+ 'bytes',
+ {'bytes': {'type': 'bytes'}},
+ {'bytes': '2K'},
+ {'bytes': 2048},
+ set(('bytes',)),
+ ),
+ (
+ 'bits',
+ {'bits': {'type': 'bits'}},
+ {'bits': '1Mb'},
+ {'bits': 1048576},
+ set(('bits',)),
+ ),
+ (
+ 'jsonarg',
+ {'some_json': {'type': 'jsonarg'}},
+ {'some_json': '{"users": {"bob": {"role": "accountant"}}}'},
+ {'some_json': '{"users": {"bob": {"role": "accountant"}}}'},
+ set(('some_json',)),
+ ),
+ (
+ 'jsonarg-list',
+ {'some_json': {'type': 'jsonarg'}},
+ {'some_json': ['one', 'two']},
+ {'some_json': '["one", "two"]'},
+ set(('some_json',)),
+ ),
+ (
+ 'jsonarg-dict',
+ {'some_json': {'type': 'jsonarg'}},
+ {'some_json': {"users": {"bob": {"role": "accountant"}}}},
+ {'some_json': '{"users": {"bob": {"role": "accountant"}}}'},
+ set(('some_json',)),
+ ),
+ (
+ 'defaults',
+ {'param': {'default': 'DEFAULT'}},
+ {},
+ {'param': 'DEFAULT'},
+ set(('param',)),
+ ),
+ (
+ 'elements',
+ {'numbers': {'type': 'list', 'elements': 'int'}},
+ {'numbers': [55, 33, 34, '22']},
+ {'numbers': [55, 33, 34, 22]},
+ set(('numbers',)),
+ ),
+ (
+ 'aliases',
+ {'src': {'aliases': ['path', 'source']}},
+ {'src': '/tmp'},
+ {'src': '/tmp'},
+ set(('src (path, source)',)),
+ )
+]
+
+
+@pytest.mark.parametrize(
+ ('arg_spec', 'parameters', 'expected', 'valid_params'),
+ (i[1:] for i in VALID_SPECS),
+ ids=[i[0] for i in VALID_SPECS]
+)
+def test_valid_spec(arg_spec, parameters, expected, valid_params, mocker):
+ mocker.patch('ansible.module_utils.common.validation.os.path.expanduser', return_value='/home/ansible/bin')
+ mocker.patch('ansible.module_utils.common.validation.os.path.expandvars', return_value='/home/ansible/bin')
+
+ v = ArgumentSpecValidator(arg_spec)
+ result = v.validate(parameters)
+
+ assert isinstance(result, ValidationResult)
+ assert result.validated_parameters == expected
+ assert result.unsupported_parameters == set()
+ assert result.error_messages == []
+ assert v._valid_parameter_names == valid_params
+
+ # Again to check caching
+ assert v._valid_parameter_names == valid_params
diff --git a/test/units/module_utils/common/parameters/test_check_arguments.py b/test/units/module_utils/common/parameters/test_check_arguments.py
new file mode 100644
index 0000000..5311217
--- /dev/null
+++ b/test/units/module_utils/common/parameters/test_check_arguments.py
@@ -0,0 +1,38 @@
+# -*- 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
+
+
+import pytest
+
+from ansible.module_utils.common.parameters import _get_unsupported_parameters
+
+
+@pytest.fixture
+def argument_spec():
+ return {
+ 'state': {'aliases': ['status']},
+ 'enabled': {},
+ }
+
+
+@pytest.mark.parametrize(
+ ('module_parameters', 'legal_inputs', 'expected'),
+ (
+ ({'fish': 'food'}, ['state', 'enabled'], set(['fish'])),
+ ({'state': 'enabled', 'path': '/var/lib/path'}, None, set(['path'])),
+ ({'state': 'enabled', 'path': '/var/lib/path'}, ['state', 'path'], set()),
+ ({'state': 'enabled', 'path': '/var/lib/path'}, ['state'], set(['path'])),
+ ({}, None, set()),
+ ({'state': 'enabled'}, None, set()),
+ ({'status': 'enabled', 'enabled': True, 'path': '/var/lib/path'}, None, set(['path'])),
+ ({'status': 'enabled', 'enabled': True}, None, set()),
+ )
+)
+def test_check_arguments(argument_spec, module_parameters, legal_inputs, expected, mocker):
+ result = _get_unsupported_parameters(argument_spec, module_parameters, legal_inputs)
+
+ assert result == expected
diff --git a/test/units/module_utils/common/parameters/test_handle_aliases.py b/test/units/module_utils/common/parameters/test_handle_aliases.py
new file mode 100644
index 0000000..e20a888
--- /dev/null
+++ b/test/units/module_utils/common/parameters/test_handle_aliases.py
@@ -0,0 +1,74 @@
+# -*- 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
+
+
+import pytest
+
+from ansible.module_utils.common.parameters import _handle_aliases
+from ansible.module_utils._text import to_native
+
+
+def test_handle_aliases_no_aliases():
+ argument_spec = {
+ 'name': {'type': 'str'},
+ }
+
+ params = {
+ 'name': 'foo',
+ 'path': 'bar'
+ }
+
+ expected = {}
+ result = _handle_aliases(argument_spec, params)
+
+ assert expected == result
+
+
+def test_handle_aliases_basic():
+ argument_spec = {
+ 'name': {'type': 'str', 'aliases': ['surname', 'nick']},
+ }
+
+ params = {
+ 'name': 'foo',
+ 'path': 'bar',
+ 'surname': 'foo',
+ 'nick': 'foo',
+ }
+
+ expected = {'surname': 'name', 'nick': 'name'}
+ result = _handle_aliases(argument_spec, params)
+
+ assert expected == result
+
+
+def test_handle_aliases_value_error():
+ argument_spec = {
+ 'name': {'type': 'str', 'aliases': ['surname', 'nick'], 'default': 'bob', 'required': True},
+ }
+
+ params = {
+ 'name': 'foo',
+ }
+
+ with pytest.raises(ValueError) as ve:
+ _handle_aliases(argument_spec, params)
+ assert 'internal error: aliases must be a list or tuple' == to_native(ve.error)
+
+
+def test_handle_aliases_type_error():
+ argument_spec = {
+ 'name': {'type': 'str', 'aliases': 'surname'},
+ }
+
+ params = {
+ 'name': 'foo',
+ }
+
+ with pytest.raises(TypeError) as te:
+ _handle_aliases(argument_spec, params)
+ assert 'internal error: required and default are mutually exclusive' in to_native(te.error)
diff --git a/test/units/module_utils/common/parameters/test_list_deprecations.py b/test/units/module_utils/common/parameters/test_list_deprecations.py
new file mode 100644
index 0000000..6f0bb71
--- /dev/null
+++ b/test/units/module_utils/common/parameters/test_list_deprecations.py
@@ -0,0 +1,44 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils.common.parameters import _list_deprecations
+
+
+@pytest.fixture
+def params():
+ return {
+ 'name': 'bob',
+ 'dest': '/etc/hosts',
+ 'state': 'present',
+ 'value': 5,
+ }
+
+
+def test_list_deprecations():
+ argument_spec = {
+ 'old': {'type': 'str', 'removed_in_version': '2.5'},
+ 'foo': {'type': 'dict', 'options': {'old': {'type': 'str', 'removed_in_version': 1.0}}},
+ 'bar': {'type': 'list', 'elements': 'dict', 'options': {'old': {'type': 'str', 'removed_in_version': '2.10'}}},
+ }
+
+ params = {
+ 'name': 'rod',
+ 'old': 'option',
+ 'foo': {'old': 'value'},
+ 'bar': [{'old': 'value'}, {}],
+ }
+ result = _list_deprecations(argument_spec, params)
+ assert len(result) == 3
+ result.sort(key=lambda entry: entry['msg'])
+ assert result[0]['msg'] == """Param 'bar["old"]' is deprecated. See the module docs for more information"""
+ assert result[0]['version'] == '2.10'
+ assert result[1]['msg'] == """Param 'foo["old"]' is deprecated. See the module docs for more information"""
+ assert result[1]['version'] == 1.0
+ assert result[2]['msg'] == "Param 'old' is deprecated. See the module docs for more information"
+ assert result[2]['version'] == '2.5'
diff --git a/test/units/module_utils/common/parameters/test_list_no_log_values.py b/test/units/module_utils/common/parameters/test_list_no_log_values.py
new file mode 100644
index 0000000..ac0e735
--- /dev/null
+++ b/test/units/module_utils/common/parameters/test_list_no_log_values.py
@@ -0,0 +1,228 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils.common.parameters import _list_no_log_values
+
+
+@pytest.fixture
+def argument_spec():
+ # Allow extra specs to be passed to the fixture, which will be added to the output
+ def _argument_spec(extra_opts=None):
+ spec = {
+ 'secret': {'type': 'str', 'no_log': True},
+ 'other_secret': {'type': 'str', 'no_log': True},
+ 'state': {'type': 'str'},
+ 'value': {'type': 'int'},
+ }
+
+ if extra_opts:
+ spec.update(extra_opts)
+
+ return spec
+
+ return _argument_spec
+
+
+@pytest.fixture
+def module_parameters():
+ # Allow extra parameters to be passed to the fixture, which will be added to the output
+ def _module_parameters(extra_params=None):
+ params = {
+ 'secret': 'under',
+ 'other_secret': 'makeshift',
+ 'state': 'present',
+ 'value': 5,
+ }
+
+ if extra_params:
+ params.update(extra_params)
+
+ return params
+
+ return _module_parameters
+
+
+def test_list_no_log_values_no_secrets(module_parameters):
+ argument_spec = {
+ 'other_secret': {'type': 'str', 'no_log': False},
+ 'state': {'type': 'str'},
+ 'value': {'type': 'int'},
+ }
+ expected = set()
+ assert expected == _list_no_log_values(argument_spec, module_parameters)
+
+
+def test_list_no_log_values(argument_spec, module_parameters):
+ expected = set(('under', 'makeshift'))
+ assert expected == _list_no_log_values(argument_spec(), module_parameters())
+
+
+@pytest.mark.parametrize('extra_params', [
+ {'subopt1': 1},
+ {'subopt1': 3.14159},
+ {'subopt1': ['one', 'two']},
+ {'subopt1': ('one', 'two')},
+])
+def test_list_no_log_values_invalid_suboptions(argument_spec, module_parameters, extra_params):
+ extra_opts = {
+ 'subopt1': {
+ 'type': 'dict',
+ 'options': {
+ 'sub_1_1': {},
+ }
+ }
+ }
+
+ with pytest.raises(TypeError, match=r"(Value '.*?' in the sub parameter field '.*?' must by a dict, not '.*?')"
+ r"|(dictionary requested, could not parse JSON or key=value)"):
+ _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params))
+
+
+def test_list_no_log_values_suboptions(argument_spec, module_parameters):
+ extra_opts = {
+ 'subopt1': {
+ 'type': 'dict',
+ 'options': {
+ 'sub_1_1': {'no_log': True},
+ 'sub_1_2': {'type': 'list'},
+ }
+ }
+ }
+
+ extra_params = {
+ 'subopt1': {
+ 'sub_1_1': 'bagel',
+ 'sub_1_2': ['pebble'],
+ }
+ }
+
+ expected = set(('under', 'makeshift', 'bagel'))
+ assert expected == _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params))
+
+
+def test_list_no_log_values_sub_suboptions(argument_spec, module_parameters):
+ extra_opts = {
+ 'sub_level_1': {
+ 'type': 'dict',
+ 'options': {
+ 'l1_1': {'no_log': True},
+ 'l1_2': {},
+ 'l1_3': {
+ 'type': 'dict',
+ 'options': {
+ 'l2_1': {'no_log': True},
+ 'l2_2': {},
+ }
+ }
+ }
+ }
+ }
+
+ extra_params = {
+ 'sub_level_1': {
+ 'l1_1': 'saucy',
+ 'l1_2': 'napped',
+ 'l1_3': {
+ 'l2_1': 'corporate',
+ 'l2_2': 'tinsmith',
+ }
+ }
+ }
+
+ expected = set(('under', 'makeshift', 'saucy', 'corporate'))
+ assert expected == _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params))
+
+
+def test_list_no_log_values_suboptions_list(argument_spec, module_parameters):
+ extra_opts = {
+ 'subopt1': {
+ 'type': 'list',
+ 'elements': 'dict',
+ 'options': {
+ 'sub_1_1': {'no_log': True},
+ 'sub_1_2': {},
+ }
+ }
+ }
+
+ extra_params = {
+ 'subopt1': [
+ {
+ 'sub_1_1': ['playroom', 'luxury'],
+ 'sub_1_2': 'deuce',
+ },
+ {
+ 'sub_1_2': ['squishier', 'finished'],
+ }
+ ]
+ }
+
+ expected = set(('under', 'makeshift', 'playroom', 'luxury'))
+ assert expected == _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params))
+
+
+def test_list_no_log_values_sub_suboptions_list(argument_spec, module_parameters):
+ extra_opts = {
+ 'subopt1': {
+ 'type': 'list',
+ 'elements': 'dict',
+ 'options': {
+ 'sub_1_1': {'no_log': True},
+ 'sub_1_2': {},
+ 'subopt2': {
+ 'type': 'list',
+ 'elements': 'dict',
+ 'options': {
+ 'sub_2_1': {'no_log': True, 'type': 'list'},
+ 'sub_2_2': {},
+ }
+ }
+ }
+ }
+ }
+
+ extra_params = {
+ 'subopt1': {
+ 'sub_1_1': ['playroom', 'luxury'],
+ 'sub_1_2': 'deuce',
+ 'subopt2': [
+ {
+ 'sub_2_1': ['basis', 'gave'],
+ 'sub_2_2': 'liquid',
+ },
+ {
+ 'sub_2_1': ['composure', 'thumping']
+ },
+ ]
+ }
+ }
+
+ expected = set(('under', 'makeshift', 'playroom', 'luxury', 'basis', 'gave', 'composure', 'thumping'))
+ assert expected == _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params))
+
+
+@pytest.mark.parametrize('extra_params, expected', (
+ ({'subopt_dict': 'dict_subopt1=rekindle-scandal,dict_subopt2=subgroupavenge'}, ('rekindle-scandal',)),
+ ({'subopt_dict': 'dict_subopt1=aversion-mutable dict_subopt2=subgroupavenge'}, ('aversion-mutable',)),
+ ({'subopt_dict': ['dict_subopt1=blip-marine,dict_subopt2=subgroupavenge', 'dict_subopt1=tipping,dict_subopt2=hardening']}, ('blip-marine', 'tipping')),
+))
+def test_string_suboptions_as_string(argument_spec, module_parameters, extra_params, expected):
+ extra_opts = {
+ 'subopt_dict': {
+ 'type': 'dict',
+ 'options': {
+ 'dict_subopt1': {'no_log': True},
+ 'dict_subopt2': {},
+ },
+ },
+ }
+
+ result = set(('under', 'makeshift'))
+ result.update(expected)
+ assert result == _list_no_log_values(argument_spec(extra_opts), module_parameters(extra_params))
diff --git a/test/units/module_utils/common/process/test_get_bin_path.py b/test/units/module_utils/common/process/test_get_bin_path.py
new file mode 100644
index 0000000..7c0bd0a
--- /dev/null
+++ b/test/units/module_utils/common/process/test_get_bin_path.py
@@ -0,0 +1,39 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils.common.process import get_bin_path
+
+
+def test_get_bin_path(mocker):
+ path = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
+ mocker.patch.dict('os.environ', {'PATH': path})
+ mocker.patch('os.pathsep', ':')
+
+ mocker.patch('os.path.isdir', return_value=False)
+ mocker.patch('ansible.module_utils.common.process.is_executable', return_value=True)
+
+ # pytest-mock 2.0.0 will throw when os.path.exists is messed with
+ # and then another method is patched afterwards. Likely
+ # something in the pytest-mock chain uses os.path.exists internally, and
+ # since pytest-mock prohibits context-specific patching, there's not a
+ # good solution. For now, just patch os.path.exists last.
+ mocker.patch('os.path.exists', side_effect=[False, True])
+
+ assert '/usr/local/bin/notacommand' == get_bin_path('notacommand')
+
+
+def test_get_path_path_raise_valueerror(mocker):
+ mocker.patch.dict('os.environ', {'PATH': ''})
+
+ mocker.patch('os.path.exists', return_value=False)
+ mocker.patch('os.path.isdir', return_value=False)
+ mocker.patch('ansible.module_utils.common.process.is_executable', return_value=True)
+
+ with pytest.raises(ValueError, match='Failed to find required executable "notacommand"'):
+ get_bin_path('notacommand')
diff --git a/test/units/module_utils/common/test_collections.py b/test/units/module_utils/common/test_collections.py
new file mode 100644
index 0000000..95b2a40
--- /dev/null
+++ b/test/units/module_utils/common/test_collections.py
@@ -0,0 +1,175 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018–2019, Sviatoslav Sydorenko <webknjaz@redhat.com>
+# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
+"""Test low-level utility functions from ``module_utils.common.collections``."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+
+from ansible.module_utils.six import Iterator
+from ansible.module_utils.common._collections_compat import Sequence
+from ansible.module_utils.common.collections import ImmutableDict, is_iterable, is_sequence
+
+
+class SeqStub:
+ """Stub emulating a sequence type.
+
+ >>> from collections.abc import Sequence
+ >>> assert issubclass(SeqStub, Sequence)
+ >>> assert isinstance(SeqStub(), Sequence)
+ """
+
+
+Sequence.register(SeqStub)
+
+
+class IteratorStub(Iterator):
+ def __next__(self):
+ raise StopIteration
+
+
+class IterableStub:
+ def __iter__(self):
+ return IteratorStub()
+
+
+class FakeAnsibleVaultEncryptedUnicode(Sequence):
+ __ENCRYPTED__ = True
+
+ def __init__(self, data):
+ self.data = data
+
+ def __getitem__(self, index):
+ return self.data[index]
+
+ def __len__(self):
+ return len(self.data)
+
+
+TEST_STRINGS = u'he', u'Україна', u'Česká republika'
+TEST_STRINGS = TEST_STRINGS + tuple(s.encode('utf-8') for s in TEST_STRINGS) + (FakeAnsibleVaultEncryptedUnicode(u'foo'),)
+
+TEST_ITEMS_NON_SEQUENCES = (
+ {}, object(), frozenset(),
+ 4, 0.,
+) + TEST_STRINGS
+
+TEST_ITEMS_SEQUENCES = (
+ [], (),
+ SeqStub(),
+)
+TEST_ITEMS_SEQUENCES = TEST_ITEMS_SEQUENCES + (
+ # Iterable effectively containing nested random data:
+ TEST_ITEMS_NON_SEQUENCES,
+)
+
+
+@pytest.mark.parametrize('sequence_input', TEST_ITEMS_SEQUENCES)
+def test_sequence_positive(sequence_input):
+ """Test that non-string item sequences are identified correctly."""
+ assert is_sequence(sequence_input)
+ assert is_sequence(sequence_input, include_strings=False)
+
+
+@pytest.mark.parametrize('non_sequence_input', TEST_ITEMS_NON_SEQUENCES)
+def test_sequence_negative(non_sequence_input):
+ """Test that non-sequences are identified correctly."""
+ assert not is_sequence(non_sequence_input)
+
+
+@pytest.mark.parametrize('string_input', TEST_STRINGS)
+def test_sequence_string_types_with_strings(string_input):
+ """Test that ``is_sequence`` can separate string and non-string."""
+ assert is_sequence(string_input, include_strings=True)
+
+
+@pytest.mark.parametrize('string_input', TEST_STRINGS)
+def test_sequence_string_types_without_strings(string_input):
+ """Test that ``is_sequence`` can separate string and non-string."""
+ assert not is_sequence(string_input, include_strings=False)
+
+
+@pytest.mark.parametrize(
+ 'seq',
+ ([], (), {}, set(), frozenset(), IterableStub()),
+)
+def test_iterable_positive(seq):
+ assert is_iterable(seq)
+
+
+@pytest.mark.parametrize(
+ 'seq', (IteratorStub(), object(), 5, 9.)
+)
+def test_iterable_negative(seq):
+ assert not is_iterable(seq)
+
+
+@pytest.mark.parametrize('string_input', TEST_STRINGS)
+def test_iterable_including_strings(string_input):
+ assert is_iterable(string_input, include_strings=True)
+
+
+@pytest.mark.parametrize('string_input', TEST_STRINGS)
+def test_iterable_excluding_strings(string_input):
+ assert not is_iterable(string_input, include_strings=False)
+
+
+class TestImmutableDict:
+ def test_scalar(self):
+ imdict = ImmutableDict({1: 2})
+ assert imdict[1] == 2
+
+ def test_string(self):
+ imdict = ImmutableDict({u'café': u'くらとみ'})
+ assert imdict[u'café'] == u'くらとみ'
+
+ def test_container(self):
+ imdict = ImmutableDict({(1, 2): ['1', '2']})
+ assert imdict[(1, 2)] == ['1', '2']
+
+ def test_from_tuples(self):
+ imdict = ImmutableDict((('a', 1), ('b', 2)))
+ assert frozenset(imdict.items()) == frozenset((('a', 1), ('b', 2)))
+
+ def test_from_kwargs(self):
+ imdict = ImmutableDict(a=1, b=2)
+ assert frozenset(imdict.items()) == frozenset((('a', 1), ('b', 2)))
+
+ def test_immutable(self):
+ imdict = ImmutableDict({1: 2})
+
+ expected_reason = r"^'ImmutableDict' object does not support item assignment$"
+
+ with pytest.raises(TypeError, match=expected_reason):
+ imdict[1] = 3
+
+ with pytest.raises(TypeError, match=expected_reason):
+ imdict[5] = 3
+
+ def test_hashable(self):
+ # ImmutableDict is hashable when all of its values are hashable
+ imdict = ImmutableDict({u'café': u'くらとみ'})
+ assert hash(imdict)
+
+ def test_nonhashable(self):
+ # ImmutableDict is unhashable when one of its values is unhashable
+ imdict = ImmutableDict({u'café': u'くらとみ', 1: [1, 2]})
+
+ expected_reason = r"^unhashable type: 'list'$"
+
+ with pytest.raises(TypeError, match=expected_reason):
+ hash(imdict)
+
+ def test_len(self):
+ imdict = ImmutableDict({1: 2, 'a': 'b'})
+ assert len(imdict) == 2
+
+ def test_repr(self):
+ initial_data = {1: 2, 'a': 'b'}
+ initial_data_repr = repr(initial_data)
+ imdict = ImmutableDict(initial_data)
+ actual_repr = repr(imdict)
+ expected_repr = "ImmutableDict({0})".format(initial_data_repr)
+ assert actual_repr == expected_repr
diff --git a/test/units/module_utils/common/test_dict_transformations.py b/test/units/module_utils/common/test_dict_transformations.py
new file mode 100644
index 0000000..ba55299
--- /dev/null
+++ b/test/units/module_utils/common/test_dict_transformations.py
@@ -0,0 +1,153 @@
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2017, Will Thames <will.thames@xvt.com.au>
+# 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 pytest
+
+from ansible.module_utils.common.dict_transformations import (
+ _camel_to_snake,
+ _snake_to_camel,
+ camel_dict_to_snake_dict,
+ dict_merge,
+ recursive_diff,
+)
+
+
+EXPECTED_SNAKIFICATION = {
+ 'alllower': 'alllower',
+ 'TwoWords': 'two_words',
+ 'AllUpperAtEND': 'all_upper_at_end',
+ 'AllUpperButPLURALs': 'all_upper_but_plurals',
+ 'TargetGroupARNs': 'target_group_arns',
+ 'HTTPEndpoints': 'http_endpoints',
+ 'PLURALs': 'plurals'
+}
+
+EXPECTED_REVERSIBLE = {
+ 'TwoWords': 'two_words',
+ 'AllUpperAtEND': 'all_upper_at_e_n_d',
+ 'AllUpperButPLURALs': 'all_upper_but_p_l_u_r_a_ls',
+ 'TargetGroupARNs': 'target_group_a_r_ns',
+ 'HTTPEndpoints': 'h_t_t_p_endpoints',
+ 'PLURALs': 'p_l_u_r_a_ls'
+}
+
+
+class TestCaseCamelToSnake:
+
+ def test_camel_to_snake(self):
+ for (k, v) in EXPECTED_SNAKIFICATION.items():
+ assert _camel_to_snake(k) == v
+
+ def test_reversible_camel_to_snake(self):
+ for (k, v) in EXPECTED_REVERSIBLE.items():
+ assert _camel_to_snake(k, reversible=True) == v
+
+
+class TestCaseSnakeToCamel:
+
+ def test_snake_to_camel_reversed(self):
+ for (k, v) in EXPECTED_REVERSIBLE.items():
+ assert _snake_to_camel(v, capitalize_first=True) == k
+
+
+class TestCaseCamelToSnakeAndBack:
+ def test_camel_to_snake_and_back(self):
+ for (k, v) in EXPECTED_REVERSIBLE.items():
+ assert _snake_to_camel(_camel_to_snake(k, reversible=True), capitalize_first=True) == k
+
+
+class TestCaseCamelDictToSnakeDict:
+ def test_ignore_list(self):
+ camel_dict = dict(Hello=dict(One='one', Two='two'), World=dict(Three='three', Four='four'))
+ snake_dict = camel_dict_to_snake_dict(camel_dict, ignore_list='World')
+ assert snake_dict['hello'] == dict(one='one', two='two')
+ assert snake_dict['world'] == dict(Three='three', Four='four')
+
+
+class TestCaseDictMerge:
+ def test_dict_merge(self):
+ base = dict(obj2=dict(), b1=True, b2=False, b3=False,
+ one=1, two=2, three=3, obj1=dict(key1=1, key2=2),
+ l1=[1, 3], l2=[1, 2, 3], l4=[4],
+ nested=dict(n1=dict(n2=2)))
+
+ other = dict(b1=True, b2=False, b3=True, b4=True,
+ one=1, three=4, four=4, obj1=dict(key1=2),
+ l1=[2, 1], l2=[3, 2, 1], l3=[1],
+ nested=dict(n1=dict(n2=2, n3=3)))
+
+ result = dict_merge(base, other)
+
+ # string assertions
+ assert 'one' in result
+ assert 'two' in result
+ assert result['three'] == 4
+ assert result['four'] == 4
+
+ # dict assertions
+ assert 'obj1' in result
+ assert 'key1' in result['obj1']
+ assert 'key2' in result['obj1']
+
+ # list assertions
+ # this line differs from the network_utils/common test of the function of the
+ # same name as this method does not merge lists
+ assert result['l1'], [2, 1]
+ assert 'l2' in result
+ assert result['l3'], [1]
+ assert 'l4' in result
+
+ # nested assertions
+ assert 'obj1' in result
+ assert result['obj1']['key1'], 2
+ assert 'key2' in result['obj1']
+
+ # bool assertions
+ assert 'b1' in result
+ assert 'b2' in result
+ assert result['b3']
+ assert result['b4']
+
+
+class TestCaseAzureIncidental:
+
+ def test_dict_merge_invalid_dict(self):
+ ''' if b is not a dict, return b '''
+ res = dict_merge({}, None)
+ assert res is None
+
+ def test_merge_sub_dicts(self):
+ '''merge sub dicts '''
+ a = {'a': {'a1': 1}}
+ b = {'a': {'b1': 2}}
+ c = {'a': {'a1': 1, 'b1': 2}}
+ res = dict_merge(a, b)
+ assert res == c
+
+
+class TestCaseRecursiveDiff:
+ def test_recursive_diff(self):
+ a = {'foo': {'bar': [{'baz': {'qux': 'ham_sandwich'}}]}}
+ c = {'foo': {'bar': [{'baz': {'qux': 'ham_sandwich'}}]}}
+ b = {'foo': {'bar': [{'baz': {'qux': 'turkey_sandwich'}}]}}
+
+ assert recursive_diff(a, b) is not None
+ assert len(recursive_diff(a, b)) == 2
+ assert recursive_diff(a, c) is None
+
+ @pytest.mark.parametrize(
+ 'p1, p2', (
+ ([1, 2], [2, 3]),
+ ({1: 2}, [2, 3]),
+ ([1, 2], {2: 3}),
+ ({2: 3}, 'notadict'),
+ ('notadict', {2: 3}),
+ )
+ )
+ def test_recursive_diff_negative(self, p1, p2):
+ with pytest.raises(TypeError, match="Unable to diff"):
+ recursive_diff(p1, p2)
diff --git a/test/units/module_utils/common/test_locale.py b/test/units/module_utils/common/test_locale.py
new file mode 100644
index 0000000..9d95986
--- /dev/null
+++ b/test/units/module_utils/common/test_locale.py
@@ -0,0 +1,42 @@
+# -*- coding: utf-8 -*-
+# (c) 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 units.compat.mock import MagicMock
+
+from ansible.module_utils.common.locale import get_best_parsable_locale
+
+
+class TestLocale:
+ """Tests for get_best_paresable_locale"""
+
+ mock_module = MagicMock()
+ mock_module.get_bin_path = MagicMock(return_value='/usr/bin/locale')
+
+ def test_finding_best(self):
+ self.mock_module.run_command = MagicMock(return_value=(0, "C.utf8\nen_US.utf8\nC\nPOSIX\n", ''))
+ locale = get_best_parsable_locale(self.mock_module)
+ assert locale == 'C.utf8'
+
+ def test_finding_last(self):
+ self.mock_module.run_command = MagicMock(return_value=(0, "fr_FR.utf8\nen_UK.utf8\nC\nPOSIX\n", ''))
+ locale = get_best_parsable_locale(self.mock_module)
+ assert locale == 'C'
+
+ def test_finding_middle(self):
+ self.mock_module.run_command = MagicMock(return_value=(0, "fr_FR.utf8\nen_US.utf8\nC\nPOSIX\n", ''))
+ locale = get_best_parsable_locale(self.mock_module)
+ assert locale == 'en_US.utf8'
+
+ def test_finding_prefered(self):
+ self.mock_module.run_command = MagicMock(return_value=(0, "es_ES.utf8\nMINE\nC\nPOSIX\n", ''))
+ locale = get_best_parsable_locale(self.mock_module, preferences=['MINE', 'C.utf8'])
+ assert locale == 'MINE'
+
+ def test_finding_C_on_no_match(self):
+ self.mock_module.run_command = MagicMock(return_value=(0, "fr_FR.UTF8\nMINE\n", ''))
+ locale = get_best_parsable_locale(self.mock_module)
+ assert locale == 'C'
diff --git a/test/units/module_utils/common/test_network.py b/test/units/module_utils/common/test_network.py
new file mode 100644
index 0000000..27d9503
--- /dev/null
+++ b/test/units/module_utils/common/test_network.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# (c) 2017 Red Hat, Inc.
+# 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 pytest
+
+from ansible.module_utils.common.network import (
+ to_bits,
+ to_masklen,
+ to_netmask,
+ to_subnet,
+ to_ipv6_network,
+ is_masklen,
+ is_netmask
+)
+
+
+def test_to_masklen():
+ assert 24 == to_masklen('255.255.255.0')
+
+
+def test_to_masklen_invalid():
+ with pytest.raises(ValueError):
+ to_masklen('255')
+
+
+def test_to_netmask():
+ assert '255.0.0.0' == to_netmask(8)
+ assert '255.0.0.0' == to_netmask('8')
+
+
+def test_to_netmask_invalid():
+ with pytest.raises(ValueError):
+ to_netmask(128)
+
+
+def test_to_subnet():
+ result = to_subnet('192.168.1.1', 24)
+ assert '192.168.1.0/24' == result
+
+ result = to_subnet('192.168.1.1', 24, dotted_notation=True)
+ assert '192.168.1.0 255.255.255.0' == result
+
+
+def test_to_subnet_invalid():
+ with pytest.raises(ValueError):
+ to_subnet('foo', 'bar')
+
+
+def test_is_masklen():
+ assert is_masklen(32)
+ assert not is_masklen(33)
+ assert not is_masklen('foo')
+
+
+def test_is_netmask():
+ assert is_netmask('255.255.255.255')
+ assert not is_netmask(24)
+ assert not is_netmask('foo')
+
+
+def test_to_ipv6_network():
+ assert '2001:db8::' == to_ipv6_network('2001:db8::')
+ assert '2001:0db8:85a3::' == to_ipv6_network('2001:0db8:85a3:0000:0000:8a2e:0370:7334')
+ assert '2001:0db8:85a3::' == to_ipv6_network('2001:0db8:85a3:0:0:8a2e:0370:7334')
+
+
+def test_to_bits():
+ assert to_bits('0') == '00000000'
+ assert to_bits('1') == '00000001'
+ assert to_bits('2') == '00000010'
+ assert to_bits('1337') == '10100111001'
+ assert to_bits('127.0.0.1') == '01111111000000000000000000000001'
+ assert to_bits('255.255.255.255') == '11111111111111111111111111111111'
+ assert to_bits('255.255.255.0') == '11111111111111111111111100000000'
diff --git a/test/units/module_utils/common/test_sys_info.py b/test/units/module_utils/common/test_sys_info.py
new file mode 100644
index 0000000..18aafe5
--- /dev/null
+++ b/test/units/module_utils/common/test_sys_info.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# (c) 2017-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
+
+import pytest
+
+from units.compat.mock import patch
+
+from ansible.module_utils.six.moves import builtins
+
+# Functions being tested
+from ansible.module_utils.common.sys_info import get_distribution
+from ansible.module_utils.common.sys_info import get_distribution_version
+from ansible.module_utils.common.sys_info import get_platform_subclass
+
+
+realimport = builtins.__import__
+
+
+@pytest.fixture
+def platform_linux(mocker):
+ mocker.patch('platform.system', return_value='Linux')
+
+
+#
+# get_distribution tests
+#
+
+@pytest.mark.parametrize(
+ ('system', 'dist'),
+ (
+ ('Darwin', 'Darwin'),
+ ('SunOS', 'Solaris'),
+ ('FreeBSD', 'Freebsd'),
+ ),
+)
+def test_get_distribution_not_linux(system, dist, mocker):
+ """For platforms other than Linux, return the distribution"""
+ mocker.patch('platform.system', return_value=system)
+ mocker.patch('ansible.module_utils.common.sys_info.distro.id', return_value=dist)
+ assert get_distribution() == dist
+
+
+@pytest.mark.usefixtures("platform_linux")
+class TestGetDistribution:
+ """Tests for get_distribution that have to find something"""
+ def test_distro_known(self):
+ with patch('ansible.module_utils.distro.id', return_value="alpine"):
+ assert get_distribution() == "Alpine"
+
+ with patch('ansible.module_utils.distro.id', return_value="arch"):
+ assert get_distribution() == "Arch"
+
+ with patch('ansible.module_utils.distro.id', return_value="centos"):
+ assert get_distribution() == "Centos"
+
+ with patch('ansible.module_utils.distro.id', return_value="clear-linux-os"):
+ assert get_distribution() == "Clear-linux-os"
+
+ with patch('ansible.module_utils.distro.id', return_value="coreos"):
+ assert get_distribution() == "Coreos"
+
+ with patch('ansible.module_utils.distro.id', return_value="debian"):
+ assert get_distribution() == "Debian"
+
+ with patch('ansible.module_utils.distro.id', return_value="flatcar"):
+ assert get_distribution() == "Flatcar"
+
+ with patch('ansible.module_utils.distro.id', return_value="linuxmint"):
+ assert get_distribution() == "Linuxmint"
+
+ with patch('ansible.module_utils.distro.id', return_value="opensuse"):
+ assert get_distribution() == "Opensuse"
+
+ with patch('ansible.module_utils.distro.id', return_value="oracle"):
+ assert get_distribution() == "Oracle"
+
+ with patch('ansible.module_utils.distro.id', return_value="raspian"):
+ assert get_distribution() == "Raspian"
+
+ with patch('ansible.module_utils.distro.id', return_value="rhel"):
+ assert get_distribution() == "Redhat"
+
+ with patch('ansible.module_utils.distro.id', return_value="ubuntu"):
+ assert get_distribution() == "Ubuntu"
+
+ with patch('ansible.module_utils.distro.id', return_value="virtuozzo"):
+ assert get_distribution() == "Virtuozzo"
+
+ with patch('ansible.module_utils.distro.id', return_value="foo"):
+ assert get_distribution() == "Foo"
+
+ def test_distro_unknown(self):
+ with patch('ansible.module_utils.distro.id', return_value=""):
+ assert get_distribution() == "OtherLinux"
+
+ def test_distro_amazon_linux_short(self):
+ with patch('ansible.module_utils.distro.id', return_value="amzn"):
+ assert get_distribution() == "Amazon"
+
+ def test_distro_amazon_linux_long(self):
+ with patch('ansible.module_utils.distro.id', return_value="amazon"):
+ assert get_distribution() == "Amazon"
+
+
+#
+# get_distribution_version tests
+#
+
+@pytest.mark.parametrize(
+ ('system', 'version'),
+ (
+ ('Darwin', '19.6.0'),
+ ('SunOS', '11.4'),
+ ('FreeBSD', '12.1'),
+ ),
+)
+def test_get_distribution_version_not_linux(mocker, system, version):
+ """If it's not Linux, then it has no distribution"""
+ mocker.patch('platform.system', return_value=system)
+ mocker.patch('ansible.module_utils.common.sys_info.distro.version', return_value=version)
+ assert get_distribution_version() == version
+
+
+@pytest.mark.usefixtures("platform_linux")
+def test_distro_found():
+ with patch('ansible.module_utils.distro.version', return_value="1"):
+ assert get_distribution_version() == "1"
+
+
+#
+# Tests for get_platform_subclass
+#
+
+class TestGetPlatformSubclass:
+ class LinuxTest:
+ pass
+
+ class Foo(LinuxTest):
+ platform = "Linux"
+ distribution = None
+
+ class Bar(LinuxTest):
+ platform = "Linux"
+ distribution = "Bar"
+
+ def test_not_linux(self):
+ # if neither match, the fallback should be the top-level class
+ with patch('platform.system', return_value="Foo"):
+ with patch('ansible.module_utils.common.sys_info.get_distribution', return_value=None):
+ assert get_platform_subclass(self.LinuxTest) is self.LinuxTest
+
+ @pytest.mark.usefixtures("platform_linux")
+ def test_get_distribution_none(self):
+ # match just the platform class, not a specific distribution
+ with patch('ansible.module_utils.common.sys_info.get_distribution', return_value=None):
+ assert get_platform_subclass(self.LinuxTest) is self.Foo
+
+ @pytest.mark.usefixtures("platform_linux")
+ def test_get_distribution_found(self):
+ # match both the distribution and platform class
+ with patch('ansible.module_utils.common.sys_info.get_distribution', return_value="Bar"):
+ assert get_platform_subclass(self.LinuxTest) is self.Bar
diff --git a/test/units/module_utils/common/test_utils.py b/test/units/module_utils/common/test_utils.py
new file mode 100644
index 0000000..ef95239
--- /dev/null
+++ b/test/units/module_utils/common/test_utils.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# (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
+
+from ansible.module_utils.common.sys_info import get_all_subclasses
+
+
+#
+# Tests for get_all_subclasses
+#
+
+class TestGetAllSubclasses:
+ class Base:
+ pass
+
+ class BranchI(Base):
+ pass
+
+ class BranchII(Base):
+ pass
+
+ class BranchIA(BranchI):
+ pass
+
+ class BranchIB(BranchI):
+ pass
+
+ class BranchIIA(BranchII):
+ pass
+
+ class BranchIIB(BranchII):
+ pass
+
+ def test_bottom_level(self):
+ assert get_all_subclasses(self.BranchIIB) == set()
+
+ def test_one_inheritance(self):
+ assert set(get_all_subclasses(self.BranchII)) == set([self.BranchIIA, self.BranchIIB])
+
+ def test_toplevel(self):
+ assert set(get_all_subclasses(self.Base)) == set([self.BranchI, self.BranchII,
+ self.BranchIA, self.BranchIB,
+ self.BranchIIA, self.BranchIIB])
diff --git a/test/units/module_utils/common/text/converters/test_container_to_bytes.py b/test/units/module_utils/common/text/converters/test_container_to_bytes.py
new file mode 100644
index 0000000..091545e
--- /dev/null
+++ b/test/units/module_utils/common/text/converters/test_container_to_bytes.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru>
+# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+
+from ansible.module_utils.common.text.converters import container_to_bytes
+
+
+DEFAULT_ENCODING = 'utf-8'
+DEFAULT_ERR_HANDLER = 'surrogate_or_strict'
+
+
+@pytest.mark.parametrize(
+ 'test_input,expected',
+ [
+ ({1: 1}, {1: 1}),
+ ([1, 2], [1, 2]),
+ ((1, 2), (1, 2)),
+ (1, 1),
+ (1.1, 1.1),
+ (b'str', b'str'),
+ (u'str', b'str'),
+ ([u'str'], [b'str']),
+ ((u'str'), (b'str')),
+ ({u'str': u'str'}, {b'str': b'str'}),
+ ]
+)
+@pytest.mark.parametrize('encoding', ['utf-8', 'latin1', 'shift_jis', 'big5', 'koi8_r'])
+@pytest.mark.parametrize('errors', ['strict', 'surrogate_or_strict', 'surrogate_then_replace'])
+def test_container_to_bytes(test_input, expected, encoding, errors):
+ """Test for passing objects to container_to_bytes()."""
+ assert container_to_bytes(test_input, encoding=encoding, errors=errors) == expected
+
+
+@pytest.mark.parametrize(
+ 'test_input,expected',
+ [
+ ({1: 1}, {1: 1}),
+ ([1, 2], [1, 2]),
+ ((1, 2), (1, 2)),
+ (1, 1),
+ (1.1, 1.1),
+ (True, True),
+ (None, None),
+ (u'str', u'str'.encode(DEFAULT_ENCODING)),
+ (u'くらとみ', u'くらとみ'.encode(DEFAULT_ENCODING)),
+ (u'café', u'café'.encode(DEFAULT_ENCODING)),
+ (b'str', u'str'.encode(DEFAULT_ENCODING)),
+ (u'str', u'str'.encode(DEFAULT_ENCODING)),
+ ([u'str'], [u'str'.encode(DEFAULT_ENCODING)]),
+ ((u'str'), (u'str'.encode(DEFAULT_ENCODING))),
+ ({u'str': u'str'}, {u'str'.encode(DEFAULT_ENCODING): u'str'.encode(DEFAULT_ENCODING)}),
+ ]
+)
+def test_container_to_bytes_default_encoding_err(test_input, expected):
+ """
+ Test for passing objects to container_to_bytes(). Default encoding and errors
+ """
+ assert container_to_bytes(test_input, encoding=DEFAULT_ENCODING,
+ errors=DEFAULT_ERR_HANDLER) == expected
+
+
+@pytest.mark.parametrize(
+ 'test_input,encoding',
+ [
+ (u'くらとみ', 'latin1'),
+ (u'café', 'shift_jis'),
+ ]
+)
+@pytest.mark.parametrize('errors', ['surrogate_or_strict', 'strict'])
+def test_container_to_bytes_incomp_chars_and_encod(test_input, encoding, errors):
+ """
+ Test for passing incompatible characters and encodings container_to_bytes().
+ """
+ with pytest.raises(UnicodeEncodeError, match="codec can't encode"):
+ container_to_bytes(test_input, encoding=encoding, errors=errors)
+
+
+@pytest.mark.parametrize(
+ 'test_input,encoding,expected',
+ [
+ (u'くらとみ', 'latin1', b'????'),
+ (u'café', 'shift_jis', b'caf?'),
+ ]
+)
+def test_container_to_bytes_surrogate_then_replace(test_input, encoding, expected):
+ """
+ Test for container_to_bytes() with surrogate_then_replace err handler.
+ """
+ assert container_to_bytes(test_input, encoding=encoding,
+ errors='surrogate_then_replace') == expected
diff --git a/test/units/module_utils/common/text/converters/test_container_to_text.py b/test/units/module_utils/common/text/converters/test_container_to_text.py
new file mode 100644
index 0000000..39038f5
--- /dev/null
+++ b/test/units/module_utils/common/text/converters/test_container_to_text.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru>
+# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+
+from ansible.module_utils.common.text.converters import container_to_text
+
+
+DEFAULT_ENCODING = 'utf-8'
+DEFAULT_ERR_HANDLER = 'surrogate_or_strict'
+
+
+@pytest.mark.parametrize(
+ 'test_input,expected',
+ [
+ ({1: 1}, {1: 1}),
+ ([1, 2], [1, 2]),
+ ((1, 2), (1, 2)),
+ (1, 1),
+ (1.1, 1.1),
+ (b'str', u'str'),
+ (u'str', u'str'),
+ ([b'str'], [u'str']),
+ ((b'str'), (u'str')),
+ ({b'str': b'str'}, {u'str': u'str'}),
+ ]
+)
+@pytest.mark.parametrize('encoding', ['utf-8', 'latin1', 'shift-jis', 'big5', 'koi8_r', ])
+@pytest.mark.parametrize('errors', ['strict', 'surrogate_or_strict', 'surrogate_then_replace', ])
+def test_container_to_text_different_types(test_input, expected, encoding, errors):
+ """Test for passing objects to container_to_text()."""
+ assert container_to_text(test_input, encoding=encoding, errors=errors) == expected
+
+
+@pytest.mark.parametrize(
+ 'test_input,expected',
+ [
+ ({1: 1}, {1: 1}),
+ ([1, 2], [1, 2]),
+ ((1, 2), (1, 2)),
+ (1, 1),
+ (1.1, 1.1),
+ (True, True),
+ (None, None),
+ (u'str', u'str'),
+ (u'くらとみ'.encode(DEFAULT_ENCODING), u'くらとみ'),
+ (u'café'.encode(DEFAULT_ENCODING), u'café'),
+ (u'str'.encode(DEFAULT_ENCODING), u'str'),
+ ([u'str'.encode(DEFAULT_ENCODING)], [u'str']),
+ ((u'str'.encode(DEFAULT_ENCODING)), (u'str')),
+ ({b'str': b'str'}, {u'str': u'str'}),
+ ]
+)
+def test_container_to_text_default_encoding_and_err(test_input, expected):
+ """
+ Test for passing objects to container_to_text(). Default encoding and errors
+ """
+ assert container_to_text(test_input, encoding=DEFAULT_ENCODING,
+ errors=DEFAULT_ERR_HANDLER) == expected
+
+
+@pytest.mark.parametrize(
+ 'test_input,encoding,expected',
+ [
+ (u'й'.encode('utf-8'), 'latin1', u'й'),
+ (u'café'.encode('utf-8'), 'shift_jis', u'cafテゥ'),
+ ]
+)
+@pytest.mark.parametrize('errors', ['strict', 'surrogate_or_strict', 'surrogate_then_replace', ])
+def test_container_to_text_incomp_encod_chars(test_input, encoding, errors, expected):
+ """
+ Test for passing incompatible characters and encodings container_to_text().
+ """
+ assert container_to_text(test_input, encoding=encoding, errors=errors) == expected
diff --git a/test/units/module_utils/common/text/converters/test_json_encode_fallback.py b/test/units/module_utils/common/text/converters/test_json_encode_fallback.py
new file mode 100644
index 0000000..022f38f
--- /dev/null
+++ b/test/units/module_utils/common/text/converters/test_json_encode_fallback.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru>
+# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+
+from datetime import datetime, timedelta, tzinfo
+
+from ansible.module_utils.common.text.converters import _json_encode_fallback
+
+
+class timezone(tzinfo):
+ """Simple timezone implementation for use until we drop Python 2.7 support."""
+ def __init__(self, offset):
+ self._offset = offset
+
+ def utcoffset(self, dt):
+ return self._offset
+
+ def dst(self, dt):
+ return timedelta(0)
+
+ def tzname(self, dt):
+ return None
+
+
+@pytest.mark.parametrize(
+ 'test_input,expected',
+ [
+ (set([1]), [1]),
+ (datetime(2019, 5, 14, 13, 39, 38, 569047), '2019-05-14T13:39:38.569047'),
+ (datetime(2019, 5, 14, 13, 47, 16, 923866), '2019-05-14T13:47:16.923866'),
+ (datetime(2019, 6, 15, 14, 45, tzinfo=timezone(timedelta(0))), '2019-06-15T14:45:00+00:00'),
+ (datetime(2019, 6, 15, 14, 45, tzinfo=timezone(timedelta(hours=1, minutes=40))), '2019-06-15T14:45:00+01:40'),
+ ]
+)
+def test_json_encode_fallback(test_input, expected):
+ """
+ Test for passing expected objects to _json_encode_fallback().
+ """
+ assert _json_encode_fallback(test_input) == expected
+
+
+@pytest.mark.parametrize(
+ 'test_input',
+ [
+ 1,
+ 1.1,
+ u'string',
+ b'string',
+ [1, 2],
+ True,
+ None,
+ {1: 1},
+ (1, 2),
+ ]
+)
+def test_json_encode_fallback_default_behavior(test_input):
+ """
+ Test for _json_encode_fallback() default behavior.
+
+ It must fail with TypeError.
+ """
+ with pytest.raises(TypeError, match='Cannot json serialize'):
+ _json_encode_fallback(test_input)
diff --git a/test/units/module_utils/common/text/converters/test_jsonify.py b/test/units/module_utils/common/text/converters/test_jsonify.py
new file mode 100644
index 0000000..a341531
--- /dev/null
+++ b/test/units/module_utils/common/text/converters/test_jsonify.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru>
+# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+
+from ansible.module_utils.common.text.converters import jsonify
+
+
+@pytest.mark.parametrize(
+ 'test_input,expected',
+ [
+ (1, '1'),
+ (u'string', u'"string"'),
+ (u'くらとみ', u'"\\u304f\\u3089\\u3068\\u307f"'),
+ (u'café', u'"caf\\u00e9"'),
+ (b'string', u'"string"'),
+ (False, u'false'),
+ (u'string'.encode('utf-8'), u'"string"'),
+ ]
+)
+def test_jsonify(test_input, expected):
+ """Test for jsonify()."""
+ assert jsonify(test_input) == expected
diff --git a/test/units/module_utils/common/text/converters/test_to_str.py b/test/units/module_utils/common/text/converters/test_to_str.py
new file mode 100644
index 0000000..712ed85
--- /dev/null
+++ b/test/units/module_utils/common/text/converters/test_to_str.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# 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
+
+import itertools
+
+import pytest
+
+from ansible.module_utils.six import PY3
+
+from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native
+
+
+# Format: byte representation, text representation, encoding of byte representation
+VALID_STRINGS = (
+ (b'abcde', u'abcde', 'ascii'),
+ (b'caf\xc3\xa9', u'caf\xe9', 'utf-8'),
+ (b'caf\xe9', u'caf\xe9', 'latin-1'),
+ # u'くらとみ'
+ (b'\xe3\x81\x8f\xe3\x82\x89\xe3\x81\xa8\xe3\x81\xbf', u'\u304f\u3089\u3068\u307f', 'utf-8'),
+ (b'\x82\xad\x82\xe7\x82\xc6\x82\xdd', u'\u304f\u3089\u3068\u307f', 'shift-jis'),
+)
+
+
+@pytest.mark.parametrize('in_string, encoding, expected',
+ itertools.chain(((d[0], d[2], d[1]) for d in VALID_STRINGS),
+ ((d[1], d[2], d[1]) for d in VALID_STRINGS)))
+def test_to_text(in_string, encoding, expected):
+ """test happy path of decoding to text"""
+ assert to_text(in_string, encoding) == expected
+
+
+@pytest.mark.parametrize('in_string, encoding, expected',
+ itertools.chain(((d[0], d[2], d[0]) for d in VALID_STRINGS),
+ ((d[1], d[2], d[0]) for d in VALID_STRINGS)))
+def test_to_bytes(in_string, encoding, expected):
+ """test happy path of encoding to bytes"""
+ assert to_bytes(in_string, encoding) == expected
+
+
+@pytest.mark.parametrize('in_string, encoding, expected',
+ itertools.chain(((d[0], d[2], d[1] if PY3 else d[0]) for d in VALID_STRINGS),
+ ((d[1], d[2], d[1] if PY3 else d[0]) for d in VALID_STRINGS)))
+def test_to_native(in_string, encoding, expected):
+ """test happy path of encoding to native strings"""
+ assert to_native(in_string, encoding) == expected
diff --git a/test/units/module_utils/common/text/formatters/test_bytes_to_human.py b/test/units/module_utils/common/text/formatters/test_bytes_to_human.py
new file mode 100644
index 0000000..41475f5
--- /dev/null
+++ b/test/units/module_utils/common/text/formatters/test_bytes_to_human.py
@@ -0,0 +1,116 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru>
+# 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 pytest
+
+from ansible.module_utils.common.text.formatters import bytes_to_human
+
+
+@pytest.mark.parametrize(
+ 'input_data,expected',
+ [
+ (0, u'0.00 Bytes'),
+ (0.5, u'0.50 Bytes'),
+ (0.54, u'0.54 Bytes'),
+ (1024, u'1.00 KB'),
+ (1025, u'1.00 KB'),
+ (1536, u'1.50 KB'),
+ (1790, u'1.75 KB'),
+ (1048576, u'1.00 MB'),
+ (1073741824, u'1.00 GB'),
+ (1099511627776, u'1.00 TB'),
+ (1125899906842624, u'1.00 PB'),
+ (1152921504606846976, u'1.00 EB'),
+ (1180591620717411303424, u'1.00 ZB'),
+ (1208925819614629174706176, u'1.00 YB'),
+ ]
+)
+def test_bytes_to_human(input_data, expected):
+ """Test of bytes_to_human function, only proper numbers are passed."""
+ assert bytes_to_human(input_data) == expected
+
+
+@pytest.mark.parametrize(
+ 'input_data,expected',
+ [
+ (0, u'0.00 bits'),
+ (0.5, u'0.50 bits'),
+ (0.54, u'0.54 bits'),
+ (1024, u'1.00 Kb'),
+ (1025, u'1.00 Kb'),
+ (1536, u'1.50 Kb'),
+ (1790, u'1.75 Kb'),
+ (1048576, u'1.00 Mb'),
+ (1073741824, u'1.00 Gb'),
+ (1099511627776, u'1.00 Tb'),
+ (1125899906842624, u'1.00 Pb'),
+ (1152921504606846976, u'1.00 Eb'),
+ (1180591620717411303424, u'1.00 Zb'),
+ (1208925819614629174706176, u'1.00 Yb'),
+ ]
+)
+def test_bytes_to_human_isbits(input_data, expected):
+ """Test of bytes_to_human function with isbits=True proper results."""
+ assert bytes_to_human(input_data, isbits=True) == expected
+
+
+@pytest.mark.parametrize(
+ 'input_data,unit,expected',
+ [
+ (0, u'B', u'0.00 Bytes'),
+ (0.5, u'B', u'0.50 Bytes'),
+ (0.54, u'B', u'0.54 Bytes'),
+ (1024, u'K', u'1.00 KB'),
+ (1536, u'K', u'1.50 KB'),
+ (1790, u'K', u'1.75 KB'),
+ (1048576, u'M', u'1.00 MB'),
+ (1099511627776, u'T', u'1.00 TB'),
+ (1152921504606846976, u'E', u'1.00 EB'),
+ (1180591620717411303424, u'Z', u'1.00 ZB'),
+ (1208925819614629174706176, u'Y', u'1.00 YB'),
+ (1025, u'KB', u'1025.00 Bytes'),
+ (1073741824, u'Gb', u'1073741824.00 Bytes'),
+ (1125899906842624, u'Pb', u'1125899906842624.00 Bytes'),
+ ]
+)
+def test_bytes_to_human_unit(input_data, unit, expected):
+ """Test unit argument of bytes_to_human function proper results."""
+ assert bytes_to_human(input_data, unit=unit) == expected
+
+
+@pytest.mark.parametrize(
+ 'input_data,unit,expected',
+ [
+ (0, u'B', u'0.00 bits'),
+ (0.5, u'B', u'0.50 bits'),
+ (0.54, u'B', u'0.54 bits'),
+ (1024, u'K', u'1.00 Kb'),
+ (1536, u'K', u'1.50 Kb'),
+ (1790, u'K', u'1.75 Kb'),
+ (1048576, u'M', u'1.00 Mb'),
+ (1099511627776, u'T', u'1.00 Tb'),
+ (1152921504606846976, u'E', u'1.00 Eb'),
+ (1180591620717411303424, u'Z', u'1.00 Zb'),
+ (1208925819614629174706176, u'Y', u'1.00 Yb'),
+ (1025, u'KB', u'1025.00 bits'),
+ (1073741824, u'Gb', u'1073741824.00 bits'),
+ (1125899906842624, u'Pb', u'1125899906842624.00 bits'),
+ ]
+)
+def test_bytes_to_human_unit_isbits(input_data, unit, expected):
+ """Test unit argument of bytes_to_human function with isbits=True proper results."""
+ assert bytes_to_human(input_data, isbits=True, unit=unit) == expected
+
+
+@pytest.mark.parametrize('input_data', [0j, u'1B', [1], {1: 1}, None, b'1B'])
+def test_bytes_to_human_illegal_size(input_data):
+ """Test of bytes_to_human function, illegal objects are passed as a size."""
+ e_regexp = (r'(no ordering relation is defined for complex numbers)|'
+ r'(unsupported operand type\(s\) for /)|(unorderable types)|'
+ r'(not supported between instances of)')
+ with pytest.raises(TypeError, match=e_regexp):
+ bytes_to_human(input_data)
diff --git a/test/units/module_utils/common/text/formatters/test_human_to_bytes.py b/test/units/module_utils/common/text/formatters/test_human_to_bytes.py
new file mode 100644
index 0000000..d02699a
--- /dev/null
+++ b/test/units/module_utils/common/text/formatters/test_human_to_bytes.py
@@ -0,0 +1,185 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru>
+# Copyright 2019, Sviatoslav Sydorenko <webknjaz@redhat.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+
+from ansible.module_utils.common.text.formatters import human_to_bytes
+
+
+NUM_IN_METRIC = {
+ 'K': 2 ** 10,
+ 'M': 2 ** 20,
+ 'G': 2 ** 30,
+ 'T': 2 ** 40,
+ 'P': 2 ** 50,
+ 'E': 2 ** 60,
+ 'Z': 2 ** 70,
+ 'Y': 2 ** 80,
+}
+
+
+@pytest.mark.parametrize(
+ 'input_data,expected',
+ [
+ (0, 0),
+ (u'0B', 0),
+ (1024, NUM_IN_METRIC['K']),
+ (u'1024B', NUM_IN_METRIC['K']),
+ (u'1K', NUM_IN_METRIC['K']),
+ (u'1KB', NUM_IN_METRIC['K']),
+ (u'1M', NUM_IN_METRIC['M']),
+ (u'1MB', NUM_IN_METRIC['M']),
+ (u'1G', NUM_IN_METRIC['G']),
+ (u'1GB', NUM_IN_METRIC['G']),
+ (u'1T', NUM_IN_METRIC['T']),
+ (u'1TB', NUM_IN_METRIC['T']),
+ (u'1P', NUM_IN_METRIC['P']),
+ (u'1PB', NUM_IN_METRIC['P']),
+ (u'1E', NUM_IN_METRIC['E']),
+ (u'1EB', NUM_IN_METRIC['E']),
+ (u'1Z', NUM_IN_METRIC['Z']),
+ (u'1ZB', NUM_IN_METRIC['Z']),
+ (u'1Y', NUM_IN_METRIC['Y']),
+ (u'1YB', NUM_IN_METRIC['Y']),
+ ]
+)
+def test_human_to_bytes_number(input_data, expected):
+ """Test of human_to_bytes function, only number arg is passed."""
+ assert human_to_bytes(input_data) == expected
+
+
+@pytest.mark.parametrize(
+ 'input_data,unit',
+ [
+ (u'1024', 'B'),
+ (1, u'K'),
+ (1, u'KB'),
+ (u'1', u'M'),
+ (u'1', u'MB'),
+ (1, u'G'),
+ (1, u'GB'),
+ (1, u'T'),
+ (1, u'TB'),
+ (u'1', u'P'),
+ (u'1', u'PB'),
+ (u'1', u'E'),
+ (u'1', u'EB'),
+ (u'1', u'Z'),
+ (u'1', u'ZB'),
+ (u'1', u'Y'),
+ (u'1', u'YB'),
+ ]
+)
+def test_human_to_bytes_number_unit(input_data, unit):
+ """Test of human_to_bytes function, number and default_unit args are passed."""
+ assert human_to_bytes(input_data, default_unit=unit) == NUM_IN_METRIC.get(unit[0], 1024)
+
+
+@pytest.mark.parametrize('test_input', [u'1024s', u'1024w', ])
+def test_human_to_bytes_wrong_unit(test_input):
+ """Test of human_to_bytes function, wrong units."""
+ with pytest.raises(ValueError, match="The suffix must be one of"):
+ human_to_bytes(test_input)
+
+
+@pytest.mark.parametrize('test_input', [u'b1bbb', u'm2mmm', u'', u' ', -1])
+def test_human_to_bytes_wrong_number(test_input):
+ """Test of human_to_bytes function, number param is invalid string / number."""
+ with pytest.raises(ValueError, match="can't interpret"):
+ human_to_bytes(test_input)
+
+
+@pytest.mark.parametrize(
+ 'input_data,expected',
+ [
+ (0, 0),
+ (u'0B', 0),
+ (u'1024b', 1024),
+ (u'1024B', 1024),
+ (u'1K', NUM_IN_METRIC['K']),
+ (u'1Kb', NUM_IN_METRIC['K']),
+ (u'1M', NUM_IN_METRIC['M']),
+ (u'1Mb', NUM_IN_METRIC['M']),
+ (u'1G', NUM_IN_METRIC['G']),
+ (u'1Gb', NUM_IN_METRIC['G']),
+ (u'1T', NUM_IN_METRIC['T']),
+ (u'1Tb', NUM_IN_METRIC['T']),
+ (u'1P', NUM_IN_METRIC['P']),
+ (u'1Pb', NUM_IN_METRIC['P']),
+ (u'1E', NUM_IN_METRIC['E']),
+ (u'1Eb', NUM_IN_METRIC['E']),
+ (u'1Z', NUM_IN_METRIC['Z']),
+ (u'1Zb', NUM_IN_METRIC['Z']),
+ (u'1Y', NUM_IN_METRIC['Y']),
+ (u'1Yb', NUM_IN_METRIC['Y']),
+ ]
+)
+def test_human_to_bytes_isbits(input_data, expected):
+ """Test of human_to_bytes function, isbits = True."""
+ assert human_to_bytes(input_data, isbits=True) == expected
+
+
+@pytest.mark.parametrize(
+ 'input_data,unit',
+ [
+ (1024, 'b'),
+ (1024, 'B'),
+ (1, u'K'),
+ (1, u'Kb'),
+ (u'1', u'M'),
+ (u'1', u'Mb'),
+ (1, u'G'),
+ (1, u'Gb'),
+ (1, u'T'),
+ (1, u'Tb'),
+ (u'1', u'P'),
+ (u'1', u'Pb'),
+ (u'1', u'E'),
+ (u'1', u'Eb'),
+ (u'1', u'Z'),
+ (u'1', u'Zb'),
+ (u'1', u'Y'),
+ (u'1', u'Yb'),
+ ]
+)
+def test_human_to_bytes_isbits_default_unit(input_data, unit):
+ """Test of human_to_bytes function, isbits = True and default_unit args are passed."""
+ assert human_to_bytes(input_data, default_unit=unit, isbits=True) == NUM_IN_METRIC.get(unit[0], 1024)
+
+
+@pytest.mark.parametrize(
+ 'test_input,isbits',
+ [
+ ('1024Kb', False),
+ ('10Mb', False),
+ ('1Gb', False),
+ ('10MB', True),
+ ('2KB', True),
+ ('4GB', True),
+ ]
+)
+def test_human_to_bytes_isbits_wrong_unit(test_input, isbits):
+ """Test of human_to_bytes function, unit identifier is in an invalid format for isbits value."""
+ with pytest.raises(ValueError, match="Value is not a valid string"):
+ human_to_bytes(test_input, isbits=isbits)
+
+
+@pytest.mark.parametrize(
+ 'test_input,unit,isbits',
+ [
+ (1024, 'Kb', False),
+ ('10', 'Mb', False),
+ ('10', 'MB', True),
+ (2, 'KB', True),
+ ('4', 'GB', True),
+ ]
+)
+def test_human_to_bytes_isbits_wrong_default_unit(test_input, unit, isbits):
+ """Test of human_to_bytes function, default_unit is in an invalid format for isbits value."""
+ with pytest.raises(ValueError, match="Value is not a valid string"):
+ human_to_bytes(test_input, default_unit=unit, isbits=isbits)
diff --git a/test/units/module_utils/common/text/formatters/test_lenient_lowercase.py b/test/units/module_utils/common/text/formatters/test_lenient_lowercase.py
new file mode 100644
index 0000000..1ecc013
--- /dev/null
+++ b/test/units/module_utils/common/text/formatters/test_lenient_lowercase.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru>
+# 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 datetime import datetime
+
+import pytest
+
+from ansible.module_utils.common.text.formatters import lenient_lowercase
+
+
+INPUT_LIST = [
+ u'HELLO',
+ u'Ёлка',
+ u'cafÉ',
+ u'くらとみ',
+ b'HELLO',
+ 1,
+ {1: 'Dict'},
+ True,
+ [1],
+ 3.14159,
+]
+
+EXPECTED_LIST = [
+ u'hello',
+ u'ёлка',
+ u'café',
+ u'くらとみ',
+ b'hello',
+ 1,
+ {1: 'Dict'},
+ True,
+ [1],
+ 3.14159,
+]
+
+result_list = lenient_lowercase(INPUT_LIST)
+
+
+@pytest.mark.parametrize(
+ 'input_value,expected_outcome',
+ [
+ (result_list[0], EXPECTED_LIST[0]),
+ (result_list[1], EXPECTED_LIST[1]),
+ (result_list[2], EXPECTED_LIST[2]),
+ (result_list[3], EXPECTED_LIST[3]),
+ (result_list[4], EXPECTED_LIST[4]),
+ (result_list[5], EXPECTED_LIST[5]),
+ (result_list[6], EXPECTED_LIST[6]),
+ (result_list[7], EXPECTED_LIST[7]),
+ (result_list[8], EXPECTED_LIST[8]),
+ (result_list[9], EXPECTED_LIST[9]),
+ ]
+)
+def test_lenient_lowercase(input_value, expected_outcome):
+ """Test that lenient_lowercase() proper results."""
+ assert input_value == expected_outcome
+
+
+@pytest.mark.parametrize('input_data', [1, False, 1.001, 1j, datetime.now(), ])
+def test_lenient_lowercase_illegal_data_type(input_data):
+ """Test passing objects of illegal types to lenient_lowercase()."""
+ with pytest.raises(TypeError, match='object is not iterable'):
+ lenient_lowercase(input_data)
diff --git a/test/units/module_utils/common/validation/test_check_missing_parameters.py b/test/units/module_utils/common/validation/test_check_missing_parameters.py
new file mode 100644
index 0000000..6cbcb8b
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_missing_parameters.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# 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 pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_required_one_of
+from ansible.module_utils.common.validation import check_missing_parameters
+
+
+@pytest.fixture
+def arguments_terms():
+ return {"path": ""}
+
+
+def test_check_missing_parameters():
+ assert check_missing_parameters([], {}) == []
+
+
+def test_check_missing_parameters_list():
+ expected = "missing required arguments: path"
+
+ with pytest.raises(TypeError) as e:
+ check_missing_parameters({}, ["path"])
+
+ assert to_native(e.value) == expected
+
+
+def test_check_missing_parameters_positive():
+ assert check_missing_parameters({"path": "/foo"}, ["path"]) == []
diff --git a/test/units/module_utils/common/validation/test_check_mutually_exclusive.py b/test/units/module_utils/common/validation/test_check_mutually_exclusive.py
new file mode 100644
index 0000000..7bf9076
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_mutually_exclusive.py
@@ -0,0 +1,57 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_mutually_exclusive
+
+
+@pytest.fixture
+def mutually_exclusive_terms():
+ return [
+ ('string1', 'string2',),
+ ('box', 'fox', 'socks'),
+ ]
+
+
+def test_check_mutually_exclusive(mutually_exclusive_terms):
+ params = {
+ 'string1': 'cat',
+ 'fox': 'hat',
+ }
+ assert check_mutually_exclusive(mutually_exclusive_terms, params) == []
+
+
+def test_check_mutually_exclusive_found(mutually_exclusive_terms):
+ params = {
+ 'string1': 'cat',
+ 'string2': 'hat',
+ 'fox': 'red',
+ 'socks': 'blue',
+ }
+ expected = "parameters are mutually exclusive: string1|string2, box|fox|socks"
+
+ with pytest.raises(TypeError) as e:
+ check_mutually_exclusive(mutually_exclusive_terms, params)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_mutually_exclusive_none():
+ terms = None
+ params = {
+ 'string1': 'cat',
+ 'fox': 'hat',
+ }
+ assert check_mutually_exclusive(terms, params) == []
+
+
+def test_check_mutually_exclusive_no_params(mutually_exclusive_terms):
+ with pytest.raises(TypeError) as te:
+ check_mutually_exclusive(mutually_exclusive_terms, None)
+ assert "'NoneType' object is not iterable" in to_native(te.value)
diff --git a/test/units/module_utils/common/validation/test_check_required_arguments.py b/test/units/module_utils/common/validation/test_check_required_arguments.py
new file mode 100644
index 0000000..1dd5458
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_required_arguments.py
@@ -0,0 +1,88 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_required_arguments
+
+
+@pytest.fixture
+def arguments_terms():
+ return {
+ 'foo': {
+ 'required': True,
+ },
+ 'bar': {
+ 'required': False,
+ },
+ 'tomato': {
+ 'irrelevant': 72,
+ },
+ }
+
+
+@pytest.fixture
+def arguments_terms_multiple():
+ return {
+ 'foo': {
+ 'required': True,
+ },
+ 'bar': {
+ 'required': True,
+ },
+ 'tomato': {
+ 'irrelevant': 72,
+ },
+ }
+
+
+def test_check_required_arguments(arguments_terms):
+ params = {
+ 'foo': 'hello',
+ 'bar': 'haha',
+ }
+ assert check_required_arguments(arguments_terms, params) == []
+
+
+def test_check_required_arguments_missing(arguments_terms):
+ params = {
+ 'apples': 'woohoo',
+ }
+ expected = "missing required arguments: foo"
+
+ with pytest.raises(TypeError) as e:
+ check_required_arguments(arguments_terms, params)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_arguments_missing_multiple(arguments_terms_multiple):
+ params = {
+ 'apples': 'woohoo',
+ }
+ expected = "missing required arguments: bar, foo"
+
+ with pytest.raises(TypeError) as e:
+ check_required_arguments(arguments_terms_multiple, params)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_arguments_missing_none():
+ terms = None
+ params = {
+ 'foo': 'bar',
+ 'baz': 'buzz',
+ }
+ assert check_required_arguments(terms, params) == []
+
+
+def test_check_required_arguments_no_params(arguments_terms):
+ with pytest.raises(TypeError) as te:
+ check_required_arguments(arguments_terms, None)
+ assert "'NoneType' is not iterable" in to_native(te.value)
diff --git a/test/units/module_utils/common/validation/test_check_required_by.py b/test/units/module_utils/common/validation/test_check_required_by.py
new file mode 100644
index 0000000..62cccff
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_required_by.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# 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 pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_required_by
+
+
+@pytest.fixture
+def path_arguments_terms():
+ return {
+ "path": ["mode", "owner"],
+ }
+
+
+def test_check_required_by():
+ arguments_terms = {}
+ params = {}
+ assert check_required_by(arguments_terms, params) == {}
+
+
+def test_check_required_by_missing():
+ arguments_terms = {
+ "force": "force_reason",
+ }
+ params = {"force": True}
+ expected = "missing parameter(s) required by 'force': force_reason"
+
+ with pytest.raises(TypeError) as e:
+ check_required_by(arguments_terms, params)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_by_multiple(path_arguments_terms):
+ params = {
+ "path": "/foo/bar",
+ }
+ expected = "missing parameter(s) required by 'path': mode, owner"
+
+ with pytest.raises(TypeError) as e:
+ check_required_by(path_arguments_terms, params)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_by_single(path_arguments_terms):
+ params = {"path": "/foo/bar", "mode": "0700"}
+ expected = "missing parameter(s) required by 'path': owner"
+
+ with pytest.raises(TypeError) as e:
+ check_required_by(path_arguments_terms, params)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_by_missing_none(path_arguments_terms):
+ params = {
+ "path": "/foo/bar",
+ "mode": "0700",
+ "owner": "root",
+ }
+ assert check_required_by(path_arguments_terms, params)
+
+
+def test_check_required_by_options_context(path_arguments_terms):
+ params = {"path": "/foo/bar", "mode": "0700"}
+
+ options_context = ["foo_context"]
+
+ expected = "missing parameter(s) required by 'path': owner found in foo_context"
+
+ with pytest.raises(TypeError) as e:
+ check_required_by(path_arguments_terms, params, options_context)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_by_missing_multiple_options_context(path_arguments_terms):
+ params = {
+ "path": "/foo/bar",
+ }
+ options_context = ["foo_context"]
+
+ expected = (
+ "missing parameter(s) required by 'path': mode, owner found in foo_context"
+ )
+
+ with pytest.raises(TypeError) as e:
+ check_required_by(path_arguments_terms, params, options_context)
+
+ assert to_native(e.value) == expected
diff --git a/test/units/module_utils/common/validation/test_check_required_if.py b/test/units/module_utils/common/validation/test_check_required_if.py
new file mode 100644
index 0000000..4189164
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_required_if.py
@@ -0,0 +1,79 @@
+# -*- coding: utf-8 -*-
+# 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 pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_required_if
+
+
+def test_check_required_if():
+ arguments_terms = {}
+ params = {}
+ assert check_required_if(arguments_terms, params) == []
+
+
+def test_check_required_if_missing():
+ arguments_terms = [["state", "present", ("path",)]]
+ params = {"state": "present"}
+ expected = "state is present but all of the following are missing: path"
+
+ with pytest.raises(TypeError) as e:
+ check_required_if(arguments_terms, params)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_if_missing_required():
+ arguments_terms = [["state", "present", ("path", "owner"), True]]
+ params = {"state": "present"}
+ expected = "state is present but any of the following are missing: path, owner"
+
+ with pytest.raises(TypeError) as e:
+ check_required_if(arguments_terms, params)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_if_missing_multiple():
+ arguments_terms = [["state", "present", ("path", "owner")]]
+ params = {
+ "state": "present",
+ }
+ expected = "state is present but all of the following are missing: path, owner"
+
+ with pytest.raises(TypeError) as e:
+ check_required_if(arguments_terms, params)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_if_missing_multiple_with_context():
+ arguments_terms = [["state", "present", ("path", "owner")]]
+ params = {
+ "state": "present",
+ }
+ options_context = ["foo_context"]
+ expected = "state is present but all of the following are missing: path, owner found in foo_context"
+
+ with pytest.raises(TypeError) as e:
+ check_required_if(arguments_terms, params, options_context)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_if_multiple():
+ arguments_terms = [["state", "present", ("path", "owner")]]
+ params = {
+ "state": "present",
+ "path": "/foo",
+ "owner": "root",
+ }
+ options_context = ["foo_context"]
+ assert check_required_if(arguments_terms, params) == []
+ assert check_required_if(arguments_terms, params, options_context) == []
diff --git a/test/units/module_utils/common/validation/test_check_required_one_of.py b/test/units/module_utils/common/validation/test_check_required_one_of.py
new file mode 100644
index 0000000..b081889
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_required_one_of.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# 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 pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_required_one_of
+
+
+@pytest.fixture
+def arguments_terms():
+ return [["path", "owner"]]
+
+
+def test_check_required_one_of():
+ assert check_required_one_of([], {}) == []
+
+
+def test_check_required_one_of_missing(arguments_terms):
+ params = {"state": "present"}
+ expected = "one of the following is required: path, owner"
+
+ with pytest.raises(TypeError) as e:
+ check_required_one_of(arguments_terms, params)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_one_of_provided(arguments_terms):
+ params = {"state": "present", "path": "/foo"}
+ assert check_required_one_of(arguments_terms, params) == []
+
+
+def test_check_required_one_of_context(arguments_terms):
+ params = {"state": "present"}
+ expected = "one of the following is required: path, owner found in foo_context"
+ option_context = ["foo_context"]
+
+ with pytest.raises(TypeError) as e:
+ check_required_one_of(arguments_terms, params, option_context)
+
+ assert to_native(e.value) == expected
diff --git a/test/units/module_utils/common/validation/test_check_required_together.py b/test/units/module_utils/common/validation/test_check_required_together.py
new file mode 100644
index 0000000..8a2daab
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_required_together.py
@@ -0,0 +1,57 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_required_together
+
+
+@pytest.fixture
+def together_terms():
+ return [
+ ['bananas', 'potatoes'],
+ ['cats', 'wolves']
+ ]
+
+
+def test_check_required_together(together_terms):
+ params = {
+ 'bananas': 'hello',
+ 'potatoes': 'this is here too',
+ 'dogs': 'haha',
+ }
+ assert check_required_together(together_terms, params) == []
+
+
+def test_check_required_together_missing(together_terms):
+ params = {
+ 'bananas': 'woohoo',
+ 'wolves': 'uh oh',
+ }
+ expected = "parameters are required together: bananas, potatoes"
+
+ with pytest.raises(TypeError) as e:
+ check_required_together(together_terms, params)
+
+ assert to_native(e.value) == expected
+
+
+def test_check_required_together_missing_none():
+ terms = None
+ params = {
+ 'foo': 'bar',
+ 'baz': 'buzz',
+ }
+ assert check_required_together(terms, params) == []
+
+
+def test_check_required_together_no_params(together_terms):
+ with pytest.raises(TypeError) as te:
+ check_required_together(together_terms, None)
+
+ assert "'NoneType' object is not iterable" in to_native(te.value)
diff --git a/test/units/module_utils/common/validation/test_check_type_bits.py b/test/units/module_utils/common/validation/test_check_type_bits.py
new file mode 100644
index 0000000..7f6b11d
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_type_bits.py
@@ -0,0 +1,43 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_type_bits
+
+
+def test_check_type_bits():
+ test_cases = (
+ ('1', 1),
+ (99, 99),
+ (1.5, 2),
+ ('1.5', 2),
+ ('2b', 2),
+ ('2k', 2048),
+ ('2K', 2048),
+ ('1m', 1048576),
+ ('1M', 1048576),
+ ('1g', 1073741824),
+ ('1G', 1073741824),
+ (1073741824, 1073741824),
+ )
+ for case in test_cases:
+ assert case[1] == check_type_bits(case[0])
+
+
+def test_check_type_bits_fail():
+ test_cases = (
+ 'foo',
+ '2KB',
+ '1MB',
+ '1GB',
+ )
+ for case in test_cases:
+ with pytest.raises(TypeError) as e:
+ check_type_bits(case)
+ assert 'cannot be converted to a Bit value' in to_native(e.value)
diff --git a/test/units/module_utils/common/validation/test_check_type_bool.py b/test/units/module_utils/common/validation/test_check_type_bool.py
new file mode 100644
index 0000000..bd867dc
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_type_bool.py
@@ -0,0 +1,49 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_type_bool
+
+
+def test_check_type_bool():
+ test_cases = (
+ (True, True),
+ (False, False),
+ ('1', True),
+ ('on', True),
+ (1, True),
+ ('0', False),
+ (0, False),
+ ('n', False),
+ ('f', False),
+ ('false', False),
+ ('true', True),
+ ('y', True),
+ ('t', True),
+ ('yes', True),
+ ('no', False),
+ ('off', False),
+ )
+ for case in test_cases:
+ assert case[1] == check_type_bool(case[0])
+
+
+def test_check_type_bool_fail():
+ default_test_msg = 'cannot be converted to a bool'
+ test_cases = (
+ ({'k1': 'v1'}, 'is not a valid bool'),
+ (3.14159, default_test_msg),
+ (-1, default_test_msg),
+ (-90810398401982340981023948192349081, default_test_msg),
+ (90810398401982340981023948192349081, default_test_msg),
+ )
+ for case in test_cases:
+ with pytest.raises(TypeError) as e:
+ check_type_bool(case)
+ assert 'cannot be converted to a bool' in to_native(e.value)
diff --git a/test/units/module_utils/common/validation/test_check_type_bytes.py b/test/units/module_utils/common/validation/test_check_type_bytes.py
new file mode 100644
index 0000000..6ff62dc
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_type_bytes.py
@@ -0,0 +1,50 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_type_bytes
+
+
+def test_check_type_bytes():
+ test_cases = (
+ ('1', 1),
+ (99, 99),
+ (1.5, 2),
+ ('1.5', 2),
+ ('2b', 2),
+ ('2B', 2),
+ ('2k', 2048),
+ ('2K', 2048),
+ ('2KB', 2048),
+ ('1m', 1048576),
+ ('1M', 1048576),
+ ('1MB', 1048576),
+ ('1g', 1073741824),
+ ('1G', 1073741824),
+ ('1GB', 1073741824),
+ (1073741824, 1073741824),
+ )
+ for case in test_cases:
+ assert case[1] == check_type_bytes(case[0])
+
+
+def test_check_type_bytes_fail():
+ test_cases = (
+ 'foo',
+ '2kb',
+ '2Kb',
+ '1mb',
+ '1Mb',
+ '1gb',
+ '1Gb',
+ )
+ for case in test_cases:
+ with pytest.raises(TypeError) as e:
+ check_type_bytes(case)
+ assert 'cannot be converted to a Byte value' in to_native(e.value)
diff --git a/test/units/module_utils/common/validation/test_check_type_dict.py b/test/units/module_utils/common/validation/test_check_type_dict.py
new file mode 100644
index 0000000..75638c5
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_type_dict.py
@@ -0,0 +1,34 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils.common.validation import check_type_dict
+
+
+def test_check_type_dict():
+ test_cases = (
+ ({'k1': 'v1'}, {'k1': 'v1'}),
+ ('k1=v1,k2=v2', {'k1': 'v1', 'k2': 'v2'}),
+ ('k1=v1, k2=v2', {'k1': 'v1', 'k2': 'v2'}),
+ ('k1=v1, k2=v2, k3=v3', {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}),
+ ('{"key": "value", "list": ["one", "two"]}', {'key': 'value', 'list': ['one', 'two']})
+ )
+ for case in test_cases:
+ assert case[1] == check_type_dict(case[0])
+
+
+def test_check_type_dict_fail():
+ test_cases = (
+ 1,
+ 3.14159,
+ [1, 2],
+ 'a',
+ )
+ for case in test_cases:
+ with pytest.raises(TypeError):
+ check_type_dict(case)
diff --git a/test/units/module_utils/common/validation/test_check_type_float.py b/test/units/module_utils/common/validation/test_check_type_float.py
new file mode 100644
index 0000000..57837fa
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_type_float.py
@@ -0,0 +1,38 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_type_float
+
+
+def test_check_type_float():
+ test_cases = (
+ ('1.5', 1.5),
+ ('''1.5''', 1.5),
+ (u'1.5', 1.5),
+ (1002, 1002.0),
+ (1.0, 1.0),
+ (3.141592653589793, 3.141592653589793),
+ ('3.141592653589793', 3.141592653589793),
+ (b'3.141592653589793', 3.141592653589793),
+ )
+ for case in test_cases:
+ assert case[1] == check_type_float(case[0])
+
+
+def test_check_type_float_fail():
+ test_cases = (
+ {'k1': 'v1'},
+ ['a', 'b'],
+ 'b',
+ )
+ for case in test_cases:
+ with pytest.raises(TypeError) as e:
+ check_type_float(case)
+ assert 'cannot be converted to a float' in to_native(e.value)
diff --git a/test/units/module_utils/common/validation/test_check_type_int.py b/test/units/module_utils/common/validation/test_check_type_int.py
new file mode 100644
index 0000000..22cedf6
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_type_int.py
@@ -0,0 +1,34 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_type_int
+
+
+def test_check_type_int():
+ test_cases = (
+ ('1', 1),
+ (u'1', 1),
+ (1002, 1002),
+ )
+ for case in test_cases:
+ assert case[1] == check_type_int(case[0])
+
+
+def test_check_type_int_fail():
+ test_cases = (
+ {'k1': 'v1'},
+ (b'1', 1),
+ (3.14159, 3),
+ 'b',
+ )
+ for case in test_cases:
+ with pytest.raises(TypeError) as e:
+ check_type_int(case)
+ assert 'cannot be converted to an int' in to_native(e.value)
diff --git a/test/units/module_utils/common/validation/test_check_type_jsonarg.py b/test/units/module_utils/common/validation/test_check_type_jsonarg.py
new file mode 100644
index 0000000..e78e54b
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_type_jsonarg.py
@@ -0,0 +1,36 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_type_jsonarg
+
+
+def test_check_type_jsonarg():
+ test_cases = (
+ ('a', 'a'),
+ ('a ', 'a'),
+ (b'99', b'99'),
+ (b'99 ', b'99'),
+ ({'k1': 'v1'}, '{"k1": "v1"}'),
+ ([1, 'a'], '[1, "a"]'),
+ ((1, 2, 'three'), '[1, 2, "three"]'),
+ )
+ for case in test_cases:
+ assert case[1] == check_type_jsonarg(case[0])
+
+
+def test_check_type_jsonarg_fail():
+ test_cases = (
+ 1.5,
+ 910313498012384012341982374109384098,
+ )
+ for case in test_cases:
+ with pytest.raises(TypeError) as e:
+ check_type_jsonarg(case)
+ assert 'cannot be converted to a json string' in to_native(e.value)
diff --git a/test/units/module_utils/common/validation/test_check_type_list.py b/test/units/module_utils/common/validation/test_check_type_list.py
new file mode 100644
index 0000000..3f7a9ee
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_type_list.py
@@ -0,0 +1,32 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils.common.validation import check_type_list
+
+
+def test_check_type_list():
+ test_cases = (
+ ([1, 2], [1, 2]),
+ (1, ['1']),
+ (['a', 'b'], ['a', 'b']),
+ ('a', ['a']),
+ (3.14159, ['3.14159']),
+ ('a,b,1,2', ['a', 'b', '1', '2'])
+ )
+ for case in test_cases:
+ assert case[1] == check_type_list(case[0])
+
+
+def test_check_type_list_failure():
+ test_cases = (
+ {'k1': 'v1'},
+ )
+ for case in test_cases:
+ with pytest.raises(TypeError):
+ check_type_list(case)
diff --git a/test/units/module_utils/common/validation/test_check_type_path.py b/test/units/module_utils/common/validation/test_check_type_path.py
new file mode 100644
index 0000000..d6ff433
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_type_path.py
@@ -0,0 +1,28 @@
+# -*- 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
+
+import re
+
+import os
+from ansible.module_utils.common.validation import check_type_path
+
+
+def mock_expand(value):
+ return re.sub(r'~|\$HOME', '/home/testuser', value)
+
+
+def test_check_type_path(monkeypatch):
+ monkeypatch.setattr(os.path, 'expandvars', mock_expand)
+ monkeypatch.setattr(os.path, 'expanduser', mock_expand)
+ test_cases = (
+ ('~/foo', '/home/testuser/foo'),
+ ('$HOME/foo', '/home/testuser/foo'),
+ ('/home/jane', '/home/jane'),
+ (u'/home/jané', u'/home/jané'),
+ )
+ for case in test_cases:
+ assert case[1] == check_type_path(case[0])
diff --git a/test/units/module_utils/common/validation/test_check_type_raw.py b/test/units/module_utils/common/validation/test_check_type_raw.py
new file mode 100644
index 0000000..988e554
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_type_raw.py
@@ -0,0 +1,23 @@
+# -*- 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.common.validation import check_type_raw
+
+
+def test_check_type_raw():
+ test_cases = (
+ (1, 1),
+ ('1', '1'),
+ ('a', 'a'),
+ ({'k1': 'v1'}, {'k1': 'v1'}),
+ ([1, 2], [1, 2]),
+ (b'42', b'42'),
+ (u'42', u'42'),
+ )
+ for case in test_cases:
+ assert case[1] == check_type_raw(case[0])
diff --git a/test/units/module_utils/common/validation/test_check_type_str.py b/test/units/module_utils/common/validation/test_check_type_str.py
new file mode 100644
index 0000000..f10dad2
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_check_type_str.py
@@ -0,0 +1,33 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.module_utils.common.validation import check_type_str
+
+
+TEST_CASES = (
+ ('string', 'string'),
+ (100, '100'),
+ (1.5, '1.5'),
+ ({'k1': 'v1'}, "{'k1': 'v1'}"),
+ ([1, 2, 'three'], "[1, 2, 'three']"),
+ ((1, 2,), '(1, 2)'),
+)
+
+
+@pytest.mark.parametrize('value, expected', TEST_CASES)
+def test_check_type_str(value, expected):
+ assert expected == check_type_str(value)
+
+
+@pytest.mark.parametrize('value, expected', TEST_CASES[1:])
+def test_check_type_str_no_conversion(value, expected):
+ with pytest.raises(TypeError) as e:
+ check_type_str(value, allow_conversion=False)
+ assert 'is not a string and conversion is not allowed' in to_native(e.value)
diff --git a/test/units/module_utils/common/validation/test_count_terms.py b/test/units/module_utils/common/validation/test_count_terms.py
new file mode 100644
index 0000000..f41dc40
--- /dev/null
+++ b/test/units/module_utils/common/validation/test_count_terms.py
@@ -0,0 +1,40 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils.common.validation import count_terms
+
+
+@pytest.fixture
+def params():
+ return {
+ 'name': 'bob',
+ 'dest': '/etc/hosts',
+ 'state': 'present',
+ 'value': 5,
+ }
+
+
+def test_count_terms(params):
+ check = set(('name', 'dest'))
+ assert count_terms(check, params) == 2
+
+
+def test_count_terms_str_input(params):
+ check = 'name'
+ assert count_terms(check, params) == 1
+
+
+def test_count_terms_tuple_input(params):
+ check = ('name', 'dest')
+ assert count_terms(check, params) == 2
+
+
+def test_count_terms_list_input(params):
+ check = ['name', 'dest']
+ assert count_terms(check, params) == 2
diff --git a/test/units/module_utils/common/warnings/test_deprecate.py b/test/units/module_utils/common/warnings/test_deprecate.py
new file mode 100644
index 0000000..08c1b35
--- /dev/null
+++ b/test/units/module_utils/common/warnings/test_deprecate.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# (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
+
+import pytest
+
+from ansible.module_utils.common import warnings
+
+from ansible.module_utils.common.warnings import deprecate, get_deprecation_messages
+from ansible.module_utils.six import PY3
+
+
+@pytest.fixture
+def deprecation_messages():
+ return [
+ {'msg': 'First deprecation', 'version': None, 'collection_name': None},
+ {'msg': 'Second deprecation', 'version': None, 'collection_name': 'ansible.builtin'},
+ {'msg': 'Third deprecation', 'version': '2.14', 'collection_name': None},
+ {'msg': 'Fourth deprecation', 'version': '2.9', 'collection_name': None},
+ {'msg': 'Fifth deprecation', 'version': '2.9', 'collection_name': 'ansible.builtin'},
+ {'msg': 'Sixth deprecation', 'date': '2199-12-31', 'collection_name': None},
+ {'msg': 'Seventh deprecation', 'date': '2199-12-31', 'collection_name': 'ansible.builtin'},
+ ]
+
+
+@pytest.fixture
+def reset(monkeypatch):
+ monkeypatch.setattr(warnings, '_global_deprecations', [])
+
+
+def test_deprecate_message_only(reset):
+ deprecate('Deprecation message')
+ assert warnings._global_deprecations == [
+ {'msg': 'Deprecation message', 'version': None, 'collection_name': None}]
+
+
+def test_deprecate_with_collection(reset):
+ deprecate(msg='Deprecation message', collection_name='ansible.builtin')
+ assert warnings._global_deprecations == [
+ {'msg': 'Deprecation message', 'version': None, 'collection_name': 'ansible.builtin'}]
+
+
+def test_deprecate_with_version(reset):
+ deprecate(msg='Deprecation message', version='2.14')
+ assert warnings._global_deprecations == [
+ {'msg': 'Deprecation message', 'version': '2.14', 'collection_name': None}]
+
+
+def test_deprecate_with_version_and_collection(reset):
+ deprecate(msg='Deprecation message', version='2.14', collection_name='ansible.builtin')
+ assert warnings._global_deprecations == [
+ {'msg': 'Deprecation message', 'version': '2.14', 'collection_name': 'ansible.builtin'}]
+
+
+def test_deprecate_with_date(reset):
+ deprecate(msg='Deprecation message', date='2199-12-31')
+ assert warnings._global_deprecations == [
+ {'msg': 'Deprecation message', 'date': '2199-12-31', 'collection_name': None}]
+
+
+def test_deprecate_with_date_and_collection(reset):
+ deprecate(msg='Deprecation message', date='2199-12-31', collection_name='ansible.builtin')
+ assert warnings._global_deprecations == [
+ {'msg': 'Deprecation message', 'date': '2199-12-31', 'collection_name': 'ansible.builtin'}]
+
+
+def test_multiple_deprecations(deprecation_messages, reset):
+ for d in deprecation_messages:
+ deprecate(**d)
+
+ assert deprecation_messages == warnings._global_deprecations
+
+
+def test_get_deprecation_messages(deprecation_messages, reset):
+ for d in deprecation_messages:
+ deprecate(**d)
+
+ accessor_deprecations = get_deprecation_messages()
+ assert isinstance(accessor_deprecations, tuple)
+ assert len(accessor_deprecations) == 7
+
+
+@pytest.mark.parametrize(
+ 'test_case',
+ (
+ 1,
+ True,
+ [1],
+ {'k1': 'v1'},
+ (1, 2),
+ 6.62607004,
+ b'bytestr' if PY3 else None,
+ None,
+ )
+)
+def test_deprecate_failure(test_case):
+ with pytest.raises(TypeError, match='deprecate requires a string not a %s' % type(test_case)):
+ deprecate(test_case)
diff --git a/test/units/module_utils/common/warnings/test_warn.py b/test/units/module_utils/common/warnings/test_warn.py
new file mode 100644
index 0000000..41e1a7b
--- /dev/null
+++ b/test/units/module_utils/common/warnings/test_warn.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+# (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
+
+import pytest
+
+from ansible.module_utils.common import warnings
+
+from ansible.module_utils.common.warnings import warn, get_warning_messages
+from ansible.module_utils.six import PY3
+
+
+@pytest.fixture
+def warning_messages():
+ return [
+ 'First warning',
+ 'Second warning',
+ 'Third warning',
+ ]
+
+
+def test_warn():
+ warn('Warning message')
+ assert warnings._global_warnings == ['Warning message']
+
+
+def test_multiple_warningss(warning_messages):
+ for w in warning_messages:
+ warn(w)
+
+ assert warning_messages == warnings._global_warnings
+
+
+def test_get_warning_messages(warning_messages):
+ for w in warning_messages:
+ warn(w)
+
+ accessor_warnings = get_warning_messages()
+ assert isinstance(accessor_warnings, tuple)
+ assert len(accessor_warnings) == 3
+
+
+@pytest.mark.parametrize(
+ 'test_case',
+ (
+ 1,
+ True,
+ [1],
+ {'k1': 'v1'},
+ (1, 2),
+ 6.62607004,
+ b'bytestr' if PY3 else None,
+ None,
+ )
+)
+def test_warn_failure(test_case):
+ with pytest.raises(TypeError, match='warn requires a string not a %s' % type(test_case)):
+ warn(test_case)
diff --git a/test/units/module_utils/conftest.py b/test/units/module_utils/conftest.py
new file mode 100644
index 0000000..8bc13c4
--- /dev/null
+++ b/test/units/module_utils/conftest.py
@@ -0,0 +1,72 @@
+# 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
+
+import json
+import sys
+from io import BytesIO
+
+import pytest
+
+import ansible.module_utils.basic
+from ansible.module_utils.six import PY3, string_types
+from ansible.module_utils._text import to_bytes
+from ansible.module_utils.common._collections_compat import MutableMapping
+
+
+@pytest.fixture
+def stdin(mocker, request):
+ old_args = ansible.module_utils.basic._ANSIBLE_ARGS
+ ansible.module_utils.basic._ANSIBLE_ARGS = None
+ old_argv = sys.argv
+ sys.argv = ['ansible_unittest']
+
+ if isinstance(request.param, string_types):
+ args = request.param
+ elif isinstance(request.param, MutableMapping):
+ if 'ANSIBLE_MODULE_ARGS' not in request.param:
+ request.param = {'ANSIBLE_MODULE_ARGS': request.param}
+ if '_ansible_remote_tmp' not in request.param['ANSIBLE_MODULE_ARGS']:
+ request.param['ANSIBLE_MODULE_ARGS']['_ansible_remote_tmp'] = '/tmp'
+ if '_ansible_keep_remote_files' not in request.param['ANSIBLE_MODULE_ARGS']:
+ request.param['ANSIBLE_MODULE_ARGS']['_ansible_keep_remote_files'] = False
+ args = json.dumps(request.param)
+ else:
+ raise Exception('Malformed data to the stdin pytest fixture')
+
+ fake_stdin = BytesIO(to_bytes(args, errors='surrogate_or_strict'))
+ if PY3:
+ mocker.patch('ansible.module_utils.basic.sys.stdin', mocker.MagicMock())
+ mocker.patch('ansible.module_utils.basic.sys.stdin.buffer', fake_stdin)
+ else:
+ mocker.patch('ansible.module_utils.basic.sys.stdin', fake_stdin)
+
+ yield fake_stdin
+
+ ansible.module_utils.basic._ANSIBLE_ARGS = old_args
+ sys.argv = old_argv
+
+
+@pytest.fixture
+def am(stdin, request):
+ old_args = ansible.module_utils.basic._ANSIBLE_ARGS
+ ansible.module_utils.basic._ANSIBLE_ARGS = None
+ old_argv = sys.argv
+ sys.argv = ['ansible_unittest']
+
+ argspec = {}
+ if hasattr(request, 'param'):
+ if isinstance(request.param, dict):
+ argspec = request.param
+
+ am = ansible.module_utils.basic.AnsibleModule(
+ argument_spec=argspec,
+ )
+ am._name = 'ansible_unittest'
+
+ yield am
+
+ ansible.module_utils.basic._ANSIBLE_ARGS = old_args
+ sys.argv = old_argv
diff --git a/test/units/module_utils/facts/__init__.py b/test/units/module_utils/facts/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/facts/__init__.py
diff --git a/test/units/module_utils/facts/base.py b/test/units/module_utils/facts/base.py
new file mode 100644
index 0000000..33d3087
--- /dev/null
+++ b/test/units/module_utils/facts/base.py
@@ -0,0 +1,65 @@
+# base unit test classes for ansible/module_utils/facts/ tests
+# -*- coding: utf-8 -*-
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from units.compat.mock import Mock, patch
+
+
+class BaseFactsTest(unittest.TestCase):
+ # just a base class, not an actual test
+ __test__ = False
+
+ gather_subset = ['all']
+ valid_subsets = None
+ fact_namespace = None
+ collector_class = None
+
+ # a dict ansible_facts. Some fact collectors depend on facts gathered by
+ # other collectors (like 'ansible_architecture' or 'ansible_system') which
+ # can be passed via the collected_facts arg to collect()
+ collected_facts = None
+
+ def _mock_module(self):
+ mock_module = Mock()
+ mock_module.params = {'gather_subset': self.gather_subset,
+ 'gather_timeout': 5,
+ 'filter': '*'}
+ mock_module.get_bin_path = Mock(return_value=None)
+ return mock_module
+
+ @patch('platform.system', return_value='Linux')
+ @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value='systemd')
+ def test_collect(self, mock_gfc, mock_ps):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts)
+ self.assertIsInstance(facts_dict, dict)
+ return facts_dict
+
+ @patch('platform.system', return_value='Linux')
+ @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value='systemd')
+ def test_collect_with_namespace(self, mock_gfc, mock_ps):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect_with_namespace(module=module,
+ collected_facts=self.collected_facts)
+ self.assertIsInstance(facts_dict, dict)
+ return facts_dict
diff --git a/test/units/module_utils/facts/fixtures/cpuinfo/aarch64-4cpu-cpuinfo b/test/units/module_utils/facts/fixtures/cpuinfo/aarch64-4cpu-cpuinfo
new file mode 100644
index 0000000..c3caa01
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/cpuinfo/aarch64-4cpu-cpuinfo
@@ -0,0 +1,40 @@
+processor : 0
+Processor : AArch64 Processor rev 4 (aarch64)
+Hardware : sun50iw1p1
+BogoMIPS : 48.00
+Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
+processor : 1
+Processor : AArch64 Processor rev 4 (aarch64)
+Hardware : sun50iw1p1
+BogoMIPS : 48.00
+Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
+processor : 2
+Processor : AArch64 Processor rev 4 (aarch64)
+Hardware : sun50iw1p1
+BogoMIPS : 48.00
+Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
+processor : 3
+Processor : AArch64 Processor rev 4 (aarch64)
+Hardware : sun50iw1p1
+BogoMIPS : 48.00
+Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
diff --git a/test/units/module_utils/facts/fixtures/cpuinfo/arm64-4cpu-cpuinfo b/test/units/module_utils/facts/fixtures/cpuinfo/arm64-4cpu-cpuinfo
new file mode 100644
index 0000000..38fd06e
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/cpuinfo/arm64-4cpu-cpuinfo
@@ -0,0 +1,32 @@
+processor : 0
+BogoMIPS : 48.00
+Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
+processor : 1
+BogoMIPS : 48.00
+Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
+processor : 2
+BogoMIPS : 48.00
+Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
+processor : 3
+BogoMIPS : 48.00
+Features : fp asimd evtstrm aes pmull sha1 sha2 crc32 cpuid
+CPU implementer : 0x41
+CPU architecture: 8
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
diff --git a/test/units/module_utils/facts/fixtures/cpuinfo/armv6-rev7-1cpu-cpuinfo b/test/units/module_utils/facts/fixtures/cpuinfo/armv6-rev7-1cpu-cpuinfo
new file mode 100644
index 0000000..84ee16c
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/cpuinfo/armv6-rev7-1cpu-cpuinfo
@@ -0,0 +1,12 @@
+processor : 0
+model name : ARMv6-compatible processor rev 7 (v6l)
+BogoMIPS : 697.95
+Features : half thumb fastmult vfp edsp java tls
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x0
+CPU part : 0xb76
+CPU revision : 7
+Hardware : BCM2835
+Revision : 0010
+Serial : 000000004a0abca9
diff --git a/test/units/module_utils/facts/fixtures/cpuinfo/armv7-rev3-8cpu-cpuinfo b/test/units/module_utils/facts/fixtures/cpuinfo/armv7-rev3-8cpu-cpuinfo
new file mode 100644
index 0000000..d4b4d3b
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/cpuinfo/armv7-rev3-8cpu-cpuinfo
@@ -0,0 +1,75 @@
+processor : 0
+model name : ARMv7 Processor rev 3 (v7l)
+BogoMIPS : 12.00
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x0
+CPU part : 0xc07
+CPU revision : 3
+processor : 1
+model name : ARMv7 Processor rev 3 (v7l)
+BogoMIPS : 12.00
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x0
+CPU part : 0xc07
+CPU revision : 3
+processor : 2
+model name : ARMv7 Processor rev 3 (v7l)
+BogoMIPS : 12.00
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x0
+CPU part : 0xc07
+CPU revision : 3
+processor : 3
+model name : ARMv7 Processor rev 3 (v7l)
+BogoMIPS : 12.00
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x0
+CPU part : 0xc07
+CPU revision : 3
+processor : 4
+model name : ARMv7 Processor rev 3 (v7l)
+BogoMIPS : 120.00
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x2
+CPU part : 0xc0f
+CPU revision : 3
+processor : 5
+model name : ARMv7 Processor rev 3 (v7l)
+BogoMIPS : 120.00
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x2
+CPU part : 0xc0f
+CPU revision : 3
+processor : 6
+model name : ARMv7 Processor rev 3 (v7l)
+BogoMIPS : 120.00
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x2
+CPU part : 0xc0f
+CPU revision : 3
+processor : 7
+model name : ARMv7 Processor rev 3 (v7l)
+BogoMIPS : 120.00
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x2
+CPU part : 0xc0f
+CPU revision : 3
+Hardware : ODROID-XU4
+Revision : 0100
+Serial : 0000000000000000
diff --git a/test/units/module_utils/facts/fixtures/cpuinfo/armv7-rev4-4cpu-cpuinfo b/test/units/module_utils/facts/fixtures/cpuinfo/armv7-rev4-4cpu-cpuinfo
new file mode 100644
index 0000000..f36075c
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/cpuinfo/armv7-rev4-4cpu-cpuinfo
@@ -0,0 +1,39 @@
+processor : 0
+model name : ARMv7 Processor rev 4 (v7l)
+BogoMIPS : 38.40
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
+processor : 1
+model name : ARMv7 Processor rev 4 (v7l)
+BogoMIPS : 38.40
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
+processor : 2
+model name : ARMv7 Processor rev 4 (v7l)
+BogoMIPS : 38.40
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
+processor : 3
+model name : ARMv7 Processor rev 4 (v7l)
+BogoMIPS : 38.40
+Features : half thumb fastmult vfp edsp neon vfpv3 tls vfpv4 idiva idivt vfpd32 lpae evtstrm crc32
+CPU implementer : 0x41
+CPU architecture: 7
+CPU variant : 0x0
+CPU part : 0xd03
+CPU revision : 4
+Hardware : BCM2835
+Revision : a02082
+Serial : 000000007881bb80
diff --git a/test/units/module_utils/facts/fixtures/cpuinfo/ppc64-power7-rhel7-8cpu-cpuinfo b/test/units/module_utils/facts/fixtures/cpuinfo/ppc64-power7-rhel7-8cpu-cpuinfo
new file mode 100644
index 0000000..1309c58
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/cpuinfo/ppc64-power7-rhel7-8cpu-cpuinfo
@@ -0,0 +1,44 @@
+processor : 0
+cpu : POWER7 (architected), altivec supported
+clock : 3550.000000MHz
+revision : 2.1 (pvr 003f 0201)
+
+processor : 1
+cpu : POWER7 (architected), altivec supported
+clock : 3550.000000MHz
+revision : 2.1 (pvr 003f 0201)
+
+processor : 2
+cpu : POWER7 (architected), altivec supported
+clock : 3550.000000MHz
+revision : 2.1 (pvr 003f 0201)
+
+processor : 3
+cpu : POWER7 (architected), altivec supported
+clock : 3550.000000MHz
+revision : 2.1 (pvr 003f 0201)
+
+processor : 4
+cpu : POWER7 (architected), altivec supported
+clock : 3550.000000MHz
+revision : 2.1 (pvr 003f 0201)
+
+processor : 5
+cpu : POWER7 (architected), altivec supported
+clock : 3550.000000MHz
+revision : 2.1 (pvr 003f 0201)
+
+processor : 6
+cpu : POWER7 (architected), altivec supported
+clock : 3550.000000MHz
+revision : 2.1 (pvr 003f 0201)
+
+processor : 7
+cpu : POWER7 (architected), altivec supported
+clock : 3550.000000MHz
+revision : 2.1 (pvr 003f 0201)
+
+timebase : 512000000
+platform : pSeries
+model : IBM,8231-E2B
+machine : CHRP IBM,8231-E2B \ No newline at end of file
diff --git a/test/units/module_utils/facts/fixtures/cpuinfo/ppc64le-power8-24cpu-cpuinfo b/test/units/module_utils/facts/fixtures/cpuinfo/ppc64le-power8-24cpu-cpuinfo
new file mode 100644
index 0000000..4cbd5ac
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/cpuinfo/ppc64le-power8-24cpu-cpuinfo
@@ -0,0 +1,125 @@
+processor : 0
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 1
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 2
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 3
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 4
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 5
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 6
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 7
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 8
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 9
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 10
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 11
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 12
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 13
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 14
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 15
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 16
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 17
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 18
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 19
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 20
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 21
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 22
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+processor : 23
+cpu : POWER8 (architected), altivec supported
+clock : 3425.000000MHz
+revision : 2.1 (pvr 004b 0201)
+
+timebase : 512000000
+platform : pSeries
+model : IBM,8247-21L
+machine : CHRP IBM,8247-21L
+
diff --git a/test/units/module_utils/facts/fixtures/cpuinfo/sparc-t5-debian-ldom-24vcpu b/test/units/module_utils/facts/fixtures/cpuinfo/sparc-t5-debian-ldom-24vcpu
new file mode 100644
index 0000000..8c29faa
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/cpuinfo/sparc-t5-debian-ldom-24vcpu
@@ -0,0 +1,61 @@
+cpu : UltraSparc T5 (Niagara5)
+fpu : UltraSparc T5 integrated FPU
+pmu : niagara5
+prom : OBP 4.38.12 2018/03/28 14:54
+type : sun4v
+ncpus probed : 24
+ncpus active : 24
+D$ parity tl1 : 0
+I$ parity tl1 : 0
+cpucaps : flush,stbar,swap,muldiv,v9,blkinit,n2,mul32,div32,v8plus,popc,vis,vis2,ASIBlkInit,fmaf,vis3,hpc,ima,pause,cbcond,aes,des,kasumi,camellia,md5,sha1,sha256,sha512,mpmul,montmul,montsqr,crc32c
+Cpu0ClkTck : 00000000d6924470
+Cpu1ClkTck : 00000000d6924470
+Cpu2ClkTck : 00000000d6924470
+Cpu3ClkTck : 00000000d6924470
+Cpu4ClkTck : 00000000d6924470
+Cpu5ClkTck : 00000000d6924470
+Cpu6ClkTck : 00000000d6924470
+Cpu7ClkTck : 00000000d6924470
+Cpu8ClkTck : 00000000d6924470
+Cpu9ClkTck : 00000000d6924470
+Cpu10ClkTck : 00000000d6924470
+Cpu11ClkTck : 00000000d6924470
+Cpu12ClkTck : 00000000d6924470
+Cpu13ClkTck : 00000000d6924470
+Cpu14ClkTck : 00000000d6924470
+Cpu15ClkTck : 00000000d6924470
+Cpu16ClkTck : 00000000d6924470
+Cpu17ClkTck : 00000000d6924470
+Cpu18ClkTck : 00000000d6924470
+Cpu19ClkTck : 00000000d6924470
+Cpu20ClkTck : 00000000d6924470
+Cpu21ClkTck : 00000000d6924470
+Cpu22ClkTck : 00000000d6924470
+Cpu23ClkTck : 00000000d6924470
+MMU Type : Hypervisor (sun4v)
+MMU PGSZs : 8K,64K,4MB,256MB
+State:
+CPU0: online
+CPU1: online
+CPU2: online
+CPU3: online
+CPU4: online
+CPU5: online
+CPU6: online
+CPU7: online
+CPU8: online
+CPU9: online
+CPU10: online
+CPU11: online
+CPU12: online
+CPU13: online
+CPU14: online
+CPU15: online
+CPU16: online
+CPU17: online
+CPU18: online
+CPU19: online
+CPU20: online
+CPU21: online
+CPU22: online
+CPU23: online
diff --git a/test/units/module_utils/facts/fixtures/cpuinfo/x86_64-2cpu-cpuinfo b/test/units/module_utils/facts/fixtures/cpuinfo/x86_64-2cpu-cpuinfo
new file mode 100644
index 0000000..1d233f8
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/cpuinfo/x86_64-2cpu-cpuinfo
@@ -0,0 +1,56 @@
+processor : 0
+vendor_id : GenuineIntel
+cpu family : 6
+model : 62
+model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz
+stepping : 4
+microcode : 0x1
+cpu MHz : 2799.998
+cache size : 16384 KB
+physical id : 0
+siblings : 1
+core id : 0
+cpu cores : 1
+apicid : 0
+initial apicid : 0
+fpu : yes
+fpu_exception : yes
+cpuid level : 13
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp l'
+m constant_tsc arch_perfmon rep_good nopl xtopology cpuid tsc_known_freq pni pclmulqdq ssse3 cx16 pcid sse4_1 sse4_2 x2apic popcnt tsc_deadlin'
+e_timer aes xsave avx f16c rdrand hypervisor lahf_lm pti fsgsbase tsc_adjust smep erms xsaveopt arat
+bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips : 5602.32
+clflush size : 64
+cache_alignment : 64
+address sizes : 40 bits physical, 48 bits virtual
+power management:
+processor : 1
+vendor_id : GenuineIntel
+cpu family : 6
+model : 62
+model name : Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz
+stepping : 4
+microcode : 0x1
+cpu MHz : 2799.998
+cache size : 16384 KB
+physical id : 1
+siblings : 1
+core id : 0
+cpu cores : 1
+apicid : 1
+initial apicid : 1
+fpu : yes
+fpu_exception : yes
+cpuid level : 13
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ss syscall nx pdpe1gb rdtscp l'
+m constant_tsc arch_perfmon rep_good nopl xtopology cpuid tsc_known_freq pni pclmulqdq ssse3 cx16 pcid sse4_1 sse4_2 x2apic popcnt tsc_deadlin'
+e_timer aes xsave avx f16c rdrand hypervisor lahf_lm pti fsgsbase tsc_adjust smep erms xsaveopt arat
+bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf
+bogomips : 5602.32
+clflush size : 64
+cache_alignment : 64
+address sizes : 40 bits physical, 48 bits virtual
+power management:
diff --git a/test/units/module_utils/facts/fixtures/cpuinfo/x86_64-4cpu-cpuinfo b/test/units/module_utils/facts/fixtures/cpuinfo/x86_64-4cpu-cpuinfo
new file mode 100644
index 0000000..fcc396d
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/cpuinfo/x86_64-4cpu-cpuinfo
@@ -0,0 +1,104 @@
+processor : 0
+vendor_id : AuthenticAMD
+cpu family : 15
+model : 65
+model name : Dual-Core AMD Opteron(tm) Processor 2216
+stepping : 2
+cpu MHz : 1000.000
+cache size : 1024 KB
+physical id : 0
+siblings : 2
+core id : 0
+cpu cores : 2
+apicid : 0
+initial apicid : 0
+fpu : yes
+fpu_exception : yes
+cpuid level : 1
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt '
+rdtscp lm 3dnowext 3dnow art rep_good nopl extd_apicid pni cx16 lahf_lm cmp_legacy svm extapic cr8_legacy retpoline_amd vmmcall
+bogomips : 1994.60
+TLB size : 1024 4K pages
+clflush size : 64
+cache_alignment : 64
+address sizes : 40 bits physical, 48 bits virtual
+power management: ts fid vid ttp tm stc
+processor : 1
+vendor_id : AuthenticAMD
+cpu family : 15
+model : 65
+model name : Dual-Core AMD Opteron(tm) Processor 2216
+stepping : 2
+cpu MHz : 1000.000
+cache size : 1024 KB
+physical id : 0
+siblings : 2
+core id : 1
+cpu cores : 2
+apicid : 1
+initial apicid : 1
+fpu : yes
+fpu_exception : yes
+cpuid level : 1
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt '
+rdtscp lm 3dnowext 3dnow art rep_good nopl extd_apicid pni cx16 lahf_lm cmp_legacy svm extapic cr8_legacy retpoline_amd vmmcall
+bogomips : 1994.60
+TLB size : 1024 4K pages
+clflush size : 64
+cache_alignment : 64
+address sizes : 40 bits physical, 48 bits virtual
+power management: ts fid vid ttp tm stc
+processor : 2
+vendor_id : AuthenticAMD
+cpu family : 15
+model : 65
+model name : Dual-Core AMD Opteron(tm) Processor 2216
+stepping : 2
+cpu MHz : 1000.000
+cache size : 1024 KB
+physical id : 1
+siblings : 2
+core id : 0
+cpu cores : 2
+apicid : 2
+initial apicid : 2
+fpu : yes
+fpu_exception : yes
+cpuid level : 1
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt '
+rdtscp lm 3dnowext 3dnow art rep_good nopl extd_apicid pni cx16 lahf_lm cmp_legacy svm extapic cr8_legacy retpoline_amd vmmcall
+bogomips : 1994.60
+TLB size : 1024 4K pages
+clflush size : 64
+cache_alignment : 64
+address sizes : 40 bits physical, 48 bits virtual
+power management: ts fid vid ttp tm stc
+processor : 3
+vendor_id : AuthenticAMD
+cpu family : 15
+model : 65
+model name : Dual-Core AMD Opteron(tm) Processor 2216
+stepping : 2
+cpu MHz : 1000.000
+cache size : 1024 KB
+physical id : 1
+siblings : 2
+core id : 1
+cpu cores : 2
+apicid : 3
+initial apicid : 3
+fpu : yes
+fpu_exception : yes
+cpuid level : 1
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt '
+rdtscp lm 3dnowext 3dnow art rep_good nopl extd_apicid pni cx16 lahf_lm cmp_legacy svm extapic cr8_legacy retpoline_amd vmmcall
+bogomips : 1994.60
+TLB size : 1024 4K pages
+clflush size : 64
+cache_alignment : 64
+address sizes : 40 bits physical, 48 bits virtual
+power management: ts fid vid ttp tm stc
diff --git a/test/units/module_utils/facts/fixtures/cpuinfo/x86_64-8cpu-cpuinfo b/test/units/module_utils/facts/fixtures/cpuinfo/x86_64-8cpu-cpuinfo
new file mode 100644
index 0000000..63abea2
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/cpuinfo/x86_64-8cpu-cpuinfo
@@ -0,0 +1,216 @@
+processor : 0
+vendor_id : GenuineIntel
+cpu family : 6
+model : 60
+model name : Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz
+stepping : 3
+microcode : 0x20
+cpu MHz : 2703.625
+cache size : 6144 KB
+physical id : 0
+siblings : 8
+core id : 0
+cpu cores : 4
+apicid : 0
+initial apicid : 0
+fpu : yes
+fpu_exception : yes
+cpuid level : 13
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm epb tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts
+bugs :
+bogomips : 5388.06
+clflush size : 64
+cache_alignment : 64
+address sizes : 39 bits physical, 48 bits virtual
+power management:
+
+processor : 1
+vendor_id : GenuineIntel
+cpu family : 6
+model : 60
+model name : Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz
+stepping : 3
+microcode : 0x20
+cpu MHz : 3398.565
+cache size : 6144 KB
+physical id : 0
+siblings : 8
+core id : 0
+cpu cores : 4
+apicid : 1
+initial apicid : 1
+fpu : yes
+fpu_exception : yes
+cpuid level : 13
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm epb tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts
+bugs :
+bogomips : 5393.53
+clflush size : 64
+cache_alignment : 64
+address sizes : 39 bits physical, 48 bits virtual
+power management:
+
+processor : 2
+vendor_id : GenuineIntel
+cpu family : 6
+model : 60
+model name : Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz
+stepping : 3
+microcode : 0x20
+cpu MHz : 3390.325
+cache size : 6144 KB
+physical id : 0
+siblings : 8
+core id : 1
+cpu cores : 4
+apicid : 2
+initial apicid : 2
+fpu : yes
+fpu_exception : yes
+cpuid level : 13
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm epb tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts
+bugs :
+bogomips : 5391.63
+clflush size : 64
+cache_alignment : 64
+address sizes : 39 bits physical, 48 bits virtual
+power management:
+
+processor : 3
+vendor_id : GenuineIntel
+cpu family : 6
+model : 60
+model name : Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz
+stepping : 3
+microcode : 0x20
+cpu MHz : 3262.774
+cache size : 6144 KB
+physical id : 0
+siblings : 8
+core id : 1
+cpu cores : 4
+apicid : 3
+initial apicid : 3
+fpu : yes
+fpu_exception : yes
+cpuid level : 13
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm epb tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts
+bugs :
+bogomips : 5392.08
+clflush size : 64
+cache_alignment : 64
+address sizes : 39 bits physical, 48 bits virtual
+power management:
+
+processor : 4
+vendor_id : GenuineIntel
+cpu family : 6
+model : 60
+model name : Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz
+stepping : 3
+microcode : 0x20
+cpu MHz : 2905.169
+cache size : 6144 KB
+physical id : 0
+siblings : 8
+core id : 2
+cpu cores : 4
+apicid : 4
+initial apicid : 4
+fpu : yes
+fpu_exception : yes
+cpuid level : 13
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm epb tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts
+bugs :
+bogomips : 5391.97
+clflush size : 64
+cache_alignment : 64
+address sizes : 39 bits physical, 48 bits virtual
+power management:
+
+processor : 5
+vendor_id : GenuineIntel
+cpu family : 6
+model : 60
+model name : Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz
+stepping : 3
+microcode : 0x20
+cpu MHz : 1834.826
+cache size : 6144 KB
+physical id : 0
+siblings : 8
+core id : 2
+cpu cores : 4
+apicid : 5
+initial apicid : 5
+fpu : yes
+fpu_exception : yes
+cpuid level : 13
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm epb tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts
+bugs :
+bogomips : 5392.11
+clflush size : 64
+cache_alignment : 64
+address sizes : 39 bits physical, 48 bits virtual
+power management:
+
+processor : 6
+vendor_id : GenuineIntel
+cpu family : 6
+model : 60
+model name : Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz
+stepping : 3
+microcode : 0x20
+cpu MHz : 2781.573
+cache size : 6144 KB
+physical id : 0
+siblings : 8
+core id : 3
+cpu cores : 4
+apicid : 6
+initial apicid : 6
+fpu : yes
+fpu_exception : yes
+cpuid level : 13
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm epb tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts
+bugs :
+bogomips : 5391.98
+clflush size : 64
+cache_alignment : 64
+address sizes : 39 bits physical, 48 bits virtual
+power management:
+
+processor : 7
+vendor_id : GenuineIntel
+cpu family : 6
+model : 60
+model name : Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz
+stepping : 3
+microcode : 0x20
+cpu MHz : 3593.353
+cache size : 6144 KB
+physical id : 0
+siblings : 8
+core id : 3
+cpu cores : 4
+apicid : 7
+initial apicid : 7
+fpu : yes
+fpu_exception : yes
+cpuid level : 13
+wp : yes
+flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm epb tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid xsaveopt dtherm ida arat pln pts
+bugs :
+bogomips : 5392.07
+clflush size : 64
+cache_alignment : 64
+address sizes : 39 bits physical, 48 bits virtual
+power management:
+
diff --git a/test/units/module_utils/facts/fixtures/distribution_files/ClearLinux b/test/units/module_utils/facts/fixtures/distribution_files/ClearLinux
new file mode 100644
index 0000000..a5442de
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/distribution_files/ClearLinux
@@ -0,0 +1,10 @@
+NAME="Clear Linux OS"
+VERSION=1
+ID=clear-linux-os
+ID_LIKE=clear-linux-os
+VERSION_ID=28120
+PRETTY_NAME="Clear Linux OS"
+ANSI_COLOR="1;35"
+HOME_URL="https://clearlinux.org"
+SUPPORT_URL="https://clearlinux.org"
+BUG_REPORT_URL="mailto:dev@lists.clearlinux.org"',
diff --git a/test/units/module_utils/facts/fixtures/distribution_files/CoreOS b/test/units/module_utils/facts/fixtures/distribution_files/CoreOS
new file mode 100644
index 0000000..806ce30
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/distribution_files/CoreOS
@@ -0,0 +1,10 @@
+NAME="Container Linux by CoreOS"
+ID=coreos
+VERSION=1911.5.0
+VERSION_ID=1911.5.0
+BUILD_ID=2018-12-15-2317
+PRETTY_NAME="Container Linux by CoreOS 1911.5.0 (Rhyolite)"
+ANSI_COLOR="38;5;75"
+HOME_URL="https://coreos.com/"
+BUG_REPORT_URL="https://issues.coreos.com"
+COREOS_BOARD="amd64-usr"
diff --git a/test/units/module_utils/facts/fixtures/distribution_files/LinuxMint b/test/units/module_utils/facts/fixtures/distribution_files/LinuxMint
new file mode 100644
index 0000000..850f6b7
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/distribution_files/LinuxMint
@@ -0,0 +1,12 @@
+NAME="Linux Mint"
+VERSION="19.1 (Tessa)"
+ID=linuxmint
+ID_LIKE=ubuntu
+PRETTY_NAME="Linux Mint 19.1"
+VERSION_ID="19.1"
+HOME_URL="https://www.linuxmint.com/"
+SUPPORT_URL="https://forums.ubuntu.com/"
+BUG_REPORT_URL="http://linuxmint-troubleshooting-guide.readthedocs.io/en/latest/"
+PRIVACY_POLICY_URL="https://www.linuxmint.com/"
+VERSION_CODENAME=tessa
+UBUNTU_CODENAME=bionic
diff --git a/test/units/module_utils/facts/fixtures/distribution_files/Slackware b/test/units/module_utils/facts/fixtures/distribution_files/Slackware
new file mode 100644
index 0000000..1147d29
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/distribution_files/Slackware
@@ -0,0 +1 @@
+Slackware 14.1
diff --git a/test/units/module_utils/facts/fixtures/distribution_files/SlackwareCurrent b/test/units/module_utils/facts/fixtures/distribution_files/SlackwareCurrent
new file mode 100644
index 0000000..62c046c
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/distribution_files/SlackwareCurrent
@@ -0,0 +1 @@
+Slackware 14.2+
diff --git a/test/units/module_utils/facts/fixtures/findmount_output.txt b/test/units/module_utils/facts/fixtures/findmount_output.txt
new file mode 100644
index 0000000..299a262
--- /dev/null
+++ b/test/units/module_utils/facts/fixtures/findmount_output.txt
@@ -0,0 +1,40 @@
+/sys sysfs sysfs rw,nosuid,nodev,noexec,relatime,seclabel
+/proc proc proc rw,nosuid,nodev,noexec,relatime
+/dev devtmpfs devtmpfs rw,nosuid,seclabel,size=8044400k,nr_inodes=2011100,mode=755
+/sys/kernel/security securityfs securityfs rw,nosuid,nodev,noexec,relatime
+/dev/shm tmpfs tmpfs rw,nosuid,nodev,seclabel
+/dev/pts devpts devpts rw,nosuid,noexec,relatime,seclabel,gid=5,mode=620,ptmxmode=000
+/run tmpfs tmpfs rw,nosuid,nodev,seclabel,mode=755
+/sys/fs/cgroup tmpfs tmpfs ro,nosuid,nodev,noexec,seclabel,mode=755
+/sys/fs/cgroup/systemd cgroup cgroup rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,na
+/sys/fs/pstore pstore pstore rw,nosuid,nodev,noexec,relatime,seclabel
+/sys/fs/cgroup/devices cgroup cgroup rw,nosuid,nodev,noexec,relatime,devices
+/sys/fs/cgroup/freezer cgroup cgroup rw,nosuid,nodev,noexec,relatime,freezer
+/sys/fs/cgroup/memory cgroup cgroup rw,nosuid,nodev,noexec,relatime,memory
+/sys/fs/cgroup/pids cgroup cgroup rw,nosuid,nodev,noexec,relatime,pids
+/sys/fs/cgroup/blkio cgroup cgroup rw,nosuid,nodev,noexec,relatime,blkio
+/sys/fs/cgroup/cpuset cgroup cgroup rw,nosuid,nodev,noexec,relatime,cpuset
+/sys/fs/cgroup/cpu,cpuacct cgroup cgroup rw,nosuid,nodev,noexec,relatime,cpu,cpuacct
+/sys/fs/cgroup/hugetlb cgroup cgroup rw,nosuid,nodev,noexec,relatime,hugetlb
+/sys/fs/cgroup/perf_event cgroup cgroup rw,nosuid,nodev,noexec,relatime,perf_event
+/sys/fs/cgroup/net_cls,net_prio cgroup cgroup rw,nosuid,nodev,noexec,relatime,net_cls,net_prio
+/sys/kernel/config configfs configfs rw,relatime
+/ /dev/mapper/fedora_dhcp129--186-root ext4 rw,relatime,seclabel,data=ordered
+/sys/fs/selinux selinuxfs selinuxfs rw,relatime
+/proc/sys/fs/binfmt_misc systemd-1 autofs rw,relatime,fd=24,pgrp=1,timeout=0,minproto=5,maxproto=5,direct
+/sys/kernel/debug debugfs debugfs rw,relatime,seclabel
+/dev/hugepages hugetlbfs hugetlbfs rw,relatime,seclabel
+/tmp tmpfs tmpfs rw,seclabel
+/dev/mqueue mqueue mqueue rw,relatime,seclabel
+/var/lib/machines /dev/loop0 btrfs rw,relatime,seclabel,space_cache,subvolid=5,subvol=/
+/boot /dev/sda1 ext4 rw,relatime,seclabel,data=ordered
+/home /dev/mapper/fedora_dhcp129--186-home ext4 rw,relatime,seclabel,data=ordered
+/run/user/1000 tmpfs tmpfs rw,nosuid,nodev,relatime,seclabel,size=1611044k,mode=700,uid=1000,gid=1000
+/run/user/1000/gvfs gvfsd-fuse fuse.gvfsd-fuse rw,nosuid,nodev,relatime,user_id=1000,group_id=1000
+/sys/fs/fuse/connections fusectl fusectl rw,relatime
+/not/a/real/bind_mount /dev/sdz4[/some/other/path] ext4 rw,relatime,seclabel,data=ordered
+/home/adrian/sshfs-grimlock grimlock.g.a: fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000
+/home/adrian/sshfs-grimlock-single-quote grimlock.g.a:test_path/path_with'single_quotes
+ fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000
+/home/adrian/sshfs-grimlock-single-quote-2 grimlock.g.a:path_with'single_quotes fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000
+/home/adrian/fotos grimlock.g.a:/mnt/data/foto's fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000
diff --git a/test/units/module_utils/facts/hardware/__init__.py b/test/units/module_utils/facts/hardware/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/facts/hardware/__init__.py
diff --git a/test/units/module_utils/facts/hardware/aix_data.py b/test/units/module_utils/facts/hardware/aix_data.py
new file mode 100644
index 0000000..d1a6c9a
--- /dev/null
+++ b/test/units/module_utils/facts/hardware/aix_data.py
@@ -0,0 +1,75 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+AIX_PROCESSOR_TEST_SCENARIOS = [
+ {
+ 'comment': 'AIX 7.2 (gcc119 on GCC farm)',
+ 'lsdev_output': [
+ 'proc0 Available 00-00 Processor',
+ 'proc8 Available 00-08 Processor',
+ 'proc16 Available 00-16 Processor',
+ 'proc24 Available 00-24 Processor',
+ 'proc32 Available 00-32 Processor',
+ 'proc40 Available 00-40 Processor',
+ 'proc48 Available 00-48 Processor',
+ 'proc56 Available 00-56 Processor',
+ 'proc64 Available 00-64 Processor',
+ 'proc72 Available 00-72 Processor',
+ ],
+ 'lsattr_type_output': ['type PowerPC_POWER8 Processor type False'],
+ 'lsattr_smt_threads_output': [
+ 'smt_threads 8 Processor SMT threads False'
+ ],
+ 'expected_result': {
+ 'processor': ['PowerPC_POWER8'],
+ 'processor_count': 1,
+ 'processor_cores': 10,
+ 'processor_threads_per_core': 8,
+ 'processor_vcpus': 80
+ },
+ },
+ {
+ 'comment': 'AIX 7.1 (gcc111 on GCC farm)',
+ 'lsdev_output': [
+ 'proc0 Available 00-00 Processor',
+ 'proc4 Available 00-04 Processor',
+ 'proc8 Available 00-08 Processor',
+ 'proc12 Available 00-12 Processor',
+ 'proc16 Available 00-16 Processor',
+ 'proc20 Available 00-20 Processor',
+ 'proc24 Available 00-24 Processor',
+ 'proc28 Available 00-28 Processor',
+ 'proc32 Available 00-32 Processor',
+ 'proc36 Available 00-36 Processor',
+ 'proc40 Available 00-40 Processor',
+ 'proc44 Available 00-44 Processor',
+ ],
+ 'lsattr_type_output': ['type PowerPC_POWER7 Processor type False'],
+ 'lsattr_smt_threads_output': [
+ 'smt_threads 4 Processor SMT threads False'
+ ],
+ 'expected_result': {
+ 'processor': ['PowerPC_POWER7'],
+ 'processor_count': 1,
+ 'processor_cores': 12,
+ 'processor_threads_per_core': 4,
+ 'processor_vcpus': 48
+ },
+ },
+]
diff --git a/test/units/module_utils/facts/hardware/linux_data.py b/test/units/module_utils/facts/hardware/linux_data.py
new file mode 100644
index 0000000..3879188
--- /dev/null
+++ b/test/units/module_utils/facts/hardware/linux_data.py
@@ -0,0 +1,633 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+LSBLK_OUTPUT = b"""
+/dev/sda
+/dev/sda1 32caaec3-ef40-4691-a3b6-438c3f9bc1c0
+/dev/sda2 66Ojcd-ULtu-1cZa-Tywo-mx0d-RF4O-ysA9jK
+/dev/mapper/fedora_dhcp129--186-swap eae6059d-2fbe-4d1c-920d-a80bbeb1ac6d
+/dev/mapper/fedora_dhcp129--186-root d34cf5e3-3449-4a6c-8179-a1feb2bca6ce
+/dev/mapper/fedora_dhcp129--186-home 2d3e4853-fa69-4ccf-8a6a-77b05ab0a42d
+/dev/sr0
+/dev/loop0 0f031512-ab15-497d-9abd-3a512b4a9390
+/dev/loop1 7c1b0f30-cf34-459f-9a70-2612f82b870a
+/dev/loop9 0f031512-ab15-497d-9abd-3a512b4a9390
+/dev/loop9 7c1b4444-cf34-459f-9a70-2612f82b870a
+/dev/mapper/docker-253:1-1050967-pool
+/dev/loop2
+/dev/mapper/docker-253:1-1050967-pool
+"""
+
+LSBLK_OUTPUT_2 = b"""
+/dev/sda
+/dev/sda1 32caaec3-ef40-4691-a3b6-438c3f9bc1c0
+/dev/sda2 66Ojcd-ULtu-1cZa-Tywo-mx0d-RF4O-ysA9jK
+/dev/mapper/fedora_dhcp129--186-swap eae6059d-2fbe-4d1c-920d-a80bbeb1ac6d
+/dev/mapper/fedora_dhcp129--186-root d34cf5e3-3449-4a6c-8179-a1feb2bca6ce
+/dev/mapper/fedora_dhcp129--186-home 2d3e4853-fa69-4ccf-8a6a-77b05ab0a42d
+/dev/mapper/an-example-mapper with a space in the name 84639acb-013f-4d2f-9392-526a572b4373
+/dev/sr0
+/dev/loop0 0f031512-ab15-497d-9abd-3a512b4a9390
+"""
+
+LSBLK_UUIDS = {'/dev/sda1': '66Ojcd-ULtu-1cZa-Tywo-mx0d-RF4O-ysA9jK'}
+
+UDEVADM_UUID = 'N/A'
+
+UDEVADM_OUTPUT = """
+UDEV_LOG=3
+DEVPATH=/devices/pci0000:00/0000:00:07.0/virtio2/block/vda/vda1
+MAJOR=252
+MINOR=1
+DEVNAME=/dev/vda1
+DEVTYPE=partition
+SUBSYSTEM=block
+MPATH_SBIN_PATH=/sbin
+ID_PATH=pci-0000:00:07.0-virtio-pci-virtio2
+ID_PART_TABLE_TYPE=dos
+ID_FS_UUID=57b1a3e7-9019-4747-9809-7ec52bba9179
+ID_FS_UUID_ENC=57b1a3e7-9019-4747-9809-7ec52bba9179
+ID_FS_VERSION=1.0
+ID_FS_TYPE=ext4
+ID_FS_USAGE=filesystem
+LVM_SBIN_PATH=/sbin
+DEVLINKS=/dev/block/252:1 /dev/disk/by-path/pci-0000:00:07.0-virtio-pci-virtio2-part1 /dev/disk/by-uuid/57b1a3e7-9019-4747-9809-7ec52bba9179
+"""
+
+MTAB = """
+sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0
+proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
+devtmpfs /dev devtmpfs rw,seclabel,nosuid,size=8044400k,nr_inodes=2011100,mode=755 0 0
+securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0
+tmpfs /dev/shm tmpfs rw,seclabel,nosuid,nodev 0 0
+devpts /dev/pts devpts rw,seclabel,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0
+tmpfs /run tmpfs rw,seclabel,nosuid,nodev,mode=755 0 0
+tmpfs /sys/fs/cgroup tmpfs ro,seclabel,nosuid,nodev,noexec,mode=755 0 0
+cgroup /sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd 0 0
+pstore /sys/fs/pstore pstore rw,seclabel,nosuid,nodev,noexec,relatime 0 0
+cgroup /sys/fs/cgroup/devices cgroup rw,nosuid,nodev,noexec,relatime,devices 0 0
+cgroup /sys/fs/cgroup/freezer cgroup rw,nosuid,nodev,noexec,relatime,freezer 0 0
+cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 0 0
+cgroup /sys/fs/cgroup/pids cgroup rw,nosuid,nodev,noexec,relatime,pids 0 0
+cgroup /sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0
+cgroup /sys/fs/cgroup/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset 0 0
+cgroup /sys/fs/cgroup/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0
+cgroup /sys/fs/cgroup/hugetlb cgroup rw,nosuid,nodev,noexec,relatime,hugetlb 0 0
+cgroup /sys/fs/cgroup/perf_event cgroup rw,nosuid,nodev,noexec,relatime,perf_event 0 0
+cgroup /sys/fs/cgroup/net_cls,net_prio cgroup rw,nosuid,nodev,noexec,relatime,net_cls,net_prio 0 0
+configfs /sys/kernel/config configfs rw,relatime 0 0
+/dev/mapper/fedora_dhcp129--186-root / ext4 rw,seclabel,relatime,data=ordered 0 0
+selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
+systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=24,pgrp=1,timeout=0,minproto=5,maxproto=5,direct 0 0
+debugfs /sys/kernel/debug debugfs rw,seclabel,relatime 0 0
+hugetlbfs /dev/hugepages hugetlbfs rw,seclabel,relatime 0 0
+tmpfs /tmp tmpfs rw,seclabel 0 0
+mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0
+/dev/loop0 /var/lib/machines btrfs rw,seclabel,relatime,space_cache,subvolid=5,subvol=/ 0 0
+/dev/sda1 /boot ext4 rw,seclabel,relatime,data=ordered 0 0
+/dev/mapper/fedora_dhcp129--186-home /home ext4 rw,seclabel,relatime,data=ordered 0 0
+tmpfs /run/user/1000 tmpfs rw,seclabel,nosuid,nodev,relatime,size=1611044k,mode=700,uid=1000,gid=1000 0 0
+gvfsd-fuse /run/user/1000/gvfs fuse.gvfsd-fuse rw,nosuid,nodev,relatime,user_id=1000,group_id=1000 0 0
+fusectl /sys/fs/fuse/connections fusectl rw,relatime 0 0
+grimlock.g.a: /home/adrian/sshfs-grimlock fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000 0 0
+grimlock.g.a:test_path/path_with'single_quotes /home/adrian/sshfs-grimlock-single-quote fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000 0 0
+grimlock.g.a:path_with'single_quotes /home/adrian/sshfs-grimlock-single-quote-2 fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000 0 0
+grimlock.g.a:/mnt/data/foto's /home/adrian/fotos fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000 0 0
+"""
+
+MTAB_ENTRIES = [
+ [
+ 'sysfs',
+ '/sys',
+ 'sysfs',
+ 'rw,seclabel,nosuid,nodev,noexec,relatime',
+ '0',
+ '0'
+ ],
+ ['proc', '/proc', 'proc', 'rw,nosuid,nodev,noexec,relatime', '0', '0'],
+ [
+ 'devtmpfs',
+ '/dev',
+ 'devtmpfs',
+ 'rw,seclabel,nosuid,size=8044400k,nr_inodes=2011100,mode=755',
+ '0',
+ '0'
+ ],
+ [
+ 'securityfs',
+ '/sys/kernel/security',
+ 'securityfs',
+ 'rw,nosuid,nodev,noexec,relatime',
+ '0',
+ '0'
+ ],
+ ['tmpfs', '/dev/shm', 'tmpfs', 'rw,seclabel,nosuid,nodev', '0', '0'],
+ [
+ 'devpts',
+ '/dev/pts',
+ 'devpts',
+ 'rw,seclabel,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000',
+ '0',
+ '0'
+ ],
+ ['tmpfs', '/run', 'tmpfs', 'rw,seclabel,nosuid,nodev,mode=755', '0', '0'],
+ [
+ 'tmpfs',
+ '/sys/fs/cgroup',
+ 'tmpfs',
+ 'ro,seclabel,nosuid,nodev,noexec,mode=755',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/systemd',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd',
+ '0',
+ '0'
+ ],
+ [
+ 'pstore',
+ '/sys/fs/pstore',
+ 'pstore',
+ 'rw,seclabel,nosuid,nodev,noexec,relatime',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/devices',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,devices',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/freezer',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,freezer',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/memory',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,memory',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/pids',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,pids',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/blkio',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,blkio',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/cpuset',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,cpuset',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/cpu,cpuacct',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,cpu,cpuacct',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/hugetlb',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,hugetlb',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/perf_event',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,perf_event',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/net_cls,net_prio',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,net_cls,net_prio',
+ '0',
+ '0'
+ ],
+ ['configfs', '/sys/kernel/config', 'configfs', 'rw,relatime', '0', '0'],
+ [
+ '/dev/mapper/fedora_dhcp129--186-root',
+ '/',
+ 'ext4',
+ 'rw,seclabel,relatime,data=ordered',
+ '0',
+ '0'
+ ],
+ ['selinuxfs', '/sys/fs/selinux', 'selinuxfs', 'rw,relatime', '0', '0'],
+ [
+ 'systemd-1',
+ '/proc/sys/fs/binfmt_misc',
+ 'autofs',
+ 'rw,relatime,fd=24,pgrp=1,timeout=0,minproto=5,maxproto=5,direct',
+ '0',
+ '0'
+ ],
+ ['debugfs', '/sys/kernel/debug', 'debugfs', 'rw,seclabel,relatime', '0', '0'],
+ [
+ 'hugetlbfs',
+ '/dev/hugepages',
+ 'hugetlbfs',
+ 'rw,seclabel,relatime',
+ '0',
+ '0'
+ ],
+ ['tmpfs', '/tmp', 'tmpfs', 'rw,seclabel', '0', '0'],
+ ['mqueue', '/dev/mqueue', 'mqueue', 'rw,seclabel,relatime', '0', '0'],
+ [
+ '/dev/loop0',
+ '/var/lib/machines',
+ 'btrfs',
+ 'rw,seclabel,relatime,space_cache,subvolid=5,subvol=/',
+ '0',
+ '0'
+ ],
+ ['/dev/sda1', '/boot', 'ext4', 'rw,seclabel,relatime,data=ordered', '0', '0'],
+ # A 'none' fstype
+ ['/dev/sdz3', '/not/a/real/device', 'none', 'rw,seclabel,relatime,data=ordered', '0', '0'],
+ # lets assume this is a bindmount
+ ['/dev/sdz4', '/not/a/real/bind_mount', 'ext4', 'rw,seclabel,relatime,data=ordered', '0', '0'],
+ [
+ '/dev/mapper/fedora_dhcp129--186-home',
+ '/home',
+ 'ext4',
+ 'rw,seclabel,relatime,data=ordered',
+ '0',
+ '0'
+ ],
+ [
+ 'tmpfs',
+ '/run/user/1000',
+ 'tmpfs',
+ 'rw,seclabel,nosuid,nodev,relatime,size=1611044k,mode=700,uid=1000,gid=1000',
+ '0',
+ '0'
+ ],
+ [
+ 'gvfsd-fuse',
+ '/run/user/1000/gvfs',
+ 'fuse.gvfsd-fuse',
+ 'rw,nosuid,nodev,relatime,user_id=1000,group_id=1000',
+ '0',
+ '0'
+ ],
+ ['fusectl', '/sys/fs/fuse/connections', 'fusectl', 'rw,relatime', '0', '0']]
+
+STATVFS_INFO = {'/': {'block_available': 10192323,
+ 'block_size': 4096,
+ 'block_total': 12868728,
+ 'block_used': 2676405,
+ 'inode_available': 3061699,
+ 'inode_total': 3276800,
+ 'inode_used': 215101,
+ 'size_available': 41747755008,
+ 'size_total': 52710309888},
+ '/not/a/real/bind_mount': {},
+ '/home': {'block_available': 1001578731,
+ 'block_size': 4096,
+ 'block_total': 105871006,
+ 'block_used': 5713133,
+ 'inode_available': 26860880,
+ 'inode_total': 26902528,
+ 'inode_used': 41648,
+ 'size_available': 410246647808,
+ 'size_total': 433647640576},
+ '/var/lib/machines': {'block_available': 10192316,
+ 'block_size': 4096,
+ 'block_total': 12868728,
+ 'block_used': 2676412,
+ 'inode_available': 3061699,
+ 'inode_total': 3276800,
+ 'inode_used': 215101,
+ 'size_available': 41747726336,
+ 'size_total': 52710309888},
+ '/boot': {'block_available': 187585,
+ 'block_size': 4096,
+ 'block_total': 249830,
+ 'block_used': 62245,
+ 'inode_available': 65096,
+ 'inode_total': 65536,
+ 'inode_used': 440,
+ 'size_available': 768348160,
+ 'size_total': 1023303680}
+ }
+
+# ['/dev/sdz4', '/not/a/real/bind_mount', 'ext4', 'rw,seclabel,relatime,data=ordered', '0', '0'],
+
+BIND_MOUNTS = ['/not/a/real/bind_mount']
+
+CPU_INFO_TEST_SCENARIOS = [
+ {
+ 'architecture': 'armv61',
+ 'nproc_out': 1,
+ 'sched_getaffinity': set([0]),
+ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv6-rev7-1cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': ['0', 'ARMv6-compatible processor rev 7 (v6l)'],
+ 'processor_cores': 1,
+ 'processor_count': 1,
+ 'processor_nproc': 1,
+ 'processor_threads_per_core': 1,
+ 'processor_vcpus': 1},
+ },
+ {
+ 'architecture': 'armv71',
+ 'nproc_out': 4,
+ 'sched_getaffinity': set([0, 1, 2, 3]),
+ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv7-rev4-4cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'ARMv7 Processor rev 4 (v7l)',
+ '1', 'ARMv7 Processor rev 4 (v7l)',
+ '2', 'ARMv7 Processor rev 4 (v7l)',
+ '3', 'ARMv7 Processor rev 4 (v7l)',
+ ],
+ 'processor_cores': 1,
+ 'processor_count': 4,
+ 'processor_nproc': 4,
+ 'processor_threads_per_core': 1,
+ 'processor_vcpus': 4},
+ },
+ {
+ 'architecture': 'aarch64',
+ 'nproc_out': 4,
+ 'sched_getaffinity': set([0, 1, 2, 3]),
+ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/aarch64-4cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'AArch64 Processor rev 4 (aarch64)',
+ '1', 'AArch64 Processor rev 4 (aarch64)',
+ '2', 'AArch64 Processor rev 4 (aarch64)',
+ '3', 'AArch64 Processor rev 4 (aarch64)',
+ ],
+ 'processor_cores': 1,
+ 'processor_count': 4,
+ 'processor_nproc': 4,
+ 'processor_threads_per_core': 1,
+ 'processor_vcpus': 4},
+ },
+ {
+ 'architecture': 'x86_64',
+ 'nproc_out': 4,
+ 'sched_getaffinity': set([0, 1, 2, 3]),
+ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-4cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'AuthenticAMD', 'Dual-Core AMD Opteron(tm) Processor 2216',
+ '1', 'AuthenticAMD', 'Dual-Core AMD Opteron(tm) Processor 2216',
+ '2', 'AuthenticAMD', 'Dual-Core AMD Opteron(tm) Processor 2216',
+ '3', 'AuthenticAMD', 'Dual-Core AMD Opteron(tm) Processor 2216',
+ ],
+ 'processor_cores': 2,
+ 'processor_count': 2,
+ 'processor_nproc': 4,
+ 'processor_threads_per_core': 1,
+ 'processor_vcpus': 4},
+ },
+ {
+ 'architecture': 'x86_64',
+ 'nproc_out': 4,
+ 'sched_getaffinity': set([0, 1, 2, 3]),
+ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-8cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'GenuineIntel', 'Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz',
+ '1', 'GenuineIntel', 'Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz',
+ '2', 'GenuineIntel', 'Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz',
+ '3', 'GenuineIntel', 'Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz',
+ '4', 'GenuineIntel', 'Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz',
+ '5', 'GenuineIntel', 'Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz',
+ '6', 'GenuineIntel', 'Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz',
+ '7', 'GenuineIntel', 'Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz',
+ ],
+ 'processor_cores': 4,
+ 'processor_count': 1,
+ 'processor_nproc': 4,
+ 'processor_threads_per_core': 2,
+ 'processor_vcpus': 8},
+ },
+ {
+ 'architecture': 'arm64',
+ 'nproc_out': 4,
+ 'sched_getaffinity': set([0, 1, 2, 3]),
+ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/arm64-4cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': ['0', '1', '2', '3'],
+ 'processor_cores': 1,
+ 'processor_count': 4,
+ 'processor_nproc': 4,
+ 'processor_threads_per_core': 1,
+ 'processor_vcpus': 4},
+ },
+ {
+ 'architecture': 'armv71',
+ 'nproc_out': 8,
+ 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7]),
+ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/armv7-rev3-8cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'ARMv7 Processor rev 3 (v7l)',
+ '1', 'ARMv7 Processor rev 3 (v7l)',
+ '2', 'ARMv7 Processor rev 3 (v7l)',
+ '3', 'ARMv7 Processor rev 3 (v7l)',
+ '4', 'ARMv7 Processor rev 3 (v7l)',
+ '5', 'ARMv7 Processor rev 3 (v7l)',
+ '6', 'ARMv7 Processor rev 3 (v7l)',
+ '7', 'ARMv7 Processor rev 3 (v7l)',
+ ],
+ 'processor_cores': 1,
+ 'processor_count': 8,
+ 'processor_nproc': 8,
+ 'processor_threads_per_core': 1,
+ 'processor_vcpus': 8},
+ },
+ {
+ 'architecture': 'x86_64',
+ 'nproc_out': 2,
+ 'sched_getaffinity': set([0, 1]),
+ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/x86_64-2cpu-cpuinfo')).readlines(),
+ 'expected_result': {
+ 'processor': [
+ '0', 'GenuineIntel', 'Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz',
+ '1', 'GenuineIntel', 'Intel(R) Xeon(R) CPU E5-2680 v2 @ 2.80GHz',
+ ],
+ 'processor_cores': 1,
+ 'processor_count': 2,
+ 'processor_nproc': 2,
+ 'processor_threads_per_core': 1,
+ 'processor_vcpus': 2},
+ },
+ {
+ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/ppc64-power7-rhel7-8cpu-cpuinfo')).readlines(),
+ 'architecture': 'ppc64',
+ 'nproc_out': 8,
+ 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7]),
+ 'expected_result': {
+ 'processor': [
+ '0', 'POWER7 (architected), altivec supported',
+ '1', 'POWER7 (architected), altivec supported',
+ '2', 'POWER7 (architected), altivec supported',
+ '3', 'POWER7 (architected), altivec supported',
+ '4', 'POWER7 (architected), altivec supported',
+ '5', 'POWER7 (architected), altivec supported',
+ '6', 'POWER7 (architected), altivec supported',
+ '7', 'POWER7 (architected), altivec supported'
+ ],
+ 'processor_cores': 1,
+ 'processor_count': 8,
+ 'processor_nproc': 8,
+ 'processor_threads_per_core': 1,
+ 'processor_vcpus': 8
+ },
+ },
+ {
+ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/ppc64le-power8-24cpu-cpuinfo')).readlines(),
+ 'architecture': 'ppc64le',
+ 'nproc_out': 24,
+ 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]),
+ 'expected_result': {
+ 'processor': [
+ '0', 'POWER8 (architected), altivec supported',
+ '1', 'POWER8 (architected), altivec supported',
+ '2', 'POWER8 (architected), altivec supported',
+ '3', 'POWER8 (architected), altivec supported',
+ '4', 'POWER8 (architected), altivec supported',
+ '5', 'POWER8 (architected), altivec supported',
+ '6', 'POWER8 (architected), altivec supported',
+ '7', 'POWER8 (architected), altivec supported',
+ '8', 'POWER8 (architected), altivec supported',
+ '9', 'POWER8 (architected), altivec supported',
+ '10', 'POWER8 (architected), altivec supported',
+ '11', 'POWER8 (architected), altivec supported',
+ '12', 'POWER8 (architected), altivec supported',
+ '13', 'POWER8 (architected), altivec supported',
+ '14', 'POWER8 (architected), altivec supported',
+ '15', 'POWER8 (architected), altivec supported',
+ '16', 'POWER8 (architected), altivec supported',
+ '17', 'POWER8 (architected), altivec supported',
+ '18', 'POWER8 (architected), altivec supported',
+ '19', 'POWER8 (architected), altivec supported',
+ '20', 'POWER8 (architected), altivec supported',
+ '21', 'POWER8 (architected), altivec supported',
+ '22', 'POWER8 (architected), altivec supported',
+ '23', 'POWER8 (architected), altivec supported',
+ ],
+ 'processor_cores': 1,
+ 'processor_count': 24,
+ 'processor_nproc': 24,
+ 'processor_threads_per_core': 1,
+ 'processor_vcpus': 24
+ },
+ },
+ {
+ 'cpuinfo': open(os.path.join(os.path.dirname(__file__), '../fixtures/cpuinfo/sparc-t5-debian-ldom-24vcpu')).readlines(),
+ 'architecture': 'sparc64',
+ 'nproc_out': 24,
+ 'sched_getaffinity': set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]),
+ 'expected_result': {
+ 'processor': [
+ 'UltraSparc T5 (Niagara5)',
+ ],
+ 'processor_cores': 1,
+ 'processor_count': 24,
+ 'processor_nproc': 24,
+ 'processor_threads_per_core': 1,
+ 'processor_vcpus': 24
+ },
+ },
+]
+
+SG_INQ_OUTPUTS = ["""
+Identify controller for /dev/nvme0n1:
+ Model number: Amazon Elastic Block Store
+ Serial number: vol0123456789
+ Firmware revision: 1.0
+ Version: 0.0
+ No optional admin command support
+ No optional NVM command support
+ PCI vendor ID VID/SSVID: 0x1d0f/0x1d0f
+ IEEE OUI Identifier: 0xa002dc
+ Controller ID: 0x0
+ Number of namespaces: 1
+ Maximum data transfer size: 64 pages
+ Namespace 1 (deduced from device name):
+ Namespace size/capacity: 62914560/62914560 blocks
+ Namespace utilization: 0 blocks
+ Number of LBA formats: 1
+ Index LBA size: 0
+ LBA format 0 support: <-- active
+ Logical block size: 512 bytes
+ Approximate namespace size: 32 GB
+ Metadata size: 0 bytes
+ Relative performance: Best [0x0]
+""", """
+Identify controller for /dev/nvme0n1:
+ Model number: Amazon Elastic Block Store
+ Unit serial number: vol0123456789
+ Firmware revision: 1.0
+ Version: 0.0
+ No optional admin command support
+ No optional NVM command support
+ PCI vendor ID VID/SSVID: 0x1d0f/0x1d0f
+ IEEE OUI Identifier: 0xa002dc
+ Controller ID: 0x0
+ Number of namespaces: 1
+ Maximum data transfer size: 64 pages
+ Namespace 1 (deduced from device name):
+ Namespace size/capacity: 62914560/62914560 blocks
+ Namespace utilization: 0 blocks
+ Number of LBA formats: 1
+ Index LBA size: 0
+ LBA format 0 support: <-- active
+ Logical block size: 512 bytes
+ Approximate namespace size: 32 GB
+ Metadata size: 0 bytes
+ Relative performance: Best [0x0]
+"""]
diff --git a/test/units/module_utils/facts/hardware/test_aix_processor.py b/test/units/module_utils/facts/hardware/test_aix_processor.py
new file mode 100644
index 0000000..94d9b9e
--- /dev/null
+++ b/test/units/module_utils/facts/hardware/test_aix_processor.py
@@ -0,0 +1,24 @@
+# -*- 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
+
+from ansible.module_utils.facts.hardware import aix
+import pytest
+
+from . aix_data import AIX_PROCESSOR_TEST_SCENARIOS
+
+
+@pytest.mark.parametrize('scenario', AIX_PROCESSOR_TEST_SCENARIOS)
+def test_get_cpu_info(mocker, scenario):
+ commands_results = [
+ (0, "\n".join(scenario['lsdev_output']), ''),
+ (0, "\n".join(scenario['lsattr_type_output']), ''),
+ (0, "\n".join(scenario['lsattr_smt_threads_output']), ''),
+ ]
+ module = mocker.Mock()
+ module.run_command = mocker.Mock(side_effect=commands_results)
+ inst = aix.AIXHardware(module=module)
+ assert scenario['expected_result'] == inst.get_cpu_facts()
diff --git a/test/units/module_utils/facts/hardware/test_linux.py b/test/units/module_utils/facts/hardware/test_linux.py
new file mode 100644
index 0000000..e3e07e7
--- /dev/null
+++ b/test/units/module_utils/facts/hardware/test_linux.py
@@ -0,0 +1,198 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+from units.compat import unittest
+from units.compat.mock import Mock, patch
+
+from ansible.module_utils.facts import timeout
+
+from ansible.module_utils.facts.hardware import linux
+
+from . linux_data import LSBLK_OUTPUT, LSBLK_OUTPUT_2, LSBLK_UUIDS, MTAB, MTAB_ENTRIES, BIND_MOUNTS, STATVFS_INFO, UDEVADM_UUID, UDEVADM_OUTPUT, SG_INQ_OUTPUTS
+
+with open(os.path.join(os.path.dirname(__file__), '../fixtures/findmount_output.txt')) as f:
+ FINDMNT_OUTPUT = f.read()
+
+GET_MOUNT_SIZE = {}
+
+
+def mock_get_mount_size(mountpoint):
+ return STATVFS_INFO.get(mountpoint, {})
+
+
+class TestFactsLinuxHardwareGetMountFacts(unittest.TestCase):
+
+ # FIXME: mock.patch instead
+ def setUp(self):
+ timeout.GATHER_TIMEOUT = 10
+
+ def tearDown(self):
+ timeout.GATHER_TIMEOUT = None
+
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._mtab_entries', return_value=MTAB_ENTRIES)
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._find_bind_mounts', return_value=BIND_MOUNTS)
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._lsblk_uuid', return_value=LSBLK_UUIDS)
+ @patch('ansible.module_utils.facts.hardware.linux.get_mount_size', side_effect=mock_get_mount_size)
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._udevadm_uuid', return_value=UDEVADM_UUID)
+ def test_get_mount_facts(self,
+ mock_get_mount_size,
+ mock_lsblk_uuid,
+ mock_find_bind_mounts,
+ mock_mtab_entries,
+ mock_udevadm_uuid):
+ module = Mock()
+ # Returns a LinuxHardware-ish
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+
+ # Nothing returned, just self.facts modified as a side effect
+ mount_facts = lh.get_mount_facts()
+ self.assertIsInstance(mount_facts, dict)
+ self.assertIn('mounts', mount_facts)
+ self.assertIsInstance(mount_facts['mounts'], list)
+ self.assertIsInstance(mount_facts['mounts'][0], dict)
+
+ home_expected = {'block_available': 1001578731,
+ 'block_size': 4096,
+ 'block_total': 105871006,
+ 'block_used': 5713133,
+ 'device': '/dev/mapper/fedora_dhcp129--186-home',
+ 'fstype': 'ext4',
+ 'inode_available': 26860880,
+ 'inode_total': 26902528,
+ 'inode_used': 41648,
+ 'mount': '/home',
+ 'options': 'rw,seclabel,relatime,data=ordered',
+ 'size_available': 410246647808,
+ 'size_total': 433647640576,
+ 'uuid': 'N/A'}
+ home_info = [x for x in mount_facts['mounts'] if x['mount'] == '/home'][0]
+
+ self.maxDiff = 4096
+ self.assertDictEqual(home_info, home_expected)
+
+ @patch('ansible.module_utils.facts.hardware.linux.get_file_content', return_value=MTAB)
+ def test_get_mtab_entries(self, mock_get_file_content):
+
+ module = Mock()
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ mtab_entries = lh._mtab_entries()
+ self.assertIsInstance(mtab_entries, list)
+ self.assertIsInstance(mtab_entries[0], list)
+ self.assertEqual(len(mtab_entries), 38)
+
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_findmnt', return_value=(0, FINDMNT_OUTPUT, ''))
+ def test_find_bind_mounts(self, mock_run_findmnt):
+ module = Mock()
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ bind_mounts = lh._find_bind_mounts()
+
+ # If bind_mounts becomes another seq type, feel free to change
+ self.assertIsInstance(bind_mounts, set)
+ self.assertEqual(len(bind_mounts), 1)
+ self.assertIn('/not/a/real/bind_mount', bind_mounts)
+
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_findmnt', return_value=(37, '', ''))
+ def test_find_bind_mounts_non_zero(self, mock_run_findmnt):
+ module = Mock()
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ bind_mounts = lh._find_bind_mounts()
+
+ self.assertIsInstance(bind_mounts, set)
+ self.assertEqual(len(bind_mounts), 0)
+
+ def test_find_bind_mounts_no_findmnts(self):
+ module = Mock()
+ module.get_bin_path = Mock(return_value=None)
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ bind_mounts = lh._find_bind_mounts()
+
+ self.assertIsInstance(bind_mounts, set)
+ self.assertEqual(len(bind_mounts), 0)
+
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_lsblk', return_value=(0, LSBLK_OUTPUT, ''))
+ def test_lsblk_uuid(self, mock_run_lsblk):
+ module = Mock()
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ lsblk_uuids = lh._lsblk_uuid()
+
+ self.assertIsInstance(lsblk_uuids, dict)
+ self.assertIn(b'/dev/loop9', lsblk_uuids)
+ self.assertIn(b'/dev/sda1', lsblk_uuids)
+ self.assertEqual(lsblk_uuids[b'/dev/sda1'], b'32caaec3-ef40-4691-a3b6-438c3f9bc1c0')
+
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_lsblk', return_value=(37, LSBLK_OUTPUT, ''))
+ def test_lsblk_uuid_non_zero(self, mock_run_lsblk):
+ module = Mock()
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ lsblk_uuids = lh._lsblk_uuid()
+
+ self.assertIsInstance(lsblk_uuids, dict)
+ self.assertEqual(len(lsblk_uuids), 0)
+
+ def test_lsblk_uuid_no_lsblk(self):
+ module = Mock()
+ module.get_bin_path = Mock(return_value=None)
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ lsblk_uuids = lh._lsblk_uuid()
+
+ self.assertIsInstance(lsblk_uuids, dict)
+ self.assertEqual(len(lsblk_uuids), 0)
+
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_lsblk', return_value=(0, LSBLK_OUTPUT_2, ''))
+ def test_lsblk_uuid_dev_with_space_in_name(self, mock_run_lsblk):
+ module = Mock()
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ lsblk_uuids = lh._lsblk_uuid()
+ self.assertIsInstance(lsblk_uuids, dict)
+ self.assertIn(b'/dev/loop0', lsblk_uuids)
+ self.assertIn(b'/dev/sda1', lsblk_uuids)
+ self.assertEqual(lsblk_uuids[b'/dev/mapper/an-example-mapper with a space in the name'], b'84639acb-013f-4d2f-9392-526a572b4373')
+ self.assertEqual(lsblk_uuids[b'/dev/sda1'], b'32caaec3-ef40-4691-a3b6-438c3f9bc1c0')
+
+ def test_udevadm_uuid(self):
+ module = Mock()
+ module.run_command = Mock(return_value=(0, UDEVADM_OUTPUT, '')) # (rc, out, err)
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ udevadm_uuid = lh._udevadm_uuid('mock_device')
+
+ self.assertEqual(udevadm_uuid, '57b1a3e7-9019-4747-9809-7ec52bba9179')
+
+ def test_get_sg_inq_serial(self):
+ # Valid outputs
+ for sq_inq_output in SG_INQ_OUTPUTS:
+ module = Mock()
+ module.run_command = Mock(return_value=(0, sq_inq_output, '')) # (rc, out, err)
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ sg_inq_serial = lh._get_sg_inq_serial('/usr/bin/sg_inq', 'nvme0n1')
+ self.assertEqual(sg_inq_serial, 'vol0123456789')
+
+ # Invalid output
+ module = Mock()
+ module.run_command = Mock(return_value=(0, '', '')) # (rc, out, err)
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ sg_inq_serial = lh._get_sg_inq_serial('/usr/bin/sg_inq', 'nvme0n1')
+ self.assertEqual(sg_inq_serial, None)
+
+ # Non zero rc
+ module = Mock()
+ module.run_command = Mock(return_value=(42, '', 'Error 42')) # (rc, out, err)
+ lh = linux.LinuxHardware(module=module, load_on_init=False)
+ sg_inq_serial = lh._get_sg_inq_serial('/usr/bin/sg_inq', 'nvme0n1')
+ self.assertEqual(sg_inq_serial, None)
diff --git a/test/units/module_utils/facts/hardware/test_linux_get_cpu_info.py b/test/units/module_utils/facts/hardware/test_linux_get_cpu_info.py
new file mode 100644
index 0000000..aea8694
--- /dev/null
+++ b/test/units/module_utils/facts/hardware/test_linux_get_cpu_info.py
@@ -0,0 +1,62 @@
+# -*- 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.facts.hardware import linux
+
+from . linux_data import CPU_INFO_TEST_SCENARIOS
+
+
+def test_get_cpu_info(mocker):
+ module = mocker.Mock()
+ inst = linux.LinuxHardware(module)
+
+ mocker.patch('os.path.exists', return_value=False)
+ mocker.patch('os.access', return_value=True)
+ for test in CPU_INFO_TEST_SCENARIOS:
+ mocker.patch('ansible.module_utils.facts.hardware.linux.get_file_lines', side_effect=[[], test['cpuinfo']])
+ mocker.patch('os.sched_getaffinity', create=True, return_value=test['sched_getaffinity'])
+ module.run_command.return_value = (0, test['nproc_out'], '')
+ collected_facts = {'ansible_architecture': test['architecture']}
+
+ assert test['expected_result'] == inst.get_cpu_facts(collected_facts=collected_facts)
+
+
+def test_get_cpu_info_nproc(mocker):
+ module = mocker.Mock()
+ inst = linux.LinuxHardware(module)
+
+ mocker.patch('os.path.exists', return_value=False)
+ mocker.patch('os.access', return_value=True)
+ for test in CPU_INFO_TEST_SCENARIOS:
+ mocker.patch('ansible.module_utils.facts.hardware.linux.get_file_lines', side_effect=[[], test['cpuinfo']])
+ mocker.patch('os.sched_getaffinity', create=True, side_effect=AttributeError)
+ mocker.patch('ansible.module_utils.facts.hardware.linux.get_bin_path', return_value='/usr/bin/nproc')
+ module.run_command.return_value = (0, test['nproc_out'], '')
+ collected_facts = {'ansible_architecture': test['architecture']}
+
+ assert test['expected_result'] == inst.get_cpu_facts(collected_facts=collected_facts)
+
+
+def test_get_cpu_info_missing_arch(mocker):
+ module = mocker.Mock()
+ inst = linux.LinuxHardware(module)
+
+ # ARM and Power will report incorrect processor count if architecture is not available
+ mocker.patch('os.path.exists', return_value=False)
+ mocker.patch('os.access', return_value=True)
+ for test in CPU_INFO_TEST_SCENARIOS:
+ mocker.patch('ansible.module_utils.facts.hardware.linux.get_file_lines', side_effect=[[], test['cpuinfo']])
+ mocker.patch('os.sched_getaffinity', create=True, return_value=test['sched_getaffinity'])
+
+ module.run_command.return_value = (0, test['nproc_out'], '')
+
+ test_result = inst.get_cpu_facts()
+
+ if test['architecture'].startswith(('armv', 'aarch', 'ppc')):
+ assert test['expected_result'] != test_result
+ else:
+ assert test['expected_result'] == test_result
diff --git a/test/units/module_utils/facts/hardware/test_sunos_get_uptime_facts.py b/test/units/module_utils/facts/hardware/test_sunos_get_uptime_facts.py
new file mode 100644
index 0000000..e14a2da
--- /dev/null
+++ b/test/units/module_utils/facts/hardware/test_sunos_get_uptime_facts.py
@@ -0,0 +1,20 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import time
+from ansible.module_utils.facts.hardware import sunos
+
+
+def test_sunos_get_uptime_facts(mocker):
+ kstat_output = '\nunix:0:system_misc:boot_time\t1548249689\n'
+
+ module_mock = mocker.patch('ansible.module_utils.basic.AnsibleModule')
+ module = module_mock()
+ module.run_command.return_value = (0, kstat_output, '')
+
+ inst = sunos.SunOSHardware(module)
+
+ mocker.patch('time.time', return_value=1567052602.5089788)
+ expected = int(time.time()) - 1548249689
+ result = inst.get_uptime_facts()
+ assert expected == result['uptime_seconds']
diff --git a/test/units/module_utils/facts/network/__init__.py b/test/units/module_utils/facts/network/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/facts/network/__init__.py
diff --git a/test/units/module_utils/facts/network/test_fc_wwn.py b/test/units/module_utils/facts/network/test_fc_wwn.py
new file mode 100644
index 0000000..32a3a43
--- /dev/null
+++ b/test/units/module_utils/facts/network/test_fc_wwn.py
@@ -0,0 +1,137 @@
+# -*- 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.facts.network import fc_wwn
+from units.compat.mock import Mock
+
+
+# AIX lsdev
+LSDEV_OUTPUT = """
+fcs0 Defined 00-00 8Gb PCI Express Dual Port FC Adapter (df1000f114108a03)
+fcs1 Available 04-00 8Gb PCI Express Dual Port FC Adapter (df1000f114108a03)
+"""
+
+# a bit cutted output of lscfg (from Z0 to ZC)
+LSCFG_OUTPUT = """
+ fcs1 U78CB.001.WZS00ZS-P1-C9-T1 8Gb PCI Express Dual Port FC Adapter (df1000f114108a03)
+
+ Part Number.................00E0806
+ Serial Number...............1C4090830F
+ Manufacturer................001C
+ EC Level.................... D77161
+ Customer Card ID Number.....577D
+ FRU Number..................00E0806
+ Device Specific.(ZM)........3
+ Network Address.............10000090FA551508
+ ROS Level and ID............027820B7
+ Device Specific.(Z0)........31004549
+ Device Specific.(ZC)........00000000
+ Hardware Location Code......U78CB.001.WZS00ZS-P1-C9-T1
+"""
+
+# Solaris
+FCINFO_OUTPUT = """
+HBA Port WWN: 10000090fa1658de
+ Port Mode: Initiator
+ Port ID: 30100
+ OS Device Name: /dev/cfg/c13
+ Manufacturer: Emulex
+ Model: LPe12002-S
+ Firmware Version: LPe12002-S 2.01a12
+ FCode/BIOS Version: Boot:5.03a0 Fcode:3.01a1
+ Serial Number: 4925381+13090001ER
+ Driver Name: emlxs
+ Driver Version: 3.3.00.1 (2018.01.05.16.30)
+ Type: N-port
+ State: online
+ Supported Speeds: 2Gb 4Gb 8Gb
+ Current Speed: 8Gb
+ Node WWN: 20000090fa1658de
+ NPIV Not Supported
+"""
+
+IOSCAN_OUT = """
+Class I H/W Path Driver S/W State H/W Type Description
+==================================================================
+fc 0 2/0/10/1/0 fcd CLAIMED INTERFACE HP AB379-60101 4Gb Dual Port PCI/PCI-X Fibre Channel Adapter (FC Port 1)
+ /dev/fcd0
+"""
+
+FCMSUTIL_OUT = """
+ Vendor ID is = 0x1077
+ Device ID is = 0x2422
+ PCI Sub-system Vendor ID is = 0x103C
+ PCI Sub-system ID is = 0x12D7
+ PCI Mode = PCI-X 133 MHz
+ ISP Code version = 5.4.0
+ ISP Chip version = 3
+ Topology = PTTOPT_FABRIC
+ Link Speed = 4Gb
+ Local N_Port_id is = 0x010300
+ Previous N_Port_id is = None
+ N_Port Node World Wide Name = 0x50060b00006975ed
+ N_Port Port World Wide Name = 0x50060b00006975ec
+ Switch Port World Wide Name = 0x200300051e046c0f
+ Switch Node World Wide Name = 0x100000051e046c0f
+ N_Port Symbolic Port Name = server1_fcd0
+ N_Port Symbolic Node Name = server1_HP-UX_B.11.31
+ Driver state = ONLINE
+ Hardware Path is = 2/0/10/1/0
+ Maximum Frame Size = 2048
+ Driver-Firmware Dump Available = NO
+ Driver-Firmware Dump Timestamp = N/A
+ TYPE = PFC
+ NPIV Supported = YES
+ Driver Version = @(#) fcd B.11.31.1103 Dec 6 2010
+"""
+
+
+def mock_get_bin_path(cmd, required=False, opt_dirs=None):
+ result = None
+ if cmd == 'lsdev':
+ result = '/usr/sbin/lsdev'
+ elif cmd == 'lscfg':
+ result = '/usr/sbin/lscfg'
+ elif cmd == 'fcinfo':
+ result = '/usr/sbin/fcinfo'
+ elif cmd == 'ioscan':
+ result = '/usr/bin/ioscan'
+ elif cmd == 'fcmsutil':
+ result = '/opt/fcms/bin/fcmsutil'
+ return result
+
+
+def mock_run_command(cmd):
+ rc = 0
+ if 'lsdev' in cmd:
+ result = LSDEV_OUTPUT
+ elif 'lscfg' in cmd:
+ result = LSCFG_OUTPUT
+ elif 'fcinfo' in cmd:
+ result = FCINFO_OUTPUT
+ elif 'ioscan' in cmd:
+ result = IOSCAN_OUT
+ elif 'fcmsutil' in cmd:
+ result = FCMSUTIL_OUT
+ else:
+ rc = 1
+ result = 'Error'
+ return (rc, result, '')
+
+
+def test_get_fc_wwn_info(mocker):
+ module = Mock()
+ inst = fc_wwn.FcWwnInitiatorFactCollector()
+
+ mocker.patch.object(module, 'get_bin_path', side_effect=mock_get_bin_path)
+ mocker.patch.object(module, 'run_command', side_effect=mock_run_command)
+
+ d = {'aix6': ['10000090FA551508'], 'sunos5': ['10000090fa1658de'], 'hp-ux11': ['0x50060b00006975ec']}
+ for key, value in d.items():
+ mocker.patch('sys.platform', key)
+ wwn_expected = {"fibre_channel_wwn": value}
+ assert wwn_expected == inst.collect(module=module)
diff --git a/test/units/module_utils/facts/network/test_generic_bsd.py b/test/units/module_utils/facts/network/test_generic_bsd.py
new file mode 100644
index 0000000..f061f04
--- /dev/null
+++ b/test/units/module_utils/facts/network/test_generic_bsd.py
@@ -0,0 +1,217 @@
+# -*- coding: utf-8 -*-
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat.mock import Mock
+from units.compat import unittest
+
+from ansible.module_utils.facts.network import generic_bsd
+
+
+def get_bin_path(command):
+ if command == 'ifconfig':
+ return 'fake/ifconfig'
+ elif command == 'route':
+ return 'fake/route'
+ return None
+
+
+netbsd_ifconfig_a_out_7_1 = r'''
+lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 33624
+ inet 127.0.0.1 netmask 0xff000000
+ inet6 ::1 prefixlen 128
+ inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1
+re0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
+ capabilities=3f80<TSO4,IP4CSUM_Rx,IP4CSUM_Tx,TCP4CSUM_Rx,TCP4CSUM_Tx>
+ capabilities=3f80<UDP4CSUM_Rx,UDP4CSUM_Tx>
+ enabled=0
+ ec_capabilities=3<VLAN_MTU,VLAN_HWTAGGING>
+ ec_enabled=0
+ address: 52:54:00:63:55:af
+ media: Ethernet autoselect (100baseTX full-duplex)
+ status: active
+ inet 192.168.122.205 netmask 0xffffff00 broadcast 192.168.122.255
+ inet6 fe80::5054:ff:fe63:55af%re0 prefixlen 64 scopeid 0x2
+'''
+
+netbsd_ifconfig_a_out_post_7_1 = r'''
+lo0: flags=0x8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 33624
+ inet 127.0.0.1/8 flags 0x0
+ inet6 ::1/128 flags 0x20<NODAD>
+ inet6 fe80::1%lo0/64 flags 0x0 scopeid 0x1
+re0: flags=0x8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> mtu 1500
+ capabilities=3f80<TSO4,IP4CSUM_Rx,IP4CSUM_Tx,TCP4CSUM_Rx,TCP4CSUM_Tx>
+ capabilities=3f80<UDP4CSUM_Rx,UDP4CSUM_Tx>
+ enabled=0
+ ec_capabilities=3<VLAN_MTU,VLAN_HWTAGGING>
+ ec_enabled=0
+ address: 52:54:00:63:55:af
+ media: Ethernet autoselect (100baseTX full-duplex)
+ status: active
+ inet 192.168.122.205/24 broadcast 192.168.122.255 flags 0x0
+ inet6 fe80::5054:ff:fe63:55af%re0/64 flags 0x0 scopeid 0x2
+'''
+
+NETBSD_EXPECTED = {'all_ipv4_addresses': ['192.168.122.205'],
+ 'all_ipv6_addresses': ['fe80::5054:ff:fe63:55af%re0'],
+ 'default_ipv4': {},
+ 'default_ipv6': {},
+ 'interfaces': ['lo0', 're0'],
+ 'lo0': {'device': 'lo0',
+ 'flags': ['UP', 'LOOPBACK', 'RUNNING', 'MULTICAST'],
+ 'ipv4': [{'address': '127.0.0.1',
+ 'broadcast': '127.255.255.255',
+ 'netmask': '255.0.0.0',
+ 'network': '127.0.0.0'}],
+ 'ipv6': [{'address': '::1', 'prefix': '128'},
+ {'address': 'fe80::1%lo0', 'prefix': '64', 'scope': '0x1'}],
+ 'macaddress': 'unknown',
+ 'mtu': '33624',
+ 'type': 'loopback'},
+ 're0': {'device': 're0',
+ 'flags': ['UP', 'BROADCAST', 'RUNNING', 'SIMPLEX', 'MULTICAST'],
+ 'ipv4': [{'address': '192.168.122.205',
+ 'broadcast': '192.168.122.255',
+ 'netmask': '255.255.255.0',
+ 'network': '192.168.122.0'}],
+ 'ipv6': [{'address': 'fe80::5054:ff:fe63:55af%re0',
+ 'prefix': '64',
+ 'scope': '0x2'}],
+ 'macaddress': 'unknown',
+ 'media': 'Ethernet',
+ 'media_options': [],
+ 'media_select': 'autoselect',
+ 'media_type': '100baseTX',
+ 'mtu': '1500',
+ 'status': 'active',
+ 'type': 'ether'}}
+
+
+def run_command_old_ifconfig(command):
+ if command == 'fake/route':
+ return 0, 'Foo', ''
+ if command == ['fake/ifconfig', '-a']:
+ return 0, netbsd_ifconfig_a_out_7_1, ''
+ return 1, '', ''
+
+
+def run_command_post_7_1_ifconfig(command):
+ if command == 'fake/route':
+ return 0, 'Foo', ''
+ if command == ['fake/ifconfig', '-a']:
+ return 0, netbsd_ifconfig_a_out_post_7_1, ''
+ return 1, '', ''
+
+
+class TestGenericBsdNetworkNetBSD(unittest.TestCase):
+ gather_subset = ['all']
+
+ def setUp(self):
+ self.maxDiff = None
+ self.longMessage = True
+
+ # TODO: extract module run_command/get_bin_path usage to methods I can mock without mocking all of run_command
+ def test(self):
+ module = self._mock_module()
+ module.get_bin_path.side_effect = get_bin_path
+ module.run_command.side_effect = run_command_old_ifconfig
+
+ bsd_net = generic_bsd.GenericBsdIfconfigNetwork(module)
+
+ res = bsd_net.populate()
+ self.assertDictEqual(res, NETBSD_EXPECTED)
+
+ def test_ifconfig_post_7_1(self):
+ module = self._mock_module()
+ module.get_bin_path.side_effect = get_bin_path
+ module.run_command.side_effect = run_command_post_7_1_ifconfig
+
+ bsd_net = generic_bsd.GenericBsdIfconfigNetwork(module)
+
+ res = bsd_net.populate()
+ self.assertDictEqual(res, NETBSD_EXPECTED)
+
+ def test_netbsd_ifconfig_old_and_new(self):
+ module_new = self._mock_module()
+ module_new.get_bin_path.side_effect = get_bin_path
+ module_new.run_command.side_effect = run_command_post_7_1_ifconfig
+
+ bsd_net_new = generic_bsd.GenericBsdIfconfigNetwork(module_new)
+ res_new = bsd_net_new.populate()
+
+ module_old = self._mock_module()
+ module_old.get_bin_path.side_effect = get_bin_path
+ module_old.run_command.side_effect = run_command_old_ifconfig
+
+ bsd_net_old = generic_bsd.GenericBsdIfconfigNetwork(module_old)
+ res_old = bsd_net_old.populate()
+
+ self.assertDictEqual(res_old, res_new)
+ self.assertDictEqual(res_old, NETBSD_EXPECTED)
+ self.assertDictEqual(res_new, NETBSD_EXPECTED)
+
+ def _mock_module(self):
+ mock_module = Mock()
+ mock_module.params = {'gather_subset': self.gather_subset,
+ 'gather_timeout': 5,
+ 'filter': '*'}
+ mock_module.get_bin_path = Mock(return_value=None)
+ return mock_module
+
+ def test_ensure_correct_netmask_parsing(self):
+ n = generic_bsd.GenericBsdIfconfigNetwork(None)
+ lines = [
+ 'inet 192.168.7.113 netmask 0xffffff00 broadcast 192.168.7.255',
+ 'inet 10.109.188.206 --> 10.109.188.206 netmask 0xffffe000',
+ ]
+ expected = [
+ (
+ {
+ 'ipv4': [
+ {
+ 'address': '192.168.7.113',
+ 'netmask': '255.255.255.0',
+ 'network': '192.168.7.0',
+ 'broadcast': '192.168.7.255'
+ }
+ ]
+ },
+ {'all_ipv4_addresses': ['192.168.7.113']},
+ ),
+ (
+ {
+ 'ipv4': [
+ {
+ 'address': '10.109.188.206',
+ 'netmask': '255.255.224.0',
+ 'network': '10.109.160.0',
+ 'broadcast': '10.109.191.255'
+ }
+ ]
+ },
+ {'all_ipv4_addresses': ['10.109.188.206']},
+ ),
+ ]
+ for i, line in enumerate(lines):
+ words = line.split()
+ current_if = {'ipv4': []}
+ ips = {'all_ipv4_addresses': []}
+ n.parse_inet_line(words, current_if, ips)
+ self.assertDictEqual(current_if, expected[i][0])
+ self.assertDictEqual(ips, expected[i][1])
diff --git a/test/units/module_utils/facts/network/test_iscsi_get_initiator.py b/test/units/module_utils/facts/network/test_iscsi_get_initiator.py
new file mode 100644
index 0000000..2048ba2
--- /dev/null
+++ b/test/units/module_utils/facts/network/test_iscsi_get_initiator.py
@@ -0,0 +1,54 @@
+# -*- 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.facts.network import iscsi
+from units.compat.mock import Mock
+
+
+# AIX # lsattr -E -l iscsi0
+LSATTR_OUTPUT = """
+disc_filename /etc/iscsi/targets Configuration file False
+disc_policy file Discovery Policy True
+initiator_name iqn.localhost.hostid.7f000002 iSCSI Initiator Name True
+isns_srvnames auto iSNS Servers IP Addresses True
+isns_srvports iSNS Servers Port Numbers True
+max_targets 16 Maximum Targets Allowed True
+num_cmd_elems 200 Maximum number of commands to queue to driver True
+"""
+
+# HP-UX # iscsiutil -l
+ISCSIUTIL_OUTPUT = """
+Initiator Name : iqn.2001-04.com.hp.stor:svcio
+Initiator Alias :
+Authentication Method : None
+CHAP Method : CHAP_UNI
+Initiator CHAP Name :
+CHAP Secret :
+NAS Hostname :
+NAS Secret :
+Radius Server Hostname :
+Header Digest : None,CRC32C (default)
+Data Digest : None,CRC32C (default)
+SLP Scope list for iSLPD :
+"""
+
+
+def test_get_iscsi_info(mocker):
+ module = Mock()
+ inst = iscsi.IscsiInitiatorNetworkCollector()
+
+ mocker.patch('sys.platform', 'aix6')
+ mocker.patch('ansible.module_utils.facts.network.iscsi.get_bin_path', return_value='/usr/sbin/lsattr')
+ mocker.patch.object(module, 'run_command', return_value=(0, LSATTR_OUTPUT, ''))
+ aix_iscsi_expected = {"iscsi_iqn": "iqn.localhost.hostid.7f000002"}
+ assert aix_iscsi_expected == inst.collect(module=module)
+
+ mocker.patch('sys.platform', 'hp-ux')
+ mocker.patch('ansible.module_utils.facts.network.iscsi.get_bin_path', return_value='/opt/iscsi/bin/iscsiutil')
+ mocker.patch.object(module, 'run_command', return_value=(0, ISCSIUTIL_OUTPUT, ''))
+ hpux_iscsi_expected = {"iscsi_iqn": " iqn.2001-04.com.hp.stor:svcio"}
+ assert hpux_iscsi_expected == inst.collect(module=module)
diff --git a/test/units/module_utils/facts/other/__init__.py b/test/units/module_utils/facts/other/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/facts/other/__init__.py
diff --git a/test/units/module_utils/facts/other/test_facter.py b/test/units/module_utils/facts/other/test_facter.py
new file mode 100644
index 0000000..7466338
--- /dev/null
+++ b/test/units/module_utils/facts/other/test_facter.py
@@ -0,0 +1,228 @@
+# unit tests for ansible other facter fact collector
+# -*- coding: utf-8 -*-
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat.mock import Mock, patch
+
+from .. base import BaseFactsTest
+
+from ansible.module_utils.facts.other.facter import FacterFactCollector
+
+facter_json_output = '''
+{
+ "operatingsystemmajrelease": "25",
+ "hardwareisa": "x86_64",
+ "kernel": "Linux",
+ "path": "/home/testuser/src/ansible/bin:/home/testuser/perl5/bin:/home/testuser/perl5/bin:/home/testuser/bin:/home/testuser/.local/bin:/home/testuser/pythons/bin:/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/testuser/.cabal/bin:/home/testuser/gopath/bin:/home/testuser/.rvm/bin",
+ "memorysize": "15.36 GB",
+ "memoryfree": "4.88 GB",
+ "swapsize": "7.70 GB",
+ "swapfree": "6.75 GB",
+ "swapsize_mb": "7880.00",
+ "swapfree_mb": "6911.41",
+ "memorysize_mb": "15732.95",
+ "memoryfree_mb": "4997.68",
+ "lsbmajdistrelease": "25",
+ "macaddress": "02:42:ea:15:d8:84",
+ "id": "testuser",
+ "domain": "example.com",
+ "augeasversion": "1.7.0",
+ "os": {
+ "name": "Fedora",
+ "family": "RedHat",
+ "release": {
+ "major": "25",
+ "full": "25"
+ },
+ "lsb": {
+ "distcodename": "TwentyFive",
+ "distid": "Fedora",
+ "distdescription": "Fedora release 25 (Twenty Five)",
+ "release": ":core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch",
+ "distrelease": "25",
+ "majdistrelease": "25"
+ }
+ },
+ "processors": {
+ "models": [
+ "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz"
+ ],
+ "count": 8,
+ "physicalcount": 1
+ },
+ "architecture": "x86_64",
+ "hardwaremodel": "x86_64",
+ "operatingsystem": "Fedora",
+ "processor0": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "processor1": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "processor2": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "processor3": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "processor4": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "processor5": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "processor6": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "processor7": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "processorcount": 8,
+ "uptime_seconds": 1558090,
+ "fqdn": "myhostname.example.com",
+ "rubyversion": "2.3.3",
+ "gid": "testuser",
+ "physicalprocessorcount": 1,
+ "netmask": "255.255.0.0",
+ "uniqueid": "a8c01301",
+ "uptime_days": 18,
+ "interfaces": "docker0,em1,lo,vethf20ff12,virbr0,virbr1,virbr0_nic,virbr1_nic,wlp4s0",
+ "ipaddress_docker0": "172.17.0.1",
+ "macaddress_docker0": "02:42:ea:15:d8:84",
+ "netmask_docker0": "255.255.0.0",
+ "mtu_docker0": 1500,
+ "macaddress_em1": "3c:97:0e:e9:28:8e",
+ "mtu_em1": 1500,
+ "ipaddress_lo": "127.0.0.1",
+ "netmask_lo": "255.0.0.0",
+ "mtu_lo": 65536,
+ "macaddress_vethf20ff12": "ae:6e:2b:1e:a1:31",
+ "mtu_vethf20ff12": 1500,
+ "ipaddress_virbr0": "192.168.137.1",
+ "macaddress_virbr0": "52:54:00:ce:82:5e",
+ "netmask_virbr0": "255.255.255.0",
+ "mtu_virbr0": 1500,
+ "ipaddress_virbr1": "192.168.121.1",
+ "macaddress_virbr1": "52:54:00:b4:68:a9",
+ "netmask_virbr1": "255.255.255.0",
+ "mtu_virbr1": 1500,
+ "macaddress_virbr0_nic": "52:54:00:ce:82:5e",
+ "mtu_virbr0_nic": 1500,
+ "macaddress_virbr1_nic": "52:54:00:b4:68:a9",
+ "mtu_virbr1_nic": 1500,
+ "ipaddress_wlp4s0": "192.168.1.19",
+ "macaddress_wlp4s0": "5c:51:4f:e6:a8:e3",
+ "netmask_wlp4s0": "255.255.255.0",
+ "mtu_wlp4s0": 1500,
+ "virtual": "physical",
+ "is_virtual": false,
+ "partitions": {
+ "sda2": {
+ "size": "499091456"
+ },
+ "sda1": {
+ "uuid": "32caaec3-ef40-4691-a3b6-438c3f9bc1c0",
+ "size": "1024000",
+ "mount": "/boot"
+ }
+ },
+ "lsbdistcodename": "TwentyFive",
+ "lsbrelease": ":core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch", # noqa
+ "filesystems": "btrfs,ext2,ext3,ext4,xfs",
+ "system_uptime": {
+ "seconds": 1558090,
+ "hours": 432,
+ "days": 18,
+ "uptime": "18 days"
+ },
+ "ipaddress": "172.17.0.1",
+ "timezone": "EDT",
+ "ps": "ps -ef",
+ "rubyplatform": "x86_64-linux",
+ "rubysitedir": "/usr/local/share/ruby/site_ruby",
+ "uptime": "18 days",
+ "lsbdistrelease": "25",
+ "operatingsystemrelease": "25",
+ "facterversion": "2.4.3",
+ "kernelrelease": "4.9.14-200.fc25.x86_64",
+ "lsbdistdescription": "Fedora release 25 (Twenty Five)",
+ "network_docker0": "172.17.0.0",
+ "network_lo": "127.0.0.0",
+ "network_virbr0": "192.168.137.0",
+ "network_virbr1": "192.168.121.0",
+ "network_wlp4s0": "192.168.1.0",
+ "lsbdistid": "Fedora",
+ "selinux": true,
+ "selinux_enforced": false,
+ "selinux_policyversion": "30",
+ "selinux_current_mode": "permissive",
+ "selinux_config_mode": "permissive",
+ "selinux_config_policy": "targeted",
+ "hostname": "myhostname",
+ "osfamily": "RedHat",
+ "kernelmajversion": "4.9",
+ "blockdevice_sr0_size": 1073741312,
+ "blockdevice_sr0_vendor": "MATSHITA",
+ "blockdevice_sr0_model": "DVD-RAM UJ8E2",
+ "blockdevice_sda_size": 256060514304,
+ "blockdevice_sda_vendor": "ATA",
+ "blockdevice_sda_model": "SAMSUNG MZ7TD256",
+ "blockdevices": "sda,sr0",
+ "uptime_hours": 432,
+ "kernelversion": "4.9.14"
+}
+'''
+
+
+class TestFacterCollector(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'facter']
+ valid_subsets = ['facter']
+ fact_namespace = 'ansible_facter'
+ collector_class = FacterFactCollector
+
+ def _mock_module(self):
+ mock_module = Mock()
+ mock_module.params = {'gather_subset': self.gather_subset,
+ 'gather_timeout': 10,
+ 'filter': '*'}
+ mock_module.get_bin_path = Mock(return_value='/not/actually/facter')
+ mock_module.run_command = Mock(return_value=(0, facter_json_output, ''))
+ return mock_module
+
+ @patch('ansible.module_utils.facts.other.facter.FacterFactCollector.get_facter_output')
+ def test_bogus_json(self, mock_get_facter_output):
+ module = self._mock_module()
+
+ # bogus json
+ mock_get_facter_output.return_value = '{'
+
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module)
+
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict, {})
+
+ @patch('ansible.module_utils.facts.other.facter.FacterFactCollector.run_facter')
+ def test_facter_non_zero_return_code(self, mock_run_facter):
+ module = self._mock_module()
+
+ # bogus json
+ mock_run_facter.return_value = (1, '{}', '')
+
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module)
+
+ self.assertIsInstance(facts_dict, dict)
+
+ # This assumes no 'facter' entry at all is correct
+ self.assertNotIn('facter', facts_dict)
+ self.assertEqual(facts_dict, {})
diff --git a/test/units/module_utils/facts/other/test_ohai.py b/test/units/module_utils/facts/other/test_ohai.py
new file mode 100644
index 0000000..42a72d9
--- /dev/null
+++ b/test/units/module_utils/facts/other/test_ohai.py
@@ -0,0 +1,6768 @@
+# unit tests for ansible ohai fact collector
+# -*- coding: utf-8 -*-
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat.mock import Mock, patch
+
+from .. base import BaseFactsTest
+
+from ansible.module_utils.facts.other.ohai import OhaiFactCollector
+
+ohai_json_output = r'''
+{
+ "kernel": {
+ "name": "Linux",
+ "release": "4.9.14-200.fc25.x86_64",
+ "version": "#1 SMP Mon Mar 13 19:26:40 UTC 2017",
+ "machine": "x86_64",
+ "processor": "x86_64",
+ "os": "GNU/Linux",
+ "modules": {
+ "binfmt_misc": {
+ "size": "20480",
+ "refcount": "1"
+ },
+ "veth": {
+ "size": "16384",
+ "refcount": "0"
+ },
+ "xfs": {
+ "size": "1200128",
+ "refcount": "1"
+ },
+ "xt_addrtype": {
+ "size": "16384",
+ "refcount": "2"
+ },
+ "br_netfilter": {
+ "size": "24576",
+ "refcount": "0"
+ },
+ "dm_thin_pool": {
+ "size": "65536",
+ "refcount": "2"
+ },
+ "dm_persistent_data": {
+ "size": "69632",
+ "refcount": "1"
+ },
+ "dm_bio_prison": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "libcrc32c": {
+ "size": "16384",
+ "refcount": "2"
+ },
+ "rfcomm": {
+ "size": "77824",
+ "refcount": "14",
+ "version": "1.11"
+ },
+ "fuse": {
+ "size": "102400",
+ "refcount": "3"
+ },
+ "ccm": {
+ "size": "20480",
+ "refcount": "2"
+ },
+ "xt_CHECKSUM": {
+ "size": "16384",
+ "refcount": "2"
+ },
+ "iptable_mangle": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "ipt_MASQUERADE": {
+ "size": "16384",
+ "refcount": "7"
+ },
+ "nf_nat_masquerade_ipv4": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "iptable_nat": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "nf_nat_ipv4": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "nf_nat": {
+ "size": "28672",
+ "refcount": "2"
+ },
+ "nf_conntrack_ipv4": {
+ "size": "16384",
+ "refcount": "4"
+ },
+ "nf_defrag_ipv4": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "xt_conntrack": {
+ "size": "16384",
+ "refcount": "3"
+ },
+ "nf_conntrack": {
+ "size": "106496",
+ "refcount": "5"
+ },
+ "ip6t_REJECT": {
+ "size": "16384",
+ "refcount": "2"
+ },
+ "nf_reject_ipv6": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "tun": {
+ "size": "28672",
+ "refcount": "4"
+ },
+ "bridge": {
+ "size": "135168",
+ "refcount": "1",
+ "version": "2.3"
+ },
+ "stp": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "llc": {
+ "size": "16384",
+ "refcount": "2"
+ },
+ "ebtable_filter": {
+ "size": "16384",
+ "refcount": "0"
+ },
+ "ebtables": {
+ "size": "36864",
+ "refcount": "1"
+ },
+ "ip6table_filter": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "ip6_tables": {
+ "size": "28672",
+ "refcount": "1"
+ },
+ "cmac": {
+ "size": "16384",
+ "refcount": "3"
+ },
+ "uhid": {
+ "size": "20480",
+ "refcount": "2"
+ },
+ "bnep": {
+ "size": "20480",
+ "refcount": "2",
+ "version": "1.3"
+ },
+ "btrfs": {
+ "size": "1056768",
+ "refcount": "1"
+ },
+ "xor": {
+ "size": "24576",
+ "refcount": "1"
+ },
+ "raid6_pq": {
+ "size": "106496",
+ "refcount": "1"
+ },
+ "loop": {
+ "size": "28672",
+ "refcount": "6"
+ },
+ "arc4": {
+ "size": "16384",
+ "refcount": "2"
+ },
+ "snd_hda_codec_hdmi": {
+ "size": "45056",
+ "refcount": "1"
+ },
+ "intel_rapl": {
+ "size": "20480",
+ "refcount": "0"
+ },
+ "x86_pkg_temp_thermal": {
+ "size": "16384",
+ "refcount": "0"
+ },
+ "intel_powerclamp": {
+ "size": "16384",
+ "refcount": "0"
+ },
+ "coretemp": {
+ "size": "16384",
+ "refcount": "0"
+ },
+ "kvm_intel": {
+ "size": "192512",
+ "refcount": "0"
+ },
+ "kvm": {
+ "size": "585728",
+ "refcount": "1"
+ },
+ "irqbypass": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "crct10dif_pclmul": {
+ "size": "16384",
+ "refcount": "0"
+ },
+ "crc32_pclmul": {
+ "size": "16384",
+ "refcount": "0"
+ },
+ "iTCO_wdt": {
+ "size": "16384",
+ "refcount": "0",
+ "version": "1.11"
+ },
+ "ghash_clmulni_intel": {
+ "size": "16384",
+ "refcount": "0"
+ },
+ "mei_wdt": {
+ "size": "16384",
+ "refcount": "0"
+ },
+ "iTCO_vendor_support": {
+ "size": "16384",
+ "refcount": "1",
+ "version": "1.04"
+ },
+ "iwlmvm": {
+ "size": "364544",
+ "refcount": "0"
+ },
+ "intel_cstate": {
+ "size": "16384",
+ "refcount": "0"
+ },
+ "uvcvideo": {
+ "size": "90112",
+ "refcount": "0",
+ "version": "1.1.1"
+ },
+ "videobuf2_vmalloc": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "intel_uncore": {
+ "size": "118784",
+ "refcount": "0"
+ },
+ "videobuf2_memops": {
+ "size": "16384",
+ "refcount": "1"
+ },
+ "videobuf2_v4l2": {
+ "size": "24576",
+ "refcount": "1"
+ },
+ "videobuf2_core": {
+ "size": "40960",
+ "refcount": "2"
+ },
+ "intel_rapl_perf": {
+ "size": "16384",
+ "refcount": "0"
+ },
+ "mac80211": {
+ "size": "749568",
+ "refcount": "1"
+ },
+ "videodev": {
+ "size": "172032",
+ "refcount": "3"
+ },
+ "snd_usb_audio": {
+ "size": "180224",
+ "refcount": "3"
+ },
+ "e1000e": {
+ "size": "249856",
+ "refcount": "0",
+ "version": "3.2.6-k"
+ }
+ }
+ },
+ "os": "linux",
+ "os_version": "4.9.14-200.fc25.x86_64",
+ "lsb": {
+ "id": "Fedora",
+ "description": "Fedora release 25 (Twenty Five)",
+ "release": "25",
+ "codename": "TwentyFive"
+ },
+ "platform": "fedora",
+ "platform_version": "25",
+ "platform_family": "fedora",
+ "packages": {
+ "ansible": {
+ "epoch": "0",
+ "version": "2.2.1.0",
+ "release": "1.fc25",
+ "installdate": "1486050042",
+ "arch": "noarch"
+ },
+ "python3": {
+ "epoch": "0",
+ "version": "3.5.3",
+ "release": "3.fc25",
+ "installdate": "1490025957",
+ "arch": "x86_64"
+ },
+ "kernel": {
+ "epoch": "0",
+ "version": "4.9.6",
+ "release": "200.fc25",
+ "installdate": "1486047522",
+ "arch": "x86_64"
+ },
+ "glibc": {
+ "epoch": "0",
+ "version": "2.24",
+ "release": "4.fc25",
+ "installdate": "1483402427",
+ "arch": "x86_64"
+ }
+ },
+ "chef_packages": {
+ ohai": {
+ "version": "13.0.0",
+ "ohai_root": "/home/some_user/.gem/ruby/gems/ohai-13.0.0/lib/ohai"
+ }
+ },
+ "dmi": {
+ "dmidecode_version": "3.0"
+ },
+ "uptime_seconds": 2509008,
+ "uptime": "29 days 00 hours 56 minutes 48 seconds",
+ "idletime_seconds": 19455087,
+ "idletime": "225 days 04 hours 11 minutes 27 seconds",
+ "memory": {
+ "swap": {
+ "cached": "262436kB",
+ "total": "8069116kB",
+ "free": "5154396kB"
+ },
+ "hugepages": {
+ "total": "0",
+ "free": "0",
+ "reserved": "0",
+ "surplus": "0"
+ },
+ "total": "16110540kB",
+ "free": "3825844kB",
+ "buffers": "377240kB",
+ "cached": "3710084kB",
+ "active": "8104320kB",
+ "inactive": "3192920kB",
+ "dirty": "812kB",
+ "writeback": "0kB",
+ "anon_pages": "7124992kB",
+ "mapped": "580700kB",
+ "slab": "622848kB",
+ "slab_reclaimable": "307300kB",
+ "slab_unreclaim": "315548kB",
+ "page_tables": "157572kB",
+ "nfs_unstable": "0kB",
+ "bounce": "0kB",
+ "commit_limit": "16124384kB",
+ "committed_as": "31345068kB",
+ "vmalloc_total": "34359738367kB",
+ "vmalloc_used": "0kB",
+ "vmalloc_chunk": "0kB",
+ "hugepage_size": "2048kB"
+ },
+ "filesystem": {
+ "by_device": {
+ "devtmpfs": {
+ "kb_size": "8044124",
+ "kb_used": "0",
+ "kb_available": "8044124",
+ "percent_used": "0%",
+ "total_inodes": "2011031",
+ "inodes_used": "629",
+ "inodes_available": "2010402",
+ "inodes_percent_used": "1%",
+ "fs_type": "devtmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "seclabel",
+ "size=8044124k",
+ "nr_inodes=2011031",
+ "mode=755"
+ ],
+ "mounts": [
+ "/dev"
+ ]
+ },
+ "tmpfs": {
+ "kb_size": "1611052",
+ "kb_used": "72",
+ "kb_available": "1610980",
+ "percent_used": "1%",
+ "total_inodes": "2013817",
+ "inodes_used": "36",
+ "inodes_available": "2013781",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "seclabel",
+ "size=1611052k",
+ "mode=700",
+ "uid=1000",
+ "gid=1000"
+ ],
+ "mounts": [
+ "/dev/shm",
+ "/run",
+ "/sys/fs/cgroup",
+ "/tmp",
+ "/run/user/0",
+ "/run/user/1000"
+ ]
+ },
+ "/dev/mapper/fedora_host--186-root": {
+ "kb_size": "51475068",
+ "kb_used": "42551284",
+ "kb_available": "6285960",
+ "percent_used": "88%",
+ "total_inodes": "3276800",
+ "inodes_used": "532908",
+ "inodes_available": "2743892",
+ "inodes_percent_used": "17%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "12312331-3449-4a6c-8179-a1feb2bca6ce",
+ "mounts": [
+ "/",
+ "/var/lib/docker/devicemapper"
+ ]
+ },
+ "/dev/sda1": {
+ "kb_size": "487652",
+ "kb_used": "126628",
+ "kb_available": "331328",
+ "percent_used": "28%",
+ "total_inodes": "128016",
+ "inodes_used": "405",
+ "inodes_available": "127611",
+ "inodes_percent_used": "1%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "12312311-ef40-4691-a3b6-438c3f9bc1c0",
+ "mounts": [
+ "/boot"
+ ]
+ },
+ "/dev/mapper/fedora_host--186-home": {
+ "kb_size": "185948124",
+ "kb_used": "105904724",
+ "kb_available": "70574680",
+ "percent_used": "61%",
+ "total_inodes": "11821056",
+ "inodes_used": "1266687",
+ "inodes_available": "10554369",
+ "inodes_percent_used": "11%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "2d3e4853-fa69-4ccf-8a6a-77b05ab0a42d",
+ "mounts": [
+ "/home"
+ ]
+ },
+ "/dev/loop0": {
+ "kb_size": "512000",
+ "kb_used": "16672",
+ "kb_available": "429056",
+ "percent_used": "4%",
+ "fs_type": "btrfs",
+ "uuid": "0f031512-ab15-497d-9abd-3a512b4a9390",
+ "mounts": [
+ "/var/lib/machines"
+ ]
+ },
+ "sysfs": {
+ "fs_type": "sysfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ],
+ "mounts": [
+ "/sys"
+ ]
+ },
+ "proc": {
+ "fs_type": "proc",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ],
+ "mounts": [
+ "/proc"
+ ]
+ },
+ "securityfs": {
+ "fs_type": "securityfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ],
+ "mounts": [
+ "/sys/kernel/security"
+ ]
+ },
+ "devpts": {
+ "fs_type": "devpts",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "noexec",
+ "relatime",
+ "seclabel",
+ "gid=5",
+ "mode=620",
+ "ptmxmode=000"
+ ],
+ "mounts": [
+ "/dev/pts"
+ ]
+ },
+ "cgroup": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "net_cls",
+ "net_prio"
+ ],
+ "mounts": [
+ "/sys/fs/cgroup/systemd",
+ "/sys/fs/cgroup/devices",
+ "/sys/fs/cgroup/cpuset",
+ "/sys/fs/cgroup/perf_event",
+ "/sys/fs/cgroup/hugetlb",
+ "/sys/fs/cgroup/cpu,cpuacct",
+ "/sys/fs/cgroup/blkio",
+ "/sys/fs/cgroup/freezer",
+ "/sys/fs/cgroup/memory",
+ "/sys/fs/cgroup/pids",
+ "/sys/fs/cgroup/net_cls,net_prio"
+ ]
+ },
+ "pstore": {
+ "fs_type": "pstore",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ],
+ "mounts": [
+ "/sys/fs/pstore"
+ ]
+ },
+ "configfs": {
+ "fs_type": "configfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "mounts": [
+ "/sys/kernel/config"
+ ]
+ },
+ "selinuxfs": {
+ "fs_type": "selinuxfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "mounts": [
+ "/sys/fs/selinux"
+ ]
+ },
+ "debugfs": {
+ "fs_type": "debugfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "mounts": [
+ "/sys/kernel/debug"
+ ]
+ },
+ "hugetlbfs": {
+ "fs_type": "hugetlbfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "mounts": [
+ "/dev/hugepages"
+ ]
+ },
+ "mqueue": {
+ "fs_type": "mqueue",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "mounts": [
+ "/dev/mqueue"
+ ]
+ },
+ "systemd-1": {
+ "fs_type": "autofs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "fd=40",
+ "pgrp=1",
+ "timeout=0",
+ "minproto=5",
+ "maxproto=5",
+ "direct",
+ "pipe_ino=17610"
+ ],
+ "mounts": [
+ "/proc/sys/fs/binfmt_misc"
+ ]
+ },
+ "/var/lib/machines.raw": {
+ "fs_type": "btrfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "space_cache",
+ "subvolid=5",
+ "subvol=/"
+ ],
+ "mounts": [
+ "/var/lib/machines"
+ ]
+ },
+ "fusectl": {
+ "fs_type": "fusectl",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "mounts": [
+ "/sys/fs/fuse/connections"
+ ]
+ },
+ "gvfsd-fuse": {
+ "fs_type": "fuse.gvfsd-fuse",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "user_id=1000",
+ "group_id=1000"
+ ],
+ "mounts": [
+ "/run/user/1000/gvfs"
+ ]
+ },
+ "binfmt_misc": {
+ "fs_type": "binfmt_misc",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "mounts": [
+ "/proc/sys/fs/binfmt_misc"
+ ]
+ },
+ "/dev/mapper/docker-253:1-1180487-0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8": {
+ "fs_type": "xfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "context=\"system_u:object_r:container_file_t:s0:c523",
+ "c681\"",
+ "nouuid",
+ "attr2",
+ "inode64",
+ "logbsize=64k",
+ "sunit=128",
+ "swidth=128",
+ "noquota"
+ ],
+ "uuid": "00e2aa25-20d8-4ad7-b3a5-c501f2f4c123",
+ "mounts": [
+ "/var/lib/docker/devicemapper/mnt/0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8"
+ ]
+ },
+ "shm": {
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "context=\"system_u:object_r:container_file_t:s0:c523",
+ "c681\"",
+ "size=65536k"
+ ],
+ "mounts": [
+ "/var/lib/docker/containers/426e513ed508a451e3f70440eed040761f81529e4bc4240e7522d331f3f3bc12/shm"
+ ]
+ },
+ "nsfs": {
+ "fs_type": "nsfs",
+ "mount_options": [
+ "rw"
+ ],
+ "mounts": [
+ "/run/docker/netns/1ce89fd79f3d"
+ ]
+ },
+ "tracefs": {
+ "fs_type": "tracefs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "mounts": [
+ "/sys/kernel/debug/tracing"
+ ]
+ },
+ "/dev/loop1": {
+ "fs_type": "xfs",
+ "uuid": "00e2aa25-20d8-4ad7-b3a5-c501f2f4c123",
+ "mounts": [
+
+ ]
+ },
+ "/dev/mapper/docker-253:1-1180487-pool": {
+ "mounts": [
+
+ ]
+ },
+ "/dev/sr0": {
+ "mounts": [
+
+ ]
+ },
+ "/dev/loop2": {
+ "mounts": [
+
+ ]
+ },
+ "/dev/sda": {
+ "mounts": [
+
+ ]
+ },
+ "/dev/sda2": {
+ "fs_type": "LVM2_member",
+ "uuid": "66Ojcd-ULtu-1cZa-Tywo-mx0d-RF4O-ysA9jK",
+ "mounts": [
+
+ ]
+ },
+ "/dev/mapper/fedora_host--186-swap": {
+ "fs_type": "swap",
+ "uuid": "eae6059d-2fbe-4d1c-920d-a80bbeb1ac6d",
+ "mounts": [
+
+ ]
+ }
+ },
+ "by_mountpoint": {
+ "/dev": {
+ "kb_size": "8044124",
+ "kb_used": "0",
+ "kb_available": "8044124",
+ "percent_used": "0%",
+ "total_inodes": "2011031",
+ "inodes_used": "629",
+ "inodes_available": "2010402",
+ "inodes_percent_used": "1%",
+ "fs_type": "devtmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "seclabel",
+ "size=8044124k",
+ "nr_inodes=2011031",
+ "mode=755"
+ ],
+ "devices": [
+ "devtmpfs"
+ ]
+ },
+ "/dev/shm": {
+ "kb_size": "8055268",
+ "kb_used": "96036",
+ "kb_available": "7959232",
+ "percent_used": "2%",
+ "total_inodes": "2013817",
+ "inodes_used": "217",
+ "inodes_available": "2013600",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/run": {
+ "kb_size": "8055268",
+ "kb_used": "2280",
+ "kb_available": "8052988",
+ "percent_used": "1%",
+ "total_inodes": "2013817",
+ "inodes_used": "1070",
+ "inodes_available": "2012747",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel",
+ "mode=755"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/sys/fs/cgroup": {
+ "kb_size": "8055268",
+ "kb_used": "0",
+ "kb_available": "8055268",
+ "percent_used": "0%",
+ "total_inodes": "2013817",
+ "inodes_used": "16",
+ "inodes_available": "2013801",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "ro",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "seclabel",
+ "mode=755"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/": {
+ "kb_size": "51475068",
+ "kb_used": "42551284",
+ "kb_available": "6285960",
+ "percent_used": "88%",
+ "total_inodes": "3276800",
+ "inodes_used": "532908",
+ "inodes_available": "2743892",
+ "inodes_percent_used": "17%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "d34cf5e3-3449-4a6c-8179-a1feb2bca6ce",
+ "devices": [
+ "/dev/mapper/fedora_host--186-root"
+ ]
+ },
+ "/tmp": {
+ "kb_size": "8055268",
+ "kb_used": "848396",
+ "kb_available": "7206872",
+ "percent_used": "11%",
+ "total_inodes": "2013817",
+ "inodes_used": "1353",
+ "inodes_available": "2012464",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/boot": {
+ "kb_size": "487652",
+ "kb_used": "126628",
+ "kb_available": "331328",
+ "percent_used": "28%",
+ "total_inodes": "128016",
+ "inodes_used": "405",
+ "inodes_available": "127611",
+ "inodes_percent_used": "1%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "32caaec3-ef40-4691-a3b6-438c3f9bc1c0",
+ "devices": [
+ "/dev/sda1"
+ ]
+ },
+ "/home": {
+ "kb_size": "185948124",
+ "kb_used": "105904724",
+ "kb_available": "70574680",
+ "percent_used": "61%",
+ "total_inodes": "11821056",
+ "inodes_used": "1266687",
+ "inodes_available": "10554369",
+ "inodes_percent_used": "11%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "2d3e4853-fa69-4ccf-8a6a-77b05ab0a42d",
+ "devices": [
+ "/dev/mapper/fedora_host--186-home"
+ ]
+ },
+ "/var/lib/machines": {
+ "kb_size": "512000",
+ "kb_used": "16672",
+ "kb_available": "429056",
+ "percent_used": "4%",
+ "fs_type": "btrfs",
+ "uuid": "0f031512-ab15-497d-9abd-3a512b4a9390",
+ "devices": [
+ "/dev/loop0",
+ "/var/lib/machines.raw"
+ ],
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "space_cache",
+ "subvolid=5",
+ "subvol=/"
+ ]
+ },
+ "/run/user/0": {
+ "kb_size": "1611052",
+ "kb_used": "0",
+ "kb_available": "1611052",
+ "percent_used": "0%",
+ "total_inodes": "2013817",
+ "inodes_used": "7",
+ "inodes_available": "2013810",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "seclabel",
+ "size=1611052k",
+ "mode=700"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/run/user/1000": {
+ "kb_size": "1611052",
+ "kb_used": "72",
+ "kb_available": "1610980",
+ "percent_used": "1%",
+ "total_inodes": "2013817",
+ "inodes_used": "36",
+ "inodes_available": "2013781",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "seclabel",
+ "size=1611052k",
+ "mode=700",
+ "uid=1000",
+ "gid=1000"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/sys": {
+ "fs_type": "sysfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ],
+ "devices": [
+ "sysfs"
+ ]
+ },
+ "/proc": {
+ "fs_type": "proc",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ],
+ "devices": [
+ "proc"
+ ]
+ },
+ "/sys/kernel/security": {
+ "fs_type": "securityfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ],
+ "devices": [
+ "securityfs"
+ ]
+ },
+ "/dev/pts": {
+ "fs_type": "devpts",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "noexec",
+ "relatime",
+ "seclabel",
+ "gid=5",
+ "mode=620",
+ "ptmxmode=000"
+ ],
+ "devices": [
+ "devpts"
+ ]
+ },
+ "/sys/fs/cgroup/systemd": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "xattr",
+ "release_agent=/usr/lib/systemd/systemd-cgroups-agent",
+ "name=systemd"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/pstore": {
+ "fs_type": "pstore",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ],
+ "devices": [
+ "pstore"
+ ]
+ },
+ "/sys/fs/cgroup/devices": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "devices"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/cpuset": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "cpuset"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/perf_event": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "perf_event"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/hugetlb": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "hugetlb"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/cpu,cpuacct": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "cpu",
+ "cpuacct"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/blkio": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "blkio"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/freezer": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "freezer"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/memory": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "memory"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/pids": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "pids"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/net_cls,net_prio": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "net_cls",
+ "net_prio"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/kernel/config": {
+ "fs_type": "configfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "devices": [
+ "configfs"
+ ]
+ },
+ "/sys/fs/selinux": {
+ "fs_type": "selinuxfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "devices": [
+ "selinuxfs"
+ ]
+ },
+ "/sys/kernel/debug": {
+ "fs_type": "debugfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "devices": [
+ "debugfs"
+ ]
+ },
+ "/dev/hugepages": {
+ "fs_type": "hugetlbfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "devices": [
+ "hugetlbfs"
+ ]
+ },
+ "/dev/mqueue": {
+ "fs_type": "mqueue",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "devices": [
+ "mqueue"
+ ]
+ },
+ "/proc/sys/fs/binfmt_misc": {
+ "fs_type": "binfmt_misc",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "devices": [
+ "systemd-1",
+ "binfmt_misc"
+ ]
+ },
+ "/sys/fs/fuse/connections": {
+ "fs_type": "fusectl",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "devices": [
+ "fusectl"
+ ]
+ },
+ "/run/user/1000/gvfs": {
+ "fs_type": "fuse.gvfsd-fuse",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "user_id=1000",
+ "group_id=1000"
+ ],
+ "devices": [
+ "gvfsd-fuse"
+ ]
+ },
+ "/var/lib/docker/devicemapper": {
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "d34cf5e3-3449-4a6c-8179-a1feb2bca6ce",
+ "devices": [
+ "/dev/mapper/fedora_host--186-root"
+ ]
+ },
+ "/var/lib/docker/devicemapper/mnt/0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8": {
+ "fs_type": "xfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "context=\"system_u:object_r:container_file_t:s0:c523",
+ "c681\"",
+ "nouuid",
+ "attr2",
+ "inode64",
+ "logbsize=64k",
+ "sunit=128",
+ "swidth=128",
+ "noquota"
+ ],
+ "uuid": "00e2aa25-20d8-4ad7-b3a5-c501f2f4c123",
+ "devices": [
+ "/dev/mapper/docker-253:1-1180487-0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8"
+ ]
+ },
+ "/var/lib/docker/containers/426e513ed508a451e3f70440eed040761f81529e4bc4240e7522d331f3f3bc12/shm": {
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "context=\"system_u:object_r:container_file_t:s0:c523",
+ "c681\"",
+ "size=65536k"
+ ],
+ "devices": [
+ "shm"
+ ]
+ },
+ "/run/docker/netns/1ce89fd79f3d": {
+ "fs_type": "nsfs",
+ "mount_options": [
+ "rw"
+ ],
+ "devices": [
+ "nsfs"
+ ]
+ },
+ "/sys/kernel/debug/tracing": {
+ "fs_type": "tracefs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "devices": [
+ "tracefs"
+ ]
+ }
+ },
+ "by_pair": {
+ "devtmpfs,/dev": {
+ "device": "devtmpfs",
+ "kb_size": "8044124",
+ "kb_used": "0",
+ "kb_available": "8044124",
+ "percent_used": "0%",
+ "mount": "/dev",
+ "total_inodes": "2011031",
+ "inodes_used": "629",
+ "inodes_available": "2010402",
+ "inodes_percent_used": "1%",
+ "fs_type": "devtmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "seclabel",
+ "size=8044124k",
+ "nr_inodes=2011031",
+ "mode=755"
+ ]
+ },
+ "tmpfs,/dev/shm": {
+ "device": "tmpfs",
+ "kb_size": "8055268",
+ "kb_used": "96036",
+ "kb_available": "7959232",
+ "percent_used": "2%",
+ "mount": "/dev/shm",
+ "total_inodes": "2013817",
+ "inodes_used": "217",
+ "inodes_available": "2013600",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel"
+ ]
+ },
+ "tmpfs,/run": {
+ "device": "tmpfs",
+ "kb_size": "8055268",
+ "kb_used": "2280",
+ "kb_available": "8052988",
+ "percent_used": "1%",
+ "mount": "/run",
+ "total_inodes": "2013817",
+ "inodes_used": "1070",
+ "inodes_available": "2012747",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel",
+ "mode=755"
+ ]
+ },
+ "tmpfs,/sys/fs/cgroup": {
+ "device": "tmpfs",
+ "kb_size": "8055268",
+ "kb_used": "0",
+ "kb_available": "8055268",
+ "percent_used": "0%",
+ "mount": "/sys/fs/cgroup",
+ "total_inodes": "2013817",
+ "inodes_used": "16",
+ "inodes_available": "2013801",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "ro",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "seclabel",
+ "mode=755"
+ ]
+ },
+ "/dev/mapper/fedora_host--186-root,/": {
+ "device": "/dev/mapper/fedora_host--186-root",
+ "kb_size": "51475068",
+ "kb_used": "42551284",
+ "kb_available": "6285960",
+ "percent_used": "88%",
+ "mount": "/",
+ "total_inodes": "3276800",
+ "inodes_used": "532908",
+ "inodes_available": "2743892",
+ "inodes_percent_used": "17%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "d34cf5e3-3449-4a6c-8179-a1feb2bca6ce"
+ },
+ "tmpfs,/tmp": {
+ "device": "tmpfs",
+ "kb_size": "8055268",
+ "kb_used": "848396",
+ "kb_available": "7206872",
+ "percent_used": "11%",
+ "mount": "/tmp",
+ "total_inodes": "2013817",
+ "inodes_used": "1353",
+ "inodes_available": "2012464",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel"
+ ]
+ },
+ "/dev/sda1,/boot": {
+ "device": "/dev/sda1",
+ "kb_size": "487652",
+ "kb_used": "126628",
+ "kb_available": "331328",
+ "percent_used": "28%",
+ "mount": "/boot",
+ "total_inodes": "128016",
+ "inodes_used": "405",
+ "inodes_available": "127611",
+ "inodes_percent_used": "1%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "32caaec3-ef40-4691-a3b6-438c3f9bc1c0"
+ },
+ "/dev/mapper/fedora_host--186-home,/home": {
+ "device": "/dev/mapper/fedora_host--186-home",
+ "kb_size": "185948124",
+ "kb_used": "105904724",
+ "kb_available": "70574680",
+ "percent_used": "61%",
+ "mount": "/home",
+ "total_inodes": "11821056",
+ "inodes_used": "1266687",
+ "inodes_available": "10554369",
+ "inodes_percent_used": "11%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "2d3e4853-fa69-4ccf-8a6a-77b05ab0a42d"
+ },
+ "/dev/loop0,/var/lib/machines": {
+ "device": "/dev/loop0",
+ "kb_size": "512000",
+ "kb_used": "16672",
+ "kb_available": "429056",
+ "percent_used": "4%",
+ "mount": "/var/lib/machines",
+ "fs_type": "btrfs",
+ "uuid": "0f031512-ab15-497d-9abd-3a512b4a9390"
+ },
+ "tmpfs,/run/user/0": {
+ "device": "tmpfs",
+ "kb_size": "1611052",
+ "kb_used": "0",
+ "kb_available": "1611052",
+ "percent_used": "0%",
+ "mount": "/run/user/0",
+ "total_inodes": "2013817",
+ "inodes_used": "7",
+ "inodes_available": "2013810",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "seclabel",
+ "size=1611052k",
+ "mode=700"
+ ]
+ },
+ "tmpfs,/run/user/1000": {
+ "device": "tmpfs",
+ "kb_size": "1611052",
+ "kb_used": "72",
+ "kb_available": "1610980",
+ "percent_used": "1%",
+ "mount": "/run/user/1000",
+ "total_inodes": "2013817",
+ "inodes_used": "36",
+ "inodes_available": "2013781",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "seclabel",
+ "size=1611052k",
+ "mode=700",
+ "uid=1000",
+ "gid=1000"
+ ]
+ },
+ "sysfs,/sys": {
+ "device": "sysfs",
+ "mount": "/sys",
+ "fs_type": "sysfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ]
+ },
+ "proc,/proc": {
+ "device": "proc",
+ "mount": "/proc",
+ "fs_type": "proc",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ]
+ },
+ "securityfs,/sys/kernel/security": {
+ "device": "securityfs",
+ "mount": "/sys/kernel/security",
+ "fs_type": "securityfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ]
+ },
+ "devpts,/dev/pts": {
+ "device": "devpts",
+ "mount": "/dev/pts",
+ "fs_type": "devpts",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "noexec",
+ "relatime",
+ "seclabel",
+ "gid=5",
+ "mode=620",
+ "ptmxmode=000"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/systemd": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/systemd",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "xattr",
+ "release_agent=/usr/lib/systemd/systemd-cgroups-agent",
+ "name=systemd"
+ ]
+ },
+ "pstore,/sys/fs/pstore": {
+ "device": "pstore",
+ "mount": "/sys/fs/pstore",
+ "fs_type": "pstore",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/devices": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/devices",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "devices"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/cpuset": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/cpuset",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "cpuset"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/perf_event": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/perf_event",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "perf_event"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/hugetlb": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/hugetlb",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "hugetlb"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/cpu,cpuacct": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/cpu,cpuacct",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "cpu",
+ "cpuacct"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/blkio": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/blkio",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "blkio"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/freezer": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/freezer",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "freezer"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/memory": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/memory",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "memory"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/pids": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/pids",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "pids"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/net_cls,net_prio": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/net_cls,net_prio",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "net_cls",
+ "net_prio"
+ ]
+ },
+ "configfs,/sys/kernel/config": {
+ "device": "configfs",
+ "mount": "/sys/kernel/config",
+ "fs_type": "configfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ]
+ },
+ "selinuxfs,/sys/fs/selinux": {
+ "device": "selinuxfs",
+ "mount": "/sys/fs/selinux",
+ "fs_type": "selinuxfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ]
+ },
+ "debugfs,/sys/kernel/debug": {
+ "device": "debugfs",
+ "mount": "/sys/kernel/debug",
+ "fs_type": "debugfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ]
+ },
+ "hugetlbfs,/dev/hugepages": {
+ "device": "hugetlbfs",
+ "mount": "/dev/hugepages",
+ "fs_type": "hugetlbfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ]
+ },
+ "mqueue,/dev/mqueue": {
+ "device": "mqueue",
+ "mount": "/dev/mqueue",
+ "fs_type": "mqueue",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ]
+ },
+ "systemd-1,/proc/sys/fs/binfmt_misc": {
+ "device": "systemd-1",
+ "mount": "/proc/sys/fs/binfmt_misc",
+ "fs_type": "autofs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "fd=40",
+ "pgrp=1",
+ "timeout=0",
+ "minproto=5",
+ "maxproto=5",
+ "direct",
+ "pipe_ino=17610"
+ ]
+ },
+ "/var/lib/machines.raw,/var/lib/machines": {
+ "device": "/var/lib/machines.raw",
+ "mount": "/var/lib/machines",
+ "fs_type": "btrfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "space_cache",
+ "subvolid=5",
+ "subvol=/"
+ ]
+ },
+ "fusectl,/sys/fs/fuse/connections": {
+ "device": "fusectl",
+ "mount": "/sys/fs/fuse/connections",
+ "fs_type": "fusectl",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ]
+ },
+ "gvfsd-fuse,/run/user/1000/gvfs": {
+ "device": "gvfsd-fuse",
+ "mount": "/run/user/1000/gvfs",
+ "fs_type": "fuse.gvfsd-fuse",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "user_id=1000",
+ "group_id=1000"
+ ]
+ },
+ "/dev/mapper/fedora_host--186-root,/var/lib/docker/devicemapper": {
+ "device": "/dev/mapper/fedora_host--186-root",
+ "mount": "/var/lib/docker/devicemapper",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "d34cf5e3-3449-4a6c-8179-a1feb2bca6ce"
+ },
+ "binfmt_misc,/proc/sys/fs/binfmt_misc": {
+ "device": "binfmt_misc",
+ "mount": "/proc/sys/fs/binfmt_misc",
+ "fs_type": "binfmt_misc",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ]
+ },
+ "/dev/mapper/docker-253:1-1180487-0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8,/var/lib/docker/devicemapper/mnt/0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8": {
+ "device": "/dev/mapper/docker-253:1-1180487-0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8",
+ "mount": "/var/lib/docker/devicemapper/mnt/0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8",
+ "fs_type": "xfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "context=\"system_u:object_r:container_file_t:s0:c523",
+ "c681\"",
+ "nouuid",
+ "attr2",
+ "inode64",
+ "logbsize=64k",
+ "sunit=128",
+ "swidth=128",
+ "noquota"
+ ],
+ "uuid": "00e2aa25-20d8-4ad7-b3a5-c501f2f4c123"
+ },
+ "shm,/var/lib/docker/containers/426e513ed508a451e3f70440eed040761f81529e4bc4240e7522d331f3f3bc12/shm": {
+ "device": "shm",
+ "mount": "/var/lib/docker/containers/426e513ed508a451e3f70440eed040761f81529e4bc4240e7522d331f3f3bc12/shm",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "context=\"system_u:object_r:container_file_t:s0:c523",
+ "c681\"",
+ "size=65536k"
+ ]
+ },
+ "nsfs,/run/docker/netns/1ce89fd79f3d": {
+ "device": "nsfs",
+ "mount": "/run/docker/netns/1ce89fd79f3d",
+ "fs_type": "nsfs",
+ "mount_options": [
+ "rw"
+ ]
+ },
+ "tracefs,/sys/kernel/debug/tracing": {
+ "device": "tracefs",
+ "mount": "/sys/kernel/debug/tracing",
+ "fs_type": "tracefs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ]
+ },
+ "/dev/loop1,": {
+ "device": "/dev/loop1",
+ "fs_type": "xfs",
+ "uuid": "00e2aa25-20d8-4ad7-b3a5-c501f2f4c123"
+ },
+ "/dev/mapper/docker-253:1-1180487-pool,": {
+ "device": "/dev/mapper/docker-253:1-1180487-pool"
+ },
+ "/dev/sr0,": {
+ "device": "/dev/sr0"
+ },
+ "/dev/loop2,": {
+ "device": "/dev/loop2"
+ },
+ "/dev/sda,": {
+ "device": "/dev/sda"
+ },
+ "/dev/sda2,": {
+ "device": "/dev/sda2",
+ "fs_type": "LVM2_member",
+ "uuid": "66Ojcd-ULtu-1cZa-Tywo-mx0d-RF4O-ysA9jK"
+ },
+ "/dev/mapper/fedora_host--186-swap,": {
+ "device": "/dev/mapper/fedora_host--186-swap",
+ "fs_type": "swap",
+ "uuid": "eae6059d-2fbe-4d1c-920d-a80bbeb1ac6d"
+ }
+ }
+ },
+ "filesystem2": {
+ "by_device": {
+ "devtmpfs": {
+ "kb_size": "8044124",
+ "kb_used": "0",
+ "kb_available": "8044124",
+ "percent_used": "0%",
+ "total_inodes": "2011031",
+ "inodes_used": "629",
+ "inodes_available": "2010402",
+ "inodes_percent_used": "1%",
+ "fs_type": "devtmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "seclabel",
+ "size=8044124k",
+ "nr_inodes=2011031",
+ "mode=755"
+ ],
+ "mounts": [
+ "/dev"
+ ]
+ },
+ "tmpfs": {
+ "kb_size": "1611052",
+ "kb_used": "72",
+ "kb_available": "1610980",
+ "percent_used": "1%",
+ "total_inodes": "2013817",
+ "inodes_used": "36",
+ "inodes_available": "2013781",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "seclabel",
+ "size=1611052k",
+ "mode=700",
+ "uid=1000",
+ "gid=1000"
+ ],
+ "mounts": [
+ "/dev/shm",
+ "/run",
+ "/sys/fs/cgroup",
+ "/tmp",
+ "/run/user/0",
+ "/run/user/1000"
+ ]
+ },
+ "/dev/mapper/fedora_host--186-root": {
+ "kb_size": "51475068",
+ "kb_used": "42551284",
+ "kb_available": "6285960",
+ "percent_used": "88%",
+ "total_inodes": "3276800",
+ "inodes_used": "532908",
+ "inodes_available": "2743892",
+ "inodes_percent_used": "17%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "d34cf5e3-3449-4a6c-8179-a1feb2bca6ce",
+ "mounts": [
+ "/",
+ "/var/lib/docker/devicemapper"
+ ]
+ },
+ "/dev/sda1": {
+ "kb_size": "487652",
+ "kb_used": "126628",
+ "kb_available": "331328",
+ "percent_used": "28%",
+ "total_inodes": "128016",
+ "inodes_used": "405",
+ "inodes_available": "127611",
+ "inodes_percent_used": "1%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "32caaec3-ef40-4691-a3b6-438c3f9bc1c0",
+ "mounts": [
+ "/boot"
+ ]
+ },
+ "/dev/mapper/fedora_host--186-home": {
+ "kb_size": "185948124",
+ "kb_used": "105904724",
+ "kb_available": "70574680",
+ "percent_used": "61%",
+ "total_inodes": "11821056",
+ "inodes_used": "1266687",
+ "inodes_available": "10554369",
+ "inodes_percent_used": "11%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "2d3e4853-fa69-4ccf-8a6a-77b05ab0a42d",
+ "mounts": [
+ "/home"
+ ]
+ },
+ "/dev/loop0": {
+ "kb_size": "512000",
+ "kb_used": "16672",
+ "kb_available": "429056",
+ "percent_used": "4%",
+ "fs_type": "btrfs",
+ "uuid": "0f031512-ab15-497d-9abd-3a512b4a9390",
+ "mounts": [
+ "/var/lib/machines"
+ ]
+ },
+ "sysfs": {
+ "fs_type": "sysfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ],
+ "mounts": [
+ "/sys"
+ ]
+ },
+ "proc": {
+ "fs_type": "proc",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ],
+ "mounts": [
+ "/proc"
+ ]
+ },
+ "securityfs": {
+ "fs_type": "securityfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ],
+ "mounts": [
+ "/sys/kernel/security"
+ ]
+ },
+ "devpts": {
+ "fs_type": "devpts",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "noexec",
+ "relatime",
+ "seclabel",
+ "gid=5",
+ "mode=620",
+ "ptmxmode=000"
+ ],
+ "mounts": [
+ "/dev/pts"
+ ]
+ },
+ "cgroup": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "net_cls",
+ "net_prio"
+ ],
+ "mounts": [
+ "/sys/fs/cgroup/systemd",
+ "/sys/fs/cgroup/devices",
+ "/sys/fs/cgroup/cpuset",
+ "/sys/fs/cgroup/perf_event",
+ "/sys/fs/cgroup/hugetlb",
+ "/sys/fs/cgroup/cpu,cpuacct",
+ "/sys/fs/cgroup/blkio",
+ "/sys/fs/cgroup/freezer",
+ "/sys/fs/cgroup/memory",
+ "/sys/fs/cgroup/pids",
+ "/sys/fs/cgroup/net_cls,net_prio"
+ ]
+ },
+ "pstore": {
+ "fs_type": "pstore",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ],
+ "mounts": [
+ "/sys/fs/pstore"
+ ]
+ },
+ "configfs": {
+ "fs_type": "configfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "mounts": [
+ "/sys/kernel/config"
+ ]
+ },
+ "selinuxfs": {
+ "fs_type": "selinuxfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "mounts": [
+ "/sys/fs/selinux"
+ ]
+ },
+ "debugfs": {
+ "fs_type": "debugfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "mounts": [
+ "/sys/kernel/debug"
+ ]
+ },
+ "hugetlbfs": {
+ "fs_type": "hugetlbfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "mounts": [
+ "/dev/hugepages"
+ ]
+ },
+ "mqueue": {
+ "fs_type": "mqueue",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "mounts": [
+ "/dev/mqueue"
+ ]
+ },
+ "systemd-1": {
+ "fs_type": "autofs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "fd=40",
+ "pgrp=1",
+ "timeout=0",
+ "minproto=5",
+ "maxproto=5",
+ "direct",
+ "pipe_ino=17610"
+ ],
+ "mounts": [
+ "/proc/sys/fs/binfmt_misc"
+ ]
+ },
+ "/var/lib/machines.raw": {
+ "fs_type": "btrfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "space_cache",
+ "subvolid=5",
+ "subvol=/"
+ ],
+ "mounts": [
+ "/var/lib/machines"
+ ]
+ },
+ "fusectl": {
+ "fs_type": "fusectl",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "mounts": [
+ "/sys/fs/fuse/connections"
+ ]
+ },
+ "gvfsd-fuse": {
+ "fs_type": "fuse.gvfsd-fuse",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "user_id=1000",
+ "group_id=1000"
+ ],
+ "mounts": [
+ "/run/user/1000/gvfs"
+ ]
+ },
+ "binfmt_misc": {
+ "fs_type": "binfmt_misc",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "mounts": [
+ "/proc/sys/fs/binfmt_misc"
+ ]
+ },
+ "/dev/mapper/docker-253:1-1180487-0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8": {
+ "fs_type": "xfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "context=\"system_u:object_r:container_file_t:s0:c523",
+ "c681\"",
+ "nouuid",
+ "attr2",
+ "inode64",
+ "logbsize=64k",
+ "sunit=128",
+ "swidth=128",
+ "noquota"
+ ],
+ "uuid": "00e2aa25-20d8-4ad7-b3a5-c501f2f4c123",
+ "mounts": [
+ "/var/lib/docker/devicemapper/mnt/0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8"
+ ]
+ },
+ "shm": {
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "context=\"system_u:object_r:container_file_t:s0:c523",
+ "c681\"",
+ "size=65536k"
+ ],
+ "mounts": [
+ "/var/lib/docker/containers/426e513ed508a451e3f70440eed040761f81529e4bc4240e7522d331f3f3bc12/shm"
+ ]
+ },
+ "nsfs": {
+ "fs_type": "nsfs",
+ "mount_options": [
+ "rw"
+ ],
+ "mounts": [
+ "/run/docker/netns/1ce89fd79f3d"
+ ]
+ },
+ "tracefs": {
+ "fs_type": "tracefs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "mounts": [
+ "/sys/kernel/debug/tracing"
+ ]
+ },
+ "/dev/loop1": {
+ "fs_type": "xfs",
+ "uuid": "00e2aa25-20d8-4ad7-b3a5-c501f2f4c123",
+ "mounts": [
+
+ ]
+ },
+ "/dev/mapper/docker-253:1-1180487-pool": {
+ "mounts": [
+
+ ]
+ },
+ "/dev/sr0": {
+ "mounts": [
+
+ ]
+ },
+ "/dev/loop2": {
+ "mounts": [
+
+ ]
+ },
+ "/dev/sda": {
+ "mounts": [
+
+ ]
+ },
+ "/dev/sda2": {
+ "fs_type": "LVM2_member",
+ "uuid": "66Ojcd-ULtu-1cZa-Tywo-mx0d-RF4O-ysA9jK",
+ "mounts": [
+
+ ]
+ },
+ "/dev/mapper/fedora_host--186-swap": {
+ "fs_type": "swap",
+ "uuid": "eae6059d-2fbe-4d1c-920d-a80bbeb1ac6d",
+ "mounts": [
+
+ ]
+ }
+ },
+ "by_mountpoint": {
+ "/dev": {
+ "kb_size": "8044124",
+ "kb_used": "0",
+ "kb_available": "8044124",
+ "percent_used": "0%",
+ "total_inodes": "2011031",
+ "inodes_used": "629",
+ "inodes_available": "2010402",
+ "inodes_percent_used": "1%",
+ "fs_type": "devtmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "seclabel",
+ "size=8044124k",
+ "nr_inodes=2011031",
+ "mode=755"
+ ],
+ "devices": [
+ "devtmpfs"
+ ]
+ },
+ "/dev/shm": {
+ "kb_size": "8055268",
+ "kb_used": "96036",
+ "kb_available": "7959232",
+ "percent_used": "2%",
+ "total_inodes": "2013817",
+ "inodes_used": "217",
+ "inodes_available": "2013600",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/run": {
+ "kb_size": "8055268",
+ "kb_used": "2280",
+ "kb_available": "8052988",
+ "percent_used": "1%",
+ "total_inodes": "2013817",
+ "inodes_used": "1070",
+ "inodes_available": "2012747",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel",
+ "mode=755"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/sys/fs/cgroup": {
+ "kb_size": "8055268",
+ "kb_used": "0",
+ "kb_available": "8055268",
+ "percent_used": "0%",
+ "total_inodes": "2013817",
+ "inodes_used": "16",
+ "inodes_available": "2013801",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "ro",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "seclabel",
+ "mode=755"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/": {
+ "kb_size": "51475068",
+ "kb_used": "42551284",
+ "kb_available": "6285960",
+ "percent_used": "88%",
+ "total_inodes": "3276800",
+ "inodes_used": "532908",
+ "inodes_available": "2743892",
+ "inodes_percent_used": "17%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "d34cf5e3-3449-4a6c-8179-a1feb2bca6ce",
+ "devices": [
+ "/dev/mapper/fedora_host--186-root"
+ ]
+ },
+ "/tmp": {
+ "kb_size": "8055268",
+ "kb_used": "848396",
+ "kb_available": "7206872",
+ "percent_used": "11%",
+ "total_inodes": "2013817",
+ "inodes_used": "1353",
+ "inodes_available": "2012464",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/boot": {
+ "kb_size": "487652",
+ "kb_used": "126628",
+ "kb_available": "331328",
+ "percent_used": "28%",
+ "total_inodes": "128016",
+ "inodes_used": "405",
+ "inodes_available": "127611",
+ "inodes_percent_used": "1%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "32caaec3-ef40-4691-a3b6-438c3f9bc1c0",
+ "devices": [
+ "/dev/sda1"
+ ]
+ },
+ "/home": {
+ "kb_size": "185948124",
+ "kb_used": "105904724",
+ "kb_available": "70574680",
+ "percent_used": "61%",
+ "total_inodes": "11821056",
+ "inodes_used": "1266687",
+ "inodes_available": "10554369",
+ "inodes_percent_used": "11%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "2d3e4853-fa69-4ccf-8a6a-77b05ab0a42d",
+ "devices": [
+ "/dev/mapper/fedora_host--186-home"
+ ]
+ },
+ "/var/lib/machines": {
+ "kb_size": "512000",
+ "kb_used": "16672",
+ "kb_available": "429056",
+ "percent_used": "4%",
+ "fs_type": "btrfs",
+ "uuid": "0f031512-ab15-497d-9abd-3a512b4a9390",
+ "devices": [
+ "/dev/loop0",
+ "/var/lib/machines.raw"
+ ],
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "space_cache",
+ "subvolid=5",
+ "subvol=/"
+ ]
+ },
+ "/run/user/0": {
+ "kb_size": "1611052",
+ "kb_used": "0",
+ "kb_available": "1611052",
+ "percent_used": "0%",
+ "total_inodes": "2013817",
+ "inodes_used": "7",
+ "inodes_available": "2013810",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "seclabel",
+ "size=1611052k",
+ "mode=700"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/run/user/1000": {
+ "kb_size": "1611052",
+ "kb_used": "72",
+ "kb_available": "1610980",
+ "percent_used": "1%",
+ "total_inodes": "2013817",
+ "inodes_used": "36",
+ "inodes_available": "2013781",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "seclabel",
+ "size=1611052k",
+ "mode=700",
+ "uid=1000",
+ "gid=1000"
+ ],
+ "devices": [
+ "tmpfs"
+ ]
+ },
+ "/sys": {
+ "fs_type": "sysfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ],
+ "devices": [
+ "sysfs"
+ ]
+ },
+ "/proc": {
+ "fs_type": "proc",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ],
+ "devices": [
+ "proc"
+ ]
+ },
+ "/sys/kernel/security": {
+ "fs_type": "securityfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ],
+ "devices": [
+ "securityfs"
+ ]
+ },
+ "/dev/pts": {
+ "fs_type": "devpts",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "noexec",
+ "relatime",
+ "seclabel",
+ "gid=5",
+ "mode=620",
+ "ptmxmode=000"
+ ],
+ "devices": [
+ "devpts"
+ ]
+ },
+ "/sys/fs/cgroup/systemd": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "xattr",
+ "release_agent=/usr/lib/systemd/systemd-cgroups-agent",
+ "name=systemd"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/pstore": {
+ "fs_type": "pstore",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ],
+ "devices": [
+ "pstore"
+ ]
+ },
+ "/sys/fs/cgroup/devices": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "devices"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/cpuset": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "cpuset"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/perf_event": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "perf_event"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/hugetlb": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "hugetlb"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/cpu,cpuacct": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "cpu",
+ "cpuacct"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/blkio": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "blkio"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/freezer": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "freezer"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/memory": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "memory"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/pids": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "pids"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/fs/cgroup/net_cls,net_prio": {
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "net_cls",
+ "net_prio"
+ ],
+ "devices": [
+ "cgroup"
+ ]
+ },
+ "/sys/kernel/config": {
+ "fs_type": "configfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "devices": [
+ "configfs"
+ ]
+ },
+ "/sys/fs/selinux": {
+ "fs_type": "selinuxfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "devices": [
+ "selinuxfs"
+ ]
+ },
+ "/sys/kernel/debug": {
+ "fs_type": "debugfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "devices": [
+ "debugfs"
+ ]
+ },
+ "/dev/hugepages": {
+ "fs_type": "hugetlbfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "devices": [
+ "hugetlbfs"
+ ]
+ },
+ "/dev/mqueue": {
+ "fs_type": "mqueue",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ],
+ "devices": [
+ "mqueue"
+ ]
+ },
+ "/proc/sys/fs/binfmt_misc": {
+ "fs_type": "binfmt_misc",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "devices": [
+ "systemd-1",
+ "binfmt_misc"
+ ]
+ },
+ "/sys/fs/fuse/connections": {
+ "fs_type": "fusectl",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "devices": [
+ "fusectl"
+ ]
+ },
+ "/run/user/1000/gvfs": {
+ "fs_type": "fuse.gvfsd-fuse",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "user_id=1000",
+ "group_id=1000"
+ ],
+ "devices": [
+ "gvfsd-fuse"
+ ]
+ },
+ "/var/lib/docker/devicemapper": {
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "d34cf5e3-3449-4a6c-8179-a1feb2bca6ce",
+ "devices": [
+ "/dev/mapper/fedora_host--186-root"
+ ]
+ },
+ {
+ "/run/docker/netns/1ce89fd79f3d": {
+ "fs_type": "nsfs",
+ "mount_options": [
+ "rw"
+ ],
+ "devices": [
+ "nsfs"
+ ]
+ },
+ "/sys/kernel/debug/tracing": {
+ "fs_type": "tracefs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ],
+ "devices": [
+ "tracefs"
+ ]
+ }
+ },
+ "by_pair": {
+ "devtmpfs,/dev": {
+ "device": "devtmpfs",
+ "kb_size": "8044124",
+ "kb_used": "0",
+ "kb_available": "8044124",
+ "percent_used": "0%",
+ "mount": "/dev",
+ "total_inodes": "2011031",
+ "inodes_used": "629",
+ "inodes_available": "2010402",
+ "inodes_percent_used": "1%",
+ "fs_type": "devtmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "seclabel",
+ "size=8044124k",
+ "nr_inodes=2011031",
+ "mode=755"
+ ]
+ },
+ "tmpfs,/dev/shm": {
+ "device": "tmpfs",
+ "kb_size": "8055268",
+ "kb_used": "96036",
+ "kb_available": "7959232",
+ "percent_used": "2%",
+ "mount": "/dev/shm",
+ "total_inodes": "2013817",
+ "inodes_used": "217",
+ "inodes_available": "2013600",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel"
+ ]
+ },
+ "tmpfs,/run": {
+ "device": "tmpfs",
+ "kb_size": "8055268",
+ "kb_used": "2280",
+ "kb_available": "8052988",
+ "percent_used": "1%",
+ "mount": "/run",
+ "total_inodes": "2013817",
+ "inodes_used": "1070",
+ "inodes_available": "2012747",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel",
+ "mode=755"
+ ]
+ },
+ "tmpfs,/sys/fs/cgroup": {
+ "device": "tmpfs",
+ "kb_size": "8055268",
+ "kb_used": "0",
+ "kb_available": "8055268",
+ "percent_used": "0%",
+ "mount": "/sys/fs/cgroup",
+ "total_inodes": "2013817",
+ "inodes_used": "16",
+ "inodes_available": "2013801",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "ro",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "seclabel",
+ "mode=755"
+ ]
+ },
+ "/dev/mapper/fedora_host--186-root,/": {
+ "device": "/dev/mapper/fedora_host--186-root",
+ "kb_size": "51475068",
+ "kb_used": "42551284",
+ "kb_available": "6285960",
+ "percent_used": "88%",
+ "mount": "/",
+ "total_inodes": "3276800",
+ "inodes_used": "532908",
+ "inodes_available": "2743892",
+ "inodes_percent_used": "17%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "d34cf5e3-3449-4a6c-8179-a1feb2bca6ce"
+ },
+ "tmpfs,/tmp": {
+ "device": "tmpfs",
+ "kb_size": "8055268",
+ "kb_used": "848396",
+ "kb_available": "7206872",
+ "percent_used": "11%",
+ "mount": "/tmp",
+ "total_inodes": "2013817",
+ "inodes_used": "1353",
+ "inodes_available": "2012464",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "seclabel"
+ ]
+ },
+ "/dev/sda1,/boot": {
+ "device": "/dev/sda1",
+ "kb_size": "487652",
+ "kb_used": "126628",
+ "kb_available": "331328",
+ "percent_used": "28%",
+ "mount": "/boot",
+ "total_inodes": "128016",
+ "inodes_used": "405",
+ "inodes_available": "127611",
+ "inodes_percent_used": "1%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "32caaec3-ef40-4691-a3b6-438c3f9bc1c0"
+ },
+ "/dev/mapper/fedora_host--186-home,/home": {
+ "device": "/dev/mapper/fedora_host--186-home",
+ "kb_size": "185948124",
+ "kb_used": "105904724",
+ "kb_available": "70574680",
+ "percent_used": "61%",
+ "mount": "/home",
+ "total_inodes": "11821056",
+ "inodes_used": "1266687",
+ "inodes_available": "10554369",
+ "inodes_percent_used": "11%",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "2d3e4853-fa69-4ccf-8a6a-77b05ab0a42d"
+ },
+ "/dev/loop0,/var/lib/machines": {
+ "device": "/dev/loop0",
+ "kb_size": "512000",
+ "kb_used": "16672",
+ "kb_available": "429056",
+ "percent_used": "4%",
+ "mount": "/var/lib/machines",
+ "fs_type": "btrfs",
+ "uuid": "0f031512-ab15-497d-9abd-3a512b4a9390"
+ },
+ "tmpfs,/run/user/0": {
+ "device": "tmpfs",
+ "kb_size": "1611052",
+ "kb_used": "0",
+ "kb_available": "1611052",
+ "percent_used": "0%",
+ "mount": "/run/user/0",
+ "total_inodes": "2013817",
+ "inodes_used": "7",
+ "inodes_available": "2013810",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "seclabel",
+ "size=1611052k",
+ "mode=700"
+ ]
+ },
+ "tmpfs,/run/user/1000": {
+ "device": "tmpfs",
+ "kb_size": "1611052",
+ "kb_used": "72",
+ "kb_available": "1610980",
+ "percent_used": "1%",
+ "mount": "/run/user/1000",
+ "total_inodes": "2013817",
+ "inodes_used": "36",
+ "inodes_available": "2013781",
+ "inodes_percent_used": "1%",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "seclabel",
+ "size=1611052k",
+ "mode=700",
+ "uid=1000",
+ "gid=1000"
+ ]
+ },
+ "sysfs,/sys": {
+ "device": "sysfs",
+ "mount": "/sys",
+ "fs_type": "sysfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ]
+ },
+ "proc,/proc": {
+ "device": "proc",
+ "mount": "/proc",
+ "fs_type": "proc",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ]
+ },
+ "securityfs,/sys/kernel/security": {
+ "device": "securityfs",
+ "mount": "/sys/kernel/security",
+ "fs_type": "securityfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime"
+ ]
+ },
+ "devpts,/dev/pts": {
+ "device": "devpts",
+ "mount": "/dev/pts",
+ "fs_type": "devpts",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "noexec",
+ "relatime",
+ "seclabel",
+ "gid=5",
+ "mode=620",
+ "ptmxmode=000"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/systemd": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/systemd",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "xattr",
+ "release_agent=/usr/lib/systemd/systemd-cgroups-agent",
+ "name=systemd"
+ ]
+ },
+ "pstore,/sys/fs/pstore": {
+ "device": "pstore",
+ "mount": "/sys/fs/pstore",
+ "fs_type": "pstore",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "seclabel"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/devices": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/devices",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "devices"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/cpuset": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/cpuset",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "cpuset"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/perf_event": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/perf_event",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "perf_event"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/hugetlb": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/hugetlb",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "hugetlb"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/cpu,cpuacct": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/cpu,cpuacct",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "cpu",
+ "cpuacct"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/blkio": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/blkio",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "blkio"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/freezer": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/freezer",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "freezer"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/memory": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/memory",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "memory"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/pids": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/pids",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "pids"
+ ]
+ },
+ "cgroup,/sys/fs/cgroup/net_cls,net_prio": {
+ "device": "cgroup",
+ "mount": "/sys/fs/cgroup/net_cls,net_prio",
+ "fs_type": "cgroup",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "net_cls",
+ "net_prio"
+ ]
+ },
+ "configfs,/sys/kernel/config": {
+ "device": "configfs",
+ "mount": "/sys/kernel/config",
+ "fs_type": "configfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ]
+ },
+ "selinuxfs,/sys/fs/selinux": {
+ "device": "selinuxfs",
+ "mount": "/sys/fs/selinux",
+ "fs_type": "selinuxfs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ]
+ },
+ "debugfs,/sys/kernel/debug": {
+ "device": "debugfs",
+ "mount": "/sys/kernel/debug",
+ "fs_type": "debugfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ]
+ },
+ "hugetlbfs,/dev/hugepages": {
+ "device": "hugetlbfs",
+ "mount": "/dev/hugepages",
+ "fs_type": "hugetlbfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ]
+ },
+ "mqueue,/dev/mqueue": {
+ "device": "mqueue",
+ "mount": "/dev/mqueue",
+ "fs_type": "mqueue",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel"
+ ]
+ },
+ "systemd-1,/proc/sys/fs/binfmt_misc": {
+ "device": "systemd-1",
+ "mount": "/proc/sys/fs/binfmt_misc",
+ "fs_type": "autofs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "fd=40",
+ "pgrp=1",
+ "timeout=0",
+ "minproto=5",
+ "maxproto=5",
+ "direct",
+ "pipe_ino=17610"
+ ]
+ },
+ "/var/lib/machines.raw,/var/lib/machines": {
+ "device": "/var/lib/machines.raw",
+ "mount": "/var/lib/machines",
+ "fs_type": "btrfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "space_cache",
+ "subvolid=5",
+ "subvol=/"
+ ]
+ },
+ "fusectl,/sys/fs/fuse/connections": {
+ "device": "fusectl",
+ "mount": "/sys/fs/fuse/connections",
+ "fs_type": "fusectl",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ]
+ },
+ "gvfsd-fuse,/run/user/1000/gvfs": {
+ "device": "gvfsd-fuse",
+ "mount": "/run/user/1000/gvfs",
+ "fs_type": "fuse.gvfsd-fuse",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "relatime",
+ "user_id=1000",
+ "group_id=1000"
+ ]
+ },
+ "/dev/mapper/fedora_host--186-root,/var/lib/docker/devicemapper": {
+ "device": "/dev/mapper/fedora_host--186-root",
+ "mount": "/var/lib/docker/devicemapper",
+ "fs_type": "ext4",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "seclabel",
+ "data=ordered"
+ ],
+ "uuid": "d34cf5e3-3449-4a6c-8179-a1feb2bca6ce"
+ },
+ "binfmt_misc,/proc/sys/fs/binfmt_misc": {
+ "device": "binfmt_misc",
+ "mount": "/proc/sys/fs/binfmt_misc",
+ "fs_type": "binfmt_misc",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ]
+ },
+ "/dev/mapper/docker-253:1-1180487-0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8,/var/lib/docker/devicemapper/mnt/0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8": {
+ "device": "/dev/mapper/docker-253:1-1180487-0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8",
+ "mount": "/var/lib/docker/devicemapper/mnt/0868fce108cd2524a4823aad8d665cca018ead39550ca088c440ab05deec13f8",
+ "fs_type": "xfs",
+ "mount_options": [
+ "rw",
+ "relatime",
+ "context=\"system_u:object_r:container_file_t:s0:c523",
+ "c681\"",
+ "nouuid",
+ "attr2",
+ "inode64",
+ "logbsize=64k",
+ "sunit=128",
+ "swidth=128",
+ "noquota"
+ ],
+ "uuid": "00e2aa25-20d8-4ad7-b3a5-c501f2f4c123"
+ },
+ "shm,/var/lib/docker/containers/426e513ed508a451e3f70440eed040761f81529e4bc4240e7522d331f3f3bc12/shm": {
+ "device": "shm",
+ "mount": "/var/lib/docker/containers/426e513ed508a451e3f70440eed040761f81529e4bc4240e7522d331f3f3bc12/shm",
+ "fs_type": "tmpfs",
+ "mount_options": [
+ "rw",
+ "nosuid",
+ "nodev",
+ "noexec",
+ "relatime",
+ "context=\"system_u:object_r:container_file_t:s0:c523",
+ "c681\"",
+ "size=65536k"
+ ]
+ },
+ "nsfs,/run/docker/netns/1ce89fd79f3d": {
+ "device": "nsfs",
+ "mount": "/run/docker/netns/1ce89fd79f3d",
+ "fs_type": "nsfs",
+ "mount_options": [
+ "rw"
+ ]
+ },
+ "tracefs,/sys/kernel/debug/tracing": {
+ "device": "tracefs",
+ "mount": "/sys/kernel/debug/tracing",
+ "fs_type": "tracefs",
+ "mount_options": [
+ "rw",
+ "relatime"
+ ]
+ },
+ "/dev/loop1,": {
+ "device": "/dev/loop1",
+ "fs_type": "xfs",
+ "uuid": "00e2aa25-20d8-4ad7-b3a5-c501f2f4c123"
+ },
+ "/dev/mapper/docker-253:1-1180487-pool,": {
+ "device": "/dev/mapper/docker-253:1-1180487-pool"
+ },
+ "/dev/sr0,": {
+ "device": "/dev/sr0"
+ },
+ "/dev/loop2,": {
+ "device": "/dev/loop2"
+ },
+ "/dev/sda,": {
+ "device": "/dev/sda"
+ },
+ "/dev/sda2,": {
+ "device": "/dev/sda2",
+ "fs_type": "LVM2_member",
+ "uuid": "66Ojcd-ULtu-1cZa-Tywo-mx0d-RF4O-ysA9jK"
+ },
+ "/dev/mapper/fedora_host--186-swap,": {
+ "device": "/dev/mapper/fedora_host--186-swap",
+ "fs_type": "swap",
+ "uuid": "eae6059d-2fbe-4d1c-920d-a80bbeb1ac6d"
+ }
+ }
+ },
+ "virtualization": {
+ "systems": {
+ "kvm": "host"
+ },
+ "system": "kvm",
+ "role": "host",
+ "libvirt_version": "2.2.0",
+ "uri": "qemu:///system",
+ "capabilities": {
+
+ },
+ "nodeinfo": {
+ "cores": 4,
+ "cpus": 8,
+ "memory": 16110540,
+ "mhz": 2832,
+ "model": "x86_64",
+ "nodes": 1,
+ "sockets": 1,
+ "threads": 2
+ },
+ "domains": {
+
+ },
+ "networks": {
+ "vagrant-libvirt": {
+ "bridge_name": "virbr1",
+ "uuid": "877ddb27-b39c-427e-a7bf-1aa829389eeb"
+ },
+ "default": {
+ "bridge_name": "virbr0",
+ "uuid": "750d2567-23a8-470d-8a2b-71cd651e30d1"
+ }
+ },
+ "storage": {
+ "virt-images": {
+ "autostart": true,
+ "uuid": "d8a189fa-f98c-462f-9ea4-204eb77a96a1",
+ "allocation": 106412863488,
+ "available": 83998015488,
+ "capacity": 190410878976,
+ "state": 2,
+ "volumes": {
+ "rhel-atomic-host-standard-2014-7-1.qcow2": {
+ "key": "/home/some_user/virt-images/rhel-atomic-host-standard-2014-7-1.qcow2",
+ "name": "rhel-atomic-host-standard-2014-7-1.qcow2",
+ "path": "/home/some_user/virt-images/rhel-atomic-host-standard-2014-7-1.qcow2",
+ "allocation": 1087115264,
+ "capacity": 8589934592,
+ "type": 0
+ },
+ "atomic-beta-instance-7.qcow2": {
+ "key": "/home/some_user/virt-images/atomic-beta-instance-7.qcow2",
+ "name": "atomic-beta-instance-7.qcow2",
+ "path": "/home/some_user/virt-images/atomic-beta-instance-7.qcow2",
+ "allocation": 200704,
+ "capacity": 8589934592,
+ "type": 0
+ },
+ "os1-atomic-meta-data": {
+ "key": "/home/some_user/virt-images/os1-atomic-meta-data",
+ "name": "os1-atomic-meta-data",
+ "path": "/home/some_user/virt-images/os1-atomic-meta-data",
+ "allocation": 4096,
+ "capacity": 49,
+ "type": 0
+ },
+ "atomic-user-data": {
+ "key": "/home/some_user/virt-images/atomic-user-data",
+ "name": "atomic-user-data",
+ "path": "/home/some_user/virt-images/atomic-user-data",
+ "allocation": 4096,
+ "capacity": 512,
+ "type": 0
+ },
+ "qemu-snap.txt": {
+ "key": "/home/some_user/virt-images/qemu-snap.txt",
+ "name": "qemu-snap.txt",
+ "path": "/home/some_user/virt-images/qemu-snap.txt",
+ "allocation": 4096,
+ "capacity": 111,
+ "type": 0
+ },
+ "atomic-beta-instance-5.qcow2": {
+ "key": "/home/some_user/virt-images/atomic-beta-instance-5.qcow2",
+ "name": "atomic-beta-instance-5.qcow2",
+ "path": "/home/some_user/virt-images/atomic-beta-instance-5.qcow2",
+ "allocation": 339091456,
+ "capacity": 8589934592,
+ "type": 0
+ },
+ "meta-data": {
+ "key": "/home/some_user/virt-images/meta-data",
+ "name": "meta-data",
+ "path": "/home/some_user/virt-images/meta-data",
+ "allocation": 4096,
+ "capacity": 49,
+ "type": 0
+ },
+ "atomic-beta-instance-8.qcow2": {
+ "key": "/home/some_user/virt-images/atomic-beta-instance-8.qcow2",
+ "name": "atomic-beta-instance-8.qcow2",
+ "path": "/home/some_user/virt-images/atomic-beta-instance-8.qcow2",
+ "allocation": 322576384,
+ "capacity": 8589934592,
+ "type": 0
+ },
+ "user-data": {
+ "key": "/home/some_user/virt-images/user-data",
+ "name": "user-data",
+ "path": "/home/some_user/virt-images/user-data",
+ "allocation": 4096,
+ "capacity": 512,
+ "type": 0
+ },
+ "rhel-6-2015-10-16.qcow2": {
+ "key": "/home/some_user/virt-images/rhel-6-2015-10-16.qcow2",
+ "name": "rhel-6-2015-10-16.qcow2",
+ "path": "/home/some_user/virt-images/rhel-6-2015-10-16.qcow2",
+ "allocation": 7209422848,
+ "capacity": 17179869184,
+ "type": 0
+ },
+ "atomic_demo_notes.txt": {
+ "key": "/home/some_user/virt-images/atomic_demo_notes.txt",
+ "name": "atomic_demo_notes.txt",
+ "path": "/home/some_user/virt-images/atomic_demo_notes.txt",
+ "allocation": 4096,
+ "capacity": 354,
+ "type": 0
+ },
+ "packer-windows-2012-R2-standard": {
+ "key": "/home/some_user/virt-images/packer-windows-2012-R2-standard",
+ "name": "packer-windows-2012-R2-standard",
+ "path": "/home/some_user/virt-images/packer-windows-2012-R2-standard",
+ "allocation": 16761495552,
+ "capacity": 64424509440,
+ "type": 0
+ },
+ "atomic3-cidata.iso": {
+ "key": "/home/some_user/virt-images/atomic3-cidata.iso",
+ "name": "atomic3-cidata.iso",
+ "path": "/home/some_user/virt-images/atomic3-cidata.iso",
+ "allocation": 376832,
+ "capacity": 374784,
+ "type": 0
+ },
+ ".atomic_demo_notes.txt.swp": {
+ "key": "/home/some_user/virt-images/.atomic_demo_notes.txt.swp",
+ "name": ".atomic_demo_notes.txt.swp",
+ "path": "/home/some_user/virt-images/.atomic_demo_notes.txt.swp",
+ "allocation": 12288,
+ "capacity": 12288,
+ "type": 0
+ },
+ "rhel7-2015-10-13.qcow2": {
+ "key": "/home/some_user/virt-images/rhel7-2015-10-13.qcow2",
+ "name": "rhel7-2015-10-13.qcow2",
+ "path": "/home/some_user/virt-images/rhel7-2015-10-13.qcow2",
+ "allocation": 4679413760,
+ "capacity": 12884901888,
+ "type": 0
+ }
+ }
+ },
+ "default": {
+ "autostart": true,
+ "uuid": "c8d9d160-efc0-4207-81c2-e79d6628f7e1",
+ "allocation": 43745488896,
+ "available": 8964980736,
+ "capacity": 52710469632,
+ "state": 2,
+ "volumes": {
+ "s3than-VAGRANTSLASH-trusty64_vagrant_box_image_0.0.1.img": {
+ "key": "/var/lib/libvirt/images/s3than-VAGRANTSLASH-trusty64_vagrant_box_image_0.0.1.img",
+ "name": "s3than-VAGRANTSLASH-trusty64_vagrant_box_image_0.0.1.img",
+ "path": "/var/lib/libvirt/images/s3than-VAGRANTSLASH-trusty64_vagrant_box_image_0.0.1.img",
+ "allocation": 1258622976,
+ "capacity": 42949672960,
+ "type": 0
+ },
+ "centos-7.0_vagrant_box_image.img": {
+ "key": "/var/lib/libvirt/images/centos-7.0_vagrant_box_image.img",
+ "name": "centos-7.0_vagrant_box_image.img",
+ "path": "/var/lib/libvirt/images/centos-7.0_vagrant_box_image.img",
+ "allocation": 1649414144,
+ "capacity": 42949672960,
+ "type": 0
+ },
+ "baremettle-VAGRANTSLASH-centos-5.10_vagrant_box_image_1.0.0.img": {
+ "key": "/var/lib/libvirt/images/baremettle-VAGRANTSLASH-centos-5.10_vagrant_box_image_1.0.0.img",
+ "name": "baremettle-VAGRANTSLASH-centos-5.10_vagrant_box_image_1.0.0.img",
+ "path": "/var/lib/libvirt/images/baremettle-VAGRANTSLASH-centos-5.10_vagrant_box_image_1.0.0.img",
+ "allocation": 810422272,
+ "capacity": 42949672960,
+ "type": 0
+ },
+ "centos-6_vagrant_box_image.img": {
+ "key": "/var/lib/libvirt/images/centos-6_vagrant_box_image.img",
+ "name": "centos-6_vagrant_box_image.img",
+ "path": "/var/lib/libvirt/images/centos-6_vagrant_box_image.img",
+ "allocation": 1423642624,
+ "capacity": 42949672960,
+ "type": 0
+ },
+ "centos5-ansible_default.img": {
+ "key": "/var/lib/libvirt/images/centos5-ansible_default.img",
+ "name": "centos5-ansible_default.img",
+ "path": "/var/lib/libvirt/images/centos5-ansible_default.img",
+ "allocation": 8986624,
+ "capacity": 42949672960,
+ "type": 0
+ },
+ "ubuntu_default.img": {
+ "key": "/var/lib/libvirt/images/ubuntu_default.img",
+ "name": "ubuntu_default.img",
+ "path": "/var/lib/libvirt/images/ubuntu_default.img",
+ "allocation": 3446833152,
+ "capacity": 42949672960,
+ "type": 0
+ }
+ }
+ },
+ "boot-scratch": {
+ "autostart": true,
+ "uuid": "e5ef4360-b889-4843-84fb-366e8fb30f20",
+ "allocation": 43745488896,
+ "available": 8964980736,
+ "capacity": 52710469632,
+ "state": 2,
+ "volumes": {
+
+ }
+ }
+ }
+ },
+ "network": {
+ "interfaces": {
+ "lo": {
+ "mtu": "65536",
+ "flags": [
+ "LOOPBACK",
+ "UP",
+ "LOWER_UP"
+ ],
+ "encapsulation": "Loopback",
+ "addresses": {
+ "127.0.0.1": {
+ "family": "inet",
+ "prefixlen": "8",
+ "netmask": "255.0.0.0",
+ "scope": "Node",
+ "ip_scope": "LOOPBACK"
+ },
+ "::1": {
+ "family": "inet6",
+ "prefixlen": "128",
+ "scope": "Node",
+ "tags": [
+
+ ],
+ "ip_scope": "LINK LOCAL LOOPBACK"
+ }
+ },
+ "state": "unknown"
+ },
+ "em1": {
+ "type": "em",
+ "number": "1",
+ "mtu": "1500",
+ "flags": [
+ "BROADCAST",
+ "MULTICAST",
+ "UP"
+ ],
+ "encapsulation": "Ethernet",
+ "addresses": {
+ "3C:97:0E:E9:28:8E": {
+ "family": "lladdr"
+ }
+ },
+ "state": "down",
+ "link_speed": 0,
+ "duplex": "Unknown! (255)",
+ "port": "Twisted Pair",
+ "transceiver": "internal",
+ "auto_negotiation": "on",
+ "mdi_x": "Unknown (auto)",
+ "ring_params": {
+ "max_rx": 4096,
+ "max_rx_mini": 0,
+ "max_rx_jumbo": 0,
+ "max_tx": 4096,
+ "current_rx": 256,
+ "current_rx_mini": 0,
+ "current_rx_jumbo": 0,
+ "current_tx": 256
+ }
+ },
+ "wlp4s0": {
+ "type": "wlp4s",
+ "number": "0",
+ "mtu": "1500",
+ "flags": [
+ "BROADCAST",
+ "MULTICAST",
+ "UP",
+ "LOWER_UP"
+ ],
+ "encapsulation": "Ethernet",
+ "addresses": {
+ "5C:51:4F:E6:A8:E3": {
+ "family": "lladdr"
+ },
+ "192.168.1.19": {
+ "family": "inet",
+ "prefixlen": "24",
+ "netmask": "255.255.255.0",
+ "broadcast": "192.168.1.255",
+ "scope": "Global",
+ "ip_scope": "RFC1918 PRIVATE"
+ },
+ "fe80::5e51:4fff:fee6:a8e3": {
+ "family": "inet6",
+ "prefixlen": "64",
+ "scope": "Link",
+ "tags": [
+
+ ],
+ "ip_scope": "LINK LOCAL UNICAST"
+ }
+ },
+ "state": "up",
+ "arp": {
+ "192.168.1.33": "00:11:d9:39:3e:e0",
+ "192.168.1.20": "ac:3a:7a:a7:49:e8",
+ "192.168.1.17": "00:09:b0:d0:64:19",
+ "192.168.1.22": "ac:bc:32:82:30:bb",
+ "192.168.1.15": "00:11:32:2e:10:d5",
+ "192.168.1.1": "84:1b:5e:03:50:b2",
+ "192.168.1.34": "00:11:d9:5f:e8:e6",
+ "192.168.1.16": "dc:a5:f4:ac:22:3a",
+ "192.168.1.21": "74:c2:46:73:28:d8",
+ "192.168.1.27": "00:17:88:09:3c:bb",
+ "192.168.1.24": "08:62:66:90:a2:b8"
+ },
+ "routes": [
+ {
+ "destination": "default",
+ "family": "inet",
+ "via": "192.168.1.1",
+ "metric": "600",
+ "proto": "static"
+ },
+ {
+ "destination": "66.187.232.64",
+ "family": "inet",
+ "via": "192.168.1.1",
+ "metric": "600",
+ "proto": "static"
+ },
+ {
+ "destination": "192.168.1.0/24",
+ "family": "inet",
+ "scope": "link",
+ "metric": "600",
+ "proto": "kernel",
+ "src": "192.168.1.19"
+ },
+ {
+ "destination": "192.168.1.1",
+ "family": "inet",
+ "scope": "link",
+ "metric": "600",
+ "proto": "static"
+ },
+ {
+ "destination": "fe80::/64",
+ "family": "inet6",
+ "metric": "256",
+ "proto": "kernel"
+ }
+ ],
+ "ring_params": {
+ "max_rx": 0,
+ "max_rx_mini": 0,
+ "max_rx_jumbo": 0,
+ "max_tx": 0,
+ "current_rx": 0,
+ "current_rx_mini": 0,
+ "current_rx_jumbo": 0,
+ "current_tx": 0
+ }
+ },
+ "virbr1": {
+ "type": "virbr",
+ "number": "1",
+ "mtu": "1500",
+ "flags": [
+ "BROADCAST",
+ "MULTICAST",
+ "UP"
+ ],
+ "encapsulation": "Ethernet",
+ "addresses": {
+ "52:54:00:B4:68:A9": {
+ "family": "lladdr"
+ },
+ "192.168.121.1": {
+ "family": "inet",
+ "prefixlen": "24",
+ "netmask": "255.255.255.0",
+ "broadcast": "192.168.121.255",
+ "scope": "Global",
+ "ip_scope": "RFC1918 PRIVATE"
+ }
+ },
+ "state": "1",
+ "routes": [
+ {
+ "destination": "192.168.121.0/24",
+ "family": "inet",
+ "scope": "link",
+ "proto": "kernel",
+ "src": "192.168.121.1"
+ }
+ ],
+ "ring_params": {
+
+ }
+ },
+ "virbr1-nic": {
+ "type": "virbr",
+ "number": "1-nic",
+ "mtu": "1500",
+ "flags": [
+ "BROADCAST",
+ "MULTICAST"
+ ],
+ "encapsulation": "Ethernet",
+ "addresses": {
+ "52:54:00:B4:68:A9": {
+ "family": "lladdr"
+ }
+ },
+ "state": "disabled",
+ "link_speed": 10,
+ "duplex": "Full",
+ "port": "Twisted Pair",
+ "transceiver": "internal",
+ "auto_negotiation": "off",
+ "mdi_x": "Unknown",
+ "ring_params": {
+
+ }
+ },
+ "virbr0": {
+ "type": "virbr",
+ "number": "0",
+ "mtu": "1500",
+ "flags": [
+ "BROADCAST",
+ "MULTICAST",
+ "UP"
+ ],
+ "encapsulation": "Ethernet",
+ "addresses": {
+ "52:54:00:CE:82:5E": {
+ "family": "lladdr"
+ },
+ "192.168.137.1": {
+ "family": "inet",
+ "prefixlen": "24",
+ "netmask": "255.255.255.0",
+ "broadcast": "192.168.137.255",
+ "scope": "Global",
+ "ip_scope": "RFC1918 PRIVATE"
+ }
+ },
+ "state": "1",
+ "routes": [
+ {
+ "destination": "192.168.137.0/24",
+ "family": "inet",
+ "scope": "link",
+ "proto": "kernel",
+ "src": "192.168.137.1"
+ }
+ ],
+ "ring_params": {
+
+ }
+ },
+ "virbr0-nic": {
+ "type": "virbr",
+ "number": "0-nic",
+ "mtu": "1500",
+ "flags": [
+ "BROADCAST",
+ "MULTICAST"
+ ],
+ "encapsulation": "Ethernet",
+ "addresses": {
+ "52:54:00:CE:82:5E": {
+ "family": "lladdr"
+ }
+ },
+ "state": "disabled",
+ "link_speed": 10,
+ "duplex": "Full",
+ "port": "Twisted Pair",
+ "transceiver": "internal",
+ "auto_negotiation": "off",
+ "mdi_x": "Unknown",
+ "ring_params": {
+
+ }
+ },
+ "docker0": {
+ "type": "docker",
+ "number": "0",
+ "mtu": "1500",
+ "flags": [
+ "BROADCAST",
+ "MULTICAST",
+ "UP",
+ "LOWER_UP"
+ ],
+ "encapsulation": "Ethernet",
+ "addresses": {
+ "02:42:EA:15:D8:84": {
+ "family": "lladdr"
+ },
+ "172.17.0.1": {
+ "family": "inet",
+ "prefixlen": "16",
+ "netmask": "255.255.0.0",
+ "scope": "Global",
+ "ip_scope": "RFC1918 PRIVATE"
+ },
+ "fe80::42:eaff:fe15:d884": {
+ "family": "inet6",
+ "prefixlen": "64",
+ "scope": "Link",
+ "tags": [
+
+ ],
+ "ip_scope": "LINK LOCAL UNICAST"
+ }
+ },
+ "state": "0",
+ "arp": {
+ "172.17.0.2": "02:42:ac:11:00:02",
+ "172.17.0.4": "02:42:ac:11:00:04",
+ "172.17.0.3": "02:42:ac:11:00:03"
+ },
+ "routes": [
+ {
+ "destination": "172.17.0.0/16",
+ "family": "inet",
+ "scope": "link",
+ "proto": "kernel",
+ "src": "172.17.0.1"
+ },
+ {
+ "destination": "fe80::/64",
+ "family": "inet6",
+ "metric": "256",
+ "proto": "kernel"
+ }
+ ],
+ "ring_params": {
+
+ }
+ },
+ "vethf20ff12": {
+ "type": "vethf20ff1",
+ "number": "2",
+ "mtu": "1500",
+ "flags": [
+ "BROADCAST",
+ "MULTICAST",
+ "UP",
+ "LOWER_UP"
+ ],
+ "encapsulation": "Ethernet",
+ "addresses": {
+ "AE:6E:2B:1E:A1:31": {
+ "family": "lladdr"
+ },
+ "fe80::ac6e:2bff:fe1e:a131": {
+ "family": "inet6",
+ "prefixlen": "64",
+ "scope": "Link",
+ "tags": [
+
+ ],
+ "ip_scope": "LINK LOCAL UNICAST"
+ }
+ },
+ "state": "forwarding",
+ "routes": [
+ {
+ "destination": "fe80::/64",
+ "family": "inet6",
+ "metric": "256",
+ "proto": "kernel"
+ }
+ ],
+ "link_speed": 10000,
+ "duplex": "Full",
+ "port": "Twisted Pair",
+ "transceiver": "internal",
+ "auto_negotiation": "off",
+ "mdi_x": "Unknown",
+ "ring_params": {
+
+ }
+ },
+ "tun0": {
+ "type": "tun",
+ "number": "0",
+ "mtu": "1360",
+ "flags": [
+ "MULTICAST",
+ "NOARP",
+ "UP",
+ "LOWER_UP"
+ ],
+ "addresses": {
+ "10.10.120.68": {
+ "family": "inet",
+ "prefixlen": "21",
+ "netmask": "255.255.248.0",
+ "broadcast": "10.10.127.255",
+ "scope": "Global",
+ "ip_scope": "RFC1918 PRIVATE"
+ },
+ "fe80::365e:885c:31ca:7670": {
+ "family": "inet6",
+ "prefixlen": "64",
+ "scope": "Link",
+ "tags": [
+ "flags",
+ "800"
+ ],
+ "ip_scope": "LINK LOCAL UNICAST"
+ }
+ },
+ "state": "unknown",
+ "routes": [
+ {
+ "destination": "10.0.0.0/8",
+ "family": "inet",
+ "via": "10.10.120.1",
+ "metric": "50",
+ "proto": "static"
+ },
+ {
+ "destination": "10.10.120.0/21",
+ "family": "inet",
+ "scope": "link",
+ "metric": "50",
+ "proto": "kernel",
+ "src": "10.10.120.68"
+ },
+ {
+ "destination": "fe80::/64",
+ "family": "inet6",
+ "metric": "256",
+ "proto": "kernel"
+ }
+ ]
+ }
+ },
+ "default_interface": "wlp4s0",
+ "default_gateway": "192.168.1.1"
+ },
+ "counters": {
+ "network": {
+ "interfaces": {
+ "lo": {
+ "tx": {
+ "queuelen": "1",
+ "bytes": "202568405",
+ "packets": "1845473",
+ "errors": "0",
+ "drop": "0",
+ "carrier": "0",
+ "collisions": "0"
+ },
+ "rx": {
+ "bytes": "202568405",
+ "packets": "1845473",
+ "errors": "0",
+ "drop": "0",
+ "overrun": "0"
+ }
+ },
+ "em1": {
+ "tx": {
+ "queuelen": "1000",
+ "bytes": "673898037",
+ "packets": "1631282",
+ "errors": "0",
+ "drop": "0",
+ "carrier": "0",
+ "collisions": "0"
+ },
+ "rx": {
+ "bytes": "1536186718",
+ "packets": "1994394",
+ "errors": "0",
+ "drop": "0",
+ "overrun": "0"
+ }
+ },
+ "wlp4s0": {
+ "tx": {
+ "queuelen": "1000",
+ "bytes": "3927670539",
+ "packets": "15146886",
+ "errors": "0",
+ "drop": "0",
+ "carrier": "0",
+ "collisions": "0"
+ },
+ "rx": {
+ "bytes": "12367173401",
+ "packets": "23981258",
+ "errors": "0",
+ "drop": "0",
+ "overrun": "0"
+ }
+ },
+ "virbr1": {
+ "tx": {
+ "queuelen": "1000",
+ "bytes": "0",
+ "packets": "0",
+ "errors": "0",
+ "drop": "0",
+ "carrier": "0",
+ "collisions": "0"
+ },
+ "rx": {
+ "bytes": "0",
+ "packets": "0",
+ "errors": "0",
+ "drop": "0",
+ "overrun": "0"
+ }
+ },
+ "virbr1-nic": {
+ "tx": {
+ "queuelen": "1000",
+ "bytes": "0",
+ "packets": "0",
+ "errors": "0",
+ "drop": "0",
+ "carrier": "0",
+ "collisions": "0"
+ },
+ "rx": {
+ "bytes": "0",
+ "packets": "0",
+ "errors": "0",
+ "drop": "0",
+ "overrun": "0"
+ }
+ },
+ "virbr0": {
+ "tx": {
+ "queuelen": "1000",
+ "bytes": "0",
+ "packets": "0",
+ "errors": "0",
+ "drop": "0",
+ "carrier": "0",
+ "collisions": "0"
+ },
+ "rx": {
+ "bytes": "0",
+ "packets": "0",
+ "errors": "0",
+ "drop": "0",
+ "overrun": "0"
+ }
+ },
+ "virbr0-nic": {
+ "tx": {
+ "queuelen": "1000",
+ "bytes": "0",
+ "packets": "0",
+ "errors": "0",
+ "drop": "0",
+ "carrier": "0",
+ "collisions": "0"
+ },
+ "rx": {
+ "bytes": "0",
+ "packets": "0",
+ "errors": "0",
+ "drop": "0",
+ "overrun": "0"
+ }
+ },
+ "docker0": {
+ "rx": {
+ "bytes": "2471313",
+ "packets": "36915",
+ "errors": "0",
+ "drop": "0",
+ "overrun": "0"
+ },
+ "tx": {
+ "bytes": "413371670",
+ "packets": "127713",
+ "errors": "0",
+ "drop": "0",
+ "carrier": "0",
+ "collisions": "0"
+ }
+ },
+ "vethf20ff12": {
+ "rx": {
+ "bytes": "34391",
+ "packets": "450",
+ "errors": "0",
+ "drop": "0",
+ "overrun": "0"
+ },
+ "tx": {
+ "bytes": "17919115",
+ "packets": "108069",
+ "errors": "0",
+ "drop": "0",
+ "carrier": "0",
+ "collisions": "0"
+ }
+ },
+ "tun0": {
+ "tx": {
+ "queuelen": "100",
+ "bytes": "22343462",
+ "packets": "253442",
+ "errors": "0",
+ "drop": "0",
+ "carrier": "0",
+ "collisions": "0"
+ },
+ "rx": {
+ "bytes": "115160002",
+ "packets": "197529",
+ "errors": "0",
+ "drop": "0",
+ "overrun": "0"
+ }
+ }
+ }
+ }
+ },
+ "ipaddress": "192.168.1.19",
+ "macaddress": "5C:51:4F:E6:A8:E3",
+ "ip6address": "fe80::42:eaff:fe15:d884",
+ "cpu": {
+ "0": {
+ "vendor_id": "GenuineIntel",
+ "family": "6",
+ "model": "60",
+ "model_name": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "stepping": "3",
+ "mhz": "3238.714",
+ "cache_size": "6144 KB",
+ "physical_id": "0",
+ "core_id": "0",
+ "cores": "4",
+ "flags": [
+ "fpu",
+ "vme",
+ "de",
+ "pse",
+ "tsc",
+ "msr",
+ "pae",
+ "mce",
+ "cx8",
+ "apic",
+ "sep",
+ "mtrr",
+ "pge",
+ "mca",
+ "cmov",
+ "pat",
+ "pse36",
+ "clflush",
+ "dts",
+ "acpi",
+ "mmx",
+ "fxsr",
+ "sse",
+ "sse2",
+ "ss",
+ "ht",
+ "tm",
+ "pbe",
+ "syscall",
+ "nx",
+ "pdpe1gb",
+ "rdtscp",
+ "lm",
+ "constant_tsc",
+ "arch_perfmon",
+ "pebs",
+ "bts",
+ "rep_good",
+ "nopl",
+ "xtopology",
+ "nonstop_tsc",
+ "aperfmperf",
+ "eagerfpu",
+ "pni",
+ "pclmulqdq",
+ "dtes64",
+ "monitor",
+ "ds_cpl",
+ "vmx",
+ "smx",
+ "est",
+ "tm2",
+ "ssse3",
+ "sdbg",
+ "fma",
+ "cx16",
+ "xtpr",
+ "pdcm",
+ "pcid",
+ "sse4_1",
+ "sse4_2",
+ "x2apic",
+ "movbe",
+ "popcnt",
+ "tsc_deadline_timer",
+ "aes",
+ "xsave",
+ "avx",
+ "f16c",
+ "rdrand",
+ "lahf_lm",
+ "abm",
+ "epb",
+ "tpr_shadow",
+ "vnmi",
+ "flexpriority",
+ "ept",
+ "vpid",
+ "fsgsbase",
+ "tsc_adjust",
+ "bmi1",
+ "avx2",
+ "smep",
+ "bmi2",
+ "erms",
+ "invpcid",
+ "xsaveopt",
+ "dtherm",
+ "ida",
+ "arat",
+ "pln",
+ "pts"
+ ]
+ },
+ "1": {
+ "vendor_id": "GenuineIntel",
+ "family": "6",
+ "model": "60",
+ "model_name": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "stepping": "3",
+ "mhz": "3137.200",
+ "cache_size": "6144 KB",
+ "physical_id": "0",
+ "core_id": "0",
+ "cores": "4",
+ "flags": [
+ "fpu",
+ "vme",
+ "de",
+ "pse",
+ "tsc",
+ "msr",
+ "pae",
+ "mce",
+ "cx8",
+ "apic",
+ "sep",
+ "mtrr",
+ "pge",
+ "mca",
+ "cmov",
+ "pat",
+ "pse36",
+ "clflush",
+ "dts",
+ "acpi",
+ "mmx",
+ "fxsr",
+ "sse",
+ "sse2",
+ "ss",
+ "ht",
+ "tm",
+ "pbe",
+ "syscall",
+ "nx",
+ "pdpe1gb",
+ "rdtscp",
+ "lm",
+ "constant_tsc",
+ "arch_perfmon",
+ "pebs",
+ "bts",
+ "rep_good",
+ "nopl",
+ "xtopology",
+ "nonstop_tsc",
+ "aperfmperf",
+ "eagerfpu",
+ "pni",
+ "pclmulqdq",
+ "dtes64",
+ "monitor",
+ "ds_cpl",
+ "vmx",
+ "smx",
+ "est",
+ "tm2",
+ "ssse3",
+ "sdbg",
+ "fma",
+ "cx16",
+ "xtpr",
+ "pdcm",
+ "pcid",
+ "sse4_1",
+ "sse4_2",
+ "x2apic",
+ "movbe",
+ "popcnt",
+ "tsc_deadline_timer",
+ "aes",
+ "xsave",
+ "avx",
+ "f16c",
+ "rdrand",
+ "lahf_lm",
+ "abm",
+ "epb",
+ "tpr_shadow",
+ "vnmi",
+ "flexpriority",
+ "ept",
+ "vpid",
+ "fsgsbase",
+ "tsc_adjust",
+ "bmi1",
+ "avx2",
+ "smep",
+ "bmi2",
+ "erms",
+ "invpcid",
+ "xsaveopt",
+ "dtherm",
+ "ida",
+ "arat",
+ "pln",
+ "pts"
+ ]
+ },
+ "2": {
+ "vendor_id": "GenuineIntel",
+ "family": "6",
+ "model": "60",
+ "model_name": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "stepping": "3",
+ "mhz": "3077.050",
+ "cache_size": "6144 KB",
+ "physical_id": "0",
+ "core_id": "1",
+ "cores": "4",
+ "flags": [
+ "fpu",
+ "vme",
+ "de",
+ "pse",
+ "tsc",
+ "msr",
+ "pae",
+ "mce",
+ "cx8",
+ "apic",
+ "sep",
+ "mtrr",
+ "pge",
+ "mca",
+ "cmov",
+ "pat",
+ "pse36",
+ "clflush",
+ "dts",
+ "acpi",
+ "mmx",
+ "fxsr",
+ "sse",
+ "sse2",
+ "ss",
+ "ht",
+ "tm",
+ "pbe",
+ "syscall",
+ "nx",
+ "pdpe1gb",
+ "rdtscp",
+ "lm",
+ "constant_tsc",
+ "arch_perfmon",
+ "pebs",
+ "bts",
+ "rep_good",
+ "nopl",
+ "xtopology",
+ "nonstop_tsc",
+ "aperfmperf",
+ "eagerfpu",
+ "pni",
+ "pclmulqdq",
+ "dtes64",
+ "monitor",
+ "ds_cpl",
+ "vmx",
+ "smx",
+ "est",
+ "tm2",
+ "ssse3",
+ "sdbg",
+ "fma",
+ "cx16",
+ "xtpr",
+ "pdcm",
+ "pcid",
+ "sse4_1",
+ "sse4_2",
+ "x2apic",
+ "movbe",
+ "popcnt",
+ "tsc_deadline_timer",
+ "aes",
+ "xsave",
+ "avx",
+ "f16c",
+ "rdrand",
+ "lahf_lm",
+ "abm",
+ "epb",
+ "tpr_shadow",
+ "vnmi",
+ "flexpriority",
+ "ept",
+ "vpid",
+ "fsgsbase",
+ "tsc_adjust",
+ "bmi1",
+ "avx2",
+ "smep",
+ "bmi2",
+ "erms",
+ "invpcid",
+ "xsaveopt",
+ "dtherm",
+ "ida",
+ "arat",
+ "pln",
+ "pts"
+ ]
+ },
+ "3": {
+ "vendor_id": "GenuineIntel",
+ "family": "6",
+ "model": "60",
+ "model_name": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "stepping": "3",
+ "mhz": "2759.655",
+ "cache_size": "6144 KB",
+ "physical_id": "0",
+ "core_id": "1",
+ "cores": "4",
+ "flags": [
+ "fpu",
+ "vme",
+ "de",
+ "pse",
+ "tsc",
+ "msr",
+ "pae",
+ "mce",
+ "cx8",
+ "apic",
+ "sep",
+ "mtrr",
+ "pge",
+ "mca",
+ "cmov",
+ "pat",
+ "pse36",
+ "clflush",
+ "dts",
+ "acpi",
+ "mmx",
+ "fxsr",
+ "sse",
+ "sse2",
+ "ss",
+ "ht",
+ "tm",
+ "pbe",
+ "syscall",
+ "nx",
+ "pdpe1gb",
+ "rdtscp",
+ "lm",
+ "constant_tsc",
+ "arch_perfmon",
+ "pebs",
+ "bts",
+ "rep_good",
+ "nopl",
+ "xtopology",
+ "nonstop_tsc",
+ "aperfmperf",
+ "eagerfpu",
+ "pni",
+ "pclmulqdq",
+ "dtes64",
+ "monitor",
+ "ds_cpl",
+ "vmx",
+ "smx",
+ "est",
+ "tm2",
+ "ssse3",
+ "sdbg",
+ "fma",
+ "cx16",
+ "xtpr",
+ "pdcm",
+ "pcid",
+ "sse4_1",
+ "sse4_2",
+ "x2apic",
+ "movbe",
+ "popcnt",
+ "tsc_deadline_timer",
+ "aes",
+ "xsave",
+ "avx",
+ "f16c",
+ "rdrand",
+ "lahf_lm",
+ "abm",
+ "epb",
+ "tpr_shadow",
+ "vnmi",
+ "flexpriority",
+ "ept",
+ "vpid",
+ "fsgsbase",
+ "tsc_adjust",
+ "bmi1",
+ "avx2",
+ "smep",
+ "bmi2",
+ "erms",
+ "invpcid",
+ "xsaveopt",
+ "dtherm",
+ "ida",
+ "arat",
+ "pln",
+ "pts"
+ ]
+ },
+ "4": {
+ "vendor_id": "GenuineIntel",
+ "family": "6",
+ "model": "60",
+ "model_name": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "stepping": "3",
+ "mhz": "3419.000",
+ "cache_size": "6144 KB",
+ "physical_id": "0",
+ "core_id": "2",
+ "cores": "4",
+ "flags": [
+ "fpu",
+ "vme",
+ "de",
+ "pse",
+ "tsc",
+ "msr",
+ "pae",
+ "mce",
+ "cx8",
+ "apic",
+ "sep",
+ "mtrr",
+ "pge",
+ "mca",
+ "cmov",
+ "pat",
+ "pse36",
+ "clflush",
+ "dts",
+ "acpi",
+ "mmx",
+ "fxsr",
+ "sse",
+ "sse2",
+ "ss",
+ "ht",
+ "tm",
+ "pbe",
+ "syscall",
+ "nx",
+ "pdpe1gb",
+ "rdtscp",
+ "lm",
+ "constant_tsc",
+ "arch_perfmon",
+ "pebs",
+ "bts",
+ "rep_good",
+ "nopl",
+ "xtopology",
+ "nonstop_tsc",
+ "aperfmperf",
+ "eagerfpu",
+ "pni",
+ "pclmulqdq",
+ "dtes64",
+ "monitor",
+ "ds_cpl",
+ "vmx",
+ "smx",
+ "est",
+ "tm2",
+ "ssse3",
+ "sdbg",
+ "fma",
+ "cx16",
+ "xtpr",
+ "pdcm",
+ "pcid",
+ "sse4_1",
+ "sse4_2",
+ "x2apic",
+ "movbe",
+ "popcnt",
+ "tsc_deadline_timer",
+ "aes",
+ "xsave",
+ "avx",
+ "f16c",
+ "rdrand",
+ "lahf_lm",
+ "abm",
+ "epb",
+ "tpr_shadow",
+ "vnmi",
+ "flexpriority",
+ "ept",
+ "vpid",
+ "fsgsbase",
+ "tsc_adjust",
+ "bmi1",
+ "avx2",
+ "smep",
+ "bmi2",
+ "erms",
+ "invpcid",
+ "xsaveopt",
+ "dtherm",
+ "ida",
+ "arat",
+ "pln",
+ "pts"
+ ]
+ },
+ "5": {
+ "vendor_id": "GenuineIntel",
+ "family": "6",
+ "model": "60",
+ "model_name": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "stepping": "3",
+ "mhz": "2752.569",
+ "cache_size": "6144 KB",
+ "physical_id": "0",
+ "core_id": "2",
+ "cores": "4",
+ "flags": [
+ "fpu",
+ "vme",
+ "de",
+ "pse",
+ "tsc",
+ "msr",
+ "pae",
+ "mce",
+ "cx8",
+ "apic",
+ "sep",
+ "mtrr",
+ "pge",
+ "mca",
+ "cmov",
+ "pat",
+ "pse36",
+ "clflush",
+ "dts",
+ "acpi",
+ "mmx",
+ "fxsr",
+ "sse",
+ "sse2",
+ "ss",
+ "ht",
+ "tm",
+ "pbe",
+ "syscall",
+ "nx",
+ "pdpe1gb",
+ "rdtscp",
+ "lm",
+ "constant_tsc",
+ "arch_perfmon",
+ "pebs",
+ "bts",
+ "rep_good",
+ "nopl",
+ "xtopology",
+ "nonstop_tsc",
+ "aperfmperf",
+ "eagerfpu",
+ "pni",
+ "pclmulqdq",
+ "dtes64",
+ "monitor",
+ "ds_cpl",
+ "vmx",
+ "smx",
+ "est",
+ "tm2",
+ "ssse3",
+ "sdbg",
+ "fma",
+ "cx16",
+ "xtpr",
+ "pdcm",
+ "pcid",
+ "sse4_1",
+ "sse4_2",
+ "x2apic",
+ "movbe",
+ "popcnt",
+ "tsc_deadline_timer",
+ "aes",
+ "xsave",
+ "avx",
+ "f16c",
+ "rdrand",
+ "lahf_lm",
+ "abm",
+ "epb",
+ "tpr_shadow",
+ "vnmi",
+ "flexpriority",
+ "ept",
+ "vpid",
+ "fsgsbase",
+ "tsc_adjust",
+ "bmi1",
+ "avx2",
+ "smep",
+ "bmi2",
+ "erms",
+ "invpcid",
+ "xsaveopt",
+ "dtherm",
+ "ida",
+ "arat",
+ "pln",
+ "pts"
+ ]
+ },
+ "6": {
+ "vendor_id": "GenuineIntel",
+ "family": "6",
+ "model": "60",
+ "model_name": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "stepping": "3",
+ "mhz": "2953.619",
+ "cache_size": "6144 KB",
+ "physical_id": "0",
+ "core_id": "3",
+ "cores": "4",
+ "flags": [
+ "fpu",
+ "vme",
+ "de",
+ "pse",
+ "tsc",
+ "msr",
+ "pae",
+ "mce",
+ "cx8",
+ "apic",
+ "sep",
+ "mtrr",
+ "pge",
+ "mca",
+ "cmov",
+ "pat",
+ "pse36",
+ "clflush",
+ "dts",
+ "acpi",
+ "mmx",
+ "fxsr",
+ "sse",
+ "sse2",
+ "ss",
+ "ht",
+ "tm",
+ "pbe",
+ "syscall",
+ "nx",
+ "pdpe1gb",
+ "rdtscp",
+ "lm",
+ "constant_tsc",
+ "arch_perfmon",
+ "pebs",
+ "bts",
+ "rep_good",
+ "nopl",
+ "xtopology",
+ "nonstop_tsc",
+ "aperfmperf",
+ "eagerfpu",
+ "pni",
+ "pclmulqdq",
+ "dtes64",
+ "monitor",
+ "ds_cpl",
+ "vmx",
+ "smx",
+ "est",
+ "tm2",
+ "ssse3",
+ "sdbg",
+ "fma",
+ "cx16",
+ "xtpr",
+ "pdcm",
+ "pcid",
+ "sse4_1",
+ "sse4_2",
+ "x2apic",
+ "movbe",
+ "popcnt",
+ "tsc_deadline_timer",
+ "aes",
+ "xsave",
+ "avx",
+ "f16c",
+ "rdrand",
+ "lahf_lm",
+ "abm",
+ "epb",
+ "tpr_shadow",
+ "vnmi",
+ "flexpriority",
+ "ept",
+ "vpid",
+ "fsgsbase",
+ "tsc_adjust",
+ "bmi1",
+ "avx2",
+ "smep",
+ "bmi2",
+ "erms",
+ "invpcid",
+ "xsaveopt",
+ "dtherm",
+ "ida",
+ "arat",
+ "pln",
+ "pts"
+ ]
+ },
+ "7": {
+ "vendor_id": "GenuineIntel",
+ "family": "6",
+ "model": "60",
+ "model_name": "Intel(R) Core(TM) i7-4800MQ CPU @ 2.70GHz",
+ "stepping": "3",
+ "mhz": "2927.087",
+ "cache_size": "6144 KB",
+ "physical_id": "0",
+ "core_id": "3",
+ "cores": "4",
+ "flags": [
+ "fpu",
+ "vme",
+ "de",
+ "pse",
+ "tsc",
+ "msr",
+ "pae",
+ "mce",
+ "cx8",
+ "apic",
+ "sep",
+ "mtrr",
+ "pge",
+ "mca",
+ "cmov",
+ "pat",
+ "pse36",
+ "clflush",
+ "dts",
+ "acpi",
+ "mmx",
+ "fxsr",
+ "sse",
+ "sse2",
+ "ss",
+ "ht",
+ "tm",
+ "pbe",
+ "syscall",
+ "nx",
+ "pdpe1gb",
+ "rdtscp",
+ "lm",
+ "constant_tsc",
+ "arch_perfmon",
+ "pebs",
+ "bts",
+ "rep_good",
+ "nopl",
+ "xtopology",
+ "nonstop_tsc",
+ "aperfmperf",
+ "eagerfpu",
+ "pni",
+ "pclmulqdq",
+ "dtes64",
+ "monitor",
+ "ds_cpl",
+ "vmx",
+ "smx",
+ "est",
+ "tm2",
+ "ssse3",
+ "sdbg",
+ "fma",
+ "cx16",
+ "xtpr",
+ "pdcm",
+ "pcid",
+ "sse4_1",
+ "sse4_2",
+ "x2apic",
+ "movbe",
+ "popcnt",
+ "tsc_deadline_timer",
+ "aes",
+ "xsave",
+ "avx",
+ "f16c",
+ "rdrand",
+ "lahf_lm",
+ "abm",
+ "epb",
+ "tpr_shadow",
+ "vnmi",
+ "flexpriority",
+ "ept",
+ "vpid",
+ "fsgsbase",
+ "tsc_adjust",
+ "bmi1",
+ "avx2",
+ "smep",
+ "bmi2",
+ "erms",
+ "invpcid",
+ "xsaveopt",
+ "dtherm",
+ "ida",
+ "arat",
+ "pln",
+ "pts"
+ ]
+ },
+ "total": 8,
+ "real": 1,
+ "cores": 4
+ },
+ "etc": {
+ "passwd": {
+ "root": {
+ "dir": "/root",
+ "gid": 0,
+ "uid": 0,
+ "shell": "/bin/bash",
+ "gecos": "root"
+ },
+ "bin": {
+ "dir": "/bin",
+ "gid": 1,
+ "uid": 1,
+ "shell": "/sbin/nologin",
+ "gecos": "bin"
+ },
+ "daemon": {
+ "dir": "/sbin",
+ "gid": 2,
+ "uid": 2,
+ "shell": "/sbin/nologin",
+ "gecos": "daemon"
+ },
+ "adm": {
+ "dir": "/var/adm",
+ "gid": 4,
+ "uid": 3,
+ "shell": "/sbin/nologin",
+ "gecos": "adm"
+ },
+ "lp": {
+ "dir": "/var/spool/lpd",
+ "gid": 7,
+ "uid": 4,
+ "shell": "/sbin/nologin",
+ "gecos": "lp"
+ },
+ "sync": {
+ "dir": "/sbin",
+ "gid": 0,
+ "uid": 5,
+ "shell": "/bin/sync",
+ "gecos": "sync"
+ },
+ "shutdown": {
+ "dir": "/sbin",
+ "gid": 0,
+ "uid": 6,
+ "shell": "/sbin/shutdown",
+ "gecos": "shutdown"
+ },
+ "halt": {
+ "dir": "/sbin",
+ "gid": 0,
+ "uid": 7,
+ "shell": "/sbin/halt",
+ "gecos": "halt"
+ },
+ "mail": {
+ "dir": "/var/spool/mail",
+ "gid": 12,
+ "uid": 8,
+ "shell": "/sbin/nologin",
+ "gecos": "mail"
+ },
+ "operator": {
+ "dir": "/root",
+ "gid": 0,
+ "uid": 11,
+ "shell": "/sbin/nologin",
+ "gecos": "operator"
+ },
+ "games": {
+ "dir": "/usr/games",
+ "gid": 100,
+ "uid": 12,
+ "shell": "/sbin/nologin",
+ "gecos": "games"
+ },
+ "ftp": {
+ "dir": "/var/ftp",
+ "gid": 50,
+ "uid": 14,
+ "shell": "/sbin/nologin",
+ "gecos": "FTP User"
+ },
+ "nobody": {
+ "dir": "/",
+ "gid": 99,
+ "uid": 99,
+ "shell": "/sbin/nologin",
+ "gecos": "Nobody"
+ },
+ "avahi-autoipd": {
+ "dir": "/var/lib/avahi-autoipd",
+ "gid": 170,
+ "uid": 170,
+ "shell": "/sbin/nologin",
+ "gecos": "Avahi IPv4LL Stack"
+ },
+ "dbus": {
+ "dir": "/",
+ "gid": 81,
+ "uid": 81,
+ "shell": "/sbin/nologin",
+ "gecos": "System message bus"
+ },
+ "polkitd": {
+ "dir": "/",
+ "gid": 999,
+ "uid": 999,
+ "shell": "/sbin/nologin",
+ "gecos": "User for polkitd"
+ },
+ "abrt": {
+ "dir": "/etc/abrt",
+ "gid": 173,
+ "uid": 173,
+ "shell": "/sbin/nologin",
+ "gecos": ""
+ },
+ "usbmuxd": {
+ "dir": "/",
+ "gid": 113,
+ "uid": 113,
+ "shell": "/sbin/nologin",
+ "gecos": "usbmuxd user"
+ },
+ "colord": {
+ "dir": "/var/lib/colord",
+ "gid": 998,
+ "uid": 998,
+ "shell": "/sbin/nologin",
+ "gecos": "User for colord"
+ },
+ "geoclue": {
+ "dir": "/var/lib/geoclue",
+ "gid": 997,
+ "uid": 997,
+ "shell": "/sbin/nologin",
+ "gecos": "User for geoclue"
+ },
+ "rpc": {
+ "dir": "/var/lib/rpcbind",
+ "gid": 32,
+ "uid": 32,
+ "shell": "/sbin/nologin",
+ "gecos": "Rpcbind Daemon"
+ },
+ "rpcuser": {
+ "dir": "/var/lib/nfs",
+ "gid": 29,
+ "uid": 29,
+ "shell": "/sbin/nologin",
+ "gecos": "RPC Service User"
+ },
+ "nfsnobody": {
+ "dir": "/var/lib/nfs",
+ "gid": 65534,
+ "uid": 65534,
+ "shell": "/sbin/nologin",
+ "gecos": "Anonymous NFS User"
+ },
+ "qemu": {
+ "dir": "/",
+ "gid": 107,
+ "uid": 107,
+ "shell": "/sbin/nologin",
+ "gecos": "qemu user"
+ },
+ "rtkit": {
+ "dir": "/proc",
+ "gid": 172,
+ "uid": 172,
+ "shell": "/sbin/nologin",
+ "gecos": "RealtimeKit"
+ },
+ "radvd": {
+ "dir": "/",
+ "gid": 75,
+ "uid": 75,
+ "shell": "/sbin/nologin",
+ "gecos": "radvd user"
+ },
+ "tss": {
+ "dir": "/dev/null",
+ "gid": 59,
+ "uid": 59,
+ "shell": "/sbin/nologin",
+ "gecos": "Account used by the trousers package to sandbox the tcsd daemon"
+ },
+ "unbound": {
+ "dir": "/etc/unbound",
+ "gid": 995,
+ "uid": 996,
+ "shell": "/sbin/nologin",
+ "gecos": "Unbound DNS resolver"
+ },
+ "openvpn": {
+ "dir": "/etc/openvpn",
+ "gid": 994,
+ "uid": 995,
+ "shell": "/sbin/nologin",
+ "gecos": "OpenVPN"
+ },
+ "saslauth": {
+ "dir": "/run/saslauthd",
+ "gid": 76,
+ "uid": 994,
+ "shell": "/sbin/nologin",
+ "gecos": "\"Saslauthd user\""
+ },
+ "avahi": {
+ "dir": "/var/run/avahi-daemon",
+ "gid": 70,
+ "uid": 70,
+ "shell": "/sbin/nologin",
+ "gecos": "Avahi mDNS/DNS-SD Stack"
+ },
+ "pulse": {
+ "dir": "/var/run/pulse",
+ "gid": 992,
+ "uid": 993,
+ "shell": "/sbin/nologin",
+ "gecos": "PulseAudio System Daemon"
+ },
+ "gdm": {
+ "dir": "/var/lib/gdm",
+ "gid": 42,
+ "uid": 42,
+ "shell": "/sbin/nologin",
+ "gecos": ""
+ },
+ "gnome-initial-setup": {
+ "dir": "/run/gnome-initial-setup/",
+ "gid": 990,
+ "uid": 992,
+ "shell": "/sbin/nologin",
+ "gecos": ""
+ },
+ "nm-openconnect": {
+ "dir": "/",
+ "gid": 989,
+ "uid": 991,
+ "shell": "/sbin/nologin",
+ "gecos": "NetworkManager user for OpenConnect"
+ },
+ "sshd": {
+ "dir": "/var/empty/sshd",
+ "gid": 74,
+ "uid": 74,
+ "shell": "/sbin/nologin",
+ "gecos": "Privilege-separated SSH"
+ },
+ "chrony": {
+ "dir": "/var/lib/chrony",
+ "gid": 988,
+ "uid": 990,
+ "shell": "/sbin/nologin",
+ "gecos": ""
+ },
+ "tcpdump": {
+ "dir": "/",
+ "gid": 72,
+ "uid": 72,
+ "shell": "/sbin/nologin",
+ "gecos": ""
+ },
+ "some_user": {
+ "dir": "/home/some_user",
+ "gid": 1000,
+ "uid": 1000,
+ "shell": "/bin/bash",
+ "gecos": "some_user"
+ },
+ "systemd-journal-gateway": {
+ "dir": "/var/log/journal",
+ "gid": 191,
+ "uid": 191,
+ "shell": "/sbin/nologin",
+ "gecos": "Journal Gateway"
+ },
+ "postgres": {
+ "dir": "/var/lib/pgsql",
+ "gid": 26,
+ "uid": 26,
+ "shell": "/bin/bash",
+ "gecos": "PostgreSQL Server"
+ },
+ "dockerroot": {
+ "dir": "/var/lib/docker",
+ "gid": 977,
+ "uid": 984,
+ "shell": "/sbin/nologin",
+ "gecos": "Docker User"
+ },
+ "apache": {
+ "dir": "/usr/share/httpd",
+ "gid": 48,
+ "uid": 48,
+ "shell": "/sbin/nologin",
+ "gecos": "Apache"
+ },
+ "systemd-network": {
+ "dir": "/",
+ "gid": 974,
+ "uid": 982,
+ "shell": "/sbin/nologin",
+ "gecos": "systemd Network Management"
+ },
+ "systemd-resolve": {
+ "dir": "/",
+ "gid": 973,
+ "uid": 981,
+ "shell": "/sbin/nologin",
+ "gecos": "systemd Resolver"
+ },
+ "systemd-bus-proxy": {
+ "dir": "/",
+ "gid": 972,
+ "uid": 980,
+ "shell": "/sbin/nologin",
+ "gecos": "systemd Bus Proxy"
+ },
+ "systemd-journal-remote": {
+ "dir": "//var/log/journal/remote",
+ "gid": 970,
+ "uid": 979,
+ "shell": "/sbin/nologin",
+ "gecos": "Journal Remote"
+ },
+ "systemd-journal-upload": {
+ "dir": "//var/log/journal/upload",
+ "gid": 969,
+ "uid": 978,
+ "shell": "/sbin/nologin",
+ "gecos": "Journal Upload"
+ },
+ "setroubleshoot": {
+ "dir": "/var/lib/setroubleshoot",
+ "gid": 967,
+ "uid": 977,
+ "shell": "/sbin/nologin",
+ "gecos": ""
+ },
+ "oprofile": {
+ "dir": "/var/lib/oprofile",
+ "gid": 16,
+ "uid": 16,
+ "shell": "/sbin/nologin",
+ "gecos": "Special user account to be used by OProfile"
+ }
+ },
+ "group": {
+ "root": {
+ "gid": 0,
+ "members": [
+
+ ]
+ },
+ "bin": {
+ "gid": 1,
+ "members": [
+
+ ]
+ },
+ "daemon": {
+ "gid": 2,
+ "members": [
+
+ ]
+ },
+ "sys": {
+ "gid": 3,
+ "members": [
+
+ ]
+ },
+ "adm": {
+ "gid": 4,
+ "members": [
+ "logcheck"
+ ]
+ },
+ "tty": {
+ "gid": 5,
+ "members": [
+
+ ]
+ },
+ "disk": {
+ "gid": 6,
+ "members": [
+
+ ]
+ },
+ "lp": {
+ "gid": 7,
+ "members": [
+
+ ]
+ },
+ "mem": {
+ "gid": 8,
+ "members": [
+
+ ]
+ },
+ "kmem": {
+ "gid": 9,
+ "members": [
+
+ ]
+ },
+ "wheel": {
+ "gid": 10,
+ "members": [
+
+ ]
+ },
+ "cdrom": {
+ "gid": 11,
+ "members": [
+
+ ]
+ },
+ "mail": {
+ "gid": 12,
+ "members": [
+
+ ]
+ },
+ "man": {
+ "gid": 15,
+ "members": [
+
+ ]
+ },
+ "dialout": {
+ "gid": 18,
+ "members": [
+ "lirc"
+ ]
+ },
+ "floppy": {
+ "gid": 19,
+ "members": [
+
+ ]
+ },
+ "games": {
+ "gid": 20,
+ "members": [
+
+ ]
+ },
+ "tape": {
+ "gid": 30,
+ "members": [
+
+ ]
+ },
+ "video": {
+ "gid": 39,
+ "members": [
+
+ ]
+ },
+ "ftp": {
+ "gid": 50,
+ "members": [
+
+ ]
+ },
+ "lock": {
+ "gid": 54,
+ "members": [
+ "lirc"
+ ]
+ },
+ "audio": {
+ "gid": 63,
+ "members": [
+
+ ]
+ },
+ "nobody": {
+ "gid": 99,
+ "members": [
+
+ ]
+ },
+ "users": {
+ "gid": 100,
+ "members": [
+
+ ]
+ },
+ "utmp": {
+ "gid": 22,
+ "members": [
+
+ ]
+ },
+ "utempter": {
+ "gid": 35,
+ "members": [
+
+ ]
+ },
+ "avahi-autoipd": {
+ "gid": 170,
+ "members": [
+
+ ]
+ },
+ "systemd-journal": {
+ "gid": 190,
+ "members": [
+
+ ]
+ },
+ "dbus": {
+ "gid": 81,
+ "members": [
+
+ ]
+ },
+ "polkitd": {
+ "gid": 999,
+ "members": [
+
+ ]
+ },
+ "abrt": {
+ "gid": 173,
+ "members": [
+
+ ]
+ },
+ "dip": {
+ "gid": 40,
+ "members": [
+
+ ]
+ },
+ "usbmuxd": {
+ "gid": 113,
+ "members": [
+
+ ]
+ },
+ "colord": {
+ "gid": 998,
+ "members": [
+
+ ]
+ },
+ "geoclue": {
+ "gid": 997,
+ "members": [
+
+ ]
+ },
+ "ssh_keys": {
+ "gid": 996,
+ "members": [
+
+ ]
+ },
+ "rpc": {
+ "gid": 32,
+ "members": [
+
+ ]
+ },
+ "rpcuser": {
+ "gid": 29,
+ "members": [
+
+ ]
+ },
+ "nfsnobody": {
+ "gid": 65534,
+ "members": [
+
+ ]
+ },
+ "kvm": {
+ "gid": 36,
+ "members": [
+ "qemu"
+ ]
+ },
+ "qemu": {
+ "gid": 107,
+ "members": [
+
+ ]
+ },
+ "rtkit": {
+ "gid": 172,
+ "members": [
+
+ ]
+ },
+ "radvd": {
+ "gid": 75,
+ "members": [
+
+ ]
+ },
+ "tss": {
+ "gid": 59,
+ "members": [
+
+ ]
+ },
+ "unbound": {
+ "gid": 995,
+ "members": [
+
+ ]
+ },
+ "openvpn": {
+ "gid": 994,
+ "members": [
+
+ ]
+ },
+ "saslauth": {
+ "gid": 76,
+ "members": [
+
+ ]
+ },
+ "avahi": {
+ "gid": 70,
+ "members": [
+
+ ]
+ },
+ "brlapi": {
+ "gid": 993,
+ "members": [
+
+ ]
+ },
+ "pulse": {
+ "gid": 992,
+ "members": [
+
+ ]
+ },
+ "pulse-access": {
+ "gid": 991,
+ "members": [
+
+ ]
+ },
+ "gdm": {
+ "gid": 42,
+ "members": [
+
+ ]
+ },
+ "gnome-initial-setup": {
+ "gid": 990,
+ "members": [
+
+ ]
+ },
+ "nm-openconnect": {
+ "gid": 989,
+ "members": [
+
+ ]
+ },
+ "sshd": {
+ "gid": 74,
+ "members": [
+
+ ]
+ },
+ "slocate": {
+ "gid": 21,
+ "members": [
+
+ ]
+ },
+ "chrony": {
+ "gid": 988,
+ "members": [
+
+ ]
+ },
+ "tcpdump": {
+ "gid": 72,
+ "members": [
+
+ ]
+ },
+ "some_user": {
+ "gid": 1000,
+ "members": [
+ "some_user"
+ ]
+ },
+ "docker": {
+ "gid": 986,
+ "members": [
+ "some_user"
+ ]
+ }
+ },
+ "c": {
+ "gcc": {
+ "target": "x86_64-redhat-linux",
+ "configured_with": "../configure --enable-bootstrap --enable-languages=c,c++,objc,obj-c++,fortran,ada,go,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array --disable-libgcj --with-isl --enable-libmpx --enable-gnu-indirect-function --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux",
+ "thread_model": "posix",
+ "description": "gcc version 6.3.1 20161221 (Red Hat 6.3.1-1) (GCC) ",
+ "version": "6.3.1"
+ },
+ "glibc": {
+ "version": "2.24",
+ "description": "GNU C Library (GNU libc) stable release version 2.24, by Roland McGrath et al."
+ }
+ },
+ "lua": {
+ "version": "5.3.4"
+ },
+ "ruby": {
+ "platform": "x86_64-linux",
+ "version": "2.3.3",
+ "release_date": "2016-11-21",
+ "target": "x86_64-redhat-linux-gnu",
+ "target_cpu": "x86_64",
+ "target_vendor": "redhat",
+ "target_os": "linux",
+ "host": "x86_64-redhat-linux-gnu",
+ "host_cpu": "x86_64",
+ "host_os": "linux-gnu",
+ "host_vendor": "redhat",
+ "bin_dir": "/usr/bin",
+ "ruby_bin": "/usr/bin/ruby",
+ "gems_dir": "/home/some_user/.gem/ruby",
+ "gem_bin": "/usr/bin/gem"
+ }
+ },
+ "command": {
+ "ps": "ps -ef"
+ },
+ "root_group": "root",
+ "fips": {
+ "kernel": {
+ "enabled": false
+ }
+ },
+ "hostname": "myhostname",
+ "machinename": "myhostname",
+ "fqdn": "myhostname",
+ "domain": null,
+ "machine_id": "1234567abcede123456123456123456a",
+ "privateaddress": "192.168.1.100",
+ "keys": {
+ "ssh": {
+
+ }
+ },
+ "time": {
+ "timezone": "EDT"
+ },
+ "sessions": {
+ "by_session": {
+ "1918": {
+ "session": "1918",
+ "uid": "1000",
+ "user": "some_user",
+ "seat": null
+ },
+ "5": {
+ "session": "5",
+ "uid": "1000",
+ "user": "some_user",
+ "seat": "seat0"
+ },
+ "3": {
+ "session": "3",
+ "uid": "0",
+ "user": "root",
+ "seat": "seat0"
+ }
+ },
+ "by_user": {
+ "some_user": [
+ {
+ "session": "1918",
+ "uid": "1000",
+ "user": "some_user",
+ "seat": null
+ },
+ {
+ "session": "5",
+ "uid": "1000",
+ "user": "some_user",
+ "seat": "seat0"
+ }
+ ],
+ "root": [
+ {
+ "session": "3",
+ "uid": "0",
+ "user": "root",
+ "seat": "seat0"
+ }
+ ]
+ }
+ },
+ "hostnamectl": {
+ "static_hostname": "myhostname",
+ "icon_name": "computer-laptop",
+ "chassis": "laptop",
+ "machine_id": "24dc16bd7694404c825b517ab46d9d6b",
+ "machine_id": "12345123451234512345123451242323",
+ "boot_id": "3d5d5512341234123412341234123423",
+ "operating_system": "Fedora 25 (Workstation Edition)",
+ "cpe_os_name": "cpe",
+ "kernel": "Linux 4.9.14-200.fc25.x86_64",
+ "architecture": "x86-64"
+ },
+ "block_device": {
+ "dm-1": {
+ "size": "104857600",
+ "removable": "0",
+ "rotational": "0",
+ "physical_block_size": "512",
+ "logical_block_size": "512"
+ },
+ "loop1": {
+ "size": "209715200",
+ "removable": "0",
+ "rotational": "1",
+ "physical_block_size": "512",
+ "logical_block_size": "512"
+ },
+ "sr0": {
+ "size": "2097151",
+ "removable": "1",
+ "model": "DVD-RAM UJ8E2",
+ "rev": "SB01",
+ "state": "running",
+ "timeout": "30",
+ "vendor": "MATSHITA",
+ "queue_depth": "1",
+ "rotational": "1",
+ "physical_block_size": "512",
+ "logical_block_size": "512"
+ },
+ "dm-2": {
+ "size": "378093568",
+ "removable": "0",
+ "rotational": "0",
+ "physical_block_size": "512",
+ "logical_block_size": "512"
+ },
+ "loop2": {
+ "size": "4194304",
+ "removable": "0",
+ "rotational": "1",
+ "physical_block_size": "512",
+ "logical_block_size": "512"
+ },
+ "dm-0": {
+ "size": "16138240",
+ "removable": "0",
+ "rotational": "0",
+ "physical_block_size": "512",
+ "logical_block_size": "512"
+ },
+ "loop0": {
+ "size": "1024000",
+ "removable": "0",
+ "rotational": "1",
+ "physical_block_size": "512",
+ "logical_block_size": "512"
+ },
+ "sda": {
+ "size": "500118192",
+ "removable": "0",
+ "model": "SAMSUNG MZ7TD256",
+ "rev": "2L5Q",
+ "state": "running",
+ "timeout": "30",
+ "vendor": "ATA",
+ "queue_depth": "31",
+ "rotational": "0",
+ "physical_block_size": "512",
+ "logical_block_size": "512"
+ },
+ "dm-5": {
+ "size": "20971520",
+ "removable": "0",
+ "rotational": "1",
+ "physical_block_size": "512",
+ "logical_block_size": "512"
+ },
+ "dm-3": {
+ "size": "209715200",
+ "removable": "0",
+ "rotational": "1",
+ "physical_block_size": "512",
+ "logical_block_size": "512"
+ }
+ },
+ "sysconf": {
+ "LINK_MAX": 65000,
+ "_POSIX_LINK_MAX": 65000,
+ "MAX_CANON": 255,
+ "_POSIX_MAX_CANON": 255,
+ "MAX_INPUT": 255,
+ "_POSIX_MAX_INPUT": 255,
+ "NAME_MAX": 255,
+ "_POSIX_NAME_MAX": 255,
+ "PATH_MAX": 4096,
+ "_POSIX_PATH_MAX": 4096,
+ "PIPE_BUF": 4096,
+ "_POSIX_PIPE_BUF": 4096,
+ "SOCK_MAXBUF": null,
+ "_POSIX_ASYNC_IO": null,
+ "_POSIX_CHOWN_RESTRICTED": 1,
+ "_POSIX_NO_TRUNC": 1,
+ "_POSIX_PRIO_IO": null,
+ "_POSIX_SYNC_IO": null,
+ "_POSIX_VDISABLE": 0,
+ "ARG_MAX": 2097152,
+ "ATEXIT_MAX": 2147483647,
+ "CHAR_BIT": 8,
+ "CHAR_MAX": 127,
+ "CHAR_MIN": -128,
+ "CHILD_MAX": 62844,
+ "CLK_TCK": 100,
+ "INT_MAX": 2147483647,
+ "INT_MIN": -2147483648,
+ "IOV_MAX": 1024,
+ "LOGNAME_MAX": 256,
+ "LONG_BIT": 64,
+ "MB_LEN_MAX": 16,
+ "NGROUPS_MAX": 65536,
+ "NL_ARGMAX": 4096,
+ "NL_LANGMAX": 2048,
+ "NL_MSGMAX": 2147483647,
+ "NL_NMAX": 2147483647,
+ "NL_SETMAX": 2147483647,
+ "NL_TEXTMAX": 2147483647,
+ "NSS_BUFLEN_GROUP": 1024,
+ "NSS_BUFLEN_PASSWD": 1024,
+ "NZERO": 20,
+ "OPEN_MAX": 1024,
+ "PAGESIZE": 4096,
+ "PAGE_SIZE": 4096,
+ "PASS_MAX": 8192,
+ "PTHREAD_DESTRUCTOR_ITERATIONS": 4,
+ "PTHREAD_KEYS_MAX": 1024,
+ "PTHREAD_STACK_MIN": 16384,
+ "PTHREAD_THREADS_MAX": null,
+ "SCHAR_MAX": 127,
+ "SCHAR_MIN": -128,
+ "SHRT_MAX": 32767,
+ "SHRT_MIN": -32768,
+ "SSIZE_MAX": 32767,
+ "TTY_NAME_MAX": 32,
+ "TZNAME_MAX": 6,
+ "UCHAR_MAX": 255,
+ "UINT_MAX": 4294967295,
+ "UIO_MAXIOV": 1024,
+ "ULONG_MAX": 18446744073709551615,
+ "USHRT_MAX": 65535,
+ "WORD_BIT": 32,
+ "_AVPHYS_PAGES": 955772,
+ "_NPROCESSORS_CONF": 8,
+ "_NPROCESSORS_ONLN": 8,
+ "_PHYS_PAGES": 4027635,
+ "_POSIX_ARG_MAX": 2097152,
+ "_POSIX_ASYNCHRONOUS_IO": 200809,
+ "_POSIX_CHILD_MAX": 62844,
+ "_POSIX_FSYNC": 200809,
+ "_POSIX_JOB_CONTROL": 1,
+ "_POSIX_MAPPED_FILES": 200809,
+ "_POSIX_MEMLOCK": 200809,
+ "_POSIX_MEMLOCK_RANGE": 200809,
+ "_POSIX_MEMORY_PROTECTION": 200809,
+ "_POSIX_MESSAGE_PASSING": 200809,
+ "_POSIX_NGROUPS_MAX": 65536,
+ "_POSIX_OPEN_MAX": 1024,
+ "_POSIX_PII": null,
+ "_POSIX_PII_INTERNET": null,
+ "_POSIX_PII_INTERNET_DGRAM": null,
+ "_POSIX_PII_INTERNET_STREAM": null,
+ "_POSIX_PII_OSI": null,
+ "_POSIX_PII_OSI_CLTS": null,
+ "_POSIX_PII_OSI_COTS": null,
+ "_POSIX_PII_OSI_M": null,
+ "_POSIX_PII_SOCKET": null,
+ "_POSIX_PII_XTI": null,
+ "_POSIX_POLL": null,
+ "_POSIX_PRIORITIZED_IO": 200809,
+ "_POSIX_PRIORITY_SCHEDULING": 200809,
+ "_POSIX_REALTIME_SIGNALS": 200809,
+ "_POSIX_SAVED_IDS": 1,
+ "_POSIX_SELECT": null,
+ "_POSIX_SEMAPHORES": 200809,
+ "_POSIX_SHARED_MEMORY_OBJECTS": 200809,
+ "_POSIX_SSIZE_MAX": 32767,
+ "_POSIX_STREAM_MAX": 16,
+ "_POSIX_SYNCHRONIZED_IO": 200809,
+ "_POSIX_THREADS": 200809,
+ "_POSIX_THREAD_ATTR_STACKADDR": 200809,
+ "_POSIX_THREAD_ATTR_STACKSIZE": 200809,
+ "_POSIX_THREAD_PRIORITY_SCHEDULING": 200809,
+ "_POSIX_THREAD_PRIO_INHERIT": 200809,
+ "_POSIX_THREAD_PRIO_PROTECT": 200809,
+ "_POSIX_THREAD_ROBUST_PRIO_INHERIT": null,
+ "_POSIX_THREAD_ROBUST_PRIO_PROTECT": null,
+ "_POSIX_THREAD_PROCESS_SHARED": 200809,
+ "_POSIX_THREAD_SAFE_FUNCTIONS": 200809,
+ "_POSIX_TIMERS": 200809,
+ "TIMER_MAX": null,
+ "_POSIX_TZNAME_MAX": 6,
+ "_POSIX_VERSION": 200809,
+ "_T_IOV_MAX": null,
+ "_XOPEN_CRYPT": 1,
+ "_XOPEN_ENH_I18N": 1,
+ "_XOPEN_LEGACY": 1,
+ "_XOPEN_REALTIME": 1,
+ "_XOPEN_REALTIME_THREADS": 1,
+ "_XOPEN_SHM": 1,
+ "_XOPEN_UNIX": 1,
+ "_XOPEN_VERSION": 700,
+ "_XOPEN_XCU_VERSION": 4,
+ "_XOPEN_XPG2": 1,
+ "_XOPEN_XPG3": 1,
+ "_XOPEN_XPG4": 1,
+ "BC_BASE_MAX": 99,
+ "BC_DIM_MAX": 2048,
+ "BC_SCALE_MAX": 99,
+ "BC_STRING_MAX": 1000,
+ "CHARCLASS_NAME_MAX": 2048,
+ "COLL_WEIGHTS_MAX": 255,
+ "EQUIV_CLASS_MAX": null,
+ "EXPR_NEST_MAX": 32,
+ "LINE_MAX": 2048,
+ "POSIX2_BC_BASE_MAX": 99,
+ "POSIX2_BC_DIM_MAX": 2048,
+ "POSIX2_BC_SCALE_MAX": 99,
+ "POSIX2_BC_STRING_MAX": 1000,
+ "POSIX2_CHAR_TERM": 200809,
+ "POSIX2_COLL_WEIGHTS_MAX": 255,
+ "POSIX2_C_BIND": 200809,
+ "POSIX2_C_DEV": 200809,
+ "POSIX2_C_VERSION": 200809,
+ "POSIX2_EXPR_NEST_MAX": 32,
+ "POSIX2_FORT_DEV": null,
+ "POSIX2_FORT_RUN": null,
+ "_POSIX2_LINE_MAX": 2048,
+ "POSIX2_LINE_MAX": 2048,
+ "POSIX2_LOCALEDEF": 200809,
+ "POSIX2_RE_DUP_MAX": 32767,
+ "POSIX2_SW_DEV": 200809,
+ "POSIX2_UPE": null,
+ "POSIX2_VERSION": 200809,
+ "RE_DUP_MAX": 32767,
+ "PATH": "/usr/bin",
+ "CS_PATH": "/usr/bin",
+ "LFS_CFLAGS": null,
+ "LFS_LDFLAGS": null,
+ "LFS_LIBS": null,
+ "LFS_LINTFLAGS": null,
+ "LFS64_CFLAGS": "-D_LARGEFILE64_SOURCE",
+ "LFS64_LDFLAGS": null,
+ "LFS64_LIBS": null,
+ "LFS64_LINTFLAGS": "-D_LARGEFILE64_SOURCE",
+ "_XBS5_WIDTH_RESTRICTED_ENVS": "XBS5_LP64_OFF64",
+ "XBS5_WIDTH_RESTRICTED_ENVS": "XBS5_LP64_OFF64",
+ "_XBS5_ILP32_OFF32": null,
+ "XBS5_ILP32_OFF32_CFLAGS": null,
+ "XBS5_ILP32_OFF32_LDFLAGS": null,
+ "XBS5_ILP32_OFF32_LIBS": null,
+ "XBS5_ILP32_OFF32_LINTFLAGS": null,
+ "_XBS5_ILP32_OFFBIG": null,
+ "XBS5_ILP32_OFFBIG_CFLAGS": null,
+ "XBS5_ILP32_OFFBIG_LDFLAGS": null,
+ "XBS5_ILP32_OFFBIG_LIBS": null,
+ "XBS5_ILP32_OFFBIG_LINTFLAGS": null,
+ "_XBS5_LP64_OFF64": 1,
+ "XBS5_LP64_OFF64_CFLAGS": "-m64",
+ "XBS5_LP64_OFF64_LDFLAGS": "-m64",
+ "XBS5_LP64_OFF64_LIBS": null,
+ "XBS5_LP64_OFF64_LINTFLAGS": null,
+ "_XBS5_LPBIG_OFFBIG": null,
+ "XBS5_LPBIG_OFFBIG_CFLAGS": null,
+ "XBS5_LPBIG_OFFBIG_LDFLAGS": null,
+ "XBS5_LPBIG_OFFBIG_LIBS": null,
+ "XBS5_LPBIG_OFFBIG_LINTFLAGS": null,
+ "_POSIX_V6_ILP32_OFF32": null,
+ "POSIX_V6_ILP32_OFF32_CFLAGS": null,
+ "POSIX_V6_ILP32_OFF32_LDFLAGS": null,
+ "POSIX_V6_ILP32_OFF32_LIBS": null,
+ "POSIX_V6_ILP32_OFF32_LINTFLAGS": null,
+ "_POSIX_V6_WIDTH_RESTRICTED_ENVS": "POSIX_V6_LP64_OFF64",
+ "POSIX_V6_WIDTH_RESTRICTED_ENVS": "POSIX_V6_LP64_OFF64",
+ "_POSIX_V6_ILP32_OFFBIG": null,
+ "POSIX_V6_ILP32_OFFBIG_CFLAGS": null,
+ "POSIX_V6_ILP32_OFFBIG_LDFLAGS": null,
+ "POSIX_V6_ILP32_OFFBIG_LIBS": null,
+ "POSIX_V6_ILP32_OFFBIG_LINTFLAGS": null,
+ "_POSIX_V6_LP64_OFF64": 1,
+ "POSIX_V6_LP64_OFF64_CFLAGS": "-m64",
+ "POSIX_V6_LP64_OFF64_LDFLAGS": "-m64",
+ "POSIX_V6_LP64_OFF64_LIBS": null,
+ "POSIX_V6_LP64_OFF64_LINTFLAGS": null,
+ "_POSIX_V6_LPBIG_OFFBIG": null,
+ "POSIX_V6_LPBIG_OFFBIG_CFLAGS": null,
+ "POSIX_V6_LPBIG_OFFBIG_LDFLAGS": null,
+ "POSIX_V6_LPBIG_OFFBIG_LIBS": null,
+ "POSIX_V6_LPBIG_OFFBIG_LINTFLAGS": null,
+ "_POSIX_V7_ILP32_OFF32": null,
+ "POSIX_V7_ILP32_OFF32_CFLAGS": null,
+ "POSIX_V7_ILP32_OFF32_LDFLAGS": null,
+ "POSIX_V7_ILP32_OFF32_LIBS": null,
+ "POSIX_V7_ILP32_OFF32_LINTFLAGS": null,
+ "_POSIX_V7_WIDTH_RESTRICTED_ENVS": "POSIX_V7_LP64_OFF64",
+ "POSIX_V7_WIDTH_RESTRICTED_ENVS": "POSIX_V7_LP64_OFF64",
+ "_POSIX_V7_ILP32_OFFBIG": null,
+ "POSIX_V7_ILP32_OFFBIG_CFLAGS": null,
+ "POSIX_V7_ILP32_OFFBIG_LDFLAGS": null,
+ "POSIX_V7_ILP32_OFFBIG_LIBS": null,
+ "POSIX_V7_ILP32_OFFBIG_LINTFLAGS": null,
+ "_POSIX_V7_LP64_OFF64": 1,
+ "POSIX_V7_LP64_OFF64_CFLAGS": "-m64",
+ "POSIX_V7_LP64_OFF64_LDFLAGS": "-m64",
+ "POSIX_V7_LP64_OFF64_LIBS": null,
+ "POSIX_V7_LP64_OFF64_LINTFLAGS": null,
+ "_POSIX_V7_LPBIG_OFFBIG": null,
+ "POSIX_V7_LPBIG_OFFBIG_CFLAGS": null,
+ "POSIX_V7_LPBIG_OFFBIG_LDFLAGS": null,
+ "POSIX_V7_LPBIG_OFFBIG_LIBS": null,
+ "POSIX_V7_LPBIG_OFFBIG_LINTFLAGS": null,
+ "_POSIX_ADVISORY_INFO": 200809,
+ "_POSIX_BARRIERS": 200809,
+ "_POSIX_BASE": null,
+ "_POSIX_C_LANG_SUPPORT": null,
+ "_POSIX_C_LANG_SUPPORT_R": null,
+ "_POSIX_CLOCK_SELECTION": 200809,
+ "_POSIX_CPUTIME": 200809,
+ "_POSIX_THREAD_CPUTIME": 200809,
+ "_POSIX_DEVICE_SPECIFIC": null,
+ "_POSIX_DEVICE_SPECIFIC_R": null,
+ "_POSIX_FD_MGMT": null,
+ "_POSIX_FIFO": null,
+ "_POSIX_PIPE": null,
+ "_POSIX_FILE_ATTRIBUTES": null,
+ "_POSIX_FILE_LOCKING": null,
+ "_POSIX_FILE_SYSTEM": null,
+ "_POSIX_MONOTONIC_CLOCK": 200809,
+ "_POSIX_MULTI_PROCESS": null,
+ "_POSIX_SINGLE_PROCESS": null,
+ "_POSIX_NETWORKING": null,
+ "_POSIX_READER_WRITER_LOCKS": 200809,
+ "_POSIX_SPIN_LOCKS": 200809,
+ "_POSIX_REGEXP": 1,
+ "_REGEX_VERSION": null,
+ "_POSIX_SHELL": 1,
+ "_POSIX_SIGNALS": null,
+ "_POSIX_SPAWN": 200809,
+ "_POSIX_SPORADIC_SERVER": null,
+ "_POSIX_THREAD_SPORADIC_SERVER": null,
+ "_POSIX_SYSTEM_DATABASE": null,
+ "_POSIX_SYSTEM_DATABASE_R": null,
+ "_POSIX_TIMEOUTS": 200809,
+ "_POSIX_TYPED_MEMORY_OBJECTS": null,
+ "_POSIX_USER_GROUPS": null,
+ "_POSIX_USER_GROUPS_R": null,
+ "POSIX2_PBS": null,
+ "POSIX2_PBS_ACCOUNTING": null,
+ "POSIX2_PBS_LOCATE": null,
+ "POSIX2_PBS_TRACK": null,
+ "POSIX2_PBS_MESSAGE": null,
+ "SYMLOOP_MAX": null,
+ "STREAM_MAX": 16,
+ "AIO_LISTIO_MAX": null,
+ "AIO_MAX": null,
+ "AIO_PRIO_DELTA_MAX": 20,
+ "DELAYTIMER_MAX": 2147483647,
+ "HOST_NAME_MAX": 64,
+ "LOGIN_NAME_MAX": 256,
+ "MQ_OPEN_MAX": null,
+ "MQ_PRIO_MAX": 32768,
+ "_POSIX_DEVICE_IO": null,
+ "_POSIX_TRACE": null,
+ "_POSIX_TRACE_EVENT_FILTER": null,
+ "_POSIX_TRACE_INHERIT": null,
+ "_POSIX_TRACE_LOG": null,
+ "RTSIG_MAX": 32,
+ "SEM_NSEMS_MAX": null,
+ "SEM_VALUE_MAX": 2147483647,
+ "SIGQUEUE_MAX": 62844,
+ "FILESIZEBITS": 64,
+ "POSIX_ALLOC_SIZE_MIN": 4096,
+ "POSIX_REC_INCR_XFER_SIZE": null,
+ "POSIX_REC_MAX_XFER_SIZE": null,
+ "POSIX_REC_MIN_XFER_SIZE": 4096,
+ "POSIX_REC_XFER_ALIGN": 4096,
+ "SYMLINK_MAX": null,
+ "GNU_LIBC_VERSION": "glibc 2.24",
+ "GNU_LIBPTHREAD_VERSION": "NPTL 2.24",
+ "POSIX2_SYMLINKS": 1,
+ "LEVEL1_ICACHE_SIZE": 32768,
+ "LEVEL1_ICACHE_ASSOC": 8,
+ "LEVEL1_ICACHE_LINESIZE": 64,
+ "LEVEL1_DCACHE_SIZE": 32768,
+ "LEVEL1_DCACHE_ASSOC": 8,
+ "LEVEL1_DCACHE_LINESIZE": 64,
+ "LEVEL2_CACHE_SIZE": 262144,
+ "LEVEL2_CACHE_ASSOC": 8,
+ "LEVEL2_CACHE_LINESIZE": 64,
+ "LEVEL3_CACHE_SIZE": 6291456,
+ "LEVEL3_CACHE_ASSOC": 12,
+ "LEVEL3_CACHE_LINESIZE": 64,
+ "LEVEL4_CACHE_SIZE": 0,
+ "LEVEL4_CACHE_ASSOC": 0,
+ "LEVEL4_CACHE_LINESIZE": 0,
+ "IPV6": 200809,
+ "RAW_SOCKETS": 200809,
+ "_POSIX_IPV6": 200809,
+ "_POSIX_RAW_SOCKETS": 200809
+ },
+ "init_package": "systemd",
+ "shells": [
+ "/bin/sh",
+ "/bin/bash",
+ "/sbin/nologin",
+ "/usr/bin/sh",
+ "/usr/bin/bash",
+ "/usr/sbin/nologin",
+ "/usr/bin/zsh",
+ "/bin/zsh"
+ ],
+ "ohai_time": 1492535225.41052,
+ "cloud_v2": null,
+ "cloud": null
+}
+''' # noqa
+
+
+class TestOhaiCollector(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'ohai']
+ valid_subsets = ['ohai']
+ fact_namespace = 'ansible_ohai'
+ collector_class = OhaiFactCollector
+
+ def _mock_module(self):
+ mock_module = Mock()
+ mock_module.params = {'gather_subset': self.gather_subset,
+ 'gather_timeout': 10,
+ 'filter': '*'}
+ mock_module.get_bin_path = Mock(return_value='/not/actually/ohai')
+ mock_module.run_command = Mock(return_value=(0, ohai_json_output, ''))
+ return mock_module
+
+ @patch('ansible.module_utils.facts.other.ohai.OhaiFactCollector.get_ohai_output')
+ def test_bogus_json(self, mock_get_ohai_output):
+ module = self._mock_module()
+
+ # bogus json
+ mock_get_ohai_output.return_value = '{'
+
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module)
+
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict, {})
+
+ @patch('ansible.module_utils.facts.other.ohai.OhaiFactCollector.run_ohai')
+ def test_ohai_non_zero_return_code(self, mock_run_ohai):
+ module = self._mock_module()
+
+ # bogus json
+ mock_run_ohai.return_value = (1, '{}', '')
+
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module)
+
+ self.assertIsInstance(facts_dict, dict)
+
+ # This assumes no 'ohai' entry at all is correct
+ self.assertNotIn('ohai', facts_dict)
+ self.assertEqual(facts_dict, {})
diff --git a/test/units/module_utils/facts/system/__init__.py b/test/units/module_utils/facts/system/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/facts/system/__init__.py
diff --git a/test/units/module_utils/facts/system/distribution/__init__.py b/test/units/module_utils/facts/system/distribution/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/__init__.py
diff --git a/test/units/module_utils/facts/system/distribution/conftest.py b/test/units/module_utils/facts/system/distribution/conftest.py
new file mode 100644
index 0000000..d27b97f
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/conftest.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
+
+
+import pytest
+
+from units.compat.mock import Mock
+
+
+@pytest.fixture
+def mock_module():
+ mock_module = Mock()
+ mock_module.params = {'gather_subset': ['all'],
+ 'gather_timeout': 5,
+ 'filter': '*'}
+ mock_module.get_bin_path = Mock(return_value=None)
+ return mock_module
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/almalinux_8_3_beta.json b/test/units/module_utils/facts/system/distribution/fixtures/almalinux_8_3_beta.json
new file mode 100644
index 0000000..2d8df50
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/almalinux_8_3_beta.json
@@ -0,0 +1,53 @@
+{
+ "name": "AlmaLinux 8.3",
+ "distro": {
+ "codename": "Purple Manul",
+ "id": "almalinux",
+ "name": "AlmaLinux",
+ "version": "8.3",
+ "version_best": "8.3",
+ "lsb_release_info": {
+ "lsb_version": ":core-4.1-amd64:core-4.1-noarch",
+ "distributor_id": "AlmaLinux",
+ "description": "AlmaLinux release 8.3 Beta (Purple Manul)",
+ "release": "8.3",
+ "codename": "PurpleManul"
+ },
+ "os_release_info": {
+ "name": "AlmaLinux",
+ "version": "8.3 (Purple Manul)",
+ "id": "almalinux",
+ "id_like": "rhel centos fedora",
+ "version_id": "8.3",
+ "platform_id": "platform:el8",
+ "pretty_name": "AlmaLinux 8.3 Beta (Purple Manul)",
+ "ansi_color": "0;34",
+ "cpe_name": "cpe:/o:almalinux:almalinux:8.3:beta",
+ "home_url": "https://almalinux.org/",
+ "bug_report_url": "https://bugs.almalinux.org/",
+ "almalinux_mantisbt_project": "AlmaLinux-8",
+ "almalinux_mantisbt_project_version": "8",
+ "codename": "Purple Manul"
+ }
+ },
+ "input": {
+ "/etc/centos-release": "AlmaLinux release 8.3 Beta (Purple Manul)\n",
+ "/etc/redhat-release": "AlmaLinux release 8.3 Beta (Purple Manul)\n",
+ "/etc/system-release": "AlmaLinux release 8.3 Beta (Purple Manul)\n",
+ "/etc/os-release": "NAME=\"AlmaLinux\"\nVERSION=\"8.3 (Purple Manul)\"\nID=\"almalinux\"\nID_LIKE=\"rhel centos fedora\"\nVERSION_ID=\"8.3\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"AlmaLinux 8.3 Beta (Purple Manul)\"\nANSI_COLOR=\"0;34\"\nCPE_NAME=\"cpe:/o:almalinux:almalinux:8.3:beta\"\nHOME_URL=\"https://almalinux.org/\"\nBUG_REPORT_URL=\"https://bugs.almalinux.org/\"\n\nALMALINUX_MANTISBT_PROJECT=\"AlmaLinux-8\" \nALMALINUX_MANTISBT_PROJECT_VERSION=\"8\" \n\n",
+ "/usr/lib/os-release": "NAME=\"AlmaLinux\"\nVERSION=\"8.3 (Purple Manul)\"\nID=\"almalinux\"\nID_LIKE=\"rhel centos fedora\"\nVERSION_ID=\"8.3\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"AlmaLinux 8.3 Beta (Purple Manul)\"\nANSI_COLOR=\"0;34\"\nCPE_NAME=\"cpe:/o:almalinux:almalinux:8.3:beta\"\nHOME_URL=\"https://almalinux.org/\"\nBUG_REPORT_URL=\"https://bugs.almalinux.org/\"\n\nALMALINUX_MANTISBT_PROJECT=\"AlmaLinux-8\" \nALMALINUX_MANTISBT_PROJECT_VERSION=\"8\" \n\n"
+ },
+ "platform.dist": [
+ "almalinux",
+ "8.3",
+ "Purple Manul"
+ ],
+ "result": {
+ "distribution": "AlmaLinux",
+ "distribution_version": "8.3",
+ "distribution_release": "Purple Manul",
+ "distribution_major_version": "8",
+ "os_family": "RedHat"
+ },
+ "platform.release": "4.18.0-240.el8.x86_64"
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2.json b/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2.json
new file mode 100644
index 0000000..d98070e
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2.json
@@ -0,0 +1,39 @@
+{
+ "platform.dist": [
+ "amzn",
+ "2",
+ ""
+ ],
+ "input": {
+ "/etc/os-release": "NAME=\"Amazon Linux\"\nVERSION=\"2\"\nID=\"amzn\"\nID_LIKE=\"centos rhel fedora\"\nVERSION_ID=\"2\"\nPRETTY_NAME=\"Amazon Linux 2\"\nANSI_COLOR=\"0;33\"\nCPE_NAME=\"cpe:2.3:o:amazon:amazon_linux:2\"\nHOME_URL=\"https://amazonlinux.com/\"\n",
+ "/etc/system-release": "Amazon Linux release 2 (Karoo)\n"
+ },
+ "name": "Amazon 2",
+ "result": {
+ "distribution_release": "NA",
+ "distribution": "Amazon",
+ "distribution_major_version": "2",
+ "os_family": "RedHat",
+ "distribution_version": "2"
+ },
+ "distro": {
+ "id": "amzn",
+ "name": "Amazon Linux",
+ "version": "2",
+ "codename": "",
+ "version_best": "2",
+ "lsb_release_info": {},
+ "os_release_info": {
+ "ansi_color": "0;33",
+ "id_like": "centos rhel fedora",
+ "version_id": "2",
+ "pretty_name": "Amazon Linux 2",
+ "name": "Amazon Linux",
+ "version": "2",
+ "home_url": "https://amazonlinux.com/",
+ "id": "amzn",
+ "cpe_name": "cpe:2.3:o:amazon:amazon_linux:2"
+ }
+ },
+ "platform.release": "4.14.181-142.260.amzn2.x86_64"
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2016.03.json b/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2016.03.json
new file mode 100644
index 0000000..38449e4
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2016.03.json
@@ -0,0 +1,40 @@
+{
+ "name": "Amazon 2016.03",
+ "platform.release": "4.14.94-73.73.amzn1.x86_64",
+ "result": {
+ "distribution_release": "NA",
+ "distribution": "Amazon",
+ "distribution_major_version": "2016",
+ "distribution_minor_version": "03",
+ "os_family": "RedHat",
+ "distribution_version": "2016.03"
+ },
+ "platform.dist": [
+ "amzn",
+ "2016.03",
+ ""
+ ],
+ "input": {
+ "/etc/os-release": "NAME=\"Amazon Linux AMI\"\nVERSION=\"2016.03\"\nID=\"amzn\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"2016.03\"\nPRETTY_NAME=\"Amazon Linux AMI 2016.03\"\nANSI_COLOR=\"0;33\"\nCPE_NAME=\"cpe:/o:amazon:linux:2016.03:ga\"\nHOME_URL=\"http://aws.amazon.com/amazon-linux-ami/\"\n",
+ "/etc/system-release": "Amazon Linux AMI release 2016.03\n"
+ },
+ "distro": {
+ "version_best": "2016.03",
+ "os_release_info": {
+ "name": "Amazon Linux AMI",
+ "ansi_color": "0;33",
+ "id_like": "rhel fedora",
+ "version_id": "2016.03",
+ "pretty_name": "Amazon Linux AMI 2016.03",
+ "version": "2016.03",
+ "home_url": "http://aws.amazon.com/amazon-linux-ami/",
+ "cpe_name": "cpe:/o:amazon:linux:2016.03:ga",
+ "id": "amzn"
+ },
+ "version": "2016.03",
+ "codename": "",
+ "lsb_release_info": {},
+ "id": "amzn",
+ "name": "Amazon Linux AMI"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2018.03.json b/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2018.03.json
new file mode 100644
index 0000000..2461e72
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2018.03.json
@@ -0,0 +1,40 @@
+{
+ "name": "Amazon 2018.03",
+ "platform.release": "4.14.94-73.73.amzn1.x86_64",
+ "result": {
+ "distribution_release": "NA",
+ "distribution": "Amazon",
+ "distribution_major_version": "2018",
+ "distribution_minor_version": "03",
+ "os_family": "RedHat",
+ "distribution_version": "2018.03"
+ },
+ "platform.dist": [
+ "amzn",
+ "2018.03",
+ ""
+ ],
+ "input": {
+ "/etc/os-release": "NAME=\"Amazon Linux AMI\"\nVERSION=\"2018.03\"\nID=\"amzn\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"2018.03\"\nPRETTY_NAME=\"Amazon Linux AMI 2018.03\"\nANSI_COLOR=\"0;33\"\nCPE_NAME=\"cpe:/o:amazon:linux:2018.03:ga\"\nHOME_URL=\"http://aws.amazon.com/amazon-linux-ami/\"\n",
+ "/etc/system-release": "Amazon Linux AMI release 2018.03\n"
+ },
+ "distro": {
+ "version_best": "2018.03",
+ "os_release_info": {
+ "name": "Amazon Linux AMI",
+ "ansi_color": "0;33",
+ "id_like": "rhel fedora",
+ "version_id": "2018.03",
+ "pretty_name": "Amazon Linux AMI 2018.03",
+ "version": "2018.03",
+ "home_url": "http://aws.amazon.com/amazon-linux-ami/",
+ "cpe_name": "cpe:/o:amazon:linux:2018.03:ga",
+ "id": "amzn"
+ },
+ "version": "2018.03",
+ "codename": "",
+ "lsb_release_info": {},
+ "id": "amzn",
+ "name": "Amazon Linux AMI"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2_karoo.json b/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2_karoo.json
new file mode 100644
index 0000000..e430ff6
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_2_karoo.json
@@ -0,0 +1,34 @@
+{
+ "platform.dist": [
+ "",
+ "",
+ ""
+ ],
+ "input": {
+ "/etc/system-release": "Amazon Linux release 2 (Karoo)",
+ "/etc/os-release": ""
+ },
+ "name": "Amazon Linux 2 - Karoo",
+ "result": {
+ "distribution_release": "NA",
+ "distribution": "Amazon",
+ "distribution_major_version": "2",
+ "os_family": "RedHat",
+ "distribution_version": "2"
+ },
+ "distro": {
+ "id": "amzn",
+ "version": "2",
+ "codename": "",
+ "os_release_info": {
+ "name": "Amazon Linux AMI",
+ "ansi_color": "0;33",
+ "id_like": "rhel fedora",
+ "version_id": "2",
+ "pretty_name": "Amazon Linux release 2 (Karoo)",
+ "version": "2",
+ "home_url": "https://amazonlinux.com/",
+ "id": "amzn"
+ }
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_release_2.json b/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_release_2.json
new file mode 100644
index 0000000..9fa6090
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/amazon_linux_release_2.json
@@ -0,0 +1,34 @@
+{
+ "platform.dist": [
+ "",
+ "",
+ ""
+ ],
+ "input": {
+ "/etc/system-release": "Amazon Linux release 2",
+ "/etc/os-release": ""
+ },
+ "name": "Amazon Linux 2",
+ "result": {
+ "distribution_release": "NA",
+ "distribution": "Amazon",
+ "distribution_major_version": "2",
+ "os_family": "RedHat",
+ "distribution_version": "2"
+ },
+ "distro": {
+ "id": "amzn",
+ "version": "2",
+ "codename": "",
+ "os_release_info": {
+ "name": "Amazon Linux AMI",
+ "ansi_color": "0;33",
+ "id_like": "rhel fedora",
+ "version_id": "2",
+ "pretty_name": "Amazon Linux release 2",
+ "version": "2",
+ "home_url": "",
+ "id": "amzn"
+ }
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/arch_linux_na.json b/test/units/module_utils/facts/system/distribution/fixtures/arch_linux_na.json
new file mode 100644
index 0000000..88d9ad8
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/arch_linux_na.json
@@ -0,0 +1,24 @@
+{
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "arch",
+ "name": "Arch Linux",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"Arch Linux\"\nPRETTY_NAME=\"Arch Linux\"\nID=arch\nID_LIKE=archlinux\nANSI_COLOR=\"0;36\"\nHOME_URL=\"https://www.archlinux.org/\"\nSUPPORT_URL=\"https://bbs.archlinux.org/\"\nBUG_REPORT_URL=\"https://bugs.archlinux.org/\"\n\n",
+ "/etc/arch-release": ""
+ },
+ "name": "Arch Linux NA",
+ "result": {
+ "distribution_release": "NA",
+ "distribution": "Archlinux",
+ "distribution_major_version": "NA",
+ "os_family": "Archlinux",
+ "distribution_version": "NA"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/arch_linux_no_arch-release_na.json b/test/units/module_utils/facts/system/distribution/fixtures/arch_linux_no_arch-release_na.json
new file mode 100644
index 0000000..a24bb3a
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/arch_linux_no_arch-release_na.json
@@ -0,0 +1,23 @@
+{
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "arch",
+ "name": "Arch Linux",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"Arch Linux\"\nPRETTY_NAME=\"Arch Linux\"\nID=arch\nID_LIKE=archlinux\nANSI_COLOR=\"0;36\"\nHOME_URL=\"https://www.archlinux.org/\"\nSUPPORT_URL=\"https://bbs.archlinux.org/\"\nBUG_REPORT_URL=\"https://bugs.archlinux.org/\"\n\n"
+ },
+ "name": "Arch Linux no arch-release NA",
+ "result": {
+ "distribution_release": "NA",
+ "distribution": "Archlinux",
+ "distribution_major_version": "NA",
+ "os_family": "Archlinux",
+ "distribution_version": "NA"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/archlinux_rolling.json b/test/units/module_utils/facts/system/distribution/fixtures/archlinux_rolling.json
new file mode 100644
index 0000000..8f35636
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/archlinux_rolling.json
@@ -0,0 +1,31 @@
+{
+ "name": "Archlinux rolling",
+ "distro": {
+ "codename": "n/a",
+ "id": "arch",
+ "name": "Arch",
+ "version": "rolling",
+ "version_best": "rolling",
+ "lsb_release_info": {
+ "lsb_version": "1.4",
+ "distributor_id": "Arch",
+ "description": "Arch Linux",
+ "release": "rolling",
+ "codename": "n/a"
+ },
+ "os_release_info": {}
+ },
+ "input": {
+ "/etc/arch-release": "Arch Linux release\n",
+ "/etc/lsb-release": "LSB_VERSION=1.4\nDISTRIB_ID=Arch\nDISTRIB_RELEASE=rolling\nDISTRIB_DESCRIPTION=\"Arch Linux\"\n",
+ "/usr/lib/os-release": "NAME=\"Arch Linux\"\nPRETTY_NAME=\"Arch Linux\"\nID=arch\nBUILD_ID=rolling\nANSI_COLOR=\"0;36\"\nHOME_URL=\"https://www.archlinux.org/\"\nDOCUMENTATION_URL=\"https://wiki.archlinux.org/\"\nSUPPORT_URL=\"https://bbs.archlinux.org/\"\nBUG_REPORT_URL=\"https://bugs.archlinux.org/\"\nLOGO=archlinux\n"
+ },
+ "platform.dist": ["arch", "rolling", "n/a"],
+ "result": {
+ "distribution": "Archlinux",
+ "distribution_version": "rolling",
+ "distribution_release": "n/a",
+ "distribution_major_version": "rolling",
+ "os_family": "Archlinux"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/centos_6.7.json b/test/units/module_utils/facts/system/distribution/fixtures/centos_6.7.json
new file mode 100644
index 0000000..c99a073
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/centos_6.7.json
@@ -0,0 +1,31 @@
+{
+ "name": "CentOS 6.7",
+ "platform.dist": ["centos", "6.7", "Final"],
+ "distro": {
+ "codename": "Final",
+ "id": "centos",
+ "name": "CentOS Linux",
+ "version": "6.7",
+ "version_best": "6.7",
+ "os_release_info": {},
+ "lsb_release_info": {
+ "release": "6.7",
+ "codename": "Final",
+ "distributor_id": "CentOS",
+ "lsb_version": ":base-4.0-amd64:base-4.0-noarch:core-4.0-amd64:core-4.0-noarch",
+ "description": "CentOS release 6.7 (Final)"
+ }
+ },
+ "input": {
+ "/etc/redhat-release": "CentOS release 6.7 (Final)\n",
+ "/etc/lsb-release": "LSB_VERSION=base-4.0-amd64:base-4.0-noarch:core-4.0-amd64:core-4.0-noarch:graphics-4.0-amd64:graphics-4.0-noarch:printing-4.0-amd64:printing-4.0-noarch\n",
+ "/etc/system-release": "CentOS release 6.7 (Final)\n"
+ },
+ "result": {
+ "distribution_release": "Final",
+ "distribution": "CentOS",
+ "distribution_major_version": "6",
+ "os_family": "RedHat",
+ "distribution_version": "6.7"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/centos_8_1.json b/test/units/module_utils/facts/system/distribution/fixtures/centos_8_1.json
new file mode 100644
index 0000000..338959b
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/centos_8_1.json
@@ -0,0 +1,54 @@
+{
+ "name": "CentOS 8.1",
+ "distro": {
+ "codename": "Core",
+ "id": "centos",
+ "name": "CentOS Linux",
+ "version": "8",
+ "version_best": "8.1.1911",
+ "lsb_release_info": {
+ "lsb_version": ":core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch",
+ "distributor_id": "CentOS",
+ "description": "CentOS Linux release 8.1.1911 (Core)",
+ "release": "8.1.1911",
+ "codename": "Core"
+ },
+ "os_release_info": {
+ "name": "CentOS Linux",
+ "version": "8 (Core)",
+ "id": "centos",
+ "id_like": "rhel fedora",
+ "version_id": "8",
+ "platform_id": "platform:el8",
+ "pretty_name": "CentOS Linux 8 (Core)",
+ "ansi_color": "0;31",
+ "cpe_name": "cpe:/o:centos:centos:8",
+ "home_url": "https://www.centos.org/",
+ "bug_report_url": "https://bugs.centos.org/",
+ "centos_mantisbt_project": "CentOS-8",
+ "centos_mantisbt_project_version": "8",
+ "redhat_support_product": "centos",
+ "redhat_support_product_version": "8",
+ "codename": "Core"
+ }
+ },
+ "input": {
+ "/etc/centos-release": "CentOS Linux release 8.1.1911 (Core) \n",
+ "/etc/redhat-release": "CentOS Linux release 8.1.1911 (Core) \n",
+ "/etc/system-release": "CentOS Linux release 8.1.1911 (Core) \n",
+ "/etc/os-release": "NAME=\"CentOS Linux\"\nVERSION=\"8 (Core)\"\nID=\"centos\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"8\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"CentOS Linux 8 (Core)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:centos:centos:8\"\nHOME_URL=\"https://www.centos.org/\"\nBUG_REPORT_URL=\"https://bugs.centos.org/\"\n\nCENTOS_MANTISBT_PROJECT=\"CentOS-8\"\nCENTOS_MANTISBT_PROJECT_VERSION=\"8\"\nREDHAT_SUPPORT_PRODUCT=\"centos\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"8\"\n\n"
+ },
+ "platform.dist": [
+ "centos",
+ "8",
+ "Core"
+ ],
+ "result": {
+ "distribution": "CentOS",
+ "distribution_version": "8.1",
+ "distribution_release": "Core",
+ "distribution_major_version": "8",
+ "os_family": "RedHat"
+ },
+ "platform.release": "4.18.0-147.el8.x86_64"
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/centos_stream_8.json b/test/units/module_utils/facts/system/distribution/fixtures/centos_stream_8.json
new file mode 100644
index 0000000..1e4166b
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/centos_stream_8.json
@@ -0,0 +1,46 @@
+{
+ "name": "CentOS 8",
+ "distro": {
+ "codename": "",
+ "id": "centos",
+ "name": "CentOS Stream",
+ "version": "8",
+ "version_best": "8",
+ "lsb_release_info": {},
+ "os_release_info": {
+ "name": "CentOS Stream",
+ "version": "8",
+ "id": "centos",
+ "id_like": "rhel fedora",
+ "version_id": "8",
+ "platform_id": "platform:el8",
+ "pretty_name": "CentOS Stream 8",
+ "ansi_color": "0;31",
+ "cpe_name": "cpe:/o:centos:centos:8",
+ "home_url": "https://centos.org/",
+ "bug_report_url": "https://bugzilla.redhat.com/",
+ "redhat_support_product": "Red Hat Enterprise Linux 8",
+ "redhat_support_product_version": "CentOS Stream"
+ }
+ },
+ "input": {
+ "/etc/centos-release": "CentOS Stream release 8\n",
+ "/etc/redhat-release": "CentOS Stream release 8\n",
+ "/etc/system-release": "CentOS Stream release 8\n",
+ "/etc/os-release": "NAME=\"CentOS Stream\"\nVERSION=\"8\"\nID=\"centos\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"8\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"CentOS Stream 8\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:centos:centos:8\"\nHOME_URL=\"https://centos.org/\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\nREDHAT_SUPPORT_PRODUCT=\"Red Hat Enterprise Linux 8\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"CentOS Stream\"\n",
+ "/usr/lib/os-release": "NAME=\"CentOS Stream\"\nVERSION=\"8\"\nID=\"centos\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"8\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"CentOS Stream 8\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:centos:centos:8\"\nHOME_URL=\"https://centos.org/\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\nREDHAT_SUPPORT_PRODUCT=\"Red Hat Enterprise Linux 8\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"CentOS Stream\"\n"
+ },
+ "platform.dist": [
+ "centos",
+ "8",
+ ""
+ ],
+ "result": {
+ "distribution": "CentOS",
+ "distribution_version": "8",
+ "distribution_release": "Stream",
+ "distribution_major_version": "8",
+ "os_family": "RedHat"
+ },
+ "platform.release": "4.18.0-257.el8.x86_64"
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/clearlinux_26580.json b/test/units/module_utils/facts/system/distribution/fixtures/clearlinux_26580.json
new file mode 100644
index 0000000..1a99a86
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/clearlinux_26580.json
@@ -0,0 +1,24 @@
+{
+ "platform.dist": ["Clear Linux OS", "26580", "clear-linux-os"],
+ "distro": {
+ "codename": "",
+ "id": "clear-linux-os",
+ "name": "Clear Linux OS",
+ "version": "26580",
+ "version_best": "26580",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"Clear Linux OS\"\nVERSION=1\nID=clear-linux-os\nID_LIKE=clear-linux-os\nVERSION_ID=26580\nPRETTY_NAME=\"Clear Linux OS\"\nANSI_COLOR=\"1;35\"\nHOME_URL=\"https://clearlinux.org\"\nSUPPORT_URL=\"https://clearlinux.org\"\nBUG_REPORT_URL=\"mailto:dev@lists.clearlinux.org\"\nPRIVACY_POLICY_URL=\"http://www.intel.com/privacy\"",
+ "/usr/lib/os-release": "NAME=\"Clear Linux OS\"\nVERSION=1\nID=clear-linux-os\nID_LIKE=clear-linux-os\nVERSION_ID=26580\nPRETTY_NAME=\"Clear Linux OS\"\nANSI_COLOR=\"1;35\"\nHOME_URL=\"https://clearlinux.org\"\nSUPPORT_URL=\"https://clearlinux.org\"\nBUG_REPORT_URL=\"mailto:dev@lists.clearlinux.org\"\nPRIVACY_POLICY_URL=\"http://www.intel.com/privacy\""
+ },
+ "name": "ClearLinux 26580",
+ "result": {
+ "distribution_release": "clear-linux-os",
+ "distribution": "Clear Linux OS",
+ "distribution_major_version": "26580",
+ "os_family": "ClearLinux",
+ "distribution_version": "26580"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/clearlinux_28120.json b/test/units/module_utils/facts/system/distribution/fixtures/clearlinux_28120.json
new file mode 100644
index 0000000..30b7668
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/clearlinux_28120.json
@@ -0,0 +1,24 @@
+{
+ "platform.dist": ["Clear Linux OS", "28120", "clear-linux-os"],
+ "distro": {
+ "codename": "",
+ "id": "clear-linux-os",
+ "name": "Clear Linux OS",
+ "version": "28120",
+ "version_best": "28120",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"Clear Linux OS\"\nVERSION=1\nID=clear-linux-os\nID_LIKE=clear-linux-os\nVERSION_ID=28120\nPRETTY_NAME=\"Clear Linux OS\"\nANSI_COLOR=\"1;35\"\nHOME_URL=\"https://clearlinux.org\"\nSUPPORT_URL=\"https://clearlinux.org\"\nBUG_REPORT_URL=\"mailto:dev@lists.clearlinux.org\"\nPRIVACY_POLICY_URL=\"http://www.intel.com/privacy\"",
+ "/usr/lib/os-release": "NAME=\"Clear Linux OS\"\nVERSION=1\nID=clear-linux-os\nID_LIKE=clear-linux-os\nVERSION_ID=28120\nPRETTY_NAME=\"Clear Linux OS\"\nANSI_COLOR=\"1;35\"\nHOME_URL=\"https://clearlinux.org\"\nSUPPORT_URL=\"https://clearlinux.org\"\nBUG_REPORT_URL=\"mailto:dev@lists.clearlinux.org\"\nPRIVACY_POLICY_URL=\"http://www.intel.com/privacy\""
+ },
+ "name": "ClearLinux 28120",
+ "result": {
+ "distribution_release": "clear-linux-os",
+ "distribution": "Clear Linux OS",
+ "distribution_major_version": "28120",
+ "os_family": "ClearLinux",
+ "distribution_version": "28120"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/core_os_1911.5.0.json b/test/units/module_utils/facts/system/distribution/fixtures/core_os_1911.5.0.json
new file mode 100644
index 0000000..af43704
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/core_os_1911.5.0.json
@@ -0,0 +1,23 @@
+{
+ "name": "Core OS",
+ "input": {
+ "/usr/lib/os-release": "NAME=\"Container Linux by CoreOS\"\nID=coreos\nVERSION=1911.5.0\nVERSION_ID=1911.5.0\nBUILD_ID=2018-12-15-2317\nPRETTY_NAME=\"Container Linux by CoreOS 1911.5.0 (Rhyolite)\"\nANSI_COLOR=\"38;5;75\"\nHOME_URL=\"https://coreos.com/\"\nBUG_REPORT_URL=\"https://issues.coreos.com\"\nCOREOS_BOARD=\"amd64-usr\"",
+ "/etc/lsb-release": "DISTRIB_ID=CoreOS\nDISTRIB_RELEASE=1911.5.0\nDISTRIB_CODENAME=\"Rhyolite\"\nDISTRIB_DESCRIPTION=\"CoreOS 1911.5.0 (Rhyolite)\""
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "Rhyolite",
+ "id": "coreos",
+ "name": "CoreOS",
+ "version": "1911.5.0",
+ "version_best": "1911.5.0",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "platform.release": "",
+ "result": {
+ "distribution": "Coreos",
+ "distribution_major_version": "1911",
+ "distribution_version": "1911.5.0"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/core_os_976.0.0.json b/test/units/module_utils/facts/system/distribution/fixtures/core_os_976.0.0.json
new file mode 100644
index 0000000..ccd06d9
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/core_os_976.0.0.json
@@ -0,0 +1,23 @@
+{
+ "name": "Core OS",
+ "input": {
+ "/etc/os-release": "NAME=CoreOS\nID=coreos\nVERSION=976.0.0\nVERSION_ID=976.0.0\nBUILD_ID=2016-03-03-2324\nPRETTY_NAME=\"CoreOS 976.0.0 (Coeur Rouge)\"\nANSI_COLOR=\"1;32\"\nHOME_URL=\"https://coreos.com/\"\nBUG_REPORT_URL=\"https://github.com/coreos/bugs/issues\"",
+ "/etc/lsb-release": "DISTRIB_ID=CoreOS\nDISTRIB_RELEASE=976.0.0\nDISTRIB_CODENAME=\"Coeur Rouge\"\nDISTRIB_DESCRIPTION=\"CoreOS 976.0.0 (Coeur Rouge)\""
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "Coeur Rouge",
+ "id": "coreos",
+ "name": "CoreOS",
+ "version": "976.0.0",
+ "version_best": "976.0.0",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "platform.release": "",
+ "result": {
+ "distribution": "CoreOS",
+ "distribution_major_version": "976",
+ "distribution_version": "976.0.0"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/cumulus_linux_2.5.4.json b/test/units/module_utils/facts/system/distribution/fixtures/cumulus_linux_2.5.4.json
new file mode 100644
index 0000000..ad9c3f7
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/cumulus_linux_2.5.4.json
@@ -0,0 +1,23 @@
+{
+ "name": "Cumulus Linux 2.5.4",
+ "input": {
+ "/etc/os-release": "NAME=\"Cumulus Linux\"\nVERSION_ID=2.5.4\nVERSION=\"2.5.4-6dc6e80-201510091936-build\"\nPRETTY_NAME=\"Cumulus Linux\"\nID=cumulus-linux\nID_LIKE=debian\nCPE_NAME=cpe:/o:cumulusnetworks:cumulus_linux:2.5.4-6dc6e80-201510091936-build\nHOME_URL=\"http://www.cumulusnetworks.com/\"\nSUPPORT_URL=\"http://support.cumulusnetworks.com/\""
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "cumulus-linux",
+ "name": "Cumulus Linux",
+ "version": "2.5.4",
+ "version_best": "2.5.4",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "Cumulus Linux",
+ "distribution_major_version": "2",
+ "distribution_release": "2.5.4-6dc6e80-201510091936-build",
+ "os_family": "Debian",
+ "distribution_version": "2.5.4"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/cumulus_linux_3.7.3.json b/test/units/module_utils/facts/system/distribution/fixtures/cumulus_linux_3.7.3.json
new file mode 100644
index 0000000..ec44af1
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/cumulus_linux_3.7.3.json
@@ -0,0 +1,23 @@
+{
+ "name": "Cumulus Linux 3.7.3",
+ "input": {
+ "/etc/os-release": "NAME=\"Cumulus Linux\"\nVERSION_ID=3.7.3\nVERSION=\"Cumulus Linux 3.7.3\"\nPRETTY_NAME=\"Cumulus Linux\"\nID=cumulus-linux\nID_LIKE=debian\nCPE_NAME=cpe:/o:cumulusnetworks:cumulus_linux:3.7.3\nHOME_URL=\"http://www.cumulusnetworks.com/\"\nSUPPORT_URL=\"http://support.cumulusnetworks.com/\""
+ },
+ "platform.dist": ["debian", "8.11", ""],
+ "distro": {
+ "codename": "",
+ "id": "cumulus-linux",
+ "name": "Cumulus Linux",
+ "version": "3.7.3",
+ "version_best": "3.7.3",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "Cumulus Linux",
+ "distribution_major_version": "3",
+ "distribution_release": "Cumulus Linux 3.7.3",
+ "os_family": "Debian",
+ "distribution_version": "3.7.3"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/debian_10.json b/test/units/module_utils/facts/system/distribution/fixtures/debian_10.json
new file mode 100644
index 0000000..5ac3f45
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/debian_10.json
@@ -0,0 +1,42 @@
+{
+ "name": "Debian 10",
+ "distro": {
+ "codename": "buster",
+ "id": "debian",
+ "name": "Debian GNU/Linux",
+ "version": "10",
+ "version_best": "10",
+ "lsb_release_info": {
+ "distributor_id": "Debian",
+ "description": "Debian GNU/Linux 10 (buster)",
+ "release": "10",
+ "codename": "buster"
+ },
+ "os_release_info": {
+ "pretty_name": "Debian GNU/Linux 10 (buster)",
+ "name": "Debian GNU/Linux",
+ "version_id": "10",
+ "version": "10 (buster)",
+ "version_codename": "buster",
+ "id": "debian",
+ "home_url": "https://www.debian.org/",
+ "support_url": "https://www.debian.org/support",
+ "bug_report_url": "https://bugs.debian.org/",
+ "codename": "buster"
+ }
+ },
+ "input": {
+ "/etc/os-release": "PRETTY_NAME=\"Debian GNU/Linux 10 (buster)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"10\"\nVERSION=\"10 (buster)\"\nVERSION_CODENAME=buster\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n",
+ "/usr/lib/os-release": "PRETTY_NAME=\"Debian GNU/Linux 10 (buster)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"10\"\nVERSION=\"10 (buster)\"\nVERSION_CODENAME=buster\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"\n",
+ "/etc/debian_version": "10.7\n"
+ },
+ "platform.dist": ["debian", "10", "buster"],
+ "result": {
+ "distribution": "Debian",
+ "distribution_version": "10",
+ "distribution_release": "buster",
+ "distribution_major_version": "10",
+ "distribution_minor_version": "7",
+ "os_family": "Debian"
+ }
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/debian_7.9.json b/test/units/module_utils/facts/system/distribution/fixtures/debian_7.9.json
new file mode 100644
index 0000000..894c942
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/debian_7.9.json
@@ -0,0 +1,39 @@
+{
+ "name": "Debian 7.9",
+ "input": {
+ "/etc/os-release": "PRETTY_NAME=\"Debian GNU/Linux 7 (wheezy)\"\nNAME=\"Debian GNU/Linux\"\nVERSION_ID=\"7\"\nVERSION=\"7 (wheezy)\"\nID=debian\nANSI_COLOR=\"1;31\"\nHOME_URL=\"http://www.debian.org/\"\nSUPPORT_URL=\"http://www.debian.org/support/\"\nBUG_REPORT_URL=\"http://bugs.debian.org/\""
+ },
+ "platform.dist": ["debian", "7.9", ""],
+ "distro": {
+ "codename": "wheezy",
+ "id": "debian",
+ "name": "Debian GNU/Linux",
+ "version": "7",
+ "version_best": "7.9",
+ "os_release_info": {
+ "name": "Debian GNU/Linux",
+ "ansi_color": "1;31",
+ "support_url": "http://www.debian.org/support/",
+ "version_id": "7",
+ "bug_report_url": "http://bugs.debian.org/",
+ "pretty_name": "Debian GNU/Linux 7 (wheezy)",
+ "version": "7 (wheezy)",
+ "codename": "wheezy",
+ "home_url": "http://www.debian.org/",
+ "id": "debian"
+ },
+ "lsb_release_info": {
+ "release": "7.9",
+ "codename": "wheezy",
+ "distributor_id": "Debian",
+ "description": "Debian GNU/Linux 7.9 (wheezy)"
+ }
+ },
+ "result": {
+ "distribution": "Debian",
+ "distribution_major_version": "7",
+ "distribution_release": "wheezy",
+ "os_family": "Debian",
+ "distribution_version": "7.9"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/debian_stretch_sid.json b/test/units/module_utils/facts/system/distribution/fixtures/debian_stretch_sid.json
new file mode 100644
index 0000000..2338830
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/debian_stretch_sid.json
@@ -0,0 +1,36 @@
+{
+ "name": "Debian stretch/sid",
+ "input": {
+ "/etc/os-release": "PRETTY_NAME=\"Debian GNU/Linux stretch/sid\"\nNAME=\"Debian GNU/Linux\"\nID=debian\nHOME_URL=\"https://www.debian.org/\"\nSUPPORT_URL=\"https://www.debian.org/support\"\nBUG_REPORT_URL=\"https://bugs.debian.org/\"",
+ "/etc/debian_version": "stretch/sid\n"
+ },
+ "platform.dist": ["debian", "stretch/sid", ""],
+ "distro": {
+ "codename": "stretch",
+ "id": "debian",
+ "name": "Debian GNU/Linux",
+ "version": "9",
+ "version_best": "9.8",
+ "lsb_release_info": {
+ "release": "unstable",
+ "codename": "sid",
+ "distributor_id": "Debian",
+ "description": "Debian GNU/Linux stretch/sid"
+ },
+ "os_release_info": {
+ "name": "Debian GNU/Linux",
+ "support_url": "https://www.debian.org/support",
+ "bug_report_url": "https://bugs.debian.org/",
+ "pretty_name": "Debian GNU/Linux stretch/sid",
+ "home_url": "https://www.debian.org/",
+ "id": "debian"
+ }
+ },
+ "result": {
+ "distribution": "Debian",
+ "distribution_major_version": "9",
+ "distribution_release": "stretch",
+ "os_family": "Debian",
+ "distribution_version": "9.8"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/deepin_20.4.json b/test/units/module_utils/facts/system/distribution/fixtures/deepin_20.4.json
new file mode 100644
index 0000000..ca5d50d
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/deepin_20.4.json
@@ -0,0 +1,29 @@
+{
+ "name": "Deepin 20.4",
+ "distro": {
+ "codename": "apricot",
+ "id": "Deepin",
+ "name": "Deepin",
+ "version": "20.4",
+ "version_best": "20.4",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/os-release": "PRETTY_NAME=\"Deepin 20.4\"\nNAME=\"Deepin\"\nVERSION_ID=\"20.4\"\nVERSION=\"20.4\"\nVERSION_CODENAME=\"apricot\"\nID=Deepin\nHOME_URL=\"https://www.deepin.org/\"\nBUG_REPORT_URL=\"https://bbs.deepin.org/\"\n",
+ "/etc/lsb-release": "DISTRIB_ID=Deepin\nDISTRIB_RELEASE=20.4\nDISTRIB_DESCRIPTION=\"Deepin 20.4\"\nDISTRIB_CODENAME=apricot\n",
+ "/usr/lib/os-release": "PRETTY_NAME=\"Deepin 20.4\"\nNAME=\"Deepin\"\nVERSION_ID=\"20.4\"\nVERSION=\"20.4\"\nVERSION_CODENAME=\"apricot\"\nID=Deepin\nHOME_URL=\"https://www.deepin.org/\"\nBUG_REPORT_URL=\"https://bbs.deepin.org/\"\n"
+ },
+ "platform.dist": [
+ "Deepin",
+ "20.4",
+ "apricot"
+ ],
+ "result": {
+ "distribution": "Deepin",
+ "distribution_version": "20.4",
+ "distribution_release": "apricot",
+ "distribution_major_version": "20",
+ "os_family": "Debian"
+ }
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/devuan.json b/test/units/module_utils/facts/system/distribution/fixtures/devuan.json
new file mode 100644
index 0000000..d02fc2e
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/devuan.json
@@ -0,0 +1,23 @@
+{
+ "name": "Devuan",
+ "input": {
+ "/etc/os-release": "PRETTY_NAME=\"Devuan GNU/Linux ascii\"\nNAME=\"Devuan GNU/Linux\"\nID=devuan\nHOME_URL=\"https://www.devuan.org/\"\nSUPPORT_URL=\"https://devuan.org/os/community\"\nBUG_REPORT_URL=\"https://bugs.devuan.org/\""
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "devuan",
+ "name": "Devuan GNU/Linux",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "Devuan",
+ "distribution_major_version": "NA",
+ "distribution_release": "ascii",
+ "os_family": "Debian",
+ "distribution_version": "NA"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/dragonfly_5.2.2.json b/test/units/module_utils/facts/system/distribution/fixtures/dragonfly_5.2.2.json
new file mode 100644
index 0000000..5b99a48
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/dragonfly_5.2.2.json
@@ -0,0 +1,25 @@
+{
+ "name": "DragonFly v5.2.0-RELEASE #3",
+ "input": {},
+ "platform.system": "DragonFly",
+ "platform.release": "5.2-RELEASE",
+ "command_output": {
+ "/sbin/sysctl -n kern.version": "DragonFly v5.2.0-RELEASE #1: Mon Apr 9 00:17:53 EDT 2018\nroot@www.shiningsilence.com:/usr/obj/home/justin/release/5_2/sys/X86_64_GENERIC"
+ },
+ "distro": {
+ "codename": "",
+ "id": "dragonfly",
+ "name": "DragonFly",
+ "version": "5.2",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "DragonFly",
+ "distribution_major_version": "5",
+ "distribution_release": "5.2-RELEASE",
+ "os_family": "DragonFly",
+ "distribution_version": "5.2.0"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/dragonfly_5.6.2.json b/test/units/module_utils/facts/system/distribution/fixtures/dragonfly_5.6.2.json
new file mode 100644
index 0000000..90ec620
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/dragonfly_5.6.2.json
@@ -0,0 +1,25 @@
+{
+ "name": "DragonFly v5.6.2-RELEASE #3",
+ "input": {},
+ "platform.system": "DragonFly",
+ "platform.release": "5.6-RELEASE",
+ "command_output": {
+ "/sbin/sysctl -n kern.version": "DragonFly v5.6.2-RELEASE #3: Sat Aug 10 10:28:36 EDT 2019\nroot@www.shiningsilence.com:/usr/obj/home/justin/release/5_6/sys/X86_64_GENERIC"
+ },
+ "distro": {
+ "codename": "",
+ "id": "dragonfly",
+ "name": "DragonFly",
+ "version": "5.2",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "DragonFly",
+ "distribution_major_version": "5",
+ "distribution_release": "5.6-RELEASE",
+ "os_family": "DragonFly",
+ "distribution_version": "5.6.2"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/eurolinux_8.5.json b/test/units/module_utils/facts/system/distribution/fixtures/eurolinux_8.5.json
new file mode 100644
index 0000000..add1b73
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/eurolinux_8.5.json
@@ -0,0 +1,46 @@
+{
+ "name": "EuroLinux 8.5",
+ "distro": {
+ "codename": "Tirana",
+ "id": "eurolinux",
+ "name": "EuroLinux",
+ "version": "8.5",
+ "version_best": "8.5",
+ "lsb_release_info": {},
+ "os_release_info": {
+ "name": "EuroLinux",
+ "version": "8.5 (Tirana)",
+ "id": "eurolinux",
+ "id_like": "rhel fedora centos",
+ "version_id": "8.5",
+ "platform_id": "platform:el8",
+ "pretty_name": "EuroLinux 8.5 (Tirana)",
+ "ansi_color": "0;34",
+ "cpe_name": "cpe:/o:eurolinux:eurolinux:8",
+ "home_url": "https://www.euro-linux.com/",
+ "bug_report_url": "https://github.com/EuroLinux/eurolinux-distro-bugs-and-rfc/",
+ "redhat_support_product": "EuroLinux",
+ "redhat_support_product_version": "8",
+ "codename": "Tirana"
+ }
+ },
+ "input": {
+ "/etc/redhat-release": "EuroLinux release 8.5 (Tirana) \n",
+ "/etc/system-release": "EuroLinux release 8.5 (Tirana) \n",
+ "/etc/os-release": "NAME=\"EuroLinux\"\nVERSION=\"8.5 (Tirana)\"\nID=\"eurolinux\"\nID_LIKE=\"rhel fedora centos\"\nVERSION_ID=\"8.5\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"EuroLinux 8.5 (Tirana)\"\nANSI_COLOR=\"0;34\"\nCPE_NAME=\"cpe:/o:eurolinux:eurolinux:8\"\nHOME_URL=\"https://www.euro-linux.com/\"\nBUG_REPORT_URL=\"https://github.com/EuroLinux/eurolinux-distro-bugs-and-rfc/\"\nREDHAT_SUPPORT_PRODUCT=\"EuroLinux\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"8\"\n",
+ "/usr/lib/os-release": "NAME=\"EuroLinux\"\nVERSION=\"8.5 (Tirana)\"\nID=\"eurolinux\"\nID_LIKE=\"rhel fedora centos\"\nVERSION_ID=\"8.5\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"EuroLinux 8.5 (Tirana)\"\nANSI_COLOR=\"0;34\"\nCPE_NAME=\"cpe:/o:eurolinux:eurolinux:8\"\nHOME_URL=\"https://www.euro-linux.com/\"\nBUG_REPORT_URL=\"https://github.com/EuroLinux/eurolinux-distro-bugs-and-rfc/\"\nREDHAT_SUPPORT_PRODUCT=\"EuroLinux\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"8\"\n"
+ },
+ "platform.dist": [
+ "eurolinux",
+ "8.5",
+ "Tirana"
+ ],
+ "result": {
+ "distribution": "EuroLinux",
+ "distribution_version": "8.5",
+ "distribution_release": "Tirana",
+ "distribution_major_version": "8",
+ "os_family": "RedHat"
+ },
+ "platform.release": "4.18.0-348.2.1.el8_5.x86_64"
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/fedora_22.json b/test/units/module_utils/facts/system/distribution/fixtures/fedora_22.json
new file mode 100644
index 0000000..cec68d4
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/fedora_22.json
@@ -0,0 +1,25 @@
+{
+ "name": "Fedora 22",
+ "platform.dist": ["fedora", "22", "Twenty Two"],
+ "distro": {
+ "codename": "Twenty Two",
+ "id": "fedora",
+ "name": "Fedora",
+ "version": "22",
+ "version_best": "22",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/redhat-release": "Fedora release 22 (Twenty Two)\n",
+ "/etc/os-release": "NAME=Fedora\nVERSION=\"22 (Twenty Two)\"\nID=fedora\nVERSION_ID=22\nPRETTY_NAME=\"Fedora 22 (Twenty Two)\"\nANSI_COLOR=\"0;34\"\nCPE_NAME=\"cpe:/o:fedoraproject:fedora:22\"\nHOME_URL=\"https://fedoraproject.org/\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\nREDHAT_BUGZILLA_PRODUCT=\"Fedora\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=22\nREDHAT_SUPPORT_PRODUCT=\"Fedora\"\nREDHAT_SUPPORT_PRODUCT_VERSION=22\nPRIVACY_POLICY_URL=https://fedoraproject.org/wiki/Legal:PrivacyPolicy\n",
+ "/etc/system-release": "Fedora release 22 (Twenty Two)\n"
+ },
+ "result": {
+ "distribution_release": "Twenty Two",
+ "distribution": "Fedora",
+ "distribution_major_version": "22",
+ "os_family": "RedHat",
+ "distribution_version": "22"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/fedora_25.json b/test/units/module_utils/facts/system/distribution/fixtures/fedora_25.json
new file mode 100644
index 0000000..70b5bc3
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/fedora_25.json
@@ -0,0 +1,25 @@
+{
+ "platform.dist": ["fedora", "25", "Rawhide"],
+ "distro": {
+ "codename": "Rawhide",
+ "id": "fedora",
+ "name": "Fedora",
+ "version": "25",
+ "version_best": "25",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/redhat-release": "Fedora release 25 (Rawhide)\n",
+ "/etc/os-release": "NAME=Fedora\nVERSION=\"25 (Workstation Edition)\"\nID=fedora\nVERSION_ID=25\nPRETTY_NAME=\"Fedora 25 (Workstation Edition)\"\nANSI_COLOR=\"0;34\"\nCPE_NAME=\"cpe:/o:fedoraproject:fedora:25\"\nHOME_URL=\"https://fedoraproject.org/\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\nREDHAT_BUGZILLA_PRODUCT=\"Fedora\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=rawhide\nREDHAT_SUPPORT_PRODUCT=\"Fedora\"\nREDHAT_SUPPORT_PRODUCT_VERSION=rawhide\nPRIVACY_POLICY_URL=https://fedoraproject.org/wiki/Legal:PrivacyPolicy\nVARIANT=\"Workstation Edition\"\nVARIANT_ID=workstation\n",
+ "/etc/system-release": "Fedora release 25 (Rawhide)\n"
+ },
+ "name": "Fedora 25",
+ "result": {
+ "distribution_release": "Rawhide",
+ "distribution": "Fedora",
+ "distribution_major_version": "25",
+ "os_family": "RedHat",
+ "distribution_version": "25"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/fedora_31.json b/test/units/module_utils/facts/system/distribution/fixtures/fedora_31.json
new file mode 100644
index 0000000..e6d905e
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/fedora_31.json
@@ -0,0 +1,55 @@
+{
+ "name": "Fedora 31",
+ "distro": {
+ "codename": "",
+ "id": "fedora",
+ "name": "Fedora",
+ "version": "31",
+ "version_best": "31",
+ "lsb_release_info": {
+ "lsb_version": ":core-4.1-amd64:core-4.1-noarch",
+ "distributor_id": "Fedora",
+ "description": "Fedora release 31 (Thirty One)",
+ "release": "31",
+ "codename": "ThirtyOne"
+ },
+ "os_release_info": {
+ "name": "Fedora",
+ "version": "31 (Workstation Edition)",
+ "id": "fedora",
+ "version_id": "31",
+ "version_codename": "",
+ "platform_id": "platform:f31",
+ "pretty_name": "Fedora 31 (Workstation Edition)",
+ "ansi_color": "0;34",
+ "logo": "fedora-logo-icon",
+ "cpe_name": "cpe:/o:fedoraproject:fedora:31",
+ "home_url": "https://fedoraproject.org/",
+ "documentation_url": "https://docs.fedoraproject.org/en-US/fedora/f31/system-administrators-guide/",
+ "support_url": "https://fedoraproject.org/wiki/Communicating_and_getting_help",
+ "bug_report_url": "https://bugzilla.redhat.com/",
+ "redhat_bugzilla_product": "Fedora",
+ "redhat_bugzilla_product_version": "31",
+ "redhat_support_product": "Fedora",
+ "redhat_support_product_version": "31",
+ "privacy_policy_url": "https://fedoraproject.org/wiki/Legal:PrivacyPolicy",
+ "variant": "Workstation Edition",
+ "variant_id": "workstation",
+ "codename": ""
+ }
+ },
+ "input": {
+ "/etc/redhat-release": "Fedora release 31 (Thirty One)\n",
+ "/etc/system-release": "Fedora release 31 (Thirty One)\n",
+ "/etc/os-release": "NAME=Fedora\nVERSION=\"31 (Workstation Edition)\"\nID=fedora\nVERSION_ID=31\nVERSION_CODENAME=\"\"\nPLATFORM_ID=\"platform:f31\"\nPRETTY_NAME=\"Fedora 31 (Workstation Edition)\"\nANSI_COLOR=\"0;34\"\nLOGO=fedora-logo-icon\nCPE_NAME=\"cpe:/o:fedoraproject:fedora:31\"\nHOME_URL=\"https://fedoraproject.org/\"\nDOCUMENTATION_URL=\"https://docs.fedoraproject.org/en-US/fedora/f31/system-administrators-guide/\"\nSUPPORT_URL=\"https://fedoraproject.org/wiki/Communicating_and_getting_help\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\nREDHAT_BUGZILLA_PRODUCT=\"Fedora\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=31\nREDHAT_SUPPORT_PRODUCT=\"Fedora\"\nREDHAT_SUPPORT_PRODUCT_VERSION=31\nPRIVACY_POLICY_URL=\"https://fedoraproject.org/wiki/Legal:PrivacyPolicy\"\nVARIANT=\"Workstation Edition\"\nVARIANT_ID=workstation\n",
+ "/usr/lib/os-release": "NAME=Fedora\nVERSION=\"31 (Workstation Edition)\"\nID=fedora\nVERSION_ID=31\nVERSION_CODENAME=\"\"\nPLATFORM_ID=\"platform:f31\"\nPRETTY_NAME=\"Fedora 31 (Workstation Edition)\"\nANSI_COLOR=\"0;34\"\nLOGO=fedora-logo-icon\nCPE_NAME=\"cpe:/o:fedoraproject:fedora:31\"\nHOME_URL=\"https://fedoraproject.org/\"\nDOCUMENTATION_URL=\"https://docs.fedoraproject.org/en-US/fedora/f31/system-administrators-guide/\"\nSUPPORT_URL=\"https://fedoraproject.org/wiki/Communicating_and_getting_help\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\nREDHAT_BUGZILLA_PRODUCT=\"Fedora\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=31\nREDHAT_SUPPORT_PRODUCT=\"Fedora\"\nREDHAT_SUPPORT_PRODUCT_VERSION=31\nPRIVACY_POLICY_URL=\"https://fedoraproject.org/wiki/Legal:PrivacyPolicy\"\nVARIANT=\"Workstation Edition\"\nVARIANT_ID=workstation\n"
+ },
+ "platform.dist": ["fedora", "31", ""],
+ "result": {
+ "distribution": "Fedora",
+ "distribution_version": "31",
+ "distribution_release": "",
+ "distribution_major_version": "31",
+ "os_family": "RedHat"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/flatcar_3139.2.0.json b/test/units/module_utils/facts/system/distribution/fixtures/flatcar_3139.2.0.json
new file mode 100644
index 0000000..3cd7fa7
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/flatcar_3139.2.0.json
@@ -0,0 +1,43 @@
+{
+ "name": "Flatcar Container Linux by Kinvolk 3139.2.0",
+ "distro": {
+ "codename": "",
+ "id": "flatcar",
+ "name": "Flatcar Container Linux by Kinvolk",
+ "version": "3139.2.0",
+ "version_best": "3139.2.0",
+ "lsb_release_info": {},
+ "os_release_info": {
+ "name": "Flatcar Container Linux by Kinvolk",
+ "id": "flatcar",
+ "id_like": "coreos",
+ "version": "3139.2.0",
+ "version_id": "3139.2.0",
+ "build_id": "2022-04-05-1803",
+ "pretty_name": "Flatcar Container Linux by Kinvolk 3139.2.0 (Oklo)",
+ "ansi_color": "38;5;75",
+ "home_url": "https://flatcar-linux.org/",
+ "bug_report_url": "https://issues.flatcar-linux.org",
+ "flatcar_board": "amd64-usr",
+ "cpe_name": "cpe:2.3:o:flatcar-linux:flatcar_linux:3139.2.0:*:*:*:*:*:*:*"
+ }
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"Flatcar Container Linux by Kinvolk\"\nID=flatcar\nID_LIKE=coreos\nVERSION=3139.2.0\nVERSION_ID=3139.2.0\nBUILD_ID=2022-04-05-1803\nPRETTY_NAME=\"Flatcar Container Linux by Kinvolk 3139.2.0 (Oklo)\"\nANSI_COLOR=\"38;5;75\"\nHOME_URL=\"https://flatcar-linux.org/\"\nBUG_REPORT_URL=\"https://issues.flatcar-linux.org\"\nFLATCAR_BOARD=\"amd64-usr\"\nCPE_NAME=\"cpe:2.3:o:flatcar-linux:flatcar_linux:3139.2.0:*:*:*:*:*:*:*\"\n",
+ "/etc/lsb-release": "DISTRIB_ID=\"Flatcar Container Linux by Kinvolk\"\nDISTRIB_RELEASE=3139.2.0\nDISTRIB_CODENAME=\"Oklo\"\nDISTRIB_DESCRIPTION=\"Flatcar Container Linux by Kinvolk 3139.2.0 (Oklo)\"\n",
+ "/usr/lib/os-release": "NAME=\"Flatcar Container Linux by Kinvolk\"\nID=flatcar\nID_LIKE=coreos\nVERSION=3139.2.0\nVERSION_ID=3139.2.0\nBUILD_ID=2022-04-05-1803\nPRETTY_NAME=\"Flatcar Container Linux by Kinvolk 3139.2.0 (Oklo)\"\nANSI_COLOR=\"38;5;75\"\nHOME_URL=\"https://flatcar-linux.org/\"\nBUG_REPORT_URL=\"https://issues.flatcar-linux.org\"\nFLATCAR_BOARD=\"amd64-usr\"\nCPE_NAME=\"cpe:2.3:o:flatcar-linux:flatcar_linux:3139.2.0:*:*:*:*:*:*:*\"\n"
+ },
+ "platform.dist": [
+ "flatcar",
+ "3139.2.0",
+ ""
+ ],
+ "result": {
+ "distribution": "Flatcar",
+ "distribution_version": "3139.2.0",
+ "distribution_release": "NA",
+ "distribution_major_version": "3139",
+ "os_family": "Flatcar"
+ },
+ "platform.release": "5.15.32-flatcar"
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/kali_2019.1.json b/test/units/module_utils/facts/system/distribution/fixtures/kali_2019.1.json
new file mode 100644
index 0000000..096b66f
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/kali_2019.1.json
@@ -0,0 +1,25 @@
+{
+ "name": "Kali 2019.1",
+ "input": {
+ "/etc/os-release": "PRETTY_NAME=\"Kali GNU/Linux Rolling\"\nNAME=\"Kali GNU/Linux\"\nID=kali\nVERSION=\"2019.1\"\nVERSION_ID=\"2019.1\"\nID_LIKE=debian\nANSI_COLOR=\"1;31\"\nHOME_URL=\"https://www.kali.org/\"\nSUPPORT_URL=\"https://forums.kali.org/\"\nBUG_REPORT_URL=\"https://bugs.kali.org/\"\n",
+ "/etc/lsb-release": "DISTRIB_ID=Kali\nDISTRIB_RELEASE=kali-rolling\nDISTRIB_CODENAME=kali-rolling\nDISTRIB_DESCRIPTION=\"Kali GNU/Linux Rolling\"\n",
+ "/usr/lib/os-release": "PRETTY_NAME=\"Kali GNU/Linux Rolling\"\nNAME=\"Kali GNU/Linux\"\nID=kali\nVERSION=\"2019.1\"\nVERSION_ID=\"2019.1\"\nID_LIKE=debian\nANSI_COLOR=\"1;31\"\nHOME_URL=\"https://www.kali.org/\"\nSUPPORT_URL=\"https://forums.kali.org/\"\nBUG_REPORT_URL=\"https://bugs.kali.org/\"\n"
+ },
+ "platform.dist": ["kali", "2019.1", ""],
+ "distro": {
+ "codename": "kali-rolling",
+ "id": "kali",
+ "name": "Kali GNU/Linux Rolling",
+ "version": "2019.1",
+ "version_best": "2019.1",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "Kali",
+ "distribution_version": "2019.1",
+ "distribution_release": "kali-rolling",
+ "distribution_major_version": "2019",
+ "os_family": "Debian"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/kde_neon_16.04.json b/test/units/module_utils/facts/system/distribution/fixtures/kde_neon_16.04.json
new file mode 100644
index 0000000..5ff59c7
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/kde_neon_16.04.json
@@ -0,0 +1,42 @@
+{
+ "platform.dist": ["neon", "16.04", "xenial"],
+ "distro": {
+ "codename": "xenial",
+ "id": "neon",
+ "name": "KDE neon",
+ "version": "16.04",
+ "version_best": "16.04",
+ "os_release_info": {
+ "support_url": "http://help.ubuntu.com/",
+ "version_codename": "xenial",
+ "pretty_name": "Ubuntu 16.04.6 LTS",
+ "home_url": "http://www.ubuntu.com/",
+ "bug_report_url": "http://bugs.launchpad.net/ubuntu/",
+ "version": "16.04.6 LTS (Xenial Xerus)",
+ "version_id": "16.04",
+ "id": "ubuntu",
+ "ubuntu_codename": "xenial",
+ "codename": "xenial",
+ "name": "Ubuntu",
+ "id_like": "debian"
+ },
+ "lsb_release_info": {
+ "description": "Ubuntu 16.04.6 LTS",
+ "release": "16.04",
+ "distributor_id": "Ubuntu",
+ "codename": "xenial"
+ }
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"KDE neon\"\nVERSION=\"5.8\"\nID=neon\nID_LIKE=\"ubuntu debian\"\nPRETTY_NAME=\"KDE neon User Edition 5.8\"\nVERSION_ID=\"16.04\"\nHOME_URL=\"http://neon.kde.org/\"\nSUPPORT_URL=\"http://neon.kde.org/\"\nBUG_REPORT_URL=\"http://bugs.kde.org/\"\nVERSION_CODENAME=xenial\nUBUNTU_CODENAME=xenial\n",
+ "/etc/lsb-release": "DISTRIB_ID=neon\nDISTRIB_RELEASE=16.04\nDISTRIB_CODENAME=xenial\nDISTRIB_DESCRIPTION=\"KDE neon User Edition 5.8\"\n"
+ },
+ "name": "KDE neon 16.04",
+ "result": {
+ "distribution_release": "xenial",
+ "distribution": "KDE neon",
+ "distribution_major_version": "16",
+ "os_family": "Debian",
+ "distribution_version": "16.04"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/kylin_linux_advanced_server_v10.json b/test/units/module_utils/facts/system/distribution/fixtures/kylin_linux_advanced_server_v10.json
new file mode 100644
index 0000000..e929b5a
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/kylin_linux_advanced_server_v10.json
@@ -0,0 +1,38 @@
+{
+ "name": "Kylin Linux Advanced Server V10",
+ "distro": {
+ "codename": "Sword",
+ "id": "kylin",
+ "name": "Kylin Linux Advanced Server",
+ "version": "V10",
+ "version_best": "V10",
+ "lsb_release_info": {},
+ "os_release_info": {
+ "name": "Kylin Linux Advanced Server",
+ "version": "V10 (Sword)",
+ "id": "kylin",
+ "version_id": "V10",
+ "pretty_name": "Kylin Linux Advanced Server V10 (Sword)",
+ "ansi_color": "0;31",
+ "codename": "Sword"
+ }
+ },
+ "input": {
+ "/etc/system-release": "Kylin Linux Advanced Server release V10 (Sword)\n",
+ "/etc/os-release": "NAME=\"Kylin Linux Advanced Server\"\nVERSION=\"V10 (Sword)\"\nID=\"kylin\"\nVERSION_ID=\"V10\"\nPRETTY_NAME=\"Kylin Linux Advanced Server V10 (Sword)\"\nANSI_COLOR=\"0;31\"\n\n",
+ "/etc/lsb-release": "DISTRIB_ID=Kylin\nDISTRIB_RELEASE=V10\nDISTRIB_CODENAME=juniper\nDISTRIB_DESCRIPTION=\"Kylin V10\"\nDISTRIB_KYLIN_RELEASE=V10\nDISTRIB_VERSION_TYPE=enterprise\nDISTRIB_VERSION_MODE=normal\n"
+ },
+ "platform.dist": [
+ "kylin",
+ "V10",
+ "Sword"
+ ],
+ "result": {
+ "distribution": "Kylin Linux Advanced Server",
+ "distribution_version": "V10",
+ "distribution_release": "Sword",
+ "distribution_major_version": "V10",
+ "os_family": "RedHat"
+ },
+ "platform.release": "4.19.90-24.4.v2101.ky10.x86_64"
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/linux_mint_18.2.json b/test/units/module_utils/facts/system/distribution/fixtures/linux_mint_18.2.json
new file mode 100644
index 0000000..74e628e
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/linux_mint_18.2.json
@@ -0,0 +1,25 @@
+{
+ "platform.dist": ["linuxmint", "18.2", "sonya"],
+ "input": {
+ "/etc/os-release": "NAME=\"Linux Mint\"\nVERSION=\"18.2 (Sonya)\"\nID=linuxmint\nID_LIKE=ubuntu\nPRETTY_NAME=\"Linux Mint 18.2\"\nVERSION_ID=\"18.2\"\nHOME_URL=\"http://www.linuxmint.com/\"\nSUPPORT_URL=\"http://forums.linuxmint.com/\"\nBUG_REPORT_URL=\"http://bugs.launchpad.net/linuxmint/\"\nVERSION_CODENAME=sonya\nUBUNTU_CODENAME=xenial\n",
+ "/usr/lib/os-release": "NAME=\"Linux Mint\"\nVERSION=\"18.2 (Sonya)\"\nID=linuxmint\nID_LIKE=ubuntu\nPRETTY_NAME=\"Linux Mint 18.2\"\nVERSION_ID=\"18.2\"\nHOME_URL=\"http://www.linuxmint.com/\"\nSUPPORT_URL=\"http://forums.linuxmint.com/\"\nBUG_REPORT_URL=\"http://bugs.launchpad.net/linuxmint/\"\nVERSION_CODENAME=sonya\nUBUNTU_CODENAME=xenial\n",
+ "/etc/lsb-release": "DISTRIB_ID=LinuxMint\nDISTRIB_RELEASE=18.2\nDISTRIB_CODENAME=sonya\nDISTRIB_DESCRIPTION=\"Linux Mint 18.2 Sonya\"\n"
+ },
+ "result": {
+ "distribution_release": "sonya",
+ "distribution": "Linux Mint",
+ "distribution_major_version": "18",
+ "os_family": "Debian",
+ "distribution_version": "18.2"
+ },
+ "name": "Linux Mint 18.2",
+ "distro": {
+ "codename": "sonya",
+ "version": "18.2",
+ "id": "linuxmint",
+ "version_best": "18.2",
+ "name": "Linux Mint",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/linux_mint_19.1.json b/test/units/module_utils/facts/system/distribution/fixtures/linux_mint_19.1.json
new file mode 100644
index 0000000..7712856
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/linux_mint_19.1.json
@@ -0,0 +1,24 @@
+{
+ "platform.dist": ["linuxmint", "19.1", "tessa"],
+ "input": {
+ "/usr/lib/os-release": "NAME=\"Linux Mint\"\nVERSION=\"19.1 (Tessa)\"\nID=linuxmint\nID_LIKE=ubuntu\nPRETTY_NAME=\"Linux Mint 19.1\"\nVERSION_ID=\"19.1\"\nHOME_URL=\"https://www.linuxmint.com/\"\nSUPPORT_URL=\"https://forums.ubuntu.com/\"\nBUG_REPORT_URL=\"http: //linuxmint-troubleshooting-guide.readthedocs.io/en/latest/\"\nPRIVACY_POLICY_URL=\"https://www.linuxmint.com/\"\nVERSION_CODENAME=tessa\nUBUNTU_CODENAME=bionic\n",
+ "/etc/lsb-release": "DISTRIB_ID=LinuxMint\nDISTRIB_RELEASE=19.1\nDISTRIB_CODENAME=tessa\nDISTRIB_DESCRIPTION=\"Linux Mint 19.1 Tessa\"\n"
+ },
+ "result": {
+ "distribution_release": "tessa",
+ "distribution": "Linux Mint",
+ "distribution_major_version": "19",
+ "os_family": "Debian",
+ "distribution_version": "19.1"
+ },
+ "name": "Linux Mint 19.1",
+ "distro": {
+ "codename": "tessa",
+ "version": "19.1",
+ "id": "linuxmint",
+ "version_best": "19.1",
+ "name": "Linux Mint",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/netbsd_8.2.json b/test/units/module_utils/facts/system/distribution/fixtures/netbsd_8.2.json
new file mode 100644
index 0000000..65c4ed6
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/netbsd_8.2.json
@@ -0,0 +1,25 @@
+{
+ "name": "NetBSD 8.2 (GENERIC) #0",
+ "input": {},
+ "platform.system": "NetBSD",
+ "platform.release": "8.2",
+ "command_output": {
+ "/sbin/sysctl -n kern.version": "NetBSD 8.2 (GENERIC) #0: Tue Mar 31 05:08:40 UTC 2020\n mkrepro@mkrepro.NetBSD.org:/usr/src/sys/arch/amd64/compile/GENERIC"
+ },
+ "distro": {
+ "codename": "",
+ "id": "netbsd",
+ "name": "NetBSD",
+ "version": "8.2",
+ "version_best": "8.2",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "NetBSD",
+ "distribution_major_version": "8",
+ "distribution_release": "8.2",
+ "os_family": "NetBSD",
+ "distribution_version": "8.2"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/nexenta_3.json b/test/units/module_utils/facts/system/distribution/fixtures/nexenta_3.json
new file mode 100644
index 0000000..bdc942b
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/nexenta_3.json
@@ -0,0 +1,25 @@
+{
+ "name": "Nexenta 3",
+ "uname_v": "NexentaOS_134f",
+ "result": {
+ "distribution_release": "Open Storage Appliance v3.1.6",
+ "distribution": "Nexenta",
+ "os_family": "Solaris",
+ "distribution_version": "3.1.6"
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "",
+ "name": "",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "platform.release:": "",
+ "input": {
+ "/etc/release": " Open Storage Appliance v3.1.6\n Copyright (c) 2014 Nexenta Systems, Inc. All Rights Reserved.\n Copyright (c) 2011 Oracle. All Rights Reserved.\n Use is subject to license terms.\n"
+ },
+ "platform.system": "SunOS"
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/nexenta_4.json b/test/units/module_utils/facts/system/distribution/fixtures/nexenta_4.json
new file mode 100644
index 0000000..d24e9bc
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/nexenta_4.json
@@ -0,0 +1,24 @@
+{
+ "name": "Nexenta 4",
+ "uname_v": "NexentaOS_4:cd604cd066",
+ "result": {
+ "distribution_release": "Open Storage Appliance 4.0.3-FP2",
+ "distribution": "Nexenta",
+ "os_family": "Solaris",
+ "distribution_version": "4.0.3-FP2"
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "",
+ "name": "",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/release": " Open Storage Appliance 4.0.3-FP2\n Copyright (c) 2014 Nexenta Systems, Inc. All Rights Reserved.\n Copyright (c) 2010 Oracle. All Rights Reserved.\n Use is subject to license terms.\n"
+ },
+ "platform.system": "SunOS"
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/omnios.json b/test/units/module_utils/facts/system/distribution/fixtures/omnios.json
new file mode 100644
index 0000000..8bb2b44
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/omnios.json
@@ -0,0 +1,24 @@
+{
+ "name": "OmniOS",
+ "uname_v": "omnios-10b9c79",
+ "result": {
+ "distribution_release": "OmniOS v11 r151012",
+ "distribution": "OmniOS",
+ "os_family": "Solaris",
+ "distribution_version": "r151012"
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "",
+ "name": "",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/release": " OmniOS v11 r151012\n Copyright 2014 OmniTI Computer Consulting, Inc. All rights reserved.\n Use is subject to license terms.\n\n"
+ },
+ "platform.system": "SunOS"
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/openeuler_20.03.json b/test/units/module_utils/facts/system/distribution/fixtures/openeuler_20.03.json
new file mode 100644
index 0000000..8310386
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/openeuler_20.03.json
@@ -0,0 +1,28 @@
+{
+ "platform.dist": [
+ "openeuler",
+ "20.03",
+ "LTS"
+ ],
+ "input": {
+ "/etc/os-release": "NAME=\"openEuler\"\nVERSION=\"20.03 (LTS)\"\nID=\"openEuler\"\nVERSION_ID=\"20.03\"\nPRETTY_NAME=\"openEuler 20.03 (LTS)\"\nANSI_COLOR=\"0;31\"\n\n",
+ "/etc/system-release": "openEuler release 20.03 (LTS)\n"
+ },
+ "result": {
+ "distribution_release": "LTS",
+ "distribution": "openEuler",
+ "distribution_major_version": "20",
+ "os_family": "RedHat",
+ "distribution_version": "20.03"
+ },
+ "name": "openEuler 20.03",
+ "distro": {
+ "codename": "LTS",
+ "version": "20.03",
+ "id": "openeuler",
+ "version_best": "20.03",
+ "name": "openEuler",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/openindiana.json b/test/units/module_utils/facts/system/distribution/fixtures/openindiana.json
new file mode 100644
index 0000000..a055bb0
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/openindiana.json
@@ -0,0 +1,24 @@
+{
+ "name": "OpenIndiana",
+ "uname_v": "oi_151a9",
+ "result": {
+ "distribution_release": "OpenIndiana Development oi_151.1.9 X86 (powered by illumos)",
+ "distribution": "OpenIndiana",
+ "os_family": "Solaris",
+ "distribution_version": "oi_151a9"
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "",
+ "name": "",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/release": " OpenIndiana Development oi_151.1.9 X86 (powered by illumos)\n Copyright 2011 Oracle and/or its affiliates. All rights reserved.\n Use is subject to license terms.\n Assembled 17 January 2014\n"
+ },
+ "platform.system": "SunOS"
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/opensuse_13.2.json b/test/units/module_utils/facts/system/distribution/fixtures/opensuse_13.2.json
new file mode 100644
index 0000000..76d3a33
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/opensuse_13.2.json
@@ -0,0 +1,24 @@
+{
+ "name": "openSUSE 13.2",
+ "input": {
+ "/etc/SuSE-release": "openSUSE 13.2 (x86_64)\nVERSION = 13.2\nCODENAME = Harlequin\n# /etc/SuSE-release is deprecated and will be removed in the future, use /etc/os-release instead",
+ "/etc/os-release": "NAME=openSUSE\nVERSION=\"13.2 (Harlequin)\"\nVERSION_ID=\"13.2\"\nPRETTY_NAME=\"openSUSE 13.2 (Harlequin) (x86_64)\"\nID=opensuse\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:opensuse:opensuse:13.2\"\nBUG_REPORT_URL=\"https://bugs.opensuse.org\"\nHOME_URL=\"https://opensuse.org/\"\nID_LIKE=\"suse\""
+ },
+ "platform.dist": ["SuSE", "13.2", "x86_64"],
+ "distro": {
+ "codename": "",
+ "id": "opensuse-harlequin",
+ "name": "openSUSE Harlequin",
+ "version": "13.2",
+ "version_best": "13.2",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "openSUSE",
+ "distribution_major_version": "13",
+ "distribution_release": "2",
+ "os_family": "Suse",
+ "distribution_version": "13.2"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_15.0.json b/test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_15.0.json
new file mode 100644
index 0000000..54f1265
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_15.0.json
@@ -0,0 +1,23 @@
+{
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "opensuse-leap",
+ "name": "openSUSE Leap",
+ "version": "15.0",
+ "version_best": "15.0",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"openSUSE Leap\"\n# VERSION=\"15.0\"\nID=opensuse-leap\nID_LIKE=\"suse opensuse\"\nVERSION_ID=\"15.0\"\nPRETTY_NAME=\"openSUSE Leap 15.0\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:opensuse:leap:15.0\"\nBUG_REPORT_URL=\"https://bugs.opensuse.org\"\nHOME_URL=\"https://www.opensuse.org/\"\n"
+ },
+ "name": "openSUSE Leap 15.0",
+ "result": {
+ "distribution_release": "0",
+ "distribution": "openSUSE Leap",
+ "distribution_major_version": "15",
+ "os_family": "Suse",
+ "distribution_version": "15.0"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_15.1.json b/test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_15.1.json
new file mode 100644
index 0000000..d029423
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_15.1.json
@@ -0,0 +1,36 @@
+{
+ "name": "openSUSE Leap 15.1",
+ "distro": {
+ "codename": "",
+ "id": "opensuse-leap",
+ "name": "openSUSE Leap",
+ "version": "15.1",
+ "version_best": "15.1",
+ "lsb_release_info": {},
+ "os_release_info": {
+ "name": "openSUSE Leap",
+ "version": "15.1",
+ "codename": "",
+ "id": "opensuse-leap",
+ "id_like": "suse opensuse",
+ "version_id": "15.1",
+ "pretty_name": "openSUSE Leap 15.1",
+ "ansi_color": "0;32",
+ "cpe_name": "cpe:/o:opensuse:leap:15.1",
+ "bug_report_url": "https://bugs.opensuse.org",
+ "home_url": "https://www.opensuse.org/"
+ }
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"openSUSE Leap\"\nVERSION=\"15.1\"\nID=\"opensuse-leap\"\nID_LIKE=\"suse opensuse\"\nVERSION_ID=\"15.1\"\nPRETTY_NAME=\"openSUSE Leap 15.1\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:opensuse:leap:15.1\"\nBUG_REPORT_URL=\"https://bugs.opensuse.org\"\nHOME_URL=\"https://www.opensuse.org/\"\n",
+ "/usr/lib/os-release": "NAME=\"openSUSE Leap\"\nVERSION=\"15.1\"\nID=\"opensuse-leap\"\nID_LIKE=\"suse opensuse\"\nVERSION_ID=\"15.1\"\nPRETTY_NAME=\"openSUSE Leap 15.1\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:opensuse:leap:15.1\"\nBUG_REPORT_URL=\"https://bugs.opensuse.org\"\nHOME_URL=\"https://www.opensuse.org/\"\n"
+ },
+ "platform.dist": ["opensuse-leap", "15.1", ""],
+ "result": {
+ "distribution": "openSUSE Leap",
+ "distribution_version": "15.1",
+ "distribution_release": "1",
+ "distribution_major_version": "15",
+ "os_family": "Suse"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_42.1.json b/test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_42.1.json
new file mode 100644
index 0000000..2142932
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/opensuse_leap_42.1.json
@@ -0,0 +1,24 @@
+{
+ "name": "openSUSE Leap 42.1",
+ "input": {
+ "/etc/os-release": "NAME=\"openSUSE Leap\"\nVERSION=\"42.1\"\nVERSION_ID=\"42.1\"\nPRETTY_NAME=\"openSUSE Leap 42.1 (x86_64)\"\nID=opensuse\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:opensuse:opensuse:42.1\"\nBUG_REPORT_URL=\"https://bugs.opensuse.org\"\nHOME_URL=\"https://opensuse.org/\"\nID_LIKE=\"suse\"",
+ "/etc/SuSE-release": "openSUSE 42.1 (x86_64)\nVERSION = 42.1\nCODENAME = Malachite\n# /etc/SuSE-release is deprecated and will be removed in the future, use /etc/os-release instead"
+ },
+ "platform.dist": ["SuSE", "42.1", "x86_64"],
+ "distro": {
+ "codename": "",
+ "id": "opensuse-leap",
+ "name": "openSUSE Leap",
+ "version": "42.1",
+ "version_best": "42.1",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "openSUSE Leap",
+ "distribution_major_version": "42",
+ "distribution_release": "1",
+ "os_family": "Suse",
+ "distribution_version": "42.1"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/opensuse_tumbleweed_20160917.json b/test/units/module_utils/facts/system/distribution/fixtures/opensuse_tumbleweed_20160917.json
new file mode 100644
index 0000000..db1a26c
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/opensuse_tumbleweed_20160917.json
@@ -0,0 +1,23 @@
+{
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "opensuse-tumbleweed",
+ "name": "openSUSE Tumbleweed",
+ "version": "20160917",
+ "version_best": "20160917",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"openSUSE Tumbleweed\"\n# VERSION=\"20160917\"\nID=opensuse\nID_LIKE=\"suse\"\nVERSION_ID=\"20160917\"\nPRETTY_NAME=\"openSUSE Tumbleweed\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:opensuse:tumbleweed:20160917\"\nBUG_REPORT_URL=\"https://bugs.opensuse.org\"\nHOME_URL=\"https://www.opensuse.org/\"\n"
+ },
+ "name": "openSUSE Tumbleweed 20160917",
+ "result": {
+ "distribution_release": "",
+ "distribution": "openSUSE Tumbleweed",
+ "distribution_major_version": "20160917",
+ "os_family": "Suse",
+ "distribution_version": "20160917"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/osmc.json b/test/units/module_utils/facts/system/distribution/fixtures/osmc.json
new file mode 100644
index 0000000..98a4923
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/osmc.json
@@ -0,0 +1,23 @@
+{
+ "name": "OSMC",
+ "input": {
+ "/etc/os-release": "PRETTY_NAME=\"Open Source Media Center\"\nNAME=\"OSMC\"\nVERSION=\"March 2022\"\nVERSION_ID=\"2022.03-1\"\nID=osmc\nID_LIKE=debian\nANSI_COLOR=\"1;31\"\nHOME_URL=\"https://www.osmc.tv\"\nSUPPORT_URL=\"https://www.osmc.tv\"\nBUG_REPORT_URL=\"https://www.osmc.tv\""
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "osmc",
+ "name": "OSMC",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "OSMC",
+ "distribution_major_version": "NA",
+ "distribution_release": "NA",
+ "os_family": "Debian",
+ "distribution_version": "March 2022"
+ }
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/pardus_19.1.json b/test/units/module_utils/facts/system/distribution/fixtures/pardus_19.1.json
new file mode 100644
index 0000000..daf8f6e
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/pardus_19.1.json
@@ -0,0 +1,41 @@
+{
+ "name": "Pardus GNU/Linux 19.1",
+ "distro": {
+ "codename": "ondokuz",
+ "id": "pradus",
+ "name": "Pardus GNU/Linux",
+ "version": "19.1",
+ "version_best": "19.1",
+ "lsb_release_info": {
+ "release": "19.1",
+ "codename": "ondokuz",
+ "distributor_id": "Pardus",
+ "description": "Pardus GNU/Linux Ondokuz"
+ },
+ "os_release_info": {
+ "pardus_codename": "ondokuz",
+ "name": "Pardus GNU/Linux",
+ "version_codename": "ondokuz",
+ "id_like": "debian",
+ "version_id": "19.1",
+ "bug_report_url": "https://talep.pardus.org.tr/",
+ "pretty_name": "Pardus GNU/Linux Ondokuz",
+ "version": "19.1 (Ondokuz)",
+ "codename": "ondokuz",
+ "home_url": "https://www.pardus.org.tr/",
+ "id": "pardus",
+ "support_url": "https://forum.pardus.org.tr/"
+ }
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"Pardus GNU/Linux\"\nVERSION=\"19.1 (Ondokuz)\"\nID=pardus\nID_LIKE=debian\nPRETTY_NAME=\"Pardus GNU/Linux Ondokuz\"\nVERSION_ID=\"19.1\"\nHOME_URL=\"https://www.pardus.org.tr/\"\nSUPPORT_URL=\"https://forum.pardus.org.tr/\"\nBUG_REPORT_URL=\"https://talep.pardus.org.tr/\"\nVERSION_CODENAME=ondokuz\nPARDUS_CODENAME=ondokuz"
+ },
+ "platform.dist": ["debian", "10.0", ""],
+ "result": {
+ "distribution": "Pardus GNU/Linux",
+ "distribution_version": "19.1",
+ "distribution_release": "ondokuz",
+ "distribution_major_version": "19",
+ "os_family": "Debian"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/parrot_4.8.json b/test/units/module_utils/facts/system/distribution/fixtures/parrot_4.8.json
new file mode 100644
index 0000000..fd10ff6
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/parrot_4.8.json
@@ -0,0 +1,25 @@
+{
+ "name": "Parrot 4.8",
+ "input": {
+ "/etc/os-release": "PRETTY_NAME=\"Parrot GNU/Linux 4.8\"\nNAME=\"Parrot GNU/Linux\"\nID=parrot\nVERSION=\"4.8\"\nVERSION_ID=\"4.8\"\nID_LIKE=debian\nHOME_URL=\"https://www.parrotlinux.org/\"\nSUPPORT_URL=\"https://community.parrotlinux.org/\"\nBUG_REPORT_URL=\"https://nest.parrot.sh/\"\n",
+ "/etc/lsb-release": "DISTRIB_ID=Parrot\nDISTRIB_RELEASE=4.8\nDISTRIB_CODENAME=rolling\nDISTRIB_DESCRIPTION=\"Parrot 4.8\"\n",
+ "/usr/lib/os-release": "PRETTY_NAME=\"Parrot GNU/Linux 4.8\"\nNAME=\"Parrot GNU/Linux\"\nID=parrot\nVERSION=\"4.8\"\nVERSION_ID=\"4.8\"\nID_LIKE=debian\nHOME_URL=\"https://www.parrotlinux.org/\"\nSUPPORT_URL=\"https://community.parrotlinux.org/\"\nBUG_REPORT_URL=\"https://nest.parrot.sh/\"\n"
+ },
+ "platform.dist": ["parrot", "4.8", ""],
+ "distro": {
+ "codename": "rolling",
+ "id": "parrot",
+ "name": "Parrot GNU/Linux",
+ "version": "4.8",
+ "version_best": "4.8",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "Parrot",
+ "distribution_version": "4.8",
+ "distribution_release": "rolling",
+ "distribution_major_version": "4",
+ "os_family": "Debian"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/pop_os_20.04.json b/test/units/module_utils/facts/system/distribution/fixtures/pop_os_20.04.json
new file mode 100644
index 0000000..d3184ef
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/pop_os_20.04.json
@@ -0,0 +1,29 @@
+{
+ "name": "Pop!_OS 20.04",
+ "distro": {
+ "codename": "focal",
+ "id": "pop",
+ "name": "Pop!_OS",
+ "version": "20.04",
+ "version_best": "20.04",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"Pop!_OS\"\nVERSION=\"20.04\"\nID=pop\nID_LIKE=\"ubuntu debian\"\nPRETTY_NAME=\"Pop!_OS 20.04\"\nVERSION_ID=\"20.04\"\nHOME_URL=\"https://system76.com/pop\"\nSUPPORT_URL=\"http://support.system76.com\"\nBUG_REPORT_URL=\"https://github.com/pop-os/pop/issues\"\nPRIVACY_POLICY_URL=\"https://system76.com/privacy\"\nVERSION_CODENAME=focal\nUBUNTU_CODENAME=focal\nLOGO=distributor-logo-pop-os\n",
+ "/etc/lsb-release": "DISTRIB_ID=Pop\nDISTRIB_RELEASE=20.04\nDISTRIB_CODENAME=focal\nDISTRIB_DESCRIPTION=\"Pop!_OS 20.04\"\n",
+ "/usr/lib/os-release": "NAME=\"Pop!_OS\"\nVERSION=\"20.04\"\nID=pop\nID_LIKE=\"ubuntu debian\"\nPRETTY_NAME=\"Pop!_OS 20.04\"\nVERSION_ID=\"20.04\"\nHOME_URL=\"https://system76.com/pop\"\nSUPPORT_URL=\"http://support.system76.com\"\nBUG_REPORT_URL=\"https://github.com/pop-os/pop/issues\"\nPRIVACY_POLICY_URL=\"https://system76.com/privacy\"\nVERSION_CODENAME=focal\nUBUNTU_CODENAME=focal\nLOGO=distributor-logo-pop-os\n"
+ },
+ "platform.dist": [
+ "pop",
+ "20.04",
+ "focal"
+ ],
+ "result": {
+ "distribution": "Pop!_OS",
+ "distribution_version": "20.04",
+ "distribution_release": "focal",
+ "distribution_major_version": "20",
+ "os_family": "Debian"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/redhat_6.7.json b/test/units/module_utils/facts/system/distribution/fixtures/redhat_6.7.json
new file mode 100644
index 0000000..27a77d0
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/redhat_6.7.json
@@ -0,0 +1,25 @@
+{
+ "name": "RedHat 6.7",
+ "platform.dist": ["redhat", "6.7", "Santiago"],
+ "distro": {
+ "codename": "Santiago",
+ "id": "rhel",
+ "name": "RedHat Enterprise Linux",
+ "version": "6.7",
+ "version_best": "6.7",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/redhat-release": "Red Hat Enterprise Linux Server release 6.7 (Santiago)\n",
+ "/etc/lsb-release": "LSB_VERSION=base-4.0-amd64:base-4.0-noarch:core-4.0-amd64:core-4.0-noarch:graphics-4.0-amd64:graphics-4.0-noarch:printing-4.0-amd64:printing-4.0-noarch\n",
+ "/etc/system-release": "Red Hat Enterprise Linux Server release 6.7 (Santiago)\n"
+ },
+ "result": {
+ "distribution_release": "Santiago",
+ "distribution": "RedHat",
+ "distribution_major_version": "6",
+ "os_family": "RedHat",
+ "distribution_version": "6.7"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/redhat_7.2.json b/test/units/module_utils/facts/system/distribution/fixtures/redhat_7.2.json
new file mode 100644
index 0000000..3900f82
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/redhat_7.2.json
@@ -0,0 +1,25 @@
+{
+ "name": "RedHat 7.2",
+ "platform.dist": ["redhat", "7.2", "Maipo"],
+ "distro": {
+ "codename": "Maipo",
+ "id": "rhel",
+ "name": "RedHat Enterprise Linux",
+ "version": "7.2",
+ "version_best": "7.2",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/redhat-release": "Red Hat Enterprise Linux Server release 7.2 (Maipo)\n",
+ "/etc/os-release": "NAME=\"Red Hat Enterprise Linux Server\"\nVERSION=\"7.2 (Maipo)\"\nID=\"rhel\"\nID_LIKE=\"fedora\"\nVERSION_ID=\"7.2\"\nPRETTY_NAME=\"Red Hat Enterprise Linux Server 7.2 (Maipo)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:redhat:enterprise_linux:7.2:GA:server\"\nHOME_URL=\"https://www.redhat.com/\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\n\nREDHAT_BUGZILLA_PRODUCT=\"Red Hat Enterprise Linux 7\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=7.2\nREDHAT_SUPPORT_PRODUCT=\"Red Hat Enterprise Linux\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"7.2\"\n",
+ "/etc/system-release": "Red Hat Enterprise Linux Server release 7.2 (Maipo)\n"
+ },
+ "result": {
+ "distribution_release": "Maipo",
+ "distribution": "RedHat",
+ "distribution_major_version": "7",
+ "os_family": "RedHat",
+ "distribution_version": "7.2"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/redhat_7.7.json b/test/units/module_utils/facts/system/distribution/fixtures/redhat_7.7.json
new file mode 100644
index 0000000..b240efc
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/redhat_7.7.json
@@ -0,0 +1,43 @@
+{
+ "name": "RedHat 7.7",
+ "distro": {
+ "codename": "Maipo",
+ "id": "rhel",
+ "name": "Red Hat Enterprise Linux Server",
+ "version": "7.7",
+ "version_best": "7.7",
+ "lsb_release_info": {},
+ "os_release_info": {
+ "name": "Red Hat Enterprise Linux Server",
+ "version": "7.7 (Maipo)",
+ "id": "rhel",
+ "id_like": "fedora",
+ "variant": "Server",
+ "variant_id": "server",
+ "version_id": "7.7",
+ "pretty_name": "Red Hat Enterprise Linux Server 7.7 (Maipo)",
+ "ansi_color": "0;31",
+ "cpe_name": "cpe:/o:redhat:enterprise_linux:7.7:GA:server",
+ "home_url": "https://www.redhat.com/",
+ "bug_report_url": "https://bugzilla.redhat.com/",
+ "redhat_bugzilla_product": "Red Hat Enterprise Linux 7",
+ "redhat_bugzilla_product_version": "7.7",
+ "redhat_support_product": "Red Hat Enterprise Linux",
+ "redhat_support_product_version": "7.7",
+ "codename": "Maipo"
+ }
+ },
+ "input": {
+ "/etc/redhat-release": "Red Hat Enterprise Linux Server release 7.7 (Maipo)\n",
+ "/etc/system-release": "Red Hat Enterprise Linux Server release 7.7 (Maipo)\n",
+ "/etc/os-release": "NAME=\"Red Hat Enterprise Linux Server\"\nVERSION=\"7.7 (Maipo)\"\nID=\"rhel\"\nID_LIKE=\"fedora\"\nVARIANT=\"Server\"\nVARIANT_ID=\"server\"\nVERSION_ID=\"7.7\"\nPRETTY_NAME=\"Red Hat Enterprise Linux Server 7.7 (Maipo)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:redhat:enterprise_linux:7.7:GA:server\"\nHOME_URL=\"https://www.redhat.com/\"\nBUG_REPORT_URL=\"https://bugzilla.redhat.com/\"\n\nREDHAT_BUGZILLA_PRODUCT=\"Red Hat Enterprise Linux 7\"\nREDHAT_BUGZILLA_PRODUCT_VERSION=7.7\nREDHAT_SUPPORT_PRODUCT=\"Red Hat Enterprise Linux\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"7.7\"\n"
+ },
+ "platform.dist": ["rhel", "7.7", "Maipo"],
+ "result": {
+ "distribution": "RedHat",
+ "distribution_version": "7.7",
+ "distribution_release": "Maipo",
+ "distribution_major_version": "7",
+ "os_family": "RedHat"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/rockylinux_8_3.json b/test/units/module_utils/facts/system/distribution/fixtures/rockylinux_8_3.json
new file mode 100644
index 0000000..8c3ff76
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/rockylinux_8_3.json
@@ -0,0 +1,46 @@
+{
+ "name": "Rocky 8.3",
+ "distro": {
+ "codename": "",
+ "id": "rocky",
+ "name": "Rocky Linux",
+ "version": "8.3",
+ "version_best": "8.3",
+ "lsb_release_info": {},
+ "os_release_info": {
+ "name": "Rocky Linux",
+ "version": "8.3",
+ "id": "rocky",
+ "id_like": "rhel fedora",
+ "version_id": "8.3",
+ "platform_id": "platform:el8",
+ "pretty_name": "Rocky Linux 8.3",
+ "ansi_color": "0;31",
+ "cpe_name": "cpe:/o:rocky:rocky:8",
+ "home_url": "https://rockylinux.org/",
+ "bug_report_url": "https://bugs.rockylinux.org/",
+ "rocky_support_product": "Rocky Linux",
+ "rocky_support_product_version": "8"
+ }
+ },
+ "input": {
+ "/etc/redhat-release": "Rocky Linux release 8.3\n",
+ "/etc/system-release": "Rocky Linux release 8.3\n",
+ "/etc/rocky-release": "Rocky Linux release 8.3\n",
+ "/etc/os-release": "NAME=\"Rocky Linux\"\nVERSION=\"8.3\"\nID=\"rocky\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"8.3\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"Rocky Linux 8.3\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:rocky:rocky:8\"\nHOME_URL=\"https://rockylinux.org/\"\nBUG_REPORT_URL=\"https://bugs.rockylinux.org/\"\nROCKY_SUPPORT_PRODUCT=\"Rocky Linux\"\nROCKY_SUPPORT_PRODUCT_VERSION=\"8\"\n",
+ "/usr/lib/os-release": "NAME=\"Rocky Linux\"\nVERSION=\"8.3\"\nID=\"rocky\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"8.3\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"Rocky Linux 8.3\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:rocky:rocky:8\"\nHOME_URL=\"https://rockylinux.org/\"\nBUG_REPORT_URL=\"https://bugs.rockylinux.org/\"\nROCKY_SUPPORT_PRODUCT=\"Rocky Linux\"\nROCKY_SUPPORT_PRODUCT_VERSION=\"8\"\n"
+ },
+ "platform.dist": [
+ "rocky",
+ "8.3",
+ ""
+ ],
+ "result": {
+ "distribution": "Rocky",
+ "distribution_version": "8.3",
+ "distribution_release": "NA",
+ "distribution_major_version": "8",
+ "os_family": "RedHat"
+ },
+ "platform.release": "4.18.0-240.22.1.el8.x86_64"
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/sles_11.3.json b/test/units/module_utils/facts/system/distribution/fixtures/sles_11.3.json
new file mode 100644
index 0000000..be71f1c
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/sles_11.3.json
@@ -0,0 +1,23 @@
+{
+ "name": "SLES 11.3",
+ "input": {
+ "/etc/SuSE-release": "SUSE Linux Enterprise Server 11 (x86_64)\nVERSION = 11\nPATCHLEVEL = 3"
+ },
+ "platform.dist": ["SuSE", "11", "x86_64"],
+ "distro": {
+ "codename": "",
+ "id": "sles",
+ "name": "SUSE Linux Enterprise Server",
+ "version": "11",
+ "version_best": "11",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "SLES",
+ "distribution_major_version": "11",
+ "distribution_release": "3",
+ "os_family": "Suse",
+ "distribution_version": "11.3"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/sles_11.4.json b/test/units/module_utils/facts/system/distribution/fixtures/sles_11.4.json
new file mode 100644
index 0000000..3e4012a
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/sles_11.4.json
@@ -0,0 +1,24 @@
+{
+ "name": "SLES 11.4",
+ "input": {
+ "/etc/SuSE-release": "\nSUSE Linux Enterprise Server 11 (x86_64)\nVERSION = 11\nPATCHLEVEL = 4",
+ "/etc/os-release": "NAME=\"SLES\"\nVERSION=\"11.4\"\nVERSION_ID=\"11.4\"\nPRETTY_NAME=\"SUSE Linux Enterprise Server 11 SP4\"\nID=\"sles\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:suse:sles:11:4\""
+ },
+ "platform.dist": ["SuSE", "11", "x86_64"],
+ "distro": {
+ "codename": "",
+ "id": "sles",
+ "name": "SUSE Linux Enterprise Server",
+ "version": "11.4",
+ "version_best": "11.4",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "SLES",
+ "distribution_major_version": "11",
+ "distribution_release": "4",
+ "os_family": "Suse",
+ "distribution_version": "11.4"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/sles_12_sp0.json b/test/units/module_utils/facts/system/distribution/fixtures/sles_12_sp0.json
new file mode 100644
index 0000000..e84bbe5
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/sles_12_sp0.json
@@ -0,0 +1,24 @@
+{
+ "name": "SLES 12 SP0",
+ "input": {
+ "/etc/SuSE-release": "\nSUSE Linux Enterprise Server 12 (x86_64)\nVERSION = 12\nPATCHLEVEL = 0\n# This file is deprecated and will be removed in a future service pack or release.\n# Please check /etc/os-release for details about this release.",
+ "/etc/os-release": "NAME=\"SLES\"\nVERSION=\"12\"\nVERSION_ID=\"12\"\nPRETTY_NAME=\"SUSE Linux Enterprise Server 12\"\nID=\"sles\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:suse:sles:12\""
+ },
+ "platform.dist": ["SuSE", "12", "x86_64"],
+ "distro": {
+ "codename": "",
+ "id": "sles",
+ "name": "SUSE Linux Enterprise Server",
+ "version": "12",
+ "version_best": "12",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "SLES",
+ "distribution_major_version": "12",
+ "distribution_release": "0",
+ "os_family": "Suse",
+ "distribution_version": "12"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/sles_12_sp1.json b/test/units/module_utils/facts/system/distribution/fixtures/sles_12_sp1.json
new file mode 100644
index 0000000..c78d53d
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/sles_12_sp1.json
@@ -0,0 +1,24 @@
+{
+ "name": "SLES 12 SP1",
+ "input": {
+ "/etc/SuSE-release": "\nSUSE Linux Enterprise Server 12 (x86_64)\nVERSION = 12\nPATCHLEVEL = 0\n# This file is deprecated and will be removed in a future service pack or release.\n# Please check /etc/os-release for details about this release.",
+ "/etc/os-release": "NAME=\"SLES\"\nVERSION=\"12-SP1\"\nVERSION_ID=\"12.1\"\nPRETTY_NAME=\"SUSE Linux Enterprise Server 12 SP1\"\nID=\"sles\"\nANSI_COLOR=\"0;32\"\nCPE_NAME=\"cpe:/o:suse:sles:12:sp1\""
+ },
+ "platform.dist": ["SuSE", "12", "x86_64"],
+ "distro": {
+ "codename": "",
+ "id": "sles",
+ "name": "SUSE Linux Enterprise Server",
+ "version": "12.1",
+ "version_best": "12.1",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "SLES",
+ "distribution_major_version": "12",
+ "distribution_release": "1",
+ "os_family": "Suse",
+ "distribution_version": "12.1"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/smartos_global_zone.json b/test/units/module_utils/facts/system/distribution/fixtures/smartos_global_zone.json
new file mode 100644
index 0000000..ae01a10
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/smartos_global_zone.json
@@ -0,0 +1,24 @@
+{
+ "name": "SmartOS Global Zone",
+ "uname_v": "joyent_20160330T234717Z",
+ "result": {
+ "distribution_release": "SmartOS 20160330T234717Z x86_64",
+ "distribution": "SmartOS",
+ "os_family": "Solaris",
+ "distribution_version": "joyent_20160330T234717Z"
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "",
+ "name": "",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/release": " SmartOS 20160330T234717Z x86_64\n Copyright 2010 Sun Microsystems, Inc. All Rights Reserved.\n Copyright 2010-2012 Joyent, Inc. All Rights Reserved.\n Use is subject to license terms.\n\n Built with the following components:\n\n[\n { \"repo\": \"smartos-live\", \"branch\": \"release-20160331\", \"rev\": \"a77c410f2afe6dc9853a915733caec3609cc50f1\", \"commit_date\": \"1459340323\", \"url\": \"git@github.com:joyent/smartos-live.git\" }\n , { \"repo\": \"illumos-joyent\", \"branch\": \"release-20160331\", \"rev\": \"ab664c06caf06e9ce7586bff956e7709df1e702e\", \"commit_date\": \"1459362533\", \"url\": \"/root/data/jenkins/workspace/smartos/MG/build/illumos-joyent\" }\n , { \"repo\": \"illumos-extra\", \"branch\": \"release-20160331\", \"rev\": \"cc723855bceace3df7860b607c9e3827d47e0ff4\", \"commit_date\": \"1458153188\", \"url\": \"/root/data/jenkins/workspace/smartos/MG/build/illumos-extra\" }\n , { \"repo\": \"kvm\", \"branch\": \"release-20160331\", \"rev\": \"a8befd521c7e673749c64f118585814009fe4b73\", \"commit_date\": \"1450081968\", \"url\": \"/root/data/jenkins/workspace/smartos/MG/build/illumos-kvm\" }\n , { \"repo\": \"kvm-cmd\", \"branch\": \"release-20160331\", \"rev\": \"c1a197c8e4582c68739ab08f7e3198b2392c9820\", \"commit_date\": \"1454723558\", \"url\": \"/root/data/jenkins/workspace/smartos/MG/build/illumos-kvm-cmd\" }\n , { \"repo\": \"mdata-client\", \"branch\": \"release-20160331\", \"rev\": \"58158c44603a3316928975deccc5d10864832770\", \"commit_date\": \"1429917227\", \"url\": \"/root/data/jenkins/workspace/smartos/MG/build/mdata-client\" }\n]\n"
+ },
+ "platform.system": "SunOS"
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/smartos_zone.json b/test/units/module_utils/facts/system/distribution/fixtures/smartos_zone.json
new file mode 100644
index 0000000..8f20113
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/smartos_zone.json
@@ -0,0 +1,25 @@
+{
+ "name": "SmartOS Zone",
+ "uname_v": "joyent_20160330T234717Z",
+ "result": {
+ "distribution_release": "SmartOS x86_64",
+ "distribution": "SmartOS",
+ "os_family": "Solaris",
+ "distribution_version": "14.3.0"
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "",
+ "name": "",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/release": " SmartOS x86_64\n Copyright 2010 Sun Microsystems, Inc. All Rights Reserved.\n Copyright 2010-2013 Joyent, Inc. All Rights Reserved.\n Use is subject to license terms.\n See joyent_20141002T182809Z for assembly date and time.\n",
+ "/etc/product": "Name: Joyent Instance\nImage: base64 14.3.0\nDocumentation: http://wiki.joyent.com/jpc2/Base+Instance\n"
+ },
+ "platform.system": "SunOS"
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/smgl_na.json b/test/units/module_utils/facts/system/distribution/fixtures/smgl_na.json
new file mode 100644
index 0000000..f3436b8
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/smgl_na.json
@@ -0,0 +1,23 @@
+{
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "smgl",
+ "name": "Source Mage GNU/Linux",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/sourcemage-release": "Source Mage GNU/Linux x86_64-pc-linux-gnu\nInstalled from tarball using chroot image (Grimoire 0.61-rc) on Thu May 17 17:31:37 UTC 2012\n"
+ },
+ "name": "SMGL NA",
+ "result": {
+ "distribution_release": "NA",
+ "distribution": "SMGL",
+ "distribution_major_version": "NA",
+ "os_family": "SMGL",
+ "distribution_version": "NA"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/solaris_10.json b/test/units/module_utils/facts/system/distribution/fixtures/solaris_10.json
new file mode 100644
index 0000000..de1dbdc
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/solaris_10.json
@@ -0,0 +1,25 @@
+{
+ "name": "Solaris 10",
+ "uname_r": "5.10",
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "",
+ "name": "",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/release": " Oracle Solaris 10 1/13 s10x_u11wos_24a X86\n Copyright (c) 1983, 2013, Oracle and/or its affiliates. All rights reserved.\n Assembled 17 January 2013\n"
+ },
+ "platform.system": "SunOS",
+ "result": {
+ "distribution_release": "Oracle Solaris 10 1/13 s10x_u11wos_24a X86",
+ "distribution": "Solaris",
+ "os_family": "Solaris",
+ "distribution_major_version": "10",
+ "distribution_version": "10"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/solaris_11.3.json b/test/units/module_utils/facts/system/distribution/fixtures/solaris_11.3.json
new file mode 100644
index 0000000..056abe4
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/solaris_11.3.json
@@ -0,0 +1,25 @@
+{
+ "name": "Solaris 11.3",
+ "uname_r": "5.11",
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "",
+ "name": "",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/release": " Oracle Solaris 11.3 X86\n Copyright (c) 1983, 2018, Oracle and/or its affiliates. All rights reserved.\n Assembled 09 May 2018\n"
+ },
+ "platform.system": "SunOS",
+ "result": {
+ "distribution_release": "Oracle Solaris 11.3 X86",
+ "distribution": "Solaris",
+ "os_family": "Solaris",
+ "distribution_major_version": "11",
+ "distribution_version": "11.3"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/solaris_11.4.json b/test/units/module_utils/facts/system/distribution/fixtures/solaris_11.4.json
new file mode 100644
index 0000000..462d550
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/solaris_11.4.json
@@ -0,0 +1,35 @@
+{
+ "name": "Solaris 11.4",
+ "uname_r": "5.11",
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "",
+ "name": "",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {
+ "support_url": "https://support.oracle.com/",
+ "name": "Oracle Solaris",
+ "pretty_name": "Oracle Solaris 11.4",
+ "version": "11.4",
+ "id": "solaris",
+ "version_id": "11.4",
+ "build_id": "11.4.0.0.1.15.0",
+ "home_url": "https://www.oracle.com/solaris/",
+ "cpe_name": "cpe:/o:oracle:solaris:11:4"
+ },
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/release": " Oracle Solaris 11.4 SPARC\n Copyright (c) 1983, 2018, Oracle and/or its affiliates. All rights reserved.\n Assembled 14 September 2018\n"
+ },
+ "platform.system": "SunOS",
+ "result": {
+ "distribution_release": "Oracle Solaris 11.4 SPARC",
+ "distribution": "Solaris",
+ "os_family": "Solaris",
+ "distribution_major_version": "11",
+ "distribution_version": "11.4"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/solaris_11.json b/test/units/module_utils/facts/system/distribution/fixtures/solaris_11.json
new file mode 100644
index 0000000..749b8bc
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/solaris_11.json
@@ -0,0 +1,26 @@
+{
+ "name": "Solaris 11",
+ "uname_v": "11.0",
+ "uname_r": "5.11",
+ "result": {
+ "distribution_release": "Oracle Solaris 11 11/11 X86",
+ "distribution": "Solaris",
+ "os_family": "Solaris",
+ "distribution_major_version": "11",
+ "distribution_version": "11"
+ },
+ "platform.dist": ["", "", ""],
+ "distro": {
+ "codename": "",
+ "id": "",
+ "name": "",
+ "version": "",
+ "version_best": "",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/release": " Oracle Solaris 11 11/11 X86\n Copyright (c) 1983, 2011, Oracle and/or its affiliates. All rights reserved.\n Assembled 18 October 2011\n"
+ },
+ "platform.system": "SunOS"
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/steamos_2.0.json b/test/units/module_utils/facts/system/distribution/fixtures/steamos_2.0.json
new file mode 100644
index 0000000..7cb9c12
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/steamos_2.0.json
@@ -0,0 +1,40 @@
+{
+ "name": "SteamOS 2.0",
+ "input": {
+ "/etc/os-release": "PRETTY_NAME=\"SteamOS GNU/Linux 2.0 (brewmaster)\"\nNAME=\"SteamOS GNU/Linux\"\nVERSION_ID=\"2\"\nVERSION=\"2 (brewmaster)\"\nID=steamos\nID_LIKE=debian\nHOME_URL=\"http://www.steampowered.com/\"\nSUPPORT_URL=\"http://support.steampowered.com/\"\nBUG_REPORT_URL=\"http://support.steampowered.com/\"",
+ "/etc/lsb-release": "DISTRIB_ID=SteamOS\nDISTRIB_RELEASE=2.0\nDISTRIB_CODENAME=brewmaster\nDISTRIB_DESCRIPTION=\"SteamOS 2.0\""
+ },
+ "platform.dist": ["Steamos", "2.0", "brewmaster"],
+ "distro": {
+ "codename": "brewmaster",
+ "id": "steamos",
+ "name": "SteamOS GNU/Linux",
+ "version": "2.0",
+ "version_best": "2.0",
+ "os_release_info": {
+ "bug_report_url": "http://support.steampowered.com/",
+ "id_like": "debian",
+ "version_id": "2",
+ "pretty_name": "SteamOS GNU/Linux 2.0 (brewmaster)",
+ "version": "2 (brewmaster)",
+ "home_url": "http://www.steampowered.com/",
+ "name": "SteamOS GNU/Linux",
+ "support_url": "http://support.steampowered.com/",
+ "codename": "brewmaster",
+ "id": "steamos"
+ },
+ "lsb_release_info": {
+ "codename": "brewmaster",
+ "description": "SteamOS 2.0",
+ "distributor_id": "SteamOS",
+ "release": "2.0"
+ }
+ },
+ "result": {
+ "distribution": "SteamOS",
+ "distribution_major_version": "2",
+ "distribution_release": "brewmaster",
+ "os_family": "Debian",
+ "distribution_version": "2.0"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/tencentos_3_1.json b/test/units/module_utils/facts/system/distribution/fixtures/tencentos_3_1.json
new file mode 100644
index 0000000..f1051dd
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/tencentos_3_1.json
@@ -0,0 +1,50 @@
+{
+ "name": "TencentOS 3.1",
+ "distro": {
+ "codename": "Final",
+ "id": "tencentos",
+ "name": "TencentOS Server",
+ "version": "3.1",
+ "version_best": "3.1",
+ "lsb_release_info": {},
+ "os_release_info": {
+ "name": "TencentOS Server",
+ "version": "3.1 (Final)",
+ "id": "tencentos",
+ "id_like": "rhel fedora centos",
+ "version_id": "3.1",
+ "platform_id": "platform:el8",
+ "pretty_name": "TencentOS Server 3.1 (Final)",
+ "ansi_color": "0;31",
+ "cpe_name": "cpe:/o:tencentos:tencentos:3",
+ "home_url": "https://tlinux.qq.com/",
+ "bug_report_url": "https://tlinux.qq.com/",
+ "centos_mantisbt_project": "CentOS-8",
+ "centos_mantisbt_project_version": "8",
+ "redhat_support_product": "centos",
+ "redhat_support_product_version": "8",
+ "name_orig": "CentOS Linux",
+ "codename": "Final"
+ }
+ },
+ "input": {
+ "/etc/centos-release": "NAME=\"TencentOS Server\"\nVERSION=\"3.1 (Final)\"\nID=\"tencentos\"\nID_LIKE=\"rhel fedora centos\"\nVERSION_ID=\"3.1\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"TencentOS Server 3.1 (Final)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:tencentos:tencentos:3\"\nHOME_URL=\"https://tlinux.qq.com/\"\nBUG_REPORT_URL=\"https://tlinux.qq.com/\"\n\nCENTOS_MANTISBT_PROJECT=\"CentOS-8\"\nCENTOS_MANTISBT_PROJECT_VERSION=\"8\"\nREDHAT_SUPPORT_PRODUCT=\"centos\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"8\"\nNAME_ORIG=\"CentOS Linux\"\n",
+ "/etc/redhat-release": "CentOS Linux release 8.4.2105 (Core)\n",
+ "/etc/system-release": "NAME=\"TencentOS Server\"\nVERSION=\"3.1 (Final)\"\nID=\"tencentos\"\nID_LIKE=\"rhel fedora centos\"\nVERSION_ID=\"3.1\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"TencentOS Server 3.1 (Final)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:tencentos:tencentos:3\"\nHOME_URL=\"https://tlinux.qq.com/\"\nBUG_REPORT_URL=\"https://tlinux.qq.com/\"\n\nCENTOS_MANTISBT_PROJECT=\"CentOS-8\"\nCENTOS_MANTISBT_PROJECT_VERSION=\"8\"\nREDHAT_SUPPORT_PRODUCT=\"centos\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"8\"\nNAME_ORIG=\"CentOS Linux\"\n",
+ "/etc/os-release": "NAME=\"TencentOS Server\"\nVERSION=\"3.1 (Final)\"\nID=\"tencentos\"\nID_LIKE=\"rhel fedora centos\"\nVERSION_ID=\"3.1\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"TencentOS Server 3.1 (Final)\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:tencentos:tencentos:3\"\nHOME_URL=\"https://tlinux.qq.com/\"\nBUG_REPORT_URL=\"https://tlinux.qq.com/\"\n\nCENTOS_MANTISBT_PROJECT=\"CentOS-8\"\nCENTOS_MANTISBT_PROJECT_VERSION=\"8\"\nREDHAT_SUPPORT_PRODUCT=\"centos\"\nREDHAT_SUPPORT_PRODUCT_VERSION=\"8\"\nNAME_ORIG=\"CentOS Linux\"\n",
+ "/usr/lib/os-release": "NAME=\"CentOS Linux\"\nVERSION=\"8\"\nID=\"centos\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"8\"\nPLATFORM_ID=\"platform:el8\"\nPRETTY_NAME=\"CentOS Linux 8\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:centos:centos:8\"\nHOME_URL=\"https://centos.org/\"\nBUG_REPORT_URL=\"https://bugs.centos.org/\"\nCENTOS_MANTISBT_PROJECT=\"CentOS-8\"\nCENTOS_MANTISBT_PROJECT_VERSION=\"8\"\n"
+ },
+ "platform.dist": [
+ "tencentos",
+ "3.1",
+ "Final"
+ ],
+ "result": {
+ "distribution": "TencentOS",
+ "distribution_version": "3.1",
+ "distribution_release": "Final",
+ "distribution_major_version": "3",
+ "os_family": "RedHat"
+ },
+ "platform.release": "5.4.32-19-0001"
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/truenas_12.0rc1.json b/test/units/module_utils/facts/system/distribution/fixtures/truenas_12.0rc1.json
new file mode 100644
index 0000000..9a9efe3
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/truenas_12.0rc1.json
@@ -0,0 +1,39 @@
+{
+ "name": "FreeBSD 12.2",
+ "distro": {
+ "codename": "",
+ "id": "freebsd",
+ "name": "FreeBSD",
+ "version": "12.2",
+ "version_best": "12.2",
+ "lsb_release_info": {},
+ "os_release_info": {
+ "name": "FreeBSD",
+ "version": "12.2-PRERELEASE",
+ "version_id": "12.2",
+ "id": "freebsd",
+ "ansi_color": "0;31",
+ "pretty_name": "FreeBSD 12.2-PRERELEASE",
+ "cpe_name": "cpe:/o:freebsd:freebsd:12.2",
+ "home_url": "https://FreeBSD.org/",
+ "bug_report_url": "https://bugs.FreeBSD.org/"
+ }
+ },
+ "input": {
+ "/etc/os-release": "NAME=FreeBSD\nVERSION=12.2-PRERELEASE\nVERSION_ID=12.2\nID=freebsd\nANSI_COLOR=\"0;31\"\nPRETTY_NAME=\"FreeBSD 12.2-PRERELEASE\"\nCPE_NAME=cpe:/o:freebsd:freebsd:12.2\nHOME_URL=https://FreeBSD.org/\nBUG_REPORT_URL=https://bugs.FreeBSD.org/\n"
+ },
+ "platform.dist": [
+ "freebsd",
+ "12.2",
+ ""
+ ],
+ "result": {
+ "distribution": "FreeBSD",
+ "distribution_version": "12.2",
+ "distribution_release": "12.2-PRERELEASE",
+ "distribution_major_version": "12",
+ "os_family": "FreeBSD"
+ },
+ "platform.system": "FreeBSD",
+ "platform.release": "12.2-PRERELEASE"
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_10.04_guess.json b/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_10.04_guess.json
new file mode 100644
index 0000000..38a6040
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_10.04_guess.json
@@ -0,0 +1,23 @@
+{
+ "name": "Ubuntu 10.04 guess",
+ "input": {
+ "/etc/lsb-release": "DISTRIB_ID=Ubuntu\nDISTRIB_RELEASE=10.04\nDISTRIB_CODENAME=lucid\nDISTRIB_DESCRIPTION=\"Ubuntu 10.04.4 LTS"
+ },
+ "platform.dist": ["Ubuntu", "10.04", "lucid"],
+ "distro": {
+ "codename": "lucid",
+ "id": "ubuntu",
+ "name": "Ubuntu",
+ "version": "10.04",
+ "version_best": "10.04.1",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "Ubuntu",
+ "distribution_major_version": "10",
+ "distribution_release": "lucid",
+ "os_family": "Debian",
+ "distribution_version": "10.04"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_12.04.json b/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_12.04.json
new file mode 100644
index 0000000..01203b5
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_12.04.json
@@ -0,0 +1,24 @@
+{
+ "name": "Ubuntu 12.04",
+ "input": {
+ "/etc/lsb-release": "DISTRIB_ID=Ubuntu\nDISTRIB_RELEASE=12.04\nDISTRIB_CODENAME=precise\nDISTRIB_DESCRIPTION=\"Ubuntu 12.04.5 LTS\"",
+ "/etc/os-release": "NAME=\"Ubuntu\"\nVERSION=\"12.04.5 LTS, Precise Pangolin\"\nID=ubuntu\nID_LIKE=debian\nPRETTY_NAME=\"Ubuntu precise (12.04.5 LTS)\"\nVERSION_ID=\"12.04\""
+ },
+ "platform.dist": ["Ubuntu", "12.04", "precise"],
+ "distro": {
+ "codename": "precise",
+ "id": "ubuntu",
+ "name": "Ubuntu",
+ "version": "12.04",
+ "version_best": "12.04.5",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "Ubuntu",
+ "distribution_major_version": "12",
+ "distribution_release": "precise",
+ "os_family": "Debian",
+ "distribution_version": "12.04"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_14.04.json b/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_14.04.json
new file mode 100644
index 0000000..5d5af0a
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_14.04.json
@@ -0,0 +1,24 @@
+{
+ "name": "Ubuntu 14.04",
+ "input": {
+ "/etc/lsb-release": "DISTRIB_ID=Ubuntu\nDISTRIB_RELEASE=14.04\nDISTRIB_CODENAME=trusty\nDISTRIB_DESCRIPTION=\"Ubuntu 14.04.4 LTS\"",
+ "/etc/os-release": "NAME=\"Ubuntu\"\nVERSION=\"14.04.4 LTS, Trusty Tahr\"\nID=ubuntu\nID_LIKE=debian\nPRETTY_NAME=\"Ubuntu 14.04.4 LTS\"\nVERSION_ID=\"14.04\"\nHOME_URL=\"http://www.ubuntu.com/\"\nSUPPORT_URL=\"http://help.ubuntu.com/\"\nBUG_REPORT_URL=\"http://bugs.launchpad.net/ubuntu/\""
+ },
+ "platform.dist": ["Ubuntu", "14.04", "trusty"],
+ "distro": {
+ "codename": "trusty",
+ "id": "ubuntu",
+ "name": "Ubuntu",
+ "version": "14.04",
+ "version_best": "14.04.4",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "result": {
+ "distribution": "Ubuntu",
+ "distribution_major_version": "14",
+ "distribution_release": "trusty",
+ "os_family": "Debian",
+ "distribution_version": "14.04"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_16.04.json b/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_16.04.json
new file mode 100644
index 0000000..f8f50a9
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_16.04.json
@@ -0,0 +1,24 @@
+{
+ "platform.dist": ["Ubuntu", "16.04", "xenial"],
+ "distro": {
+ "codename": "xenial",
+ "id": "ubuntu",
+ "name": "Ubuntu",
+ "version": "16.04",
+ "version_best": "16.04.6",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"Ubuntu\"\nVERSION=\"16.04 LTS (Xenial Xerus)\"\nID=ubuntu\nID_LIKE=debian\nPRETTY_NAME=\"Ubuntu 16.04 LTS\"\nVERSION_ID=\"16.04\"\nHOME_URL=\"http://www.ubuntu.com/\"\nSUPPORT_URL=\"http://help.ubuntu.com/\"\nBUG_REPORT_URL=\"http://bugs.launchpad.net/ubuntu/\"\nUBUNTU_CODENAME=xenial\n",
+ "/etc/lsb-release": "DISTRIB_ID=Ubuntu\nDISTRIB_RELEASE=16.04\nDISTRIB_CODENAME=xenial\nDISTRIB_DESCRIPTION=\"Ubuntu 16.04 LTS\"\n"
+ },
+ "name": "Ubuntu 16.04",
+ "result": {
+ "distribution_release": "xenial",
+ "distribution": "Ubuntu",
+ "distribution_major_version": "16",
+ "os_family": "Debian",
+ "distribution_version": "16.04"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_18.04.json b/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_18.04.json
new file mode 100644
index 0000000..12d15b5
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/ubuntu_18.04.json
@@ -0,0 +1,39 @@
+{
+ "name": "Ubuntu 18.04",
+ "distro": {
+ "codename": "bionic",
+ "id": "ubuntu",
+ "name": "Ubuntu",
+ "version": "18.04",
+ "version_best": "18.04.3",
+ "lsb_release_info": {},
+ "os_release_info": {
+ "name": "Ubuntu",
+ "version": "18.04.3 LTS (Bionic Beaver)",
+ "id": "ubuntu",
+ "id_like": "debian",
+ "pretty_name": "Ubuntu 18.04.3 LTS",
+ "version_id": "18.04",
+ "home_url": "https://www.ubuntu.com/",
+ "support_url": "https://help.ubuntu.com/",
+ "bug_report_url": "https://bugs.launchpad.net/ubuntu/",
+ "privacy_policy_url": "https://www.ubuntu.com/legal/terms-and-policies/privacy-policy",
+ "version_codename": "bionic",
+ "ubuntu_codename": "bionic",
+ "codename": "bionic"
+ }
+ },
+ "input": {
+ "/etc/os-release": "NAME=\"Ubuntu\"\nVERSION=\"18.04.3 LTS (Bionic Beaver)\"\nID=ubuntu\nID_LIKE=debian\nPRETTY_NAME=\"Ubuntu 18.04.3 LTS\"\nVERSION_ID=\"18.04\"\nHOME_URL=\"https://www.ubuntu.com/\"\nSUPPORT_URL=\"https://help.ubuntu.com/\"\nBUG_REPORT_URL=\"https://bugs.launchpad.net/ubuntu/\"\nPRIVACY_POLICY_URL=\"https://www.ubuntu.com/legal/terms-and-policies/privacy-policy\"\nVERSION_CODENAME=bionic\nUBUNTU_CODENAME=bionic\n",
+ "/etc/lsb-release": "DISTRIB_ID=Ubuntu\nDISTRIB_RELEASE=18.04\nDISTRIB_CODENAME=bionic\nDISTRIB_DESCRIPTION=\"Ubuntu 18.04.3 LTS\"\n",
+ "/usr/lib/os-release": "NAME=\"Ubuntu\"\nVERSION=\"18.04.3 LTS (Bionic Beaver)\"\nID=ubuntu\nID_LIKE=debian\nPRETTY_NAME=\"Ubuntu 18.04.3 LTS\"\nVERSION_ID=\"18.04\"\nHOME_URL=\"https://www.ubuntu.com/\"\nSUPPORT_URL=\"https://help.ubuntu.com/\"\nBUG_REPORT_URL=\"https://bugs.launchpad.net/ubuntu/\"\nPRIVACY_POLICY_URL=\"https://www.ubuntu.com/legal/terms-and-policies/privacy-policy\"\nVERSION_CODENAME=bionic\nUBUNTU_CODENAME=bionic\n"
+ },
+ "platform.dist": ["ubuntu", "18.04", "bionic"],
+ "result": {
+ "distribution": "Ubuntu",
+ "distribution_version": "18.04",
+ "distribution_release": "bionic",
+ "distribution_major_version": "18",
+ "os_family": "Debian"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/uos_20.json b/test/units/module_utils/facts/system/distribution/fixtures/uos_20.json
new file mode 100644
index 0000000..d51f62d
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/uos_20.json
@@ -0,0 +1,29 @@
+{
+ "name": "Uos 20",
+ "distro": {
+ "codename": "fou",
+ "id": "Uos",
+ "name": "Uos",
+ "version": "20",
+ "version_best": "20",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/os-release": "PRETTY_NAME=\"UnionTech OS Server 20\"\nNAME=\"UnionTech OS Server 20\"\nVERSION_ID=\"20\"\nVERSION=\"20\"\nID=UOS\nHOME_URL=\"https://www.chinauos.com/\"\nBUG_REPORT_URL=\"https://bbs.chinauos.com/\"\nVERSION_CODENAME=fou",
+ "/etc/lsb-release": "DISTRIB_ID=uos\nDISTRIB_RELEASE=20\nDISTRIB_DESCRIPTION=\"UnionTech OS Server 20\"\nDISTRIB_CODENAME=fou\n",
+ "/usr/lib/os-release": "PRETTY_NAME=\"UnionTech OS Server 20\"\nNAME=\"UnionTech OS Server 20\"\nVERSION_ID=\"20\"\nVERSION=\"20\"\nID=UOS\nHOME_URL=\"https://www.chinauos.com/\"\nBUG_REPORT_URL=\"https://bbs.chinauos.com/\"\nVERSION_CODENAME=fou"
+ },
+ "platform.dist": [
+ "uos",
+ "20",
+ "fou"
+ ],
+ "result": {
+ "distribution": "Uos",
+ "distribution_version": "20",
+ "distribution_release": "fou",
+ "distribution_major_version": "20",
+ "os_family": "Debian"
+ }
+}
diff --git a/test/units/module_utils/facts/system/distribution/fixtures/virtuozzo_7.3.json b/test/units/module_utils/facts/system/distribution/fixtures/virtuozzo_7.3.json
new file mode 100644
index 0000000..d9c2f47
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/fixtures/virtuozzo_7.3.json
@@ -0,0 +1,25 @@
+{
+ "name": "Virtuozzo 7.3",
+ "platform.dist": ["redhat", "7.3", ""],
+ "distro": {
+ "codename": "",
+ "id": "virtuozzo",
+ "name": "Virtuozzo Linux",
+ "version": "7.3",
+ "version_best": "7.3",
+ "os_release_info": {},
+ "lsb_release_info": {}
+ },
+ "input": {
+ "/etc/redhat-release": "Virtuozzo Linux release 7.3\n",
+ "/etc/os-release": "NAME=\"Virtuozzo\"\nVERSION=\"7.0.3\"\nID=\"virtuozzo\"\nID_LIKE=\"rhel fedora\"\nVERSION_ID=\"7\"\nPRETTY_NAME=\"Virtuozzo release 7.0.3\"\nANSI_COLOR=\"0;31\"\nCPE_NAME=\"cpe:/o:virtuozzoproject:vz:7\"\nHOME_URL=\"http://www.virtuozzo.com\"\nBUG_REPORT_URL=\"https://bugs.openvz.org/\"\n",
+ "/etc/system-release": "Virtuozzo release 7.0.3 (640)\n"
+ },
+ "result": {
+ "distribution_release": "NA",
+ "distribution": "Virtuozzo",
+ "distribution_major_version": "7",
+ "os_family": "RedHat",
+ "distribution_version": "7.3"
+ }
+} \ No newline at end of file
diff --git a/test/units/module_utils/facts/system/distribution/test_distribution_sles4sap.py b/test/units/module_utils/facts/system/distribution/test_distribution_sles4sap.py
new file mode 100644
index 0000000..ab465ea
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/test_distribution_sles4sap.py
@@ -0,0 +1,33 @@
+# -*- 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
+
+import pytest
+
+from ansible.module_utils.facts.system.distribution import DistributionFiles
+
+
+@pytest.mark.parametrize('realpath', ('SUSE_SLES_SAP.prod', 'SLES_SAP.prod'))
+def test_distribution_sles4sap_suse_sles_sap(mock_module, mocker, realpath):
+ mocker.patch('os.path.islink', return_value=True)
+ mocker.patch('os.path.realpath', return_value='/etc/products.d/' + realpath)
+
+ test_input = {
+ 'name': 'SUSE',
+ 'path': '',
+ 'data': 'suse',
+ 'collected_facts': None,
+ }
+
+ test_result = (
+ True,
+ {
+ 'distribution': 'SLES_SAP',
+ }
+ )
+
+ distribution = DistributionFiles(module=mock_module())
+ assert test_result == distribution.parse_distribution_file_SUSE(**test_input)
diff --git a/test/units/module_utils/facts/system/distribution/test_distribution_version.py b/test/units/module_utils/facts/system/distribution/test_distribution_version.py
new file mode 100644
index 0000000..a990274
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/test_distribution_version.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+# 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
+
+import glob
+import json
+import os
+import pytest
+from itertools import product
+
+from ansible.module_utils.six.moves import builtins
+
+# the module we are actually testing (sort of)
+from ansible.module_utils.facts.system.distribution import DistributionFactCollector
+
+# to generate the testcase data, you can use the script gen_distribution_version_testcase.py in hacking/tests
+TESTSETS = []
+
+for datafile in glob.glob(os.path.join(os.path.dirname(__file__), 'fixtures/*.json')):
+ with open(os.path.join(os.path.dirname(__file__), '%s' % datafile)) as f:
+ TESTSETS.append(json.loads(f.read()))
+
+
+@pytest.mark.parametrize("stdin, testcase", product([{}], TESTSETS), ids=lambda x: x.get('name'), indirect=['stdin'])
+def test_distribution_version(am, mocker, testcase):
+ """tests the distribution parsing code of the Facts class
+
+ testsets have
+ * a name (for output/debugging only)
+ * input files that are faked
+ * those should be complete and also include "irrelevant" files that might be mistaken as coming from other distributions
+ * all files that are not listed here are assumed to not exist at all
+ * the output of ansible.module_utils.distro.linux_distribution() [called platform.dist() for historical reasons]
+ * results for the ansible variables distribution* and os_family
+
+ """
+
+ # prepare some mock functions to get the testdata in
+ def mock_get_file_content(fname, default=None, strip=True):
+ """give fake content if it exists, otherwise pretend the file is empty"""
+ data = default
+ if fname in testcase['input']:
+ # for debugging
+ print('faked %s for %s' % (fname, testcase['name']))
+ data = testcase['input'][fname].strip()
+ if strip and data is not None:
+ data = data.strip()
+ return data
+
+ def mock_get_file_lines(fname, strip=True):
+ """give fake lines if file exists, otherwise return empty list"""
+ data = mock_get_file_content(fname=fname, strip=strip)
+ if data:
+ return [data]
+ return []
+
+ def mock_get_uname(am, flags):
+ if '-v' in flags:
+ return testcase.get('uname_v', None)
+ elif '-r' in flags:
+ return testcase.get('uname_r', None)
+ else:
+ return None
+
+ def mock_file_exists(fname, allow_empty=False):
+ if fname not in testcase['input']:
+ return False
+
+ if allow_empty:
+ return True
+ return bool(len(testcase['input'][fname]))
+
+ def mock_platform_system():
+ return testcase.get('platform.system', 'Linux')
+
+ def mock_platform_release():
+ return testcase.get('platform.release', '')
+
+ def mock_platform_version():
+ return testcase.get('platform.version', '')
+
+ def mock_distro_name():
+ return testcase['distro']['name']
+
+ def mock_distro_id():
+ return testcase['distro']['id']
+
+ def mock_distro_version(best=False):
+ if best:
+ return testcase['distro']['version_best']
+ return testcase['distro']['version']
+
+ def mock_distro_codename():
+ return testcase['distro']['codename']
+
+ def mock_distro_os_release_info():
+ return testcase['distro']['os_release_info']
+
+ def mock_distro_lsb_release_info():
+ return testcase['distro']['lsb_release_info']
+
+ def mock_open(filename, mode='r'):
+ if filename in testcase['input']:
+ file_object = mocker.mock_open(read_data=testcase['input'][filename]).return_value
+ file_object.__iter__.return_value = testcase['input'][filename].splitlines(True)
+ else:
+ file_object = real_open(filename, mode)
+ return file_object
+
+ def mock_os_path_is_file(filename):
+ if filename in testcase['input']:
+ return True
+ return False
+
+ def mock_run_command_output(v, command):
+ ret = (0, '', '')
+ if 'command_output' in testcase:
+ ret = (0, testcase['command_output'].get(command, ''), '')
+ return ret
+
+ mocker.patch('ansible.module_utils.facts.system.distribution.get_file_content', mock_get_file_content)
+ mocker.patch('ansible.module_utils.facts.system.distribution.get_file_lines', mock_get_file_lines)
+ mocker.patch('ansible.module_utils.facts.system.distribution.get_uname', mock_get_uname)
+ mocker.patch('ansible.module_utils.facts.system.distribution._file_exists', mock_file_exists)
+ mocker.patch('ansible.module_utils.distro.name', mock_distro_name)
+ mocker.patch('ansible.module_utils.distro.id', mock_distro_id)
+ mocker.patch('ansible.module_utils.distro.version', mock_distro_version)
+ mocker.patch('ansible.module_utils.distro.codename', mock_distro_codename)
+ mocker.patch(
+ 'ansible.module_utils.common.sys_info.distro.os_release_info',
+ mock_distro_os_release_info)
+ mocker.patch(
+ 'ansible.module_utils.common.sys_info.distro.lsb_release_info',
+ mock_distro_lsb_release_info)
+ mocker.patch('os.path.isfile', mock_os_path_is_file)
+ mocker.patch('platform.system', mock_platform_system)
+ mocker.patch('platform.release', mock_platform_release)
+ mocker.patch('platform.version', mock_platform_version)
+ mocker.patch('ansible.module_utils.basic.AnsibleModule.run_command', mock_run_command_output)
+
+ real_open = builtins.open
+ mocker.patch.object(builtins, 'open', new=mock_open)
+
+ # run Facts()
+ distro_collector = DistributionFactCollector()
+ generated_facts = distro_collector.collect(am)
+
+ # compare with the expected output
+
+ # testcase['result'] has a list of variables and values it expects Facts() to set
+ for key, val in testcase['result'].items():
+ assert key in generated_facts
+ msg = 'Comparing value of %s on %s, should: %s, is: %s' %\
+ (key, testcase['name'], val, generated_facts[key])
+ assert generated_facts[key] == val, msg
diff --git a/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_ClearLinux.py b/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_ClearLinux.py
new file mode 100644
index 0000000..c095756
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_ClearLinux.py
@@ -0,0 +1,51 @@
+# -*- 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
+
+import os
+import pytest
+
+from ansible.module_utils.facts.system.distribution import DistributionFiles
+
+
+@pytest.fixture
+def test_input():
+ return {
+ 'name': 'Clearlinux',
+ 'path': '/usr/lib/os-release',
+ 'collected_facts': None,
+ }
+
+
+def test_parse_distribution_file_clear_linux(mock_module, test_input):
+ test_input['data'] = open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files/ClearLinux')).read()
+
+ result = (
+ True,
+ {
+ 'distribution': 'Clear Linux OS',
+ 'distribution_major_version': '28120',
+ 'distribution_release': 'clear-linux-os',
+ 'distribution_version': '28120'
+ }
+ )
+
+ distribution = DistributionFiles(module=mock_module())
+ assert result == distribution.parse_distribution_file_ClearLinux(**test_input)
+
+
+@pytest.mark.parametrize('distro_file', ('CoreOS', 'LinuxMint'))
+def test_parse_distribution_file_clear_linux_no_match(mock_module, distro_file, test_input):
+ """
+ Test against data from Linux Mint and CoreOS to ensure we do not get a reported
+ match from parse_distribution_file_ClearLinux()
+ """
+ test_input['data'] = open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files', distro_file)).read()
+
+ result = (False, {})
+
+ distribution = DistributionFiles(module=mock_module())
+ assert result == distribution.parse_distribution_file_ClearLinux(**test_input)
diff --git a/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_Slackware.py b/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_Slackware.py
new file mode 100644
index 0000000..53fd4ea
--- /dev/null
+++ b/test/units/module_utils/facts/system/distribution/test_parse_distribution_file_Slackware.py
@@ -0,0 +1,37 @@
+# -*- 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
+
+import os
+import pytest
+
+from ansible.module_utils.facts.system.distribution import DistributionFiles
+
+
+@pytest.mark.parametrize(
+ ('distro_file', 'expected_version'),
+ (
+ ('Slackware', '14.1'),
+ ('SlackwareCurrent', '14.2+'),
+ )
+)
+def test_parse_distribution_file_slackware(mock_module, distro_file, expected_version):
+ test_input = {
+ 'name': 'Slackware',
+ 'data': open(os.path.join(os.path.dirname(__file__), '../../fixtures/distribution_files', distro_file)).read(),
+ 'path': '/etc/os-release',
+ 'collected_facts': None,
+ }
+
+ result = (
+ True,
+ {
+ 'distribution': 'Slackware',
+ 'distribution_version': expected_version
+ }
+ )
+ distribution = DistributionFiles(module=mock_module())
+ assert result == distribution.parse_distribution_file_Slackware(**test_input)
diff --git a/test/units/module_utils/facts/system/test_cmdline.py b/test/units/module_utils/facts/system/test_cmdline.py
new file mode 100644
index 0000000..59cfd11
--- /dev/null
+++ b/test/units/module_utils/facts/system/test_cmdline.py
@@ -0,0 +1,67 @@
+# unit tests for ansible system cmdline fact collectors
+# -*- coding: utf-8 -*-
+# 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
+
+import pytest
+from ansible.module_utils.facts.system.cmdline import CmdLineFactCollector
+
+test_data = [
+ (
+ "crashkernel=auto rd.lvm.lv=fedora_test-elementary-os/root rd.lvm.lv=fedora_test-elementary-os/swap rhgb quiet",
+ {
+ 'crashkernel': 'auto',
+ 'quiet': True,
+ 'rd.lvm.lv': [
+ 'fedora_test-elementary-os/root',
+ 'fedora_test-elementary-os/swap',
+ ],
+ 'rhgb': True
+ }
+ ),
+ (
+ "root=/dev/mapper/vg_ssd-root ro rd.lvm.lv=fedora_xenon/root rd.lvm.lv=fedora_xenon/swap rhgb quiet "
+ "resume=/dev/mapper/fedora_xenon-swap crashkernel=128M zswap.enabled=1",
+ {
+ 'crashkernel': '128M',
+ 'quiet': True,
+ 'rd.lvm.lv': [
+ 'fedora_xenon/root',
+ 'fedora_xenon/swap'
+ ],
+ 'resume': '/dev/mapper/fedora_xenon-swap',
+ 'rhgb': True,
+ 'ro': True,
+ 'root': '/dev/mapper/vg_ssd-root',
+ 'zswap.enabled': '1'
+ }
+ ),
+ (
+ "rhgb",
+ {
+ "rhgb": True
+ }
+ ),
+ (
+ "root=/dev/mapper/vg_ssd-root",
+ {
+ 'root': '/dev/mapper/vg_ssd-root',
+ }
+ ),
+ (
+ "",
+ {},
+ )
+]
+
+test_ids = ['lvm_1', 'lvm_2', 'single_without_equal_sign', 'single_with_equal_sign', 'blank_cmdline']
+
+
+@pytest.mark.parametrize("cmdline, cmdline_dict", test_data, ids=test_ids)
+def test_cmd_line_factor(cmdline, cmdline_dict):
+ cmdline_facter = CmdLineFactCollector()
+ parsed_cmdline = cmdline_facter._parse_proc_cmdline_facts(data=cmdline)
+ assert parsed_cmdline == cmdline_dict
diff --git a/test/units/module_utils/facts/system/test_lsb.py b/test/units/module_utils/facts/system/test_lsb.py
new file mode 100644
index 0000000..e2ed2ec
--- /dev/null
+++ b/test/units/module_utils/facts/system/test_lsb.py
@@ -0,0 +1,108 @@
+# unit tests for ansible system lsb fact collectors
+# -*- coding: utf-8 -*-
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat.mock import Mock, patch
+
+from .. base import BaseFactsTest
+
+from ansible.module_utils.facts.system.lsb import LSBFactCollector
+
+
+lsb_release_a_fedora_output = '''
+LSB Version: :core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch
+Distributor ID: Fedora
+Description: Fedora release 25 (Twenty Five)
+Release: 25
+Codename: TwentyFive
+''' # noqa
+
+# FIXME: a
+etc_lsb_release_ubuntu14 = '''DISTRIB_ID=Ubuntu
+DISTRIB_RELEASE=14.04
+DISTRIB_CODENAME=trusty
+DISTRIB_DESCRIPTION="Ubuntu 14.04.3 LTS"
+'''
+etc_lsb_release_no_decimal = '''DISTRIB_ID=AwesomeOS
+DISTRIB_RELEASE=11
+DISTRIB_CODENAME=stonehenge
+DISTRIB_DESCRIPTION="AwesomeÖS 11"
+'''
+
+
+class TestLSBFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'lsb']
+ valid_subsets = ['lsb']
+ fact_namespace = 'ansible_lsb'
+ collector_class = LSBFactCollector
+
+ def _mock_module(self):
+ mock_module = Mock()
+ mock_module.params = {'gather_subset': self.gather_subset,
+ 'gather_timeout': 10,
+ 'filter': '*'}
+ mock_module.get_bin_path = Mock(return_value='/usr/bin/lsb_release')
+ mock_module.run_command = Mock(return_value=(0, lsb_release_a_fedora_output, ''))
+ return mock_module
+
+ def test_lsb_release_bin(self):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module)
+
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict['lsb']['release'], '25')
+ self.assertEqual(facts_dict['lsb']['id'], 'Fedora')
+ self.assertEqual(facts_dict['lsb']['description'], 'Fedora release 25 (Twenty Five)')
+ self.assertEqual(facts_dict['lsb']['codename'], 'TwentyFive')
+ self.assertEqual(facts_dict['lsb']['major_release'], '25')
+
+ def test_etc_lsb_release(self):
+ module = self._mock_module()
+ module.get_bin_path = Mock(return_value=None)
+ with patch('ansible.module_utils.facts.system.lsb.os.path.exists',
+ return_value=True):
+ with patch('ansible.module_utils.facts.system.lsb.get_file_lines',
+ return_value=etc_lsb_release_ubuntu14.splitlines()):
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module)
+
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict['lsb']['release'], '14.04')
+ self.assertEqual(facts_dict['lsb']['id'], 'Ubuntu')
+ self.assertEqual(facts_dict['lsb']['description'], 'Ubuntu 14.04.3 LTS')
+ self.assertEqual(facts_dict['lsb']['codename'], 'trusty')
+
+ def test_etc_lsb_release_no_decimal_release(self):
+ module = self._mock_module()
+ module.get_bin_path = Mock(return_value=None)
+ with patch('ansible.module_utils.facts.system.lsb.os.path.exists',
+ return_value=True):
+ with patch('ansible.module_utils.facts.system.lsb.get_file_lines',
+ return_value=etc_lsb_release_no_decimal.splitlines()):
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module)
+
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict['lsb']['release'], '11')
+ self.assertEqual(facts_dict['lsb']['id'], 'AwesomeOS')
+ self.assertEqual(facts_dict['lsb']['description'], 'AwesomeÖS 11')
+ self.assertEqual(facts_dict['lsb']['codename'], 'stonehenge')
diff --git a/test/units/module_utils/facts/system/test_user.py b/test/units/module_utils/facts/system/test_user.py
new file mode 100644
index 0000000..5edfe14
--- /dev/null
+++ b/test/units/module_utils/facts/system/test_user.py
@@ -0,0 +1,40 @@
+# unit tests for ansible system lsb fact collectors
+# -*- coding: utf-8 -*-
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils.facts.system.user import UserFactCollector
+
+import os
+
+
+def test_logname():
+ """ Test if ``UserFactCollector`` still works with LOGNAME set """
+ collector = UserFactCollector()
+
+ unmodified_facts = collector.collect()
+ # Change logname env var and check if the collector still finds
+ # the pw entry.
+ os.environ["LOGNAME"] = "NONEXISTINGUSERDONTEXISTPLEASE"
+ modified_facts = collector.collect()
+
+ # Set logname should be different to the real name.
+ assert unmodified_facts['user_id'] != modified_facts['user_id']
+ # Actual UID is the same.
+ assert unmodified_facts['user_uid'] == modified_facts['user_uid']
diff --git a/test/units/module_utils/facts/test_ansible_collector.py b/test/units/module_utils/facts/test_ansible_collector.py
new file mode 100644
index 0000000..47d88df
--- /dev/null
+++ b/test/units/module_utils/facts/test_ansible_collector.py
@@ -0,0 +1,524 @@
+# -*- coding: utf-8 -*-
+#
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+# for testing
+from units.compat import unittest
+from units.compat.mock import Mock, patch
+
+from ansible.module_utils.facts import collector
+from ansible.module_utils.facts import ansible_collector
+from ansible.module_utils.facts import namespace
+
+from ansible.module_utils.facts.other.facter import FacterFactCollector
+from ansible.module_utils.facts.other.ohai import OhaiFactCollector
+
+from ansible.module_utils.facts.system.apparmor import ApparmorFactCollector
+from ansible.module_utils.facts.system.caps import SystemCapabilitiesFactCollector
+from ansible.module_utils.facts.system.date_time import DateTimeFactCollector
+from ansible.module_utils.facts.system.env import EnvFactCollector
+from ansible.module_utils.facts.system.distribution import DistributionFactCollector
+from ansible.module_utils.facts.system.dns import DnsFactCollector
+from ansible.module_utils.facts.system.fips import FipsFactCollector
+from ansible.module_utils.facts.system.local import LocalFactCollector
+from ansible.module_utils.facts.system.lsb import LSBFactCollector
+from ansible.module_utils.facts.system.pkg_mgr import PkgMgrFactCollector, OpenBSDPkgMgrFactCollector
+from ansible.module_utils.facts.system.platform import PlatformFactCollector
+from ansible.module_utils.facts.system.python import PythonFactCollector
+from ansible.module_utils.facts.system.selinux import SelinuxFactCollector
+from ansible.module_utils.facts.system.service_mgr import ServiceMgrFactCollector
+from ansible.module_utils.facts.system.user import UserFactCollector
+
+# from ansible.module_utils.facts.hardware.base import HardwareCollector
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.virtual.base import VirtualCollector
+
+ALL_COLLECTOR_CLASSES = \
+ [PlatformFactCollector,
+ DistributionFactCollector,
+ SelinuxFactCollector,
+ ApparmorFactCollector,
+ SystemCapabilitiesFactCollector,
+ FipsFactCollector,
+ PkgMgrFactCollector,
+ OpenBSDPkgMgrFactCollector,
+ ServiceMgrFactCollector,
+ LSBFactCollector,
+ DateTimeFactCollector,
+ UserFactCollector,
+ LocalFactCollector,
+ EnvFactCollector,
+ DnsFactCollector,
+ PythonFactCollector,
+ # FIXME: re-enable when hardware doesnt Hardware() doesnt munge self.facts
+ # HardwareCollector
+ NetworkCollector,
+ VirtualCollector,
+ OhaiFactCollector,
+ FacterFactCollector]
+
+
+def mock_module(gather_subset=None,
+ filter=None):
+ if gather_subset is None:
+ gather_subset = ['all', '!facter', '!ohai']
+ if filter is None:
+ filter = '*'
+ mock_module = Mock()
+ mock_module.params = {'gather_subset': gather_subset,
+ 'gather_timeout': 5,
+ 'filter': filter}
+ mock_module.get_bin_path = Mock(return_value=None)
+ return mock_module
+
+
+def _collectors(module,
+ all_collector_classes=None,
+ minimal_gather_subset=None):
+ gather_subset = module.params.get('gather_subset')
+ if all_collector_classes is None:
+ all_collector_classes = ALL_COLLECTOR_CLASSES
+ if minimal_gather_subset is None:
+ minimal_gather_subset = frozenset([])
+
+ collector_classes = \
+ collector.collector_classes_from_gather_subset(all_collector_classes=all_collector_classes,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=gather_subset)
+
+ collectors = []
+ for collector_class in collector_classes:
+ collector_obj = collector_class()
+ collectors.append(collector_obj)
+
+ # Add a collector that knows what gather_subset we used so it it can provide a fact
+ collector_meta_data_collector = \
+ ansible_collector.CollectorMetaDataCollector(gather_subset=gather_subset,
+ module_setup=True)
+ collectors.append(collector_meta_data_collector)
+
+ return collectors
+
+
+ns = namespace.PrefixFactNamespace('ansible_facts', 'ansible_')
+
+
+# FIXME: this is brute force, but hopefully enough to get some refactoring to make facts testable
+class TestInPlace(unittest.TestCase):
+ def _mock_module(self, gather_subset=None):
+ return mock_module(gather_subset=gather_subset)
+
+ def _collectors(self, module,
+ all_collector_classes=None,
+ minimal_gather_subset=None):
+ return _collectors(module=module,
+ all_collector_classes=all_collector_classes,
+ minimal_gather_subset=minimal_gather_subset)
+
+ def test(self):
+ gather_subset = ['all']
+ mock_module = self._mock_module(gather_subset=gather_subset)
+ all_collector_classes = [EnvFactCollector]
+ collectors = self._collectors(mock_module,
+ all_collector_classes=all_collector_classes)
+
+ fact_collector = \
+ ansible_collector.AnsibleFactCollector(collectors=collectors,
+ namespace=ns)
+
+ res = fact_collector.collect(module=mock_module)
+ self.assertIsInstance(res, dict)
+ self.assertIn('env', res)
+ self.assertIn('gather_subset', res)
+ self.assertEqual(res['gather_subset'], ['all'])
+
+ def test1(self):
+ gather_subset = ['all']
+ mock_module = self._mock_module(gather_subset=gather_subset)
+ collectors = self._collectors(mock_module)
+
+ fact_collector = \
+ ansible_collector.AnsibleFactCollector(collectors=collectors,
+ namespace=ns)
+
+ res = fact_collector.collect(module=mock_module)
+ self.assertIsInstance(res, dict)
+ # just assert it's not almost empty
+ # with run_command and get_file_content mock, many facts are empty, like network
+ self.assertGreater(len(res), 20)
+
+ def test_empty_all_collector_classes(self):
+ mock_module = self._mock_module()
+ all_collector_classes = []
+
+ collectors = self._collectors(mock_module,
+ all_collector_classes=all_collector_classes)
+
+ fact_collector = \
+ ansible_collector.AnsibleFactCollector(collectors=collectors,
+ namespace=ns)
+
+ res = fact_collector.collect()
+ self.assertIsInstance(res, dict)
+ # just assert it's not almost empty
+ self.assertLess(len(res), 3)
+
+# def test_facts_class(self):
+# mock_module = self._mock_module()
+# Facts(mock_module)
+
+# def test_facts_class_load_on_init_false(self):
+# mock_module = self._mock_module()
+# Facts(mock_module, load_on_init=False)
+# # FIXME: assert something
+
+
+class TestCollectedFacts(unittest.TestCase):
+ gather_subset = ['all', '!facter', '!ohai']
+ min_fact_count = 30
+ max_fact_count = 1000
+
+ # TODO: add ansible_cmdline, ansible_*_pubkey* back when TempFactCollector goes away
+ expected_facts = ['date_time',
+ 'user_id', 'distribution',
+ 'gather_subset', 'module_setup',
+ 'env']
+ not_expected_facts = ['facter', 'ohai']
+
+ collected_facts = {}
+
+ def _mock_module(self, gather_subset=None):
+ return mock_module(gather_subset=self.gather_subset)
+
+ @patch('platform.system', return_value='Linux')
+ @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value='systemd')
+ def setUp(self, mock_gfc, mock_ps):
+ mock_module = self._mock_module()
+ collectors = self._collectors(mock_module)
+
+ fact_collector = \
+ ansible_collector.AnsibleFactCollector(collectors=collectors,
+ namespace=ns)
+ self.facts = fact_collector.collect(module=mock_module,
+ collected_facts=self.collected_facts)
+
+ def _collectors(self, module,
+ all_collector_classes=None,
+ minimal_gather_subset=None):
+ return _collectors(module=module,
+ all_collector_classes=all_collector_classes,
+ minimal_gather_subset=minimal_gather_subset)
+
+ def test_basics(self):
+ self._assert_basics(self.facts)
+
+ def test_expected_facts(self):
+ self._assert_expected_facts(self.facts)
+
+ def test_not_expected_facts(self):
+ self._assert_not_expected_facts(self.facts)
+
+ def _assert_basics(self, facts):
+ self.assertIsInstance(facts, dict)
+ # just assert it's not almost empty
+ self.assertGreaterEqual(len(facts), self.min_fact_count)
+ # and that is not huge number of keys
+ self.assertLess(len(facts), self.max_fact_count)
+
+ # everything starts with ansible_ namespace
+ def _assert_ansible_namespace(self, facts):
+
+ # FIXME: kluge for non-namespace fact
+ facts.pop('module_setup', None)
+ facts.pop('gather_subset', None)
+
+ for fact_key in facts:
+ self.assertTrue(fact_key.startswith('ansible_'),
+ 'The fact name "%s" does not startwith "ansible_"' % fact_key)
+
+ def _assert_expected_facts(self, facts):
+
+ facts_keys = sorted(facts.keys())
+ for expected_fact in self.expected_facts:
+ self.assertIn(expected_fact, facts_keys)
+
+ def _assert_not_expected_facts(self, facts):
+
+ facts_keys = sorted(facts.keys())
+ for not_expected_fact in self.not_expected_facts:
+ self.assertNotIn(not_expected_fact, facts_keys)
+
+
+class ProvidesOtherFactCollector(collector.BaseFactCollector):
+ name = 'provides_something'
+ _fact_ids = set(['needed_fact'])
+
+ def collect(self, module=None, collected_facts=None):
+ return {'needed_fact': 'THE_NEEDED_FACT_VALUE'}
+
+
+class RequiresOtherFactCollector(collector.BaseFactCollector):
+ name = 'requires_something'
+
+ def collect(self, module=None, collected_facts=None):
+ collected_facts = collected_facts or {}
+ fact_dict = {}
+ fact_dict['needed_fact'] = collected_facts['needed_fact']
+ fact_dict['compound_fact'] = "compound-%s" % collected_facts['needed_fact']
+ return fact_dict
+
+
+class ConCatFactCollector(collector.BaseFactCollector):
+ name = 'concat_collected'
+
+ def collect(self, module=None, collected_facts=None):
+ collected_facts = collected_facts or {}
+ fact_dict = {}
+ con_cat_list = []
+ for key, value in collected_facts.items():
+ con_cat_list.append(value)
+
+ fact_dict['concat_fact'] = '-'.join(con_cat_list)
+ return fact_dict
+
+
+class TestCollectorDepsWithFilter(unittest.TestCase):
+ gather_subset = ['all', '!facter', '!ohai']
+
+ def _mock_module(self, gather_subset=None, filter=None):
+ return mock_module(gather_subset=self.gather_subset,
+ filter=filter)
+
+ def setUp(self):
+ self.mock_module = self._mock_module()
+ self.collectors = self._collectors(mock_module)
+
+ def _collectors(self, module,
+ all_collector_classes=None,
+ minimal_gather_subset=None):
+ return [ProvidesOtherFactCollector(),
+ RequiresOtherFactCollector()]
+
+ def test_no_filter(self):
+ _mock_module = mock_module(gather_subset=['all', '!facter', '!ohai'])
+
+ facts_dict = self._collect(_mock_module)
+
+ expected = {'needed_fact': 'THE_NEEDED_FACT_VALUE',
+ 'compound_fact': 'compound-THE_NEEDED_FACT_VALUE'}
+
+ self.assertEqual(expected, facts_dict)
+
+ def test_with_filter_on_compound_fact(self):
+ _mock_module = mock_module(gather_subset=['all', '!facter', '!ohai'],
+ filter='compound_fact')
+
+ facts_dict = self._collect(_mock_module)
+
+ expected = {'compound_fact': 'compound-THE_NEEDED_FACT_VALUE'}
+
+ self.assertEqual(expected, facts_dict)
+
+ def test_with_filter_on_needed_fact(self):
+ _mock_module = mock_module(gather_subset=['all', '!facter', '!ohai'],
+ filter='needed_fact')
+
+ facts_dict = self._collect(_mock_module)
+
+ expected = {'needed_fact': 'THE_NEEDED_FACT_VALUE'}
+
+ self.assertEqual(expected, facts_dict)
+
+ def test_with_filter_on_compound_gather_compound(self):
+ _mock_module = mock_module(gather_subset=['!all', '!any', 'compound_fact'],
+ filter='compound_fact')
+
+ facts_dict = self._collect(_mock_module)
+
+ expected = {'compound_fact': 'compound-THE_NEEDED_FACT_VALUE'}
+
+ self.assertEqual(expected, facts_dict)
+
+ def test_with_filter_no_match(self):
+ _mock_module = mock_module(gather_subset=['all', '!facter', '!ohai'],
+ filter='ansible_this_doesnt_exist')
+
+ facts_dict = self._collect(_mock_module)
+
+ expected = {}
+ self.assertEqual(expected, facts_dict)
+
+ def test_concat_collector(self):
+ _mock_module = mock_module(gather_subset=['all', '!facter', '!ohai'])
+
+ _collectors = self._collectors(_mock_module)
+ _collectors.append(ConCatFactCollector())
+
+ fact_collector = \
+ ansible_collector.AnsibleFactCollector(collectors=_collectors,
+ namespace=ns,
+ filter_spec=_mock_module.params['filter'])
+
+ collected_facts = {}
+ facts_dict = fact_collector.collect(module=_mock_module,
+ collected_facts=collected_facts)
+ self.assertIn('concat_fact', facts_dict)
+ self.assertTrue('THE_NEEDED_FACT_VALUE' in facts_dict['concat_fact'])
+
+ def test_concat_collector_with_filter_on_concat(self):
+ _mock_module = mock_module(gather_subset=['all', '!facter', '!ohai'],
+ filter='concat_fact')
+
+ _collectors = self._collectors(_mock_module)
+ _collectors.append(ConCatFactCollector())
+
+ fact_collector = \
+ ansible_collector.AnsibleFactCollector(collectors=_collectors,
+ namespace=ns,
+ filter_spec=_mock_module.params['filter'])
+
+ collected_facts = {}
+ facts_dict = fact_collector.collect(module=_mock_module,
+ collected_facts=collected_facts)
+ self.assertIn('concat_fact', facts_dict)
+ self.assertTrue('THE_NEEDED_FACT_VALUE' in facts_dict['concat_fact'])
+ self.assertTrue('compound' in facts_dict['concat_fact'])
+
+ def _collect(self, _mock_module, collected_facts=None):
+ _collectors = self._collectors(_mock_module)
+
+ fact_collector = \
+ ansible_collector.AnsibleFactCollector(collectors=_collectors,
+ namespace=ns,
+ filter_spec=_mock_module.params['filter'])
+ facts_dict = fact_collector.collect(module=_mock_module,
+ collected_facts=collected_facts)
+ return facts_dict
+
+
+class ExceptionThrowingCollector(collector.BaseFactCollector):
+ def collect(self, module=None, collected_facts=None):
+ raise Exception('A collector failed')
+
+
+class TestExceptionCollectedFacts(TestCollectedFacts):
+
+ def _collectors(self, module,
+ all_collector_classes=None,
+ minimal_gather_subset=None):
+ collectors = _collectors(module=module,
+ all_collector_classes=all_collector_classes,
+ minimal_gather_subset=minimal_gather_subset)
+
+ c = [ExceptionThrowingCollector()] + collectors
+ return c
+
+
+class TestOnlyExceptionCollector(TestCollectedFacts):
+ expected_facts = []
+ min_fact_count = 0
+
+ def _collectors(self, module,
+ all_collector_classes=None,
+ minimal_gather_subset=None):
+ return [ExceptionThrowingCollector()]
+
+
+class TestMinimalCollectedFacts(TestCollectedFacts):
+ gather_subset = ['!all']
+ min_fact_count = 1
+ max_fact_count = 10
+ expected_facts = ['gather_subset',
+ 'module_setup']
+ not_expected_facts = ['lsb']
+
+
+class TestFacterCollectedFacts(TestCollectedFacts):
+ gather_subset = ['!all', 'facter']
+ min_fact_count = 1
+ max_fact_count = 10
+ expected_facts = ['gather_subset',
+ 'module_setup']
+ not_expected_facts = ['lsb']
+
+
+class TestOhaiCollectedFacts(TestCollectedFacts):
+ gather_subset = ['!all', 'ohai']
+ min_fact_count = 1
+ max_fact_count = 10
+ expected_facts = ['gather_subset',
+ 'module_setup']
+ not_expected_facts = ['lsb']
+
+
+class TestPkgMgrFacts(TestCollectedFacts):
+ gather_subset = ['pkg_mgr']
+ min_fact_count = 1
+ max_fact_count = 20
+ expected_facts = ['gather_subset',
+ 'module_setup',
+ 'pkg_mgr']
+ collected_facts = {
+ "ansible_distribution": "Fedora",
+ "ansible_distribution_major_version": "28",
+ "ansible_os_family": "RedHat"
+ }
+
+
+class TestPkgMgrOSTreeFacts(TestPkgMgrFacts):
+ @patch(
+ 'ansible.module_utils.facts.system.pkg_mgr.os.path.exists',
+ side_effect=lambda x: x == '/run/ostree-booted')
+ def _recollect_facts(self, distribution, version, mock_exists):
+ self.collected_facts['ansible_distribution'] = distribution
+ self.collected_facts['ansible_distribution_major_version'] = \
+ str(version)
+ # Recollect facts
+ self.setUp()
+ self.assertIn('pkg_mgr', self.facts)
+ self.assertEqual(self.facts['pkg_mgr'], 'atomic_container')
+
+ def test_is_rhel_edge_ostree(self):
+ self._recollect_facts('RedHat', 8)
+
+ def test_is_fedora_ostree(self):
+ self._recollect_facts('Fedora', 33)
+
+
+class TestOpenBSDPkgMgrFacts(TestPkgMgrFacts):
+ def test_is_openbsd_pkg(self):
+ self.assertIn('pkg_mgr', self.facts)
+ self.assertEqual(self.facts['pkg_mgr'], 'openbsd_pkg')
+
+ def setUp(self):
+ self.patcher = patch('platform.system')
+ mock_platform = self.patcher.start()
+ mock_platform.return_value = 'OpenBSD'
+
+ mock_module = self._mock_module()
+ collectors = self._collectors(mock_module)
+
+ fact_collector = \
+ ansible_collector.AnsibleFactCollector(collectors=collectors,
+ namespace=ns)
+ self.facts = fact_collector.collect(module=mock_module)
+
+ def tearDown(self):
+ self.patcher.stop()
diff --git a/test/units/module_utils/facts/test_collector.py b/test/units/module_utils/facts/test_collector.py
new file mode 100644
index 0000000..4fc4bc5
--- /dev/null
+++ b/test/units/module_utils/facts/test_collector.py
@@ -0,0 +1,563 @@
+# This file is part of Ansible
+# -*- coding: utf-8 -*-
+#
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from collections import defaultdict
+import pprint
+
+# for testing
+from units.compat import unittest
+
+from ansible.module_utils.facts import collector
+
+from ansible.module_utils.facts import default_collectors
+
+
+class TestFindCollectorsForPlatform(unittest.TestCase):
+ def test(self):
+ compat_platforms = [{'system': 'Generic'}]
+ res = collector.find_collectors_for_platform(default_collectors.collectors,
+ compat_platforms)
+ for coll_class in res:
+ self.assertIn(coll_class._platform, ('Generic'))
+
+ def test_linux(self):
+ compat_platforms = [{'system': 'Linux'}]
+ res = collector.find_collectors_for_platform(default_collectors.collectors,
+ compat_platforms)
+ for coll_class in res:
+ self.assertIn(coll_class._platform, ('Linux'))
+
+ def test_linux_or_generic(self):
+ compat_platforms = [{'system': 'Generic'}, {'system': 'Linux'}]
+ res = collector.find_collectors_for_platform(default_collectors.collectors,
+ compat_platforms)
+ for coll_class in res:
+ self.assertIn(coll_class._platform, ('Generic', 'Linux'))
+
+
+class TestSelectCollectorNames(unittest.TestCase):
+
+ def _assert_equal_detail(self, obj1, obj2, msg=None):
+ msg = 'objects are not equal\n%s\n\n!=\n\n%s' % (pprint.pformat(obj1), pprint.pformat(obj2))
+ return self.assertEqual(obj1, obj2, msg)
+
+ def test(self):
+ collector_names = ['distribution', 'all_ipv4_addresses',
+ 'local', 'pkg_mgr']
+ all_fact_subsets = self._all_fact_subsets()
+ res = collector.select_collector_classes(collector_names,
+ all_fact_subsets)
+
+ expected = [default_collectors.DistributionFactCollector,
+ default_collectors.PkgMgrFactCollector]
+
+ self._assert_equal_detail(res, expected)
+
+ def test_default_collectors(self):
+ platform_info = {'system': 'Generic'}
+ compat_platforms = [platform_info]
+ collectors_for_platform = collector.find_collectors_for_platform(default_collectors.collectors,
+ compat_platforms)
+
+ all_fact_subsets, aliases_map = collector.build_fact_id_to_collector_map(collectors_for_platform)
+
+ all_valid_subsets = frozenset(all_fact_subsets.keys())
+ collector_names = collector.get_collector_names(valid_subsets=all_valid_subsets,
+ aliases_map=aliases_map,
+ platform_info=platform_info)
+ complete_collector_names = collector._solve_deps(collector_names, all_fact_subsets)
+
+ dep_map = collector.build_dep_data(complete_collector_names, all_fact_subsets)
+
+ ordered_deps = collector.tsort(dep_map)
+ ordered_collector_names = [x[0] for x in ordered_deps]
+
+ res = collector.select_collector_classes(ordered_collector_names,
+ all_fact_subsets)
+
+ self.assertTrue(res.index(default_collectors.ServiceMgrFactCollector) >
+ res.index(default_collectors.DistributionFactCollector),
+ res)
+ self.assertTrue(res.index(default_collectors.ServiceMgrFactCollector) >
+ res.index(default_collectors.PlatformFactCollector),
+ res)
+
+ def _all_fact_subsets(self, data=None):
+ all_fact_subsets = defaultdict(list)
+ _data = {'pkg_mgr': [default_collectors.PkgMgrFactCollector],
+ 'distribution': [default_collectors.DistributionFactCollector],
+ 'network': [default_collectors.LinuxNetworkCollector]}
+ data = data or _data
+ for key, value in data.items():
+ all_fact_subsets[key] = value
+ return all_fact_subsets
+
+
+class TestGetCollectorNames(unittest.TestCase):
+ def test_none(self):
+ res = collector.get_collector_names()
+ self.assertIsInstance(res, set)
+ self.assertEqual(res, set([]))
+
+ def test_empty_sets(self):
+ res = collector.get_collector_names(valid_subsets=frozenset([]),
+ minimal_gather_subset=frozenset([]),
+ gather_subset=[])
+ self.assertIsInstance(res, set)
+ self.assertEqual(res, set([]))
+
+ def test_empty_valid_and_min_with_all_gather_subset(self):
+ res = collector.get_collector_names(valid_subsets=frozenset([]),
+ minimal_gather_subset=frozenset([]),
+ gather_subset=['all'])
+ self.assertIsInstance(res, set)
+ self.assertEqual(res, set([]))
+
+ def test_one_valid_with_all_gather_subset(self):
+ valid_subsets = frozenset(['my_fact'])
+ res = collector.get_collector_names(valid_subsets=valid_subsets,
+ minimal_gather_subset=frozenset([]),
+ gather_subset=['all'])
+ self.assertIsInstance(res, set)
+ self.assertEqual(res, set(['my_fact']))
+
+ def _compare_res(self, gather_subset1, gather_subset2,
+ valid_subsets=None, min_subset=None):
+
+ valid_subsets = valid_subsets or frozenset()
+ minimal_gather_subset = min_subset or frozenset()
+
+ res1 = collector.get_collector_names(valid_subsets=valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=gather_subset1)
+
+ res2 = collector.get_collector_names(valid_subsets=valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=gather_subset2)
+
+ return res1, res2
+
+ def test_not_all_other_order(self):
+ valid_subsets = frozenset(['min_fact', 'something_else', 'whatever'])
+ minimal_gather_subset = frozenset(['min_fact'])
+
+ res1, res2 = self._compare_res(['!all', 'whatever'],
+ ['whatever', '!all'],
+ valid_subsets=valid_subsets,
+ min_subset=minimal_gather_subset)
+ self.assertEqual(res1, res2)
+ self.assertEqual(res1, set(['min_fact', 'whatever']))
+
+ def test_not_all_other_order_min(self):
+ valid_subsets = frozenset(['min_fact', 'something_else', 'whatever'])
+ minimal_gather_subset = frozenset(['min_fact'])
+
+ res1, res2 = self._compare_res(['!min_fact', 'whatever'],
+ ['whatever', '!min_fact'],
+ valid_subsets=valid_subsets,
+ min_subset=minimal_gather_subset)
+ self.assertEqual(res1, res2)
+ self.assertEqual(res1, set(['whatever']))
+
+ def test_one_minimal_with_all_gather_subset(self):
+ my_fact = 'my_fact'
+ valid_subsets = frozenset([my_fact])
+ minimal_gather_subset = valid_subsets
+
+ res = collector.get_collector_names(valid_subsets=valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=['all'])
+ self.assertIsInstance(res, set)
+ self.assertEqual(res, set(['my_fact']))
+
+ def test_with_all_gather_subset(self):
+ valid_subsets = frozenset(['my_fact', 'something_else', 'whatever'])
+ minimal_gather_subset = frozenset(['my_fact'])
+
+ # even with '!all', the minimal_gather_subset should be returned
+ res = collector.get_collector_names(valid_subsets=valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=['all'])
+ self.assertIsInstance(res, set)
+ self.assertEqual(res, set(['my_fact', 'something_else', 'whatever']))
+
+ def test_one_minimal_with_not_all_gather_subset(self):
+ valid_subsets = frozenset(['my_fact', 'something_else', 'whatever'])
+ minimal_gather_subset = frozenset(['my_fact'])
+
+ # even with '!all', the minimal_gather_subset should be returned
+ res = collector.get_collector_names(valid_subsets=valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=['!all'])
+ self.assertIsInstance(res, set)
+ self.assertEqual(res, set(['my_fact']))
+
+ def test_gather_subset_excludes(self):
+ valid_subsets = frozenset(['my_fact', 'something_else', 'whatever'])
+ minimal_gather_subset = frozenset(['min_fact', 'min_another'])
+
+ # even with '!all', the minimal_gather_subset should be returned
+ res = collector.get_collector_names(valid_subsets=valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ # gather_subset=set(['all', '!my_fact', '!whatever']))
+ # gather_subset=['all', '!my_fact', '!whatever'])
+ gather_subset=['!min_fact', '!whatever'])
+ self.assertIsInstance(res, set)
+ # min_another is in minimal_gather_subset, so always returned
+ self.assertEqual(res, set(['min_another']))
+
+ def test_gather_subset_excludes_ordering(self):
+ valid_subsets = frozenset(['my_fact', 'something_else', 'whatever'])
+ minimal_gather_subset = frozenset(['my_fact'])
+
+ res = collector.get_collector_names(valid_subsets=valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=['!all', 'whatever'])
+ self.assertIsInstance(res, set)
+ # excludes are higher precedence than includes, so !all excludes everything
+ # and then minimal_gather_subset is added. so '!all', 'other' == '!all'
+ self.assertEqual(res, set(['my_fact', 'whatever']))
+
+ def test_gather_subset_excludes_min(self):
+ valid_subsets = frozenset(['min_fact', 'something_else', 'whatever'])
+ minimal_gather_subset = frozenset(['min_fact'])
+
+ res = collector.get_collector_names(valid_subsets=valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=['whatever', '!min'])
+ self.assertIsInstance(res, set)
+ # excludes are higher precedence than includes, so !all excludes everything
+ # and then minimal_gather_subset is added. so '!all', 'other' == '!all'
+ self.assertEqual(res, set(['whatever']))
+
+ def test_gather_subset_excludes_min_and_all(self):
+ valid_subsets = frozenset(['min_fact', 'something_else', 'whatever'])
+ minimal_gather_subset = frozenset(['min_fact'])
+
+ res = collector.get_collector_names(valid_subsets=valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=['whatever', '!all', '!min'])
+ self.assertIsInstance(res, set)
+ # excludes are higher precedence than includes, so !all excludes everything
+ # and then minimal_gather_subset is added. so '!all', 'other' == '!all'
+ self.assertEqual(res, set(['whatever']))
+
+ def test_invaid_gather_subset(self):
+ valid_subsets = frozenset(['my_fact', 'something_else'])
+ minimal_gather_subset = frozenset(['my_fact'])
+
+ self.assertRaisesRegex(TypeError,
+ r'Bad subset .* given to Ansible.*allowed\:.*all,.*my_fact.*',
+ collector.get_collector_names,
+ valid_subsets=valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=['my_fact', 'not_a_valid_gather_subset'])
+
+
+class TestFindUnresolvedRequires(unittest.TestCase):
+ def test(self):
+ names = ['network', 'virtual', 'env']
+ all_fact_subsets = {'env': [default_collectors.EnvFactCollector],
+ 'network': [default_collectors.LinuxNetworkCollector],
+ 'virtual': [default_collectors.LinuxVirtualCollector]}
+ res = collector.find_unresolved_requires(names, all_fact_subsets)
+ # pprint.pprint(res)
+
+ self.assertIsInstance(res, set)
+ self.assertEqual(res, set(['platform', 'distribution']))
+
+ def test_resolved(self):
+ names = ['network', 'virtual', 'env', 'platform', 'distribution']
+ all_fact_subsets = {'env': [default_collectors.EnvFactCollector],
+ 'network': [default_collectors.LinuxNetworkCollector],
+ 'distribution': [default_collectors.DistributionFactCollector],
+ 'platform': [default_collectors.PlatformFactCollector],
+ 'virtual': [default_collectors.LinuxVirtualCollector]}
+ res = collector.find_unresolved_requires(names, all_fact_subsets)
+ # pprint.pprint(res)
+
+ self.assertIsInstance(res, set)
+ self.assertEqual(res, set())
+
+
+class TestBuildDepData(unittest.TestCase):
+ def test(self):
+ names = ['network', 'virtual', 'env']
+ all_fact_subsets = {'env': [default_collectors.EnvFactCollector],
+ 'network': [default_collectors.LinuxNetworkCollector],
+ 'virtual': [default_collectors.LinuxVirtualCollector]}
+ res = collector.build_dep_data(names, all_fact_subsets)
+
+ # pprint.pprint(dict(res))
+ self.assertIsInstance(res, defaultdict)
+ self.assertEqual(dict(res),
+ {'network': set(['platform', 'distribution']),
+ 'virtual': set(),
+ 'env': set()})
+
+
+class TestSolveDeps(unittest.TestCase):
+ def test_no_solution(self):
+ unresolved = set(['required_thing1', 'required_thing2'])
+ all_fact_subsets = {'env': [default_collectors.EnvFactCollector],
+ 'network': [default_collectors.LinuxNetworkCollector],
+ 'virtual': [default_collectors.LinuxVirtualCollector]}
+
+ self.assertRaises(collector.CollectorNotFoundError,
+ collector._solve_deps,
+ unresolved,
+ all_fact_subsets)
+
+ def test(self):
+ unresolved = set(['env', 'network'])
+ all_fact_subsets = {'env': [default_collectors.EnvFactCollector],
+ 'network': [default_collectors.LinuxNetworkCollector],
+ 'virtual': [default_collectors.LinuxVirtualCollector],
+ 'platform': [default_collectors.PlatformFactCollector],
+ 'distribution': [default_collectors.DistributionFactCollector]}
+ res = collector.resolve_requires(unresolved, all_fact_subsets)
+
+ res = collector._solve_deps(unresolved, all_fact_subsets)
+
+ self.assertIsInstance(res, set)
+ for goal in unresolved:
+ self.assertIn(goal, res)
+
+
+class TestResolveRequires(unittest.TestCase):
+ def test_no_resolution(self):
+ unresolved = ['required_thing1', 'required_thing2']
+ all_fact_subsets = {'env': [default_collectors.EnvFactCollector],
+ 'network': [default_collectors.LinuxNetworkCollector],
+ 'virtual': [default_collectors.LinuxVirtualCollector]}
+ self.assertRaisesRegex(collector.UnresolvedFactDep,
+ 'unresolved fact dep.*required_thing2',
+ collector.resolve_requires,
+ unresolved, all_fact_subsets)
+
+ def test(self):
+ unresolved = ['env', 'network']
+ all_fact_subsets = {'env': [default_collectors.EnvFactCollector],
+ 'network': [default_collectors.LinuxNetworkCollector],
+ 'virtual': [default_collectors.LinuxVirtualCollector]}
+ res = collector.resolve_requires(unresolved, all_fact_subsets)
+ for goal in unresolved:
+ self.assertIn(goal, res)
+
+ def test_exception(self):
+ unresolved = ['required_thing1']
+ all_fact_subsets = {}
+ try:
+ collector.resolve_requires(unresolved, all_fact_subsets)
+ except collector.UnresolvedFactDep as exc:
+ self.assertIn(unresolved[0], '%s' % exc)
+
+
+class TestTsort(unittest.TestCase):
+ def test(self):
+ dep_map = {'network': set(['distribution', 'platform']),
+ 'virtual': set(),
+ 'platform': set(['what_platform_wants']),
+ 'what_platform_wants': set(),
+ 'network_stuff': set(['network'])}
+
+ res = collector.tsort(dep_map)
+ # pprint.pprint(res)
+
+ self.assertIsInstance(res, list)
+ names = [x[0] for x in res]
+ self.assertTrue(names.index('network_stuff') > names.index('network'))
+ self.assertTrue(names.index('platform') > names.index('what_platform_wants'))
+ self.assertTrue(names.index('network') > names.index('platform'))
+
+ def test_cycles(self):
+ dep_map = {'leaf1': set(),
+ 'leaf2': set(),
+ 'node1': set(['node2']),
+ 'node2': set(['node3']),
+ 'node3': set(['node1'])}
+
+ self.assertRaises(collector.CycleFoundInFactDeps,
+ collector.tsort,
+ dep_map)
+
+ def test_just_nodes(self):
+ dep_map = {'leaf1': set(),
+ 'leaf4': set(),
+ 'leaf3': set(),
+ 'leaf2': set()}
+
+ res = collector.tsort(dep_map)
+ self.assertIsInstance(res, list)
+ names = [x[0] for x in res]
+ # not a lot to assert here, any order of the
+ # results is valid
+ self.assertEqual(set(names), set(dep_map.keys()))
+
+ def test_self_deps(self):
+ dep_map = {'node1': set(['node1']),
+ 'node2': set(['node2'])}
+ self.assertRaises(collector.CycleFoundInFactDeps,
+ collector.tsort,
+ dep_map)
+
+ def test_unsolvable(self):
+ dep_map = {'leaf1': set(),
+ 'node2': set(['leaf2'])}
+
+ res = collector.tsort(dep_map)
+ self.assertIsInstance(res, list)
+ names = [x[0] for x in res]
+ self.assertEqual(set(names), set(dep_map.keys()))
+
+ def test_chain(self):
+ dep_map = {'leaf1': set(['leaf2']),
+ 'leaf2': set(['leaf3']),
+ 'leaf3': set(['leaf4']),
+ 'leaf4': set(),
+ 'leaf5': set(['leaf1'])}
+ res = collector.tsort(dep_map)
+ self.assertIsInstance(res, list)
+ names = [x[0] for x in res]
+ self.assertEqual(set(names), set(dep_map.keys()))
+
+ def test_multi_pass(self):
+ dep_map = {'leaf1': set(),
+ 'leaf2': set(['leaf3', 'leaf1', 'leaf4', 'leaf5']),
+ 'leaf3': set(['leaf4', 'leaf1']),
+ 'leaf4': set(['leaf1']),
+ 'leaf5': set(['leaf1'])}
+ res = collector.tsort(dep_map)
+ self.assertIsInstance(res, list)
+ names = [x[0] for x in res]
+ self.assertEqual(set(names), set(dep_map.keys()))
+ self.assertTrue(names.index('leaf1') < names.index('leaf2'))
+ for leaf in ('leaf2', 'leaf3', 'leaf4', 'leaf5'):
+ self.assertTrue(names.index('leaf1') < names.index(leaf))
+
+
+class TestCollectorClassesFromGatherSubset(unittest.TestCase):
+ maxDiff = None
+
+ def _classes(self,
+ all_collector_classes=None,
+ valid_subsets=None,
+ minimal_gather_subset=None,
+ gather_subset=None,
+ gather_timeout=None,
+ platform_info=None):
+ platform_info = platform_info or {'system': 'Linux'}
+ return collector.collector_classes_from_gather_subset(all_collector_classes=all_collector_classes,
+ valid_subsets=valid_subsets,
+ minimal_gather_subset=minimal_gather_subset,
+ gather_subset=gather_subset,
+ gather_timeout=gather_timeout,
+ platform_info=platform_info)
+
+ def test_no_args(self):
+ res = self._classes()
+ self.assertIsInstance(res, list)
+ self.assertEqual(res, [])
+
+ def test_not_all(self):
+ res = self._classes(all_collector_classes=default_collectors.collectors,
+ gather_subset=['!all'])
+ self.assertIsInstance(res, list)
+ self.assertEqual(res, [])
+
+ def test_all(self):
+ res = self._classes(all_collector_classes=default_collectors.collectors,
+ gather_subset=['all'])
+ self.assertIsInstance(res, list)
+
+ def test_hardware(self):
+ res = self._classes(all_collector_classes=default_collectors.collectors,
+ gather_subset=['hardware'])
+ self.assertIsInstance(res, list)
+ self.assertIn(default_collectors.PlatformFactCollector, res)
+ self.assertIn(default_collectors.LinuxHardwareCollector, res)
+
+ self.assertTrue(res.index(default_collectors.LinuxHardwareCollector) >
+ res.index(default_collectors.PlatformFactCollector))
+
+ def test_network(self):
+ res = self._classes(all_collector_classes=default_collectors.collectors,
+ gather_subset=['network'])
+ self.assertIsInstance(res, list)
+ self.assertIn(default_collectors.DistributionFactCollector, res)
+ self.assertIn(default_collectors.PlatformFactCollector, res)
+ self.assertIn(default_collectors.LinuxNetworkCollector, res)
+
+ self.assertTrue(res.index(default_collectors.LinuxNetworkCollector) >
+ res.index(default_collectors.PlatformFactCollector))
+ self.assertTrue(res.index(default_collectors.LinuxNetworkCollector) >
+ res.index(default_collectors.DistributionFactCollector))
+
+ # self.assertEqual(set(res, [default_collectors.DistributionFactCollector,
+ # default_collectors.PlatformFactCollector,
+ # default_collectors.LinuxNetworkCollector])
+
+ def test_env(self):
+ res = self._classes(all_collector_classes=default_collectors.collectors,
+ gather_subset=['env'])
+ self.assertIsInstance(res, list)
+ self.assertEqual(res, [default_collectors.EnvFactCollector])
+
+ def test_facter(self):
+ res = self._classes(all_collector_classes=default_collectors.collectors,
+ gather_subset=set(['env', 'facter']))
+ self.assertIsInstance(res, list)
+ self.assertEqual(set(res),
+ set([default_collectors.EnvFactCollector,
+ default_collectors.FacterFactCollector]))
+
+ def test_facter_ohai(self):
+ res = self._classes(all_collector_classes=default_collectors.collectors,
+ gather_subset=set(['env', 'facter', 'ohai']))
+ self.assertIsInstance(res, list)
+ self.assertEqual(set(res),
+ set([default_collectors.EnvFactCollector,
+ default_collectors.FacterFactCollector,
+ default_collectors.OhaiFactCollector]))
+
+ def test_just_facter(self):
+ res = self._classes(all_collector_classes=default_collectors.collectors,
+ gather_subset=set(['facter']))
+ self.assertIsInstance(res, list)
+ self.assertEqual(set(res),
+ set([default_collectors.FacterFactCollector]))
+
+ def test_collector_specified_multiple_times(self):
+ res = self._classes(all_collector_classes=default_collectors.collectors,
+ gather_subset=['platform', 'all', 'machine'])
+ self.assertIsInstance(res, list)
+ self.assertIn(default_collectors.PlatformFactCollector,
+ res)
+
+ def test_unknown_collector(self):
+ # something claims 'unknown_collector' is a valid gather_subset, but there is
+ # no FactCollector mapped to 'unknown_collector'
+ self.assertRaisesRegex(TypeError,
+ r'Bad subset.*unknown_collector.*given to Ansible.*allowed\:.*all,.*env.*',
+ self._classes,
+ all_collector_classes=default_collectors.collectors,
+ gather_subset=['env', 'unknown_collector'])
diff --git a/test/units/module_utils/facts/test_collectors.py b/test/units/module_utils/facts/test_collectors.py
new file mode 100644
index 0000000..c480602
--- /dev/null
+++ b/test/units/module_utils/facts/test_collectors.py
@@ -0,0 +1,510 @@
+# unit tests for ansible fact collectors
+# -*- coding: utf-8 -*-
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from units.compat.mock import Mock, patch
+
+from . base import BaseFactsTest
+
+from ansible.module_utils.facts import collector
+
+from ansible.module_utils.facts.system.apparmor import ApparmorFactCollector
+from ansible.module_utils.facts.system.caps import SystemCapabilitiesFactCollector
+from ansible.module_utils.facts.system.cmdline import CmdLineFactCollector
+from ansible.module_utils.facts.system.distribution import DistributionFactCollector
+from ansible.module_utils.facts.system.dns import DnsFactCollector
+from ansible.module_utils.facts.system.env import EnvFactCollector
+from ansible.module_utils.facts.system.fips import FipsFactCollector
+from ansible.module_utils.facts.system.pkg_mgr import PkgMgrFactCollector, OpenBSDPkgMgrFactCollector
+from ansible.module_utils.facts.system.platform import PlatformFactCollector
+from ansible.module_utils.facts.system.python import PythonFactCollector
+from ansible.module_utils.facts.system.selinux import SelinuxFactCollector
+from ansible.module_utils.facts.system.service_mgr import ServiceMgrFactCollector
+from ansible.module_utils.facts.system.ssh_pub_keys import SshPubKeyFactCollector
+from ansible.module_utils.facts.system.user import UserFactCollector
+
+from ansible.module_utils.facts.virtual.base import VirtualCollector
+from ansible.module_utils.facts.network.base import NetworkCollector
+from ansible.module_utils.facts.hardware.base import HardwareCollector
+
+
+class CollectorException(Exception):
+ pass
+
+
+class ExceptionThrowingCollector(collector.BaseFactCollector):
+ name = 'exc_throwing'
+
+ def __init__(self, collectors=None, namespace=None, exception=None):
+ super(ExceptionThrowingCollector, self).__init__(collectors, namespace)
+ self._exception = exception or CollectorException('collection failed')
+
+ def collect(self, module=None, collected_facts=None):
+ raise self._exception
+
+
+class TestExceptionThrowingCollector(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['exc_throwing']
+ valid_subsets = ['exc_throwing']
+ collector_class = ExceptionThrowingCollector
+
+ def test_collect(self):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ self.assertRaises(CollectorException,
+ fact_collector.collect,
+ module=module,
+ collected_facts=self.collected_facts)
+
+ def test_collect_with_namespace(self):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ self.assertRaises(CollectorException,
+ fact_collector.collect_with_namespace,
+ module=module,
+ collected_facts=self.collected_facts)
+
+
+class TestApparmorFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'apparmor']
+ valid_subsets = ['apparmor']
+ fact_namespace = 'ansible_apparmor'
+ collector_class = ApparmorFactCollector
+
+ def test_collect(self):
+ facts_dict = super(TestApparmorFacts, self).test_collect()
+ self.assertIn('status', facts_dict['apparmor'])
+
+
+class TestCapsFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'caps']
+ valid_subsets = ['caps']
+ fact_namespace = 'ansible_system_capabilities'
+ collector_class = SystemCapabilitiesFactCollector
+
+ def _mock_module(self):
+ mock_module = Mock()
+ mock_module.params = {'gather_subset': self.gather_subset,
+ 'gather_timeout': 10,
+ 'filter': '*'}
+ mock_module.get_bin_path = Mock(return_value='/usr/sbin/capsh')
+ mock_module.run_command = Mock(return_value=(0, 'Current: =ep', ''))
+ return mock_module
+
+
+class TestCmdLineFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'cmdline']
+ valid_subsets = ['cmdline']
+ fact_namespace = 'ansible_cmdline'
+ collector_class = CmdLineFactCollector
+
+ def test_parse_proc_cmdline_uefi(self):
+ uefi_cmdline = r'initrd=\70ef65e1a04a47aea04f7b5145ea3537\4.10.0-19-generic\initrd root=UUID=50973b75-4a66-4bf0-9764-2b7614489e64 ro quiet'
+ expected = {'initrd': r'\70ef65e1a04a47aea04f7b5145ea3537\4.10.0-19-generic\initrd',
+ 'root': 'UUID=50973b75-4a66-4bf0-9764-2b7614489e64',
+ 'quiet': True,
+ 'ro': True}
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector._parse_proc_cmdline(uefi_cmdline)
+
+ self.assertDictEqual(facts_dict, expected)
+
+ def test_parse_proc_cmdline_fedora(self):
+ cmdline_fedora = r'BOOT_IMAGE=/vmlinuz-4.10.16-200.fc25.x86_64 root=/dev/mapper/fedora-root ro rd.lvm.lv=fedora/root rd.luks.uuid=luks-c80b7537-358b-4a07-b88c-c59ef187479b rd.lvm.lv=fedora/swap rhgb quiet LANG=en_US.UTF-8' # noqa
+
+ expected = {'BOOT_IMAGE': '/vmlinuz-4.10.16-200.fc25.x86_64',
+ 'LANG': 'en_US.UTF-8',
+ 'quiet': True,
+ 'rd.luks.uuid': 'luks-c80b7537-358b-4a07-b88c-c59ef187479b',
+ 'rd.lvm.lv': 'fedora/swap',
+ 'rhgb': True,
+ 'ro': True,
+ 'root': '/dev/mapper/fedora-root'}
+
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector._parse_proc_cmdline(cmdline_fedora)
+
+ self.assertDictEqual(facts_dict, expected)
+
+ def test_parse_proc_cmdline_dup_console(self):
+ example = r'BOOT_IMAGE=/boot/vmlinuz-4.4.0-72-generic root=UUID=e12e46d9-06c9-4a64-a7b3-60e24b062d90 ro console=tty1 console=ttyS0'
+
+ # FIXME: Two 'console' keywords? Using a dict for the fact value here loses info. Currently the 'last' one wins
+ expected = {'BOOT_IMAGE': '/boot/vmlinuz-4.4.0-72-generic',
+ 'root': 'UUID=e12e46d9-06c9-4a64-a7b3-60e24b062d90',
+ 'ro': True,
+ 'console': 'ttyS0'}
+
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector._parse_proc_cmdline(example)
+
+ # TODO: fails because we lose a 'console'
+ self.assertDictEqual(facts_dict, expected)
+
+
+class TestDistributionFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'distribution']
+ valid_subsets = ['distribution']
+ fact_namespace = 'ansible_distribution'
+ collector_class = DistributionFactCollector
+
+
+class TestDnsFacts(BaseFactsTest):
+
+ __test__ = True
+ gather_subset = ['!all', 'dns']
+ valid_subsets = ['dns']
+ fact_namespace = 'ansible_dns'
+ collector_class = DnsFactCollector
+
+
+class TestEnvFacts(BaseFactsTest):
+
+ __test__ = True
+ gather_subset = ['!all', 'env']
+ valid_subsets = ['env']
+ fact_namespace = 'ansible_env'
+ collector_class = EnvFactCollector
+
+ def test_collect(self):
+ facts_dict = super(TestEnvFacts, self).test_collect()
+ self.assertIn('HOME', facts_dict['env'])
+
+
+class TestFipsFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'fips']
+ valid_subsets = ['fips']
+ fact_namespace = 'ansible_fips'
+ collector_class = FipsFactCollector
+
+
+class TestHardwareCollector(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'hardware']
+ valid_subsets = ['hardware']
+ fact_namespace = 'ansible_hardware'
+ collector_class = HardwareCollector
+ collected_facts = {'ansible_architecture': 'x86_64'}
+
+
+class TestNetworkCollector(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'network']
+ valid_subsets = ['network']
+ fact_namespace = 'ansible_network'
+ collector_class = NetworkCollector
+
+
+class TestPkgMgrFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'pkg_mgr']
+ valid_subsets = ['pkg_mgr']
+ fact_namespace = 'ansible_pkgmgr'
+ collector_class = PkgMgrFactCollector
+ collected_facts = {
+ "ansible_distribution": "Fedora",
+ "ansible_distribution_major_version": "28",
+ "ansible_os_family": "RedHat"
+ }
+
+ def test_collect(self):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertIn('pkg_mgr', facts_dict)
+
+
+class TestMacOSXPkgMgrFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'pkg_mgr']
+ valid_subsets = ['pkg_mgr']
+ fact_namespace = 'ansible_pkgmgr'
+ collector_class = PkgMgrFactCollector
+ collected_facts = {
+ "ansible_distribution": "MacOSX",
+ "ansible_distribution_major_version": "11",
+ "ansible_os_family": "Darwin"
+ }
+
+ @patch('ansible.module_utils.facts.system.pkg_mgr.os.path.exists', side_effect=lambda x: x == '/opt/homebrew/bin/brew')
+ def test_collect_opt_homebrew(self, p_exists):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertIn('pkg_mgr', facts_dict)
+ self.assertEqual(facts_dict['pkg_mgr'], 'homebrew')
+
+ @patch('ansible.module_utils.facts.system.pkg_mgr.os.path.exists', side_effect=lambda x: x == '/usr/local/bin/brew')
+ def test_collect_usr_homebrew(self, p_exists):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertIn('pkg_mgr', facts_dict)
+ self.assertEqual(facts_dict['pkg_mgr'], 'homebrew')
+
+ @patch('ansible.module_utils.facts.system.pkg_mgr.os.path.exists', side_effect=lambda x: x == '/opt/local/bin/port')
+ def test_collect_macports(self, p_exists):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertIn('pkg_mgr', facts_dict)
+ self.assertEqual(facts_dict['pkg_mgr'], 'macports')
+
+
+def _sanitize_os_path_apt_get(path):
+ if path == '/usr/bin/apt-get':
+ return True
+ else:
+ return False
+
+
+class TestPkgMgrFactsAptFedora(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'pkg_mgr']
+ valid_subsets = ['pkg_mgr']
+ fact_namespace = 'ansible_pkgmgr'
+ collector_class = PkgMgrFactCollector
+ collected_facts = {
+ "ansible_distribution": "Fedora",
+ "ansible_distribution_major_version": "28",
+ "ansible_os_family": "RedHat",
+ "ansible_pkg_mgr": "apt"
+ }
+
+ @patch('ansible.module_utils.facts.system.pkg_mgr.os.path.exists', side_effect=_sanitize_os_path_apt_get)
+ def test_collect(self, mock_os_path_exists):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertIn('pkg_mgr', facts_dict)
+
+
+class TestOpenBSDPkgMgrFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'pkg_mgr']
+ valid_subsets = ['pkg_mgr']
+ fact_namespace = 'ansible_pkgmgr'
+ collector_class = OpenBSDPkgMgrFactCollector
+
+ def test_collect(self):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module, collected_facts=self.collected_facts)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertIn('pkg_mgr', facts_dict)
+ self.assertEqual(facts_dict['pkg_mgr'], 'openbsd_pkg')
+
+
+class TestPlatformFactCollector(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'platform']
+ valid_subsets = ['platform']
+ fact_namespace = 'ansible_platform'
+ collector_class = PlatformFactCollector
+
+
+class TestPythonFactCollector(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'python']
+ valid_subsets = ['python']
+ fact_namespace = 'ansible_python'
+ collector_class = PythonFactCollector
+
+
+class TestSelinuxFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'selinux']
+ valid_subsets = ['selinux']
+ fact_namespace = 'ansible_selinux'
+ collector_class = SelinuxFactCollector
+
+ def test_no_selinux(self):
+ with patch('ansible.module_utils.facts.system.selinux.HAVE_SELINUX', False):
+ module = self._mock_module()
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict['selinux']['status'], 'Missing selinux Python library')
+ return facts_dict
+
+
+class TestServiceMgrFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'service_mgr']
+ valid_subsets = ['service_mgr']
+ fact_namespace = 'ansible_service_mgr'
+ collector_class = ServiceMgrFactCollector
+
+ # TODO: dedupe some of this test code
+
+ @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
+ @patch('ansible.module_utils.facts.system.service_mgr.ServiceMgrFactCollector.is_systemd_managed', return_value=False)
+ @patch('ansible.module_utils.facts.system.service_mgr.ServiceMgrFactCollector.is_systemd_managed_offline', return_value=False)
+ @patch('ansible.module_utils.facts.system.service_mgr.os.path.exists', return_value=False)
+ @pytest.mark.skip(reason='faulty test')
+ def test_service_mgr_runit_one(self, mock_gfc, mock_ism, mock_ismo, mock_ope):
+ # no /proc/1/comm, ps returns non-0
+ # should fallback to 'service'
+ module = self._mock_module()
+ module.run_command = Mock(return_value=(1, '', 'wat'))
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict['service_mgr'], 'service')
+
+ @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
+ def test_no_proc1_ps_random_init(self, mock_gfc):
+ # no /proc/1/comm, ps returns '/sbin/sys11' which we dont know
+ # should end up return 'sys11'
+ module = self._mock_module()
+ module.run_command = Mock(return_value=(0, '/sbin/sys11', ''))
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict['service_mgr'], 'sys11')
+
+ @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
+ @patch('ansible.module_utils.facts.system.service_mgr.ServiceMgrFactCollector.is_systemd_managed', return_value=False)
+ @patch('ansible.module_utils.facts.system.service_mgr.ServiceMgrFactCollector.is_systemd_managed_offline', return_value=False)
+ @patch('ansible.module_utils.facts.system.service_mgr.os.path.exists', return_value=False)
+ @pytest.mark.skip(reason='faulty test')
+ def test_service_mgr_runit_two(self, mock_gfc, mock_ism, mock_ismo, mock_ope):
+ # no /proc/1/comm, ps fails, distro and system are clowncar
+ # should end up return 'sys11'
+ module = self._mock_module()
+ module.run_command = Mock(return_value=(1, '', ''))
+ collected_facts = {'distribution': 'clowncar',
+ 'system': 'ClownCarOS'}
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module,
+ collected_facts=collected_facts)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict['service_mgr'], 'service')
+
+ @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value='runit-init')
+ @patch('ansible.module_utils.facts.system.service_mgr.os.path.islink', side_effect=lambda x: x == '/sbin/init')
+ @patch('ansible.module_utils.facts.system.service_mgr.os.readlink', side_effect=lambda x: '/sbin/runit-init' if x == '/sbin/init' else '/bin/false')
+ def test_service_mgr_runit(self, mock_gfc, mock_opl, mock_orl):
+ # /proc/1/comm contains 'runit-init', ps fails, service manager is runit
+ # should end up return 'runit'
+ module = self._mock_module()
+ module.run_command = Mock(return_value=(1, '', ''))
+ collected_facts = {'ansible_system': 'Linux'}
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module,
+ collected_facts=collected_facts)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict['service_mgr'], 'runit')
+
+ @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
+ @patch('ansible.module_utils.facts.system.service_mgr.os.path.islink', side_effect=lambda x: x == '/sbin/init')
+ @patch('ansible.module_utils.facts.system.service_mgr.os.readlink', side_effect=lambda x: '/sbin/runit-init' if x == '/sbin/init' else '/bin/false')
+ def test_service_mgr_runit_no_comm(self, mock_gfc, mock_opl, mock_orl):
+ # no /proc/1/comm, ps returns 'COMMAND\n', service manager is runit
+ # should end up return 'runit'
+ module = self._mock_module()
+ module.run_command = Mock(return_value=(1, 'COMMAND\n', ''))
+ collected_facts = {'ansible_system': 'Linux'}
+ fact_collector = self.collector_class()
+ facts_dict = fact_collector.collect(module=module,
+ collected_facts=collected_facts)
+ self.assertIsInstance(facts_dict, dict)
+ self.assertEqual(facts_dict['service_mgr'], 'runit')
+
+ # TODO: reenable these tests when we can mock more easily
+
+# @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
+# def test_sunos_fallback(self, mock_gfc):
+# # no /proc/1/comm, ps fails, 'system' is SunOS
+# # should end up return 'smf'?
+# module = self._mock_module()
+# # FIXME: the result here is a kluge to at least cover more of service_mgr.collect
+# # TODO: remove
+# # FIXME: have to force a pid for results here to get into any of the system/distro checks
+# module.run_command = Mock(return_value=(1, ' 37 ', ''))
+# collected_facts = {'system': 'SunOS'}
+# fact_collector = self.collector_class(module=module)
+# facts_dict = fact_collector.collect(collected_facts=collected_facts)
+# print('facts_dict: %s' % facts_dict)
+# self.assertIsInstance(facts_dict, dict)
+# self.assertEqual(facts_dict['service_mgr'], 'smf')
+
+# @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
+# def test_aix_fallback(self, mock_gfc):
+# # no /proc/1/comm, ps fails, 'system' is SunOS
+# # should end up return 'smf'?
+# module = self._mock_module()
+# module.run_command = Mock(return_value=(1, '', ''))
+# collected_facts = {'system': 'AIX'}
+# fact_collector = self.collector_class(module=module)
+# facts_dict = fact_collector.collect(collected_facts=collected_facts)
+# print('facts_dict: %s' % facts_dict)
+# self.assertIsInstance(facts_dict, dict)
+# self.assertEqual(facts_dict['service_mgr'], 'src')
+
+# @patch('ansible.module_utils.facts.system.service_mgr.get_file_content', return_value=None)
+# def test_linux_fallback(self, mock_gfc):
+# # no /proc/1/comm, ps fails, 'system' is SunOS
+# # should end up return 'smf'?
+# module = self._mock_module()
+# module.run_command = Mock(return_value=(1, ' 37 ', ''))
+# collected_facts = {'system': 'Linux'}
+# fact_collector = self.collector_class(module=module)
+# facts_dict = fact_collector.collect(collected_facts=collected_facts)
+# print('facts_dict: %s' % facts_dict)
+# self.assertIsInstance(facts_dict, dict)
+# self.assertEqual(facts_dict['service_mgr'], 'sdfadf')
+
+
+class TestSshPubKeyFactCollector(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'ssh_pub_keys']
+ valid_subsets = ['ssh_pub_keys']
+ fact_namespace = 'ansible_ssh_pub_leys'
+ collector_class = SshPubKeyFactCollector
+
+
+class TestUserFactCollector(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'user']
+ valid_subsets = ['user']
+ fact_namespace = 'ansible_user'
+ collector_class = UserFactCollector
+
+
+class TestVirtualFacts(BaseFactsTest):
+ __test__ = True
+ gather_subset = ['!all', 'virtual']
+ valid_subsets = ['virtual']
+ fact_namespace = 'ansible_virtual'
+ collector_class = VirtualCollector
diff --git a/test/units/module_utils/facts/test_date_time.py b/test/units/module_utils/facts/test_date_time.py
new file mode 100644
index 0000000..6abc36a
--- /dev/null
+++ b/test/units/module_utils/facts/test_date_time.py
@@ -0,0 +1,106 @@
+# -*- 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
+
+import pytest
+import datetime
+import string
+import time
+
+from ansible.module_utils.facts.system import date_time
+
+EPOCH_TS = 1594449296.123456
+DT = datetime.datetime(2020, 7, 11, 12, 34, 56, 124356)
+DT_UTC = datetime.datetime(2020, 7, 11, 2, 34, 56, 124356)
+
+
+@pytest.fixture
+def fake_now(monkeypatch):
+ """
+ Patch `datetime.datetime.fromtimestamp()`, `datetime.datetime.utcfromtimestamp()`,
+ and `time.time()` to return deterministic values.
+ """
+
+ class FakeNow:
+ @classmethod
+ def fromtimestamp(cls, timestamp):
+ return DT
+
+ @classmethod
+ def utcfromtimestamp(cls, timestamp):
+ return DT_UTC
+
+ def _time():
+ return EPOCH_TS
+
+ monkeypatch.setattr(date_time.datetime, 'datetime', FakeNow)
+ monkeypatch.setattr(time, 'time', _time)
+
+
+@pytest.fixture
+def fake_date_facts(fake_now):
+ """Return a predictable instance of collected date_time facts."""
+
+ collector = date_time.DateTimeFactCollector()
+ data = collector.collect()
+
+ return data
+
+
+@pytest.mark.parametrize(
+ ('fact_name', 'fact_value'),
+ (
+ ('year', '2020'),
+ ('month', '07'),
+ ('weekday', 'Saturday'),
+ ('weekday_number', '6'),
+ ('weeknumber', '27'),
+ ('day', '11'),
+ ('hour', '12'),
+ ('minute', '34'),
+ ('second', '56'),
+ ('date', '2020-07-11'),
+ ('time', '12:34:56'),
+ ('iso8601_basic', '20200711T123456124356'),
+ ('iso8601_basic_short', '20200711T123456'),
+ ('iso8601_micro', '2020-07-11T02:34:56.124356Z'),
+ ('iso8601', '2020-07-11T02:34:56Z'),
+ ),
+)
+def test_date_time_facts(fake_date_facts, fact_name, fact_value):
+ assert fake_date_facts['date_time'][fact_name] == fact_value
+
+
+def test_date_time_epoch(fake_date_facts):
+ """Test that format of returned epoch value is correct"""
+
+ assert fake_date_facts['date_time']['epoch'].isdigit()
+ assert len(fake_date_facts['date_time']['epoch']) == 10 # This length will not change any time soon
+ assert fake_date_facts['date_time']['epoch_int'].isdigit()
+ assert len(fake_date_facts['date_time']['epoch_int']) == 10 # This length will not change any time soon
+
+
+@pytest.mark.parametrize('fact_name', ('tz', 'tz_dst'))
+def test_date_time_tz(fake_date_facts, fact_name):
+ """
+ Test the returned value for timezone consists of only uppercase
+ letters and is the expected length.
+ """
+
+ assert fake_date_facts['date_time'][fact_name].isupper()
+ assert 2 <= len(fake_date_facts['date_time'][fact_name]) <= 5
+ assert not set(fake_date_facts['date_time'][fact_name]).difference(set(string.ascii_uppercase))
+
+
+def test_date_time_tz_offset(fake_date_facts):
+ """
+ Test that the timezone offset begins with a `+` or `-` and ends with a
+ series of integers.
+ """
+
+ assert fake_date_facts['date_time']['tz_offset'][0] in ['-', '+']
+ assert fake_date_facts['date_time']['tz_offset'][1:].isdigit()
+ assert len(fake_date_facts['date_time']['tz_offset']) == 5
diff --git a/test/units/module_utils/facts/test_facts.py b/test/units/module_utils/facts/test_facts.py
new file mode 100644
index 0000000..c794f03
--- /dev/null
+++ b/test/units/module_utils/facts/test_facts.py
@@ -0,0 +1,646 @@
+# This file is part of Ansible
+# -*- coding: utf-8 -*-
+#
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+import pytest
+
+# for testing
+from units.compat import unittest
+from units.compat.mock import Mock, patch
+
+from ansible.module_utils import facts
+from ansible.module_utils.facts import hardware
+from ansible.module_utils.facts import network
+from ansible.module_utils.facts import virtual
+
+
+class BaseTestFactsPlatform(unittest.TestCase):
+ platform_id = 'Generic'
+ fact_class = hardware.base.Hardware
+ collector_class = None
+
+ """Verify that the automagic in Hardware.__new__ selects the right subclass."""
+ @patch('platform.system')
+ def test_new(self, mock_platform):
+ if not self.fact_class:
+ pytest.skip('This platform (%s) does not have a fact_class.' % self.platform_id)
+ mock_platform.return_value = self.platform_id
+ inst = self.fact_class(module=Mock(), load_on_init=False)
+ self.assertIsInstance(inst, self.fact_class)
+ self.assertEqual(inst.platform, self.platform_id)
+
+ def test_subclass(self):
+ if not self.fact_class:
+ pytest.skip('This platform (%s) does not have a fact_class.' % self.platform_id)
+ # 'Generic' will try to map to platform.system() that we are not mocking here
+ if self.platform_id == 'Generic':
+ return
+ inst = self.fact_class(module=Mock(), load_on_init=False)
+ self.assertIsInstance(inst, self.fact_class)
+ self.assertEqual(inst.platform, self.platform_id)
+
+ def test_collector(self):
+ if not self.collector_class:
+ pytest.skip('This test class needs to be updated to specify collector_class')
+ inst = self.collector_class()
+ self.assertIsInstance(inst, self.collector_class)
+ self.assertEqual(inst._platform, self.platform_id)
+
+
+class TestLinuxFactsPlatform(BaseTestFactsPlatform):
+ platform_id = 'Linux'
+ fact_class = hardware.linux.LinuxHardware
+ collector_class = hardware.linux.LinuxHardwareCollector
+
+
+class TestHurdFactsPlatform(BaseTestFactsPlatform):
+ platform_id = 'GNU'
+ fact_class = hardware.hurd.HurdHardware
+ collector_class = hardware.hurd.HurdHardwareCollector
+
+
+class TestSunOSHardware(BaseTestFactsPlatform):
+ platform_id = 'SunOS'
+ fact_class = hardware.sunos.SunOSHardware
+ collector_class = hardware.sunos.SunOSHardwareCollector
+
+
+class TestOpenBSDHardware(BaseTestFactsPlatform):
+ platform_id = 'OpenBSD'
+ fact_class = hardware.openbsd.OpenBSDHardware
+ collector_class = hardware.openbsd.OpenBSDHardwareCollector
+
+
+class TestFreeBSDHardware(BaseTestFactsPlatform):
+ platform_id = 'FreeBSD'
+ fact_class = hardware.freebsd.FreeBSDHardware
+ collector_class = hardware.freebsd.FreeBSDHardwareCollector
+
+
+class TestDragonFlyHardware(BaseTestFactsPlatform):
+ platform_id = 'DragonFly'
+ fact_class = None
+ collector_class = hardware.dragonfly.DragonFlyHardwareCollector
+
+
+class TestNetBSDHardware(BaseTestFactsPlatform):
+ platform_id = 'NetBSD'
+ fact_class = hardware.netbsd.NetBSDHardware
+ collector_class = hardware.netbsd.NetBSDHardwareCollector
+
+
+class TestAIXHardware(BaseTestFactsPlatform):
+ platform_id = 'AIX'
+ fact_class = hardware.aix.AIXHardware
+ collector_class = hardware.aix.AIXHardwareCollector
+
+
+class TestHPUXHardware(BaseTestFactsPlatform):
+ platform_id = 'HP-UX'
+ fact_class = hardware.hpux.HPUXHardware
+ collector_class = hardware.hpux.HPUXHardwareCollector
+
+
+class TestDarwinHardware(BaseTestFactsPlatform):
+ platform_id = 'Darwin'
+ fact_class = hardware.darwin.DarwinHardware
+ collector_class = hardware.darwin.DarwinHardwareCollector
+
+
+class TestGenericNetwork(BaseTestFactsPlatform):
+ platform_id = 'Generic'
+ fact_class = network.base.Network
+
+
+class TestHurdPfinetNetwork(BaseTestFactsPlatform):
+ platform_id = 'GNU'
+ fact_class = network.hurd.HurdPfinetNetwork
+ collector_class = network.hurd.HurdNetworkCollector
+
+
+class TestLinuxNetwork(BaseTestFactsPlatform):
+ platform_id = 'Linux'
+ fact_class = network.linux.LinuxNetwork
+ collector_class = network.linux.LinuxNetworkCollector
+
+
+class TestGenericBsdIfconfigNetwork(BaseTestFactsPlatform):
+ platform_id = 'Generic_BSD_Ifconfig'
+ fact_class = network.generic_bsd.GenericBsdIfconfigNetwork
+ collector_class = None
+
+
+class TestHPUXNetwork(BaseTestFactsPlatform):
+ platform_id = 'HP-UX'
+ fact_class = network.hpux.HPUXNetwork
+ collector_class = network.hpux.HPUXNetworkCollector
+
+
+class TestDarwinNetwork(BaseTestFactsPlatform):
+ platform_id = 'Darwin'
+ fact_class = network.darwin.DarwinNetwork
+ collector_class = network.darwin.DarwinNetworkCollector
+
+
+class TestFreeBSDNetwork(BaseTestFactsPlatform):
+ platform_id = 'FreeBSD'
+ fact_class = network.freebsd.FreeBSDNetwork
+ collector_class = network.freebsd.FreeBSDNetworkCollector
+
+
+class TestDragonFlyNetwork(BaseTestFactsPlatform):
+ platform_id = 'DragonFly'
+ fact_class = network.dragonfly.DragonFlyNetwork
+ collector_class = network.dragonfly.DragonFlyNetworkCollector
+
+
+class TestAIXNetwork(BaseTestFactsPlatform):
+ platform_id = 'AIX'
+ fact_class = network.aix.AIXNetwork
+ collector_class = network.aix.AIXNetworkCollector
+
+
+class TestNetBSDNetwork(BaseTestFactsPlatform):
+ platform_id = 'NetBSD'
+ fact_class = network.netbsd.NetBSDNetwork
+ collector_class = network.netbsd.NetBSDNetworkCollector
+
+
+class TestOpenBSDNetwork(BaseTestFactsPlatform):
+ platform_id = 'OpenBSD'
+ fact_class = network.openbsd.OpenBSDNetwork
+ collector_class = network.openbsd.OpenBSDNetworkCollector
+
+
+class TestSunOSNetwork(BaseTestFactsPlatform):
+ platform_id = 'SunOS'
+ fact_class = network.sunos.SunOSNetwork
+ collector_class = network.sunos.SunOSNetworkCollector
+
+
+class TestLinuxVirtual(BaseTestFactsPlatform):
+ platform_id = 'Linux'
+ fact_class = virtual.linux.LinuxVirtual
+ collector_class = virtual.linux.LinuxVirtualCollector
+
+
+class TestFreeBSDVirtual(BaseTestFactsPlatform):
+ platform_id = 'FreeBSD'
+ fact_class = virtual.freebsd.FreeBSDVirtual
+ collector_class = virtual.freebsd.FreeBSDVirtualCollector
+
+
+class TestNetBSDVirtual(BaseTestFactsPlatform):
+ platform_id = 'NetBSD'
+ fact_class = virtual.netbsd.NetBSDVirtual
+ collector_class = virtual.netbsd.NetBSDVirtualCollector
+
+
+class TestOpenBSDVirtual(BaseTestFactsPlatform):
+ platform_id = 'OpenBSD'
+ fact_class = virtual.openbsd.OpenBSDVirtual
+ collector_class = virtual.openbsd.OpenBSDVirtualCollector
+
+
+class TestHPUXVirtual(BaseTestFactsPlatform):
+ platform_id = 'HP-UX'
+ fact_class = virtual.hpux.HPUXVirtual
+ collector_class = virtual.hpux.HPUXVirtualCollector
+
+
+class TestSunOSVirtual(BaseTestFactsPlatform):
+ platform_id = 'SunOS'
+ fact_class = virtual.sunos.SunOSVirtual
+ collector_class = virtual.sunos.SunOSVirtualCollector
+
+
+LSBLK_OUTPUT = b"""
+/dev/sda
+/dev/sda1 32caaec3-ef40-4691-a3b6-438c3f9bc1c0
+/dev/sda2 66Ojcd-ULtu-1cZa-Tywo-mx0d-RF4O-ysA9jK
+/dev/mapper/fedora_dhcp129--186-swap eae6059d-2fbe-4d1c-920d-a80bbeb1ac6d
+/dev/mapper/fedora_dhcp129--186-root d34cf5e3-3449-4a6c-8179-a1feb2bca6ce
+/dev/mapper/fedora_dhcp129--186-home 2d3e4853-fa69-4ccf-8a6a-77b05ab0a42d
+/dev/sr0
+/dev/loop0 0f031512-ab15-497d-9abd-3a512b4a9390
+/dev/loop1 7c1b0f30-cf34-459f-9a70-2612f82b870a
+/dev/loop9 0f031512-ab15-497d-9abd-3a512b4a9390
+/dev/loop9 7c1b4444-cf34-459f-9a70-2612f82b870a
+/dev/mapper/docker-253:1-1050967-pool
+/dev/loop2
+/dev/mapper/docker-253:1-1050967-pool
+"""
+
+LSBLK_OUTPUT_2 = b"""
+/dev/sda
+/dev/sda1 32caaec3-ef40-4691-a3b6-438c3f9bc1c0
+/dev/sda2 66Ojcd-ULtu-1cZa-Tywo-mx0d-RF4O-ysA9jK
+/dev/mapper/fedora_dhcp129--186-swap eae6059d-2fbe-4d1c-920d-a80bbeb1ac6d
+/dev/mapper/fedora_dhcp129--186-root d34cf5e3-3449-4a6c-8179-a1feb2bca6ce
+/dev/mapper/fedora_dhcp129--186-home 2d3e4853-fa69-4ccf-8a6a-77b05ab0a42d
+/dev/mapper/an-example-mapper with a space in the name 84639acb-013f-4d2f-9392-526a572b4373
+/dev/sr0
+/dev/loop0 0f031512-ab15-497d-9abd-3a512b4a9390
+"""
+
+LSBLK_UUIDS = {'/dev/sda1': '66Ojcd-ULtu-1cZa-Tywo-mx0d-RF4O-ysA9jK'}
+
+UDEVADM_UUID = 'N/A'
+
+MTAB = r"""
+sysfs /sys sysfs rw,seclabel,nosuid,nodev,noexec,relatime 0 0
+proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
+devtmpfs /dev devtmpfs rw,seclabel,nosuid,size=8044400k,nr_inodes=2011100,mode=755 0 0
+securityfs /sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0
+tmpfs /dev/shm tmpfs rw,seclabel,nosuid,nodev 0 0
+devpts /dev/pts devpts rw,seclabel,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0
+tmpfs /run tmpfs rw,seclabel,nosuid,nodev,mode=755 0 0
+tmpfs /sys/fs/cgroup tmpfs ro,seclabel,nosuid,nodev,noexec,mode=755 0 0
+cgroup /sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd 0 0
+pstore /sys/fs/pstore pstore rw,seclabel,nosuid,nodev,noexec,relatime 0 0
+cgroup /sys/fs/cgroup/devices cgroup rw,nosuid,nodev,noexec,relatime,devices 0 0
+cgroup /sys/fs/cgroup/freezer cgroup rw,nosuid,nodev,noexec,relatime,freezer 0 0
+cgroup /sys/fs/cgroup/memory cgroup rw,nosuid,nodev,noexec,relatime,memory 0 0
+cgroup /sys/fs/cgroup/pids cgroup rw,nosuid,nodev,noexec,relatime,pids 0 0
+cgroup /sys/fs/cgroup/blkio cgroup rw,nosuid,nodev,noexec,relatime,blkio 0 0
+cgroup /sys/fs/cgroup/cpuset cgroup rw,nosuid,nodev,noexec,relatime,cpuset 0 0
+cgroup /sys/fs/cgroup/cpu,cpuacct cgroup rw,nosuid,nodev,noexec,relatime,cpu,cpuacct 0 0
+cgroup /sys/fs/cgroup/hugetlb cgroup rw,nosuid,nodev,noexec,relatime,hugetlb 0 0
+cgroup /sys/fs/cgroup/perf_event cgroup rw,nosuid,nodev,noexec,relatime,perf_event 0 0
+cgroup /sys/fs/cgroup/net_cls,net_prio cgroup rw,nosuid,nodev,noexec,relatime,net_cls,net_prio 0 0
+configfs /sys/kernel/config configfs rw,relatime 0 0
+/dev/mapper/fedora_dhcp129--186-root / ext4 rw,seclabel,relatime,data=ordered 0 0
+selinuxfs /sys/fs/selinux selinuxfs rw,relatime 0 0
+systemd-1 /proc/sys/fs/binfmt_misc autofs rw,relatime,fd=24,pgrp=1,timeout=0,minproto=5,maxproto=5,direct 0 0
+debugfs /sys/kernel/debug debugfs rw,seclabel,relatime 0 0
+hugetlbfs /dev/hugepages hugetlbfs rw,seclabel,relatime 0 0
+tmpfs /tmp tmpfs rw,seclabel 0 0
+mqueue /dev/mqueue mqueue rw,seclabel,relatime 0 0
+/dev/loop0 /var/lib/machines btrfs rw,seclabel,relatime,space_cache,subvolid=5,subvol=/ 0 0
+/dev/sda1 /boot ext4 rw,seclabel,relatime,data=ordered 0 0
+/dev/mapper/fedora_dhcp129--186-home /home ext4 rw,seclabel,relatime,data=ordered 0 0
+tmpfs /run/user/1000 tmpfs rw,seclabel,nosuid,nodev,relatime,size=1611044k,mode=700,uid=1000,gid=1000 0 0
+gvfsd-fuse /run/user/1000/gvfs fuse.gvfsd-fuse rw,nosuid,nodev,relatime,user_id=1000,group_id=1000 0 0
+fusectl /sys/fs/fuse/connections fusectl rw,relatime 0 0
+grimlock.g.a: /home/adrian/sshfs-grimlock fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000 0 0
+grimlock.g.a:test_path/path_with'single_quotes /home/adrian/sshfs-grimlock-single-quote fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000 0 0
+grimlock.g.a:path_with'single_quotes /home/adrian/sshfs-grimlock-single-quote-2 fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000 0 0
+grimlock.g.a:/mnt/data/foto's /home/adrian/fotos fuse.sshfs rw,nosuid,nodev,relatime,user_id=1000,group_id=1000 0 0
+\\Windows\share /data/ cifs credentials=/root/.creds 0 0
+"""
+
+MTAB_ENTRIES = [
+ [
+ 'sysfs',
+ '/sys',
+ 'sysfs',
+ 'rw,seclabel,nosuid,nodev,noexec,relatime',
+ '0',
+ '0'
+ ],
+ ['proc', '/proc', 'proc', 'rw,nosuid,nodev,noexec,relatime', '0', '0'],
+ [
+ 'devtmpfs',
+ '/dev',
+ 'devtmpfs',
+ 'rw,seclabel,nosuid,size=8044400k,nr_inodes=2011100,mode=755',
+ '0',
+ '0'
+ ],
+ [
+ 'securityfs',
+ '/sys/kernel/security',
+ 'securityfs',
+ 'rw,nosuid,nodev,noexec,relatime',
+ '0',
+ '0'
+ ],
+ ['tmpfs', '/dev/shm', 'tmpfs', 'rw,seclabel,nosuid,nodev', '0', '0'],
+ [
+ 'devpts',
+ '/dev/pts',
+ 'devpts',
+ 'rw,seclabel,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000',
+ '0',
+ '0'
+ ],
+ ['tmpfs', '/run', 'tmpfs', 'rw,seclabel,nosuid,nodev,mode=755', '0', '0'],
+ [
+ 'tmpfs',
+ '/sys/fs/cgroup',
+ 'tmpfs',
+ 'ro,seclabel,nosuid,nodev,noexec,mode=755',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/systemd',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd',
+ '0',
+ '0'
+ ],
+ [
+ 'pstore',
+ '/sys/fs/pstore',
+ 'pstore',
+ 'rw,seclabel,nosuid,nodev,noexec,relatime',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/devices',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,devices',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/freezer',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,freezer',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/memory',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,memory',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/pids',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,pids',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/blkio',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,blkio',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/cpuset',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,cpuset',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/cpu,cpuacct',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,cpu,cpuacct',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/hugetlb',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,hugetlb',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/perf_event',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,perf_event',
+ '0',
+ '0'
+ ],
+ [
+ 'cgroup',
+ '/sys/fs/cgroup/net_cls,net_prio',
+ 'cgroup',
+ 'rw,nosuid,nodev,noexec,relatime,net_cls,net_prio',
+ '0',
+ '0'
+ ],
+ ['configfs', '/sys/kernel/config', 'configfs', 'rw,relatime', '0', '0'],
+ [
+ '/dev/mapper/fedora_dhcp129--186-root',
+ '/',
+ 'ext4',
+ 'rw,seclabel,relatime,data=ordered',
+ '0',
+ '0'
+ ],
+ ['selinuxfs', '/sys/fs/selinux', 'selinuxfs', 'rw,relatime', '0', '0'],
+ [
+ 'systemd-1',
+ '/proc/sys/fs/binfmt_misc',
+ 'autofs',
+ 'rw,relatime,fd=24,pgrp=1,timeout=0,minproto=5,maxproto=5,direct',
+ '0',
+ '0'
+ ],
+ ['debugfs', '/sys/kernel/debug', 'debugfs', 'rw,seclabel,relatime', '0', '0'],
+ [
+ 'hugetlbfs',
+ '/dev/hugepages',
+ 'hugetlbfs',
+ 'rw,seclabel,relatime',
+ '0',
+ '0'
+ ],
+ ['tmpfs', '/tmp', 'tmpfs', 'rw,seclabel', '0', '0'],
+ ['mqueue', '/dev/mqueue', 'mqueue', 'rw,seclabel,relatime', '0', '0'],
+ [
+ '/dev/loop0',
+ '/var/lib/machines',
+ 'btrfs',
+ 'rw,seclabel,relatime,space_cache,subvolid=5,subvol=/',
+ '0',
+ '0'
+ ],
+ ['/dev/sda1', '/boot', 'ext4', 'rw,seclabel,relatime,data=ordered', '0', '0'],
+ # A 'none' fstype
+ ['/dev/sdz3', '/not/a/real/device', 'none', 'rw,seclabel,relatime,data=ordered', '0', '0'],
+ # lets assume this is a bindmount
+ ['/dev/sdz4', '/not/a/real/bind_mount', 'ext4', 'rw,seclabel,relatime,data=ordered', '0', '0'],
+ [
+ '/dev/mapper/fedora_dhcp129--186-home',
+ '/home',
+ 'ext4',
+ 'rw,seclabel,relatime,data=ordered',
+ '0',
+ '0'
+ ],
+ [
+ 'tmpfs',
+ '/run/user/1000',
+ 'tmpfs',
+ 'rw,seclabel,nosuid,nodev,relatime,size=1611044k,mode=700,uid=1000,gid=1000',
+ '0',
+ '0'
+ ],
+ [
+ 'gvfsd-fuse',
+ '/run/user/1000/gvfs',
+ 'fuse.gvfsd-fuse',
+ 'rw,nosuid,nodev,relatime,user_id=1000,group_id=1000',
+ '0',
+ '0'
+ ],
+ ['fusectl', '/sys/fs/fuse/connections', 'fusectl', 'rw,relatime', '0', '0'],
+ # Mount path with space in the name
+ # The space is encoded as \040 since the fields in /etc/mtab are space-delimeted
+ ['/dev/sdz9', r'/mnt/foo\040bar', 'ext4', 'rw,relatime', '0', '0'],
+ ['\\\\Windows\\share', '/data/', 'cifs', 'credentials=/root/.creds', '0', '0'],
+]
+
+BIND_MOUNTS = ['/not/a/real/bind_mount']
+
+with open(os.path.join(os.path.dirname(__file__), 'fixtures/findmount_output.txt')) as f:
+ FINDMNT_OUTPUT = f.read()
+
+
+class TestFactsLinuxHardwareGetMountFacts(unittest.TestCase):
+
+ # FIXME: mock.patch instead
+ def setUp(self):
+ # The @timeout tracebacks if there isn't a GATHER_TIMEOUT is None (the default until get_all_facts sets it via global)
+ facts.GATHER_TIMEOUT = 10
+
+ def tearDown(self):
+ facts.GATHER_TIMEOUT = None
+
+ # The Hardware subclasses freakout if instaniated directly, so
+ # mock platform.system and inst Hardware() so we get a LinuxHardware()
+ # we can test.
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._mtab_entries', return_value=MTAB_ENTRIES)
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._find_bind_mounts', return_value=BIND_MOUNTS)
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._lsblk_uuid', return_value=LSBLK_UUIDS)
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._udevadm_uuid', return_value=UDEVADM_UUID)
+ def test_get_mount_facts(self,
+ mock_lsblk_uuid,
+ mock_find_bind_mounts,
+ mock_mtab_entries,
+ mock_udevadm_uuid):
+ module = Mock()
+ # Returns a LinuxHardware-ish
+ lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
+
+ # Nothing returned, just self.facts modified as a side effect
+ mount_facts = lh.get_mount_facts()
+ self.assertIsInstance(mount_facts, dict)
+ self.assertIn('mounts', mount_facts)
+ self.assertIsInstance(mount_facts['mounts'], list)
+ self.assertIsInstance(mount_facts['mounts'][0], dict)
+
+ # Find mounts with space in the mountpoint path
+ mounts_with_space = [x for x in mount_facts['mounts'] if ' ' in x['mount']]
+ self.assertEqual(len(mounts_with_space), 1)
+ self.assertEqual(mounts_with_space[0]['mount'], '/mnt/foo bar')
+
+ @patch('ansible.module_utils.facts.hardware.linux.get_file_content', return_value=MTAB)
+ def test_get_mtab_entries(self, mock_get_file_content):
+
+ module = Mock()
+ lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
+ mtab_entries = lh._mtab_entries()
+ self.assertIsInstance(mtab_entries, list)
+ self.assertIsInstance(mtab_entries[0], list)
+ self.assertEqual(len(mtab_entries), 39)
+
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_findmnt', return_value=(0, FINDMNT_OUTPUT, ''))
+ def test_find_bind_mounts(self, mock_run_findmnt):
+ module = Mock()
+ lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
+ bind_mounts = lh._find_bind_mounts()
+
+ # If bind_mounts becomes another seq type, feel free to change
+ self.assertIsInstance(bind_mounts, set)
+ self.assertEqual(len(bind_mounts), 1)
+ self.assertIn('/not/a/real/bind_mount', bind_mounts)
+
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_findmnt', return_value=(37, '', ''))
+ def test_find_bind_mounts_non_zero(self, mock_run_findmnt):
+ module = Mock()
+ lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
+ bind_mounts = lh._find_bind_mounts()
+
+ self.assertIsInstance(bind_mounts, set)
+ self.assertEqual(len(bind_mounts), 0)
+
+ def test_find_bind_mounts_no_findmnts(self):
+ module = Mock()
+ module.get_bin_path = Mock(return_value=None)
+ lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
+ bind_mounts = lh._find_bind_mounts()
+
+ self.assertIsInstance(bind_mounts, set)
+ self.assertEqual(len(bind_mounts), 0)
+
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_lsblk', return_value=(0, LSBLK_OUTPUT, ''))
+ def test_lsblk_uuid(self, mock_run_lsblk):
+ module = Mock()
+ lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
+ lsblk_uuids = lh._lsblk_uuid()
+
+ self.assertIsInstance(lsblk_uuids, dict)
+ self.assertIn(b'/dev/loop9', lsblk_uuids)
+ self.assertIn(b'/dev/sda1', lsblk_uuids)
+ self.assertEqual(lsblk_uuids[b'/dev/sda1'], b'32caaec3-ef40-4691-a3b6-438c3f9bc1c0')
+
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_lsblk', return_value=(37, LSBLK_OUTPUT, ''))
+ def test_lsblk_uuid_non_zero(self, mock_run_lsblk):
+ module = Mock()
+ lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
+ lsblk_uuids = lh._lsblk_uuid()
+
+ self.assertIsInstance(lsblk_uuids, dict)
+ self.assertEqual(len(lsblk_uuids), 0)
+
+ def test_lsblk_uuid_no_lsblk(self):
+ module = Mock()
+ module.get_bin_path = Mock(return_value=None)
+ lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
+ lsblk_uuids = lh._lsblk_uuid()
+
+ self.assertIsInstance(lsblk_uuids, dict)
+ self.assertEqual(len(lsblk_uuids), 0)
+
+ @patch('ansible.module_utils.facts.hardware.linux.LinuxHardware._run_lsblk', return_value=(0, LSBLK_OUTPUT_2, ''))
+ def test_lsblk_uuid_dev_with_space_in_name(self, mock_run_lsblk):
+ module = Mock()
+ lh = hardware.linux.LinuxHardware(module=module, load_on_init=False)
+ lsblk_uuids = lh._lsblk_uuid()
+ self.assertIsInstance(lsblk_uuids, dict)
+ self.assertIn(b'/dev/loop0', lsblk_uuids)
+ self.assertIn(b'/dev/sda1', lsblk_uuids)
+ self.assertEqual(lsblk_uuids[b'/dev/mapper/an-example-mapper with a space in the name'], b'84639acb-013f-4d2f-9392-526a572b4373')
+ self.assertEqual(lsblk_uuids[b'/dev/sda1'], b'32caaec3-ef40-4691-a3b6-438c3f9bc1c0')
diff --git a/test/units/module_utils/facts/test_sysctl.py b/test/units/module_utils/facts/test_sysctl.py
new file mode 100644
index 0000000..c369b61
--- /dev/null
+++ b/test/units/module_utils/facts/test_sysctl.py
@@ -0,0 +1,251 @@
+# This file is part of Ansible
+# -*- coding: utf-8 -*-
+#
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+import pytest
+
+# for testing
+from units.compat import unittest
+from units.compat.mock import patch, MagicMock, mock_open, Mock
+
+from ansible.module_utils.facts.sysctl import get_sysctl
+
+
+# `sysctl hw` on an openbsd machine
+OPENBSD_SYSCTL_HW = """
+hw.machine=amd64
+hw.model=AMD EPYC Processor (with IBPB)
+hw.ncpu=1
+hw.byteorder=1234
+hw.pagesize=4096
+hw.disknames=cd0:,sd0:9e1bd96cb20ab429,fd0:
+hw.diskcount=3
+hw.sensors.viomb0.raw0=0 (desired)
+hw.sensors.viomb0.raw1=0 (current)
+hw.cpuspeed=3394
+hw.vendor=QEMU
+hw.product=Standard PC (i440FX + PIIX, 1996)
+hw.version=pc-i440fx-5.1
+hw.uuid=5833415a-eefc-964f-a306-fa434d44d117
+hw.physmem=1056804864
+hw.usermem=1056792576
+hw.ncpufound=1
+hw.allowpowerdown=1
+hw.smt=0
+hw.ncpuonline=1
+"""
+
+# partial output of `sysctl kern` on an openbsd machine
+# for testing multiline parsing
+OPENBSD_SYSCTL_KERN_PARTIAL = """
+kern.ostype=OpenBSD
+kern.osrelease=6.7
+kern.osrevision=202005
+kern.version=OpenBSD 6.7 (GENERIC) #179: Thu May 7 11:02:37 MDT 2020
+ deraadt@amd64.openbsd.org:/usr/src/sys/arch/amd64/compile/GENERIC
+
+kern.maxvnodes=12447
+kern.maxproc=1310
+kern.maxfiles=7030
+kern.argmax=524288
+kern.securelevel=1
+kern.hostname=openbsd67.vm.home.elrod.me
+kern.hostid=0
+kern.clockrate=tick = 10000, tickadj = 40, hz = 100, profhz = 100, stathz = 100
+kern.posix1version=200809
+"""
+
+# partial output of `sysctl vm` on Linux. The output has tabs in it and Linux
+# sysctl has spaces around the =
+LINUX_SYSCTL_VM_PARTIAL = """
+vm.dirty_background_ratio = 10
+vm.dirty_bytes = 0
+vm.dirty_expire_centisecs = 3000
+vm.dirty_ratio = 20
+vm.dirty_writeback_centisecs = 500
+vm.dirtytime_expire_seconds = 43200
+vm.extfrag_threshold = 500
+vm.hugetlb_shm_group = 0
+vm.laptop_mode = 0
+vm.legacy_va_layout = 0
+vm.lowmem_reserve_ratio = 256 256 32 0
+vm.max_map_count = 65530
+vm.min_free_kbytes = 22914
+vm.min_slab_ratio = 5
+"""
+
+# partial output of `sysctl vm` on macOS. The output is colon-separated.
+MACOS_SYSCTL_VM_PARTIAL = """
+vm.loadavg: { 1.28 1.18 1.13 }
+vm.swapusage: total = 2048.00M used = 1017.50M free = 1030.50M (encrypted)
+vm.cs_force_kill: 0
+vm.cs_force_hard: 0
+vm.cs_debug: 0
+vm.cs_debug_fail_on_unsigned_code: 0
+vm.cs_debug_unsigned_exec_failures: 0
+vm.cs_debug_unsigned_mmap_failures: 0
+vm.cs_all_vnodes: 0
+vm.cs_system_enforcement: 1
+vm.cs_process_enforcement: 0
+vm.cs_enforcement_panic: 0
+vm.cs_library_validation: 0
+vm.global_user_wire_limit: 3006477107
+"""
+
+# Invalid/bad output
+BAD_SYSCTL = """
+this.output.is.invalid
+it.has.no.equals.sign.or.colon
+so.it.should.fail.to.parse
+"""
+
+# Mixed good/bad output
+GOOD_BAD_SYSCTL = """
+bad.output.here
+hw.smt=0
+and.bad.output.here
+"""
+
+
+class TestSysctlParsingInFacts(unittest.TestCase):
+
+ def test_get_sysctl_missing_binary(self):
+ module = MagicMock()
+ module.get_bin_path.return_value = '/usr/sbin/sysctl'
+ module.run_command.side_effect = ValueError
+ self.assertRaises(ValueError, get_sysctl, module, ['vm'])
+
+ def test_get_sysctl_nonzero_rc(self):
+ module = MagicMock()
+ module.get_bin_path.return_value = '/usr/sbin/sysctl'
+ module.run_command.return_value = (1, '', '')
+ sysctl = get_sysctl(module, ['hw'])
+ self.assertEqual(sysctl, {})
+
+ def test_get_sysctl_command_error(self):
+ module = MagicMock()
+ module.get_bin_path.return_value = '/usr/sbin/sysctl'
+ for err in (IOError, OSError):
+ module.reset_mock()
+ module.run_command.side_effect = err('foo')
+ sysctl = get_sysctl(module, ['hw'])
+ module.warn.assert_called_once_with('Unable to read sysctl: foo')
+ self.assertEqual(sysctl, {})
+
+ def test_get_sysctl_all_invalid_output(self):
+ module = MagicMock()
+ module.get_bin_path.return_value = '/sbin/sysctl'
+ module.run_command.return_value = (0, BAD_SYSCTL, '')
+ sysctl = get_sysctl(module, ['hw'])
+ module.run_command.assert_called_once_with(['/sbin/sysctl', 'hw'])
+ lines = [l for l in BAD_SYSCTL.splitlines() if l]
+ for call in module.warn.call_args_list:
+ self.assertIn('Unable to split sysctl line', call[0][0])
+ self.assertEqual(module.warn.call_count, len(lines))
+ self.assertEqual(sysctl, {})
+
+ def test_get_sysctl_mixed_invalid_output(self):
+ module = MagicMock()
+ module.get_bin_path.return_value = '/sbin/sysctl'
+ module.run_command.return_value = (0, GOOD_BAD_SYSCTL, '')
+ sysctl = get_sysctl(module, ['hw'])
+ module.run_command.assert_called_once_with(['/sbin/sysctl', 'hw'])
+ bad_lines = ['bad.output.here', 'and.bad.output.here']
+ for call in module.warn.call_args_list:
+ self.assertIn('Unable to split sysctl line', call[0][0])
+ self.assertEqual(module.warn.call_count, 2)
+ self.assertEqual(sysctl, {'hw.smt': '0'})
+
+ def test_get_sysctl_openbsd_hw(self):
+ expected_lines = [l for l in OPENBSD_SYSCTL_HW.splitlines() if l]
+ module = MagicMock()
+ module.get_bin_path.return_value = '/sbin/sysctl'
+ module.run_command.return_value = (0, OPENBSD_SYSCTL_HW, '')
+ sysctl = get_sysctl(module, ['hw'])
+ module.run_command.assert_called_once_with(['/sbin/sysctl', 'hw'])
+ self.assertEqual(len(sysctl), len(expected_lines))
+ self.assertEqual(sysctl['hw.machine'], 'amd64') # first line
+ self.assertEqual(sysctl['hw.smt'], '0') # random line
+ self.assertEqual(sysctl['hw.ncpuonline'], '1') # last line
+ # weird chars in value
+ self.assertEqual(
+ sysctl['hw.disknames'],
+ 'cd0:,sd0:9e1bd96cb20ab429,fd0:')
+ # more symbols/spaces in value
+ self.assertEqual(
+ sysctl['hw.product'],
+ 'Standard PC (i440FX + PIIX, 1996)')
+
+ def test_get_sysctl_openbsd_kern(self):
+ module = MagicMock()
+ module.get_bin_path.return_value = '/sbin/sysctl'
+ module.run_command.return_value = (0, OPENBSD_SYSCTL_KERN_PARTIAL, '')
+ sysctl = get_sysctl(module, ['kern'])
+ module.run_command.assert_called_once_with(['/sbin/sysctl', 'kern'])
+ self.assertEqual(
+ len(sysctl),
+ len(
+ [l for l
+ in OPENBSD_SYSCTL_KERN_PARTIAL.splitlines()
+ if l.startswith('kern')]))
+ self.assertEqual(sysctl['kern.ostype'], 'OpenBSD') # first line
+ self.assertEqual(sysctl['kern.maxproc'], '1310') # random line
+ self.assertEqual(sysctl['kern.posix1version'], '200809') # last line
+ # multiline
+ self.assertEqual(
+ sysctl['kern.version'],
+ 'OpenBSD 6.7 (GENERIC) #179: Thu May 7 11:02:37 MDT 2020\n '
+ 'deraadt@amd64.openbsd.org:/usr/src/sys/arch/amd64/compile/GENERIC')
+ # more symbols/spaces in value
+ self.assertEqual(
+ sysctl['kern.clockrate'],
+ 'tick = 10000, tickadj = 40, hz = 100, profhz = 100, stathz = 100')
+
+ def test_get_sysctl_linux_vm(self):
+ module = MagicMock()
+ module.get_bin_path.return_value = '/usr/sbin/sysctl'
+ module.run_command.return_value = (0, LINUX_SYSCTL_VM_PARTIAL, '')
+ sysctl = get_sysctl(module, ['vm'])
+ module.run_command.assert_called_once_with(['/usr/sbin/sysctl', 'vm'])
+ self.assertEqual(
+ len(sysctl),
+ len([l for l in LINUX_SYSCTL_VM_PARTIAL.splitlines() if l]))
+ self.assertEqual(sysctl['vm.dirty_background_ratio'], '10')
+ self.assertEqual(sysctl['vm.laptop_mode'], '0')
+ self.assertEqual(sysctl['vm.min_slab_ratio'], '5')
+ # tabs
+ self.assertEqual(sysctl['vm.lowmem_reserve_ratio'], '256\t256\t32\t0')
+
+ def test_get_sysctl_macos_vm(self):
+ module = MagicMock()
+ module.get_bin_path.return_value = '/usr/sbin/sysctl'
+ module.run_command.return_value = (0, MACOS_SYSCTL_VM_PARTIAL, '')
+ sysctl = get_sysctl(module, ['vm'])
+ module.run_command.assert_called_once_with(['/usr/sbin/sysctl', 'vm'])
+ self.assertEqual(
+ len(sysctl),
+ len([l for l in MACOS_SYSCTL_VM_PARTIAL.splitlines() if l]))
+ self.assertEqual(sysctl['vm.loadavg'], '{ 1.28 1.18 1.13 }')
+ self.assertEqual(
+ sysctl['vm.swapusage'],
+ 'total = 2048.00M used = 1017.50M free = 1030.50M (encrypted)')
diff --git a/test/units/module_utils/facts/test_timeout.py b/test/units/module_utils/facts/test_timeout.py
new file mode 100644
index 0000000..2adbc4a
--- /dev/null
+++ b/test/units/module_utils/facts/test_timeout.py
@@ -0,0 +1,171 @@
+# -*- coding: utf-8 -*-
+# (c) 2017, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+import time
+
+import pytest
+
+from ansible.module_utils.facts import timeout
+
+
+@pytest.fixture
+def set_gather_timeout_higher():
+ default_timeout = timeout.GATHER_TIMEOUT
+ timeout.GATHER_TIMEOUT = 5
+ yield
+ timeout.GATHER_TIMEOUT = default_timeout
+
+
+@pytest.fixture
+def set_gather_timeout_lower():
+ default_timeout = timeout.GATHER_TIMEOUT
+ timeout.GATHER_TIMEOUT = 2
+ yield
+ timeout.GATHER_TIMEOUT = default_timeout
+
+
+@timeout.timeout
+def sleep_amount_implicit(amount):
+ # implicit refers to the lack of argument to the decorator
+ time.sleep(amount)
+ return 'Succeeded after {0} sec'.format(amount)
+
+
+@timeout.timeout(timeout.DEFAULT_GATHER_TIMEOUT + 5)
+def sleep_amount_explicit_higher(amount):
+ # explicit refers to the argument to the decorator
+ time.sleep(amount)
+ return 'Succeeded after {0} sec'.format(amount)
+
+
+@timeout.timeout(2)
+def sleep_amount_explicit_lower(amount):
+ # explicit refers to the argument to the decorator
+ time.sleep(amount)
+ return 'Succeeded after {0} sec'.format(amount)
+
+
+#
+# Tests for how the timeout decorator is specified
+#
+
+def test_defaults_still_within_bounds():
+ # If the default changes outside of these bounds, some of the tests will
+ # no longer test the right thing. Need to review and update the timeouts
+ # in the other tests if this fails
+ assert timeout.DEFAULT_GATHER_TIMEOUT >= 4
+
+
+def test_implicit_file_default_succeeds():
+ # amount checked must be less than DEFAULT_GATHER_TIMEOUT
+ assert sleep_amount_implicit(1) == 'Succeeded after 1 sec'
+
+
+def test_implicit_file_default_timesout(monkeypatch):
+ monkeypatch.setattr(timeout, 'DEFAULT_GATHER_TIMEOUT', 1)
+ # sleep_time is greater than the default
+ sleep_time = timeout.DEFAULT_GATHER_TIMEOUT + 1
+ with pytest.raises(timeout.TimeoutError):
+ assert sleep_amount_implicit(sleep_time) == '(Not expected to succeed)'
+
+
+def test_implicit_file_overridden_succeeds(set_gather_timeout_higher):
+ # Set sleep_time greater than the default timeout and less than our new timeout
+ sleep_time = 3
+ assert sleep_amount_implicit(sleep_time) == 'Succeeded after {0} sec'.format(sleep_time)
+
+
+def test_implicit_file_overridden_timesout(set_gather_timeout_lower):
+ # Set sleep_time greater than our new timeout but less than the default
+ sleep_time = 3
+ with pytest.raises(timeout.TimeoutError):
+ assert sleep_amount_implicit(sleep_time) == '(Not expected to Succeed)'
+
+
+def test_explicit_succeeds(monkeypatch):
+ monkeypatch.setattr(timeout, 'DEFAULT_GATHER_TIMEOUT', 1)
+ # Set sleep_time greater than the default timeout and less than our new timeout
+ sleep_time = 2
+ assert sleep_amount_explicit_higher(sleep_time) == 'Succeeded after {0} sec'.format(sleep_time)
+
+
+def test_explicit_timeout():
+ # Set sleep_time greater than our new timeout but less than the default
+ sleep_time = 3
+ with pytest.raises(timeout.TimeoutError):
+ assert sleep_amount_explicit_lower(sleep_time) == '(Not expected to succeed)'
+
+
+#
+# Test that exception handling works
+#
+
+@timeout.timeout(1)
+def function_times_out():
+ time.sleep(2)
+
+
+# This is just about the same test as function_times_out but uses a separate process which is where
+# we normally have our timeouts. It's more of an integration test than a unit test.
+@timeout.timeout(1)
+def function_times_out_in_run_command(am):
+ am.run_command([sys.executable, '-c', 'import time ; time.sleep(2)'])
+
+
+@timeout.timeout(1)
+def function_other_timeout():
+ raise TimeoutError('Vanilla Timeout')
+
+
+@timeout.timeout(1)
+def function_raises():
+ 1 / 0
+
+
+@timeout.timeout(1)
+def function_catches_all_exceptions():
+ try:
+ time.sleep(10)
+ except BaseException:
+ raise RuntimeError('We should not have gotten here')
+
+
+def test_timeout_raises_timeout():
+ with pytest.raises(timeout.TimeoutError):
+ assert function_times_out() == '(Not expected to succeed)'
+
+
+@pytest.mark.parametrize('stdin', ({},), indirect=['stdin'])
+def test_timeout_raises_timeout_integration_test(am):
+ with pytest.raises(timeout.TimeoutError):
+ assert function_times_out_in_run_command(am) == '(Not expected to succeed)'
+
+
+def test_timeout_raises_other_exception():
+ with pytest.raises(ZeroDivisionError):
+ assert function_raises() == '(Not expected to succeed)'
+
+
+def test_exception_not_caught_by_called_code():
+ with pytest.raises(timeout.TimeoutError):
+ assert function_catches_all_exceptions() == '(Not expected to succeed)'
diff --git a/test/units/module_utils/facts/test_utils.py b/test/units/module_utils/facts/test_utils.py
new file mode 100644
index 0000000..28cb5d3
--- /dev/null
+++ b/test/units/module_utils/facts/test_utils.py
@@ -0,0 +1,39 @@
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from units.compat.mock import patch
+
+from ansible.module_utils.facts import utils
+
+
+class TestGetMountSize(unittest.TestCase):
+ def test(self):
+ mount_info = utils.get_mount_size('/dev/null/not/a/real/mountpoint')
+ self.assertIsInstance(mount_info, dict)
+
+ def test_proc(self):
+ mount_info = utils.get_mount_size('/proc')
+ self.assertIsInstance(mount_info, dict)
+
+ @patch('ansible.module_utils.facts.utils.os.statvfs', side_effect=OSError('intentionally induced os error'))
+ def test_oserror_on_statvfs(self, mock_statvfs):
+ mount_info = utils.get_mount_size('/dev/null/doesnt/matter')
+ self.assertIsInstance(mount_info, dict)
+ self.assertDictEqual(mount_info, {})
diff --git a/test/units/module_utils/facts/virtual/__init__.py b/test/units/module_utils/facts/virtual/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/facts/virtual/__init__.py
diff --git a/test/units/module_utils/facts/virtual/test_linux.py b/test/units/module_utils/facts/virtual/test_linux.py
new file mode 100644
index 0000000..7c13299
--- /dev/null
+++ b/test/units/module_utils/facts/virtual/test_linux.py
@@ -0,0 +1,52 @@
+# -*- 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
+
+from ansible.module_utils.facts.virtual import linux
+
+
+def mock_os_path_is_file_docker(filename):
+ if filename in ('/.dockerenv', '/.dockerinit'):
+ return True
+ return False
+
+
+def test_get_virtual_facts_docker(mocker):
+ mocker.patch('os.path.exists', mock_os_path_is_file_docker)
+
+ module = mocker.Mock()
+ module.run_command.return_value = (0, '', '')
+ inst = linux.LinuxVirtual(module)
+ facts = inst.get_virtual_facts()
+
+ expected = {
+ 'virtualization_role': 'guest',
+ 'virtualization_tech_host': set(),
+ 'virtualization_type': 'docker',
+ 'virtualization_tech_guest': set(['docker', 'container']),
+ }
+
+ assert facts == expected
+
+
+def test_get_virtual_facts_bhyve(mocker):
+ mocker.patch('os.path.exists', return_value=False)
+ mocker.patch('ansible.module_utils.facts.virtual.linux.get_file_content', return_value='')
+ mocker.patch('ansible.module_utils.facts.virtual.linux.get_file_lines', return_value=[])
+
+ module = mocker.Mock()
+ module.run_command.return_value = (0, 'BHYVE\n', '')
+ inst = linux.LinuxVirtual(module)
+
+ facts = inst.get_virtual_facts()
+ expected = {
+ 'virtualization_role': 'guest',
+ 'virtualization_tech_host': set(),
+ 'virtualization_type': 'bhyve',
+ 'virtualization_tech_guest': set(['bhyve']),
+ }
+
+ assert facts == expected
diff --git a/test/units/module_utils/json_utils/__init__.py b/test/units/module_utils/json_utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/json_utils/__init__.py
diff --git a/test/units/module_utils/json_utils/test_filter_non_json_lines.py b/test/units/module_utils/json_utils/test_filter_non_json_lines.py
new file mode 100644
index 0000000..b5b9499
--- /dev/null
+++ b/test/units/module_utils/json_utils/test_filter_non_json_lines.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+# (c) 2016, Matt Davis <mdavis@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from ansible.module_utils.json_utils import _filter_non_json_lines
+
+
+class TestAnsibleModuleExitJson(unittest.TestCase):
+ single_line_json_dict = u"""{"key": "value", "olá": "mundo"}"""
+ single_line_json_array = u"""["a","b","c"]"""
+ multi_line_json_dict = u"""{
+"key":"value"
+}"""
+ multi_line_json_array = u"""[
+"a",
+"b",
+"c"]"""
+
+ all_inputs = [
+ single_line_json_dict,
+ single_line_json_array,
+ multi_line_json_dict,
+ multi_line_json_array
+ ]
+
+ junk = [u"single line of junk", u"line 1/2 of junk\nline 2/2 of junk"]
+
+ unparsable_cases = (
+ u'No json here',
+ u'"olá": "mundo"',
+ u'{"No json": "ending"',
+ u'{"wrong": "ending"]',
+ u'["wrong": "ending"}',
+ )
+
+ def test_just_json(self):
+ for i in self.all_inputs:
+ filtered, warnings = _filter_non_json_lines(i)
+ self.assertEqual(filtered, i)
+ self.assertEqual(warnings, [])
+
+ def test_leading_junk(self):
+ for i in self.all_inputs:
+ for j in self.junk:
+ filtered, warnings = _filter_non_json_lines(j + "\n" + i)
+ self.assertEqual(filtered, i)
+ self.assertEqual(warnings, [])
+
+ def test_trailing_junk(self):
+ for i in self.all_inputs:
+ for j in self.junk:
+ filtered, warnings = _filter_non_json_lines(i + "\n" + j)
+ self.assertEqual(filtered, i)
+ self.assertEqual(warnings, [u"Module invocation had junk after the JSON data: %s" % j.strip()])
+
+ def test_leading_and_trailing_junk(self):
+ for i in self.all_inputs:
+ for j in self.junk:
+ filtered, warnings = _filter_non_json_lines("\n".join([j, i, j]))
+ self.assertEqual(filtered, i)
+ self.assertEqual(warnings, [u"Module invocation had junk after the JSON data: %s" % j.strip()])
+
+ def test_unparsable_filter_non_json_lines(self):
+ for i in self.unparsable_cases:
+ self.assertRaises(
+ ValueError,
+ _filter_non_json_lines,
+ data=i
+ )
diff --git a/test/units/module_utils/parsing/test_convert_bool.py b/test/units/module_utils/parsing/test_convert_bool.py
new file mode 100644
index 0000000..2c5f812
--- /dev/null
+++ b/test/units/module_utils/parsing/test_convert_bool.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2017 Ansible Project
+# License: GNU General Public License v3 or later (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 pytest
+
+from ansible.module_utils.parsing.convert_bool import boolean
+
+
+class TestBoolean:
+ def test_bools(self):
+ assert boolean(True) is True
+ assert boolean(False) is False
+
+ def test_none(self):
+ with pytest.raises(TypeError):
+ assert boolean(None, strict=True) is False
+ assert boolean(None, strict=False) is False
+
+ def test_numbers(self):
+ assert boolean(1) is True
+ assert boolean(0) is False
+ assert boolean(0.0) is False
+
+# Current boolean() doesn't consider these to be true values
+# def test_other_numbers(self):
+# assert boolean(2) is True
+# assert boolean(-1) is True
+# assert boolean(0.1) is True
+
+ def test_strings(self):
+ assert boolean("true") is True
+ assert boolean("TRUE") is True
+ assert boolean("t") is True
+ assert boolean("yes") is True
+ assert boolean("y") is True
+ assert boolean("on") is True
+
+ def test_junk_values_nonstrict(self):
+ assert boolean("flibbity", strict=False) is False
+ assert boolean(42, strict=False) is False
+ assert boolean(42.0, strict=False) is False
+ assert boolean(object(), strict=False) is False
+
+ def test_junk_values_strict(self):
+ with pytest.raises(TypeError):
+ assert boolean("flibbity", strict=True) is False
+
+ with pytest.raises(TypeError):
+ assert boolean(42, strict=True) is False
+
+ with pytest.raises(TypeError):
+ assert boolean(42.0, strict=True) is False
+
+ with pytest.raises(TypeError):
+ assert boolean(object(), strict=True) is False
diff --git a/test/units/module_utils/test_api.py b/test/units/module_utils/test_api.py
new file mode 100644
index 0000000..f7e768a
--- /dev/null
+++ b/test/units/module_utils/test_api.py
@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2020, Abhijeet Kasurde <akasurde@redhat.com>
+# 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.module_utils.api import rate_limit, retry, retry_with_delays_and_condition
+
+import pytest
+
+
+class CustomException(Exception):
+ pass
+
+
+class CustomBaseException(BaseException):
+ pass
+
+
+class TestRateLimit:
+
+ def test_ratelimit(self):
+ @rate_limit(rate=1, rate_limit=1)
+ def login_database():
+ return "success"
+ r = login_database()
+
+ assert r == 'success'
+
+
+class TestRetry:
+
+ def test_no_retry_required(self):
+ @retry(retries=4, retry_pause=2)
+ def login_database():
+ login_database.counter += 1
+ return 'success'
+
+ login_database.counter = 0
+ r = login_database()
+
+ assert r == 'success'
+ assert login_database.counter == 1
+
+ def test_catch_exception(self):
+
+ @retry(retries=1)
+ def login_database():
+ return 'success'
+
+ with pytest.raises(Exception, match="Retry"):
+ login_database()
+
+ def test_no_retries(self):
+
+ @retry()
+ def login_database():
+ assert False, "Should not execute"
+
+ login_database()
+
+
+class TestRetryWithDelaysAndCondition:
+
+ def test_empty_retry_iterator(self):
+ @retry_with_delays_and_condition(backoff_iterator=[])
+ def login_database():
+ login_database.counter += 1
+
+ login_database.counter = 0
+ r = login_database()
+ assert login_database.counter == 1
+
+ def test_no_retry_exception(self):
+ @retry_with_delays_and_condition(
+ backoff_iterator=[1],
+ should_retry_error=lambda x: False,
+ )
+ def login_database():
+ login_database.counter += 1
+ if login_database.counter == 1:
+ raise CustomException("Error")
+
+ login_database.counter = 0
+ with pytest.raises(CustomException, match="Error"):
+ login_database()
+ assert login_database.counter == 1
+
+ def test_no_retry_baseexception(self):
+ @retry_with_delays_and_condition(
+ backoff_iterator=[1],
+ should_retry_error=lambda x: True, # Retry all exceptions inheriting from Exception
+ )
+ def login_database():
+ login_database.counter += 1
+ if login_database.counter == 1:
+ # Raise an exception inheriting from BaseException
+ raise CustomBaseException("Error")
+
+ login_database.counter = 0
+ with pytest.raises(CustomBaseException, match="Error"):
+ login_database()
+ assert login_database.counter == 1
+
+ def test_retry_exception(self):
+ @retry_with_delays_and_condition(
+ backoff_iterator=[1],
+ should_retry_error=lambda x: isinstance(x, CustomException),
+ )
+ def login_database():
+ login_database.counter += 1
+ if login_database.counter == 1:
+ raise CustomException("Retry")
+ return 'success'
+
+ login_database.counter = 0
+ assert login_database() == 'success'
+ assert login_database.counter == 2
diff --git a/test/units/module_utils/test_connection.py b/test/units/module_utils/test_connection.py
new file mode 100644
index 0000000..bd0285b
--- /dev/null
+++ b/test/units/module_utils/test_connection.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2021, Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils import connection
+
+import pytest
+
+
+def test_set_options_credential_exposure():
+ def send(data):
+ return '{'
+
+ c = connection.Connection(connection.__file__)
+ c.send = send
+ with pytest.raises(connection.ConnectionError) as excinfo:
+ c._exec_jsonrpc('set_options', become_pass='password')
+
+ assert 'password' not in str(excinfo.value)
diff --git a/test/units/module_utils/test_distro.py b/test/units/module_utils/test_distro.py
new file mode 100644
index 0000000..bec127a
--- /dev/null
+++ b/test/units/module_utils/test_distro.py
@@ -0,0 +1,39 @@
+
+# (c) 2018 Adrian Likins <alikins@redhat.com>
+# Copyright (c) 2018 Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+# or
+# Apache License v2.0 (see http://www.apache.org/licenses/LICENSE-2.0)
+#
+# Dual licensed so any test cases could potentially be included by the upstream project
+# that module_utils/distro.py is from (https://github.com/nir0s/distro)
+
+
+# Note that nir0s/distro has many more tests in it's test suite. The tests here are
+# primarily for testing the vendoring.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.module_utils import distro
+from ansible.module_utils.six import string_types
+
+
+# Generic test case with minimal assertions about specific returned values.
+class TestDistro():
+ # should run on any platform without errors, even if non-linux without any
+ # useful info to return
+ def test_info(self):
+ info = distro.info()
+ assert isinstance(info, dict), \
+ 'distro.info() returned %s (%s) which is not a dist' % (info, type(info))
+
+ def test_id(self):
+ id = distro.id()
+ assert isinstance(id, string_types), 'distro.id() returned %s (%s) which is not a string' % (id, type(id))
+
+ def test_opensuse_leap_id(self):
+ name = distro.name()
+ if name == 'openSUSE Leap':
+ id = distro.id()
+ assert id == 'opensuse', "OpenSUSE Leap did not return 'opensuse' as id"
diff --git a/test/units/module_utils/urls/__init__.py b/test/units/module_utils/urls/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/module_utils/urls/__init__.py
diff --git a/test/units/module_utils/urls/fixtures/cbt/ecdsa_sha256.pem b/test/units/module_utils/urls/fixtures/cbt/ecdsa_sha256.pem
new file mode 100644
index 0000000..fcc6f7a
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/cbt/ecdsa_sha256.pem
@@ -0,0 +1,12 @@
+-----BEGIN CERTIFICATE-----
+MIIBjzCCATWgAwIBAgIQeNQTxkMgq4BF9tKogIGXUTAKBggqhkjOPQQ
+DAjAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MDMxN1
+oXDTE4MDUzMDA4MjMxN1owFTETMBEGA1UEAwwKU0VSVkVSMjAxNjBZM
+BMGByqGSM49AgEGCCqGSM49AwEHA0IABDAfXTLOaC3ElgErlgk2tBlM
+wf9XmGlGBw4vBtMJap1hAqbsdxFm6rhK3QU8PFFpv8Z/AtRG7ba3UwQ
+prkssClejZzBlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBg
+EFBQcDAgYIKwYBBQUHAwEwFQYDVR0RBA4wDIIKU0VSVkVSMjAxNjAdB
+gNVHQ4EFgQUnFDE8824TYAiBeX4fghEEg33UgYwCgYIKoZIzj0EAwID
+SAAwRQIhAK3rXA4/0i6nm/U7bi6y618Ci2Is8++M3tYIXnEsA7zSAiA
+w2s6bJoI+D7Xaey0Hp0gkks9z55y976keIEI+n3qkzw==
+-----END CERTIFICATE-----
diff --git a/test/units/module_utils/urls/fixtures/cbt/ecdsa_sha512.pem b/test/units/module_utils/urls/fixtures/cbt/ecdsa_sha512.pem
new file mode 100644
index 0000000..1b45be5
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/cbt/ecdsa_sha512.pem
@@ -0,0 +1,12 @@
+-----BEGIN CERTIFICATE-----
+MIIBjjCCATWgAwIBAgIQHVj2AGEwd6pOOSbcf0skQDAKBggqhkjOPQQ
+DBDAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA3NTUzOV
+oXDTE4MDUzMDA4MTUzOVowFTETMBEGA1UEAwwKU0VSVkVSMjAxNjBZM
+BMGByqGSM49AgEGCCqGSM49AwEHA0IABL8d9S++MFpfzeH8B3vG/PjA
+AWg8tGJVgsMw9nR+OfC9ltbTUwhB+yPk3JPcfW/bqsyeUgq4//LhaSp
+lOWFNaNqjZzBlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBg
+EFBQcDAgYIKwYBBQUHAwEwFQYDVR0RBA4wDIIKU0VSVkVSMjAxNjAdB
+gNVHQ4EFgQUKUkCgLlxoeai0EtQrZth1/BSc5kwCgYIKoZIzj0EAwQD
+RwAwRAIgRrV7CLpDG7KueyFA3ZDced9dPOcv2Eydx/hgrfxYEcYCIBQ
+D35JvzmqU05kSFV5eTvkhkaDObd7V55vokhm31+Li
+-----END CERTIFICATE-----
diff --git a/test/units/module_utils/urls/fixtures/cbt/rsa-pss_sha256.pem b/test/units/module_utils/urls/fixtures/cbt/rsa-pss_sha256.pem
new file mode 100644
index 0000000..fcbe01f
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/cbt/rsa-pss_sha256.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDZDCCAhugAwIBAgIUbo9YpgkeniC5jRmOgFYX3mcVJn4wPgYJKoZIhvcNAQEK
+MDGgDTALBglghkgBZQMEAgGhGjAYBgkqhkiG9w0BAQgwCwYJYIZIAWUDBAIBogQC
+AgDeMBMxETAPBgNVBAMMCE9NSSBSb290MB4XDTIwMDkwNDE4NTMyNloXDTIxMDkw
+NDE4NTMyNlowGDEWMBQGA1UEAwwNREMwMS5vbWkudGVzdDCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBANN++3POgcKcPILMdHWIEPiVENtKoJQ8iqKKeL+/
+j5oUULVuIn15H/RYMNFmStRIvj0dIL1JAq4W411wG2Tf/6niU2YSKPOAOtrVREef
+gNvMZ06TYlC8UcGCLv4dBkU3q/FELV66lX9x6LcVwf2f8VWfDg4VNuwyg/eQUIgc
+/yd5VV+1VXTf39QufVV+/hOtPptu+fBKOIuiuKm6FIqroqLri0Ysp6tWrSd7P6V4
+6zT2yd17981vaEv5Zek2t39PoLYzJb3rvqQmumgFBIUQ1eMPLFCXX8YYYC/9ByK3
+mdQaEnkD2eIOARLnojr2A228EgPpdM8phQkDzeWeYnhLiysCAwEAAaNJMEcwCQYD
+VR0TBAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGAYDVR0R
+BBEwD4INREMwMS5vbWkudGVzdDA+BgkqhkiG9w0BAQowMaANMAsGCWCGSAFlAwQC
+AaEaMBgGCSqGSIb3DQEBCDALBglghkgBZQMEAgGiBAICAN4DggEBAA66cbtiysjq
+sCaDiYyRWCS9af2DGxJ6NAyku2aX+xgmRQzUzFAN5TihcPau+zzpk2zQKHDVMhNx
+ouhTkIe6mq9KegpUuesWwkJ5KEeuBT949tIru2wAtlSVDvDcau5T9pRI/RLlnsWg
+0sWaUAh/ujL+VKk6AanC4MRV69llwJcAVxlS/tYjwC74Dr8tMT1TQcVDvywB85e9
+mA3uz8mGKfiMk2TKD6+6on0UwBMB8NbKSB5bcgp+CJ2ceeblmCOgOcOcV5PCGoFj
+fgAppr7HjfNPYaIV5l59LfKo2Bj9kXPMqA6/D4gJ3hwoJdY/NOtuNyk8cxWMnWUe
++E2Mm6ZnB3Y=
+-----END CERTIFICATE-----
diff --git a/test/units/module_utils/urls/fixtures/cbt/rsa-pss_sha512.pem b/test/units/module_utils/urls/fixtures/cbt/rsa-pss_sha512.pem
new file mode 100644
index 0000000..add3109
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/cbt/rsa-pss_sha512.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDZDCCAhugAwIBAgIUbo9YpgkeniC5jRmOgFYX3mcVJoEwPgYJKoZIhvcNAQEK
+MDGgDTALBglghkgBZQMEAgOhGjAYBgkqhkiG9w0BAQgwCwYJYIZIAWUDBAIDogQC
+AgC+MBMxETAPBgNVBAMMCE9NSSBSb290MB4XDTIwMDkwNDE4NTMyN1oXDTIxMDkw
+NDE4NTMyN1owGDEWMBQGA1UEAwwNREMwMS5vbWkudGVzdDCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBANZMAyRDBn54RfveeVtikepqsyKVKooAc471snl5
+mEEeo6ZvlOrK1VGGmo/VlF4R9iW6f5iqxG792KXk+lDtx8sbapZWk/aQa+6I9wml
+p17ocW4Otl7XyQ74UTQlxmrped0rgOk+I2Wu3IC7k64gmf/ZbL9mYN/+v8TlYYyO
+l8DQbO61XWOJpWt7yf18OxZtPcHH0dkoTEyIxIQcv6FDFNvPjmJzubpDgsfnly7R
+C0Rc2yPU702vmAfF0SGQbd6KoXUqlfy26C85vU0Fqom1Qo22ehKrfU50vZrXdaJ2
+gX14pm2kuubMjHtX/+bhNyWTxq4anCOl9/aoObZCM1D3+Y8CAwEAAaNJMEcwCQYD
+VR0TBAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGAYDVR0R
+BBEwD4INREMwMS5vbWkudGVzdDA+BgkqhkiG9w0BAQowMaANMAsGCWCGSAFlAwQC
+A6EaMBgGCSqGSIb3DQEBCDALBglghkgBZQMEAgOiBAICAL4DggEBAHgTDTn8onIi
+XFLZ3sWJ5xCBvXypqC37dKALvXxNSo75SQZpOioG4GSdW3zjJWCiudGs7BselkFv
+sHK7+5sLKTl1RvxeUoyTxnPZZmVlD3yLq8JBPxu5NScvcRwAcgm3stkq0irRnh7M
+4Clw6oSKCKI7Lc3gnbvR3QLSYHeZpUcQgVCad6O/Hi+vxFMJT8PVigG0YUoTW010
+pDpi5uh18RxCqRJnnEC7aDrVarxD9aAvqp1wqwWShfP4FZ9m57DH81RTGD2ZzGgP
+MsZU5JHVYKkO7IKKIBKuLu+O+X2aZZ4OMlMNBt2DUIJGzEBYV41+3TST9bBPD8xt
+AAIFCBcgUYY=
+-----END CERTIFICATE-----
diff --git a/test/units/module_utils/urls/fixtures/cbt/rsa_md5.pem b/test/units/module_utils/urls/fixtures/cbt/rsa_md5.pem
new file mode 100644
index 0000000..6671b73
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/cbt/rsa_md5.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDGzCCAgOgAwIBAgIQJzshhViMG5hLHIJHxa+TcTANBgkqhkiG9w0
+BAQQFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD
+MxNloXDTE4MDUzMDA4MjMxNlowFTETMBEGA1UEAwwKU0VSVkVSMjAxN
+jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN9N5GAzI7uq
+AVlI6vUqhY5+EZWCWWGRwR3FT2DEXE5++AiJxXO0i0ZfAkLu7UggtBe
+QwVNkaPD27EYzVUhy1iDo37BrFcLNpfjsjj8wVjaSmQmqvLvrvEh/BT
+C5SBgDrk2+hiMh9PrpJoB3QAMDinz5aW0rEXMKitPBBiADrczyYrliF
+AlEU6pTlKEKDUAeP7dKOBlDbCYvBxKnR3ddVH74I5T2SmNBq5gzkbKP
+nlCXdHLZSh74USu93rKDZQF8YzdTO5dcBreJDJsntyj1o49w9WCt6M7
++pg6vKvE+tRbpCm7kXq5B9PDi42Nb6//MzNaMYf9V7v5MHapvVSv3+y
+sCAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
+QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
+A1UdDgQWBBTh4L2Clr9ber6yfY3JFS3wiECL4DANBgkqhkiG9w0BAQQ
+FAAOCAQEA0JK/SL7SP9/nvqWp52vnsxVefTFehThle5DLzagmms/9gu
+oSE2I9XkQIttFMprPosaIZWt7WP42uGcZmoZOzU8kFFYJMfg9Ovyca+
+gnG28jDUMF1E74KrC7uynJiQJ4vPy8ne7F3XJ592LsNJmK577l42gAW
+u08p3TvEJFNHy2dBk/IwZp0HIPr9+JcPf7v0uL6lK930xHJHP56XLzN
+YG8vCMpJFR7wVZp3rXkJQUy3GxyHPJPjS8S43I9j+PoyioWIMEotq2+
+q0IpXU/KeNFkdGV6VPCmzhykijExOMwO6doUzIUM8orv9jYLHXYC+i6
+IFKSb6runxF1MAik+GCSA==
+-----END CERTIFICATE-----
diff --git a/test/units/module_utils/urls/fixtures/cbt/rsa_sha.pem b/test/units/module_utils/urls/fixtures/cbt/rsa_sha.pem
new file mode 100644
index 0000000..2ed2b45
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/cbt/rsa_sha.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDGzCCAgOgAwIBAgIQUDHcKGevZohJV+TkIIYC1DANBgkqhkiG9w0
+BAQ0FADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD
+MxN1oXDTE4MDUzMDA4MjMxN1owFTETMBEGA1UEAwwKU0VSVkVSMjAxN
+jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKr9bo/XXvHt
+D6Qnhb1wyLg9lDQxxe/enH49LQihtVTZMwGf2010h81QrRUe/bkHTvw
+K22s2lqj3fUpGxtEbYFWLAHxv6IFnIKd+Zi1zaCPGfas9ekqCSj3vZQ
+j7lCJVGUGuuqnSDvsed6g2Pz/g6mJUa+TzjxN+8wU5oj5YVUK+aing1
+zPSA2MDCfx3+YzjxVwNoGixOz6Yx9ijT4pUsAYQAf1o9R+6W1/IpGgu
+oax714QILT9heqIowwlHzlUZc1UAYs0/JA4CbDZaw9hlJyzMqe/aE46
+efqPDOpO3vCpOSRcSyzh02WijPvEEaPejQRWg8RX93othZ615MT7dqp
+ECAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
+QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
+A1UdDgQWBBTgod3R6vejt6kOASAApA19xIG6kTANBgkqhkiG9w0BAQ0
+FAAOCAQEAVfz0okK2bh3OQE8cWNbJ5PjJRSAJEqVUvYaTlS0Nqkyuaj
+gicP3hb/pF8FvaVaB6r7LqgBxyW5NNL1xwdNLt60M2zaULL6Fhm1vzM
+sSMc2ynkyN4++ODwii674YcQAnkUh+ZGIx+CTdZBWJfVM9dZb7QjgBT
+nVukeFwN2EOOBSpiQSBpcoeJEEAq9csDVRhEfcB8Wtz7TTItgOVsilY
+dQY56ON5XszjCki6UA3GwdQbBEHjWF2WERqXWrojrSSNOYDvxM5mrEx
+sG1npzUTsaIr9w8ty1beh/2aToCMREvpiPFOXnVV/ovHMU1lFQTNeQ0
+OI7elR0nJ0peai30eMpQQ=='
+-----END CERTIFICATE-----
diff --git a/test/units/module_utils/urls/fixtures/cbt/rsa_sha1.pem b/test/units/module_utils/urls/fixtures/cbt/rsa_sha1.pem
new file mode 100644
index 0000000..de21a67
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/cbt/rsa_sha1.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDGzCCAgOgAwIBAgIQJg/Mf5sR55xApJRK+kabbTANBgkqhkiG9w0
+BAQUFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD
+MxNloXDTE4MDUzMDA4MjMxNlowFTETMBEGA1UEAwwKU0VSVkVSMjAxN
+jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPKwYikjbzL
+Lo6JtS6cyytdMMjSrggDoTnRUKauC5/izoYJd+2YVR5YqnluBJZpoFp
+hkCgFFohUOU7qUsI1SkuGnjI8RmWTrrDsSy62BrfX+AXkoPlXo6IpHz
+HaEPxjHJdUACpn8QVWTPmdAhwTwQkeUutrm3EOVnKPX4bafNYeAyj7/
+AGEplgibuXT4/ehbzGKOkRN3ds/pZuf0xc4Q2+gtXn20tQIUt7t6iwh
+nEWjIgopFL/hX/r5q5MpF6stc1XgIwJjEzqMp76w/HUQVqaYneU4qSG
+f90ANK/TQ3aDbUNtMC/ULtIfHqHIW4POuBYXaWBsqalJL2VL3YYkKTU
+sCAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
+QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
+A1UdDgQWBBS1jgojcjPu9vqeP1uSKuiIonGwAjANBgkqhkiG9w0BAQU
+FAAOCAQEAKjHL6k5Dv/Zb7dvbYEZyx0wVhjHkCTpT3xstI3+TjfAFsu
+3zMmyFqFqzmr4pWZ/rHc3ObD4pEa24kP9hfB8nmr8oHMLebGmvkzh5h
+0GYc4dIH7Ky1yfQN51hi7/X5iN7jnnBoCJTTlgeBVYDOEBXhfXi3cLT
+u3d7nz2heyNq07gFP8iN7MfqdPZndVDYY82imLgsgar9w5d+fvnYM+k
+XWItNNCUH18M26Obp4Es/Qogo/E70uqkMHost2D+tww/7woXi36X3w/
+D2yBDyrJMJKZLmDgfpNIeCimncTOzi2IhzqJiOY/4XPsVN/Xqv0/dzG
+TDdI11kPLq4EiwxvPanCg==
+-----END CERTIFICATE-----
diff --git a/test/units/module_utils/urls/fixtures/cbt/rsa_sha256.pem b/test/units/module_utils/urls/fixtures/cbt/rsa_sha256.pem
new file mode 100644
index 0000000..fb17018
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/cbt/rsa_sha256.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDGzCCAgOgAwIBAgIQWkeAtqoFg6pNWF7xC4YXhTANBgkqhkiG9w0
+BAQsFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUyNzA5MD
+I0NFoXDTE4MDUyNzA5MjI0NFowFTETMBEGA1UEAwwKU0VSVkVSMjAxN
+jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALIPKM5uykFy
+NmVoLyvPSXGk15ZDqjYi3AbUxVFwCkVImqhefLATit3PkTUYFtAT+TC
+AwK2E4lOu1XHM+Tmp2KIOnq2oUR8qMEvfxYThEf1MHxkctFljFssZ9N
+vASDD4lzw8r0Bhl+E5PhR22Eu1Wago5bvIldojkwG+WBxPQv3ZR546L
+MUZNaBXC0RhuGj5w83lbVz75qM98wvv1ekfZYAP7lrVyHxqCTPDomEU
+I45tQQZHCZl5nRx1fPCyyYfcfqvFlLWD4Q3PZAbnw6mi0MiWJbGYKME
+1XGicjqyn/zM9XKA1t/JzChS2bxf6rsyA9I7ibdRHUxsm1JgKry2jfW
+0CAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
+QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
+A1UdDgQWBBQabLGWg1sn7AXPwYPyfE0ER921ZDANBgkqhkiG9w0BAQs
+FAAOCAQEAnRohyl6ZmOsTWCtxOJx5A8yr//NweXKwWWmFQXRmCb4bMC
+xhD4zqLDf5P6RotGV0I/SHvqz+pAtJuwmr+iyAF6WTzo3164LCfnQEu
+psfrrfMkf3txgDwQkA0oPAw3HEwOnR+tzprw3Yg9x6UoZEhi4XqP9AX
+R49jU92KrNXJcPlz5MbkzNo5t9nr2f8q39b5HBjaiBJxzdM1hxqsbfD
+KirTYbkUgPlVOo/NDmopPPb8IX8ubj/XETZG2jixD0zahgcZ1vdr/iZ
++50WSXKN2TAKBO2fwoK+2/zIWrGRxJTARfQdF+fGKuj+AERIFNh88HW
+xSDYjHQAaFMcfdUpa9GGQ==
+-----END CERTIFICATE-----
diff --git a/test/units/module_utils/urls/fixtures/cbt/rsa_sha384.pem b/test/units/module_utils/urls/fixtures/cbt/rsa_sha384.pem
new file mode 100644
index 0000000..c17f9ff
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/cbt/rsa_sha384.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDGzCCAgOgAwIBAgIQEmj1prSSQYRL2zYBEjsm5jANBgkqhkiG9w0
+BAQwFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD
+MxN1oXDTE4MDUzMDA4MjMxN1owFTETMBEGA1UEAwwKU0VSVkVSMjAxN
+jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsK5NvHi4xO
+081fRLMmPqKsKaHvXgPRykLA0SmKxpGJHfTAZzxojHVeVwOm87IvQj2
+JUh/yrRwSi5Oqrvqx29l2IC/qQt2xkAQsO51/EWkMQ5OSJsl1MN3NXW
+eRTKVoUuJzBs8XLmeraxQcBPyyLhq+WpMl/Q4ZDn1FrUEZfxV0POXgU
+dI3ApuQNRtJOb6iteBIoQyMlnof0RswBUnkiWCA/+/nzR0j33j47IfL
+nkmU4RtqkBlO13f6+e1GZ4lEcQVI2yZq4Zgu5VVGAFU2lQZ3aEVMTu9
+8HEqD6heyNp2on5G/K/DCrGWYCBiASjnX3wiSz0BYv8f3HhCgIyVKhJ
+8CAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
+QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
+A1UdDgQWBBQS/SI61S2UE8xwSgHxbkCTpZXo4TANBgkqhkiG9w0BAQw
+FAAOCAQEAMVV/WMXd9w4jtDfSrIsKaWKGtHtiMPpAJibXmSakBRwLOn
+5ZGXL2bWI/Ac2J2Y7bSzs1im2ifwmEqwzzqnpVKShIkZmtij0LS0SEr
+6Fw5IrK8tD6SH+lMMXUTvp4/lLQlgRCwOWxry/YhQSnuprx8IfSPvil
+kwZ0Ysim4Aa+X5ojlhHpWB53edX+lFrmR1YWValBnQ5DvnDyFyLR6II
+Ialp4vmkzI9e3/eOgSArksizAhpXpC9dxQBiHXdhredN0X+1BVzbgzV
+hQBEwgnAIPa+B68oDILaV0V8hvxrP6jFM4IrKoGS1cq0B+Ns0zkG7ZA
+2Q0W+3nVwSxIr6bd6hw7g==
+-----END CERTIFICATE-----
diff --git a/test/units/module_utils/urls/fixtures/cbt/rsa_sha512.pem b/test/units/module_utils/urls/fixtures/cbt/rsa_sha512.pem
new file mode 100644
index 0000000..2ed2b45
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/cbt/rsa_sha512.pem
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDGzCCAgOgAwIBAgIQUDHcKGevZohJV+TkIIYC1DANBgkqhkiG9w0
+BAQ0FADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD
+MxN1oXDTE4MDUzMDA4MjMxN1owFTETMBEGA1UEAwwKU0VSVkVSMjAxN
+jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKr9bo/XXvHt
+D6Qnhb1wyLg9lDQxxe/enH49LQihtVTZMwGf2010h81QrRUe/bkHTvw
+K22s2lqj3fUpGxtEbYFWLAHxv6IFnIKd+Zi1zaCPGfas9ekqCSj3vZQ
+j7lCJVGUGuuqnSDvsed6g2Pz/g6mJUa+TzjxN+8wU5oj5YVUK+aing1
+zPSA2MDCfx3+YzjxVwNoGixOz6Yx9ijT4pUsAYQAf1o9R+6W1/IpGgu
+oax714QILT9heqIowwlHzlUZc1UAYs0/JA4CbDZaw9hlJyzMqe/aE46
+efqPDOpO3vCpOSRcSyzh02WijPvEEaPejQRWg8RX93othZ615MT7dqp
+ECAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
+QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
+A1UdDgQWBBTgod3R6vejt6kOASAApA19xIG6kTANBgkqhkiG9w0BAQ0
+FAAOCAQEAVfz0okK2bh3OQE8cWNbJ5PjJRSAJEqVUvYaTlS0Nqkyuaj
+gicP3hb/pF8FvaVaB6r7LqgBxyW5NNL1xwdNLt60M2zaULL6Fhm1vzM
+sSMc2ynkyN4++ODwii674YcQAnkUh+ZGIx+CTdZBWJfVM9dZb7QjgBT
+nVukeFwN2EOOBSpiQSBpcoeJEEAq9csDVRhEfcB8Wtz7TTItgOVsilY
+dQY56ON5XszjCki6UA3GwdQbBEHjWF2WERqXWrojrSSNOYDvxM5mrEx
+sG1npzUTsaIr9w8ty1beh/2aToCMREvpiPFOXnVV/ovHMU1lFQTNeQ0
+OI7elR0nJ0peai30eMpQQ=='
+-----END CERTIFICATE-----
diff --git a/test/units/module_utils/urls/fixtures/client.key b/test/units/module_utils/urls/fixtures/client.key
new file mode 100644
index 0000000..0e90d95
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/client.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDTyiVxrsSyZ+Qr
+iMT6sFYCqQtkLqlIWfbpTg9B6fZc793uoMzLUGq3efiZUhhxI78dQ3gNPgs1sK3W
+heFpk1n4IL8ll1MS1uJKk2vYqzZVhjgcvQpeV9gm7bt0ndPzGj5h4fh7proPntSy
+eBvMKVoqTT7tEnapRKy3anbwRPgTt7B5jEvJkPazuIc+ooMsYOHWfvj4oVsev0N2
+SsP0o6cHcsRujFMhz/JTJ1STQxacaVuyKpXacX7Eu1MJgGt/jU/QKNREcV9LdneO
+NgqY9tNv0h+9s7DfHYXm8U3POr+bdcW6Yy4791KGCaUNtiNqT1lvu/4yd4WRkXbF
+Fm5hJUUpAgMBAAECggEBAJYOac1MSK0nEvENbJM6ERa9cwa+UM6kf176IbFP9XAP
+u6zxXWjIR3RMBSmMkyjGbQhs30hypzqZPfH61aUZ8+rsOMKHnyKAAcFZBlZzqIGc
+IXGrNwd1Mf8S/Xg4ww1BkOWFV6s0jCu5G3Z/xyI2Ql4qcOVD6bMwpzclRbQjCand
+dvqyCdMD0sRDyeOIK5hBhUY60JnWbMCu6pBU+qPoRukbRieaeDLIN1clwEqIQV78
+LLnv4n9fuGozH0JdHHfyXFytCgIJvEspZUja/5R4orADhr3ZB010RLzYvs2ndE3B
+4cF9RgxspJZeJ/P+PglViZuzj37pXy+7GAcJLR9ka4kCgYEA/l01XKwkCzMgXHW4
+UPgl1+on42BsN7T9r3S5tihOjHf4ZJWkgYzisLVX+Nc1oUI3HQfM9PDJZXMMNm7J
+ZRvERcopU26wWqr6CFPblGv8oqXHqcpeta8i3xZKoPASsTW6ssuPCEajiLZbQ1rH
+H/HP+OZIVLM/WCPgA2BckTU9JnsCgYEA1SbXllXnlwGqmjitmY1Z07rUxQ3ah/fB
+iccbbg3E4onontYXIlI5zQms3u+qBdi0ZuwaDm5Y4BetOq0a3UyxAsugqVFnzTba
+1w/sFb3fw9KeQ/il4CXkbq87nzJfDmEyqHGCCYXbijHBxnq99PkqwVpaAhHHEW0m
+vWyMUvPRY6sCgYAbtUWR0cKfYbNdvwkT8OQWcBBmSWOgcdvMmBd+y0c7L/pj4pUn
+85PiEe8CUVcrOM5OIEJoUC5wGacz6r+PfwXTYGE+EGmvhr5z18aslVLQ2OQ2D7Bf
+dDOFP6VjgKNYoHS0802iZid8RfkNDj9wsGOqRlOMvnXhAQ9u7rlGrBj8LwKBgFfo
+ph99nH8eE9N5LrfWoUZ+loQS258aInsFYB26lgnsYMEpgO8JxIb4x5BGffPdVUHh
+fDmZbxQ1D5/UhvDgUVzayI8sYMg1KHpsOa0Z2zCzK8zSvu68EgNISCm3J5cRpUft
+UHlG+K19KfMG6lMfdG+8KMUTuetI/iI/o3wOzLvzAoGAIrOh30rHt8wit7ELARyx
+wPkp2ARYXrKfX3NES4c67zSAi+3dCjxRqywqTI0gLicyMlj8zEu9YE9Ix/rl8lRZ
+nQ9LZmqv7QHzhLTUCPGgZYnemvBzo7r0eW8Oag52dbcJO6FBszfWrxskm/fX25Rb
+WPxih2vdRy814dNPW25rgdw=
+-----END PRIVATE KEY-----
diff --git a/test/units/module_utils/urls/fixtures/client.pem b/test/units/module_utils/urls/fixtures/client.pem
new file mode 100644
index 0000000..c8c7b82
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/client.pem
@@ -0,0 +1,81 @@
+Certificate:
+ Data:
+ Version: 3 (0x2)
+ Serial Number: 4099 (0x1003)
+ Signature Algorithm: sha256WithRSAEncryption
+ Issuer: C=US, ST=North Carolina, L=Durham, O=Ansible, CN=ansible.http.tests
+ Validity
+ Not Before: Mar 21 18:22:47 2018 GMT
+ Not After : Mar 18 18:22:47 2028 GMT
+ Subject: C=US, ST=North Carolina, O=Ansible, CN=client.ansible.http.tests
+ Subject Public Key Info:
+ Public Key Algorithm: rsaEncryption
+ Public-Key: (2048 bit)
+ Modulus:
+ 00:d3:ca:25:71:ae:c4:b2:67:e4:2b:88:c4:fa:b0:
+ 56:02:a9:0b:64:2e:a9:48:59:f6:e9:4e:0f:41:e9:
+ f6:5c:ef:dd:ee:a0:cc:cb:50:6a:b7:79:f8:99:52:
+ 18:71:23:bf:1d:43:78:0d:3e:0b:35:b0:ad:d6:85:
+ e1:69:93:59:f8:20:bf:25:97:53:12:d6:e2:4a:93:
+ 6b:d8:ab:36:55:86:38:1c:bd:0a:5e:57:d8:26:ed:
+ bb:74:9d:d3:f3:1a:3e:61:e1:f8:7b:a6:ba:0f:9e:
+ d4:b2:78:1b:cc:29:5a:2a:4d:3e:ed:12:76:a9:44:
+ ac:b7:6a:76:f0:44:f8:13:b7:b0:79:8c:4b:c9:90:
+ f6:b3:b8:87:3e:a2:83:2c:60:e1:d6:7e:f8:f8:a1:
+ 5b:1e:bf:43:76:4a:c3:f4:a3:a7:07:72:c4:6e:8c:
+ 53:21:cf:f2:53:27:54:93:43:16:9c:69:5b:b2:2a:
+ 95:da:71:7e:c4:bb:53:09:80:6b:7f:8d:4f:d0:28:
+ d4:44:71:5f:4b:76:77:8e:36:0a:98:f6:d3:6f:d2:
+ 1f:bd:b3:b0:df:1d:85:e6:f1:4d:cf:3a:bf:9b:75:
+ c5:ba:63:2e:3b:f7:52:86:09:a5:0d:b6:23:6a:4f:
+ 59:6f:bb:fe:32:77:85:91:91:76:c5:16:6e:61:25:
+ 45:29
+ Exponent: 65537 (0x10001)
+ X509v3 extensions:
+ X509v3 Basic Constraints:
+ CA:FALSE
+ Netscape Comment:
+ OpenSSL Generated Certificate
+ X509v3 Subject Key Identifier:
+ AF:F3:E5:2A:EB:CF:C7:7E:A4:D6:49:92:F9:29:EE:6A:1B:68:AB:0F
+ X509v3 Authority Key Identifier:
+ keyid:13:2E:30:F0:04:EA:41:5F:B7:08:BD:34:31:D7:11:EA:56:A6:99:F0
+
+ Signature Algorithm: sha256WithRSAEncryption
+ 29:62:39:25:79:58:eb:a4:b3:0c:ea:aa:1d:2b:96:7c:6e:10:
+ ce:16:07:b7:70:7f:16:da:fd:20:e6:a2:d9:b4:88:e0:f9:84:
+ 87:f8:b0:0d:77:8b:ae:27:f5:ee:e6:4f:86:a1:2d:74:07:7c:
+ c7:5d:c2:bd:e4:70:e7:42:e4:14:ee:b9:b7:63:b8:8c:6d:21:
+ 61:56:0b:96:f6:15:ba:7a:ae:80:98:ac:57:99:79:3d:7a:a9:
+ d8:26:93:30:17:53:7c:2d:02:4b:64:49:25:65:e7:69:5a:08:
+ cf:84:94:8e:6a:42:a7:d1:4f:ba:39:4b:7c:11:67:31:f7:1b:
+ 2b:cd:79:c2:28:4d:d9:88:66:d6:7f:56:4c:4b:37:d1:3d:a8:
+ d9:4a:6b:45:1d:4d:a7:12:9f:29:77:6a:55:c1:b5:1d:0e:a5:
+ b9:4f:38:16:3c:7d:85:ae:ff:23:34:c7:2c:f6:14:0f:55:ef:
+ b8:00:89:f1:b2:8a:75:15:41:81:72:d0:43:a6:86:d1:06:e6:
+ ce:81:7e:5f:33:e6:f4:19:d6:70:00:ba:48:6e:05:fd:4c:3c:
+ c3:51:1b:bd:43:1a:24:c5:79:ea:7a:f0:85:a5:40:10:85:e9:
+ 23:09:09:80:38:9d:bc:81:5e:59:8c:5a:4d:58:56:b9:71:c2:
+ 78:cd:f3:b0
+-----BEGIN CERTIFICATE-----
+MIIDuTCCAqGgAwIBAgICEAMwDQYJKoZIhvcNAQELBQAwZjELMAkGA1UEBhMCVVMx
+FzAVBgNVBAgMDk5vcnRoIENhcm9saW5hMQ8wDQYDVQQHDAZEdXJoYW0xEDAOBgNV
+BAoMB0Fuc2libGUxGzAZBgNVBAMMEmFuc2libGUuaHR0cC50ZXN0czAeFw0xODAz
+MjExODIyNDdaFw0yODAzMTgxODIyNDdaMFwxCzAJBgNVBAYTAlVTMRcwFQYDVQQI
+DA5Ob3J0aCBDYXJvbGluYTEQMA4GA1UECgwHQW5zaWJsZTEiMCAGA1UEAwwZY2xp
+ZW50LmFuc2libGUuaHR0cC50ZXN0czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBANPKJXGuxLJn5CuIxPqwVgKpC2QuqUhZ9ulOD0Hp9lzv3e6gzMtQard5
++JlSGHEjvx1DeA0+CzWwrdaF4WmTWfggvyWXUxLW4kqTa9irNlWGOBy9Cl5X2Cbt
+u3Sd0/MaPmHh+Humug+e1LJ4G8wpWipNPu0SdqlErLdqdvBE+BO3sHmMS8mQ9rO4
+hz6igyxg4dZ++PihWx6/Q3ZKw/SjpwdyxG6MUyHP8lMnVJNDFpxpW7IqldpxfsS7
+UwmAa3+NT9Ao1ERxX0t2d442Cpj202/SH72zsN8dhebxTc86v5t1xbpjLjv3UoYJ
+pQ22I2pPWW+7/jJ3hZGRdsUWbmElRSkCAwEAAaN7MHkwCQYDVR0TBAIwADAsBglg
+hkgBhvhCAQ0EHxYdT3BlblNTTCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0O
+BBYEFK/z5Srrz8d+pNZJkvkp7mobaKsPMB8GA1UdIwQYMBaAFBMuMPAE6kFftwi9
+NDHXEepWppnwMA0GCSqGSIb3DQEBCwUAA4IBAQApYjkleVjrpLMM6qodK5Z8bhDO
+Fge3cH8W2v0g5qLZtIjg+YSH+LANd4uuJ/Xu5k+GoS10B3zHXcK95HDnQuQU7rm3
+Y7iMbSFhVguW9hW6eq6AmKxXmXk9eqnYJpMwF1N8LQJLZEklZedpWgjPhJSOakKn
+0U+6OUt8EWcx9xsrzXnCKE3ZiGbWf1ZMSzfRPajZSmtFHU2nEp8pd2pVwbUdDqW5
+TzgWPH2Frv8jNMcs9hQPVe+4AInxsop1FUGBctBDpobRBubOgX5fM+b0GdZwALpI
+bgX9TDzDURu9QxokxXnqevCFpUAQhekjCQmAOJ28gV5ZjFpNWFa5ccJ4zfOw
+-----END CERTIFICATE-----
diff --git a/test/units/module_utils/urls/fixtures/client.txt b/test/units/module_utils/urls/fixtures/client.txt
new file mode 100644
index 0000000..380330f
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/client.txt
@@ -0,0 +1,3 @@
+client.pem and client.key were retrieved from httptester docker image:
+
+ansible/ansible@sha256:fa5def8c294fc50813af131c0b5737594d852abac9cbe7ba38e17bf1c8476f3f
diff --git a/test/units/module_utils/urls/fixtures/multipart.txt b/test/units/module_utils/urls/fixtures/multipart.txt
new file mode 100644
index 0000000..c80a1b8
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/multipart.txt
@@ -0,0 +1,166 @@
+--===============3996062709511591449==
+Content-Type: text/plain
+Content-Disposition: form-data; name="file1"; filename="fake_file1.txt"
+
+file_content_1
+--===============3996062709511591449==
+Content-Type: text/html
+Content-Disposition: form-data; name="file2"; filename="fake_file2.html"
+
+<html></html>
+--===============3996062709511591449==
+Content-Type: application/json
+Content-Disposition: form-data; name="file3"; filename="fake_file3.json"
+
+{"foo": "bar"}
+--===============3996062709511591449==
+Content-Transfer-Encoding: base64
+Content-Type: text/plain
+Content-Disposition: form-data; name="file4"; filename="client.pem"
+
+Q2VydGlmaWNhdGU6CiAgICBEYXRhOgogICAgICAgIFZlcnNpb246IDMgKDB4MikKICAgICAgICBT
+ZXJpYWwgTnVtYmVyOiA0MDk5ICgweDEwMDMpCiAgICBTaWduYXR1cmUgQWxnb3JpdGhtOiBzaGEy
+NTZXaXRoUlNBRW5jcnlwdGlvbgogICAgICAgIElzc3VlcjogQz1VUywgU1Q9Tm9ydGggQ2Fyb2xp
+bmEsIEw9RHVyaGFtLCBPPUFuc2libGUsIENOPWFuc2libGUuaHR0cC50ZXN0cwogICAgICAgIFZh
+bGlkaXR5CiAgICAgICAgICAgIE5vdCBCZWZvcmU6IE1hciAyMSAxODoyMjo0NyAyMDE4IEdNVAog
+ICAgICAgICAgICBOb3QgQWZ0ZXIgOiBNYXIgMTggMTg6MjI6NDcgMjAyOCBHTVQKICAgICAgICBT
+dWJqZWN0OiBDPVVTLCBTVD1Ob3J0aCBDYXJvbGluYSwgTz1BbnNpYmxlLCBDTj1jbGllbnQuYW5z
+aWJsZS5odHRwLnRlc3RzCiAgICAgICAgU3ViamVjdCBQdWJsaWMgS2V5IEluZm86CiAgICAgICAg
+ICAgIFB1YmxpYyBLZXkgQWxnb3JpdGhtOiByc2FFbmNyeXB0aW9uCiAgICAgICAgICAgICAgICBQ
+dWJsaWMtS2V5OiAoMjA0OCBiaXQpCiAgICAgICAgICAgICAgICBNb2R1bHVzOgogICAgICAgICAg
+ICAgICAgICAgIDAwOmQzOmNhOjI1OjcxOmFlOmM0OmIyOjY3OmU0OjJiOjg4OmM0OmZhOmIwOgog
+ICAgICAgICAgICAgICAgICAgIDU2OjAyOmE5OjBiOjY0OjJlOmE5OjQ4OjU5OmY2OmU5OjRlOjBm
+OjQxOmU5OgogICAgICAgICAgICAgICAgICAgIGY2OjVjOmVmOmRkOmVlOmEwOmNjOmNiOjUwOjZh
+OmI3Ojc5OmY4Ojk5OjUyOgogICAgICAgICAgICAgICAgICAgIDE4OjcxOjIzOmJmOjFkOjQzOjc4
+OjBkOjNlOjBiOjM1OmIwOmFkOmQ2Ojg1OgogICAgICAgICAgICAgICAgICAgIGUxOjY5OjkzOjU5
+OmY4OjIwOmJmOjI1Ojk3OjUzOjEyOmQ2OmUyOjRhOjkzOgogICAgICAgICAgICAgICAgICAgIDZi
+OmQ4OmFiOjM2OjU1Ojg2OjM4OjFjOmJkOjBhOjVlOjU3OmQ4OjI2OmVkOgogICAgICAgICAgICAg
+ICAgICAgIGJiOjc0OjlkOmQzOmYzOjFhOjNlOjYxOmUxOmY4OjdiOmE2OmJhOjBmOjllOgogICAg
+ICAgICAgICAgICAgICAgIGQ0OmIyOjc4OjFiOmNjOjI5OjVhOjJhOjRkOjNlOmVkOjEyOjc2OmE5
+OjQ0OgogICAgICAgICAgICAgICAgICAgIGFjOmI3OjZhOjc2OmYwOjQ0OmY4OjEzOmI3OmIwOjc5
+OjhjOjRiOmM5OjkwOgogICAgICAgICAgICAgICAgICAgIGY2OmIzOmI4Ojg3OjNlOmEyOjgzOjJj
+OjYwOmUxOmQ2OjdlOmY4OmY4OmExOgogICAgICAgICAgICAgICAgICAgIDViOjFlOmJmOjQzOjc2
+OjRhOmMzOmY0OmEzOmE3OjA3OjcyOmM0OjZlOjhjOgogICAgICAgICAgICAgICAgICAgIDUzOjIx
+OmNmOmYyOjUzOjI3OjU0OjkzOjQzOjE2OjljOjY5OjViOmIyOjJhOgogICAgICAgICAgICAgICAg
+ICAgIDk1OmRhOjcxOjdlOmM0OmJiOjUzOjA5OjgwOjZiOjdmOjhkOjRmOmQwOjI4OgogICAgICAg
+ICAgICAgICAgICAgIGQ0OjQ0OjcxOjVmOjRiOjc2Ojc3OjhlOjM2OjBhOjk4OmY2OmQzOjZmOmQy
+OgogICAgICAgICAgICAgICAgICAgIDFmOmJkOmIzOmIwOmRmOjFkOjg1OmU2OmYxOjRkOmNmOjNh
+OmJmOjliOjc1OgogICAgICAgICAgICAgICAgICAgIGM1OmJhOjYzOjJlOjNiOmY3OjUyOjg2OjA5
+OmE1OjBkOmI2OjIzOjZhOjRmOgogICAgICAgICAgICAgICAgICAgIDU5OjZmOmJiOmZlOjMyOjc3
+Ojg1OjkxOjkxOjc2OmM1OjE2OjZlOjYxOjI1OgogICAgICAgICAgICAgICAgICAgIDQ1OjI5CiAg
+ICAgICAgICAgICAgICBFeHBvbmVudDogNjU1MzcgKDB4MTAwMDEpCiAgICAgICAgWDUwOXYzIGV4
+dGVuc2lvbnM6CiAgICAgICAgICAgIFg1MDl2MyBCYXNpYyBDb25zdHJhaW50czogCiAgICAgICAg
+ICAgICAgICBDQTpGQUxTRQogICAgICAgICAgICBOZXRzY2FwZSBDb21tZW50OiAKICAgICAgICAg
+ICAgICAgIE9wZW5TU0wgR2VuZXJhdGVkIENlcnRpZmljYXRlCiAgICAgICAgICAgIFg1MDl2MyBT
+dWJqZWN0IEtleSBJZGVudGlmaWVyOiAKICAgICAgICAgICAgICAgIEFGOkYzOkU1OjJBOkVCOkNG
+OkM3OjdFOkE0OkQ2OjQ5OjkyOkY5OjI5OkVFOjZBOjFCOjY4OkFCOjBGCiAgICAgICAgICAgIFg1
+MDl2MyBBdXRob3JpdHkgS2V5IElkZW50aWZpZXI6IAogICAgICAgICAgICAgICAga2V5aWQ6MTM6
+MkU6MzA6RjA6MDQ6RUE6NDE6NUY6Qjc6MDg6QkQ6MzQ6MzE6RDc6MTE6RUE6NTY6QTY6OTk6RjAK
+CiAgICBTaWduYXR1cmUgQWxnb3JpdGhtOiBzaGEyNTZXaXRoUlNBRW5jcnlwdGlvbgogICAgICAg
+ICAyOTo2MjozOToyNTo3OTo1ODplYjphNDpiMzowYzplYTphYToxZDoyYjo5Njo3Yzo2ZToxMDoK
+ICAgICAgICAgY2U6MTY6MDc6Yjc6NzA6N2Y6MTY6ZGE6ZmQ6MjA6ZTY6YTI6ZDk6YjQ6ODg6ZTA6
+Zjk6ODQ6CiAgICAgICAgIDg3OmY4OmIwOjBkOjc3OjhiOmFlOjI3OmY1OmVlOmU2OjRmOjg2OmEx
+OjJkOjc0OjA3OjdjOgogICAgICAgICBjNzo1ZDpjMjpiZDplNDo3MDplNzo0MjplNDoxNDplZTpi
+OTpiNzo2MzpiODo4Yzo2ZDoyMToKICAgICAgICAgNjE6NTY6MGI6OTY6ZjY6MTU6YmE6N2E6YWU6
+ODA6OTg6YWM6NTc6OTk6Nzk6M2Q6N2E6YTk6CiAgICAgICAgIGQ4OjI2OjkzOjMwOjE3OjUzOjdj
+OjJkOjAyOjRiOjY0OjQ5OjI1OjY1OmU3OjY5OjVhOjA4OgogICAgICAgICBjZjo4NDo5NDo4ZTo2
+YTo0MjphNzpkMTo0ZjpiYTozOTo0Yjo3YzoxMTo2NzozMTpmNzoxYjoKICAgICAgICAgMmI6Y2Q6
+Nzk6YzI6Mjg6NGQ6ZDk6ODg6NjY6ZDY6N2Y6NTY6NGM6NGI6Mzc6ZDE6M2Q6YTg6CiAgICAgICAg
+IGQ5OjRhOjZiOjQ1OjFkOjRkOmE3OjEyOjlmOjI5Ojc3OjZhOjU1OmMxOmI1OjFkOjBlOmE1Ogog
+ICAgICAgICBiOTo0ZjozODoxNjozYzo3ZDo4NTphZTpmZjoyMzozNDpjNzoyYzpmNjoxNDowZjo1
+NTplZjoKICAgICAgICAgYjg6MDA6ODk6ZjE6YjI6OGE6NzU6MTU6NDE6ODE6NzI6ZDA6NDM6YTY6
+ODY6ZDE6MDY6ZTY6CiAgICAgICAgIGNlOjgxOjdlOjVmOjMzOmU2OmY0OjE5OmQ2OjcwOjAwOmJh
+OjQ4OjZlOjA1OmZkOjRjOjNjOgogICAgICAgICBjMzo1MToxYjpiZDo0MzoxYToyNDpjNTo3OTpl
+YTo3YTpmMDo4NTphNTo0MDoxMDo4NTplOToKICAgICAgICAgMjM6MDk6MDk6ODA6Mzg6OWQ6YmM6
+ODE6NWU6NTk6OGM6NWE6NGQ6NTg6NTY6Yjk6NzE6YzI6CiAgICAgICAgIDc4OmNkOmYzOmIwCi0t
+LS0tQkVHSU4gQ0VSVElGSUNBVEUtLS0tLQpNSUlEdVRDQ0FxR2dBd0lCQWdJQ0VBTXdEUVlKS29a
+SWh2Y05BUUVMQlFBd1pqRUxNQWtHQTFVRUJoTUNWVk14CkZ6QVZCZ05WQkFnTURrNXZjblJvSUVO
+aGNtOXNhVzVoTVE4d0RRWURWUVFIREFaRWRYSm9ZVzB4RURBT0JnTlYKQkFvTUIwRnVjMmxpYkdV
+eEd6QVpCZ05WQkFNTUVtRnVjMmxpYkdVdWFIUjBjQzUwWlhOMGN6QWVGdzB4T0RBegpNakV4T0RJ
+eU5EZGFGdzB5T0RBek1UZ3hPREl5TkRkYU1Gd3hDekFKQmdOVkJBWVRBbFZUTVJjd0ZRWURWUVFJ
+CkRBNU9iM0owYUNCRFlYSnZiR2x1WVRFUU1BNEdBMVVFQ2d3SFFXNXphV0pzWlRFaU1DQUdBMVVF
+QXd3WlkyeHAKWlc1MExtRnVjMmxpYkdVdWFIUjBjQzUwWlhOMGN6Q0NBU0l3RFFZSktvWklodmNO
+QVFFQkJRQURnZ0VQQURDQwpBUW9DZ2dFQkFOUEtKWEd1eExKbjVDdUl4UHF3VmdLcEMyUXVxVWha
+OXVsT0QwSHA5bHp2M2U2Z3pNdFFhcmQ1CitKbFNHSEVqdngxRGVBMCtDeld3cmRhRjRXbVRXZmdn
+dnlXWFV4TFc0a3FUYTlpck5sV0dPQnk5Q2w1WDJDYnQKdTNTZDAvTWFQbUhoK0h1bXVnK2UxTEo0
+Rzh3cFdpcE5QdTBTZHFsRXJMZHFkdkJFK0JPM3NIbU1TOG1ROXJPNApoejZpZ3l4ZzRkWisrUGlo
+V3g2L1EzWkt3L1NqcHdkeXhHNk1VeUhQOGxNblZKTkRGcHhwVzdJcWxkcHhmc1M3ClV3bUFhMytO
+VDlBbzFFUnhYMHQyZDQ0MkNwajIwMi9TSDcyenNOOGRoZWJ4VGM4NnY1dDF4YnBqTGp2M1VvWUoK
+cFEyMkkycFBXVys3L2pKM2haR1Jkc1VXYm1FbFJTa0NBd0VBQWFON01Ia3dDUVlEVlIwVEJBSXdB
+REFzQmdsZwpoa2dCaHZoQ0FRMEVIeFlkVDNCbGJsTlRUQ0JIWlc1bGNtRjBaV1FnUTJWeWRHbG1h
+V05oZEdVd0hRWURWUjBPCkJCWUVGSy96NVNycno4ZCtwTlpKa3ZrcDdtb2JhS3NQTUI4R0ExVWRJ
+d1FZTUJhQUZCTXVNUEFFNmtGZnR3aTkKTkRIWEVlcFdwcG53TUEwR0NTcUdTSWIzRFFFQkN3VUFB
+NElCQVFBcFlqa2xlVmpycExNTTZxb2RLNVo4YmhETwpGZ2UzY0g4VzJ2MGc1cUxadElqZytZU0gr
+TEFOZDR1dUovWHU1aytHb1MxMEIzekhYY0s5NUhEblF1UVU3cm0zClk3aU1iU0ZoVmd1VzloVzZl
+cTZBbUt4WG1YazllcW5ZSnBNd0YxTjhMUUpMWkVrbFplZHBXZ2pQaEpTT2FrS24KMFUrNk9VdDhF
+V2N4OXhzcnpYbkNLRTNaaUdiV2YxWk1TemZSUGFqWlNtdEZIVTJuRXA4cGQycFZ3YlVkRHFXNQpU
+emdXUEgyRnJ2OGpOTWNzOWhRUFZlKzRBSW54c29wMUZVR0JjdEJEcG9iUkJ1Yk9nWDVmTStiMEdk
+WndBTHBJCmJnWDlURHpEVVJ1OVF4b2t4WG5xZXZDRnBVQVFoZWtqQ1FtQU9KMjhnVjVaakZwTldG
+YTVjY0o0emZPdwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
+
+--===============3996062709511591449==
+Content-Transfer-Encoding: base64
+Content-Type: application/octet-stream
+Content-Disposition: form-data; name="file5"; filename="client.key"
+
+LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZB
+QVNDQktjd2dnU2pBZ0VBQW9JQkFRRFR5aVZ4cnNTeVorUXIKaU1UNnNGWUNxUXRrTHFsSVdmYnBU
+ZzlCNmZaYzc5M3VvTXpMVUdxM2VmaVpVaGh4STc4ZFEzZ05QZ3Mxc0szVwpoZUZwazFuNElMOGxs
+MU1TMXVKS2sydllxelpWaGpnY3ZRcGVWOWdtN2J0MG5kUHpHajVoNGZoN3Byb1BudFN5CmVCdk1L
+Vm9xVFQ3dEVuYXBSS3kzYW5id1JQZ1R0N0I1akV2SmtQYXp1SWMrb29Nc1lPSFdmdmo0b1ZzZXYw
+TjIKU3NQMG82Y0hjc1J1akZNaHovSlRKMVNUUXhhY2FWdXlLcFhhY1g3RXUxTUpnR3QvalUvUUtO
+UkVjVjlMZG5lTwpOZ3FZOXROdjBoKzlzN0RmSFlYbThVM1BPcitiZGNXNll5NDc5MUtHQ2FVTnRp
+TnFUMWx2dS80eWQ0V1JrWGJGCkZtNWhKVVVwQWdNQkFBRUNnZ0VCQUpZT2FjMU1TSzBuRXZFTmJK
+TTZFUmE5Y3dhK1VNNmtmMTc2SWJGUDlYQVAKdTZ6eFhXaklSM1JNQlNtTWt5akdiUWhzMzBoeXB6
+cVpQZkg2MWFVWjgrcnNPTUtIbnlLQUFjRlpCbFp6cUlHYwpJWEdyTndkMU1mOFMvWGc0d3cxQmtP
+V0ZWNnMwakN1NUczWi94eUkyUWw0cWNPVkQ2Yk13cHpjbFJiUWpDYW5kCmR2cXlDZE1EMHNSRHll
+T0lLNWhCaFVZNjBKbldiTUN1NnBCVStxUG9SdWtiUmllYWVETElOMWNsd0VxSVFWNzgKTExudjRu
+OWZ1R296SDBKZEhIZnlYRnl0Q2dJSnZFc3BaVWphLzVSNG9yQURocjNaQjAxMFJMell2czJuZEUz
+Qgo0Y0Y5Umd4c3BKWmVKL1ArUGdsVmladXpqMzdwWHkrN0dBY0pMUjlrYTRrQ2dZRUEvbDAxWEt3
+a0N6TWdYSFc0ClVQZ2wxK29uNDJCc043VDlyM1M1dGloT2pIZjRaSldrZ1l6aXNMVlgrTmMxb1VJ
+M0hRZk05UERKWlhNTU5tN0oKWlJ2RVJjb3BVMjZ3V3FyNkNGUGJsR3Y4b3FYSHFjcGV0YThpM3ha
+S29QQVNzVFc2c3N1UENFYWppTFpiUTFySApIL0hQK09aSVZMTS9XQ1BnQTJCY2tUVTlKbnNDZ1lF
+QTFTYlhsbFhubHdHcW1qaXRtWTFaMDdyVXhRM2FoL2ZCCmljY2JiZzNFNG9ub250WVhJbEk1elFt
+czN1K3FCZGkwWnV3YURtNVk0QmV0T3EwYTNVeXhBc3VncVZGbnpUYmEKMXcvc0ZiM2Z3OUtlUS9p
+bDRDWGticTg3bnpKZkRtRXlxSEdDQ1lYYmlqSEJ4bnE5OVBrcXdWcGFBaEhIRVcwbQp2V3lNVXZQ
+Ulk2c0NnWUFidFVXUjBjS2ZZYk5kdndrVDhPUVdjQkJtU1dPZ2Nkdk1tQmQreTBjN0wvcGo0cFVu
+Cjg1UGlFZThDVVZjck9NNU9JRUpvVUM1d0dhY3o2citQZndYVFlHRStFR212aHI1ejE4YXNsVkxR
+Mk9RMkQ3QmYKZERPRlA2VmpnS05Zb0hTMDgwMmlaaWQ4UmZrTkRqOXdzR09xUmxPTXZuWGhBUTl1
+N3JsR3JCajhMd0tCZ0ZmbwpwaDk5bkg4ZUU5TjVMcmZXb1VaK2xvUVMyNThhSW5zRllCMjZsZ25z
+WU1FcGdPOEp4SWI0eDVCR2ZmUGRWVUhoCmZEbVpieFExRDUvVWh2RGdVVnpheUk4c1lNZzFLSHBz
+T2EwWjJ6Q3pLOHpTdnU2OEVnTklTQ20zSjVjUnBVZnQKVUhsRytLMTlLZk1HNmxNZmRHKzhLTVVU
+dWV0SS9pSS9vM3dPekx2ekFvR0FJck9oMzBySHQ4d2l0N0VMQVJ5eAp3UGtwMkFSWVhyS2ZYM05F
+UzRjNjd6U0FpKzNkQ2p4UnF5d3FUSTBnTGljeU1sajh6RXU5WUU5SXgvcmw4bFJaCm5ROUxabXF2
+N1FIemhMVFVDUEdnWlluZW12QnpvN3IwZVc4T2FnNTJkYmNKTzZGQnN6ZldyeHNrbS9mWDI1UmIK
+V1B4aWgydmRSeTgxNGROUFcyNXJnZHc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
+
+--===============3996062709511591449==
+Content-Transfer-Encoding: base64
+Content-Type: text/plain
+Content-Disposition: form-data; name="file6"; filename="client.txt"
+
+Y2xpZW50LnBlbSBhbmQgY2xpZW50LmtleSB3ZXJlIHJldHJpZXZlZCBmcm9tIGh0dHB0ZXN0ZXIg
+ZG9ja2VyIGltYWdlOgoKYW5zaWJsZS9hbnNpYmxlQHNoYTI1NjpmYTVkZWY4YzI5NGZjNTA4MTNh
+ZjEzMWMwYjU3Mzc1OTRkODUyYWJhYzljYmU3YmEzOGUxN2JmMWM4NDc2ZjNmCg==
+
+--===============3996062709511591449==
+Content-Type: text/plain
+Content-Disposition: form-data; name="form_field_1"
+
+form_value_1
+--===============3996062709511591449==
+Content-Type: application/octet-stream
+Content-Disposition: form-data; name="form_field_2"
+
+form_value_2
+--===============3996062709511591449==
+Content-Type: text/html
+Content-Disposition: form-data; name="form_field_3"
+
+<html></html>
+--===============3996062709511591449==
+Content-Type: application/json
+Content-Disposition: form-data; name="form_field_4"
+
+{"foo": "bar"}
+--===============3996062709511591449==--
diff --git a/test/units/module_utils/urls/fixtures/netrc b/test/units/module_utils/urls/fixtures/netrc
new file mode 100644
index 0000000..8f12717
--- /dev/null
+++ b/test/units/module_utils/urls/fixtures/netrc
@@ -0,0 +1,3 @@
+machine ansible.com
+login user
+password passwd
diff --git a/test/units/module_utils/urls/test_RedirectHandlerFactory.py b/test/units/module_utils/urls/test_RedirectHandlerFactory.py
new file mode 100644
index 0000000..7bbe4b5
--- /dev/null
+++ b/test/units/module_utils/urls/test_RedirectHandlerFactory.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+# (c) 2018 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+from ansible.module_utils.urls import HAS_SSLCONTEXT, RedirectHandlerFactory, urllib_request, urllib_error
+from ansible.module_utils.six import StringIO
+
+import pytest
+
+
+@pytest.fixture
+def urllib_req():
+ req = urllib_request.Request(
+ 'https://ansible.com/'
+ )
+ return req
+
+
+@pytest.fixture
+def request_body():
+ return StringIO('TESTS')
+
+
+def test_no_redirs(urllib_req, request_body):
+ handler = RedirectHandlerFactory('none', False)
+ inst = handler()
+ with pytest.raises(urllib_error.HTTPError):
+ inst.redirect_request(urllib_req, request_body, 301, '301 Moved Permanently', {}, 'https://docs.ansible.com/')
+
+
+def test_urllib2_redir(urllib_req, request_body, mocker):
+ redir_request_mock = mocker.patch('ansible.module_utils.urls.urllib_request.HTTPRedirectHandler.redirect_request')
+
+ handler = RedirectHandlerFactory('urllib2', False)
+ inst = handler()
+ inst.redirect_request(urllib_req, request_body, 301, '301 Moved Permanently', {}, 'https://docs.ansible.com/')
+
+ redir_request_mock.assert_called_once_with(inst, urllib_req, request_body, 301, '301 Moved Permanently', {}, 'https://docs.ansible.com/')
+
+
+def test_all_redir(urllib_req, request_body, mocker):
+ req_mock = mocker.patch('ansible.module_utils.urls.RequestWithMethod')
+ handler = RedirectHandlerFactory('all', False)
+ inst = handler()
+ inst.redirect_request(urllib_req, request_body, 301, '301 Moved Permanently', {}, 'https://docs.ansible.com/')
+ req_mock.assert_called_once_with('https://docs.ansible.com/', data=None, headers={}, method='GET', origin_req_host='ansible.com', unverifiable=True)
+
+
+def test_all_redir_post(request_body, mocker):
+ handler = RedirectHandlerFactory('all', False)
+ inst = handler()
+
+ req = urllib_request.Request(
+ 'https://ansible.com/',
+ 'POST'
+ )
+
+ req_mock = mocker.patch('ansible.module_utils.urls.RequestWithMethod')
+ inst.redirect_request(req, request_body, 301, '301 Moved Permanently', {}, 'https://docs.ansible.com/')
+ req_mock.assert_called_once_with('https://docs.ansible.com/', data=None, headers={}, method='GET', origin_req_host='ansible.com', unverifiable=True)
+
+
+def test_redir_headers_removal(urllib_req, request_body, mocker):
+ req_mock = mocker.patch('ansible.module_utils.urls.RequestWithMethod')
+ handler = RedirectHandlerFactory('all', False)
+ inst = handler()
+
+ urllib_req.headers = {
+ 'Content-Type': 'application/json',
+ 'Content-Length': 100,
+ 'Foo': 'bar',
+ }
+
+ inst.redirect_request(urllib_req, request_body, 301, '301 Moved Permanently', {}, 'https://docs.ansible.com/')
+ req_mock.assert_called_once_with('https://docs.ansible.com/', data=None, headers={'Foo': 'bar'}, method='GET', origin_req_host='ansible.com',
+ unverifiable=True)
+
+
+def test_redir_url_spaces(urllib_req, request_body, mocker):
+ req_mock = mocker.patch('ansible.module_utils.urls.RequestWithMethod')
+ handler = RedirectHandlerFactory('all', False)
+ inst = handler()
+
+ inst.redirect_request(urllib_req, request_body, 301, '301 Moved Permanently', {}, 'https://docs.ansible.com/foo bar')
+
+ req_mock.assert_called_once_with('https://docs.ansible.com/foo%20bar', data=None, headers={}, method='GET', origin_req_host='ansible.com',
+ unverifiable=True)
+
+
+def test_redir_safe(urllib_req, request_body, mocker):
+ req_mock = mocker.patch('ansible.module_utils.urls.RequestWithMethod')
+ handler = RedirectHandlerFactory('safe', False)
+ inst = handler()
+ inst.redirect_request(urllib_req, request_body, 301, '301 Moved Permanently', {}, 'https://docs.ansible.com/')
+
+ req_mock.assert_called_once_with('https://docs.ansible.com/', data=None, headers={}, method='GET', origin_req_host='ansible.com', unverifiable=True)
+
+
+def test_redir_safe_not_safe(request_body):
+ handler = RedirectHandlerFactory('safe', False)
+ inst = handler()
+
+ req = urllib_request.Request(
+ 'https://ansible.com/',
+ 'POST'
+ )
+
+ with pytest.raises(urllib_error.HTTPError):
+ inst.redirect_request(req, request_body, 301, '301 Moved Permanently', {}, 'https://docs.ansible.com/')
+
+
+def test_redir_no_error_on_invalid(urllib_req, request_body):
+ handler = RedirectHandlerFactory('invalid', False)
+ inst = handler()
+
+ with pytest.raises(urllib_error.HTTPError):
+ inst.redirect_request(urllib_req, request_body, 301, '301 Moved Permanently', {}, 'https://docs.ansible.com/')
+
+
+def test_redir_validate_certs(urllib_req, request_body, mocker):
+ opener_mock = mocker.patch('ansible.module_utils.urls.urllib_request._opener')
+ handler = RedirectHandlerFactory('all', True)
+ inst = handler()
+ inst.redirect_request(urllib_req, request_body, 301, '301 Moved Permanently', {}, 'https://docs.ansible.com/')
+
+ assert opener_mock.add_handler.call_count == int(not HAS_SSLCONTEXT)
+
+
+def test_redir_http_error_308_urllib2(urllib_req, request_body, mocker):
+ redir_mock = mocker.patch.object(urllib_request.HTTPRedirectHandler, 'redirect_request')
+ handler = RedirectHandlerFactory('urllib2', False)
+ inst = handler()
+
+ inst.redirect_request(urllib_req, request_body, 308, '308 Permanent Redirect', {}, 'https://docs.ansible.com/')
+
+ assert redir_mock.call_count == 1
diff --git a/test/units/module_utils/urls/test_Request.py b/test/units/module_utils/urls/test_Request.py
new file mode 100644
index 0000000..d2c4ea3
--- /dev/null
+++ b/test/units/module_utils/urls/test_Request.py
@@ -0,0 +1,467 @@
+# -*- coding: utf-8 -*-
+# (c) 2018 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import datetime
+import os
+
+from ansible.module_utils.urls import (Request, open_url, urllib_request, HAS_SSLCONTEXT, cookiejar, RequestWithMethod,
+ UnixHTTPHandler, UnixHTTPSConnection, httplib)
+from ansible.module_utils.urls import SSLValidationHandler, HTTPSClientAuthHandler, RedirectHandlerFactory
+
+import pytest
+from units.compat.mock import call
+
+
+if HAS_SSLCONTEXT:
+ import ssl
+
+
+@pytest.fixture
+def urlopen_mock(mocker):
+ return mocker.patch('ansible.module_utils.urls.urllib_request.urlopen')
+
+
+@pytest.fixture
+def install_opener_mock(mocker):
+ return mocker.patch('ansible.module_utils.urls.urllib_request.install_opener')
+
+
+def test_Request_fallback(urlopen_mock, install_opener_mock, mocker):
+ here = os.path.dirname(__file__)
+ pem = os.path.join(here, 'fixtures/client.pem')
+
+ cookies = cookiejar.CookieJar()
+ request = Request(
+ headers={'foo': 'bar'},
+ use_proxy=False,
+ force=True,
+ timeout=100,
+ validate_certs=False,
+ url_username='user',
+ url_password='passwd',
+ http_agent='ansible-tests',
+ force_basic_auth=True,
+ follow_redirects='all',
+ client_cert='/tmp/client.pem',
+ client_key='/tmp/client.key',
+ cookies=cookies,
+ unix_socket='/foo/bar/baz.sock',
+ ca_path=pem,
+ ciphers=['ECDHE-RSA-AES128-SHA256'],
+ use_netrc=True,
+ )
+ fallback_mock = mocker.spy(request, '_fallback')
+
+ r = request.open('GET', 'https://ansible.com')
+
+ calls = [
+ call(None, False), # use_proxy
+ call(None, True), # force
+ call(None, 100), # timeout
+ call(None, False), # validate_certs
+ call(None, 'user'), # url_username
+ call(None, 'passwd'), # url_password
+ call(None, 'ansible-tests'), # http_agent
+ call(None, True), # force_basic_auth
+ call(None, 'all'), # follow_redirects
+ call(None, '/tmp/client.pem'), # client_cert
+ call(None, '/tmp/client.key'), # client_key
+ call(None, cookies), # cookies
+ call(None, '/foo/bar/baz.sock'), # unix_socket
+ call(None, pem), # ca_path
+ call(None, None), # unredirected_headers
+ call(None, True), # auto_decompress
+ call(None, ['ECDHE-RSA-AES128-SHA256']), # ciphers
+ call(None, True), # use_netrc
+ ]
+ fallback_mock.assert_has_calls(calls)
+
+ assert fallback_mock.call_count == 18 # All but headers use fallback
+
+ args = urlopen_mock.call_args[0]
+ assert args[1] is None # data, this is handled in the Request not urlopen
+ assert args[2] == 100 # timeout
+
+ req = args[0]
+ assert req.headers == {
+ 'Authorization': b'Basic dXNlcjpwYXNzd2Q=',
+ 'Cache-control': 'no-cache',
+ 'Foo': 'bar',
+ 'User-agent': 'ansible-tests'
+ }
+ assert req.data is None
+ assert req.get_method() == 'GET'
+
+
+def test_Request_open(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'https://ansible.com/')
+ args = urlopen_mock.call_args[0]
+ assert args[1] is None # data, this is handled in the Request not urlopen
+ assert args[2] == 10 # timeout
+
+ req = args[0]
+ assert req.headers == {}
+ assert req.data is None
+ assert req.get_method() == 'GET'
+
+ opener = install_opener_mock.call_args[0][0]
+ handlers = opener.handlers
+
+ if not HAS_SSLCONTEXT:
+ expected_handlers = (
+ SSLValidationHandler,
+ RedirectHandlerFactory(), # factory, get handler
+ )
+ else:
+ expected_handlers = (
+ RedirectHandlerFactory(), # factory, get handler
+ )
+
+ found_handlers = []
+ for handler in handlers:
+ if isinstance(handler, SSLValidationHandler) or handler.__class__.__name__ == 'RedirectHandler':
+ found_handlers.append(handler)
+
+ assert len(found_handlers) == len(expected_handlers)
+
+
+def test_Request_open_http(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'http://ansible.com/')
+ args = urlopen_mock.call_args[0]
+
+ opener = install_opener_mock.call_args[0][0]
+ handlers = opener.handlers
+
+ found_handlers = []
+ for handler in handlers:
+ if isinstance(handler, SSLValidationHandler):
+ found_handlers.append(handler)
+
+ assert len(found_handlers) == 0
+
+
+def test_Request_open_unix_socket(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'http://ansible.com/', unix_socket='/foo/bar/baz.sock')
+ args = urlopen_mock.call_args[0]
+
+ opener = install_opener_mock.call_args[0][0]
+ handlers = opener.handlers
+
+ found_handlers = []
+ for handler in handlers:
+ if isinstance(handler, UnixHTTPHandler):
+ found_handlers.append(handler)
+
+ assert len(found_handlers) == 1
+
+
+def test_Request_open_https_unix_socket(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'https://ansible.com/', unix_socket='/foo/bar/baz.sock')
+ args = urlopen_mock.call_args[0]
+
+ opener = install_opener_mock.call_args[0][0]
+ handlers = opener.handlers
+
+ found_handlers = []
+ for handler in handlers:
+ if isinstance(handler, HTTPSClientAuthHandler):
+ found_handlers.append(handler)
+
+ assert len(found_handlers) == 1
+
+ inst = found_handlers[0]._build_https_connection('foo')
+ assert isinstance(inst, UnixHTTPSConnection)
+
+
+def test_Request_open_ftp(urlopen_mock, install_opener_mock, mocker):
+ mocker.patch('ansible.module_utils.urls.ParseResultDottedDict.as_list', side_effect=AssertionError)
+
+ # Using ftp scheme should prevent the AssertionError side effect to fire
+ r = Request().open('GET', 'ftp://foo@ansible.com/')
+
+
+def test_Request_open_headers(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'http://ansible.com/', headers={'Foo': 'bar'})
+ args = urlopen_mock.call_args[0]
+ req = args[0]
+ assert req.headers == {'Foo': 'bar'}
+
+
+def test_Request_open_username(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'http://ansible.com/', url_username='user')
+
+ opener = install_opener_mock.call_args[0][0]
+ handlers = opener.handlers
+
+ expected_handlers = (
+ urllib_request.HTTPBasicAuthHandler,
+ urllib_request.HTTPDigestAuthHandler,
+ )
+
+ found_handlers = []
+ for handler in handlers:
+ if isinstance(handler, expected_handlers):
+ found_handlers.append(handler)
+ assert len(found_handlers) == 2
+ assert found_handlers[0].passwd.passwd[None] == {(('ansible.com', '/'),): ('user', None)}
+
+
+def test_Request_open_username_in_url(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'http://user2@ansible.com/')
+
+ opener = install_opener_mock.call_args[0][0]
+ handlers = opener.handlers
+
+ expected_handlers = (
+ urllib_request.HTTPBasicAuthHandler,
+ urllib_request.HTTPDigestAuthHandler,
+ )
+
+ found_handlers = []
+ for handler in handlers:
+ if isinstance(handler, expected_handlers):
+ found_handlers.append(handler)
+ assert found_handlers[0].passwd.passwd[None] == {(('ansible.com', '/'),): ('user2', '')}
+
+
+def test_Request_open_username_force_basic(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'http://ansible.com/', url_username='user', url_password='passwd', force_basic_auth=True)
+
+ opener = install_opener_mock.call_args[0][0]
+ handlers = opener.handlers
+
+ expected_handlers = (
+ urllib_request.HTTPBasicAuthHandler,
+ urllib_request.HTTPDigestAuthHandler,
+ )
+
+ found_handlers = []
+ for handler in handlers:
+ if isinstance(handler, expected_handlers):
+ found_handlers.append(handler)
+
+ assert len(found_handlers) == 0
+
+ args = urlopen_mock.call_args[0]
+ req = args[0]
+ assert req.headers.get('Authorization') == b'Basic dXNlcjpwYXNzd2Q='
+
+
+def test_Request_open_auth_in_netloc(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'http://user:passwd@ansible.com/')
+ args = urlopen_mock.call_args[0]
+ req = args[0]
+ assert req.get_full_url() == 'http://ansible.com/'
+
+ opener = install_opener_mock.call_args[0][0]
+ handlers = opener.handlers
+
+ expected_handlers = (
+ urllib_request.HTTPBasicAuthHandler,
+ urllib_request.HTTPDigestAuthHandler,
+ )
+
+ found_handlers = []
+ for handler in handlers:
+ if isinstance(handler, expected_handlers):
+ found_handlers.append(handler)
+
+ assert len(found_handlers) == 2
+
+
+def test_Request_open_netrc(urlopen_mock, install_opener_mock, monkeypatch):
+ here = os.path.dirname(__file__)
+
+ monkeypatch.setenv('NETRC', os.path.join(here, 'fixtures/netrc'))
+ r = Request().open('GET', 'http://ansible.com/')
+ args = urlopen_mock.call_args[0]
+ req = args[0]
+ assert req.headers.get('Authorization') == b'Basic dXNlcjpwYXNzd2Q='
+
+ r = Request().open('GET', 'http://foo.ansible.com/')
+ args = urlopen_mock.call_args[0]
+ req = args[0]
+ assert 'Authorization' not in req.headers
+
+ monkeypatch.setenv('NETRC', os.path.join(here, 'fixtures/netrc.nonexistant'))
+ r = Request().open('GET', 'http://ansible.com/')
+ args = urlopen_mock.call_args[0]
+ req = args[0]
+ assert 'Authorization' not in req.headers
+
+
+def test_Request_open_no_proxy(urlopen_mock, install_opener_mock, mocker):
+ build_opener_mock = mocker.patch('ansible.module_utils.urls.urllib_request.build_opener')
+
+ r = Request().open('GET', 'http://ansible.com/', use_proxy=False)
+
+ handlers = build_opener_mock.call_args[0]
+ found_handlers = []
+ for handler in handlers:
+ if isinstance(handler, urllib_request.ProxyHandler):
+ found_handlers.append(handler)
+
+ assert len(found_handlers) == 1
+
+
+@pytest.mark.skipif(not HAS_SSLCONTEXT, reason="requires SSLContext")
+def test_Request_open_no_validate_certs(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'https://ansible.com/', validate_certs=False)
+
+ opener = install_opener_mock.call_args[0][0]
+ handlers = opener.handlers
+
+ ssl_handler = None
+ for handler in handlers:
+ if isinstance(handler, HTTPSClientAuthHandler):
+ ssl_handler = handler
+ break
+
+ assert ssl_handler is not None
+
+ inst = ssl_handler._build_https_connection('foo')
+ assert isinstance(inst, httplib.HTTPSConnection)
+
+ context = ssl_handler._context
+ # Differs by Python version
+ # assert context.protocol == ssl.PROTOCOL_SSLv23
+ if ssl.OP_NO_SSLv2:
+ assert context.options & ssl.OP_NO_SSLv2
+ assert context.options & ssl.OP_NO_SSLv3
+ assert context.verify_mode == ssl.CERT_NONE
+ assert context.check_hostname is False
+
+
+def test_Request_open_client_cert(urlopen_mock, install_opener_mock):
+ here = os.path.dirname(__file__)
+
+ client_cert = os.path.join(here, 'fixtures/client.pem')
+ client_key = os.path.join(here, 'fixtures/client.key')
+
+ r = Request().open('GET', 'https://ansible.com/', client_cert=client_cert, client_key=client_key)
+
+ opener = install_opener_mock.call_args[0][0]
+ handlers = opener.handlers
+
+ ssl_handler = None
+ for handler in handlers:
+ if isinstance(handler, HTTPSClientAuthHandler):
+ ssl_handler = handler
+ break
+
+ assert ssl_handler is not None
+
+ assert ssl_handler.client_cert == client_cert
+ assert ssl_handler.client_key == client_key
+
+ https_connection = ssl_handler._build_https_connection('ansible.com')
+
+ assert https_connection.key_file == client_key
+ assert https_connection.cert_file == client_cert
+
+
+def test_Request_open_cookies(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'https://ansible.com/', cookies=cookiejar.CookieJar())
+
+ opener = install_opener_mock.call_args[0][0]
+ handlers = opener.handlers
+
+ cookies_handler = None
+ for handler in handlers:
+ if isinstance(handler, urllib_request.HTTPCookieProcessor):
+ cookies_handler = handler
+ break
+
+ assert cookies_handler is not None
+
+
+def test_Request_open_invalid_method(urlopen_mock, install_opener_mock):
+ r = Request().open('UNKNOWN', 'https://ansible.com/')
+
+ args = urlopen_mock.call_args[0]
+ req = args[0]
+
+ assert req.data is None
+ assert req.get_method() == 'UNKNOWN'
+ # assert r.status == 504
+
+
+def test_Request_open_custom_method(urlopen_mock, install_opener_mock):
+ r = Request().open('DELETE', 'https://ansible.com/')
+
+ args = urlopen_mock.call_args[0]
+ req = args[0]
+
+ assert isinstance(req, RequestWithMethod)
+
+
+def test_Request_open_user_agent(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'https://ansible.com/', http_agent='ansible-tests')
+
+ args = urlopen_mock.call_args[0]
+ req = args[0]
+
+ assert req.headers.get('User-agent') == 'ansible-tests'
+
+
+def test_Request_open_force(urlopen_mock, install_opener_mock):
+ r = Request().open('GET', 'https://ansible.com/', force=True, last_mod_time=datetime.datetime.now())
+
+ args = urlopen_mock.call_args[0]
+ req = args[0]
+
+ assert req.headers.get('Cache-control') == 'no-cache'
+ assert 'If-modified-since' not in req.headers
+
+
+def test_Request_open_last_mod(urlopen_mock, install_opener_mock):
+ now = datetime.datetime.now()
+ r = Request().open('GET', 'https://ansible.com/', last_mod_time=now)
+
+ args = urlopen_mock.call_args[0]
+ req = args[0]
+
+ assert req.headers.get('If-modified-since') == now.strftime('%a, %d %b %Y %H:%M:%S GMT')
+
+
+def test_Request_open_headers_not_dict(urlopen_mock, install_opener_mock):
+ with pytest.raises(ValueError):
+ Request().open('GET', 'https://ansible.com/', headers=['bob'])
+
+
+def test_Request_init_headers_not_dict(urlopen_mock, install_opener_mock):
+ with pytest.raises(ValueError):
+ Request(headers=['bob'])
+
+
+@pytest.mark.parametrize('method,kwargs', [
+ ('get', {}),
+ ('options', {}),
+ ('head', {}),
+ ('post', {'data': None}),
+ ('put', {'data': None}),
+ ('patch', {'data': None}),
+ ('delete', {}),
+])
+def test_methods(method, kwargs, mocker):
+ expected = method.upper()
+ open_mock = mocker.patch('ansible.module_utils.urls.Request.open')
+ request = Request()
+ getattr(request, method)('https://ansible.com')
+ open_mock.assert_called_once_with(expected, 'https://ansible.com', **kwargs)
+
+
+def test_open_url(urlopen_mock, install_opener_mock, mocker):
+ req_mock = mocker.patch('ansible.module_utils.urls.Request.open')
+ open_url('https://ansible.com/')
+ req_mock.assert_called_once_with('GET', 'https://ansible.com/', data=None, headers=None, use_proxy=True,
+ force=False, last_mod_time=None, timeout=10, validate_certs=True,
+ url_username=None, url_password=None, http_agent=None,
+ force_basic_auth=False, follow_redirects='urllib2',
+ client_cert=None, client_key=None, cookies=None, use_gssapi=False,
+ unix_socket=None, ca_path=None, unredirected_headers=None, decompress=True,
+ ciphers=None, use_netrc=True)
diff --git a/test/units/module_utils/urls/test_RequestWithMethod.py b/test/units/module_utils/urls/test_RequestWithMethod.py
new file mode 100644
index 0000000..0510519
--- /dev/null
+++ b/test/units/module_utils/urls/test_RequestWithMethod.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# (c) 2018 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.urls import RequestWithMethod
+
+
+def test_RequestWithMethod():
+ get = RequestWithMethod('https://ansible.com/', 'GET')
+ assert get.get_method() == 'GET'
+
+ post = RequestWithMethod('https://ansible.com/', 'POST', data='foo', headers={'Bar': 'baz'})
+ assert post.get_method() == 'POST'
+ assert post.get_full_url() == 'https://ansible.com/'
+ assert post.data == 'foo'
+ assert post.headers == {'Bar': 'baz'}
+
+ none = RequestWithMethod('https://ansible.com/', '')
+ assert none.get_method() == 'GET'
diff --git a/test/units/module_utils/urls/test_channel_binding.py b/test/units/module_utils/urls/test_channel_binding.py
new file mode 100644
index 0000000..ea9cd01
--- /dev/null
+++ b/test/units/module_utils/urls/test_channel_binding.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# (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 base64
+import os.path
+import pytest
+
+from ansible.module_utils import urls
+
+
+@pytest.mark.skipif(not urls.HAS_CRYPTOGRAPHY, reason='Requires cryptography to be installed')
+@pytest.mark.parametrize('certificate, expected', [
+ ('rsa_md5.pem', b'\x23\x34\xB8\x47\x6C\xBF\x4E\x6D'
+ b'\xFC\x76\x6A\x5D\x5A\x30\xD6\x64'
+ b'\x9C\x01\xBA\xE1\x66\x2A\x5C\x3A'
+ b'\x13\x02\xA9\x68\xD7\xC6\xB0\xF6'),
+ ('rsa_sha1.pem', b'\x14\xCF\xE8\xE4\xB3\x32\xB2\x0A'
+ b'\x34\x3F\xC8\x40\xB1\x8F\x9F\x6F'
+ b'\x78\x92\x6A\xFE\x7E\xC3\xE7\xB8'
+ b'\xE2\x89\x69\x61\x9B\x1E\x8F\x3E'),
+ ('rsa_sha256.pem', b'\x99\x6F\x3E\xEA\x81\x2C\x18\x70'
+ b'\xE3\x05\x49\xFF\x9B\x86\xCD\x87'
+ b'\xA8\x90\xB6\xD8\xDF\xDF\x4A\x81'
+ b'\xBE\xF9\x67\x59\x70\xDA\xDB\x26'),
+ ('rsa_sha384.pem', b'\x34\xF3\x03\xC9\x95\x28\x6F\x4B'
+ b'\x21\x4A\x9B\xA6\x43\x5B\x69\xB5'
+ b'\x1E\xCF\x37\x58\xEA\xBC\x2A\x14'
+ b'\xD7\xA4\x3F\xD2\x37\xDC\x2B\x1A'
+ b'\x1A\xD9\x11\x1C\x5C\x96\x5E\x10'
+ b'\x75\x07\xCB\x41\x98\xC0\x9F\xEC'),
+ ('rsa_sha512.pem', b'\x55\x6E\x1C\x17\x84\xE3\xB9\x57'
+ b'\x37\x0B\x7F\x54\x4F\x62\xC5\x33'
+ b'\xCB\x2C\xA5\xC1\xDA\xE0\x70\x6F'
+ b'\xAE\xF0\x05\x44\xE1\xAD\x2B\x76'
+ b'\xFF\x25\xCF\xBE\x69\xB1\xC4\xE6'
+ b'\x30\xC3\xBB\x02\x07\xDF\x11\x31'
+ b'\x4C\x67\x38\xBC\xAE\xD7\xE0\x71'
+ b'\xD7\xBF\xBF\x2C\x9D\xFA\xB8\x5D'),
+ ('rsa-pss_sha256.pem', b'\xF2\x31\xE6\xFF\x3F\x9E\x16\x1B'
+ b'\xC2\xDC\xBB\x89\x8D\x84\x47\x4E'
+ b'\x58\x9C\xD7\xC2\x7A\xDB\xEF\x8B'
+ b'\xD9\xC0\xC0\x68\xAF\x9C\x36\x6D'),
+ ('rsa-pss_sha512.pem', b'\x85\x85\x19\xB9\xE1\x0F\x23\xE2'
+ b'\x1D\x2C\xE9\xD5\x47\x2A\xAB\xCE'
+ b'\x42\x0F\xD1\x00\x75\x9C\x53\xA1'
+ b'\x7B\xB9\x79\x86\xB2\x59\x61\x27'),
+ ('ecdsa_sha256.pem', b'\xFE\xCF\x1B\x25\x85\x44\x99\x90'
+ b'\xD9\xE3\xB2\xC9\x2D\x3F\x59\x7E'
+ b'\xC8\x35\x4E\x12\x4E\xDA\x75\x1D'
+ b'\x94\x83\x7C\x2C\x89\xA2\xC1\x55'),
+ ('ecdsa_sha512.pem', b'\xE5\xCB\x68\xB2\xF8\x43\xD6\x3B'
+ b'\xF4\x0B\xCB\x20\x07\x60\x8F\x81'
+ b'\x97\x61\x83\x92\x78\x3F\x23\x30'
+ b'\xE5\xEF\x19\xA5\xBD\x8F\x0B\x2F'
+ b'\xAA\xC8\x61\x85\x5F\xBB\x63\xA2'
+ b'\x21\xCC\x46\xFC\x1E\x22\x6A\x07'
+ b'\x24\x11\xAF\x17\x5D\xDE\x47\x92'
+ b'\x81\xE0\x06\x87\x8B\x34\x80\x59'),
+])
+def test_cbt_with_cert(certificate, expected):
+ with open(os.path.join(os.path.dirname(__file__), 'fixtures', 'cbt', certificate)) as fd:
+ cert_der = base64.b64decode("".join([l.strip() for l in fd.readlines()[1:-1]]))
+
+ actual = urls.get_channel_binding_cert_hash(cert_der)
+ assert actual == expected
+
+
+def test_cbt_no_cryptography(monkeypatch):
+ monkeypatch.setattr(urls, 'HAS_CRYPTOGRAPHY', False)
+ assert urls.get_channel_binding_cert_hash(None) is None
diff --git a/test/units/module_utils/urls/test_fetch_file.py b/test/units/module_utils/urls/test_fetch_file.py
new file mode 100644
index 0000000..ed11227
--- /dev/null
+++ b/test/units/module_utils/urls/test_fetch_file.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+# Copyright: Contributors to the Ansible project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+
+from ansible.module_utils.urls import fetch_file
+
+import pytest
+from units.compat.mock import MagicMock
+
+
+class FakeTemporaryFile:
+ def __init__(self, name):
+ self.name = name
+
+
+@pytest.mark.parametrize(
+ 'url, prefix, suffix, expected', (
+ ('http://ansible.com/foo.tar.gz?foo=%s' % ('bar' * 100), 'foo', '.tar.gz', 'foo.tar.gz'),
+ ('https://www.gnu.org/licenses/gpl-3.0.txt', 'gpl-3.0', '.txt', 'gpl-3.0.txt'),
+ ('http://pyyaml.org/download/libyaml/yaml-0.2.5.tar.gz', 'yaml-0.2.5', '.tar.gz', 'yaml-0.2.5.tar.gz'),
+ (
+ 'https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz',
+ 'geckodriver-v0.26.0-linux64',
+ '.tar.gz',
+ 'geckodriver-v0.26.0-linux64.tar.gz'
+ ),
+ )
+)
+def test_file_multiple_extensions(mocker, url, prefix, suffix, expected):
+ module = mocker.Mock()
+ module.tmpdir = '/tmp'
+ module.add_cleanup_file = mocker.Mock(side_effect=AttributeError('raised intentionally'))
+
+ mock_NamedTemporaryFile = mocker.patch('ansible.module_utils.urls.tempfile.NamedTemporaryFile',
+ return_value=FakeTemporaryFile(os.path.join(module.tmpdir, expected)))
+
+ with pytest.raises(AttributeError, match='raised intentionally'):
+ fetch_file(module, url)
+
+ mock_NamedTemporaryFile.assert_called_with(dir=module.tmpdir, prefix=prefix, suffix=suffix, delete=False)
diff --git a/test/units/module_utils/urls/test_fetch_url.py b/test/units/module_utils/urls/test_fetch_url.py
new file mode 100644
index 0000000..5bfd66a
--- /dev/null
+++ b/test/units/module_utils/urls/test_fetch_url.py
@@ -0,0 +1,230 @@
+# -*- coding: utf-8 -*-
+# (c) 2018 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import socket
+import sys
+
+from ansible.module_utils.six import StringIO
+from ansible.module_utils.six.moves.http_cookiejar import Cookie
+from ansible.module_utils.six.moves.http_client import HTTPMessage
+from ansible.module_utils.urls import fetch_url, urllib_error, ConnectionError, NoSSLError, httplib
+
+import pytest
+from units.compat.mock import MagicMock
+
+
+class AnsibleModuleExit(Exception):
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+
+class ExitJson(AnsibleModuleExit):
+ pass
+
+
+class FailJson(AnsibleModuleExit):
+ pass
+
+
+@pytest.fixture
+def open_url_mock(mocker):
+ return mocker.patch('ansible.module_utils.urls.open_url')
+
+
+@pytest.fixture
+def fake_ansible_module():
+ return FakeAnsibleModule()
+
+
+class FakeAnsibleModule:
+ def __init__(self):
+ self.params = {}
+ self.tmpdir = None
+
+ def exit_json(self, *args, **kwargs):
+ raise ExitJson(*args, **kwargs)
+
+ def fail_json(self, *args, **kwargs):
+ raise FailJson(*args, **kwargs)
+
+
+def test_fetch_url_no_urlparse(mocker, fake_ansible_module):
+ mocker.patch('ansible.module_utils.urls.HAS_URLPARSE', new=False)
+
+ with pytest.raises(FailJson):
+ fetch_url(fake_ansible_module, 'http://ansible.com/')
+
+
+def test_fetch_url(open_url_mock, fake_ansible_module):
+ r, info = fetch_url(fake_ansible_module, 'http://ansible.com/')
+
+ dummy, kwargs = open_url_mock.call_args
+
+ open_url_mock.assert_called_once_with('http://ansible.com/', client_cert=None, client_key=None, cookies=kwargs['cookies'], data=None,
+ follow_redirects='urllib2', force=False, force_basic_auth='', headers=None,
+ http_agent='ansible-httpget', last_mod_time=None, method=None, timeout=10, url_password='', url_username='',
+ use_proxy=True, validate_certs=True, use_gssapi=False, unix_socket=None, ca_path=None, unredirected_headers=None,
+ decompress=True, ciphers=None, use_netrc=True)
+
+
+def test_fetch_url_params(open_url_mock, fake_ansible_module):
+ fake_ansible_module.params = {
+ 'validate_certs': False,
+ 'url_username': 'user',
+ 'url_password': 'passwd',
+ 'http_agent': 'ansible-test',
+ 'force_basic_auth': True,
+ 'follow_redirects': 'all',
+ 'client_cert': 'client.pem',
+ 'client_key': 'client.key',
+ }
+
+ r, info = fetch_url(fake_ansible_module, 'http://ansible.com/')
+
+ dummy, kwargs = open_url_mock.call_args
+
+ open_url_mock.assert_called_once_with('http://ansible.com/', client_cert='client.pem', client_key='client.key', cookies=kwargs['cookies'], data=None,
+ follow_redirects='all', force=False, force_basic_auth=True, headers=None,
+ http_agent='ansible-test', last_mod_time=None, method=None, timeout=10, url_password='passwd', url_username='user',
+ use_proxy=True, validate_certs=False, use_gssapi=False, unix_socket=None, ca_path=None, unredirected_headers=None,
+ decompress=True, ciphers=None, use_netrc=True)
+
+
+def test_fetch_url_cookies(mocker, fake_ansible_module):
+ def make_cookies(*args, **kwargs):
+ cookies = kwargs['cookies']
+ r = MagicMock()
+ try:
+ r.headers = HTTPMessage()
+ add_header = r.headers.add_header
+ except TypeError:
+ # PY2
+ r.headers = HTTPMessage(StringIO())
+ add_header = r.headers.addheader
+ r.info.return_value = r.headers
+ for name, value in (('Foo', 'bar'), ('Baz', 'qux')):
+ cookie = Cookie(
+ version=0,
+ name=name,
+ value=value,
+ port=None,
+ port_specified=False,
+ domain="ansible.com",
+ domain_specified=True,
+ domain_initial_dot=False,
+ path="/",
+ path_specified=True,
+ secure=False,
+ expires=None,
+ discard=False,
+ comment=None,
+ comment_url=None,
+ rest=None
+ )
+ cookies.set_cookie(cookie)
+ add_header('Set-Cookie', '%s=%s' % (name, value))
+
+ return r
+
+ mocker = mocker.patch('ansible.module_utils.urls.open_url', new=make_cookies)
+
+ r, info = fetch_url(fake_ansible_module, 'http://ansible.com/')
+
+ assert info['cookies'] == {'Baz': 'qux', 'Foo': 'bar'}
+
+ if sys.version_info < (3, 11):
+ # Python sorts cookies in order of most specific (ie. longest) path first
+ # items with the same path are reversed from response order
+ assert info['cookies_string'] == 'Baz=qux; Foo=bar'
+ else:
+ # Python 3.11 and later preserve the Set-Cookie order.
+ # See: https://github.com/python/cpython/pull/22745/
+ assert info['cookies_string'] == 'Foo=bar; Baz=qux'
+
+ # The key here has a `-` as opposed to what we see in the `uri` module that converts to `_`
+ # Note: this is response order, which differs from cookies_string
+ assert info['set-cookie'] == 'Foo=bar, Baz=qux'
+
+
+def test_fetch_url_nossl(open_url_mock, fake_ansible_module, mocker):
+ mocker.patch('ansible.module_utils.urls.get_distribution', return_value='notredhat')
+
+ open_url_mock.side_effect = NoSSLError
+ with pytest.raises(FailJson) as excinfo:
+ fetch_url(fake_ansible_module, 'http://ansible.com/')
+
+ assert 'python-ssl' not in excinfo.value.kwargs['msg']
+
+ mocker.patch('ansible.module_utils.urls.get_distribution', return_value='redhat')
+
+ open_url_mock.side_effect = NoSSLError
+ with pytest.raises(FailJson) as excinfo:
+ fetch_url(fake_ansible_module, 'http://ansible.com/')
+
+ assert 'python-ssl' in excinfo.value.kwargs['msg']
+ assert 'http://ansible.com/' == excinfo.value.kwargs['url']
+ assert excinfo.value.kwargs['status'] == -1
+
+
+def test_fetch_url_connectionerror(open_url_mock, fake_ansible_module):
+ open_url_mock.side_effect = ConnectionError('TESTS')
+ with pytest.raises(FailJson) as excinfo:
+ fetch_url(fake_ansible_module, 'http://ansible.com/')
+
+ assert excinfo.value.kwargs['msg'] == 'TESTS'
+ assert 'http://ansible.com/' == excinfo.value.kwargs['url']
+ assert excinfo.value.kwargs['status'] == -1
+
+ open_url_mock.side_effect = ValueError('TESTS')
+ with pytest.raises(FailJson) as excinfo:
+ fetch_url(fake_ansible_module, 'http://ansible.com/')
+
+ assert excinfo.value.kwargs['msg'] == 'TESTS'
+ assert 'http://ansible.com/' == excinfo.value.kwargs['url']
+ assert excinfo.value.kwargs['status'] == -1
+
+
+def test_fetch_url_httperror(open_url_mock, fake_ansible_module):
+ open_url_mock.side_effect = urllib_error.HTTPError(
+ 'http://ansible.com/',
+ 500,
+ 'Internal Server Error',
+ {'Content-Type': 'application/json'},
+ StringIO('TESTS')
+ )
+
+ r, info = fetch_url(fake_ansible_module, 'http://ansible.com/')
+
+ assert info == {'msg': 'HTTP Error 500: Internal Server Error', 'body': 'TESTS',
+ 'status': 500, 'url': 'http://ansible.com/', 'content-type': 'application/json'}
+
+
+def test_fetch_url_urlerror(open_url_mock, fake_ansible_module):
+ open_url_mock.side_effect = urllib_error.URLError('TESTS')
+ r, info = fetch_url(fake_ansible_module, 'http://ansible.com/')
+ assert info == {'msg': 'Request failed: <urlopen error TESTS>', 'status': -1, 'url': 'http://ansible.com/'}
+
+
+def test_fetch_url_socketerror(open_url_mock, fake_ansible_module):
+ open_url_mock.side_effect = socket.error('TESTS')
+ r, info = fetch_url(fake_ansible_module, 'http://ansible.com/')
+ assert info == {'msg': 'Connection failure: TESTS', 'status': -1, 'url': 'http://ansible.com/'}
+
+
+def test_fetch_url_exception(open_url_mock, fake_ansible_module):
+ open_url_mock.side_effect = Exception('TESTS')
+ r, info = fetch_url(fake_ansible_module, 'http://ansible.com/')
+ exception = info.pop('exception')
+ assert info == {'msg': 'An unknown error occurred: TESTS', 'status': -1, 'url': 'http://ansible.com/'}
+ assert "Exception: TESTS" in exception
+
+
+def test_fetch_url_badstatusline(open_url_mock, fake_ansible_module):
+ open_url_mock.side_effect = httplib.BadStatusLine('TESTS')
+ r, info = fetch_url(fake_ansible_module, 'http://ansible.com/')
+ assert info == {'msg': 'Connection failure: connection was closed before a valid response was received: TESTS', 'status': -1, 'url': 'http://ansible.com/'}
diff --git a/test/units/module_utils/urls/test_generic_urlparse.py b/test/units/module_utils/urls/test_generic_urlparse.py
new file mode 100644
index 0000000..7753726
--- /dev/null
+++ b/test/units/module_utils/urls/test_generic_urlparse.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# (c) 2018 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.urls import generic_urlparse
+from ansible.module_utils.six.moves.urllib.parse import urlparse, urlunparse
+
+
+def test_generic_urlparse():
+ url = 'https://ansible.com/blog'
+ parts = urlparse(url)
+ generic_parts = generic_urlparse(parts)
+ assert generic_parts.as_list() == list(parts)
+
+ assert urlunparse(generic_parts.as_list()) == url
+
+
+def test_generic_urlparse_netloc():
+ url = 'https://ansible.com:443/blog'
+ parts = urlparse(url)
+ generic_parts = generic_urlparse(parts)
+ assert generic_parts.hostname == parts.hostname
+ assert generic_parts.hostname == 'ansible.com'
+ assert generic_parts.port == 443
+ assert urlunparse(generic_parts.as_list()) == url
+
+
+def test_generic_urlparse_no_netloc():
+ url = 'https://user:passwd@ansible.com:443/blog'
+ parts = list(urlparse(url))
+ generic_parts = generic_urlparse(parts)
+ assert generic_parts.hostname == 'ansible.com'
+ assert generic_parts.port == 443
+ assert generic_parts.username == 'user'
+ assert generic_parts.password == 'passwd'
+ assert urlunparse(generic_parts.as_list()) == url
+
+
+def test_generic_urlparse_no_netloc_no_auth():
+ url = 'https://ansible.com:443/blog'
+ parts = list(urlparse(url))
+ generic_parts = generic_urlparse(parts)
+ assert generic_parts.username is None
+ assert generic_parts.password is None
+
+
+def test_generic_urlparse_no_netloc_no_host():
+ url = '/blog'
+ parts = list(urlparse(url))
+ generic_parts = generic_urlparse(parts)
+ assert generic_parts.username is None
+ assert generic_parts.password is None
+ assert generic_parts.port is None
+ assert generic_parts.hostname == ''
diff --git a/test/units/module_utils/urls/test_gzip.py b/test/units/module_utils/urls/test_gzip.py
new file mode 100644
index 0000000..c684032
--- /dev/null
+++ b/test/units/module_utils/urls/test_gzip.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+# (c) 2021 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import gzip
+import io
+import sys
+
+try:
+ from urllib.response import addinfourl
+except ImportError:
+ from urllib import addinfourl
+
+from ansible.module_utils.six import PY3
+from ansible.module_utils.six.moves import http_client
+from ansible.module_utils.urls import GzipDecodedReader, Request
+
+import pytest
+
+
+def compress(data):
+ buf = io.BytesIO()
+ try:
+ f = gzip.GzipFile(fileobj=buf, mode='wb')
+ f.write(data)
+ finally:
+ f.close()
+ return buf.getvalue()
+
+
+class Sock(io.BytesIO):
+ def makefile(self, *args, **kwds):
+ return self
+
+
+@pytest.fixture
+def urlopen_mock(mocker):
+ return mocker.patch('ansible.module_utils.urls.urllib_request.urlopen')
+
+
+JSON_DATA = b'{"foo": "bar", "baz": "qux", "sandwich": "ham", "tech_level": "pickle", "pop": "corn", "ansible": "awesome"}'
+
+
+RESP = b'''HTTP/1.1 200 OK
+Content-Type: application/json; charset=utf-8
+Set-Cookie: foo
+Set-Cookie: bar
+Content-Length: 108
+
+%s''' % JSON_DATA
+
+GZIP_RESP = b'''HTTP/1.1 200 OK
+Content-Type: application/json; charset=utf-8
+Set-Cookie: foo
+Set-Cookie: bar
+Content-Encoding: gzip
+Content-Length: 100
+
+%s''' % compress(JSON_DATA)
+
+
+def test_Request_open_gzip(urlopen_mock):
+ h = http_client.HTTPResponse(
+ Sock(GZIP_RESP),
+ method='GET',
+ )
+ h.begin()
+
+ if PY3:
+ urlopen_mock.return_value = h
+ else:
+ urlopen_mock.return_value = addinfourl(
+ h.fp,
+ h.msg,
+ 'http://ansible.com/',
+ h.status,
+ )
+ urlopen_mock.return_value.msg = h.reason
+
+ r = Request().open('GET', 'https://ansible.com/')
+ assert isinstance(r.fp, GzipDecodedReader)
+ assert r.read() == JSON_DATA
+
+
+def test_Request_open_not_gzip(urlopen_mock):
+ h = http_client.HTTPResponse(
+ Sock(RESP),
+ method='GET',
+ )
+ h.begin()
+
+ if PY3:
+ urlopen_mock.return_value = h
+ else:
+ urlopen_mock.return_value = addinfourl(
+ h.fp,
+ h.msg,
+ 'http://ansible.com/',
+ h.status,
+ )
+ urlopen_mock.return_value.msg = h.reason
+
+ r = Request().open('GET', 'https://ansible.com/')
+ assert not isinstance(r.fp, GzipDecodedReader)
+ assert r.read() == JSON_DATA
+
+
+def test_Request_open_decompress_false(urlopen_mock):
+ h = http_client.HTTPResponse(
+ Sock(RESP),
+ method='GET',
+ )
+ h.begin()
+
+ if PY3:
+ urlopen_mock.return_value = h
+ else:
+ urlopen_mock.return_value = addinfourl(
+ h.fp,
+ h.msg,
+ 'http://ansible.com/',
+ h.status,
+ )
+ urlopen_mock.return_value.msg = h.reason
+
+ r = Request().open('GET', 'https://ansible.com/', decompress=False)
+ assert not isinstance(r.fp, GzipDecodedReader)
+ assert r.read() == JSON_DATA
+
+
+def test_GzipDecodedReader_no_gzip(monkeypatch, mocker):
+ monkeypatch.delitem(sys.modules, 'gzip')
+ monkeypatch.delitem(sys.modules, 'ansible.module_utils.urls')
+
+ orig_import = __import__
+
+ def _import(*args):
+ if args[0] == 'gzip':
+ raise ImportError
+ return orig_import(*args)
+
+ if PY3:
+ mocker.patch('builtins.__import__', _import)
+ else:
+ mocker.patch('__builtin__.__import__', _import)
+
+ mod = __import__('ansible.module_utils.urls').module_utils.urls
+ assert mod.HAS_GZIP is False
+ pytest.raises(mod.MissingModuleError, mod.GzipDecodedReader, None)
diff --git a/test/units/module_utils/urls/test_prepare_multipart.py b/test/units/module_utils/urls/test_prepare_multipart.py
new file mode 100644
index 0000000..226d9ed
--- /dev/null
+++ b/test/units/module_utils/urls/test_prepare_multipart.py
@@ -0,0 +1,103 @@
+# -*- coding: utf-8 -*-
+# (c) 2020 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+
+from io import StringIO
+
+from email.message import Message
+
+import pytest
+
+from ansible.module_utils.urls import prepare_multipart
+
+
+def test_prepare_multipart():
+ fixture_boundary = b'===============3996062709511591449=='
+
+ here = os.path.dirname(__file__)
+ multipart = os.path.join(here, 'fixtures/multipart.txt')
+
+ client_cert = os.path.join(here, 'fixtures/client.pem')
+ client_key = os.path.join(here, 'fixtures/client.key')
+ client_txt = os.path.join(here, 'fixtures/client.txt')
+ fields = {
+ 'form_field_1': 'form_value_1',
+ 'form_field_2': {
+ 'content': 'form_value_2',
+ },
+ 'form_field_3': {
+ 'content': '<html></html>',
+ 'mime_type': 'text/html',
+ },
+ 'form_field_4': {
+ 'content': '{"foo": "bar"}',
+ 'mime_type': 'application/json',
+ },
+ 'file1': {
+ 'content': 'file_content_1',
+ 'filename': 'fake_file1.txt',
+ },
+ 'file2': {
+ 'content': '<html></html>',
+ 'mime_type': 'text/html',
+ 'filename': 'fake_file2.html',
+ },
+ 'file3': {
+ 'content': '{"foo": "bar"}',
+ 'mime_type': 'application/json',
+ 'filename': 'fake_file3.json',
+ },
+ 'file4': {
+ 'filename': client_cert,
+ 'mime_type': 'text/plain',
+ },
+ 'file5': {
+ 'filename': client_key,
+ 'mime_type': 'application/octet-stream'
+ },
+ 'file6': {
+ 'filename': client_txt,
+ },
+ }
+
+ content_type, b_data = prepare_multipart(fields)
+
+ headers = Message()
+ headers['Content-Type'] = content_type
+ assert headers.get_content_type() == 'multipart/form-data'
+ boundary = headers.get_boundary()
+ assert boundary is not None
+
+ with open(multipart, 'rb') as f:
+ b_expected = f.read().replace(fixture_boundary, boundary.encode())
+
+ # Depending on Python version, there may or may not be a trailing newline
+ assert b_data.rstrip(b'\r\n') == b_expected.rstrip(b'\r\n')
+
+
+def test_wrong_type():
+ pytest.raises(TypeError, prepare_multipart, 'foo')
+ pytest.raises(TypeError, prepare_multipart, {'foo': None})
+
+
+def test_empty():
+ pytest.raises(ValueError, prepare_multipart, {'foo': {}})
+
+
+def test_unknown_mime(mocker):
+ fields = {'foo': {'filename': 'foo.boom', 'content': 'foo'}}
+ mocker.patch('mimetypes.guess_type', return_value=(None, None))
+ content_type, b_data = prepare_multipart(fields)
+ assert b'Content-Type: application/octet-stream' in b_data
+
+
+def test_bad_mime(mocker):
+ fields = {'foo': {'filename': 'foo.boom', 'content': 'foo'}}
+ mocker.patch('mimetypes.guess_type', side_effect=TypeError)
+ content_type, b_data = prepare_multipart(fields)
+ assert b'Content-Type: application/octet-stream' in b_data
diff --git a/test/units/module_utils/urls/test_split.py b/test/units/module_utils/urls/test_split.py
new file mode 100644
index 0000000..7fd5fc1
--- /dev/null
+++ b/test/units/module_utils/urls/test_split.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+# Copyright: Contributors to the Ansible project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+
+from ansible.module_utils.urls import _split_multiext
+
+
+@pytest.mark.parametrize(
+ 'name, expected',
+ (
+ ('', ('', '')),
+ ('a', ('a', '')),
+ ('file.tar', ('file', '.tar')),
+ ('file.tar.', ('file.tar.', '')),
+ ('file.hidden', ('file.hidden', '')),
+ ('file.tar.gz', ('file', '.tar.gz')),
+ ('yaml-0.2.5.tar.gz', ('yaml-0.2.5', '.tar.gz')),
+ ('yaml-0.2.5.zip', ('yaml-0.2.5', '.zip')),
+ ('yaml-0.2.5.zip.hidden', ('yaml-0.2.5.zip.hidden', '')),
+ ('geckodriver-v0.26.0-linux64.tar', ('geckodriver-v0.26.0-linux64', '.tar')),
+ ('/var/lib/geckodriver-v0.26.0-linux64.tar', ('/var/lib/geckodriver-v0.26.0-linux64', '.tar')),
+ ('https://acme.com/drivers/geckodriver-v0.26.0-linux64.tar', ('https://acme.com/drivers/geckodriver-v0.26.0-linux64', '.tar')),
+ ('https://acme.com/drivers/geckodriver-v0.26.0-linux64.tar.bz', ('https://acme.com/drivers/geckodriver-v0.26.0-linux64', '.tar.bz')),
+ )
+)
+def test__split_multiext(name, expected):
+ assert expected == _split_multiext(name)
+
+
+@pytest.mark.parametrize(
+ 'args, expected',
+ (
+ (('base-v0.26.0-linux64.tar.gz', 4, 4), ('base-v0.26.0-linux64.tar.gz', '')),
+ (('base-v0.26.0.hidden', 1, 7), ('base-v0.26', '.0.hidden')),
+ (('base-v0.26.0.hidden', 3, 4), ('base-v0.26.0.hidden', '')),
+ (('base-v0.26.0.hidden.tar', 1, 7), ('base-v0.26.0', '.hidden.tar')),
+ (('base-v0.26.0.hidden.tar.gz', 1, 7), ('base-v0.26.0.hidden', '.tar.gz')),
+ (('base-v0.26.0.hidden.tar.gz', 4, 7), ('base-v0.26.0.hidden.tar.gz', '')),
+ )
+)
+def test__split_multiext_min_max(args, expected):
+ assert expected == _split_multiext(*args)
+
+
+@pytest.mark.parametrize(
+ 'kwargs, expected', (
+ (({'name': 'base-v0.25.0.tar.gz', 'count': 1}), ('base-v0.25.0.tar', '.gz')),
+ (({'name': 'base-v0.25.0.tar.gz', 'count': 2}), ('base-v0.25.0', '.tar.gz')),
+ (({'name': 'base-v0.25.0.tar.gz', 'count': 3}), ('base-v0.25.0', '.tar.gz')),
+ (({'name': 'base-v0.25.0.tar.gz', 'count': 4}), ('base-v0.25.0', '.tar.gz')),
+ (({'name': 'base-v0.25.foo.tar.gz', 'count': 3}), ('base-v0.25', '.foo.tar.gz')),
+ (({'name': 'base-v0.25.foo.tar.gz', 'count': 4}), ('base-v0', '.25.foo.tar.gz')),
+ )
+)
+def test__split_multiext_count(kwargs, expected):
+ assert expected == _split_multiext(**kwargs)
+
+
+@pytest.mark.parametrize(
+ 'name',
+ (
+ list(),
+ tuple(),
+ dict(),
+ set(),
+ 1.729879,
+ 247,
+ )
+)
+def test__split_multiext_invalid(name):
+ with pytest.raises((TypeError, AttributeError)):
+ _split_multiext(name)
diff --git a/test/units/module_utils/urls/test_urls.py b/test/units/module_utils/urls/test_urls.py
new file mode 100644
index 0000000..69c1b82
--- /dev/null
+++ b/test/units/module_utils/urls/test_urls.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+# (c) 2018 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils import urls
+from ansible.module_utils._text import to_native
+
+import pytest
+
+
+def test_build_ssl_validation_error(mocker):
+ mocker.patch.object(urls, 'HAS_SSLCONTEXT', new=False)
+ mocker.patch.object(urls, 'HAS_URLLIB3_PYOPENSSLCONTEXT', new=False)
+ mocker.patch.object(urls, 'HAS_URLLIB3_SSL_WRAP_SOCKET', new=False)
+ with pytest.raises(urls.SSLValidationError) as excinfo:
+ urls.build_ssl_validation_error('hostname', 'port', 'paths', exc=None)
+
+ assert 'python >= 2.7.9' in to_native(excinfo.value)
+ assert 'the python executable used' in to_native(excinfo.value)
+ assert 'urllib3' in to_native(excinfo.value)
+ assert 'python >= 2.6' in to_native(excinfo.value)
+ assert 'validate_certs=False' in to_native(excinfo.value)
+
+ mocker.patch.object(urls, 'HAS_SSLCONTEXT', new=True)
+ with pytest.raises(urls.SSLValidationError) as excinfo:
+ urls.build_ssl_validation_error('hostname', 'port', 'paths', exc=None)
+
+ assert 'validate_certs=False' in to_native(excinfo.value)
+
+ mocker.patch.object(urls, 'HAS_SSLCONTEXT', new=False)
+ mocker.patch.object(urls, 'HAS_URLLIB3_PYOPENSSLCONTEXT', new=True)
+ mocker.patch.object(urls, 'HAS_URLLIB3_SSL_WRAP_SOCKET', new=True)
+
+ mocker.patch.object(urls, 'HAS_SSLCONTEXT', new=True)
+ with pytest.raises(urls.SSLValidationError) as excinfo:
+ urls.build_ssl_validation_error('hostname', 'port', 'paths', exc=None)
+
+ assert 'urllib3' not in to_native(excinfo.value)
+
+ with pytest.raises(urls.SSLValidationError) as excinfo:
+ urls.build_ssl_validation_error('hostname', 'port', 'paths', exc='BOOM')
+
+ assert 'BOOM' in to_native(excinfo.value)
+
+
+def test_maybe_add_ssl_handler(mocker):
+ mocker.patch.object(urls, 'HAS_SSL', new=False)
+ with pytest.raises(urls.NoSSLError):
+ urls.maybe_add_ssl_handler('https://ansible.com/', True)
+
+ mocker.patch.object(urls, 'HAS_SSL', new=True)
+ url = 'https://user:passwd@ansible.com/'
+ handler = urls.maybe_add_ssl_handler(url, True)
+ assert handler.hostname == 'ansible.com'
+ assert handler.port == 443
+
+ url = 'https://ansible.com:4433/'
+ handler = urls.maybe_add_ssl_handler(url, True)
+ assert handler.hostname == 'ansible.com'
+ assert handler.port == 4433
+
+ url = 'https://user:passwd@ansible.com:4433/'
+ handler = urls.maybe_add_ssl_handler(url, True)
+ assert handler.hostname == 'ansible.com'
+ assert handler.port == 4433
+
+ url = 'https://ansible.com/'
+ handler = urls.maybe_add_ssl_handler(url, True)
+ assert handler.hostname == 'ansible.com'
+ assert handler.port == 443
+
+ url = 'http://ansible.com/'
+ handler = urls.maybe_add_ssl_handler(url, True)
+ assert handler is None
+
+ url = 'https://[2a00:16d8:0:7::205]:4443/'
+ handler = urls.maybe_add_ssl_handler(url, True)
+ assert handler.hostname == '2a00:16d8:0:7::205'
+ assert handler.port == 4443
+
+ url = 'https://[2a00:16d8:0:7::205]/'
+ handler = urls.maybe_add_ssl_handler(url, True)
+ assert handler.hostname == '2a00:16d8:0:7::205'
+ assert handler.port == 443
+
+
+def test_basic_auth_header():
+ header = urls.basic_auth_header('user', 'passwd')
+ assert header == b'Basic dXNlcjpwYXNzd2Q='
+
+
+def test_ParseResultDottedDict():
+ url = 'https://ansible.com/blog'
+ parts = urls.urlparse(url)
+ dotted_parts = urls.ParseResultDottedDict(parts._asdict())
+ assert parts[0] == dotted_parts.scheme
+
+ assert dotted_parts.as_list() == list(parts)
+
+
+def test_unix_socket_patch_httpconnection_connect(mocker):
+ unix_conn = mocker.patch.object(urls.UnixHTTPConnection, 'connect')
+ conn = urls.httplib.HTTPConnection('ansible.com')
+ with urls.unix_socket_patch_httpconnection_connect():
+ conn.connect()
+ assert unix_conn.call_count == 1
diff --git a/test/units/modules/__init__.py b/test/units/modules/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/modules/__init__.py
diff --git a/test/units/modules/conftest.py b/test/units/modules/conftest.py
new file mode 100644
index 0000000..a7d1e04
--- /dev/null
+++ b/test/units/modules/conftest.py
@@ -0,0 +1,31 @@
+# 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
+
+import json
+
+import pytest
+
+from ansible.module_utils.six import string_types
+from ansible.module_utils._text import to_bytes
+from ansible.module_utils.common._collections_compat import MutableMapping
+
+
+@pytest.fixture
+def patch_ansible_module(request, mocker):
+ if isinstance(request.param, string_types):
+ args = request.param
+ elif isinstance(request.param, MutableMapping):
+ if 'ANSIBLE_MODULE_ARGS' not in request.param:
+ request.param = {'ANSIBLE_MODULE_ARGS': request.param}
+ if '_ansible_remote_tmp' not in request.param['ANSIBLE_MODULE_ARGS']:
+ request.param['ANSIBLE_MODULE_ARGS']['_ansible_remote_tmp'] = '/tmp'
+ if '_ansible_keep_remote_files' not in request.param['ANSIBLE_MODULE_ARGS']:
+ request.param['ANSIBLE_MODULE_ARGS']['_ansible_keep_remote_files'] = False
+ args = json.dumps(request.param)
+ else:
+ raise Exception('Malformed data to the patch_ansible_module pytest fixture')
+
+ mocker.patch('ansible.module_utils.basic._ANSIBLE_ARGS', to_bytes(args))
diff --git a/test/units/modules/test_apt.py b/test/units/modules/test_apt.py
new file mode 100644
index 0000000..20e056f
--- /dev/null
+++ b/test/units/modules/test_apt.py
@@ -0,0 +1,53 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import collections
+import sys
+
+from units.compat.mock import Mock
+from units.compat import unittest
+
+try:
+ from ansible.modules.apt import (
+ expand_pkgspec_from_fnmatches,
+ )
+except Exception:
+ # Need some more module_utils work (porting urls.py) before we can test
+ # modules. So don't error out in this case.
+ if sys.version_info[0] >= 3:
+ pass
+
+
+class AptExpandPkgspecTestCase(unittest.TestCase):
+
+ def setUp(self):
+ FakePackage = collections.namedtuple("Package", ("name",))
+ self.fake_cache = [
+ FakePackage("apt"),
+ FakePackage("apt-utils"),
+ FakePackage("not-selected"),
+ ]
+
+ def test_trivial(self):
+ foo = ["apt"]
+ self.assertEqual(
+ expand_pkgspec_from_fnmatches(None, foo, self.fake_cache), foo)
+
+ def test_version_wildcard(self):
+ foo = ["apt=1.0*"]
+ self.assertEqual(
+ expand_pkgspec_from_fnmatches(None, foo, self.fake_cache), foo)
+
+ def test_pkgname_wildcard_version_wildcard(self):
+ foo = ["apt*=1.0*"]
+ m_mock = Mock()
+ self.assertEqual(
+ expand_pkgspec_from_fnmatches(m_mock, foo, self.fake_cache),
+ ['apt', 'apt-utils'])
+
+ def test_pkgname_expands(self):
+ foo = ["apt*"]
+ m_mock = Mock()
+ self.assertEqual(
+ expand_pkgspec_from_fnmatches(m_mock, foo, self.fake_cache),
+ ["apt", "apt-utils"])
diff --git a/test/units/modules/test_apt_key.py b/test/units/modules/test_apt_key.py
new file mode 100644
index 0000000..37cd53b
--- /dev/null
+++ b/test/units/modules/test_apt_key.py
@@ -0,0 +1,32 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+from units.compat.mock import patch, Mock
+from units.compat import unittest
+
+from ansible.modules import apt_key
+
+
+def returnc(x):
+ return 'C'
+
+
+class AptKeyTestCase(unittest.TestCase):
+
+ @patch.object(apt_key, 'apt_key_bin', '/usr/bin/apt-key')
+ @patch.object(apt_key, 'lang_env', returnc)
+ @patch.dict(os.environ, {'HTTP_PROXY': 'proxy.example.com'})
+ def test_import_key_with_http_proxy(self):
+ m_mock = Mock()
+ m_mock.run_command.return_value = (0, '', '')
+ apt_key.import_key(
+ m_mock, keyring=None, keyserver='keyserver.example.com',
+ key_id='0xDEADBEEF')
+ self.assertEqual(
+ m_mock.run_command.call_args_list[0][0][0],
+ '/usr/bin/apt-key adv --no-tty --keyserver keyserver.example.com'
+ ' --keyserver-options http-proxy=proxy.example.com'
+ ' --recv 0xDEADBEEF'
+ )
diff --git a/test/units/modules/test_async_wrapper.py b/test/units/modules/test_async_wrapper.py
new file mode 100644
index 0000000..37b1fda
--- /dev/null
+++ b/test/units/modules/test_async_wrapper.py
@@ -0,0 +1,58 @@
+# 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
+
+
+import os
+import json
+import shutil
+import tempfile
+
+import pytest
+
+from units.compat.mock import patch, MagicMock
+from ansible.modules import async_wrapper
+
+from pprint import pprint
+
+
+class TestAsyncWrapper:
+
+ def test_run_module(self, monkeypatch):
+
+ def mock_get_interpreter(module_path):
+ return ['/usr/bin/python']
+
+ module_result = {'rc': 0}
+ module_lines = [
+ '#!/usr/bin/python',
+ 'import sys',
+ 'sys.stderr.write("stderr stuff")',
+ "print('%s')" % json.dumps(module_result)
+ ]
+ module_data = '\n'.join(module_lines) + '\n'
+ module_data = module_data.encode('utf-8')
+
+ workdir = tempfile.mkdtemp()
+ fh, fn = tempfile.mkstemp(dir=workdir)
+
+ with open(fn, 'wb') as f:
+ f.write(module_data)
+
+ command = fn
+ jobid = 0
+ job_path = os.path.join(os.path.dirname(command), 'job')
+
+ monkeypatch.setattr(async_wrapper, '_get_interpreter', mock_get_interpreter)
+ monkeypatch.setattr(async_wrapper, 'job_path', job_path)
+
+ res = async_wrapper._run_module(command, jobid)
+
+ with open(os.path.join(workdir, 'job'), 'r') as f:
+ jres = json.loads(f.read())
+
+ shutil.rmtree(workdir)
+
+ assert jres.get('rc') == 0
+ assert jres.get('stderr') == 'stderr stuff'
diff --git a/test/units/modules/test_copy.py b/test/units/modules/test_copy.py
new file mode 100644
index 0000000..20c309b
--- /dev/null
+++ b/test/units/modules/test_copy.py
@@ -0,0 +1,215 @@
+# -*- coding: utf-8 -*-
+# Copyright:
+# (c) 2018 Ansible Project
+# License: 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 pytest
+
+from ansible.modules.copy import AnsibleModuleError, split_pre_existing_dir
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+THREE_DIRS_DATA = (('/dir1/dir2',
+ # 0 existing dirs: error (because / should always exist)
+ None,
+ # 1 existing dir:
+ ('/', ['dir1', 'dir2']),
+ # 2 existing dirs:
+ ('/dir1', ['dir2']),
+ # 3 existing dirs:
+ ('/dir1/dir2', [])
+ ),
+ ('/dir1/dir2/',
+ # 0 existing dirs: error (because / should always exist)
+ None,
+ # 1 existing dir:
+ ('/', ['dir1', 'dir2']),
+ # 2 existing dirs:
+ ('/dir1', ['dir2']),
+ # 3 existing dirs:
+ ('/dir1/dir2', [])
+ ),
+ )
+
+
+TWO_DIRS_DATA = (('dir1/dir2',
+ # 0 existing dirs:
+ ('.', ['dir1', 'dir2']),
+ # 1 existing dir:
+ ('dir1', ['dir2']),
+ # 2 existing dirs:
+ ('dir1/dir2', []),
+ # 3 existing dirs: Same as 2 because we never get to the third
+ ),
+ ('dir1/dir2/',
+ # 0 existing dirs:
+ ('.', ['dir1', 'dir2']),
+ # 1 existing dir:
+ ('dir1', ['dir2']),
+ # 2 existing dirs:
+ ('dir1/dir2', []),
+ # 3 existing dirs: Same as 2 because we never get to the third
+ ),
+ ('/dir1',
+ # 0 existing dirs: error (because / should always exist)
+ None,
+ # 1 existing dir:
+ ('/', ['dir1']),
+ # 2 existing dirs:
+ ('/dir1', []),
+ # 3 existing dirs: Same as 2 because we never get to the third
+ ),
+ ('/dir1/',
+ # 0 existing dirs: error (because / should always exist)
+ None,
+ # 1 existing dir:
+ ('/', ['dir1']),
+ # 2 existing dirs:
+ ('/dir1', []),
+ # 3 existing dirs: Same as 2 because we never get to the third
+ ),
+ ) + THREE_DIRS_DATA
+
+
+ONE_DIR_DATA = (('dir1',
+ # 0 existing dirs:
+ ('.', ['dir1']),
+ # 1 existing dir:
+ ('dir1', []),
+ # 2 existing dirs: Same as 1 because we never get to the third
+ ),
+ ('dir1/',
+ # 0 existing dirs:
+ ('.', ['dir1']),
+ # 1 existing dir:
+ ('dir1', []),
+ # 2 existing dirs: Same as 1 because we never get to the third
+ ),
+ ) + TWO_DIRS_DATA
+
+
+@pytest.mark.parametrize('directory, expected', ((d[0], d[4]) for d in THREE_DIRS_DATA))
+def test_split_pre_existing_dir_three_levels_exist(directory, expected, mocker):
+ mocker.patch('os.path.exists', side_effect=[True, True, True])
+ split_pre_existing_dir(directory) == expected
+
+
+@pytest.mark.parametrize('directory, expected', ((d[0], d[3]) for d in TWO_DIRS_DATA))
+def test_split_pre_existing_dir_two_levels_exist(directory, expected, mocker):
+ mocker.patch('os.path.exists', side_effect=[True, True, False])
+ split_pre_existing_dir(directory) == expected
+
+
+@pytest.mark.parametrize('directory, expected', ((d[0], d[2]) for d in ONE_DIR_DATA))
+def test_split_pre_existing_dir_one_level_exists(directory, expected, mocker):
+ mocker.patch('os.path.exists', side_effect=[True, False, False])
+ split_pre_existing_dir(directory) == expected
+
+
+@pytest.mark.parametrize('directory', (d[0] for d in ONE_DIR_DATA if d[1] is None))
+def test_split_pre_existing_dir_root_does_not_exist(directory, mocker):
+ mocker.patch('os.path.exists', return_value=False)
+ with pytest.raises(AnsibleModuleError) as excinfo:
+ split_pre_existing_dir(directory)
+ assert excinfo.value.results['msg'].startswith("The '/' directory doesn't exist on this machine.")
+
+
+@pytest.mark.parametrize('directory, expected', ((d[0], d[1]) for d in ONE_DIR_DATA if not d[0].startswith('/')))
+def test_split_pre_existing_dir_working_dir_exists(directory, expected, mocker):
+ mocker.patch('os.path.exists', return_value=False)
+ split_pre_existing_dir(directory) == expected
+
+
+#
+# Info helpful for making new test cases:
+#
+# base_mode = {'dir no perms': 0o040000,
+# 'file no perms': 0o100000,
+# 'dir all perms': 0o400000 | 0o777,
+# 'file all perms': 0o100000, | 0o777}
+#
+# perm_bits = {'x': 0b001,
+# 'w': 0b010,
+# 'r': 0b100}
+#
+# role_shift = {'u': 6,
+# 'g': 3,
+# 'o': 0}
+
+DATA = ( # Going from no permissions to setting all for user, group, and/or other
+ (0o040000, u'a+rwx', 0o0777),
+ (0o040000, u'u+rwx,g+rwx,o+rwx', 0o0777),
+ (0o040000, u'o+rwx', 0o0007),
+ (0o040000, u'g+rwx', 0o0070),
+ (0o040000, u'u+rwx', 0o0700),
+
+ # Going from all permissions to none for user, group, and/or other
+ (0o040777, u'a-rwx', 0o0000),
+ (0o040777, u'u-rwx,g-rwx,o-rwx', 0o0000),
+ (0o040777, u'o-rwx', 0o0770),
+ (0o040777, u'g-rwx', 0o0707),
+ (0o040777, u'u-rwx', 0o0077),
+
+ # now using absolute assignment from None to a set of perms
+ (0o040000, u'a=rwx', 0o0777),
+ (0o040000, u'u=rwx,g=rwx,o=rwx', 0o0777),
+ (0o040000, u'o=rwx', 0o0007),
+ (0o040000, u'g=rwx', 0o0070),
+ (0o040000, u'u=rwx', 0o0700),
+
+ # X effect on files and dirs
+ (0o040000, u'a+X', 0o0111),
+ (0o100000, u'a+X', 0),
+ (0o040000, u'a=X', 0o0111),
+ (0o100000, u'a=X', 0),
+ (0o040777, u'a-X', 0o0666),
+ # Same as chmod but is it a bug?
+ # chmod a-X statfile <== removes execute from statfile
+ (0o100777, u'a-X', 0o0666),
+
+ # Multiple permissions
+ (0o040000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0755),
+ (0o100000, u'u=rw-x+X,g=r-x+X,o=r-x+X', 0o0644),
+)
+
+UMASK_DATA = (
+ (0o100000, '+rwx', 0o770),
+ (0o100777, '-rwx', 0o007),
+)
+
+INVALID_DATA = (
+ (0o040000, u'a=foo', "bad symbolic permission for mode: a=foo"),
+ (0o040000, u'f=rwx', "bad symbolic permission for mode: f=rwx"),
+)
+
+
+@pytest.mark.parametrize('stat_info, mode_string, expected', DATA)
+def test_good_symbolic_modes(mocker, stat_info, mode_string, expected):
+ mock_stat = mocker.MagicMock()
+ mock_stat.st_mode = stat_info
+ assert AnsibleModule._symbolic_mode_to_octal(mock_stat, mode_string) == expected
+
+
+@pytest.mark.parametrize('stat_info, mode_string, expected', UMASK_DATA)
+def test_umask_with_symbolic_modes(mocker, stat_info, mode_string, expected):
+ mock_umask = mocker.patch('os.umask')
+ mock_umask.return_value = 0o7
+
+ mock_stat = mocker.MagicMock()
+ mock_stat.st_mode = stat_info
+
+ assert AnsibleModule._symbolic_mode_to_octal(mock_stat, mode_string) == expected
+
+
+@pytest.mark.parametrize('stat_info, mode_string, expected', INVALID_DATA)
+def test_invalid_symbolic_modes(mocker, stat_info, mode_string, expected):
+ mock_stat = mocker.MagicMock()
+ mock_stat.st_mode = stat_info
+ with pytest.raises(ValueError) as exc:
+ assert AnsibleModule._symbolic_mode_to_octal(mock_stat, mode_string) == 'blah'
+ assert exc.match(expected)
diff --git a/test/units/modules/test_hostname.py b/test/units/modules/test_hostname.py
new file mode 100644
index 0000000..9050fd0
--- /dev/null
+++ b/test/units/modules/test_hostname.py
@@ -0,0 +1,147 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import shutil
+import tempfile
+
+from units.compat.mock import patch, MagicMock, mock_open
+from ansible.module_utils import basic
+from ansible.module_utils.common._utils import get_all_subclasses
+from ansible.modules import hostname
+from units.modules.utils import ModuleTestCase, set_module_args
+from ansible.module_utils.six import PY2
+
+
+class TestHostname(ModuleTestCase):
+ @patch('os.path.isfile')
+ def test_stategy_get_never_writes_in_check_mode(self, isfile):
+ isfile.return_value = True
+
+ set_module_args({'name': 'fooname', '_ansible_check_mode': True})
+ subclasses = get_all_subclasses(hostname.BaseStrategy)
+ module = MagicMock()
+ for cls in subclasses:
+ instance = cls(module)
+
+ instance.module.run_command = MagicMock()
+ instance.module.run_command.return_value = (0, '', '')
+
+ m = mock_open()
+ builtins = 'builtins'
+ if PY2:
+ builtins = '__builtin__'
+ with patch('%s.open' % builtins, m):
+ instance.get_permanent_hostname()
+ instance.get_current_hostname()
+ self.assertFalse(
+ m.return_value.write.called,
+ msg='%s called write, should not have' % str(cls))
+
+ def test_all_named_strategies_exist(self):
+ """Loop through the STRATS and see if anything is missing."""
+ for _name, prefix in hostname.STRATS.items():
+ classname = "%sStrategy" % prefix
+ cls = getattr(hostname, classname, None)
+
+ if cls is None:
+ self.assertFalse(
+ cls is None, "%s is None, should be a subclass" % classname
+ )
+ else:
+ self.assertTrue(issubclass(cls, hostname.BaseStrategy))
+
+
+class TestRedhatStrategy(ModuleTestCase):
+ def setUp(self):
+ super(TestRedhatStrategy, self).setUp()
+ self.testdir = tempfile.mkdtemp(prefix='ansible-test-hostname-')
+ self.network_file = os.path.join(self.testdir, "network")
+
+ def tearDown(self):
+ super(TestRedhatStrategy, self).tearDown()
+ shutil.rmtree(self.testdir, ignore_errors=True)
+
+ @property
+ def instance(self):
+ self.module = MagicMock()
+ instance = hostname.RedHatStrategy(self.module)
+ instance.NETWORK_FILE = self.network_file
+ return instance
+
+ def test_get_permanent_hostname_missing(self):
+ self.assertIsNone(self.instance.get_permanent_hostname())
+ self.assertTrue(self.module.fail_json.called)
+ self.module.fail_json.assert_called_with(
+ "Unable to locate HOSTNAME entry in %s" % self.network_file
+ )
+
+ def test_get_permanent_hostname_line_missing(self):
+ with open(self.network_file, "w") as f:
+ f.write("# some other content\n")
+ self.assertIsNone(self.instance.get_permanent_hostname())
+ self.module.fail_json.assert_called_with(
+ "Unable to locate HOSTNAME entry in %s" % self.network_file
+ )
+
+ def test_get_permanent_hostname_existing(self):
+ with open(self.network_file, "w") as f:
+ f.write(
+ "some other content\n"
+ "HOSTNAME=foobar\n"
+ "more content\n"
+ )
+ self.assertEqual(self.instance.get_permanent_hostname(), "foobar")
+
+ def test_get_permanent_hostname_existing_whitespace(self):
+ with open(self.network_file, "w") as f:
+ f.write(
+ "some other content\n"
+ " HOSTNAME=foobar \n"
+ "more content\n"
+ )
+ self.assertEqual(self.instance.get_permanent_hostname(), "foobar")
+
+ def test_set_permanent_hostname_missing(self):
+ self.instance.set_permanent_hostname("foobar")
+ with open(self.network_file) as f:
+ self.assertEqual(f.read(), "HOSTNAME=foobar\n")
+
+ def test_set_permanent_hostname_line_missing(self):
+ with open(self.network_file, "w") as f:
+ f.write("# some other content\n")
+ self.instance.set_permanent_hostname("foobar")
+ with open(self.network_file) as f:
+ self.assertEqual(f.read(), "# some other content\nHOSTNAME=foobar\n")
+
+ def test_set_permanent_hostname_existing(self):
+ with open(self.network_file, "w") as f:
+ f.write(
+ "some other content\n"
+ "HOSTNAME=spam\n"
+ "more content\n"
+ )
+ self.instance.set_permanent_hostname("foobar")
+ with open(self.network_file) as f:
+ self.assertEqual(
+ f.read(),
+ "some other content\n"
+ "HOSTNAME=foobar\n"
+ "more content\n"
+ )
+
+ def test_set_permanent_hostname_existing_whitespace(self):
+ with open(self.network_file, "w") as f:
+ f.write(
+ "some other content\n"
+ " HOSTNAME=spam \n"
+ "more content\n"
+ )
+ self.instance.set_permanent_hostname("foobar")
+ with open(self.network_file) as f:
+ self.assertEqual(
+ f.read(),
+ "some other content\n"
+ "HOSTNAME=foobar\n"
+ "more content\n"
+ )
diff --git a/test/units/modules/test_iptables.py b/test/units/modules/test_iptables.py
new file mode 100644
index 0000000..265e770
--- /dev/null
+++ b/test/units/modules/test_iptables.py
@@ -0,0 +1,1192 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat.mock import patch
+from ansible.module_utils import basic
+from ansible.modules import iptables
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
+
+
+def get_bin_path(*args, **kwargs):
+ return "/sbin/iptables"
+
+
+def get_iptables_version(iptables_path, module):
+ return "1.8.2"
+
+
+class TestIptables(ModuleTestCase):
+
+ def setUp(self):
+ super(TestIptables, self).setUp()
+ self.mock_get_bin_path = patch.object(basic.AnsibleModule, 'get_bin_path', get_bin_path)
+ self.mock_get_bin_path.start()
+ self.addCleanup(self.mock_get_bin_path.stop) # ensure that the patching is 'undone'
+ self.mock_get_iptables_version = patch.object(iptables, 'get_iptables_version', get_iptables_version)
+ self.mock_get_iptables_version.start()
+ self.addCleanup(self.mock_get_iptables_version.stop) # ensure that the patching is 'undone'
+
+ def test_without_required_parameters(self):
+ """Failure must occurs when all parameters are missing"""
+ with self.assertRaises(AnsibleFailJson):
+ set_module_args({})
+ iptables.main()
+
+ def test_flush_table_without_chain(self):
+ """Test flush without chain, flush the table"""
+ set_module_args({
+ 'flush': True,
+ })
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.return_value = 0, '', '' # successful execution, no output
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args[0][0][0], '/sbin/iptables')
+ self.assertEqual(run_command.call_args[0][0][1], '-t')
+ self.assertEqual(run_command.call_args[0][0][2], 'filter')
+ self.assertEqual(run_command.call_args[0][0][3], '-F')
+
+ def test_flush_table_check_true(self):
+ """Test flush without parameters and check == true"""
+ set_module_args({
+ 'flush': True,
+ '_ansible_check_mode': True,
+ })
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.return_value = 0, '', '' # successful execution, no output
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 0)
+
+# TODO ADD test flush table nat
+# TODO ADD test flush with chain
+# TODO ADD test flush with chain and table nat
+
+ def test_policy_table(self):
+ """Test change policy of a chain"""
+ set_module_args({
+ 'policy': 'ACCEPT',
+ 'chain': 'INPUT',
+ })
+ commands_results = [
+ (0, 'Chain INPUT (policy DROP)\n', ''),
+ (0, '', '')
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 2)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-L',
+ 'INPUT',
+ ])
+ self.assertEqual(run_command.call_args_list[1][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-P',
+ 'INPUT',
+ 'ACCEPT',
+ ])
+
+ def test_policy_table_no_change(self):
+ """Test don't change policy of a chain if the policy is right"""
+ set_module_args({
+ 'policy': 'ACCEPT',
+ 'chain': 'INPUT',
+ })
+ commands_results = [
+ (0, 'Chain INPUT (policy ACCEPT)\n', ''),
+ (0, '', '')
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertFalse(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-L',
+ 'INPUT',
+ ])
+
+ def test_policy_table_changed_false(self):
+ """Test flush without parameters and change == false"""
+ set_module_args({
+ 'policy': 'ACCEPT',
+ 'chain': 'INPUT',
+ '_ansible_check_mode': True,
+ })
+ commands_results = [
+ (0, 'Chain INPUT (policy DROP)\n', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-L',
+ 'INPUT',
+ ])
+
+# TODO ADD test policy without chain fail
+# TODO ADD test policy with chain don't exists
+# TODO ADD test policy with wrong choice fail
+
+ def test_insert_rule_change_false(self):
+ """Test flush without parameters"""
+ set_module_args({
+ 'chain': 'OUTPUT',
+ 'source': '1.2.3.4/32',
+ 'destination': '7.8.9.10/42',
+ 'jump': 'ACCEPT',
+ 'action': 'insert',
+ '_ansible_check_mode': True,
+ })
+
+ commands_results = [
+ (1, '', ''), # check_rule_present
+ (0, '', ''), # check_chain_present
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 2)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-C',
+ 'OUTPUT',
+ '-s',
+ '1.2.3.4/32',
+ '-d',
+ '7.8.9.10/42',
+ '-j',
+ 'ACCEPT'
+ ])
+
+ def test_insert_rule(self):
+ """Test flush without parameters"""
+ set_module_args({
+ 'chain': 'OUTPUT',
+ 'source': '1.2.3.4/32',
+ 'destination': '7.8.9.10/42',
+ 'jump': 'ACCEPT',
+ 'action': 'insert'
+ })
+
+ commands_results = [
+ (1, '', ''), # check_rule_present
+ (0, '', ''), # check_chain_present
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 3)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-C',
+ 'OUTPUT',
+ '-s',
+ '1.2.3.4/32',
+ '-d',
+ '7.8.9.10/42',
+ '-j',
+ 'ACCEPT'
+ ])
+ self.assertEqual(run_command.call_args_list[2][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-I',
+ 'OUTPUT',
+ '-s',
+ '1.2.3.4/32',
+ '-d',
+ '7.8.9.10/42',
+ '-j',
+ 'ACCEPT'
+ ])
+
+ def test_append_rule_check_mode(self):
+ """Test append a redirection rule in check mode"""
+ set_module_args({
+ 'chain': 'PREROUTING',
+ 'source': '1.2.3.4/32',
+ 'destination': '7.8.9.10/42',
+ 'jump': 'REDIRECT',
+ 'table': 'nat',
+ 'to_destination': '5.5.5.5/32',
+ 'protocol': 'udp',
+ 'destination_port': '22',
+ 'to_ports': '8600',
+ '_ansible_check_mode': True,
+ })
+
+ commands_results = [
+ (1, '', ''), # check_rule_present
+ (0, '', ''), # check_chain_present
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 2)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'nat',
+ '-C',
+ 'PREROUTING',
+ '-p',
+ 'udp',
+ '-s',
+ '1.2.3.4/32',
+ '-d',
+ '7.8.9.10/42',
+ '-j',
+ 'REDIRECT',
+ '--to-destination',
+ '5.5.5.5/32',
+ '--destination-port',
+ '22',
+ '--to-ports',
+ '8600'
+ ])
+
+ def test_append_rule(self):
+ """Test append a redirection rule"""
+ set_module_args({
+ 'chain': 'PREROUTING',
+ 'source': '1.2.3.4/32',
+ 'destination': '7.8.9.10/42',
+ 'jump': 'REDIRECT',
+ 'table': 'nat',
+ 'to_destination': '5.5.5.5/32',
+ 'protocol': 'udp',
+ 'destination_port': '22',
+ 'to_ports': '8600'
+ })
+
+ commands_results = [
+ (1, '', ''), # check_rule_present
+ (0, '', ''), # check_chain_present
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 3)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'nat',
+ '-C',
+ 'PREROUTING',
+ '-p',
+ 'udp',
+ '-s',
+ '1.2.3.4/32',
+ '-d',
+ '7.8.9.10/42',
+ '-j',
+ 'REDIRECT',
+ '--to-destination',
+ '5.5.5.5/32',
+ '--destination-port',
+ '22',
+ '--to-ports',
+ '8600'
+ ])
+ self.assertEqual(run_command.call_args_list[2][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'nat',
+ '-A',
+ 'PREROUTING',
+ '-p',
+ 'udp',
+ '-s',
+ '1.2.3.4/32',
+ '-d',
+ '7.8.9.10/42',
+ '-j',
+ 'REDIRECT',
+ '--to-destination',
+ '5.5.5.5/32',
+ '--destination-port',
+ '22',
+ '--to-ports',
+ '8600'
+ ])
+
+ def test_remove_rule(self):
+ """Test flush without parameters"""
+ set_module_args({
+ 'chain': 'PREROUTING',
+ 'source': '1.2.3.4/32',
+ 'destination': '7.8.9.10/42',
+ 'jump': 'SNAT',
+ 'table': 'nat',
+ 'to_source': '5.5.5.5/32',
+ 'protocol': 'udp',
+ 'source_port': '22',
+ 'to_ports': '8600',
+ 'state': 'absent',
+ 'in_interface': 'eth0',
+ 'out_interface': 'eth1',
+ 'comment': 'this is a comment'
+ })
+
+ commands_results = [
+ (0, '', ''),
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 2)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'nat',
+ '-C',
+ 'PREROUTING',
+ '-p',
+ 'udp',
+ '-s',
+ '1.2.3.4/32',
+ '-d',
+ '7.8.9.10/42',
+ '-j',
+ 'SNAT',
+ '--to-source',
+ '5.5.5.5/32',
+ '-i',
+ 'eth0',
+ '-o',
+ 'eth1',
+ '--source-port',
+ '22',
+ '--to-ports',
+ '8600',
+ '-m',
+ 'comment',
+ '--comment',
+ 'this is a comment'
+ ])
+ self.assertEqual(run_command.call_args_list[1][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'nat',
+ '-D',
+ 'PREROUTING',
+ '-p',
+ 'udp',
+ '-s',
+ '1.2.3.4/32',
+ '-d',
+ '7.8.9.10/42',
+ '-j',
+ 'SNAT',
+ '--to-source',
+ '5.5.5.5/32',
+ '-i',
+ 'eth0',
+ '-o',
+ 'eth1',
+ '--source-port',
+ '22',
+ '--to-ports',
+ '8600',
+ '-m',
+ 'comment',
+ '--comment',
+ 'this is a comment'
+ ])
+
+ def test_remove_rule_check_mode(self):
+ """Test flush without parameters check mode"""
+ set_module_args({
+ 'chain': 'PREROUTING',
+ 'source': '1.2.3.4/32',
+ 'destination': '7.8.9.10/42',
+ 'jump': 'SNAT',
+ 'table': 'nat',
+ 'to_source': '5.5.5.5/32',
+ 'protocol': 'udp',
+ 'source_port': '22',
+ 'to_ports': '8600',
+ 'state': 'absent',
+ 'in_interface': 'eth0',
+ 'out_interface': 'eth1',
+ 'comment': 'this is a comment',
+ '_ansible_check_mode': True,
+ })
+
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'nat',
+ '-C',
+ 'PREROUTING',
+ '-p',
+ 'udp',
+ '-s',
+ '1.2.3.4/32',
+ '-d',
+ '7.8.9.10/42',
+ '-j',
+ 'SNAT',
+ '--to-source',
+ '5.5.5.5/32',
+ '-i',
+ 'eth0',
+ '-o',
+ 'eth1',
+ '--source-port',
+ '22',
+ '--to-ports',
+ '8600',
+ '-m',
+ 'comment',
+ '--comment',
+ 'this is a comment'
+ ])
+
+ def test_insert_with_reject(self):
+ """ Using reject_with with a previously defined jump: REJECT results in two Jump statements #18988 """
+ set_module_args({
+ 'chain': 'INPUT',
+ 'protocol': 'tcp',
+ 'reject_with': 'tcp-reset',
+ 'ip_version': 'ipv4',
+ })
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-C',
+ 'INPUT',
+ '-p',
+ 'tcp',
+ '-j',
+ 'REJECT',
+ '--reject-with',
+ 'tcp-reset',
+ ])
+
+ def test_insert_jump_reject_with_reject(self):
+ """ Using reject_with with a previously defined jump: REJECT results in two Jump statements #18988 """
+ set_module_args({
+ 'chain': 'INPUT',
+ 'protocol': 'tcp',
+ 'jump': 'REJECT',
+ 'reject_with': 'tcp-reset',
+ 'ip_version': 'ipv4',
+ })
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-C',
+ 'INPUT',
+ '-p',
+ 'tcp',
+ '-j',
+ 'REJECT',
+ '--reject-with',
+ 'tcp-reset',
+ ])
+
+ def test_jump_tee_gateway_negative(self):
+ """ Missing gateway when JUMP is set to TEE """
+ set_module_args({
+ 'table': 'mangle',
+ 'chain': 'PREROUTING',
+ 'in_interface': 'eth0',
+ 'protocol': 'udp',
+ 'match': 'state',
+ 'jump': 'TEE',
+ 'ctstate': ['NEW'],
+ 'destination_port': '9521',
+ 'destination': '127.0.0.1'
+ })
+
+ with self.assertRaises(AnsibleFailJson) as e:
+ iptables.main()
+ self.assertTrue(e.exception.args[0]['failed'])
+ self.assertEqual(e.exception.args[0]['msg'], 'jump is TEE but all of the following are missing: gateway')
+
+ def test_jump_tee_gateway(self):
+ """ Using gateway when JUMP is set to TEE """
+ set_module_args({
+ 'table': 'mangle',
+ 'chain': 'PREROUTING',
+ 'in_interface': 'eth0',
+ 'protocol': 'udp',
+ 'match': 'state',
+ 'jump': 'TEE',
+ 'ctstate': ['NEW'],
+ 'destination_port': '9521',
+ 'gateway': '192.168.10.1',
+ 'destination': '127.0.0.1'
+ })
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t', 'mangle',
+ '-C', 'PREROUTING',
+ '-p', 'udp',
+ '-d', '127.0.0.1',
+ '-m', 'state',
+ '-j', 'TEE',
+ '--gateway', '192.168.10.1',
+ '-i', 'eth0',
+ '--destination-port', '9521',
+ '--state', 'NEW'
+ ])
+
+ def test_tcp_flags(self):
+ """ Test various ways of inputting tcp_flags """
+ args = [
+ {
+ 'chain': 'OUTPUT',
+ 'protocol': 'tcp',
+ 'jump': 'DROP',
+ 'tcp_flags': 'flags=ALL flags_set="ACK,RST,SYN,FIN"'
+ },
+ {
+ 'chain': 'OUTPUT',
+ 'protocol': 'tcp',
+ 'jump': 'DROP',
+ 'tcp_flags': {
+ 'flags': 'ALL',
+ 'flags_set': 'ACK,RST,SYN,FIN'
+ }
+ },
+ {
+ 'chain': 'OUTPUT',
+ 'protocol': 'tcp',
+ 'jump': 'DROP',
+ 'tcp_flags': {
+ 'flags': ['ALL'],
+ 'flags_set': ['ACK', 'RST', 'SYN', 'FIN']
+ }
+ },
+
+ ]
+
+ for item in args:
+ set_module_args(item)
+
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-C',
+ 'OUTPUT',
+ '-p',
+ 'tcp',
+ '--tcp-flags',
+ 'ALL',
+ 'ACK,RST,SYN,FIN',
+ '-j',
+ 'DROP'
+ ])
+
+ def test_log_level(self):
+ """ Test various ways of log level flag """
+
+ log_levels = ['0', '1', '2', '3', '4', '5', '6', '7',
+ 'emerg', 'alert', 'crit', 'error', 'warning', 'notice', 'info', 'debug']
+
+ for log_lvl in log_levels:
+ set_module_args({
+ 'chain': 'INPUT',
+ 'jump': 'LOG',
+ 'log_level': log_lvl,
+ 'source': '1.2.3.4/32',
+ 'log_prefix': '** DROP-this_ip **'
+ })
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-C', 'INPUT',
+ '-s', '1.2.3.4/32',
+ '-j', 'LOG',
+ '--log-prefix', '** DROP-this_ip **',
+ '--log-level', log_lvl
+ ])
+
+ def test_iprange(self):
+ """ Test iprange module with its flags src_range and dst_range """
+ set_module_args({
+ 'chain': 'INPUT',
+ 'match': ['iprange'],
+ 'src_range': '192.168.1.100-192.168.1.199',
+ 'jump': 'ACCEPT'
+ })
+
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-C',
+ 'INPUT',
+ '-m',
+ 'iprange',
+ '-j',
+ 'ACCEPT',
+ '--src-range',
+ '192.168.1.100-192.168.1.199',
+ ])
+
+ set_module_args({
+ 'chain': 'INPUT',
+ 'src_range': '192.168.1.100-192.168.1.199',
+ 'dst_range': '10.0.0.50-10.0.0.100',
+ 'jump': 'ACCEPT'
+ })
+
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-C',
+ 'INPUT',
+ '-j',
+ 'ACCEPT',
+ '-m',
+ 'iprange',
+ '--src-range',
+ '192.168.1.100-192.168.1.199',
+ '--dst-range',
+ '10.0.0.50-10.0.0.100'
+ ])
+
+ set_module_args({
+ 'chain': 'INPUT',
+ 'dst_range': '10.0.0.50-10.0.0.100',
+ 'jump': 'ACCEPT'
+ })
+
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-C',
+ 'INPUT',
+ '-j',
+ 'ACCEPT',
+ '-m',
+ 'iprange',
+ '--dst-range',
+ '10.0.0.50-10.0.0.100'
+ ])
+
+ def test_insert_rule_with_wait(self):
+ """Test flush without parameters"""
+ set_module_args({
+ 'chain': 'OUTPUT',
+ 'source': '1.2.3.4/32',
+ 'destination': '7.8.9.10/42',
+ 'jump': 'ACCEPT',
+ 'action': 'insert',
+ 'wait': '10'
+ })
+
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-C',
+ 'OUTPUT',
+ '-w',
+ '10',
+ '-s',
+ '1.2.3.4/32',
+ '-d',
+ '7.8.9.10/42',
+ '-j',
+ 'ACCEPT'
+ ])
+
+ def test_comment_position_at_end(self):
+ """Test comment position to make sure it is at the end of command"""
+ set_module_args({
+ 'chain': 'INPUT',
+ 'jump': 'ACCEPT',
+ 'action': 'insert',
+ 'ctstate': ['NEW'],
+ 'comment': 'this is a comment',
+ '_ansible_check_mode': True,
+ })
+
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t',
+ 'filter',
+ '-C',
+ 'INPUT',
+ '-j',
+ 'ACCEPT',
+ '-m',
+ 'conntrack',
+ '--ctstate',
+ 'NEW',
+ '-m',
+ 'comment',
+ '--comment',
+ 'this is a comment'
+ ])
+ self.assertEqual(run_command.call_args[0][0][14], 'this is a comment')
+
+ def test_destination_ports(self):
+ """ Test multiport module usage with multiple ports """
+ set_module_args({
+ 'chain': 'INPUT',
+ 'protocol': 'tcp',
+ 'in_interface': 'eth0',
+ 'source': '192.168.0.1/32',
+ 'destination_ports': ['80', '443', '8081:8085'],
+ 'jump': 'ACCEPT',
+ 'comment': 'this is a comment',
+ })
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-C', 'INPUT',
+ '-p', 'tcp',
+ '-s', '192.168.0.1/32',
+ '-j', 'ACCEPT',
+ '-m', 'multiport',
+ '--dports', '80,443,8081:8085',
+ '-i', 'eth0',
+ '-m', 'comment',
+ '--comment', 'this is a comment'
+ ])
+
+ def test_match_set(self):
+ """ Test match_set together with match_set_flags """
+ set_module_args({
+ 'chain': 'INPUT',
+ 'protocol': 'tcp',
+ 'match_set': 'admin_hosts',
+ 'match_set_flags': 'src',
+ 'destination_port': '22',
+ 'jump': 'ACCEPT',
+ 'comment': 'this is a comment',
+ })
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-C', 'INPUT',
+ '-p', 'tcp',
+ '-j', 'ACCEPT',
+ '--destination-port', '22',
+ '-m', 'set',
+ '--match-set', 'admin_hosts', 'src',
+ '-m', 'comment',
+ '--comment', 'this is a comment'
+ ])
+
+ set_module_args({
+ 'chain': 'INPUT',
+ 'protocol': 'udp',
+ 'match_set': 'banned_hosts',
+ 'match_set_flags': 'src,dst',
+ 'jump': 'REJECT',
+ })
+ commands_results = [
+ (0, '', ''),
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-C', 'INPUT',
+ '-p', 'udp',
+ '-j', 'REJECT',
+ '-m', 'set',
+ '--match-set', 'banned_hosts', 'src,dst'
+ ])
+
+ def test_chain_creation(self):
+ """Test chain creation when absent"""
+ set_module_args({
+ 'chain': 'FOOBAR',
+ 'state': 'present',
+ 'chain_management': True,
+ })
+
+ commands_results = [
+ (1, '', ''), # check_rule_present
+ (1, '', ''), # check_chain_present
+ (0, '', ''), # create_chain
+ (0, '', ''), # append_rule
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 4)
+
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-C', 'FOOBAR',
+ ])
+
+ self.assertEqual(run_command.call_args_list[1][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-L', 'FOOBAR',
+ ])
+
+ self.assertEqual(run_command.call_args_list[2][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-N', 'FOOBAR',
+ ])
+
+ self.assertEqual(run_command.call_args_list[3][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-A', 'FOOBAR',
+ ])
+
+ commands_results = [
+ (0, '', ''), # check_rule_present
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertFalse(result.exception.args[0]['changed'])
+
+ def test_chain_creation_check_mode(self):
+ """Test chain creation when absent"""
+ set_module_args({
+ 'chain': 'FOOBAR',
+ 'state': 'present',
+ 'chain_management': True,
+ '_ansible_check_mode': True,
+ })
+
+ commands_results = [
+ (1, '', ''), # check_rule_present
+ (1, '', ''), # check_chain_present
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 2)
+
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-C', 'FOOBAR',
+ ])
+
+ self.assertEqual(run_command.call_args_list[1][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-L', 'FOOBAR',
+ ])
+
+ commands_results = [
+ (0, '', ''), # check_rule_present
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertFalse(result.exception.args[0]['changed'])
+
+ def test_chain_deletion(self):
+ """Test chain deletion when present"""
+ set_module_args({
+ 'chain': 'FOOBAR',
+ 'state': 'absent',
+ 'chain_management': True,
+ })
+
+ commands_results = [
+ (0, '', ''), # check_chain_present
+ (0, '', ''), # delete_chain
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 2)
+
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-L', 'FOOBAR',
+ ])
+
+ self.assertEqual(run_command.call_args_list[1][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-X', 'FOOBAR',
+ ])
+
+ commands_results = [
+ (1, '', ''), # check_rule_present
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertFalse(result.exception.args[0]['changed'])
+
+ def test_chain_deletion_check_mode(self):
+ """Test chain deletion when present"""
+ set_module_args({
+ 'chain': 'FOOBAR',
+ 'state': 'absent',
+ 'chain_management': True,
+ '_ansible_check_mode': True,
+ })
+
+ commands_results = [
+ (0, '', ''), # check_chain_present
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertTrue(result.exception.args[0]['changed'])
+
+ self.assertEqual(run_command.call_count, 1)
+
+ self.assertEqual(run_command.call_args_list[0][0][0], [
+ '/sbin/iptables',
+ '-t', 'filter',
+ '-L', 'FOOBAR',
+ ])
+
+ commands_results = [
+ (1, '', ''), # check_rule_present
+ ]
+
+ with patch.object(basic.AnsibleModule, 'run_command') as run_command:
+ run_command.side_effect = commands_results
+ with self.assertRaises(AnsibleExitJson) as result:
+ iptables.main()
+ self.assertFalse(result.exception.args[0]['changed'])
diff --git a/test/units/modules/test_known_hosts.py b/test/units/modules/test_known_hosts.py
new file mode 100644
index 0000000..123dd75
--- /dev/null
+++ b/test/units/modules/test_known_hosts.py
@@ -0,0 +1,110 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import tempfile
+from ansible.module_utils import basic
+
+from units.compat import unittest
+from ansible.module_utils._text import to_bytes
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible.modules.known_hosts import compute_diff, sanity_check
+
+
+class KnownHostsDiffTestCase(unittest.TestCase):
+
+ def _create_file(self, content):
+ tmp_file = tempfile.NamedTemporaryFile(prefix='ansible-test-', suffix='-known_hosts', delete=False)
+ tmp_file.write(to_bytes(content))
+ tmp_file.close()
+ self.addCleanup(os.unlink, tmp_file.name)
+ return tmp_file.name
+
+ def test_no_existing_file(self):
+ path = "/tmp/this_file_does_not_exists_known_hosts"
+ key = 'example.com ssh-rsa AAAAetc\n'
+ diff = compute_diff(path, found_line=None, replace_or_add=False, state='present', key=key)
+ self.assertEqual(diff, {
+ 'before_header': '/dev/null',
+ 'after_header': path,
+ 'before': '',
+ 'after': 'example.com ssh-rsa AAAAetc\n',
+ })
+
+ def test_key_addition(self):
+ path = self._create_file(
+ 'two.example.com ssh-rsa BBBBetc\n'
+ )
+ key = 'one.example.com ssh-rsa AAAAetc\n'
+ diff = compute_diff(path, found_line=None, replace_or_add=False, state='present', key=key)
+ self.assertEqual(diff, {
+ 'before_header': path,
+ 'after_header': path,
+ 'before': 'two.example.com ssh-rsa BBBBetc\n',
+ 'after': 'two.example.com ssh-rsa BBBBetc\none.example.com ssh-rsa AAAAetc\n',
+ })
+
+ def test_no_change(self):
+ path = self._create_file(
+ 'one.example.com ssh-rsa AAAAetc\n'
+ 'two.example.com ssh-rsa BBBBetc\n'
+ )
+ key = 'one.example.com ssh-rsa AAAAetc\n'
+ diff = compute_diff(path, found_line=1, replace_or_add=False, state='present', key=key)
+ self.assertEqual(diff, {
+ 'before_header': path,
+ 'after_header': path,
+ 'before': 'one.example.com ssh-rsa AAAAetc\ntwo.example.com ssh-rsa BBBBetc\n',
+ 'after': 'one.example.com ssh-rsa AAAAetc\ntwo.example.com ssh-rsa BBBBetc\n',
+ })
+
+ def test_key_change(self):
+ path = self._create_file(
+ 'one.example.com ssh-rsa AAAaetc\n'
+ 'two.example.com ssh-rsa BBBBetc\n'
+ )
+ key = 'one.example.com ssh-rsa AAAAetc\n'
+ diff = compute_diff(path, found_line=1, replace_or_add=True, state='present', key=key)
+ self.assertEqual(diff, {
+ 'before_header': path,
+ 'after_header': path,
+ 'before': 'one.example.com ssh-rsa AAAaetc\ntwo.example.com ssh-rsa BBBBetc\n',
+ 'after': 'two.example.com ssh-rsa BBBBetc\none.example.com ssh-rsa AAAAetc\n',
+ })
+
+ def test_key_removal(self):
+ path = self._create_file(
+ 'one.example.com ssh-rsa AAAAetc\n'
+ 'two.example.com ssh-rsa BBBBetc\n'
+ )
+ key = 'one.example.com ssh-rsa AAAAetc\n'
+ diff = compute_diff(path, found_line=1, replace_or_add=False, state='absent', key=key)
+ self.assertEqual(diff, {
+ 'before_header': path,
+ 'after_header': path,
+ 'before': 'one.example.com ssh-rsa AAAAetc\ntwo.example.com ssh-rsa BBBBetc\n',
+ 'after': 'two.example.com ssh-rsa BBBBetc\n',
+ })
+
+ def test_key_removal_no_change(self):
+ path = self._create_file(
+ 'two.example.com ssh-rsa BBBBetc\n'
+ )
+ key = 'one.example.com ssh-rsa AAAAetc\n'
+ diff = compute_diff(path, found_line=None, replace_or_add=False, state='absent', key=key)
+ self.assertEqual(diff, {
+ 'before_header': path,
+ 'after_header': path,
+ 'before': 'two.example.com ssh-rsa BBBBetc\n',
+ 'after': 'two.example.com ssh-rsa BBBBetc\n',
+ })
+
+ def test_sanity_check(self):
+ basic._load_params = lambda: {}
+ # Module used internally to execute ssh-keygen system executable
+ module = AnsibleModule(argument_spec={})
+ host = '10.0.0.1'
+ key = '%s ssh-rsa ASDF foo@bar' % (host,)
+ keygen = module.get_bin_path('ssh-keygen')
+ sanity_check(module, host, key, keygen)
diff --git a/test/units/modules/test_pip.py b/test/units/modules/test_pip.py
new file mode 100644
index 0000000..5640b80
--- /dev/null
+++ b/test/units/modules/test_pip.py
@@ -0,0 +1,40 @@
+# 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
+
+import json
+
+import pytest
+
+from ansible.modules import pip
+
+
+pytestmark = pytest.mark.usefixtures('patch_ansible_module')
+
+
+@pytest.mark.parametrize('patch_ansible_module', [{'name': 'six'}], indirect=['patch_ansible_module'])
+def test_failure_when_pip_absent(mocker, capfd):
+ mocker.patch('ansible.modules.pip._have_pip_module').return_value = False
+
+ get_bin_path = mocker.patch('ansible.module_utils.basic.AnsibleModule.get_bin_path')
+ get_bin_path.return_value = None
+
+ with pytest.raises(SystemExit):
+ pip.main()
+
+ out, err = capfd.readouterr()
+ results = json.loads(out)
+ assert results['failed']
+ assert 'pip needs to be installed' in results['msg']
+
+
+@pytest.mark.parametrize('patch_ansible_module, test_input, expected', [
+ [None, ['django>1.11.1', '<1.11.2', 'ipaddress', 'simpleproject<2.0.0', '>1.1.0'],
+ ['django>1.11.1,<1.11.2', 'ipaddress', 'simpleproject<2.0.0,>1.1.0']],
+ [None, ['django>1.11.1,<1.11.2,ipaddress', 'simpleproject<2.0.0,>1.1.0'],
+ ['django>1.11.1,<1.11.2', 'ipaddress', 'simpleproject<2.0.0,>1.1.0']],
+ [None, ['django>1.11.1', '<1.11.2', 'ipaddress,simpleproject<2.0.0,>1.1.0'],
+ ['django>1.11.1,<1.11.2', 'ipaddress', 'simpleproject<2.0.0,>1.1.0']]])
+def test_recover_package_name(test_input, expected):
+ assert pip._recover_package_name(test_input) == expected
diff --git a/test/units/modules/test_service.py b/test/units/modules/test_service.py
new file mode 100644
index 0000000..caabd74
--- /dev/null
+++ b/test/units/modules/test_service.py
@@ -0,0 +1,70 @@
+# Copyright: (c) 2021, Ansible Project
+# Copyright: (c) 2021, Abhijeet Kasurde <akasurde@redhat.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import json
+import platform
+
+import pytest
+from ansible.modules import service
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six import PY2
+from units.modules.utils import set_module_args
+
+
+def mocker_sunos_service(mocker):
+ """
+ Configure common mocker object for SunOSService
+ """
+ platform_system = mocker.patch.object(platform, "system")
+ platform_system.return_value = "SunOS"
+
+ get_bin_path = mocker.patch.object(AnsibleModule, "get_bin_path")
+ get_bin_path.return_value = "/usr/bin/svcs"
+
+ # Read a mocked /etc/release file
+ mocked_etc_release_data = mocker.mock_open(
+ read_data=" Oracle Solaris 12.0")
+ builtin_open = "__builtin__.open" if PY2 else "builtins.open"
+ mocker.patch(builtin_open, mocked_etc_release_data)
+
+ service_status = mocker.patch.object(
+ service.Service, "modify_service_state")
+ service_status.return_value = (0, "", "")
+
+ get_sunos_svcs_status = mocker.patch.object(
+ service.SunOSService, "get_sunos_svcs_status")
+ get_sunos_svcs_status.return_value = "offline"
+ get_service_status = mocker.patch.object(
+ service.Service, "get_service_status")
+ get_service_status.return_value = ""
+
+ mocker.patch('ansible.module_utils.common.sys_info.distro.id', return_value='')
+
+
+@pytest.fixture
+def mocked_sunos_service(mocker):
+ mocker_sunos_service(mocker)
+
+
+def test_sunos_service_start(mocked_sunos_service, capfd):
+ """
+ test SunOS Service Start
+ """
+ set_module_args(
+ {
+ "name": "environment",
+ "state": "started",
+ }
+ )
+ with pytest.raises(SystemExit):
+ service.main()
+
+ out, dummy = capfd.readouterr()
+ results = json.loads(out)
+ assert not results.get("failed")
+ assert results["changed"]
diff --git a/test/units/modules/test_service_facts.py b/test/units/modules/test_service_facts.py
new file mode 100644
index 0000000..07f6827
--- /dev/null
+++ b/test/units/modules/test_service_facts.py
@@ -0,0 +1,126 @@
+# -*- 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
+
+from units.compat import unittest
+from units.compat.mock import patch
+
+from ansible.module_utils import basic
+from ansible.modules.service_facts import AIXScanService
+
+
+# AIX # lssrc -a
+LSSRC_OUTPUT = """
+Subsystem Group PID Status
+ sendmail mail 5243302 active
+ syslogd ras 5636528 active
+ portmap portmap 5177768 active
+ snmpd tcpip 5308844 active
+ hostmibd tcpip 5374380 active
+ snmpmibd tcpip 5439918 active
+ aixmibd tcpip 5505456 active
+ nimsh nimclient 5571004 active
+ aso 6029758 active
+ biod nfs 6357464 active
+ nfsd nfs 5701906 active
+ rpc.mountd nfs 6488534 active
+ rpc.statd nfs 7209216 active
+ rpc.lockd nfs 7274988 active
+ qdaemon spooler 6816222 active
+ writesrv spooler 6685150 active
+ clcomd caa 7471600 active
+ sshd ssh 7602674 active
+ pfcdaemon 7012860 active
+ ctrmc rsct 6947312 active
+ IBM.HostRM rsct_rm 14418376 active
+ IBM.ConfigRM rsct_rm 6160674 active
+ IBM.DRM rsct_rm 14680550 active
+ IBM.MgmtDomainRM rsct_rm 14090676 active
+ IBM.ServiceRM rsct_rm 13828542 active
+ cthats cthats 13959668 active
+ cthags cthags 14025054 active
+ IBM.StorageRM rsct_rm 12255706 active
+ inetd tcpip 12517828 active
+ lpd spooler inoperative
+ keyserv keyserv inoperative
+ ypbind yp inoperative
+ gsclvmd inoperative
+ cdromd inoperative
+ ndpd-host tcpip inoperative
+ ndpd-router tcpip inoperative
+ netcd netcd inoperative
+ tftpd tcpip inoperative
+ routed tcpip inoperative
+ mrouted tcpip inoperative
+ rsvpd qos inoperative
+ policyd qos inoperative
+ timed tcpip inoperative
+ iptrace tcpip inoperative
+ dpid2 tcpip inoperative
+ rwhod tcpip inoperative
+ pxed tcpip inoperative
+ binld tcpip inoperative
+ xntpd tcpip inoperative
+ gated tcpip inoperative
+ dhcpcd tcpip inoperative
+ dhcpcd6 tcpip inoperative
+ dhcpsd tcpip inoperative
+ dhcpsdv6 tcpip inoperative
+ dhcprd tcpip inoperative
+ dfpd tcpip inoperative
+ named tcpip inoperative
+ automountd autofs inoperative
+ nfsrgyd nfs inoperative
+ gssd nfs inoperative
+ cpsd ike inoperative
+ tmd ike inoperative
+ isakmpd inoperative
+ ikev2d inoperative
+ iked ike inoperative
+ clconfd caa inoperative
+ ksys_vmmon inoperative
+ nimhttp inoperative
+ IBM.SRVPROXY ibmsrv inoperative
+ ctcas rsct inoperative
+ IBM.ERRM rsct_rm inoperative
+ IBM.AuditRM rsct_rm inoperative
+ isnstgtd isnstgtd inoperative
+ IBM.LPRM rsct_rm inoperative
+ cthagsglsm cthags inoperative
+"""
+
+
+class TestAIXScanService(unittest.TestCase):
+
+ def setUp(self):
+ self.mock1 = patch.object(basic.AnsibleModule, 'get_bin_path', return_value='/usr/sbin/lssrc')
+ self.mock1.start()
+ self.addCleanup(self.mock1.stop)
+ self.mock2 = patch.object(basic.AnsibleModule, 'run_command', return_value=(0, LSSRC_OUTPUT, ''))
+ self.mock2.start()
+ self.addCleanup(self.mock2.stop)
+ self.mock3 = patch('platform.system', return_value='AIX')
+ self.mock3.start()
+ self.addCleanup(self.mock3.stop)
+
+ def test_gather_services(self):
+ svcmod = AIXScanService(basic.AnsibleModule)
+ result = svcmod.gather_services()
+
+ self.assertIsInstance(result, dict)
+
+ self.assertIn('IBM.HostRM', result)
+ self.assertEqual(result['IBM.HostRM'], {
+ 'name': 'IBM.HostRM',
+ 'source': 'src',
+ 'state': 'running',
+ })
+ self.assertIn('IBM.AuditRM', result)
+ self.assertEqual(result['IBM.AuditRM'], {
+ 'name': 'IBM.AuditRM',
+ 'source': 'src',
+ 'state': 'stopped',
+ })
diff --git a/test/units/modules/test_systemd.py b/test/units/modules/test_systemd.py
new file mode 100644
index 0000000..52c212a
--- /dev/null
+++ b/test/units/modules/test_systemd.py
@@ -0,0 +1,52 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from ansible.modules.systemd import parse_systemctl_show
+
+
+class ParseSystemctlShowTestCase(unittest.TestCase):
+
+ def test_simple(self):
+ lines = [
+ 'Type=simple',
+ 'Restart=no',
+ 'Requires=system.slice sysinit.target',
+ 'Description=Blah blah blah',
+ ]
+ parsed = parse_systemctl_show(lines)
+ self.assertEqual(parsed, {
+ 'Type': 'simple',
+ 'Restart': 'no',
+ 'Requires': 'system.slice sysinit.target',
+ 'Description': 'Blah blah blah',
+ })
+
+ def test_multiline_exec(self):
+ # This was taken from a real service that specified "ExecStart=/bin/echo foo\nbar"
+ lines = [
+ 'Type=simple',
+ 'ExecStart={ path=/bin/echo ; argv[]=/bin/echo foo',
+ 'bar ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }',
+ 'Description=blah',
+ ]
+ parsed = parse_systemctl_show(lines)
+ self.assertEqual(parsed, {
+ 'Type': 'simple',
+ 'ExecStart': '{ path=/bin/echo ; argv[]=/bin/echo foo\n'
+ 'bar ; ignore_errors=no ; start_time=[n/a] ; stop_time=[n/a] ; pid=0 ; code=(null) ; status=0/0 }',
+ 'Description': 'blah',
+ })
+
+ def test_single_line_with_brace(self):
+ lines = [
+ 'Type=simple',
+ 'Description={ this is confusing',
+ 'Restart=no',
+ ]
+ parsed = parse_systemctl_show(lines)
+ self.assertEqual(parsed, {
+ 'Type': 'simple',
+ 'Description': '{ this is confusing',
+ 'Restart': 'no',
+ })
diff --git a/test/units/modules/test_unarchive.py b/test/units/modules/test_unarchive.py
new file mode 100644
index 0000000..3e7a58c
--- /dev/null
+++ b/test/units/modules/test_unarchive.py
@@ -0,0 +1,93 @@
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+
+import pytest
+
+from ansible.modules.unarchive import ZipArchive, TgzArchive
+
+
+class AnsibleModuleExit(Exception):
+ def __init__(self, *args, **kwargs):
+ self.args = args
+ self.kwargs = kwargs
+
+
+class ExitJson(AnsibleModuleExit):
+ pass
+
+
+class FailJson(AnsibleModuleExit):
+ pass
+
+
+@pytest.fixture
+def fake_ansible_module():
+ return FakeAnsibleModule()
+
+
+class FakeAnsibleModule:
+ def __init__(self):
+ self.params = {}
+ self.tmpdir = None
+
+ def exit_json(self, *args, **kwargs):
+ raise ExitJson(*args, **kwargs)
+
+ def fail_json(self, *args, **kwargs):
+ raise FailJson(*args, **kwargs)
+
+
+class TestCaseZipArchive:
+ @pytest.mark.parametrize(
+ 'side_effect, expected_reason', (
+ ([ValueError, '/bin/zipinfo'], "Unable to find required 'unzip'"),
+ (ValueError, "Unable to find required 'unzip' or 'zipinfo'"),
+ )
+ )
+ def test_no_zip_zipinfo_binary(self, mocker, fake_ansible_module, side_effect, expected_reason):
+ mocker.patch("ansible.modules.unarchive.get_bin_path", side_effect=side_effect)
+ fake_ansible_module.params = {
+ "extra_opts": "",
+ "exclude": "",
+ "include": "",
+ "io_buffer_size": 65536,
+ }
+
+ z = ZipArchive(
+ src="",
+ b_dest="",
+ file_args="",
+ module=fake_ansible_module,
+ )
+ can_handle, reason = z.can_handle_archive()
+
+ assert can_handle is False
+ assert expected_reason in reason
+ assert z.cmd_path is None
+
+
+class TestCaseTgzArchive:
+ def test_no_tar_binary(self, mocker, fake_ansible_module):
+ mocker.patch("ansible.modules.unarchive.get_bin_path", side_effect=ValueError)
+ fake_ansible_module.params = {
+ "extra_opts": "",
+ "exclude": "",
+ "include": "",
+ "io_buffer_size": 65536,
+ }
+ fake_ansible_module.check_mode = False
+
+ t = TgzArchive(
+ src="",
+ b_dest="",
+ file_args="",
+ module=fake_ansible_module,
+ )
+ can_handle, reason = t.can_handle_archive()
+
+ assert can_handle is False
+ assert 'Unable to find required' in reason
+ assert t.cmd_path is None
+ assert t.tar_type is None
diff --git a/test/units/modules/test_yum.py b/test/units/modules/test_yum.py
new file mode 100644
index 0000000..8052eff
--- /dev/null
+++ b/test/units/modules/test_yum.py
@@ -0,0 +1,222 @@
+# -*- coding: utf-8 -*-
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+
+from ansible.modules.yum import YumModule
+
+
+yum_plugin_load_error = """
+Plugin "product-id" can't be imported
+Plugin "search-disabled-repos" can't be imported
+Plugin "subscription-manager" can't be imported
+Plugin "product-id" can't be imported
+Plugin "search-disabled-repos" can't be imported
+Plugin "subscription-manager" can't be imported
+"""
+
+# from https://github.com/ansible/ansible/issues/20608#issuecomment-276106505
+wrapped_output_1 = """
+Загружены модули: fastestmirror
+Loading mirror speeds from cached hostfile
+ * base: mirror.h1host.ru
+ * extras: mirror.h1host.ru
+ * updates: mirror.h1host.ru
+
+vms-agent.x86_64 0.0-9 dev
+"""
+
+# from https://github.com/ansible/ansible/issues/20608#issuecomment-276971275
+wrapped_output_2 = """
+Загружены модули: fastestmirror
+Loading mirror speeds from cached hostfile
+ * base: mirror.corbina.net
+ * extras: mirror.corbina.net
+ * updates: mirror.corbina.net
+
+empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty.x86_64
+ 0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1-0
+ addons
+libtiff.x86_64 4.0.3-27.el7_3 updates
+"""
+
+# From https://github.com/ansible/ansible/issues/20608#issuecomment-276698431
+wrapped_output_3 = """
+Loaded plugins: fastestmirror, langpacks
+Loading mirror speeds from cached hostfile
+
+ceph.x86_64 1:11.2.0-0.el7 ceph
+ceph-base.x86_64 1:11.2.0-0.el7 ceph
+ceph-common.x86_64 1:11.2.0-0.el7 ceph
+ceph-mds.x86_64 1:11.2.0-0.el7 ceph
+ceph-mon.x86_64 1:11.2.0-0.el7 ceph
+ceph-osd.x86_64 1:11.2.0-0.el7 ceph
+ceph-selinux.x86_64 1:11.2.0-0.el7 ceph
+libcephfs1.x86_64 1:11.0.2-0.el7 ceph
+librados2.x86_64 1:11.2.0-0.el7 ceph
+libradosstriper1.x86_64 1:11.2.0-0.el7 ceph
+librbd1.x86_64 1:11.2.0-0.el7 ceph
+librgw2.x86_64 1:11.2.0-0.el7 ceph
+python-cephfs.x86_64 1:11.2.0-0.el7 ceph
+python-rados.x86_64 1:11.2.0-0.el7 ceph
+python-rbd.x86_64 1:11.2.0-0.el7 ceph
+"""
+
+# from https://github.com/ansible/ansible-modules-core/issues/4318#issuecomment-251416661
+wrapped_output_4 = """
+ipxe-roms-qemu.noarch 20160127-1.git6366fa7a.el7
+ rhelosp-9.0-director-puddle
+quota.x86_64 1:4.01-11.el7_2.1 rhelosp-rhel-7.2-z
+quota-nls.noarch 1:4.01-11.el7_2.1 rhelosp-rhel-7.2-z
+rdma.noarch 7.2_4.1_rc6-2.el7 rhelosp-rhel-7.2-z
+screen.x86_64 4.1.0-0.23.20120314git3c2946.el7_2
+ rhelosp-rhel-7.2-z
+sos.noarch 3.2-36.el7ost.2 rhelosp-9.0-puddle
+sssd-client.x86_64 1.13.0-40.el7_2.12 rhelosp-rhel-7.2-z
+"""
+
+
+# A 'normal-ish' yum check-update output, without any wrapped lines
+unwrapped_output_rhel7 = """
+
+Loaded plugins: etckeeper, product-id, search-disabled-repos, subscription-
+ : manager
+This system is not registered to Red Hat Subscription Management. You can use subscription-manager to register.
+
+NetworkManager-openvpn.x86_64 1:1.2.6-1.el7 epel
+NetworkManager-openvpn-gnome.x86_64 1:1.2.6-1.el7 epel
+cabal-install.x86_64 1.16.1.0-2.el7 epel
+cgit.x86_64 1.1-1.el7 epel
+python34-libs.x86_64 3.4.5-3.el7 epel
+python34-test.x86_64 3.4.5-3.el7 epel
+python34-tkinter.x86_64 3.4.5-3.el7 epel
+python34-tools.x86_64 3.4.5-3.el7 epel
+qgit.x86_64 2.6-4.el7 epel
+rdiff-backup.x86_64 1.2.8-12.el7 epel
+stoken-libs.x86_64 0.91-1.el7 epel
+xlockmore.x86_64 5.49-2.el7 epel
+"""
+
+# Some wrapped obsoletes for prepending to output for testing both
+wrapped_output_rhel7_obsoletes_postfix = """
+Obsoleting Packages
+ddashboard.x86_64 0.2.0.1-1.el7_3 mhlavink-developerdashboard
+ developerdashboard.x86_64 0.1.12.2-1.el7_2 @mhlavink-developerdashboard
+python-bugzilla.noarch 1.2.2-3.el7_2.1 mhlavink-developerdashboard
+ python-bugzilla-develdashboardfixes.noarch
+ 1.2.2-3.el7 @mhlavink-developerdashboard
+python2-futures.noarch 3.0.5-1.el7 epel
+ python-futures.noarch 3.0.3-1.el7 @epel
+python2-pip.noarch 8.1.2-5.el7 epel
+ python-pip.noarch 7.1.0-1.el7 @epel
+python2-pyxdg.noarch 0.25-6.el7 epel
+ pyxdg.noarch 0.25-5.el7 @epel
+python2-simplejson.x86_64 3.10.0-1.el7 epel
+ python-simplejson.x86_64 3.3.3-1.el7 @epel
+Security: kernel-3.10.0-327.28.2.el7.x86_64 is an installed security update
+Security: kernel-3.10.0-327.22.2.el7.x86_64 is the currently running version
+"""
+
+wrapped_output_multiple_empty_lines = """
+Loaded plugins: langpacks, product-id, search-disabled-repos, subscription-manager
+
+This system is not registered with an entitlement server. You can use subscription-manager to register.
+
+
+screen.x86_64 4.1.0-0.23.20120314git3c2946.el7_2
+ rhelosp-rhel-7.2-z
+sos.noarch 3.2-36.el7ost.2 rhelosp-9.0-puddle
+"""
+
+longname = """
+Loaded plugins: fastestmirror, priorities, rhnplugin
+This system is receiving updates from RHN Classic or Red Hat Satellite.
+Loading mirror speeds from cached hostfile
+
+xxxxxxxxxxxxxxxxxxxxxxxxxx.noarch
+ 1.16-1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+glibc.x86_64 2.17-157.el7_3.1 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"""
+
+
+unwrapped_output_rhel7_obsoletes = unwrapped_output_rhel7 + wrapped_output_rhel7_obsoletes_postfix
+unwrapped_output_rhel7_expected_new_obsoletes_pkgs = [
+ "ddashboard", "python-bugzilla", "python2-futures", "python2-pip",
+ "python2-pyxdg", "python2-simplejson"
+]
+unwrapped_output_rhel7_expected_old_obsoletes_pkgs = [
+ "developerdashboard", "python-bugzilla-develdashboardfixes",
+ "python-futures", "python-pip", "pyxdg", "python-simplejson"
+]
+unwrapped_output_rhel7_expected_updated_pkgs = [
+ "NetworkManager-openvpn", "NetworkManager-openvpn-gnome", "cabal-install",
+ "cgit", "python34-libs", "python34-test", "python34-tkinter",
+ "python34-tools", "qgit", "rdiff-backup", "stoken-libs", "xlockmore"
+]
+
+
+class TestYumUpdateCheckParse(unittest.TestCase):
+ def _assert_expected(self, expected_pkgs, result):
+
+ for expected_pkg in expected_pkgs:
+ self.assertIn(expected_pkg, result)
+ self.assertEqual(len(result), len(expected_pkgs))
+ self.assertIsInstance(result, dict)
+
+ def test_empty_output(self):
+ res, obs = YumModule.parse_check_update("")
+ expected_pkgs = []
+ self._assert_expected(expected_pkgs, res)
+
+ def test_longname(self):
+ res, obs = YumModule.parse_check_update(longname)
+ expected_pkgs = ['xxxxxxxxxxxxxxxxxxxxxxxxxx', 'glibc']
+ self._assert_expected(expected_pkgs, res)
+
+ def test_plugin_load_error(self):
+ res, obs = YumModule.parse_check_update(yum_plugin_load_error)
+ expected_pkgs = []
+ self._assert_expected(expected_pkgs, res)
+
+ def test_wrapped_output_1(self):
+ res, obs = YumModule.parse_check_update(wrapped_output_1)
+ expected_pkgs = ["vms-agent"]
+ self._assert_expected(expected_pkgs, res)
+
+ def test_wrapped_output_2(self):
+ res, obs = YumModule.parse_check_update(wrapped_output_2)
+ expected_pkgs = ["empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty-empty",
+ "libtiff"]
+
+ self._assert_expected(expected_pkgs, res)
+
+ def test_wrapped_output_3(self):
+ res, obs = YumModule.parse_check_update(wrapped_output_3)
+ expected_pkgs = ["ceph", "ceph-base", "ceph-common", "ceph-mds",
+ "ceph-mon", "ceph-osd", "ceph-selinux", "libcephfs1",
+ "librados2", "libradosstriper1", "librbd1", "librgw2",
+ "python-cephfs", "python-rados", "python-rbd"]
+ self._assert_expected(expected_pkgs, res)
+
+ def test_wrapped_output_4(self):
+ res, obs = YumModule.parse_check_update(wrapped_output_4)
+
+ expected_pkgs = ["ipxe-roms-qemu", "quota", "quota-nls", "rdma", "screen",
+ "sos", "sssd-client"]
+ self._assert_expected(expected_pkgs, res)
+
+ def test_wrapped_output_rhel7(self):
+ res, obs = YumModule.parse_check_update(unwrapped_output_rhel7)
+ self._assert_expected(unwrapped_output_rhel7_expected_updated_pkgs, res)
+
+ def test_wrapped_output_rhel7_obsoletes(self):
+ res, obs = YumModule.parse_check_update(unwrapped_output_rhel7_obsoletes)
+ self._assert_expected(
+ unwrapped_output_rhel7_expected_updated_pkgs + unwrapped_output_rhel7_expected_new_obsoletes_pkgs,
+ res
+ )
+ self._assert_expected(unwrapped_output_rhel7_expected_old_obsoletes_pkgs, obs)
+
+ def test_wrapped_output_multiple_empty_lines(self):
+ res, obs = YumModule.parse_check_update(wrapped_output_multiple_empty_lines)
+ self._assert_expected(['screen', 'sos'], res)
diff --git a/test/units/modules/utils.py b/test/units/modules/utils.py
new file mode 100644
index 0000000..6d169e3
--- /dev/null
+++ b/test/units/modules/utils.py
@@ -0,0 +1,50 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+
+from units.compat import unittest
+from units.compat.mock import patch
+from ansible.module_utils import basic
+from ansible.module_utils._text import to_bytes
+
+
+def set_module_args(args):
+ if '_ansible_remote_tmp' not in args:
+ args['_ansible_remote_tmp'] = '/tmp'
+ if '_ansible_keep_remote_files' not in args:
+ args['_ansible_keep_remote_files'] = False
+
+ args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
+ basic._ANSIBLE_ARGS = to_bytes(args)
+
+
+class AnsibleExitJson(Exception):
+ pass
+
+
+class AnsibleFailJson(Exception):
+ pass
+
+
+def exit_json(*args, **kwargs):
+ if 'changed' not in kwargs:
+ kwargs['changed'] = False
+ raise AnsibleExitJson(kwargs)
+
+
+def fail_json(*args, **kwargs):
+ kwargs['failed'] = True
+ raise AnsibleFailJson(kwargs)
+
+
+class ModuleTestCase(unittest.TestCase):
+
+ def setUp(self):
+ self.mock_module = patch.multiple(basic.AnsibleModule, exit_json=exit_json, fail_json=fail_json)
+ self.mock_module.start()
+ self.mock_sleep = patch('time.sleep')
+ self.mock_sleep.start()
+ set_module_args({})
+ self.addCleanup(self.mock_module.stop)
+ self.addCleanup(self.mock_sleep.stop)
diff --git a/test/units/parsing/__init__.py b/test/units/parsing/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/parsing/__init__.py
diff --git a/test/units/parsing/fixtures/ajson.json b/test/units/parsing/fixtures/ajson.json
new file mode 100644
index 0000000..dafec0b
--- /dev/null
+++ b/test/units/parsing/fixtures/ajson.json
@@ -0,0 +1,19 @@
+{
+ "password": {
+ "__ansible_vault": "$ANSIBLE_VAULT;1.1;AES256\n34646264306632313333393636316562356435376162633631326264383934326565333633366238\n3863373264326461623132613931346165636465346337310a326434313830316337393263616439\n64653937313463396366633861363266633465663730303633323534363331316164623237363831\n3536333561393238370a313330316263373938326162386433313336613532653538376662306435\n3339\n"
+ },
+ "bar": {
+ "baz": [
+ {
+ "password": {
+ "__ansible_vault": "$ANSIBLE_VAULT;1.1;AES256\n34646264306632313333393636316562356435376162633631326264383934326565333633366238\n3863373264326461623132613931346165636465346337310a326434313830316337393263616439\n64653937313463396366633861363266633465663730303633323534363331316164623237363831\n3536333561393238370a313330316263373938326162386433313336613532653538376662306435\n3338\n"
+ }
+ }
+ ]
+ },
+ "foo": {
+ "password": {
+ "__ansible_vault": "$ANSIBLE_VAULT;1.1;AES256\n34646264306632313333393636316562356435376162633631326264383934326565333633366238\n3863373264326461623132613931346165636465346337310a326434313830316337393263616439\n64653937313463396366633861363266633465663730303633323534363331316164623237363831\n3536333561393238370a313330316263373938326162386433313336613532653538376662306435\n3339\n"
+ }
+ }
+}
diff --git a/test/units/parsing/fixtures/vault.yml b/test/units/parsing/fixtures/vault.yml
new file mode 100644
index 0000000..ca33ab2
--- /dev/null
+++ b/test/units/parsing/fixtures/vault.yml
@@ -0,0 +1,6 @@
+$ANSIBLE_VAULT;1.1;AES256
+33343734386261666161626433386662623039356366656637303939306563376130623138626165
+6436333766346533353463636566313332623130383662340a393835656134633665333861393331
+37666233346464636263636530626332623035633135363732623332313534306438393366323966
+3135306561356164310a343937653834643433343734653137383339323330626437313562306630
+3035
diff --git a/test/units/parsing/test_ajson.py b/test/units/parsing/test_ajson.py
new file mode 100644
index 0000000..1b9a76b
--- /dev/null
+++ b/test/units/parsing/test_ajson.py
@@ -0,0 +1,186 @@
+# Copyright 2018, Matt Martz <matt@sivel.net>
+# Copyright 2019, Andrew Klychkov @Andersson007 <aaklychkov@mail.ru>
+# 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 json
+
+import pytest
+
+from collections.abc import Mapping
+from datetime import date, datetime, timezone, timedelta
+
+from ansible.parsing.ajson import AnsibleJSONEncoder, AnsibleJSONDecoder
+from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
+from ansible.utils.unsafe_proxy import AnsibleUnsafeText
+
+
+def test_AnsibleJSONDecoder_vault():
+ with open(os.path.join(os.path.dirname(__file__), 'fixtures/ajson.json')) as f:
+ data = json.load(f, cls=AnsibleJSONDecoder)
+
+ assert isinstance(data['password'], AnsibleVaultEncryptedUnicode)
+ assert isinstance(data['bar']['baz'][0]['password'], AnsibleVaultEncryptedUnicode)
+ assert isinstance(data['foo']['password'], AnsibleVaultEncryptedUnicode)
+
+
+def test_encode_decode_unsafe():
+ data = {
+ 'key_value': AnsibleUnsafeText(u'{#NOTACOMMENT#}'),
+ 'list': [AnsibleUnsafeText(u'{#NOTACOMMENT#}')],
+ 'list_dict': [{'key_value': AnsibleUnsafeText(u'{#NOTACOMMENT#}')}]}
+ json_expected = (
+ '{"key_value": {"__ansible_unsafe": "{#NOTACOMMENT#}"}, '
+ '"list": [{"__ansible_unsafe": "{#NOTACOMMENT#}"}], '
+ '"list_dict": [{"key_value": {"__ansible_unsafe": "{#NOTACOMMENT#}"}}]}'
+ )
+ assert json.dumps(data, cls=AnsibleJSONEncoder, preprocess_unsafe=True, sort_keys=True) == json_expected
+ assert json.loads(json_expected, cls=AnsibleJSONDecoder) == data
+
+
+def vault_data():
+ """
+ Prepare AnsibleVaultEncryptedUnicode test data for AnsibleJSONEncoder.default().
+
+ Return a list of tuples (input, expected).
+ """
+
+ with open(os.path.join(os.path.dirname(__file__), 'fixtures/ajson.json')) as f:
+ data = json.load(f, cls=AnsibleJSONDecoder)
+
+ data_0 = data['password']
+ data_1 = data['bar']['baz'][0]['password']
+
+ expected_0 = (u'$ANSIBLE_VAULT;1.1;AES256\n34646264306632313333393636316'
+ '562356435376162633631326264383934326565333633366238\n3863'
+ '373264326461623132613931346165636465346337310a32643431383'
+ '0316337393263616439\n646539373134633963666338613632666334'
+ '65663730303633323534363331316164623237363831\n35363335613'
+ '93238370a313330316263373938326162386433313336613532653538'
+ '376662306435\n3339\n')
+
+ expected_1 = (u'$ANSIBLE_VAULT;1.1;AES256\n34646264306632313333393636316'
+ '562356435376162633631326264383934326565333633366238\n3863'
+ '373264326461623132613931346165636465346337310a32643431383'
+ '0316337393263616439\n646539373134633963666338613632666334'
+ '65663730303633323534363331316164623237363831\n35363335613'
+ '93238370a313330316263373938326162386433313336613532653538'
+ '376662306435\n3338\n')
+
+ return [
+ (data_0, expected_0),
+ (data_1, expected_1),
+ ]
+
+
+class TestAnsibleJSONEncoder:
+
+ """
+ Namespace for testing AnsibleJSONEncoder.
+ """
+
+ @pytest.fixture(scope='class')
+ def mapping(self, request):
+ """
+ Returns object of Mapping mock class.
+
+ The object is used for testing handling of Mapping objects
+ in AnsibleJSONEncoder.default().
+ Using a plain dictionary instead is not suitable because
+ it is handled by default encoder of the superclass (json.JSONEncoder).
+ """
+
+ class M(Mapping):
+
+ """Mock mapping class."""
+
+ def __init__(self, *args, **kwargs):
+ self.__dict__.update(*args, **kwargs)
+
+ def __getitem__(self, key):
+ return self.__dict__[key]
+
+ def __iter__(self):
+ return iter(self.__dict__)
+
+ def __len__(self):
+ return len(self.__dict__)
+
+ return M(request.param)
+
+ @pytest.fixture
+ def ansible_json_encoder(self):
+ """Return AnsibleJSONEncoder object."""
+ return AnsibleJSONEncoder()
+
+ ###############
+ # Test methods:
+
+ @pytest.mark.parametrize(
+ 'test_input,expected',
+ [
+ (datetime(2019, 5, 14, 13, 39, 38, 569047), '2019-05-14T13:39:38.569047'),
+ (datetime(2019, 5, 14, 13, 47, 16, 923866), '2019-05-14T13:47:16.923866'),
+ (date(2019, 5, 14), '2019-05-14'),
+ (date(2020, 5, 14), '2020-05-14'),
+ (datetime(2019, 6, 15, 14, 45, tzinfo=timezone.utc), '2019-06-15T14:45:00+00:00'),
+ (datetime(2019, 6, 15, 14, 45, tzinfo=timezone(timedelta(hours=1, minutes=40))), '2019-06-15T14:45:00+01:40'),
+ ]
+ )
+ def test_date_datetime(self, ansible_json_encoder, test_input, expected):
+ """
+ Test for passing datetime.date or datetime.datetime objects to AnsibleJSONEncoder.default().
+ """
+ assert ansible_json_encoder.default(test_input) == expected
+
+ @pytest.mark.parametrize(
+ 'mapping,expected',
+ [
+ ({1: 1}, {1: 1}),
+ ({2: 2}, {2: 2}),
+ ({1: 2}, {1: 2}),
+ ({2: 1}, {2: 1}),
+ ], indirect=['mapping'],
+ )
+ def test_mapping(self, ansible_json_encoder, mapping, expected):
+ """
+ Test for passing Mapping object to AnsibleJSONEncoder.default().
+ """
+ assert ansible_json_encoder.default(mapping) == expected
+
+ @pytest.mark.parametrize('test_input,expected', vault_data())
+ def test_ansible_json_decoder_vault(self, ansible_json_encoder, test_input, expected):
+ """
+ Test for passing AnsibleVaultEncryptedUnicode to AnsibleJSONEncoder.default().
+ """
+ assert ansible_json_encoder.default(test_input) == {'__ansible_vault': expected}
+ assert json.dumps(test_input, cls=AnsibleJSONEncoder, preprocess_unsafe=True) == '{"__ansible_vault": "%s"}' % expected.replace('\n', '\\n')
+
+ @pytest.mark.parametrize(
+ 'test_input,expected',
+ [
+ ({1: 'first'}, {1: 'first'}),
+ ({2: 'second'}, {2: 'second'}),
+ ]
+ )
+ def test_default_encoder(self, ansible_json_encoder, test_input, expected):
+ """
+ Test for the default encoder of AnsibleJSONEncoder.default().
+
+ If objects of different classes that are not tested above were passed,
+ AnsibleJSONEncoder.default() invokes 'default()' method of json.JSONEncoder superclass.
+ """
+ assert ansible_json_encoder.default(test_input) == expected
+
+ @pytest.mark.parametrize('test_input', [1, 1.1, 'string', [1, 2], set('set'), True, None])
+ def test_default_encoder_unserializable(self, ansible_json_encoder, test_input):
+ """
+ Test for the default encoder of AnsibleJSONEncoder.default(), not serializable objects.
+
+ It must fail with TypeError 'object is not serializable'.
+ """
+ with pytest.raises(TypeError):
+ ansible_json_encoder.default(test_input)
diff --git a/test/units/parsing/test_dataloader.py b/test/units/parsing/test_dataloader.py
new file mode 100644
index 0000000..9ec49a8
--- /dev/null
+++ b/test/units/parsing/test_dataloader.py
@@ -0,0 +1,239 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+from units.compat import unittest
+from unittest.mock import patch, mock_open
+from ansible.errors import AnsibleParserError, yaml_strings, AnsibleFileNotFound
+from ansible.parsing.vault import AnsibleVaultError
+from ansible.module_utils._text import to_text
+from ansible.module_utils.six import PY3
+
+from units.mock.vault_helper import TextVaultSecret
+from ansible.parsing.dataloader import DataLoader
+
+from units.mock.path import mock_unfrackpath_noop
+
+
+class TestDataLoader(unittest.TestCase):
+
+ def setUp(self):
+ self._loader = DataLoader()
+
+ @patch('os.path.exists')
+ def test__is_role(self, p_exists):
+ p_exists.side_effect = lambda p: p == b'test_path/tasks/main.yml'
+ self.assertTrue(self._loader._is_role('test_path/tasks'))
+ self.assertTrue(self._loader._is_role('test_path/'))
+
+ @patch.object(DataLoader, '_get_file_contents')
+ def test_parse_json_from_file(self, mock_def):
+ mock_def.return_value = (b"""{"a": 1, "b": 2, "c": 3}""", True)
+ output = self._loader.load_from_file('dummy_json.txt')
+ self.assertEqual(output, dict(a=1, b=2, c=3))
+
+ @patch.object(DataLoader, '_get_file_contents')
+ def test_parse_yaml_from_file(self, mock_def):
+ mock_def.return_value = (b"""
+ a: 1
+ b: 2
+ c: 3
+ """, True)
+ output = self._loader.load_from_file('dummy_yaml.txt')
+ self.assertEqual(output, dict(a=1, b=2, c=3))
+
+ @patch.object(DataLoader, '_get_file_contents')
+ def test_parse_fail_from_file(self, mock_def):
+ mock_def.return_value = (b"""
+ TEXT:
+ ***
+ NOT VALID
+ """, True)
+ self.assertRaises(AnsibleParserError, self._loader.load_from_file, 'dummy_yaml_bad.txt')
+
+ @patch('ansible.errors.AnsibleError._get_error_lines_from_file')
+ @patch.object(DataLoader, '_get_file_contents')
+ def test_tab_error(self, mock_def, mock_get_error_lines):
+ mock_def.return_value = (u"""---\nhosts: localhost\nvars:\n foo: bar\n\tblip: baz""", True)
+ mock_get_error_lines.return_value = ('''\tblip: baz''', '''..foo: bar''')
+ with self.assertRaises(AnsibleParserError) as cm:
+ self._loader.load_from_file('dummy_yaml_text.txt')
+ self.assertIn(yaml_strings.YAML_COMMON_LEADING_TAB_ERROR, str(cm.exception))
+ self.assertIn('foo: bar', str(cm.exception))
+
+ @patch('ansible.parsing.dataloader.unfrackpath', mock_unfrackpath_noop)
+ @patch.object(DataLoader, '_is_role')
+ def test_path_dwim_relative(self, mock_is_role):
+ """
+ simulate a nested dynamic include:
+
+ playbook.yml:
+ - hosts: localhost
+ roles:
+ - { role: 'testrole' }
+
+ testrole/tasks/main.yml:
+ - include: "include1.yml"
+ static: no
+
+ testrole/tasks/include1.yml:
+ - include: include2.yml
+ static: no
+
+ testrole/tasks/include2.yml:
+ - debug: msg="blah"
+ """
+ mock_is_role.return_value = False
+ with patch('os.path.exists') as mock_os_path_exists:
+ mock_os_path_exists.return_value = False
+ self._loader.path_dwim_relative('/tmp/roles/testrole/tasks', 'tasks', 'included2.yml')
+
+ # Fetch first args for every call
+ # mock_os_path_exists.assert_any_call isn't used because os.path.normpath must be used in order to compare paths
+ called_args = [os.path.normpath(to_text(call[0][0])) for call in mock_os_path_exists.call_args_list]
+
+ # 'path_dwim_relative' docstrings say 'with or without explicitly named dirname subdirs':
+ self.assertIn('/tmp/roles/testrole/tasks/included2.yml', called_args)
+ self.assertIn('/tmp/roles/testrole/tasks/tasks/included2.yml', called_args)
+
+ # relative directories below are taken in account too:
+ self.assertIn('tasks/included2.yml', called_args)
+ self.assertIn('included2.yml', called_args)
+
+ def test_path_dwim_root(self):
+ self.assertEqual(self._loader.path_dwim('/'), '/')
+
+ def test_path_dwim_home(self):
+ self.assertEqual(self._loader.path_dwim('~'), os.path.expanduser('~'))
+
+ def test_path_dwim_tilde_slash(self):
+ self.assertEqual(self._loader.path_dwim('~/'), os.path.expanduser('~'))
+
+ def test_get_real_file(self):
+ self.assertEqual(self._loader.get_real_file(__file__), __file__)
+
+ def test_is_file(self):
+ self.assertTrue(self._loader.is_file(__file__))
+
+ def test_is_directory_positive(self):
+ self.assertTrue(self._loader.is_directory(os.path.dirname(__file__)))
+
+ def test_get_file_contents_none_path(self):
+ self.assertRaisesRegex(AnsibleParserError, 'Invalid filename',
+ self._loader._get_file_contents, None)
+
+ def test_get_file_contents_non_existent_path(self):
+ self.assertRaises(AnsibleFileNotFound, self._loader._get_file_contents, '/non_existent_file')
+
+
+class TestPathDwimRelativeDataLoader(unittest.TestCase):
+
+ def setUp(self):
+ self._loader = DataLoader()
+
+ def test_all_slash(self):
+ self.assertEqual(self._loader.path_dwim_relative('/', '/', '/'), '/')
+
+ def test_path_endswith_role(self):
+ self.assertEqual(self._loader.path_dwim_relative(path='foo/bar/tasks/', dirname='/', source='/'), '/')
+
+ def test_path_endswith_role_main_yml(self):
+ self.assertIn('main.yml', self._loader.path_dwim_relative(path='foo/bar/tasks/', dirname='/', source='main.yml'))
+
+ def test_path_endswith_role_source_tilde(self):
+ self.assertEqual(self._loader.path_dwim_relative(path='foo/bar/tasks/', dirname='/', source='~/'), os.path.expanduser('~'))
+
+
+class TestPathDwimRelativeStackDataLoader(unittest.TestCase):
+
+ def setUp(self):
+ self._loader = DataLoader()
+
+ def test_none(self):
+ self.assertRaisesRegex(AnsibleFileNotFound, 'on the Ansible Controller', self._loader.path_dwim_relative_stack, None, None, None)
+
+ def test_empty_strings(self):
+ self.assertEqual(self._loader.path_dwim_relative_stack('', '', ''), './')
+
+ def test_empty_lists(self):
+ self.assertEqual(self._loader.path_dwim_relative_stack([], '', '~/'), os.path.expanduser('~'))
+
+ def test_all_slash(self):
+ self.assertEqual(self._loader.path_dwim_relative_stack('/', '/', '/'), '/')
+
+ def test_path_endswith_role(self):
+ self.assertEqual(self._loader.path_dwim_relative_stack(paths=['foo/bar/tasks/'], dirname='/', source='/'), '/')
+
+ def test_path_endswith_role_source_tilde(self):
+ self.assertEqual(self._loader.path_dwim_relative_stack(paths=['foo/bar/tasks/'], dirname='/', source='~/'), os.path.expanduser('~'))
+
+ def test_path_endswith_role_source_main_yml(self):
+ self.assertRaises(AnsibleFileNotFound, self._loader.path_dwim_relative_stack, ['foo/bar/tasks/'], '/', 'main.yml')
+
+ def test_path_endswith_role_source_main_yml_source_in_dirname(self):
+ self.assertRaises(AnsibleFileNotFound, self._loader.path_dwim_relative_stack, 'foo/bar/tasks/', 'tasks', 'tasks/main.yml')
+
+
+class TestDataLoaderWithVault(unittest.TestCase):
+
+ def setUp(self):
+ self._loader = DataLoader()
+ vault_secrets = [('default', TextVaultSecret('ansible'))]
+ self._loader.set_vault_secrets(vault_secrets)
+ self.test_vault_data_path = os.path.join(os.path.dirname(__file__), 'fixtures', 'vault.yml')
+
+ def tearDown(self):
+ pass
+
+ def test_get_real_file_vault(self):
+ real_file_path = self._loader.get_real_file(self.test_vault_data_path)
+ self.assertTrue(os.path.exists(real_file_path))
+
+ def test_get_real_file_vault_no_vault(self):
+ self._loader.set_vault_secrets(None)
+ self.assertRaises(AnsibleParserError, self._loader.get_real_file, self.test_vault_data_path)
+
+ def test_get_real_file_vault_wrong_password(self):
+ wrong_vault = [('default', TextVaultSecret('wrong_password'))]
+ self._loader.set_vault_secrets(wrong_vault)
+ self.assertRaises(AnsibleVaultError, self._loader.get_real_file, self.test_vault_data_path)
+
+ def test_get_real_file_not_a_path(self):
+ self.assertRaisesRegex(AnsibleParserError, 'Invalid filename', self._loader.get_real_file, None)
+
+ @patch.multiple(DataLoader, path_exists=lambda s, x: True, is_file=lambda s, x: True)
+ def test_parse_from_vault_1_1_file(self):
+ vaulted_data = """$ANSIBLE_VAULT;1.1;AES256
+33343734386261666161626433386662623039356366656637303939306563376130623138626165
+6436333766346533353463636566313332623130383662340a393835656134633665333861393331
+37666233346464636263636530626332623035633135363732623332313534306438393366323966
+3135306561356164310a343937653834643433343734653137383339323330626437313562306630
+3035
+"""
+ if PY3:
+ builtins_name = 'builtins'
+ else:
+ builtins_name = '__builtin__'
+
+ with patch(builtins_name + '.open', mock_open(read_data=vaulted_data.encode('utf-8'))):
+ output = self._loader.load_from_file('dummy_vault.txt')
+ self.assertEqual(output, dict(foo='bar'))
diff --git a/test/units/parsing/test_mod_args.py b/test/units/parsing/test_mod_args.py
new file mode 100644
index 0000000..5d3f5d2
--- /dev/null
+++ b/test/units/parsing/test_mod_args.py
@@ -0,0 +1,137 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright 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
+
+import pytest
+import re
+
+from ansible.errors import AnsibleParserError
+from ansible.parsing.mod_args import ModuleArgsParser
+from ansible.utils.sentinel import Sentinel
+
+
+class TestModArgsDwim:
+
+ # TODO: add tests that construct ModuleArgsParser with a task reference
+ # TODO: verify the AnsibleError raised on failure knows the task
+ # and the task knows the line numbers
+
+ INVALID_MULTIPLE_ACTIONS = (
+ ({'action': 'shell echo hi', 'local_action': 'shell echo hi'}, "action and local_action are mutually exclusive"),
+ ({'action': 'shell echo hi', 'shell': 'echo hi'}, "conflicting action statements: shell, shell"),
+ ({'local_action': 'shell echo hi', 'shell': 'echo hi'}, "conflicting action statements: shell, shell"),
+ )
+
+ def _debug(self, mod, args, to):
+ print("RETURNED module = {0}".format(mod))
+ print(" args = {0}".format(args))
+ print(" to = {0}".format(to))
+
+ def test_basic_shell(self):
+ m = ModuleArgsParser(dict(shell='echo hi'))
+ mod, args, to = m.parse()
+ self._debug(mod, args, to)
+
+ assert mod == 'shell'
+ assert args == dict(
+ _raw_params='echo hi',
+ )
+ assert to is Sentinel
+
+ def test_basic_command(self):
+ m = ModuleArgsParser(dict(command='echo hi'))
+ mod, args, to = m.parse()
+ self._debug(mod, args, to)
+
+ assert mod == 'command'
+ assert args == dict(
+ _raw_params='echo hi',
+ )
+ assert to is Sentinel
+
+ def test_shell_with_modifiers(self):
+ m = ModuleArgsParser(dict(shell='/bin/foo creates=/tmp/baz removes=/tmp/bleep'))
+ mod, args, to = m.parse()
+ self._debug(mod, args, to)
+
+ assert mod == 'shell'
+ assert args == dict(
+ creates='/tmp/baz',
+ removes='/tmp/bleep',
+ _raw_params='/bin/foo',
+ )
+ assert to is Sentinel
+
+ def test_normal_usage(self):
+ m = ModuleArgsParser(dict(copy='src=a dest=b'))
+ mod, args, to = m.parse()
+ self._debug(mod, args, to)
+
+ assert mod, 'copy'
+ assert args, dict(src='a', dest='b')
+ assert to is Sentinel
+
+ def test_complex_args(self):
+ m = ModuleArgsParser(dict(copy=dict(src='a', dest='b')))
+ mod, args, to = m.parse()
+ self._debug(mod, args, to)
+
+ assert mod, 'copy'
+ assert args, dict(src='a', dest='b')
+ assert to is Sentinel
+
+ def test_action_with_complex(self):
+ m = ModuleArgsParser(dict(action=dict(module='copy', src='a', dest='b')))
+ mod, args, to = m.parse()
+ self._debug(mod, args, to)
+
+ assert mod == 'copy'
+ assert args == dict(src='a', dest='b')
+ assert to is Sentinel
+
+ def test_action_with_complex_and_complex_args(self):
+ m = ModuleArgsParser(dict(action=dict(module='copy', args=dict(src='a', dest='b'))))
+ mod, args, to = m.parse()
+ self._debug(mod, args, to)
+
+ assert mod == 'copy'
+ assert args == dict(src='a', dest='b')
+ assert to is Sentinel
+
+ def test_local_action_string(self):
+ m = ModuleArgsParser(dict(local_action='copy src=a dest=b'))
+ mod, args, delegate_to = m.parse()
+ self._debug(mod, args, delegate_to)
+
+ assert mod == 'copy'
+ assert args == dict(src='a', dest='b')
+ assert delegate_to == 'localhost'
+
+ @pytest.mark.parametrize("args_dict, msg", INVALID_MULTIPLE_ACTIONS)
+ def test_multiple_actions(self, args_dict, msg):
+ m = ModuleArgsParser(args_dict)
+ with pytest.raises(AnsibleParserError) as err:
+ m.parse()
+
+ assert err.value.args[0] == msg
+
+ def test_multiple_actions_ping_shell(self):
+ args_dict = {'ping': 'data=hi', 'shell': 'echo hi'}
+ m = ModuleArgsParser(args_dict)
+ with pytest.raises(AnsibleParserError) as err:
+ m.parse()
+
+ assert err.value.args[0].startswith("conflicting action statements: ")
+ actions = set(re.search(r'(\w+), (\w+)', err.value.args[0]).groups())
+ assert actions == set(['ping', 'shell'])
+
+ def test_bogus_action(self):
+ args_dict = {'bogusaction': {}}
+ m = ModuleArgsParser(args_dict)
+ with pytest.raises(AnsibleParserError) as err:
+ m.parse()
+
+ assert err.value.args[0].startswith("couldn't resolve module/action 'bogusaction'")
diff --git a/test/units/parsing/test_splitter.py b/test/units/parsing/test_splitter.py
new file mode 100644
index 0000000..a37de0f
--- /dev/null
+++ b/test/units/parsing/test_splitter.py
@@ -0,0 +1,110 @@
+# coding: utf-8
+# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.parsing.splitter import split_args, parse_kv
+
+import pytest
+
+SPLIT_DATA = (
+ (u'a',
+ [u'a'],
+ {u'_raw_params': u'a'}),
+ (u'a=b',
+ [u'a=b'],
+ {u'a': u'b'}),
+ (u'a="foo bar"',
+ [u'a="foo bar"'],
+ {u'a': u'foo bar'}),
+ (u'"foo bar baz"',
+ [u'"foo bar baz"'],
+ {u'_raw_params': '"foo bar baz"'}),
+ (u'foo bar baz',
+ [u'foo', u'bar', u'baz'],
+ {u'_raw_params': u'foo bar baz'}),
+ (u'a=b c="foo bar"',
+ [u'a=b', u'c="foo bar"'],
+ {u'a': u'b', u'c': u'foo bar'}),
+ (u'a="echo \\"hello world\\"" b=bar',
+ [u'a="echo \\"hello world\\""', u'b=bar'],
+ {u'a': u'echo "hello world"', u'b': u'bar'}),
+ (u'a="multi\nline"',
+ [u'a="multi\nline"'],
+ {u'a': u'multi\nline'}),
+ (u'a="blank\n\nline"',
+ [u'a="blank\n\nline"'],
+ {u'a': u'blank\n\nline'}),
+ (u'a="blank\n\n\nlines"',
+ [u'a="blank\n\n\nlines"'],
+ {u'a': u'blank\n\n\nlines'}),
+ (u'a="a long\nmessage\\\nabout a thing\n"',
+ [u'a="a long\nmessage\\\nabout a thing\n"'],
+ {u'a': u'a long\nmessage\\\nabout a thing\n'}),
+ (u'a="multiline\nmessage1\\\n" b="multiline\nmessage2\\\n"',
+ [u'a="multiline\nmessage1\\\n"', u'b="multiline\nmessage2\\\n"'],
+ {u'a': 'multiline\nmessage1\\\n', u'b': u'multiline\nmessage2\\\n'}),
+ (u'a={{jinja}}',
+ [u'a={{jinja}}'],
+ {u'a': u'{{jinja}}'}),
+ (u'a={{ jinja }}',
+ [u'a={{ jinja }}'],
+ {u'a': u'{{ jinja }}'}),
+ (u'a="{{jinja}}"',
+ [u'a="{{jinja}}"'],
+ {u'a': u'{{jinja}}'}),
+ (u'a={{ jinja }}{{jinja2}}',
+ [u'a={{ jinja }}{{jinja2}}'],
+ {u'a': u'{{ jinja }}{{jinja2}}'}),
+ (u'a="{{ jinja }}{{jinja2}}"',
+ [u'a="{{ jinja }}{{jinja2}}"'],
+ {u'a': u'{{ jinja }}{{jinja2}}'}),
+ (u'a={{jinja}} b={{jinja2}}',
+ [u'a={{jinja}}', u'b={{jinja2}}'],
+ {u'a': u'{{jinja}}', u'b': u'{{jinja2}}'}),
+ (u'a="{{jinja}}\n" b="{{jinja2}}\n"',
+ [u'a="{{jinja}}\n"', u'b="{{jinja2}}\n"'],
+ {u'a': u'{{jinja}}\n', u'b': u'{{jinja2}}\n'}),
+ (u'a="café eñyei"',
+ [u'a="café eñyei"'],
+ {u'a': u'café eñyei'}),
+ (u'a=café b=eñyei',
+ [u'a=café', u'b=eñyei'],
+ {u'a': u'café', u'b': u'eñyei'}),
+ (u'a={{ foo | some_filter(\' \', " ") }} b=bar',
+ [u'a={{ foo | some_filter(\' \', " ") }}', u'b=bar'],
+ {u'a': u'{{ foo | some_filter(\' \', " ") }}', u'b': u'bar'}),
+ (u'One\n Two\n Three\n',
+ [u'One\n ', u'Two\n ', u'Three\n'],
+ {u'_raw_params': u'One\n Two\n Three\n'}),
+)
+
+SPLIT_ARGS = ((test[0], test[1]) for test in SPLIT_DATA)
+PARSE_KV = ((test[0], test[2]) for test in SPLIT_DATA)
+
+
+@pytest.mark.parametrize("args, expected", SPLIT_ARGS)
+def test_split_args(args, expected):
+ assert split_args(args) == expected
+
+
+@pytest.mark.parametrize("args, expected", PARSE_KV)
+def test_parse_kv(args, expected):
+ assert parse_kv(args) == expected
diff --git a/test/units/parsing/test_unquote.py b/test/units/parsing/test_unquote.py
new file mode 100644
index 0000000..4b4260e
--- /dev/null
+++ b/test/units/parsing/test_unquote.py
@@ -0,0 +1,51 @@
+# coding: utf-8
+# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.parsing.quoting import unquote
+
+import pytest
+
+UNQUOTE_DATA = (
+ (u'1', u'1'),
+ (u'\'1\'', u'1'),
+ (u'"1"', u'1'),
+ (u'"1 \'2\'"', u'1 \'2\''),
+ (u'\'1 "2"\'', u'1 "2"'),
+ (u'\'1 \'2\'\'', u'1 \'2\''),
+ (u'"1\\"', u'"1\\"'),
+ (u'\'1\\\'', u'\'1\\\''),
+ (u'"1 \\"2\\" 3"', u'1 \\"2\\" 3'),
+ (u'\'1 \\\'2\\\' 3\'', u'1 \\\'2\\\' 3'),
+ (u'"', u'"'),
+ (u'\'', u'\''),
+ # Not entirely sure these are good but they match the current
+ # behaviour
+ (u'"1""2"', u'1""2'),
+ (u'\'1\'\'2\'', u'1\'\'2'),
+ (u'"1" 2 "3"', u'1" 2 "3'),
+ (u'"1"\'2\'"3"', u'1"\'2\'"3'),
+)
+
+
+@pytest.mark.parametrize("quoted, expected", UNQUOTE_DATA)
+def test_unquote(quoted, expected):
+ assert unquote(quoted) == expected
diff --git a/test/units/parsing/utils/__init__.py b/test/units/parsing/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/parsing/utils/__init__.py
diff --git a/test/units/parsing/utils/test_addresses.py b/test/units/parsing/utils/test_addresses.py
new file mode 100644
index 0000000..4f7304f
--- /dev/null
+++ b/test/units/parsing/utils/test_addresses.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import unittest
+
+from ansible.parsing.utils.addresses import parse_address
+
+
+class TestParseAddress(unittest.TestCase):
+
+ tests = {
+ # IPv4 addresses
+ '192.0.2.3': ['192.0.2.3', None],
+ '192.0.2.3:23': ['192.0.2.3', 23],
+
+ # IPv6 addresses
+ '::': ['::', None],
+ '::1': ['::1', None],
+ '[::1]:442': ['::1', 442],
+ 'abcd:ef98:7654:3210:abcd:ef98:7654:3210': ['abcd:ef98:7654:3210:abcd:ef98:7654:3210', None],
+ '[abcd:ef98:7654:3210:abcd:ef98:7654:3210]:42': ['abcd:ef98:7654:3210:abcd:ef98:7654:3210', 42],
+ '1234:5678:9abc:def0:1234:5678:9abc:def0': ['1234:5678:9abc:def0:1234:5678:9abc:def0', None],
+ '1234::9abc:def0:1234:5678:9abc:def0': ['1234::9abc:def0:1234:5678:9abc:def0', None],
+ '1234:5678::def0:1234:5678:9abc:def0': ['1234:5678::def0:1234:5678:9abc:def0', None],
+ '1234:5678:9abc::1234:5678:9abc:def0': ['1234:5678:9abc::1234:5678:9abc:def0', None],
+ '1234:5678:9abc:def0::5678:9abc:def0': ['1234:5678:9abc:def0::5678:9abc:def0', None],
+ '1234:5678:9abc:def0:1234::9abc:def0': ['1234:5678:9abc:def0:1234::9abc:def0', None],
+ '1234:5678:9abc:def0:1234:5678::def0': ['1234:5678:9abc:def0:1234:5678::def0', None],
+ '1234:5678:9abc:def0:1234:5678::': ['1234:5678:9abc:def0:1234:5678::', None],
+ '::9abc:def0:1234:5678:9abc:def0': ['::9abc:def0:1234:5678:9abc:def0', None],
+ '0:0:0:0:0:ffff:1.2.3.4': ['0:0:0:0:0:ffff:1.2.3.4', None],
+ '0:0:0:0:0:0:1.2.3.4': ['0:0:0:0:0:0:1.2.3.4', None],
+ '::ffff:1.2.3.4': ['::ffff:1.2.3.4', None],
+ '::1.2.3.4': ['::1.2.3.4', None],
+ '1234::': ['1234::', None],
+
+ # Hostnames
+ 'some-host': ['some-host', None],
+ 'some-host:80': ['some-host', 80],
+ 'some.host.com:492': ['some.host.com', 492],
+ '[some.host.com]:493': ['some.host.com', 493],
+ 'a-b.3foo_bar.com:23': ['a-b.3foo_bar.com', 23],
+ u'fóöbär': [u'fóöbär', None],
+ u'fóöbär:32': [u'fóöbär', 32],
+ u'fóöbär.éxàmplê.com:632': [u'fóöbär.éxàmplê.com', 632],
+
+ # Various errors
+ '': [None, None],
+ 'some..host': [None, None],
+ 'some.': [None, None],
+ '[example.com]': [None, None],
+ 'some-': [None, None],
+ 'some-.foo.com': [None, None],
+ 'some.-foo.com': [None, None],
+ }
+
+ range_tests = {
+ '192.0.2.[3:10]': ['192.0.2.[3:10]', None],
+ '192.0.2.[3:10]:23': ['192.0.2.[3:10]', 23],
+ 'abcd:ef98::7654:[1:9]': ['abcd:ef98::7654:[1:9]', None],
+ '[abcd:ef98::7654:[6:32]]:2222': ['abcd:ef98::7654:[6:32]', 2222],
+ '[abcd:ef98::7654:[9ab3:fcb7]]:2222': ['abcd:ef98::7654:[9ab3:fcb7]', 2222],
+ u'fóöb[a:c]r.éxàmplê.com:632': [u'fóöb[a:c]r.éxàmplê.com', 632],
+ '[a:b]foo.com': ['[a:b]foo.com', None],
+ 'foo[a:b].com': ['foo[a:b].com', None],
+ 'foo[a:b]:42': ['foo[a:b]', 42],
+ 'foo[a-b]-.com': [None, None],
+ 'foo[a-b]:32': [None, None],
+ 'foo[x-y]': [None, None],
+ }
+
+ def test_without_ranges(self):
+ for t in self.tests:
+ test = self.tests[t]
+
+ try:
+ (host, port) = parse_address(t)
+ except Exception:
+ host = None
+ port = None
+
+ assert host == test[0]
+ assert port == test[1]
+
+ def test_with_ranges(self):
+ for t in self.range_tests:
+ test = self.range_tests[t]
+
+ try:
+ (host, port) = parse_address(t, allow_ranges=True)
+ except Exception:
+ host = None
+ port = None
+
+ assert host == test[0]
+ assert port == test[1]
diff --git a/test/units/parsing/utils/test_jsonify.py b/test/units/parsing/utils/test_jsonify.py
new file mode 100644
index 0000000..37be782
--- /dev/null
+++ b/test/units/parsing/utils/test_jsonify.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# (c) 2016, James Cammarata <jimi@sngx.net>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from ansible.parsing.utils.jsonify import jsonify
+
+
+class TestJsonify(unittest.TestCase):
+ def test_jsonify_simple(self):
+ self.assertEqual(jsonify(dict(a=1, b=2, c=3)), '{"a": 1, "b": 2, "c": 3}')
+
+ def test_jsonify_simple_format(self):
+ res = jsonify(dict(a=1, b=2, c=3), format=True)
+ cleaned = "".join([x.strip() for x in res.splitlines()])
+ self.assertEqual(cleaned, '{"a": 1,"b": 2,"c": 3}')
+
+ def test_jsonify_unicode(self):
+ self.assertEqual(jsonify(dict(toshio=u'くらとみ')), u'{"toshio": "くらとみ"}')
+
+ def test_jsonify_empty(self):
+ self.assertEqual(jsonify(None), '{}')
diff --git a/test/units/parsing/utils/test_yaml.py b/test/units/parsing/utils/test_yaml.py
new file mode 100644
index 0000000..27b2905
--- /dev/null
+++ b/test/units/parsing/utils/test_yaml.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# (c) 2017, Ansible Project
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible.errors import AnsibleParserError
+from ansible.parsing.utils.yaml import from_yaml
+
+
+def test_from_yaml_simple():
+ assert from_yaml(u'---\n- test: 1\n test2: "2"\n- caf\xe9: "caf\xe9"') == [{u'test': 1, u'test2': u"2"}, {u"caf\xe9": u"caf\xe9"}]
+
+
+def test_bad_yaml():
+ with pytest.raises(AnsibleParserError):
+ from_yaml(u'foo: bar: baz')
diff --git a/test/units/parsing/vault/__init__.py b/test/units/parsing/vault/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/parsing/vault/__init__.py
diff --git a/test/units/parsing/vault/test_vault.py b/test/units/parsing/vault/test_vault.py
new file mode 100644
index 0000000..7afd356
--- /dev/null
+++ b/test/units/parsing/vault/test_vault.py
@@ -0,0 +1,870 @@
+# -*- coding: utf-8 -*-
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2016, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import binascii
+import io
+import os
+import tempfile
+
+from binascii import hexlify
+import pytest
+
+from units.compat import unittest
+from unittest.mock import patch, MagicMock
+
+from ansible import errors
+from ansible.module_utils import six
+from ansible.module_utils._text import to_bytes, to_text
+from ansible.parsing import vault
+
+from units.mock.loader import DictDataLoader
+from units.mock.vault_helper import TextVaultSecret
+
+
+class TestUnhexlify(unittest.TestCase):
+ def test(self):
+ b_plain_data = b'some text to hexlify'
+ b_data = hexlify(b_plain_data)
+ res = vault._unhexlify(b_data)
+ self.assertEqual(res, b_plain_data)
+
+ def test_odd_length(self):
+ b_data = b'123456789abcdefghijklmnopqrstuvwxyz'
+
+ self.assertRaisesRegex(vault.AnsibleVaultFormatError,
+ '.*Vault format unhexlify error.*',
+ vault._unhexlify,
+ b_data)
+
+ def test_nonhex(self):
+ b_data = b'6z36316566653264333665333637623064303639353237620a636366633565663263336335656532'
+
+ self.assertRaisesRegex(vault.AnsibleVaultFormatError,
+ '.*Vault format unhexlify error.*Non-hexadecimal digit found',
+ vault._unhexlify,
+ b_data)
+
+
+class TestParseVaulttext(unittest.TestCase):
+ def test(self):
+ vaulttext_envelope = u'''$ANSIBLE_VAULT;1.1;AES256
+33363965326261303234626463623963633531343539616138316433353830356566396130353436
+3562643163366231316662386565383735653432386435610a306664636137376132643732393835
+63383038383730306639353234326630666539346233376330303938323639306661313032396437
+6233623062366136310a633866373936313238333730653739323461656662303864663666653563
+3138'''
+
+ b_vaulttext_envelope = to_bytes(vaulttext_envelope, errors='strict', encoding='utf-8')
+ b_vaulttext, b_version, cipher_name, vault_id = vault.parse_vaulttext_envelope(b_vaulttext_envelope)
+ res = vault.parse_vaulttext(b_vaulttext)
+ self.assertIsInstance(res[0], bytes)
+ self.assertIsInstance(res[1], bytes)
+ self.assertIsInstance(res[2], bytes)
+
+ def test_non_hex(self):
+ vaulttext_envelope = u'''$ANSIBLE_VAULT;1.1;AES256
+3336396J326261303234626463623963633531343539616138316433353830356566396130353436
+3562643163366231316662386565383735653432386435610a306664636137376132643732393835
+63383038383730306639353234326630666539346233376330303938323639306661313032396437
+6233623062366136310a633866373936313238333730653739323461656662303864663666653563
+3138'''
+
+ b_vaulttext_envelope = to_bytes(vaulttext_envelope, errors='strict', encoding='utf-8')
+ b_vaulttext, b_version, cipher_name, vault_id = vault.parse_vaulttext_envelope(b_vaulttext_envelope)
+ self.assertRaisesRegex(vault.AnsibleVaultFormatError,
+ '.*Vault format unhexlify error.*Non-hexadecimal digit found',
+ vault.parse_vaulttext,
+ b_vaulttext_envelope)
+
+
+class TestVaultSecret(unittest.TestCase):
+ def test(self):
+ secret = vault.VaultSecret()
+ secret.load()
+ self.assertIsNone(secret._bytes)
+
+ def test_bytes(self):
+ some_text = u'私はガラスを食べられます。それは私を傷つけません。'
+ _bytes = to_bytes(some_text)
+ secret = vault.VaultSecret(_bytes)
+ secret.load()
+ self.assertEqual(secret.bytes, _bytes)
+
+
+class TestPromptVaultSecret(unittest.TestCase):
+ def test_empty_prompt_formats(self):
+ secret = vault.PromptVaultSecret(vault_id='test_id', prompt_formats=[])
+ secret.load()
+ self.assertIsNone(secret._bytes)
+
+ @patch('ansible.parsing.vault.display.prompt', return_value='the_password')
+ def test_prompt_formats_none(self, mock_display_prompt):
+ secret = vault.PromptVaultSecret(vault_id='test_id')
+ secret.load()
+ self.assertEqual(secret._bytes, b'the_password')
+
+ @patch('ansible.parsing.vault.display.prompt', return_value='the_password')
+ def test_custom_prompt(self, mock_display_prompt):
+ secret = vault.PromptVaultSecret(vault_id='test_id',
+ prompt_formats=['The cow flies at midnight: '])
+ secret.load()
+ self.assertEqual(secret._bytes, b'the_password')
+
+ @patch('ansible.parsing.vault.display.prompt', side_effect=EOFError)
+ def test_prompt_eoferror(self, mock_display_prompt):
+ secret = vault.PromptVaultSecret(vault_id='test_id')
+ self.assertRaisesRegex(vault.AnsibleVaultError,
+ 'EOFError.*test_id',
+ secret.load)
+
+ @patch('ansible.parsing.vault.display.prompt', side_effect=['first_password', 'second_password'])
+ def test_prompt_passwords_dont_match(self, mock_display_prompt):
+ secret = vault.PromptVaultSecret(vault_id='test_id',
+ prompt_formats=['Vault password: ',
+ 'Confirm Vault password: '])
+ self.assertRaisesRegex(errors.AnsibleError,
+ 'Passwords do not match',
+ secret.load)
+
+
+class TestFileVaultSecret(unittest.TestCase):
+ def setUp(self):
+ self.vault_password = "test-vault-password"
+ text_secret = TextVaultSecret(self.vault_password)
+ self.vault_secrets = [('foo', text_secret)]
+
+ def test(self):
+ secret = vault.FileVaultSecret()
+ self.assertIsNone(secret._bytes)
+ self.assertIsNone(secret._text)
+
+ def test_repr_empty(self):
+ secret = vault.FileVaultSecret()
+ self.assertEqual(repr(secret), "FileVaultSecret()")
+
+ def test_repr(self):
+ tmp_file = tempfile.NamedTemporaryFile(delete=False)
+ fake_loader = DictDataLoader({tmp_file.name: 'sdfadf'})
+
+ secret = vault.FileVaultSecret(loader=fake_loader, filename=tmp_file.name)
+ filename = tmp_file.name
+ tmp_file.close()
+ self.assertEqual(repr(secret), "FileVaultSecret(filename='%s')" % filename)
+
+ def test_empty_bytes(self):
+ secret = vault.FileVaultSecret()
+ self.assertIsNone(secret.bytes)
+
+ def test_file(self):
+ password = 'some password'
+
+ tmp_file = tempfile.NamedTemporaryFile(delete=False)
+ tmp_file.write(to_bytes(password))
+ tmp_file.close()
+
+ fake_loader = DictDataLoader({tmp_file.name: 'sdfadf'})
+
+ secret = vault.FileVaultSecret(loader=fake_loader, filename=tmp_file.name)
+ secret.load()
+
+ os.unlink(tmp_file.name)
+
+ self.assertEqual(secret.bytes, to_bytes(password))
+
+ def test_file_empty(self):
+
+ tmp_file = tempfile.NamedTemporaryFile(delete=False)
+ tmp_file.write(to_bytes(''))
+ tmp_file.close()
+
+ fake_loader = DictDataLoader({tmp_file.name: ''})
+
+ secret = vault.FileVaultSecret(loader=fake_loader, filename=tmp_file.name)
+ self.assertRaisesRegex(vault.AnsibleVaultPasswordError,
+ 'Invalid vault password was provided from file.*%s' % tmp_file.name,
+ secret.load)
+
+ os.unlink(tmp_file.name)
+
+ def test_file_encrypted(self):
+ vault_password = "test-vault-password"
+ text_secret = TextVaultSecret(vault_password)
+ vault_secrets = [('foo', text_secret)]
+
+ password = 'some password'
+ # 'some password' encrypted with 'test-ansible-password'
+
+ password_file_content = '''$ANSIBLE_VAULT;1.1;AES256
+61393863643638653437313566313632306462383837303132346434616433313438353634613762
+3334363431623364386164616163326537366333353663650a663634306232363432626162353665
+39623061353266373631636331643761306665343731376633623439313138396330346237653930
+6432643864346136640a653364386634666461306231353765636662316335613235383565306437
+3737
+'''
+
+ tmp_file = tempfile.NamedTemporaryFile(delete=False)
+ tmp_file.write(to_bytes(password_file_content))
+ tmp_file.close()
+
+ fake_loader = DictDataLoader({tmp_file.name: 'sdfadf'})
+ fake_loader._vault.secrets = vault_secrets
+
+ secret = vault.FileVaultSecret(loader=fake_loader, filename=tmp_file.name)
+ secret.load()
+
+ os.unlink(tmp_file.name)
+
+ self.assertEqual(secret.bytes, to_bytes(password))
+
+ def test_file_not_a_directory(self):
+ filename = '/dev/null/foobar'
+ fake_loader = DictDataLoader({filename: 'sdfadf'})
+
+ secret = vault.FileVaultSecret(loader=fake_loader, filename=filename)
+ self.assertRaisesRegex(errors.AnsibleError,
+ '.*Could not read vault password file.*/dev/null/foobar.*Not a directory',
+ secret.load)
+
+ def test_file_not_found(self):
+ tmp_file = tempfile.NamedTemporaryFile()
+ filename = os.path.realpath(tmp_file.name)
+ tmp_file.close()
+
+ fake_loader = DictDataLoader({filename: 'sdfadf'})
+
+ secret = vault.FileVaultSecret(loader=fake_loader, filename=filename)
+ self.assertRaisesRegex(errors.AnsibleError,
+ '.*Could not read vault password file.*%s.*' % filename,
+ secret.load)
+
+
+class TestScriptVaultSecret(unittest.TestCase):
+ def test(self):
+ secret = vault.ScriptVaultSecret()
+ self.assertIsNone(secret._bytes)
+ self.assertIsNone(secret._text)
+
+ def _mock_popen(self, mock_popen, return_code=0, stdout=b'', stderr=b''):
+ def communicate():
+ return stdout, stderr
+ mock_popen.return_value = MagicMock(returncode=return_code)
+ mock_popen_instance = mock_popen.return_value
+ mock_popen_instance.communicate = communicate
+
+ @patch('ansible.parsing.vault.subprocess.Popen')
+ def test_read_file(self, mock_popen):
+ self._mock_popen(mock_popen, stdout=b'some_password')
+ secret = vault.ScriptVaultSecret()
+ with patch.object(secret, 'loader') as mock_loader:
+ mock_loader.is_executable = MagicMock(return_value=True)
+ secret.load()
+
+ @patch('ansible.parsing.vault.subprocess.Popen')
+ def test_read_file_empty(self, mock_popen):
+ self._mock_popen(mock_popen, stdout=b'')
+ secret = vault.ScriptVaultSecret()
+ with patch.object(secret, 'loader') as mock_loader:
+ mock_loader.is_executable = MagicMock(return_value=True)
+ self.assertRaisesRegex(vault.AnsibleVaultPasswordError,
+ 'Invalid vault password was provided from script',
+ secret.load)
+
+ @patch('ansible.parsing.vault.subprocess.Popen')
+ def test_read_file_os_error(self, mock_popen):
+ self._mock_popen(mock_popen)
+ mock_popen.side_effect = OSError('That is not an executable')
+ secret = vault.ScriptVaultSecret()
+ with patch.object(secret, 'loader') as mock_loader:
+ mock_loader.is_executable = MagicMock(return_value=True)
+ self.assertRaisesRegex(errors.AnsibleError,
+ 'Problem running vault password script.*',
+ secret.load)
+
+ @patch('ansible.parsing.vault.subprocess.Popen')
+ def test_read_file_not_executable(self, mock_popen):
+ self._mock_popen(mock_popen)
+ secret = vault.ScriptVaultSecret()
+ with patch.object(secret, 'loader') as mock_loader:
+ mock_loader.is_executable = MagicMock(return_value=False)
+ self.assertRaisesRegex(vault.AnsibleVaultError,
+ 'The vault password script .* was not executable',
+ secret.load)
+
+ @patch('ansible.parsing.vault.subprocess.Popen')
+ def test_read_file_non_zero_return_code(self, mock_popen):
+ stderr = b'That did not work for a random reason'
+ rc = 37
+
+ self._mock_popen(mock_popen, return_code=rc, stderr=stderr)
+ secret = vault.ScriptVaultSecret(filename='/dev/null/some_vault_secret')
+ with patch.object(secret, 'loader') as mock_loader:
+ mock_loader.is_executable = MagicMock(return_value=True)
+ self.assertRaisesRegex(errors.AnsibleError,
+ r'Vault password script.*returned non-zero \(%s\): %s' % (rc, stderr),
+ secret.load)
+
+
+class TestScriptIsClient(unittest.TestCase):
+ def test_randomname(self):
+ filename = 'randomname'
+ res = vault.script_is_client(filename)
+ self.assertFalse(res)
+
+ def test_something_dash_client(self):
+ filename = 'something-client'
+ res = vault.script_is_client(filename)
+ self.assertTrue(res)
+
+ def test_something_dash_client_somethingelse(self):
+ filename = 'something-client-somethingelse'
+ res = vault.script_is_client(filename)
+ self.assertFalse(res)
+
+ def test_something_dash_client_py(self):
+ filename = 'something-client.py'
+ res = vault.script_is_client(filename)
+ self.assertTrue(res)
+
+ def test_full_path_something_dash_client_py(self):
+ filename = '/foo/bar/something-client.py'
+ res = vault.script_is_client(filename)
+ self.assertTrue(res)
+
+ def test_full_path_something_dash_client(self):
+ filename = '/foo/bar/something-client'
+ res = vault.script_is_client(filename)
+ self.assertTrue(res)
+
+ def test_full_path_something_dash_client_in_dir(self):
+ filename = '/foo/bar/something-client/but/not/filename'
+ res = vault.script_is_client(filename)
+ self.assertFalse(res)
+
+
+class TestGetFileVaultSecret(unittest.TestCase):
+ def test_file(self):
+ password = 'some password'
+
+ tmp_file = tempfile.NamedTemporaryFile(delete=False)
+ tmp_file.write(to_bytes(password))
+ tmp_file.close()
+
+ fake_loader = DictDataLoader({tmp_file.name: 'sdfadf'})
+
+ secret = vault.get_file_vault_secret(filename=tmp_file.name, loader=fake_loader)
+ secret.load()
+
+ os.unlink(tmp_file.name)
+
+ self.assertEqual(secret.bytes, to_bytes(password))
+
+ def test_file_not_a_directory(self):
+ filename = '/dev/null/foobar'
+ fake_loader = DictDataLoader({filename: 'sdfadf'})
+
+ self.assertRaisesRegex(errors.AnsibleError,
+ '.*The vault password file %s was not found.*' % filename,
+ vault.get_file_vault_secret,
+ filename=filename,
+ loader=fake_loader)
+
+ def test_file_not_found(self):
+ tmp_file = tempfile.NamedTemporaryFile()
+ filename = os.path.realpath(tmp_file.name)
+ tmp_file.close()
+
+ fake_loader = DictDataLoader({filename: 'sdfadf'})
+
+ self.assertRaisesRegex(errors.AnsibleError,
+ '.*The vault password file %s was not found.*' % filename,
+ vault.get_file_vault_secret,
+ filename=filename,
+ loader=fake_loader)
+
+
+class TestVaultIsEncrypted(unittest.TestCase):
+ def test_bytes_not_encrypted(self):
+ b_data = b"foobar"
+ self.assertFalse(vault.is_encrypted(b_data))
+
+ def test_bytes_encrypted(self):
+ b_data = b"$ANSIBLE_VAULT;9.9;TEST\n%s" % hexlify(b"ansible")
+ self.assertTrue(vault.is_encrypted(b_data))
+
+ def test_text_not_encrypted(self):
+ b_data = to_text(b"foobar")
+ self.assertFalse(vault.is_encrypted(b_data))
+
+ def test_text_encrypted(self):
+ b_data = to_text(b"$ANSIBLE_VAULT;9.9;TEST\n%s" % hexlify(b"ansible"))
+ self.assertTrue(vault.is_encrypted(b_data))
+
+ def test_invalid_text_not_ascii(self):
+ data = u"$ANSIBLE_VAULT;9.9;TEST\n%s" % u"ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ "
+ self.assertFalse(vault.is_encrypted(data))
+
+ def test_invalid_bytes_not_ascii(self):
+ data = u"$ANSIBLE_VAULT;9.9;TEST\n%s" % u"ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ "
+ b_data = to_bytes(data, encoding='utf-8')
+ self.assertFalse(vault.is_encrypted(b_data))
+
+
+class TestVaultIsEncryptedFile(unittest.TestCase):
+ def test_binary_file_handle_not_encrypted(self):
+ b_data = b"foobar"
+ b_data_fo = io.BytesIO(b_data)
+ self.assertFalse(vault.is_encrypted_file(b_data_fo))
+
+ def test_text_file_handle_not_encrypted(self):
+ data = u"foobar"
+ data_fo = io.StringIO(data)
+ self.assertFalse(vault.is_encrypted_file(data_fo))
+
+ def test_binary_file_handle_encrypted(self):
+ b_data = b"$ANSIBLE_VAULT;9.9;TEST\n%s" % hexlify(b"ansible")
+ b_data_fo = io.BytesIO(b_data)
+ self.assertTrue(vault.is_encrypted_file(b_data_fo))
+
+ def test_text_file_handle_encrypted(self):
+ data = u"$ANSIBLE_VAULT;9.9;TEST\n%s" % to_text(hexlify(b"ansible"))
+ data_fo = io.StringIO(data)
+ self.assertTrue(vault.is_encrypted_file(data_fo))
+
+ def test_binary_file_handle_invalid(self):
+ data = u"$ANSIBLE_VAULT;9.9;TEST\n%s" % u"ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ "
+ b_data = to_bytes(data)
+ b_data_fo = io.BytesIO(b_data)
+ self.assertFalse(vault.is_encrypted_file(b_data_fo))
+
+ def test_text_file_handle_invalid(self):
+ data = u"$ANSIBLE_VAULT;9.9;TEST\n%s" % u"ァ ア ィ イ ゥ ウ ェ エ ォ オ カ ガ キ ギ ク グ ケ "
+ data_fo = io.StringIO(data)
+ self.assertFalse(vault.is_encrypted_file(data_fo))
+
+ def test_file_already_read_from_finds_header(self):
+ b_data = b"$ANSIBLE_VAULT;9.9;TEST\n%s" % hexlify(b"ansible\ntesting\nfile pos")
+ b_data_fo = io.BytesIO(b_data)
+ b_data_fo.read(42) # Arbitrary number
+ self.assertTrue(vault.is_encrypted_file(b_data_fo))
+
+ def test_file_already_read_from_saves_file_pos(self):
+ b_data = b"$ANSIBLE_VAULT;9.9;TEST\n%s" % hexlify(b"ansible\ntesting\nfile pos")
+ b_data_fo = io.BytesIO(b_data)
+ b_data_fo.read(69) # Arbitrary number
+ vault.is_encrypted_file(b_data_fo)
+ self.assertEqual(b_data_fo.tell(), 69)
+
+ def test_file_with_offset(self):
+ b_data = b"JUNK$ANSIBLE_VAULT;9.9;TEST\n%s" % hexlify(b"ansible\ntesting\nfile pos")
+ b_data_fo = io.BytesIO(b_data)
+ self.assertTrue(vault.is_encrypted_file(b_data_fo, start_pos=4))
+
+ def test_file_with_count(self):
+ b_data = b"$ANSIBLE_VAULT;9.9;TEST\n%s" % hexlify(b"ansible\ntesting\nfile pos")
+ vault_length = len(b_data)
+ b_data = b_data + u'ァ ア'.encode('utf-8')
+ b_data_fo = io.BytesIO(b_data)
+ self.assertTrue(vault.is_encrypted_file(b_data_fo, count=vault_length))
+
+ def test_file_with_offset_and_count(self):
+ b_data = b"$ANSIBLE_VAULT;9.9;TEST\n%s" % hexlify(b"ansible\ntesting\nfile pos")
+ vault_length = len(b_data)
+ b_data = b'JUNK' + b_data + u'ァ ア'.encode('utf-8')
+ b_data_fo = io.BytesIO(b_data)
+ self.assertTrue(vault.is_encrypted_file(b_data_fo, start_pos=4, count=vault_length))
+
+
+@pytest.mark.skipif(not vault.HAS_CRYPTOGRAPHY,
+ reason="Skipping cryptography tests because cryptography is not installed")
+class TestVaultCipherAes256(unittest.TestCase):
+ def setUp(self):
+ self.vault_cipher = vault.VaultAES256()
+
+ def test(self):
+ self.assertIsInstance(self.vault_cipher, vault.VaultAES256)
+
+ # TODO: tag these as slow tests
+ def test_create_key_cryptography(self):
+ b_password = b'hunter42'
+ b_salt = os.urandom(32)
+ b_key_cryptography = self.vault_cipher._create_key_cryptography(b_password, b_salt, key_length=32, iv_length=16)
+ self.assertIsInstance(b_key_cryptography, six.binary_type)
+
+ def test_create_key_known_cryptography(self):
+ b_password = b'hunter42'
+
+ # A fixed salt
+ b_salt = b'q' * 32 # q is the most random letter.
+ b_key_1 = self.vault_cipher._create_key_cryptography(b_password, b_salt, key_length=32, iv_length=16)
+ self.assertIsInstance(b_key_1, six.binary_type)
+
+ # verify we get the same answer
+ # we could potentially run a few iterations of this and time it to see if it's roughly constant time
+ # and or that it exceeds some minimal time, but that would likely cause unreliable fails, esp in CI
+ b_key_2 = self.vault_cipher._create_key_cryptography(b_password, b_salt, key_length=32, iv_length=16)
+ self.assertIsInstance(b_key_2, six.binary_type)
+ self.assertEqual(b_key_1, b_key_2)
+
+ def test_is_equal_is_equal(self):
+ self.assertTrue(self.vault_cipher._is_equal(b'abcdefghijklmnopqrstuvwxyz', b'abcdefghijklmnopqrstuvwxyz'))
+
+ def test_is_equal_unequal_length(self):
+ self.assertFalse(self.vault_cipher._is_equal(b'abcdefghijklmnopqrstuvwxyz', b'abcdefghijklmnopqrstuvwx and sometimes y'))
+
+ def test_is_equal_not_equal(self):
+ self.assertFalse(self.vault_cipher._is_equal(b'abcdefghijklmnopqrstuvwxyz', b'AbcdefghijKlmnopQrstuvwxZ'))
+
+ def test_is_equal_empty(self):
+ self.assertTrue(self.vault_cipher._is_equal(b'', b''))
+
+ def test_is_equal_non_ascii_equal(self):
+ utf8_data = to_bytes(u'私はガラスを食べられます。それは私を傷つけません。')
+ self.assertTrue(self.vault_cipher._is_equal(utf8_data, utf8_data))
+
+ def test_is_equal_non_ascii_unequal(self):
+ utf8_data = to_bytes(u'私はガラスを食べられます。それは私を傷つけません。')
+ utf8_data2 = to_bytes(u'Pot să mănânc sticlă și ea nu mă rănește.')
+
+ # Test for the len optimization path
+ self.assertFalse(self.vault_cipher._is_equal(utf8_data, utf8_data2))
+ # Test for the slower, char by char comparison path
+ self.assertFalse(self.vault_cipher._is_equal(utf8_data, utf8_data[:-1] + b'P'))
+
+ def test_is_equal_non_bytes(self):
+ """ Anything not a byte string should raise a TypeError """
+ self.assertRaises(TypeError, self.vault_cipher._is_equal, u"One fish", b"two fish")
+ self.assertRaises(TypeError, self.vault_cipher._is_equal, b"One fish", u"two fish")
+ self.assertRaises(TypeError, self.vault_cipher._is_equal, 1, b"red fish")
+ self.assertRaises(TypeError, self.vault_cipher._is_equal, b"blue fish", 2)
+
+
+class TestMatchSecrets(unittest.TestCase):
+ def test_empty_tuple(self):
+ secrets = [tuple()]
+ vault_ids = ['vault_id_1']
+ self.assertRaises(ValueError,
+ vault.match_secrets,
+ secrets, vault_ids)
+
+ def test_empty_secrets(self):
+ matches = vault.match_secrets([], ['vault_id_1'])
+ self.assertEqual(matches, [])
+
+ def test_single_match(self):
+ secret = TextVaultSecret('password')
+ matches = vault.match_secrets([('default', secret)], ['default'])
+ self.assertEqual(matches, [('default', secret)])
+
+ def test_no_matches(self):
+ secret = TextVaultSecret('password')
+ matches = vault.match_secrets([('default', secret)], ['not_default'])
+ self.assertEqual(matches, [])
+
+ def test_multiple_matches(self):
+ secrets = [('vault_id1', TextVaultSecret('password1')),
+ ('vault_id2', TextVaultSecret('password2')),
+ ('vault_id1', TextVaultSecret('password3')),
+ ('vault_id4', TextVaultSecret('password4'))]
+ vault_ids = ['vault_id1', 'vault_id4']
+ matches = vault.match_secrets(secrets, vault_ids)
+
+ self.assertEqual(len(matches), 3)
+ expected = [('vault_id1', TextVaultSecret('password1')),
+ ('vault_id1', TextVaultSecret('password3')),
+ ('vault_id4', TextVaultSecret('password4'))]
+ self.assertEqual([x for x, y in matches],
+ [a for a, b in expected])
+
+
+@pytest.mark.skipif(not vault.HAS_CRYPTOGRAPHY,
+ reason="Skipping cryptography tests because cryptography is not installed")
+class TestVaultLib(unittest.TestCase):
+ def setUp(self):
+ self.vault_password = "test-vault-password"
+ text_secret = TextVaultSecret(self.vault_password)
+ self.vault_secrets = [('default', text_secret),
+ ('test_id', text_secret)]
+ self.v = vault.VaultLib(self.vault_secrets)
+
+ def _vault_secrets(self, vault_id, secret):
+ return [(vault_id, secret)]
+
+ def _vault_secrets_from_password(self, vault_id, password):
+ return [(vault_id, TextVaultSecret(password))]
+
+ def test_encrypt(self):
+ plaintext = u'Some text to encrypt in a café'
+ b_vaulttext = self.v.encrypt(plaintext)
+
+ self.assertIsInstance(b_vaulttext, six.binary_type)
+
+ b_header = b'$ANSIBLE_VAULT;1.1;AES256\n'
+ self.assertEqual(b_vaulttext[:len(b_header)], b_header)
+
+ def test_encrypt_vault_id(self):
+ plaintext = u'Some text to encrypt in a café'
+ b_vaulttext = self.v.encrypt(plaintext, vault_id='test_id')
+
+ self.assertIsInstance(b_vaulttext, six.binary_type)
+
+ b_header = b'$ANSIBLE_VAULT;1.2;AES256;test_id\n'
+ self.assertEqual(b_vaulttext[:len(b_header)], b_header)
+
+ def test_encrypt_bytes(self):
+
+ plaintext = to_bytes(u'Some text to encrypt in a café')
+ b_vaulttext = self.v.encrypt(plaintext)
+
+ self.assertIsInstance(b_vaulttext, six.binary_type)
+
+ b_header = b'$ANSIBLE_VAULT;1.1;AES256\n'
+ self.assertEqual(b_vaulttext[:len(b_header)], b_header)
+
+ def test_encrypt_no_secret_empty_secrets(self):
+ vault_secrets = []
+ v = vault.VaultLib(vault_secrets)
+
+ plaintext = u'Some text to encrypt in a café'
+ self.assertRaisesRegex(vault.AnsibleVaultError,
+ '.*A vault password must be specified to encrypt data.*',
+ v.encrypt,
+ plaintext)
+
+ def test_format_vaulttext_envelope(self):
+ cipher_name = "TEST"
+ b_ciphertext = b"ansible"
+ b_vaulttext = vault.format_vaulttext_envelope(b_ciphertext,
+ cipher_name,
+ version=self.v.b_version,
+ vault_id='default')
+ b_lines = b_vaulttext.split(b'\n')
+ self.assertGreater(len(b_lines), 1, msg="failed to properly add header")
+
+ b_header = b_lines[0]
+ # self.assertTrue(b_header.endswith(b';TEST'), msg="header does not end with cipher name")
+
+ b_header_parts = b_header.split(b';')
+ self.assertEqual(len(b_header_parts), 4, msg="header has the wrong number of parts")
+ self.assertEqual(b_header_parts[0], b'$ANSIBLE_VAULT', msg="header does not start with $ANSIBLE_VAULT")
+ self.assertEqual(b_header_parts[1], self.v.b_version, msg="header version is incorrect")
+ self.assertEqual(b_header_parts[2], b'TEST', msg="header does not end with cipher name")
+
+ # And just to verify, lets parse the results and compare
+ b_ciphertext2, b_version2, cipher_name2, vault_id2 = \
+ vault.parse_vaulttext_envelope(b_vaulttext)
+ self.assertEqual(b_ciphertext, b_ciphertext2)
+ self.assertEqual(self.v.b_version, b_version2)
+ self.assertEqual(cipher_name, cipher_name2)
+ self.assertEqual('default', vault_id2)
+
+ def test_parse_vaulttext_envelope(self):
+ b_vaulttext = b"$ANSIBLE_VAULT;9.9;TEST\nansible"
+ b_ciphertext, b_version, cipher_name, vault_id = vault.parse_vaulttext_envelope(b_vaulttext)
+ b_lines = b_ciphertext.split(b'\n')
+ self.assertEqual(b_lines[0], b"ansible", msg="Payload was not properly split from the header")
+ self.assertEqual(cipher_name, u'TEST', msg="cipher name was not properly set")
+ self.assertEqual(b_version, b"9.9", msg="version was not properly set")
+
+ def test_parse_vaulttext_envelope_crlf(self):
+ b_vaulttext = b"$ANSIBLE_VAULT;9.9;TEST\r\nansible"
+ b_ciphertext, b_version, cipher_name, vault_id = vault.parse_vaulttext_envelope(b_vaulttext)
+ b_lines = b_ciphertext.split(b'\n')
+ self.assertEqual(b_lines[0], b"ansible", msg="Payload was not properly split from the header")
+ self.assertEqual(cipher_name, u'TEST', msg="cipher name was not properly set")
+ self.assertEqual(b_version, b"9.9", msg="version was not properly set")
+
+ def test_encrypt_decrypt_aes256(self):
+ self.v.cipher_name = u'AES256'
+ plaintext = u"foobar"
+ b_vaulttext = self.v.encrypt(plaintext)
+ b_plaintext = self.v.decrypt(b_vaulttext)
+ self.assertNotEqual(b_vaulttext, b"foobar", msg="encryption failed")
+ self.assertEqual(b_plaintext, b"foobar", msg="decryption failed")
+
+ def test_encrypt_decrypt_aes256_none_secrets(self):
+ vault_secrets = self._vault_secrets_from_password('default', 'ansible')
+ v = vault.VaultLib(vault_secrets)
+
+ plaintext = u"foobar"
+ b_vaulttext = v.encrypt(plaintext)
+
+ # VaultLib will default to empty {} if secrets is None
+ v_none = vault.VaultLib(None)
+ # so set secrets None explicitly
+ v_none.secrets = None
+ self.assertRaisesRegex(vault.AnsibleVaultError,
+ '.*A vault password must be specified to decrypt data.*',
+ v_none.decrypt,
+ b_vaulttext)
+
+ def test_encrypt_decrypt_aes256_empty_secrets(self):
+ vault_secrets = self._vault_secrets_from_password('default', 'ansible')
+ v = vault.VaultLib(vault_secrets)
+
+ plaintext = u"foobar"
+ b_vaulttext = v.encrypt(plaintext)
+
+ vault_secrets_empty = []
+ v_none = vault.VaultLib(vault_secrets_empty)
+
+ self.assertRaisesRegex(vault.AnsibleVaultError,
+ '.*Attempting to decrypt but no vault secrets found.*',
+ v_none.decrypt,
+ b_vaulttext)
+
+ def test_encrypt_decrypt_aes256_multiple_secrets_all_wrong(self):
+ plaintext = u'Some text to encrypt in a café'
+ b_vaulttext = self.v.encrypt(plaintext)
+
+ vault_secrets = [('default', TextVaultSecret('another-wrong-password')),
+ ('wrong-password', TextVaultSecret('wrong-password'))]
+
+ v_multi = vault.VaultLib(vault_secrets)
+ self.assertRaisesRegex(errors.AnsibleError,
+ '.*Decryption failed.*',
+ v_multi.decrypt,
+ b_vaulttext,
+ filename='/dev/null/fake/filename')
+
+ def test_encrypt_decrypt_aes256_multiple_secrets_one_valid(self):
+ plaintext = u'Some text to encrypt in a café'
+ b_vaulttext = self.v.encrypt(plaintext)
+
+ correct_secret = TextVaultSecret(self.vault_password)
+ wrong_secret = TextVaultSecret('wrong-password')
+
+ vault_secrets = [('default', wrong_secret),
+ ('corect_secret', correct_secret),
+ ('wrong_secret', wrong_secret)]
+
+ v_multi = vault.VaultLib(vault_secrets)
+ b_plaintext = v_multi.decrypt(b_vaulttext)
+ self.assertNotEqual(b_vaulttext, to_bytes(plaintext), msg="encryption failed")
+ self.assertEqual(b_plaintext, to_bytes(plaintext), msg="decryption failed")
+
+ def test_encrypt_decrypt_aes256_existing_vault(self):
+ self.v.cipher_name = u'AES256'
+ b_orig_plaintext = b"Setec Astronomy"
+ vaulttext = u'''$ANSIBLE_VAULT;1.1;AES256
+33363965326261303234626463623963633531343539616138316433353830356566396130353436
+3562643163366231316662386565383735653432386435610a306664636137376132643732393835
+63383038383730306639353234326630666539346233376330303938323639306661313032396437
+6233623062366136310a633866373936313238333730653739323461656662303864663666653563
+3138'''
+
+ b_plaintext = self.v.decrypt(vaulttext)
+ self.assertEqual(b_plaintext, b_plaintext, msg="decryption failed")
+
+ b_vaulttext = to_bytes(vaulttext, encoding='ascii', errors='strict')
+ b_plaintext = self.v.decrypt(b_vaulttext)
+ self.assertEqual(b_plaintext, b_orig_plaintext, msg="decryption failed")
+
+ # FIXME This test isn't working quite yet.
+ @pytest.mark.skip(reason='This test is not ready yet')
+ def test_encrypt_decrypt_aes256_bad_hmac(self):
+
+ self.v.cipher_name = 'AES256'
+ # plaintext = "Setec Astronomy"
+ enc_data = '''$ANSIBLE_VAULT;1.1;AES256
+33363965326261303234626463623963633531343539616138316433353830356566396130353436
+3562643163366231316662386565383735653432386435610a306664636137376132643732393835
+63383038383730306639353234326630666539346233376330303938323639306661313032396437
+6233623062366136310a633866373936313238333730653739323461656662303864663666653563
+3138'''
+ b_data = to_bytes(enc_data, errors='strict', encoding='utf-8')
+ b_data = self.v._split_header(b_data)
+ foo = binascii.unhexlify(b_data)
+ lines = foo.splitlines()
+ # line 0 is salt, line 1 is hmac, line 2+ is ciphertext
+ b_salt = lines[0]
+ b_hmac = lines[1]
+ b_ciphertext_data = b'\n'.join(lines[2:])
+
+ b_ciphertext = binascii.unhexlify(b_ciphertext_data)
+ # b_orig_ciphertext = b_ciphertext[:]
+
+ # now muck with the text
+ # b_munged_ciphertext = b_ciphertext[:10] + b'\x00' + b_ciphertext[11:]
+ # b_munged_ciphertext = b_ciphertext
+ # assert b_orig_ciphertext != b_munged_ciphertext
+
+ b_ciphertext_data = binascii.hexlify(b_ciphertext)
+ b_payload = b'\n'.join([b_salt, b_hmac, b_ciphertext_data])
+ # reformat
+ b_invalid_ciphertext = self.v._format_output(b_payload)
+
+ # assert we throw an error
+ self.v.decrypt(b_invalid_ciphertext)
+
+ def test_decrypt_and_get_vault_id(self):
+ b_expected_plaintext = to_bytes('foo bar\n')
+ vaulttext = '''$ANSIBLE_VAULT;1.2;AES256;ansible_devel
+65616435333934613466373335363332373764363365633035303466643439313864663837393234
+3330656363343637313962633731333237313636633534630a386264363438363362326132363239
+39363166646664346264383934393935653933316263333838386362633534326664646166663736
+6462303664383765650a356637643633366663643566353036303162386237336233393065393164
+6264'''
+
+ vault_secrets = self._vault_secrets_from_password('ansible_devel', 'ansible')
+ v = vault.VaultLib(vault_secrets)
+
+ b_vaulttext = to_bytes(vaulttext)
+
+ b_plaintext, vault_id_used, vault_secret_used = v.decrypt_and_get_vault_id(b_vaulttext)
+
+ self.assertEqual(b_expected_plaintext, b_plaintext)
+ self.assertEqual(vault_id_used, 'ansible_devel')
+ self.assertEqual(vault_secret_used.text, 'ansible')
+
+ def test_decrypt_non_default_1_2(self):
+ b_expected_plaintext = to_bytes('foo bar\n')
+ vaulttext = '''$ANSIBLE_VAULT;1.2;AES256;ansible_devel
+65616435333934613466373335363332373764363365633035303466643439313864663837393234
+3330656363343637313962633731333237313636633534630a386264363438363362326132363239
+39363166646664346264383934393935653933316263333838386362633534326664646166663736
+6462303664383765650a356637643633366663643566353036303162386237336233393065393164
+6264'''
+
+ vault_secrets = self._vault_secrets_from_password('default', 'ansible')
+ v = vault.VaultLib(vault_secrets)
+
+ b_vaulttext = to_bytes(vaulttext)
+
+ b_plaintext = v.decrypt(b_vaulttext)
+ self.assertEqual(b_expected_plaintext, b_plaintext)
+
+ b_ciphertext, b_version, cipher_name, vault_id = vault.parse_vaulttext_envelope(b_vaulttext)
+ self.assertEqual('ansible_devel', vault_id)
+ self.assertEqual(b'1.2', b_version)
+
+ def test_decrypt_decrypted(self):
+ plaintext = u"ansible"
+ self.assertRaises(errors.AnsibleError, self.v.decrypt, plaintext)
+
+ b_plaintext = b"ansible"
+ self.assertRaises(errors.AnsibleError, self.v.decrypt, b_plaintext)
+
+ def test_cipher_not_set(self):
+ plaintext = u"ansible"
+ self.v.encrypt(plaintext)
+ self.assertEqual(self.v.cipher_name, "AES256")
diff --git a/test/units/parsing/vault/test_vault_editor.py b/test/units/parsing/vault/test_vault_editor.py
new file mode 100644
index 0000000..77509f0
--- /dev/null
+++ b/test/units/parsing/vault/test_vault_editor.py
@@ -0,0 +1,521 @@
+# (c) 2014, James Tanner <tanner.jc@gmail.com>
+# (c) 2014, James Cammarata, <jcammarata@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import tempfile
+from io import BytesIO, StringIO
+
+import pytest
+
+from units.compat import unittest
+from unittest.mock import patch
+
+from ansible import errors
+from ansible.parsing import vault
+from ansible.parsing.vault import VaultLib, VaultEditor, match_encrypt_secret
+
+from ansible.module_utils.six import PY3
+from ansible.module_utils._text import to_bytes, to_text
+
+from units.mock.vault_helper import TextVaultSecret
+
+v11_data = """$ANSIBLE_VAULT;1.1;AES256
+62303130653266653331306264616235333735323636616539316433666463323964623162386137
+3961616263373033353631316333623566303532663065310a393036623466376263393961326530
+64336561613965383835646464623865663966323464653236343638373165343863623638316664
+3631633031323837340a396530313963373030343933616133393566366137363761373930663833
+3739"""
+
+
+@pytest.mark.skipif(not vault.HAS_CRYPTOGRAPHY,
+ reason="Skipping cryptography tests because cryptography is not installed")
+class TestVaultEditor(unittest.TestCase):
+
+ def setUp(self):
+ self._test_dir = None
+ self.vault_password = "test-vault-password"
+ vault_secret = TextVaultSecret(self.vault_password)
+ self.vault_secrets = [('vault_secret', vault_secret),
+ ('default', vault_secret)]
+
+ @property
+ def vault_secret(self):
+ return match_encrypt_secret(self.vault_secrets)[1]
+
+ def tearDown(self):
+ if self._test_dir:
+ pass
+ # shutil.rmtree(self._test_dir)
+ self._test_dir = None
+
+ def _secrets(self, password):
+ vault_secret = TextVaultSecret(password)
+ vault_secrets = [('default', vault_secret)]
+ return vault_secrets
+
+ def test_methods_exist(self):
+ v = vault.VaultEditor(None)
+ slots = ['create_file',
+ 'decrypt_file',
+ 'edit_file',
+ 'encrypt_file',
+ 'rekey_file',
+ 'read_data',
+ 'write_data']
+ for slot in slots:
+ assert hasattr(v, slot), "VaultLib is missing the %s method" % slot
+
+ def _create_test_dir(self):
+ suffix = '_ansible_unit_test_%s_' % (self.__class__.__name__)
+ return tempfile.mkdtemp(suffix=suffix)
+
+ def _create_file(self, test_dir, name, content=None, symlink=False):
+ file_path = os.path.join(test_dir, name)
+ opened_file = open(file_path, 'wb')
+ if content:
+ opened_file.write(content)
+ opened_file.close()
+ return file_path
+
+ def _vault_editor(self, vault_secrets=None):
+ if vault_secrets is None:
+ vault_secrets = self._secrets(self.vault_password)
+ return VaultEditor(VaultLib(vault_secrets))
+
+ @patch('ansible.parsing.vault.subprocess.call')
+ def test_edit_file_helper_empty_target(self, mock_sp_call):
+ self._test_dir = self._create_test_dir()
+
+ src_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents)
+
+ mock_sp_call.side_effect = self._faux_command
+ ve = self._vault_editor()
+
+ b_ciphertext = ve._edit_file_helper(src_file_path, self.vault_secret)
+
+ self.assertNotEqual(src_contents, b_ciphertext)
+
+ def test_stdin_binary(self):
+ stdin_data = '\0'
+
+ if PY3:
+ fake_stream = StringIO(stdin_data)
+ fake_stream.buffer = BytesIO(to_bytes(stdin_data))
+ else:
+ fake_stream = BytesIO(to_bytes(stdin_data))
+
+ with patch('sys.stdin', fake_stream):
+ ve = self._vault_editor()
+ data = ve.read_data('-')
+
+ self.assertEqual(data, b'\0')
+
+ @patch('ansible.parsing.vault.subprocess.call')
+ def test_edit_file_helper_call_exception(self, mock_sp_call):
+ self._test_dir = self._create_test_dir()
+
+ src_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents)
+
+ error_txt = 'calling editor raised an exception'
+ mock_sp_call.side_effect = errors.AnsibleError(error_txt)
+
+ ve = self._vault_editor()
+
+ self.assertRaisesRegex(errors.AnsibleError,
+ error_txt,
+ ve._edit_file_helper,
+ src_file_path,
+ self.vault_secret)
+
+ @patch('ansible.parsing.vault.subprocess.call')
+ def test_edit_file_helper_symlink_target(self, mock_sp_call):
+ self._test_dir = self._create_test_dir()
+
+ src_file_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents)
+
+ src_file_link_path = os.path.join(self._test_dir, 'a_link_to_dest_file')
+
+ os.symlink(src_file_path, src_file_link_path)
+
+ mock_sp_call.side_effect = self._faux_command
+ ve = self._vault_editor()
+
+ b_ciphertext = ve._edit_file_helper(src_file_link_path, self.vault_secret)
+
+ self.assertNotEqual(src_file_contents, b_ciphertext,
+ 'b_ciphertext should be encrypted and not equal to src_contents')
+
+ def _faux_editor(self, editor_args, new_src_contents=None):
+ if editor_args[0] == 'shred':
+ return
+
+ tmp_path = editor_args[-1]
+
+ # simulate the tmp file being editted
+ tmp_file = open(tmp_path, 'wb')
+ if new_src_contents:
+ tmp_file.write(new_src_contents)
+ tmp_file.close()
+
+ def _faux_command(self, tmp_path):
+ pass
+
+ @patch('ansible.parsing.vault.subprocess.call')
+ def test_edit_file_helper_no_change(self, mock_sp_call):
+ self._test_dir = self._create_test_dir()
+
+ src_file_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents)
+
+ # editor invocation doesn't change anything
+ def faux_editor(editor_args):
+ self._faux_editor(editor_args, src_file_contents)
+
+ mock_sp_call.side_effect = faux_editor
+ ve = self._vault_editor()
+
+ ve._edit_file_helper(src_file_path, self.vault_secret, existing_data=src_file_contents)
+
+ new_target_file = open(src_file_path, 'rb')
+ new_target_file_contents = new_target_file.read()
+ self.assertEqual(src_file_contents, new_target_file_contents)
+
+ def _assert_file_is_encrypted(self, vault_editor, src_file_path, src_contents):
+ new_src_file = open(src_file_path, 'rb')
+ new_src_file_contents = new_src_file.read()
+
+ # TODO: assert that it is encrypted
+ self.assertTrue(vault.is_encrypted(new_src_file_contents))
+
+ src_file_plaintext = vault_editor.vault.decrypt(new_src_file_contents)
+
+ # the plaintext should not be encrypted
+ self.assertFalse(vault.is_encrypted(src_file_plaintext))
+
+ # and the new plaintext should match the original
+ self.assertEqual(src_file_plaintext, src_contents)
+
+ def _assert_file_is_link(self, src_file_link_path, src_file_path):
+ self.assertTrue(os.path.islink(src_file_link_path),
+ 'The dest path (%s) should be a symlink to (%s) but is not' % (src_file_link_path, src_file_path))
+
+ def test_rekey_file(self):
+ self._test_dir = self._create_test_dir()
+
+ src_file_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents)
+
+ ve = self._vault_editor()
+ ve.encrypt_file(src_file_path, self.vault_secret)
+
+ # FIXME: update to just set self._secrets or just a new vault secret id
+ new_password = 'password2:electricbugaloo'
+ new_vault_secret = TextVaultSecret(new_password)
+ new_vault_secrets = [('default', new_vault_secret)]
+ ve.rekey_file(src_file_path, vault.match_encrypt_secret(new_vault_secrets)[1])
+
+ # FIXME: can just update self._secrets here
+ new_ve = vault.VaultEditor(VaultLib(new_vault_secrets))
+ self._assert_file_is_encrypted(new_ve, src_file_path, src_file_contents)
+
+ def test_rekey_file_no_new_password(self):
+ self._test_dir = self._create_test_dir()
+
+ src_file_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents)
+
+ ve = self._vault_editor()
+ ve.encrypt_file(src_file_path, self.vault_secret)
+
+ self.assertRaisesRegex(errors.AnsibleError,
+ 'The value for the new_password to rekey',
+ ve.rekey_file,
+ src_file_path,
+ None)
+
+ def test_rekey_file_not_encrypted(self):
+ self._test_dir = self._create_test_dir()
+
+ src_file_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents)
+
+ ve = self._vault_editor()
+
+ new_password = 'password2:electricbugaloo'
+ self.assertRaisesRegex(errors.AnsibleError,
+ 'input is not vault encrypted data',
+ ve.rekey_file,
+ src_file_path, new_password)
+
+ def test_plaintext(self):
+ self._test_dir = self._create_test_dir()
+
+ src_file_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents)
+
+ ve = self._vault_editor()
+ ve.encrypt_file(src_file_path, self.vault_secret)
+
+ res = ve.plaintext(src_file_path)
+ self.assertEqual(src_file_contents, res)
+
+ def test_plaintext_not_encrypted(self):
+ self._test_dir = self._create_test_dir()
+
+ src_file_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents)
+
+ ve = self._vault_editor()
+ self.assertRaisesRegex(errors.AnsibleError,
+ 'input is not vault encrypted data',
+ ve.plaintext,
+ src_file_path)
+
+ def test_encrypt_file(self):
+ self._test_dir = self._create_test_dir()
+ src_file_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents)
+
+ ve = self._vault_editor()
+ ve.encrypt_file(src_file_path, self.vault_secret)
+
+ self._assert_file_is_encrypted(ve, src_file_path, src_file_contents)
+
+ def test_encrypt_file_symlink(self):
+ self._test_dir = self._create_test_dir()
+
+ src_file_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_file_contents)
+
+ src_file_link_path = os.path.join(self._test_dir, 'a_link_to_dest_file')
+ os.symlink(src_file_path, src_file_link_path)
+
+ ve = self._vault_editor()
+ ve.encrypt_file(src_file_link_path, self.vault_secret)
+
+ self._assert_file_is_encrypted(ve, src_file_path, src_file_contents)
+ self._assert_file_is_encrypted(ve, src_file_link_path, src_file_contents)
+
+ self._assert_file_is_link(src_file_link_path, src_file_path)
+
+ @patch('ansible.parsing.vault.subprocess.call')
+ def test_edit_file_no_vault_id(self, mock_sp_call):
+ self._test_dir = self._create_test_dir()
+ src_contents = to_bytes("some info in a file\nyup.")
+
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents)
+
+ new_src_contents = to_bytes("The info is different now.")
+
+ def faux_editor(editor_args):
+ self._faux_editor(editor_args, new_src_contents)
+
+ mock_sp_call.side_effect = faux_editor
+
+ ve = self._vault_editor()
+
+ ve.encrypt_file(src_file_path, self.vault_secret)
+ ve.edit_file(src_file_path)
+
+ new_src_file = open(src_file_path, 'rb')
+ new_src_file_contents = new_src_file.read()
+
+ self.assertTrue(b'$ANSIBLE_VAULT;1.1;AES256' in new_src_file_contents)
+
+ src_file_plaintext = ve.vault.decrypt(new_src_file_contents)
+ self.assertEqual(src_file_plaintext, new_src_contents)
+
+ @patch('ansible.parsing.vault.subprocess.call')
+ def test_edit_file_with_vault_id(self, mock_sp_call):
+ self._test_dir = self._create_test_dir()
+ src_contents = to_bytes("some info in a file\nyup.")
+
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents)
+
+ new_src_contents = to_bytes("The info is different now.")
+
+ def faux_editor(editor_args):
+ self._faux_editor(editor_args, new_src_contents)
+
+ mock_sp_call.side_effect = faux_editor
+
+ ve = self._vault_editor()
+
+ ve.encrypt_file(src_file_path, self.vault_secret,
+ vault_id='vault_secrets')
+ ve.edit_file(src_file_path)
+
+ new_src_file = open(src_file_path, 'rb')
+ new_src_file_contents = new_src_file.read()
+
+ self.assertTrue(b'$ANSIBLE_VAULT;1.2;AES256;vault_secrets' in new_src_file_contents)
+
+ src_file_plaintext = ve.vault.decrypt(new_src_file_contents)
+ self.assertEqual(src_file_plaintext, new_src_contents)
+
+ @patch('ansible.parsing.vault.subprocess.call')
+ def test_edit_file_symlink(self, mock_sp_call):
+ self._test_dir = self._create_test_dir()
+ src_contents = to_bytes("some info in a file\nyup.")
+
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents)
+
+ new_src_contents = to_bytes("The info is different now.")
+
+ def faux_editor(editor_args):
+ self._faux_editor(editor_args, new_src_contents)
+
+ mock_sp_call.side_effect = faux_editor
+
+ ve = self._vault_editor()
+
+ ve.encrypt_file(src_file_path, self.vault_secret)
+
+ src_file_link_path = os.path.join(self._test_dir, 'a_link_to_dest_file')
+
+ os.symlink(src_file_path, src_file_link_path)
+
+ ve.edit_file(src_file_link_path)
+
+ new_src_file = open(src_file_path, 'rb')
+ new_src_file_contents = new_src_file.read()
+
+ src_file_plaintext = ve.vault.decrypt(new_src_file_contents)
+
+ self._assert_file_is_link(src_file_link_path, src_file_path)
+
+ self.assertEqual(src_file_plaintext, new_src_contents)
+
+ # self.assertEqual(src_file_plaintext, new_src_contents,
+ # 'The decrypted plaintext of the editted file is not the expected contents.')
+
+ @patch('ansible.parsing.vault.subprocess.call')
+ def test_edit_file_not_encrypted(self, mock_sp_call):
+ self._test_dir = self._create_test_dir()
+ src_contents = to_bytes("some info in a file\nyup.")
+
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents)
+
+ new_src_contents = to_bytes("The info is different now.")
+
+ def faux_editor(editor_args):
+ self._faux_editor(editor_args, new_src_contents)
+
+ mock_sp_call.side_effect = faux_editor
+
+ ve = self._vault_editor()
+ self.assertRaisesRegex(errors.AnsibleError,
+ 'input is not vault encrypted data',
+ ve.edit_file,
+ src_file_path)
+
+ def test_create_file_exists(self):
+ self._test_dir = self._create_test_dir()
+ src_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents)
+
+ ve = self._vault_editor()
+ self.assertRaisesRegex(errors.AnsibleError,
+ 'please use .edit. instead',
+ ve.create_file,
+ src_file_path,
+ self.vault_secret)
+
+ def test_decrypt_file_exception(self):
+ self._test_dir = self._create_test_dir()
+ src_contents = to_bytes("some info in a file\nyup.")
+ src_file_path = self._create_file(self._test_dir, 'src_file', content=src_contents)
+
+ ve = self._vault_editor()
+ self.assertRaisesRegex(errors.AnsibleError,
+ 'input is not vault encrypted data',
+ ve.decrypt_file,
+ src_file_path)
+
+ @patch.object(vault.VaultEditor, '_editor_shell_command')
+ def test_create_file(self, mock_editor_shell_command):
+
+ def sc_side_effect(filename):
+ return ['touch', filename]
+ mock_editor_shell_command.side_effect = sc_side_effect
+
+ tmp_file = tempfile.NamedTemporaryFile()
+ os.unlink(tmp_file.name)
+
+ _secrets = self._secrets('ansible')
+ ve = self._vault_editor(_secrets)
+ ve.create_file(tmp_file.name, vault.match_encrypt_secret(_secrets)[1])
+
+ self.assertTrue(os.path.exists(tmp_file.name))
+
+ def test_decrypt_1_1(self):
+ v11_file = tempfile.NamedTemporaryFile(delete=False)
+ with v11_file as f:
+ f.write(to_bytes(v11_data))
+
+ ve = self._vault_editor(self._secrets("ansible"))
+
+ # make sure the password functions for the cipher
+ error_hit = False
+ try:
+ ve.decrypt_file(v11_file.name)
+ except errors.AnsibleError:
+ error_hit = True
+
+ # verify decrypted content
+ f = open(v11_file.name, "rb")
+ fdata = to_text(f.read())
+ f.close()
+
+ os.unlink(v11_file.name)
+
+ assert error_hit is False, "error decrypting 1.1 file"
+ assert fdata.strip() == "foo", "incorrect decryption of 1.1 file: %s" % fdata.strip()
+
+ def test_real_path_dash(self):
+ filename = '-'
+ ve = self._vault_editor()
+
+ res = ve._real_path(filename)
+ self.assertEqual(res, '-')
+
+ def test_real_path_dev_null(self):
+ filename = '/dev/null'
+ ve = self._vault_editor()
+
+ res = ve._real_path(filename)
+ self.assertEqual(res, '/dev/null')
+
+ def test_real_path_symlink(self):
+ self._test_dir = os.path.realpath(self._create_test_dir())
+ file_path = self._create_file(self._test_dir, 'test_file', content=b'this is a test file')
+ file_link_path = os.path.join(self._test_dir, 'a_link_to_test_file')
+
+ os.symlink(file_path, file_link_path)
+
+ ve = self._vault_editor()
+
+ res = ve._real_path(file_link_path)
+ self.assertEqual(res, file_path)
diff --git a/test/units/parsing/yaml/__init__.py b/test/units/parsing/yaml/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/parsing/yaml/__init__.py
diff --git a/test/units/parsing/yaml/test_constructor.py b/test/units/parsing/yaml/test_constructor.py
new file mode 100644
index 0000000..717bf35
--- /dev/null
+++ b/test/units/parsing/yaml/test_constructor.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# (c) 2020 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+from yaml import MappingNode, Mark, ScalarNode
+from yaml.constructor import ConstructorError
+
+import ansible.constants as C
+from ansible.utils.display import Display
+from ansible.parsing.yaml.constructor import AnsibleConstructor
+
+
+@pytest.fixture
+def dupe_node():
+ tag = 'tag:yaml.org,2002:map'
+ scalar_tag = 'tag:yaml.org,2002:str'
+ mark = Mark(tag, 0, 0, 0, None, None)
+ node = MappingNode(
+ tag,
+ [
+ (
+ ScalarNode(tag=scalar_tag, value='bar', start_mark=mark),
+ ScalarNode(tag=scalar_tag, value='baz', start_mark=mark)
+ ),
+ (
+ ScalarNode(tag=scalar_tag, value='bar', start_mark=mark),
+ ScalarNode(tag=scalar_tag, value='qux', start_mark=mark)
+ ),
+ ],
+ start_mark=mark
+ )
+
+ return node
+
+
+class Capture:
+ def __init__(self):
+ self.called = False
+ self.calls = []
+
+ def __call__(self, *args, **kwargs):
+ self.called = True
+ self.calls.append((
+ args,
+ kwargs
+ ))
+
+
+def test_duplicate_yaml_dict_key_ignore(dupe_node, monkeypatch):
+ monkeypatch.setattr(C, 'DUPLICATE_YAML_DICT_KEY', 'ignore')
+ cap = Capture()
+ monkeypatch.setattr(Display(), 'warning', cap)
+ ac = AnsibleConstructor()
+ ac.construct_mapping(dupe_node)
+ assert not cap.called
+
+
+def test_duplicate_yaml_dict_key_warn(dupe_node, monkeypatch):
+ monkeypatch.setattr(C, 'DUPLICATE_YAML_DICT_KEY', 'warn')
+ cap = Capture()
+ monkeypatch.setattr(Display(), 'warning', cap)
+ ac = AnsibleConstructor()
+ ac.construct_mapping(dupe_node)
+ assert cap.called
+ expected = [
+ (
+ (
+ 'While constructing a mapping from tag:yaml.org,2002:map, line 1, column 1, '
+ 'found a duplicate dict key (bar). Using last defined value only.',
+ ),
+ {}
+ )
+ ]
+ assert cap.calls == expected
+
+
+def test_duplicate_yaml_dict_key_error(dupe_node, monkeypatch, mocker):
+ monkeypatch.setattr(C, 'DUPLICATE_YAML_DICT_KEY', 'error')
+ ac = AnsibleConstructor()
+ pytest.raises(ConstructorError, ac.construct_mapping, dupe_node)
diff --git a/test/units/parsing/yaml/test_dumper.py b/test/units/parsing/yaml/test_dumper.py
new file mode 100644
index 0000000..5fbc139
--- /dev/null
+++ b/test/units/parsing/yaml/test_dumper.py
@@ -0,0 +1,123 @@
+# coding: utf-8
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import io
+import yaml
+
+from jinja2.exceptions import UndefinedError
+
+from units.compat import unittest
+from ansible.parsing import vault
+from ansible.parsing.yaml import dumper, objects
+from ansible.parsing.yaml.loader import AnsibleLoader
+from ansible.module_utils.six import PY2
+from ansible.template import AnsibleUndefined
+from ansible.utils.unsafe_proxy import AnsibleUnsafeText, AnsibleUnsafeBytes
+
+from units.mock.yaml_helper import YamlTestUtils
+from units.mock.vault_helper import TextVaultSecret
+from ansible.vars.manager import VarsWithSources
+
+
+class TestAnsibleDumper(unittest.TestCase, YamlTestUtils):
+ def setUp(self):
+ self.vault_password = "hunter42"
+ vault_secret = TextVaultSecret(self.vault_password)
+ self.vault_secrets = [('vault_secret', vault_secret)]
+ self.good_vault = vault.VaultLib(self.vault_secrets)
+ self.vault = self.good_vault
+ self.stream = self._build_stream()
+ self.dumper = dumper.AnsibleDumper
+
+ def _build_stream(self, yaml_text=None):
+ text = yaml_text or u''
+ stream = io.StringIO(text)
+ return stream
+
+ def _loader(self, stream):
+ return AnsibleLoader(stream, vault_secrets=self.vault.secrets)
+
+ def test_ansible_vault_encrypted_unicode(self):
+ plaintext = 'This is a string we are going to encrypt.'
+ avu = objects.AnsibleVaultEncryptedUnicode.from_plaintext(plaintext, vault=self.vault,
+ secret=vault.match_secrets(self.vault_secrets, ['vault_secret'])[0][1])
+
+ yaml_out = self._dump_string(avu, dumper=self.dumper)
+ stream = self._build_stream(yaml_out)
+ loader = self._loader(stream)
+
+ data_from_yaml = loader.get_single_data()
+
+ self.assertEqual(plaintext, data_from_yaml.data)
+
+ def test_bytes(self):
+ b_text = u'tréma'.encode('utf-8')
+ unsafe_object = AnsibleUnsafeBytes(b_text)
+ yaml_out = self._dump_string(unsafe_object, dumper=self.dumper)
+
+ stream = self._build_stream(yaml_out)
+ loader = self._loader(stream)
+
+ data_from_yaml = loader.get_single_data()
+
+ result = b_text
+ if PY2:
+ # https://pyyaml.org/wiki/PyYAMLDocumentation#string-conversion-python-2-only
+ # pyyaml on Python 2 can return either unicode or bytes when given byte strings.
+ # We normalize that to always return unicode on Python2 as that's right most of the
+ # time. However, this means byte strings can round trip through yaml on Python3 but
+ # not on Python2. To make this code work the same on Python2 and Python3 (we want
+ # the Python3 behaviour) we need to change the methods in Ansible to:
+ # (1) Let byte strings pass through yaml without being converted on Python2
+ # (2) Convert byte strings to text strings before being given to pyyaml (Without this,
+ # strings would end up as byte strings most of the time which would mostly be wrong)
+ # In practice, we mostly read bytes in from files and then pass that to pyyaml, for which
+ # the present behavior is correct.
+ # This is a workaround for the current behavior.
+ result = u'tr\xe9ma'
+
+ self.assertEqual(result, data_from_yaml)
+
+ def test_unicode(self):
+ u_text = u'nöel'
+ unsafe_object = AnsibleUnsafeText(u_text)
+ yaml_out = self._dump_string(unsafe_object, dumper=self.dumper)
+
+ stream = self._build_stream(yaml_out)
+ loader = self._loader(stream)
+
+ data_from_yaml = loader.get_single_data()
+
+ self.assertEqual(u_text, data_from_yaml)
+
+ def test_vars_with_sources(self):
+ try:
+ self._dump_string(VarsWithSources(), dumper=self.dumper)
+ except yaml.representer.RepresenterError:
+ self.fail("Dump VarsWithSources raised RepresenterError unexpectedly!")
+
+ def test_undefined(self):
+ undefined_object = AnsibleUndefined()
+ try:
+ yaml_out = self._dump_string(undefined_object, dumper=self.dumper)
+ except UndefinedError:
+ yaml_out = None
+
+ self.assertIsNone(yaml_out)
diff --git a/test/units/parsing/yaml/test_loader.py b/test/units/parsing/yaml/test_loader.py
new file mode 100644
index 0000000..117f80a
--- /dev/null
+++ b/test/units/parsing/yaml/test_loader.py
@@ -0,0 +1,432 @@
+# coding: utf-8
+# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from collections.abc import Sequence, Set, Mapping
+from io import StringIO
+
+from units.compat import unittest
+
+from ansible import errors
+from ansible.module_utils.six import text_type, binary_type
+from ansible.parsing.yaml.loader import AnsibleLoader
+from ansible.parsing import vault
+from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
+from ansible.parsing.yaml.dumper import AnsibleDumper
+
+from units.mock.yaml_helper import YamlTestUtils
+from units.mock.vault_helper import TextVaultSecret
+
+from yaml.parser import ParserError
+from yaml.scanner import ScannerError
+
+
+class NameStringIO(StringIO):
+ """In py2.6, StringIO doesn't let you set name because a baseclass has it
+ as readonly property"""
+ name = None
+
+ def __init__(self, *args, **kwargs):
+ super(NameStringIO, self).__init__(*args, **kwargs)
+
+
+class TestAnsibleLoaderBasic(unittest.TestCase):
+
+ def test_parse_number(self):
+ stream = StringIO(u"""
+ 1
+ """)
+ loader = AnsibleLoader(stream, 'myfile.yml')
+ data = loader.get_single_data()
+ self.assertEqual(data, 1)
+ # No line/column info saved yet
+
+ def test_parse_string(self):
+ stream = StringIO(u"""
+ Ansible
+ """)
+ loader = AnsibleLoader(stream, 'myfile.yml')
+ data = loader.get_single_data()
+ self.assertEqual(data, u'Ansible')
+ self.assertIsInstance(data, text_type)
+
+ self.assertEqual(data.ansible_pos, ('myfile.yml', 2, 17))
+
+ def test_parse_utf8_string(self):
+ stream = StringIO(u"""
+ Cafè Eñyei
+ """)
+ loader = AnsibleLoader(stream, 'myfile.yml')
+ data = loader.get_single_data()
+ self.assertEqual(data, u'Cafè Eñyei')
+ self.assertIsInstance(data, text_type)
+
+ self.assertEqual(data.ansible_pos, ('myfile.yml', 2, 17))
+
+ def test_parse_dict(self):
+ stream = StringIO(u"""
+ webster: daniel
+ oed: oxford
+ """)
+ loader = AnsibleLoader(stream, 'myfile.yml')
+ data = loader.get_single_data()
+ self.assertEqual(data, {'webster': 'daniel', 'oed': 'oxford'})
+ self.assertEqual(len(data), 2)
+ self.assertIsInstance(list(data.keys())[0], text_type)
+ self.assertIsInstance(list(data.values())[0], text_type)
+
+ # Beginning of the first key
+ self.assertEqual(data.ansible_pos, ('myfile.yml', 2, 17))
+
+ self.assertEqual(data[u'webster'].ansible_pos, ('myfile.yml', 2, 26))
+ self.assertEqual(data[u'oed'].ansible_pos, ('myfile.yml', 3, 22))
+
+ def test_parse_list(self):
+ stream = StringIO(u"""
+ - a
+ - b
+ """)
+ loader = AnsibleLoader(stream, 'myfile.yml')
+ data = loader.get_single_data()
+ self.assertEqual(data, [u'a', u'b'])
+ self.assertEqual(len(data), 2)
+ self.assertIsInstance(data[0], text_type)
+
+ self.assertEqual(data.ansible_pos, ('myfile.yml', 2, 17))
+
+ self.assertEqual(data[0].ansible_pos, ('myfile.yml', 2, 19))
+ self.assertEqual(data[1].ansible_pos, ('myfile.yml', 3, 19))
+
+ def test_parse_short_dict(self):
+ stream = StringIO(u"""{"foo": "bar"}""")
+ loader = AnsibleLoader(stream, 'myfile.yml')
+ data = loader.get_single_data()
+ self.assertEqual(data, dict(foo=u'bar'))
+
+ self.assertEqual(data.ansible_pos, ('myfile.yml', 1, 1))
+ self.assertEqual(data[u'foo'].ansible_pos, ('myfile.yml', 1, 9))
+
+ stream = StringIO(u"""foo: bar""")
+ loader = AnsibleLoader(stream, 'myfile.yml')
+ data = loader.get_single_data()
+ self.assertEqual(data, dict(foo=u'bar'))
+
+ self.assertEqual(data.ansible_pos, ('myfile.yml', 1, 1))
+ self.assertEqual(data[u'foo'].ansible_pos, ('myfile.yml', 1, 6))
+
+ def test_error_conditions(self):
+ stream = StringIO(u"""{""")
+ loader = AnsibleLoader(stream, 'myfile.yml')
+ self.assertRaises(ParserError, loader.get_single_data)
+
+ def test_tab_error(self):
+ stream = StringIO(u"""---\nhosts: localhost\nvars:\n foo: bar\n\tblip: baz""")
+ loader = AnsibleLoader(stream, 'myfile.yml')
+ self.assertRaises(ScannerError, loader.get_single_data)
+
+ def test_front_matter(self):
+ stream = StringIO(u"""---\nfoo: bar""")
+ loader = AnsibleLoader(stream, 'myfile.yml')
+ data = loader.get_single_data()
+ self.assertEqual(data, dict(foo=u'bar'))
+
+ self.assertEqual(data.ansible_pos, ('myfile.yml', 2, 1))
+ self.assertEqual(data[u'foo'].ansible_pos, ('myfile.yml', 2, 6))
+
+ # Initial indent (See: #6348)
+ stream = StringIO(u""" - foo: bar\n baz: qux""")
+ loader = AnsibleLoader(stream, 'myfile.yml')
+ data = loader.get_single_data()
+ self.assertEqual(data, [{u'foo': u'bar', u'baz': u'qux'}])
+
+ self.assertEqual(data.ansible_pos, ('myfile.yml', 1, 2))
+ self.assertEqual(data[0].ansible_pos, ('myfile.yml', 1, 4))
+ self.assertEqual(data[0][u'foo'].ansible_pos, ('myfile.yml', 1, 9))
+ self.assertEqual(data[0][u'baz'].ansible_pos, ('myfile.yml', 2, 9))
+
+
+class TestAnsibleLoaderVault(unittest.TestCase, YamlTestUtils):
+ def setUp(self):
+ self.vault_password = "hunter42"
+ vault_secret = TextVaultSecret(self.vault_password)
+ self.vault_secrets = [('vault_secret', vault_secret),
+ ('default', vault_secret)]
+ self.vault = vault.VaultLib(self.vault_secrets)
+
+ @property
+ def vault_secret(self):
+ return vault.match_encrypt_secret(self.vault_secrets)[1]
+
+ def test_wrong_password(self):
+ plaintext = u"Ansible"
+ bob_password = "this is a different password"
+
+ bobs_secret = TextVaultSecret(bob_password)
+ bobs_secrets = [('default', bobs_secret)]
+
+ bobs_vault = vault.VaultLib(bobs_secrets)
+
+ ciphertext = bobs_vault.encrypt(plaintext, vault.match_encrypt_secret(bobs_secrets)[1])
+
+ try:
+ self.vault.decrypt(ciphertext)
+ except Exception as e:
+ self.assertIsInstance(e, errors.AnsibleError)
+ self.assertEqual(e.message, 'Decryption failed (no vault secrets were found that could decrypt)')
+
+ def _encrypt_plaintext(self, plaintext):
+ # Construct a yaml repr of a vault by hand
+ vaulted_var_bytes = self.vault.encrypt(plaintext, self.vault_secret)
+
+ # add yaml tag
+ vaulted_var = vaulted_var_bytes.decode()
+ lines = vaulted_var.splitlines()
+ lines2 = []
+ for line in lines:
+ lines2.append(' %s' % line)
+
+ vaulted_var = '\n'.join(lines2)
+ tagged_vaulted_var = u"""!vault |\n%s""" % vaulted_var
+ return tagged_vaulted_var
+
+ def _build_stream(self, yaml_text):
+ stream = NameStringIO(yaml_text)
+ stream.name = 'my.yml'
+ return stream
+
+ def _loader(self, stream):
+ return AnsibleLoader(stream, vault_secrets=self.vault.secrets)
+
+ def _load_yaml(self, yaml_text, password):
+ stream = self._build_stream(yaml_text)
+ loader = self._loader(stream)
+
+ data_from_yaml = loader.get_single_data()
+
+ return data_from_yaml
+
+ def test_dump_load_cycle(self):
+ avu = AnsibleVaultEncryptedUnicode.from_plaintext('The plaintext for test_dump_load_cycle.', self.vault, self.vault_secret)
+ self._dump_load_cycle(avu)
+
+ def test_embedded_vault_from_dump(self):
+ avu = AnsibleVaultEncryptedUnicode.from_plaintext('setec astronomy', self.vault, self.vault_secret)
+ blip = {'stuff1': [{'a dict key': 24},
+ {'shhh-ssh-secrets': avu,
+ 'nothing to see here': 'move along'}],
+ 'another key': 24.1}
+
+ blip = ['some string', 'another string', avu]
+ stream = NameStringIO()
+
+ self._dump_stream(blip, stream, dumper=AnsibleDumper)
+
+ stream.seek(0)
+
+ stream.seek(0)
+
+ loader = self._loader(stream)
+
+ data_from_yaml = loader.get_data()
+
+ stream2 = NameStringIO(u'')
+ # verify we can dump the object again
+ self._dump_stream(data_from_yaml, stream2, dumper=AnsibleDumper)
+
+ def test_embedded_vault(self):
+ plaintext_var = u"""This is the plaintext string."""
+ tagged_vaulted_var = self._encrypt_plaintext(plaintext_var)
+ another_vaulted_var = self._encrypt_plaintext(plaintext_var)
+
+ different_var = u"""A different string that is not the same as the first one."""
+ different_vaulted_var = self._encrypt_plaintext(different_var)
+
+ yaml_text = u"""---\nwebster: daniel\noed: oxford\nthe_secret: %s\nanother_secret: %s\ndifferent_secret: %s""" % (tagged_vaulted_var,
+ another_vaulted_var,
+ different_vaulted_var)
+
+ data_from_yaml = self._load_yaml(yaml_text, self.vault_password)
+ vault_string = data_from_yaml['the_secret']
+
+ self.assertEqual(plaintext_var, data_from_yaml['the_secret'])
+
+ test_dict = {}
+ test_dict[vault_string] = 'did this work?'
+
+ self.assertEqual(vault_string.data, vault_string)
+
+ # This looks weird and useless, but the object in question has a custom __eq__
+ self.assertEqual(vault_string, vault_string)
+
+ another_vault_string = data_from_yaml['another_secret']
+ different_vault_string = data_from_yaml['different_secret']
+
+ self.assertEqual(vault_string, another_vault_string)
+ self.assertNotEqual(vault_string, different_vault_string)
+
+ # More testing of __eq__/__ne__
+ self.assertTrue('some string' != vault_string)
+ self.assertNotEqual('some string', vault_string)
+
+ # Note this is a compare of the str/unicode of these, they are different types
+ # so we want to test self == other, and other == self etc
+ self.assertEqual(plaintext_var, vault_string)
+ self.assertEqual(vault_string, plaintext_var)
+ self.assertFalse(plaintext_var != vault_string)
+ self.assertFalse(vault_string != plaintext_var)
+
+
+class TestAnsibleLoaderPlay(unittest.TestCase):
+
+ def setUp(self):
+ stream = NameStringIO(u"""
+ - hosts: localhost
+ vars:
+ number: 1
+ string: Ansible
+ utf8_string: Cafè Eñyei
+ dictionary:
+ webster: daniel
+ oed: oxford
+ list:
+ - a
+ - b
+ - 1
+ - 2
+ tasks:
+ - name: Test case
+ ping:
+ data: "{{ utf8_string }}"
+
+ - name: Test 2
+ ping:
+ data: "Cafè Eñyei"
+
+ - name: Test 3
+ command: "printf 'Cafè Eñyei\\n'"
+ """)
+ self.play_filename = '/path/to/myplay.yml'
+ stream.name = self.play_filename
+ self.loader = AnsibleLoader(stream)
+ self.data = self.loader.get_single_data()
+
+ def tearDown(self):
+ pass
+
+ def test_data_complete(self):
+ self.assertEqual(len(self.data), 1)
+ self.assertIsInstance(self.data, list)
+ self.assertEqual(frozenset(self.data[0].keys()), frozenset((u'hosts', u'vars', u'tasks')))
+
+ self.assertEqual(self.data[0][u'hosts'], u'localhost')
+
+ self.assertEqual(self.data[0][u'vars'][u'number'], 1)
+ self.assertEqual(self.data[0][u'vars'][u'string'], u'Ansible')
+ self.assertEqual(self.data[0][u'vars'][u'utf8_string'], u'Cafè Eñyei')
+ self.assertEqual(self.data[0][u'vars'][u'dictionary'], {
+ u'webster': u'daniel',
+ u'oed': u'oxford'
+ })
+ self.assertEqual(self.data[0][u'vars'][u'list'], [u'a', u'b', 1, 2])
+
+ self.assertEqual(self.data[0][u'tasks'], [
+ {u'name': u'Test case', u'ping': {u'data': u'{{ utf8_string }}'}},
+ {u'name': u'Test 2', u'ping': {u'data': u'Cafè Eñyei'}},
+ {u'name': u'Test 3', u'command': u'printf \'Cafè Eñyei\n\''},
+ ])
+
+ def walk(self, data):
+ # Make sure there's no str in the data
+ self.assertNotIsInstance(data, binary_type)
+
+ # Descend into various container types
+ if isinstance(data, text_type):
+ # strings are a sequence so we have to be explicit here
+ return
+ elif isinstance(data, (Sequence, Set)):
+ for element in data:
+ self.walk(element)
+ elif isinstance(data, Mapping):
+ for k, v in data.items():
+ self.walk(k)
+ self.walk(v)
+
+ # Scalars were all checked so we're good to go
+ return
+
+ def test_no_str_in_data(self):
+ # Checks that no strings are str type
+ self.walk(self.data)
+
+ def check_vars(self):
+ # Numbers don't have line/col information yet
+ # self.assertEqual(self.data[0][u'vars'][u'number'].ansible_pos, (self.play_filename, 4, 21))
+
+ self.assertEqual(self.data[0][u'vars'][u'string'].ansible_pos, (self.play_filename, 5, 29))
+ self.assertEqual(self.data[0][u'vars'][u'utf8_string'].ansible_pos, (self.play_filename, 6, 34))
+
+ self.assertEqual(self.data[0][u'vars'][u'dictionary'].ansible_pos, (self.play_filename, 8, 23))
+ self.assertEqual(self.data[0][u'vars'][u'dictionary'][u'webster'].ansible_pos, (self.play_filename, 8, 32))
+ self.assertEqual(self.data[0][u'vars'][u'dictionary'][u'oed'].ansible_pos, (self.play_filename, 9, 28))
+
+ self.assertEqual(self.data[0][u'vars'][u'list'].ansible_pos, (self.play_filename, 11, 23))
+ self.assertEqual(self.data[0][u'vars'][u'list'][0].ansible_pos, (self.play_filename, 11, 25))
+ self.assertEqual(self.data[0][u'vars'][u'list'][1].ansible_pos, (self.play_filename, 12, 25))
+ # Numbers don't have line/col info yet
+ # self.assertEqual(self.data[0][u'vars'][u'list'][2].ansible_pos, (self.play_filename, 13, 25))
+ # self.assertEqual(self.data[0][u'vars'][u'list'][3].ansible_pos, (self.play_filename, 14, 25))
+
+ def check_tasks(self):
+ #
+ # First Task
+ #
+ self.assertEqual(self.data[0][u'tasks'][0].ansible_pos, (self.play_filename, 16, 23))
+ self.assertEqual(self.data[0][u'tasks'][0][u'name'].ansible_pos, (self.play_filename, 16, 29))
+ self.assertEqual(self.data[0][u'tasks'][0][u'ping'].ansible_pos, (self.play_filename, 18, 25))
+ self.assertEqual(self.data[0][u'tasks'][0][u'ping'][u'data'].ansible_pos, (self.play_filename, 18, 31))
+
+ #
+ # Second Task
+ #
+ self.assertEqual(self.data[0][u'tasks'][1].ansible_pos, (self.play_filename, 20, 23))
+ self.assertEqual(self.data[0][u'tasks'][1][u'name'].ansible_pos, (self.play_filename, 20, 29))
+ self.assertEqual(self.data[0][u'tasks'][1][u'ping'].ansible_pos, (self.play_filename, 22, 25))
+ self.assertEqual(self.data[0][u'tasks'][1][u'ping'][u'data'].ansible_pos, (self.play_filename, 22, 31))
+
+ #
+ # Third Task
+ #
+ self.assertEqual(self.data[0][u'tasks'][2].ansible_pos, (self.play_filename, 24, 23))
+ self.assertEqual(self.data[0][u'tasks'][2][u'name'].ansible_pos, (self.play_filename, 24, 29))
+ self.assertEqual(self.data[0][u'tasks'][2][u'command'].ansible_pos, (self.play_filename, 25, 32))
+
+ def test_line_numbers(self):
+ # Check the line/column numbers are correct
+ # Note: Remember, currently dicts begin at the start of their first entry
+ self.assertEqual(self.data[0].ansible_pos, (self.play_filename, 2, 19))
+ self.assertEqual(self.data[0][u'hosts'].ansible_pos, (self.play_filename, 2, 26))
+ self.assertEqual(self.data[0][u'vars'].ansible_pos, (self.play_filename, 4, 21))
+
+ self.check_vars()
+
+ self.assertEqual(self.data[0][u'tasks'].ansible_pos, (self.play_filename, 16, 21))
+
+ self.check_tasks()
diff --git a/test/units/parsing/yaml/test_objects.py b/test/units/parsing/yaml/test_objects.py
new file mode 100644
index 0000000..f64b708
--- /dev/null
+++ b/test/units/parsing/yaml/test_objects.py
@@ -0,0 +1,164 @@
+# This file is part of Ansible
+# -*- coding: utf-8 -*-
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+#
+# Copyright 2016, Adrian Likins <alikins@redhat.com>
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+
+from ansible.errors import AnsibleError
+
+from ansible.module_utils._text import to_native
+
+from ansible.parsing import vault
+from ansible.parsing.yaml.loader import AnsibleLoader
+
+# module under test
+from ansible.parsing.yaml import objects
+
+from units.mock.yaml_helper import YamlTestUtils
+from units.mock.vault_helper import TextVaultSecret
+
+
+class TestAnsibleVaultUnicodeNoVault(unittest.TestCase, YamlTestUtils):
+ def test_empty_init(self):
+ self.assertRaises(TypeError, objects.AnsibleVaultEncryptedUnicode)
+
+ def test_empty_string_init(self):
+ seq = ''.encode('utf8')
+ self.assert_values(seq)
+
+ def test_empty_byte_string_init(self):
+ seq = b''
+ self.assert_values(seq)
+
+ def _assert_values(self, avu, seq):
+ self.assertIsInstance(avu, objects.AnsibleVaultEncryptedUnicode)
+ self.assertTrue(avu.vault is None)
+ # AnsibleVaultEncryptedUnicode without a vault should never == any string
+ self.assertNotEqual(avu, seq)
+
+ def assert_values(self, seq):
+ avu = objects.AnsibleVaultEncryptedUnicode(seq)
+ self._assert_values(avu, seq)
+
+ def test_single_char(self):
+ seq = 'a'.encode('utf8')
+ self.assert_values(seq)
+
+ def test_string(self):
+ seq = 'some letters'
+ self.assert_values(seq)
+
+ def test_byte_string(self):
+ seq = 'some letters'.encode('utf8')
+ self.assert_values(seq)
+
+
+class TestAnsibleVaultEncryptedUnicode(unittest.TestCase, YamlTestUtils):
+ def setUp(self):
+ self.good_vault_password = "hunter42"
+ good_vault_secret = TextVaultSecret(self.good_vault_password)
+ self.good_vault_secrets = [('good_vault_password', good_vault_secret)]
+ self.good_vault = vault.VaultLib(self.good_vault_secrets)
+
+ # TODO: make this use two vault secret identities instead of two vaultSecrets
+ self.wrong_vault_password = 'not-hunter42'
+ wrong_vault_secret = TextVaultSecret(self.wrong_vault_password)
+ self.wrong_vault_secrets = [('wrong_vault_password', wrong_vault_secret)]
+ self.wrong_vault = vault.VaultLib(self.wrong_vault_secrets)
+
+ self.vault = self.good_vault
+ self.vault_secrets = self.good_vault_secrets
+
+ def _loader(self, stream):
+ return AnsibleLoader(stream, vault_secrets=self.vault_secrets)
+
+ def test_dump_load_cycle(self):
+ aveu = self._from_plaintext('the test string for TestAnsibleVaultEncryptedUnicode.test_dump_load_cycle')
+ self._dump_load_cycle(aveu)
+
+ def assert_values(self, avu, seq):
+ self.assertIsInstance(avu, objects.AnsibleVaultEncryptedUnicode)
+
+ self.assertEqual(avu, seq)
+ self.assertTrue(avu.vault is self.vault)
+ self.assertIsInstance(avu.vault, vault.VaultLib)
+
+ def _from_plaintext(self, seq):
+ id_secret = vault.match_encrypt_secret(self.good_vault_secrets)
+ return objects.AnsibleVaultEncryptedUnicode.from_plaintext(seq, vault=self.vault, secret=id_secret[1])
+
+ def _from_ciphertext(self, ciphertext):
+ avu = objects.AnsibleVaultEncryptedUnicode(ciphertext)
+ avu.vault = self.vault
+ return avu
+
+ def test_empty_init(self):
+ self.assertRaises(TypeError, objects.AnsibleVaultEncryptedUnicode)
+
+ def test_empty_string_init_from_plaintext(self):
+ seq = ''
+ avu = self._from_plaintext(seq)
+ self.assert_values(avu, seq)
+
+ def test_empty_unicode_init_from_plaintext(self):
+ seq = u''
+ avu = self._from_plaintext(seq)
+ self.assert_values(avu, seq)
+
+ def test_string_from_plaintext(self):
+ seq = 'some letters'
+ avu = self._from_plaintext(seq)
+ self.assert_values(avu, seq)
+
+ def test_unicode_from_plaintext(self):
+ seq = u'some letters'
+ avu = self._from_plaintext(seq)
+ self.assert_values(avu, seq)
+
+ def test_unicode_from_plaintext_encode(self):
+ seq = u'some text here'
+ avu = self._from_plaintext(seq)
+ b_avu = avu.encode('utf-8', 'strict')
+ self.assertIsInstance(avu, objects.AnsibleVaultEncryptedUnicode)
+ self.assertEqual(b_avu, seq.encode('utf-8', 'strict'))
+ self.assertTrue(avu.vault is self.vault)
+ self.assertIsInstance(avu.vault, vault.VaultLib)
+
+ # TODO/FIXME: make sure bad password fails differently than 'thats not encrypted'
+ def test_empty_string_wrong_password(self):
+ seq = ''
+ self.vault = self.wrong_vault
+ avu = self._from_plaintext(seq)
+
+ def compare(avu, seq):
+ return avu == seq
+
+ self.assertRaises(AnsibleError, compare, avu, seq)
+
+ def test_vaulted_utf8_value_37258(self):
+ seq = u"aöffü"
+ avu = self._from_plaintext(seq)
+ self.assert_values(avu, seq)
+
+ def test_str_vaulted_utf8_value_37258(self):
+ seq = u"aöffü"
+ avu = self._from_plaintext(seq)
+ assert str(avu) == to_native(seq)
diff --git a/test/units/playbook/__init__.py b/test/units/playbook/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/playbook/__init__.py
diff --git a/test/units/playbook/role/__init__.py b/test/units/playbook/role/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/playbook/role/__init__.py
diff --git a/test/units/playbook/role/test_include_role.py b/test/units/playbook/role/test_include_role.py
new file mode 100644
index 0000000..5e7625b
--- /dev/null
+++ b/test/units/playbook/role/test_include_role.py
@@ -0,0 +1,251 @@
+# (c) 2016, Daniel Miranda <danielkza2@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from unittest.mock import patch
+
+from ansible.playbook import Play
+from ansible.playbook.role_include import IncludeRole
+from ansible.playbook.task import Task
+from ansible.vars.manager import VariableManager
+
+from units.mock.loader import DictDataLoader
+from units.mock.path import mock_unfrackpath_noop
+
+
+class TestIncludeRole(unittest.TestCase):
+
+ def setUp(self):
+
+ self.loader = DictDataLoader({
+ '/etc/ansible/roles/l1/tasks/main.yml': """
+ - shell: echo 'hello world from l1'
+ - include_role: name=l2
+ """,
+ '/etc/ansible/roles/l1/tasks/alt.yml': """
+ - shell: echo 'hello world from l1 alt'
+ - include_role: name=l2 tasks_from=alt defaults_from=alt
+ """,
+ '/etc/ansible/roles/l1/defaults/main.yml': """
+ test_variable: l1-main
+ l1_variable: l1-main
+ """,
+ '/etc/ansible/roles/l1/defaults/alt.yml': """
+ test_variable: l1-alt
+ l1_variable: l1-alt
+ """,
+ '/etc/ansible/roles/l2/tasks/main.yml': """
+ - shell: echo 'hello world from l2'
+ - include_role: name=l3
+ """,
+ '/etc/ansible/roles/l2/tasks/alt.yml': """
+ - shell: echo 'hello world from l2 alt'
+ - include_role: name=l3 tasks_from=alt defaults_from=alt
+ """,
+ '/etc/ansible/roles/l2/defaults/main.yml': """
+ test_variable: l2-main
+ l2_variable: l2-main
+ """,
+ '/etc/ansible/roles/l2/defaults/alt.yml': """
+ test_variable: l2-alt
+ l2_variable: l2-alt
+ """,
+ '/etc/ansible/roles/l3/tasks/main.yml': """
+ - shell: echo 'hello world from l3'
+ """,
+ '/etc/ansible/roles/l3/tasks/alt.yml': """
+ - shell: echo 'hello world from l3 alt'
+ """,
+ '/etc/ansible/roles/l3/defaults/main.yml': """
+ test_variable: l3-main
+ l3_variable: l3-main
+ """,
+ '/etc/ansible/roles/l3/defaults/alt.yml': """
+ test_variable: l3-alt
+ l3_variable: l3-alt
+ """
+ })
+
+ self.var_manager = VariableManager(loader=self.loader)
+
+ def tearDown(self):
+ pass
+
+ def flatten_tasks(self, tasks):
+ for task in tasks:
+ if isinstance(task, IncludeRole):
+ blocks, handlers = task.get_block_list(loader=self.loader)
+ for block in blocks:
+ for t in self.flatten_tasks(block.block):
+ yield t
+ elif isinstance(task, Task):
+ yield task
+ else:
+ for t in self.flatten_tasks(task.block):
+ yield t
+
+ def get_tasks_vars(self, play, tasks):
+ for task in self.flatten_tasks(tasks):
+ if task.implicit:
+ # skip meta: role_complete
+ continue
+ role = task._role
+ if not role:
+ continue
+
+ yield (role.get_name(),
+ self.var_manager.get_vars(play=play, task=task))
+
+ @patch('ansible.playbook.role.definition.unfrackpath',
+ mock_unfrackpath_noop)
+ def test_simple(self):
+
+ """Test one-level include with default tasks and variables"""
+
+ play = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ tasks=[
+ {'include_role': 'name=l3'}
+ ]
+ ), loader=self.loader, variable_manager=self.var_manager)
+
+ tasks = play.compile()
+ tested = False
+ for role, task_vars in self.get_tasks_vars(play, tasks):
+ tested = True
+ self.assertEqual(task_vars.get('l3_variable'), 'l3-main')
+ self.assertEqual(task_vars.get('test_variable'), 'l3-main')
+ self.assertTrue(tested)
+
+ @patch('ansible.playbook.role.definition.unfrackpath',
+ mock_unfrackpath_noop)
+ def test_simple_alt_files(self):
+
+ """Test one-level include with alternative tasks and variables"""
+
+ play = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ tasks=[{'include_role': 'name=l3 tasks_from=alt defaults_from=alt'}]),
+ loader=self.loader, variable_manager=self.var_manager)
+
+ tasks = play.compile()
+ tested = False
+ for role, task_vars in self.get_tasks_vars(play, tasks):
+ tested = True
+ self.assertEqual(task_vars.get('l3_variable'), 'l3-alt')
+ self.assertEqual(task_vars.get('test_variable'), 'l3-alt')
+ self.assertTrue(tested)
+
+ @patch('ansible.playbook.role.definition.unfrackpath',
+ mock_unfrackpath_noop)
+ def test_nested(self):
+
+ """
+ Test nested includes with default tasks and variables.
+
+ Variables from outer roles should be inherited, but overridden in inner
+ roles.
+ """
+
+ play = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ tasks=[
+ {'include_role': 'name=l1'}
+ ]
+ ), loader=self.loader, variable_manager=self.var_manager)
+
+ tasks = play.compile()
+ expected_roles = ['l1', 'l2', 'l3']
+ for role, task_vars in self.get_tasks_vars(play, tasks):
+ expected_roles.remove(role)
+ # Outer-most role must not have variables from inner roles yet
+ if role == 'l1':
+ self.assertEqual(task_vars.get('l1_variable'), 'l1-main')
+ self.assertEqual(task_vars.get('l2_variable'), None)
+ self.assertEqual(task_vars.get('l3_variable'), None)
+ self.assertEqual(task_vars.get('test_variable'), 'l1-main')
+ # Middle role must have variables from outer role, but not inner
+ elif role == 'l2':
+ self.assertEqual(task_vars.get('l1_variable'), 'l1-main')
+ self.assertEqual(task_vars.get('l2_variable'), 'l2-main')
+ self.assertEqual(task_vars.get('l3_variable'), None)
+ self.assertEqual(task_vars.get('test_variable'), 'l2-main')
+ # Inner role must have variables from both outer roles
+ elif role == 'l3':
+ self.assertEqual(task_vars.get('l1_variable'), 'l1-main')
+ self.assertEqual(task_vars.get('l2_variable'), 'l2-main')
+ self.assertEqual(task_vars.get('l3_variable'), 'l3-main')
+ self.assertEqual(task_vars.get('test_variable'), 'l3-main')
+ else:
+ self.fail()
+ self.assertFalse(expected_roles)
+
+ @patch('ansible.playbook.role.definition.unfrackpath',
+ mock_unfrackpath_noop)
+ def test_nested_alt_files(self):
+
+ """
+ Test nested includes with alternative tasks and variables.
+
+ Variables from outer roles should be inherited, but overridden in inner
+ roles.
+ """
+
+ play = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ tasks=[
+ {'include_role': 'name=l1 tasks_from=alt defaults_from=alt'}
+ ]
+ ), loader=self.loader, variable_manager=self.var_manager)
+
+ tasks = play.compile()
+ expected_roles = ['l1', 'l2', 'l3']
+ for role, task_vars in self.get_tasks_vars(play, tasks):
+ expected_roles.remove(role)
+ # Outer-most role must not have variables from inner roles yet
+ if role == 'l1':
+ self.assertEqual(task_vars.get('l1_variable'), 'l1-alt')
+ self.assertEqual(task_vars.get('l2_variable'), None)
+ self.assertEqual(task_vars.get('l3_variable'), None)
+ self.assertEqual(task_vars.get('test_variable'), 'l1-alt')
+ # Middle role must have variables from outer role, but not inner
+ elif role == 'l2':
+ self.assertEqual(task_vars.get('l1_variable'), 'l1-alt')
+ self.assertEqual(task_vars.get('l2_variable'), 'l2-alt')
+ self.assertEqual(task_vars.get('l3_variable'), None)
+ self.assertEqual(task_vars.get('test_variable'), 'l2-alt')
+ # Inner role must have variables from both outer roles
+ elif role == 'l3':
+ self.assertEqual(task_vars.get('l1_variable'), 'l1-alt')
+ self.assertEqual(task_vars.get('l2_variable'), 'l2-alt')
+ self.assertEqual(task_vars.get('l3_variable'), 'l3-alt')
+ self.assertEqual(task_vars.get('test_variable'), 'l3-alt')
+ else:
+ self.fail()
+ self.assertFalse(expected_roles)
diff --git a/test/units/playbook/role/test_role.py b/test/units/playbook/role/test_role.py
new file mode 100644
index 0000000..5d47631
--- /dev/null
+++ b/test/units/playbook/role/test_role.py
@@ -0,0 +1,423 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from collections.abc import Container
+
+from units.compat import unittest
+from unittest.mock import patch, MagicMock
+
+from ansible.errors import AnsibleError, AnsibleParserError
+from ansible.playbook.block import Block
+
+from units.mock.loader import DictDataLoader
+from units.mock.path import mock_unfrackpath_noop
+
+from ansible.playbook.role import Role
+from ansible.playbook.role.include import RoleInclude
+from ansible.playbook.role import hash_params
+
+
+class TestHashParams(unittest.TestCase):
+ def test(self):
+ params = {'foo': 'bar'}
+ res = hash_params(params)
+ self._assert_set(res)
+ self._assert_hashable(res)
+
+ def _assert_hashable(self, res):
+ a_dict = {}
+ try:
+ a_dict[res] = res
+ except TypeError as e:
+ self.fail('%s is not hashable: %s' % (res, e))
+
+ def _assert_set(self, res):
+ self.assertIsInstance(res, frozenset)
+
+ def test_dict_tuple(self):
+ params = {'foo': (1, 'bar',)}
+ res = hash_params(params)
+ self._assert_set(res)
+
+ def test_tuple(self):
+ params = (1, None, 'foo')
+ res = hash_params(params)
+ self._assert_hashable(res)
+
+ def test_tuple_dict(self):
+ params = ({'foo': 'bar'}, 37)
+ res = hash_params(params)
+ self._assert_hashable(res)
+
+ def test_list(self):
+ params = ['foo', 'bar', 1, 37, None]
+ res = hash_params(params)
+ self._assert_set(res)
+ self._assert_hashable(res)
+
+ def test_dict_with_list_value(self):
+ params = {'foo': [1, 4, 'bar']}
+ res = hash_params(params)
+ self._assert_set(res)
+ self._assert_hashable(res)
+
+ def test_empty_set(self):
+ params = set([])
+ res = hash_params(params)
+ self._assert_hashable(res)
+ self._assert_set(res)
+
+ def test_generator(self):
+ def my_generator():
+ for i in ['a', 1, None, {}]:
+ yield i
+
+ params = my_generator()
+ res = hash_params(params)
+ self._assert_hashable(res)
+
+ def test_container_but_not_iterable(self):
+ # This is a Container that is not iterable, which is unlikely but...
+ class MyContainer(Container):
+ def __init__(self, some_thing):
+ self.data = []
+ self.data.append(some_thing)
+
+ def __contains__(self, item):
+ return item in self.data
+
+ def __hash__(self):
+ return hash(self.data)
+
+ def __len__(self):
+ return len(self.data)
+
+ def __call__(self):
+ return False
+
+ foo = MyContainer('foo bar')
+ params = foo
+
+ self.assertRaises(TypeError, hash_params, params)
+
+ def test_param_dict_dupe_values(self):
+ params1 = {'foo': False}
+ params2 = {'bar': False}
+
+ res1 = hash_params(params1)
+ res2 = hash_params(params2)
+
+ hash1 = hash(res1)
+ hash2 = hash(res2)
+ self.assertNotEqual(res1, res2)
+ self.assertNotEqual(hash1, hash2)
+
+ def test_param_dupe(self):
+ params1 = {
+ # 'from_files': {},
+ 'tags': [],
+ u'testvalue': False,
+ u'testvalue2': True,
+ # 'when': []
+ }
+ params2 = {
+ # 'from_files': {},
+ 'tags': [],
+ u'testvalue': True,
+ u'testvalue2': False,
+ # 'when': []
+ }
+ res1 = hash_params(params1)
+ res2 = hash_params(params2)
+
+ self.assertNotEqual(hash(res1), hash(res2))
+ self.assertNotEqual(res1, res2)
+
+ foo = {}
+ foo[res1] = 'params1'
+ foo[res2] = 'params2'
+
+ self.assertEqual(len(foo), 2)
+
+ del foo[res2]
+ self.assertEqual(len(foo), 1)
+
+ for key in foo:
+ self.assertTrue(key in foo)
+ self.assertIn(key, foo)
+
+
+class TestRole(unittest.TestCase):
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_load_role_with_tasks(self):
+
+ fake_loader = DictDataLoader({
+ "/etc/ansible/roles/foo_tasks/tasks/main.yml": """
+ - shell: echo 'hello world'
+ """,
+ })
+
+ mock_play = MagicMock()
+ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_tasks', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+
+ self.assertEqual(str(r), 'foo_tasks')
+ self.assertEqual(len(r._task_blocks), 1)
+ assert isinstance(r._task_blocks[0], Block)
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_load_role_with_tasks_dir_vs_file(self):
+
+ fake_loader = DictDataLoader({
+ "/etc/ansible/roles/foo_tasks/tasks/custom_main/foo.yml": """
+ - command: bar
+ """,
+ "/etc/ansible/roles/foo_tasks/tasks/custom_main.yml": """
+ - command: baz
+ """,
+ })
+
+ mock_play = MagicMock()
+ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_tasks', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play, from_files=dict(tasks='custom_main'))
+
+ self.assertEqual(r._task_blocks[0]._ds[0]['command'], 'baz')
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_load_role_with_handlers(self):
+
+ fake_loader = DictDataLoader({
+ "/etc/ansible/roles/foo_handlers/handlers/main.yml": """
+ - name: test handler
+ shell: echo 'hello world'
+ """,
+ })
+
+ mock_play = MagicMock()
+ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_handlers', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+
+ self.assertEqual(len(r._handler_blocks), 1)
+ assert isinstance(r._handler_blocks[0], Block)
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_load_role_with_vars(self):
+
+ fake_loader = DictDataLoader({
+ "/etc/ansible/roles/foo_vars/defaults/main.yml": """
+ foo: bar
+ """,
+ "/etc/ansible/roles/foo_vars/vars/main.yml": """
+ foo: bam
+ """,
+ })
+
+ mock_play = MagicMock()
+ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+
+ self.assertEqual(r._default_vars, dict(foo='bar'))
+ self.assertEqual(r._role_vars, dict(foo='bam'))
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_load_role_with_vars_dirs(self):
+
+ fake_loader = DictDataLoader({
+ "/etc/ansible/roles/foo_vars/defaults/main/foo.yml": """
+ foo: bar
+ """,
+ "/etc/ansible/roles/foo_vars/vars/main/bar.yml": """
+ foo: bam
+ """,
+ })
+
+ mock_play = MagicMock()
+ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+
+ self.assertEqual(r._default_vars, dict(foo='bar'))
+ self.assertEqual(r._role_vars, dict(foo='bam'))
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_load_role_with_vars_nested_dirs(self):
+
+ fake_loader = DictDataLoader({
+ "/etc/ansible/roles/foo_vars/defaults/main/foo/bar.yml": """
+ foo: bar
+ """,
+ "/etc/ansible/roles/foo_vars/vars/main/bar/foo.yml": """
+ foo: bam
+ """,
+ })
+
+ mock_play = MagicMock()
+ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+
+ self.assertEqual(r._default_vars, dict(foo='bar'))
+ self.assertEqual(r._role_vars, dict(foo='bam'))
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_load_role_with_vars_nested_dirs_combined(self):
+
+ fake_loader = DictDataLoader({
+ "/etc/ansible/roles/foo_vars/defaults/main/foo/bar.yml": """
+ foo: bar
+ a: 1
+ """,
+ "/etc/ansible/roles/foo_vars/defaults/main/bar/foo.yml": """
+ foo: bam
+ b: 2
+ """,
+ })
+
+ mock_play = MagicMock()
+ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+
+ self.assertEqual(r._default_vars, dict(foo='bar', a=1, b=2))
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_load_role_with_vars_dir_vs_file(self):
+
+ fake_loader = DictDataLoader({
+ "/etc/ansible/roles/foo_vars/vars/main/foo.yml": """
+ foo: bar
+ """,
+ "/etc/ansible/roles/foo_vars/vars/main.yml": """
+ foo: bam
+ """,
+ })
+
+ mock_play = MagicMock()
+ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_vars', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+
+ self.assertEqual(r._role_vars, dict(foo='bam'))
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_load_role_with_metadata(self):
+
+ fake_loader = DictDataLoader({
+ '/etc/ansible/roles/foo_metadata/meta/main.yml': """
+ allow_duplicates: true
+ dependencies:
+ - bar_metadata
+ galaxy_info:
+ a: 1
+ b: 2
+ c: 3
+ """,
+ '/etc/ansible/roles/bar_metadata/meta/main.yml': """
+ dependencies:
+ - baz_metadata
+ """,
+ '/etc/ansible/roles/baz_metadata/meta/main.yml': """
+ dependencies:
+ - bam_metadata
+ """,
+ '/etc/ansible/roles/bam_metadata/meta/main.yml': """
+ dependencies: []
+ """,
+ '/etc/ansible/roles/bad1_metadata/meta/main.yml': """
+ 1
+ """,
+ '/etc/ansible/roles/bad2_metadata/meta/main.yml': """
+ foo: bar
+ """,
+ '/etc/ansible/roles/recursive1_metadata/meta/main.yml': """
+ dependencies: ['recursive2_metadata']
+ """,
+ '/etc/ansible/roles/recursive2_metadata/meta/main.yml': """
+ dependencies: ['recursive1_metadata']
+ """,
+ })
+
+ mock_play = MagicMock()
+ mock_play.collections = None
+ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load('foo_metadata', play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+
+ role_deps = r.get_direct_dependencies()
+
+ self.assertEqual(len(role_deps), 1)
+ self.assertEqual(type(role_deps[0]), Role)
+ self.assertEqual(len(role_deps[0].get_parents()), 1)
+ self.assertEqual(role_deps[0].get_parents()[0], r)
+ self.assertEqual(r._metadata.allow_duplicates, True)
+ self.assertEqual(r._metadata.galaxy_info, dict(a=1, b=2, c=3))
+
+ all_deps = r.get_all_dependencies()
+ self.assertEqual(len(all_deps), 3)
+ self.assertEqual(all_deps[0].get_name(), 'bam_metadata')
+ self.assertEqual(all_deps[1].get_name(), 'baz_metadata')
+ self.assertEqual(all_deps[2].get_name(), 'bar_metadata')
+
+ i = RoleInclude.load('bad1_metadata', play=mock_play, loader=fake_loader)
+ self.assertRaises(AnsibleParserError, Role.load, i, play=mock_play)
+
+ i = RoleInclude.load('bad2_metadata', play=mock_play, loader=fake_loader)
+ self.assertRaises(AnsibleParserError, Role.load, i, play=mock_play)
+
+ # TODO: re-enable this test once Ansible has proper role dep cycle detection
+ # that doesn't rely on stack overflows being recoverable (as they aren't in Py3.7+)
+ # see https://github.com/ansible/ansible/issues/61527
+ # i = RoleInclude.load('recursive1_metadata', play=mock_play, loader=fake_loader)
+ # self.assertRaises(AnsibleError, Role.load, i, play=mock_play)
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_load_role_complex(self):
+
+ # FIXME: add tests for the more complex uses of
+ # params and tags/when statements
+
+ fake_loader = DictDataLoader({
+ "/etc/ansible/roles/foo_complex/tasks/main.yml": """
+ - shell: echo 'hello world'
+ """,
+ })
+
+ mock_play = MagicMock()
+ mock_play.ROLE_CACHE = {}
+
+ i = RoleInclude.load(dict(role='foo_complex'), play=mock_play, loader=fake_loader)
+ r = Role.load(i, play=mock_play)
+
+ self.assertEqual(r.get_name(), "foo_complex")
diff --git a/test/units/playbook/test_attribute.py b/test/units/playbook/test_attribute.py
new file mode 100644
index 0000000..bdb37c1
--- /dev/null
+++ b/test/units/playbook/test_attribute.py
@@ -0,0 +1,57 @@
+# (c) 2015, Marius Gedminas <marius@gedmin.as>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from ansible.playbook.attribute import Attribute
+
+
+class TestAttribute(unittest.TestCase):
+
+ def setUp(self):
+ self.one = Attribute(priority=100)
+ self.two = Attribute(priority=0)
+
+ def test_eq(self):
+ self.assertTrue(self.one == self.one)
+ self.assertFalse(self.one == self.two)
+
+ def test_ne(self):
+ self.assertFalse(self.one != self.one)
+ self.assertTrue(self.one != self.two)
+
+ def test_lt(self):
+ self.assertFalse(self.one < self.one)
+ self.assertTrue(self.one < self.two)
+ self.assertFalse(self.two < self.one)
+
+ def test_gt(self):
+ self.assertFalse(self.one > self.one)
+ self.assertFalse(self.one > self.two)
+ self.assertTrue(self.two > self.one)
+
+ def test_le(self):
+ self.assertTrue(self.one <= self.one)
+ self.assertTrue(self.one <= self.two)
+ self.assertFalse(self.two <= self.one)
+
+ def test_ge(self):
+ self.assertTrue(self.one >= self.one)
+ self.assertFalse(self.one >= self.two)
+ self.assertTrue(self.two >= self.one)
diff --git a/test/units/playbook/test_base.py b/test/units/playbook/test_base.py
new file mode 100644
index 0000000..d5810e7
--- /dev/null
+++ b/test/units/playbook/test_base.py
@@ -0,0 +1,615 @@
+# (c) 2016, Adrian Likins <alikins@redhat.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+
+from ansible.errors import AnsibleParserError
+from ansible.module_utils.six import string_types
+from ansible.playbook.attribute import FieldAttribute, NonInheritableFieldAttribute
+from ansible.template import Templar
+from ansible.playbook import base
+from ansible.utils.unsafe_proxy import AnsibleUnsafeBytes, AnsibleUnsafeText
+from ansible.utils.sentinel import Sentinel
+
+from units.mock.loader import DictDataLoader
+
+
+class TestBase(unittest.TestCase):
+ ClassUnderTest = base.Base
+
+ def setUp(self):
+ self.assorted_vars = {'var_2_key': 'var_2_value',
+ 'var_1_key': 'var_1_value',
+ 'a_list': ['a_list_1', 'a_list_2'],
+ 'a_dict': {'a_dict_key': 'a_dict_value'},
+ 'a_set': set(['set_1', 'set_2']),
+ 'a_int': 42,
+ 'a_float': 37.371,
+ 'a_bool': True,
+ 'a_none': None,
+ }
+ self.b = self.ClassUnderTest()
+
+ def _base_validate(self, ds):
+ bsc = self.ClassUnderTest()
+ parent = ExampleParentBaseSubClass()
+ bsc._parent = parent
+ bsc._dep_chain = [parent]
+ parent._dep_chain = None
+ bsc.load_data(ds)
+ fake_loader = DictDataLoader({})
+ templar = Templar(loader=fake_loader)
+ bsc.post_validate(templar)
+ return bsc
+
+ def test(self):
+ self.assertIsInstance(self.b, base.Base)
+ self.assertIsInstance(self.b, self.ClassUnderTest)
+
+ # dump me doesnt return anything or change anything so not much to assert
+ def test_dump_me_empty(self):
+ self.b.dump_me()
+
+ def test_dump_me(self):
+ ds = {'environment': [],
+ 'vars': {'var_2_key': 'var_2_value',
+ 'var_1_key': 'var_1_value'}}
+ b = self._base_validate(ds)
+ b.dump_me()
+
+ def _assert_copy(self, orig, copy):
+ self.assertIsInstance(copy, self.ClassUnderTest)
+ self.assertIsInstance(copy, base.Base)
+ self.assertEqual(len(orig.fattributes), len(copy.fattributes))
+
+ sentinel = 'Empty DS'
+ self.assertEqual(getattr(orig, '_ds', sentinel), getattr(copy, '_ds', sentinel))
+
+ def test_copy_empty(self):
+ copy = self.b.copy()
+ self._assert_copy(self.b, copy)
+
+ def test_copy_with_vars(self):
+ ds = {'vars': self.assorted_vars}
+ b = self._base_validate(ds)
+
+ copy = b.copy()
+ self._assert_copy(b, copy)
+
+ def test_serialize(self):
+ ds = {}
+ ds = {'environment': [],
+ 'vars': self.assorted_vars
+ }
+ b = self._base_validate(ds)
+ ret = b.serialize()
+ self.assertIsInstance(ret, dict)
+
+ def test_deserialize(self):
+ data = {}
+
+ d = self.ClassUnderTest()
+ d.deserialize(data)
+ self.assertIn('_run_once', d.__dict__)
+ self.assertIn('_check_mode', d.__dict__)
+
+ data = {'no_log': False,
+ 'remote_user': None,
+ 'vars': self.assorted_vars,
+ 'environment': [],
+ 'run_once': False,
+ 'connection': None,
+ 'ignore_errors': False,
+ 'port': 22,
+ 'a_sentinel_with_an_unlikely_name': ['sure, a list']}
+
+ d = self.ClassUnderTest()
+ d.deserialize(data)
+ self.assertNotIn('_a_sentinel_with_an_unlikely_name', d.__dict__)
+ self.assertIn('_run_once', d.__dict__)
+ self.assertIn('_check_mode', d.__dict__)
+
+ def test_serialize_then_deserialize(self):
+ ds = {'environment': [],
+ 'vars': self.assorted_vars}
+ b = self._base_validate(ds)
+ copy = b.copy()
+ ret = b.serialize()
+ b.deserialize(ret)
+ c = self.ClassUnderTest()
+ c.deserialize(ret)
+ # TODO: not a great test, but coverage...
+ self.maxDiff = None
+ self.assertDictEqual(b.serialize(), copy.serialize())
+ self.assertDictEqual(c.serialize(), copy.serialize())
+
+ def test_post_validate_empty(self):
+ fake_loader = DictDataLoader({})
+ templar = Templar(loader=fake_loader)
+ ret = self.b.post_validate(templar)
+ self.assertIsNone(ret)
+
+ def test_get_ds_none(self):
+ ds = self.b.get_ds()
+ self.assertIsNone(ds)
+
+ def test_load_data_ds_is_none(self):
+ self.assertRaises(AssertionError, self.b.load_data, None)
+
+ def test_load_data_invalid_attr(self):
+ ds = {'not_a_valid_attr': [],
+ 'other': None}
+
+ self.assertRaises(AnsibleParserError, self.b.load_data, ds)
+
+ def test_load_data_invalid_attr_type(self):
+ ds = {'environment': True}
+
+ # environment is supposed to be a list. This
+ # seems like it shouldn't work?
+ ret = self.b.load_data(ds)
+ self.assertEqual(True, ret._environment)
+
+ def test_post_validate(self):
+ ds = {'environment': [],
+ 'port': 443}
+ b = self._base_validate(ds)
+ self.assertEqual(b.port, 443)
+ self.assertEqual(b.environment, [])
+
+ def test_post_validate_invalid_attr_types(self):
+ ds = {'environment': [],
+ 'port': 'some_port'}
+ b = self._base_validate(ds)
+ self.assertEqual(b.port, 'some_port')
+
+ def test_squash(self):
+ data = self.b.serialize()
+ self.b.squash()
+ squashed_data = self.b.serialize()
+ # TODO: assert something
+ self.assertFalse(data['squashed'])
+ self.assertTrue(squashed_data['squashed'])
+
+ def test_vars(self):
+ # vars as a dict.
+ ds = {'environment': [],
+ 'vars': {'var_2_key': 'var_2_value',
+ 'var_1_key': 'var_1_value'}}
+ b = self._base_validate(ds)
+ self.assertEqual(b.vars['var_1_key'], 'var_1_value')
+
+ def test_vars_list_of_dicts(self):
+ ds = {'environment': [],
+ 'vars': [{'var_2_key': 'var_2_value'},
+ {'var_1_key': 'var_1_value'}]
+ }
+ b = self._base_validate(ds)
+ self.assertEqual(b.vars['var_1_key'], 'var_1_value')
+
+ def test_vars_not_dict_or_list(self):
+ ds = {'environment': [],
+ 'vars': 'I am a string, not a dict or a list of dicts'}
+ self.assertRaises(AnsibleParserError, self.b.load_data, ds)
+
+ def test_vars_not_valid_identifier(self):
+ ds = {'environment': [],
+ 'vars': [{'var_2_key': 'var_2_value'},
+ {'1an-invalid identifer': 'var_1_value'}]
+ }
+ self.assertRaises(AnsibleParserError, self.b.load_data, ds)
+
+ def test_vars_is_list_but_not_of_dicts(self):
+ ds = {'environment': [],
+ 'vars': ['foo', 'bar', 'this is a string not a dict']
+ }
+ self.assertRaises(AnsibleParserError, self.b.load_data, ds)
+
+ def test_vars_is_none(self):
+ # If vars is None, we should get a empty dict back
+ ds = {'environment': [],
+ 'vars': None
+ }
+ b = self._base_validate(ds)
+ self.assertEqual(b.vars, {})
+
+ def test_validate_empty(self):
+ self.b.validate()
+ self.assertTrue(self.b._validated)
+
+ def test_getters(self):
+ # not sure why these exist, but here are tests anyway
+ loader = self.b.get_loader()
+ variable_manager = self.b.get_variable_manager()
+ self.assertEqual(loader, self.b._loader)
+ self.assertEqual(variable_manager, self.b._variable_manager)
+
+
+class TestExtendValue(unittest.TestCase):
+ # _extend_value could be a module or staticmethod but since its
+ # not, the test is here.
+ def test_extend_value_list_newlist(self):
+ b = base.Base()
+ value_list = ['first', 'second']
+ new_value_list = ['new_first', 'new_second']
+ ret = b._extend_value(value_list, new_value_list)
+ self.assertEqual(value_list + new_value_list, ret)
+
+ def test_extend_value_list_newlist_prepend(self):
+ b = base.Base()
+ value_list = ['first', 'second']
+ new_value_list = ['new_first', 'new_second']
+ ret_prepend = b._extend_value(value_list, new_value_list, prepend=True)
+ self.assertEqual(new_value_list + value_list, ret_prepend)
+
+ def test_extend_value_newlist_list(self):
+ b = base.Base()
+ value_list = ['first', 'second']
+ new_value_list = ['new_first', 'new_second']
+ ret = b._extend_value(new_value_list, value_list)
+ self.assertEqual(new_value_list + value_list, ret)
+
+ def test_extend_value_newlist_list_prepend(self):
+ b = base.Base()
+ value_list = ['first', 'second']
+ new_value_list = ['new_first', 'new_second']
+ ret = b._extend_value(new_value_list, value_list, prepend=True)
+ self.assertEqual(value_list + new_value_list, ret)
+
+ def test_extend_value_string_newlist(self):
+ b = base.Base()
+ some_string = 'some string'
+ new_value_list = ['new_first', 'new_second']
+ ret = b._extend_value(some_string, new_value_list)
+ self.assertEqual([some_string] + new_value_list, ret)
+
+ def test_extend_value_string_newstring(self):
+ b = base.Base()
+ some_string = 'some string'
+ new_value_string = 'this is the new values'
+ ret = b._extend_value(some_string, new_value_string)
+ self.assertEqual([some_string, new_value_string], ret)
+
+ def test_extend_value_list_newstring(self):
+ b = base.Base()
+ value_list = ['first', 'second']
+ new_value_string = 'this is the new values'
+ ret = b._extend_value(value_list, new_value_string)
+ self.assertEqual(value_list + [new_value_string], ret)
+
+ def test_extend_value_none_none(self):
+ b = base.Base()
+ ret = b._extend_value(None, None)
+ self.assertEqual(len(ret), 0)
+ self.assertFalse(ret)
+
+ def test_extend_value_none_list(self):
+ b = base.Base()
+ ret = b._extend_value(None, ['foo'])
+ self.assertEqual(ret, ['foo'])
+
+
+class ExampleException(Exception):
+ pass
+
+
+# naming fails me...
+class ExampleParentBaseSubClass(base.Base):
+ test_attr_parent_string = FieldAttribute(isa='string', default='A string attr for a class that may be a parent for testing')
+
+ def __init__(self):
+
+ super(ExampleParentBaseSubClass, self).__init__()
+ self._dep_chain = None
+
+ def get_dep_chain(self):
+ return self._dep_chain
+
+
+class ExampleSubClass(base.Base):
+ test_attr_blip = NonInheritableFieldAttribute(isa='string', default='example sub class test_attr_blip',
+ always_post_validate=True)
+
+ def __init__(self):
+ super(ExampleSubClass, self).__init__()
+
+ def get_dep_chain(self):
+ if self._parent:
+ return self._parent.get_dep_chain()
+ else:
+ return None
+
+
+class BaseSubClass(base.Base):
+ name = FieldAttribute(isa='string', default='', always_post_validate=True)
+ test_attr_bool = FieldAttribute(isa='bool', always_post_validate=True)
+ test_attr_int = FieldAttribute(isa='int', always_post_validate=True)
+ test_attr_float = FieldAttribute(isa='float', default=3.14159, always_post_validate=True)
+ test_attr_list = FieldAttribute(isa='list', listof=string_types, always_post_validate=True)
+ test_attr_list_no_listof = FieldAttribute(isa='list', always_post_validate=True)
+ test_attr_list_required = FieldAttribute(isa='list', listof=string_types, required=True,
+ default=list, always_post_validate=True)
+ test_attr_string = FieldAttribute(isa='string', default='the_test_attr_string_default_value')
+ test_attr_string_required = FieldAttribute(isa='string', required=True,
+ default='the_test_attr_string_default_value')
+ test_attr_percent = FieldAttribute(isa='percent', always_post_validate=True)
+ test_attr_set = FieldAttribute(isa='set', default=set, always_post_validate=True)
+ test_attr_dict = FieldAttribute(isa='dict', default=lambda: {'a_key': 'a_value'}, always_post_validate=True)
+ test_attr_class = FieldAttribute(isa='class', class_type=ExampleSubClass)
+ test_attr_class_post_validate = FieldAttribute(isa='class', class_type=ExampleSubClass,
+ always_post_validate=True)
+ test_attr_unknown_isa = FieldAttribute(isa='not_a_real_isa', always_post_validate=True)
+ test_attr_example = FieldAttribute(isa='string', default='the_default',
+ always_post_validate=True)
+ test_attr_none = FieldAttribute(isa='string', always_post_validate=True)
+ test_attr_preprocess = FieldAttribute(isa='string', default='the default for preprocess')
+ test_attr_method = FieldAttribute(isa='string', default='some attr with a getter',
+ always_post_validate=True)
+ test_attr_method_missing = FieldAttribute(isa='string', default='some attr with a missing getter',
+ always_post_validate=True)
+
+ def _get_attr_test_attr_method(self):
+ return 'foo bar'
+
+ def _validate_test_attr_example(self, attr, name, value):
+ if not isinstance(value, str):
+ raise ExampleException('test_attr_example is not a string: %s type=%s' % (value, type(value)))
+
+ def _post_validate_test_attr_example(self, attr, value, templar):
+ after_template_value = templar.template(value)
+ return after_template_value
+
+ def _post_validate_test_attr_none(self, attr, value, templar):
+ return None
+
+
+# terrible name, but it is a TestBase subclass for testing subclasses of Base
+class TestBaseSubClass(TestBase):
+ ClassUnderTest = BaseSubClass
+
+ def _base_validate(self, ds):
+ ds['test_attr_list_required'] = []
+ return super(TestBaseSubClass, self)._base_validate(ds)
+
+ def test_attr_bool(self):
+ ds = {'test_attr_bool': True}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_bool, True)
+
+ def test_attr_int(self):
+ MOST_RANDOM_NUMBER = 37
+ ds = {'test_attr_int': MOST_RANDOM_NUMBER}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_int, MOST_RANDOM_NUMBER)
+
+ def test_attr_int_del(self):
+ MOST_RANDOM_NUMBER = 37
+ ds = {'test_attr_int': MOST_RANDOM_NUMBER}
+ bsc = self._base_validate(ds)
+ del bsc.test_attr_int
+ self.assertNotIn('_test_attr_int', bsc.__dict__)
+
+ def test_attr_float(self):
+ roughly_pi = 4.0
+ ds = {'test_attr_float': roughly_pi}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_float, roughly_pi)
+
+ def test_attr_percent(self):
+ percentage = '90%'
+ percentage_float = 90.0
+ ds = {'test_attr_percent': percentage}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_percent, percentage_float)
+
+ # This method works hard and gives it its all and everything it's got. It doesn't
+ # leave anything on the field. It deserves to pass. It has earned it.
+ def test_attr_percent_110_percent(self):
+ percentage = '110.11%'
+ percentage_float = 110.11
+ ds = {'test_attr_percent': percentage}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_percent, percentage_float)
+
+ # This method is just here for the paycheck.
+ def test_attr_percent_60_no_percent_sign(self):
+ percentage = '60'
+ percentage_float = 60.0
+ ds = {'test_attr_percent': percentage}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_percent, percentage_float)
+
+ def test_attr_set(self):
+ test_set = set(['first_string_in_set', 'second_string_in_set'])
+ ds = {'test_attr_set': test_set}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_set, test_set)
+
+ def test_attr_set_string(self):
+ test_data = ['something', 'other']
+ test_value = ','.join(test_data)
+ ds = {'test_attr_set': test_value}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_set, set(test_data))
+
+ def test_attr_set_not_string_or_list(self):
+ test_value = 37.1
+ ds = {'test_attr_set': test_value}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_set, set([test_value]))
+
+ def test_attr_dict(self):
+ test_dict = {'a_different_key': 'a_different_value'}
+ ds = {'test_attr_dict': test_dict}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_dict, test_dict)
+
+ def test_attr_dict_string(self):
+ test_value = 'just_some_random_string'
+ ds = {'test_attr_dict': test_value}
+ self.assertRaisesRegex(AnsibleParserError, 'is not a dictionary', self._base_validate, ds)
+
+ def test_attr_class(self):
+ esc = ExampleSubClass()
+ ds = {'test_attr_class': esc}
+ bsc = self._base_validate(ds)
+ self.assertIs(bsc.test_attr_class, esc)
+
+ def test_attr_class_wrong_type(self):
+ not_a_esc = ExampleSubClass
+ ds = {'test_attr_class': not_a_esc}
+ bsc = self._base_validate(ds)
+ self.assertIs(bsc.test_attr_class, not_a_esc)
+
+ def test_attr_class_post_validate(self):
+ esc = ExampleSubClass()
+ ds = {'test_attr_class_post_validate': esc}
+ bsc = self._base_validate(ds)
+ self.assertIs(bsc.test_attr_class_post_validate, esc)
+
+ def test_attr_class_post_validate_class_not_instance(self):
+ not_a_esc = ExampleSubClass
+ ds = {'test_attr_class_post_validate': not_a_esc}
+ self.assertRaisesRegex(AnsibleParserError, "is not a valid.*got a <class 'type'> instead",
+ self._base_validate, ds)
+
+ def test_attr_class_post_validate_wrong_class(self):
+ not_a_esc = 37
+ ds = {'test_attr_class_post_validate': not_a_esc}
+ self.assertRaisesRegex(AnsibleParserError, 'is not a valid.*got a.*int.*instead',
+ self._base_validate, ds)
+
+ def test_attr_remote_user(self):
+ ds = {'remote_user': 'testuser'}
+ bsc = self._base_validate(ds)
+ # TODO: attemp to verify we called parent gettters etc
+ self.assertEqual(bsc.remote_user, 'testuser')
+
+ def test_attr_example_undefined(self):
+ ds = {'test_attr_example': '{{ some_var_that_shouldnt_exist_to_test_omit }}'}
+ exc_regex_str = 'test_attr_example.*has an invalid value, which includes an undefined variable.*some_var_that_shouldnt*'
+ self.assertRaises(AnsibleParserError)
+
+ def test_attr_name_undefined(self):
+ ds = {'name': '{{ some_var_that_shouldnt_exist_to_test_omit }}'}
+ bsc = self._base_validate(ds)
+ # the attribute 'name' is special cases in post_validate
+ self.assertEqual(bsc.name, '{{ some_var_that_shouldnt_exist_to_test_omit }}')
+
+ def test_subclass_validate_method(self):
+ ds = {'test_attr_list': ['string_list_item_1', 'string_list_item_2'],
+ 'test_attr_example': 'the_test_attr_example_value_string'}
+ # Not throwing an exception here is the test
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_example, 'the_test_attr_example_value_string')
+
+ def test_subclass_validate_method_invalid(self):
+ ds = {'test_attr_example': [None]}
+ self.assertRaises(ExampleException, self._base_validate, ds)
+
+ def test_attr_none(self):
+ ds = {'test_attr_none': 'foo'}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_none, None)
+
+ def test_attr_string(self):
+ the_string_value = "the new test_attr_string_value"
+ ds = {'test_attr_string': the_string_value}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_string, the_string_value)
+
+ def test_attr_string_invalid_list(self):
+ ds = {'test_attr_string': ['The new test_attr_string', 'value, however in a list']}
+ self.assertRaises(AnsibleParserError, self._base_validate, ds)
+
+ def test_attr_string_required(self):
+ the_string_value = "the new test_attr_string_required_value"
+ ds = {'test_attr_string_required': the_string_value}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_string_required, the_string_value)
+
+ def test_attr_list_invalid(self):
+ ds = {'test_attr_list': {}}
+ self.assertRaises(AnsibleParserError, self._base_validate, ds)
+
+ def test_attr_list(self):
+ string_list = ['foo', 'bar']
+ ds = {'test_attr_list': string_list}
+ bsc = self._base_validate(ds)
+ self.assertEqual(string_list, bsc._test_attr_list)
+
+ def test_attr_list_none(self):
+ ds = {'test_attr_list': None}
+ bsc = self._base_validate(ds)
+ self.assertEqual(None, bsc._test_attr_list)
+
+ def test_attr_list_no_listof(self):
+ test_list = ['foo', 'bar', 123]
+ ds = {'test_attr_list_no_listof': test_list}
+ bsc = self._base_validate(ds)
+ self.assertEqual(test_list, bsc._test_attr_list_no_listof)
+
+ def test_attr_list_required(self):
+ string_list = ['foo', 'bar']
+ ds = {'test_attr_list_required': string_list}
+ bsc = self.ClassUnderTest()
+ bsc.load_data(ds)
+ fake_loader = DictDataLoader({})
+ templar = Templar(loader=fake_loader)
+ bsc.post_validate(templar)
+ self.assertEqual(string_list, bsc._test_attr_list_required)
+
+ def test_attr_list_required_empty_string(self):
+ string_list = [""]
+ ds = {'test_attr_list_required': string_list}
+ bsc = self.ClassUnderTest()
+ bsc.load_data(ds)
+ fake_loader = DictDataLoader({})
+ templar = Templar(loader=fake_loader)
+ self.assertRaisesRegex(AnsibleParserError, 'cannot have empty values',
+ bsc.post_validate, templar)
+
+ def test_attr_unknown(self):
+ a_list = ['some string']
+ ds = {'test_attr_unknown_isa': a_list}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_unknown_isa, a_list)
+
+ def test_attr_method(self):
+ ds = {'test_attr_method': 'value from the ds'}
+ bsc = self._base_validate(ds)
+ # The value returned by the subclasses _get_attr_test_attr_method
+ self.assertEqual(bsc.test_attr_method, 'foo bar')
+
+ def test_attr_method_missing(self):
+ a_string = 'The value set from the ds'
+ ds = {'test_attr_method_missing': a_string}
+ bsc = self._base_validate(ds)
+ self.assertEqual(bsc.test_attr_method_missing, a_string)
+
+ def test_get_validated_value_string_rewrap_unsafe(self):
+ attribute = FieldAttribute(isa='string')
+ value = AnsibleUnsafeText(u'bar')
+ templar = Templar(None)
+ bsc = self.ClassUnderTest()
+ result = bsc.get_validated_value('foo', attribute, value, templar)
+ self.assertIsInstance(result, AnsibleUnsafeText)
+ self.assertEqual(result, AnsibleUnsafeText(u'bar'))
diff --git a/test/units/playbook/test_block.py b/test/units/playbook/test_block.py
new file mode 100644
index 0000000..4847123
--- /dev/null
+++ b/test/units/playbook/test_block.py
@@ -0,0 +1,82 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from ansible.playbook.block import Block
+from ansible.playbook.task import Task
+
+
+class TestBlock(unittest.TestCase):
+
+ def test_construct_empty_block(self):
+ b = Block()
+
+ def test_construct_block_with_role(self):
+ pass
+
+ def test_load_block_simple(self):
+ ds = dict(
+ block=[],
+ rescue=[],
+ always=[],
+ # otherwise=[],
+ )
+ b = Block.load(ds)
+ self.assertEqual(b.block, [])
+ self.assertEqual(b.rescue, [])
+ self.assertEqual(b.always, [])
+ # not currently used
+ # self.assertEqual(b.otherwise, [])
+
+ def test_load_block_with_tasks(self):
+ ds = dict(
+ block=[dict(action='block')],
+ rescue=[dict(action='rescue')],
+ always=[dict(action='always')],
+ # otherwise=[dict(action='otherwise')],
+ )
+ b = Block.load(ds)
+ self.assertEqual(len(b.block), 1)
+ self.assertIsInstance(b.block[0], Task)
+ self.assertEqual(len(b.rescue), 1)
+ self.assertIsInstance(b.rescue[0], Task)
+ self.assertEqual(len(b.always), 1)
+ self.assertIsInstance(b.always[0], Task)
+ # not currently used
+ # self.assertEqual(len(b.otherwise), 1)
+ # self.assertIsInstance(b.otherwise[0], Task)
+
+ def test_load_implicit_block(self):
+ ds = [dict(action='foo')]
+ b = Block.load(ds)
+ self.assertEqual(len(b.block), 1)
+ self.assertIsInstance(b.block[0], Task)
+
+ def test_deserialize(self):
+ ds = dict(
+ block=[dict(action='block')],
+ rescue=[dict(action='rescue')],
+ always=[dict(action='always')],
+ )
+ b = Block.load(ds)
+ data = dict(parent=ds, parent_type='Block')
+ b.deserialize(data)
+ self.assertIsInstance(b._parent, Block)
diff --git a/test/units/playbook/test_collectionsearch.py b/test/units/playbook/test_collectionsearch.py
new file mode 100644
index 0000000..be40d85
--- /dev/null
+++ b/test/units/playbook/test_collectionsearch.py
@@ -0,0 +1,78 @@
+# (c) 2020 Ansible Project
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.errors import AnsibleParserError
+from ansible.playbook.play import Play
+from ansible.playbook.task import Task
+from ansible.playbook.block import Block
+from ansible.playbook.collectionsearch import CollectionSearch
+
+import pytest
+
+
+def test_collection_static_warning(capsys):
+ """Test that collection name is not templated.
+
+ Also, make sure that users see the warning message for the referenced name.
+ """
+ collection_name = "foo.{{bar}}"
+ p = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ connection='local',
+ collections=collection_name,
+ ))
+ assert collection_name in p.collections
+ std_out, std_err = capsys.readouterr()
+ assert '[WARNING]: "collections" is not templatable, but we found: %s' % collection_name in std_err
+ assert '' == std_out
+
+
+def test_collection_invalid_data_play():
+ """Test that collection as a dict at the play level fails with parser error"""
+ collection_name = {'name': 'foo'}
+ with pytest.raises(AnsibleParserError):
+ Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ connection='local',
+ collections=collection_name,
+ ))
+
+
+def test_collection_invalid_data_task():
+ """Test that collection as a dict at the task level fails with parser error"""
+ collection_name = {'name': 'foo'}
+ with pytest.raises(AnsibleParserError):
+ Task.load(dict(
+ name="test task",
+ collections=collection_name,
+ ))
+
+
+def test_collection_invalid_data_block():
+ """Test that collection as a dict at the block level fails with parser error"""
+ collection_name = {'name': 'foo'}
+ with pytest.raises(AnsibleParserError):
+ Block.load(dict(
+ block=[dict(name="test task", collections=collection_name)]
+ ))
diff --git a/test/units/playbook/test_conditional.py b/test/units/playbook/test_conditional.py
new file mode 100644
index 0000000..8231d16
--- /dev/null
+++ b/test/units/playbook/test_conditional.py
@@ -0,0 +1,212 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from units.mock.loader import DictDataLoader
+from unittest.mock import MagicMock
+
+from ansible.template import Templar
+from ansible import errors
+
+from ansible.playbook import conditional
+
+
+class TestConditional(unittest.TestCase):
+ def setUp(self):
+ self.loader = DictDataLoader({})
+ self.cond = conditional.Conditional(loader=self.loader)
+ self.templar = Templar(loader=self.loader, variables={})
+
+ def _eval_con(self, when=None, variables=None):
+ when = when or []
+ variables = variables or {}
+ self.cond.when = when
+ ret = self.cond.evaluate_conditional(self.templar, variables)
+ return ret
+
+ def test_false(self):
+ when = [u"False"]
+ ret = self._eval_con(when, {})
+ self.assertFalse(ret)
+
+ def test_true(self):
+ when = [u"True"]
+ ret = self._eval_con(when, {})
+ self.assertTrue(ret)
+
+ def test_true_boolean(self):
+ self.cond.when = [True]
+ m = MagicMock()
+ ret = self.cond.evaluate_conditional(m, {})
+ self.assertTrue(ret)
+ self.assertFalse(m.is_template.called)
+
+ def test_false_boolean(self):
+ self.cond.when = [False]
+ m = MagicMock()
+ ret = self.cond.evaluate_conditional(m, {})
+ self.assertFalse(ret)
+ self.assertFalse(m.is_template.called)
+
+ def test_undefined(self):
+ when = [u"{{ some_undefined_thing }}"]
+ self.assertRaisesRegex(errors.AnsibleError, "The conditional check '{{ some_undefined_thing }}' failed",
+ self._eval_con, when, {})
+
+ def test_defined(self):
+ variables = {'some_defined_thing': True}
+ when = [u"{{ some_defined_thing }}"]
+ ret = self._eval_con(when, variables)
+ self.assertTrue(ret)
+
+ def test_dict_defined_values(self):
+ variables = {'dict_value': 1,
+ 'some_defined_dict': {'key1': 'value1',
+ 'key2': '{{ dict_value }}'}}
+
+ when = [u"some_defined_dict"]
+ ret = self._eval_con(when, variables)
+ self.assertTrue(ret)
+
+ def test_dict_defined_values_is_defined(self):
+ variables = {'dict_value': 1,
+ 'some_defined_dict': {'key1': 'value1',
+ 'key2': '{{ dict_value }}'}}
+
+ when = [u"some_defined_dict.key1 is defined"]
+ ret = self._eval_con(when, variables)
+ self.assertTrue(ret)
+
+ def test_dict_defined_multiple_values_is_defined(self):
+ variables = {'dict_value': 1,
+ 'some_defined_dict': {'key1': 'value1',
+ 'key2': '{{ dict_value }}'}}
+
+ when = [u"some_defined_dict.key1 is defined",
+ u"some_defined_dict.key2 is not undefined"]
+ ret = self._eval_con(when, variables)
+ self.assertTrue(ret)
+
+ def test_nested_hostvars_undefined_values(self):
+ variables = {'dict_value': 1,
+ 'hostvars': {'host1': {'key1': 'value1',
+ 'key2': '{{ dict_value }}'},
+ 'host2': '{{ dict_value }}',
+ 'host3': '{{ undefined_dict_value }}',
+ # no host4
+ },
+ 'some_dict': {'some_dict_key1': '{{ hostvars["host3"] }}'}
+ }
+
+ when = [u"some_dict.some_dict_key1 == hostvars['host3']"]
+ # self._eval_con(when, variables)
+ self.assertRaisesRegex(errors.AnsibleError,
+ r"The conditional check 'some_dict.some_dict_key1 == hostvars\['host3'\]' failed",
+ # "The conditional check 'some_dict.some_dict_key1 == hostvars['host3']' failed",
+ # "The conditional check 'some_dict.some_dict_key1 == hostvars['host3']' failed.",
+ self._eval_con,
+ when, variables)
+
+ def test_dict_undefined_values_bare(self):
+ variables = {'dict_value': 1,
+ 'some_defined_dict_with_undefined_values': {'key1': 'value1',
+ 'key2': '{{ dict_value }}',
+ 'key3': '{{ undefined_dict_value }}'
+ }}
+
+ # raises an exception when a non-string conditional is passed to extract_defined_undefined()
+ when = [u"some_defined_dict_with_undefined_values"]
+ self.assertRaisesRegex(errors.AnsibleError,
+ "The conditional check 'some_defined_dict_with_undefined_values' failed.",
+ self._eval_con,
+ when, variables)
+
+ def test_is_defined(self):
+ variables = {'some_defined_thing': True}
+ when = [u"some_defined_thing is defined"]
+ ret = self._eval_con(when, variables)
+ self.assertTrue(ret)
+
+ def test_is_undefined(self):
+ variables = {'some_defined_thing': True}
+ when = [u"some_defined_thing is undefined"]
+ ret = self._eval_con(when, variables)
+ self.assertFalse(ret)
+
+ def test_is_undefined_and_defined(self):
+ variables = {'some_defined_thing': True}
+ when = [u"some_defined_thing is undefined", u"some_defined_thing is defined"]
+ ret = self._eval_con(when, variables)
+ self.assertFalse(ret)
+
+ def test_is_undefined_and_defined_reversed(self):
+ variables = {'some_defined_thing': True}
+ when = [u"some_defined_thing is defined", u"some_defined_thing is undefined"]
+ ret = self._eval_con(when, variables)
+ self.assertFalse(ret)
+
+ def test_is_not_undefined(self):
+ variables = {'some_defined_thing': True}
+ when = [u"some_defined_thing is not undefined"]
+ ret = self._eval_con(when, variables)
+ self.assertTrue(ret)
+
+ def test_is_not_defined(self):
+ variables = {'some_defined_thing': True}
+ when = [u"some_undefined_thing is not defined"]
+ ret = self._eval_con(when, variables)
+ self.assertTrue(ret)
+
+ def test_is_hostvars_quotes_is_defined(self):
+ variables = {'hostvars': {'some_host': {}},
+ 'compare_targets_single': "hostvars['some_host']",
+ 'compare_targets_double': 'hostvars["some_host"]',
+ 'compare_targets': {'double': '{{ compare_targets_double }}',
+ 'single': "{{ compare_targets_single }}"},
+ }
+ when = [u"hostvars['some_host'] is defined",
+ u'hostvars["some_host"] is defined',
+ u"{{ compare_targets.double }} is defined",
+ u"{{ compare_targets.single }} is defined"]
+ ret = self._eval_con(when, variables)
+ self.assertTrue(ret)
+
+ def test_is_hostvars_quotes_is_defined_but_is_not_defined(self):
+ variables = {'hostvars': {'some_host': {}},
+ 'compare_targets_single': "hostvars['some_host']",
+ 'compare_targets_double': 'hostvars["some_host"]',
+ 'compare_targets': {'double': '{{ compare_targets_double }}',
+ 'single': "{{ compare_targets_single }}"},
+ }
+ when = [u"hostvars['some_host'] is defined",
+ u'hostvars["some_host"] is defined',
+ u"{{ compare_targets.triple }} is defined",
+ u"{{ compare_targets.quadruple }} is defined"]
+ self.assertRaisesRegex(errors.AnsibleError,
+ "The conditional check '{{ compare_targets.triple }} is defined' failed",
+ self._eval_con,
+ when, variables)
+
+ def test_is_hostvars_host_is_defined(self):
+ variables = {'hostvars': {'some_host': {}, }}
+ when = [u"hostvars['some_host'] is defined"]
+ ret = self._eval_con(when, variables)
+ self.assertTrue(ret)
+
+ def test_is_hostvars_host_undefined_is_defined(self):
+ variables = {'hostvars': {'some_host': {}, }}
+ when = [u"hostvars['some_undefined_host'] is defined"]
+ ret = self._eval_con(when, variables)
+ self.assertFalse(ret)
+
+ def test_is_hostvars_host_undefined_is_undefined(self):
+ variables = {'hostvars': {'some_host': {}, }}
+ when = [u"hostvars['some_undefined_host'] is undefined"]
+ ret = self._eval_con(when, variables)
+ self.assertTrue(ret)
+
+ def test_is_hostvars_host_undefined_is_not_defined(self):
+ variables = {'hostvars': {'some_host': {}, }}
+ when = [u"hostvars['some_undefined_host'] is not defined"]
+ ret = self._eval_con(when, variables)
+ self.assertTrue(ret)
diff --git a/test/units/playbook/test_helpers.py b/test/units/playbook/test_helpers.py
new file mode 100644
index 0000000..a89730c
--- /dev/null
+++ b/test/units/playbook/test_helpers.py
@@ -0,0 +1,373 @@
+# (c) 2016, Adrian Likins <alikins@redhat.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+from units.compat import unittest
+from unittest.mock import MagicMock
+from units.mock.loader import DictDataLoader
+
+from ansible import errors
+from ansible.playbook.block import Block
+from ansible.playbook.handler import Handler
+from ansible.playbook.task import Task
+from ansible.playbook.task_include import TaskInclude
+from ansible.playbook.role.include import RoleInclude
+
+from ansible.playbook import helpers
+
+
+class MixinForMocks(object):
+ def _setup(self):
+ # This is not a very good mixin, lots of side effects
+ self.fake_loader = DictDataLoader({'include_test.yml': "",
+ 'other_include_test.yml': ""})
+ self.mock_tqm = MagicMock(name='MockTaskQueueManager')
+
+ self.mock_play = MagicMock(name='MockPlay')
+ self.mock_play._attributes = []
+ self.mock_play._collections = None
+
+ self.mock_iterator = MagicMock(name='MockIterator')
+ self.mock_iterator._play = self.mock_play
+
+ self.mock_inventory = MagicMock(name='MockInventory')
+ self.mock_inventory._hosts_cache = dict()
+
+ def _get_host(host_name):
+ return None
+
+ self.mock_inventory.get_host.side_effect = _get_host
+ # TODO: can we use a real VariableManager?
+ self.mock_variable_manager = MagicMock(name='MockVariableManager')
+ self.mock_variable_manager.get_vars.return_value = dict()
+
+ self.mock_block = MagicMock(name='MockBlock')
+
+ # On macOS /etc is actually /private/etc, tests fail when performing literal /etc checks
+ self.fake_role_loader = DictDataLoader({os.path.join(os.path.realpath("/etc"), "ansible/roles/bogus_role/tasks/main.yml"): """
+ - shell: echo 'hello world'
+ """})
+
+ self._test_data_path = os.path.dirname(__file__)
+ self.fake_include_loader = DictDataLoader({"/dev/null/includes/test_include.yml": """
+ - include: other_test_include.yml
+ - shell: echo 'hello world'
+ """,
+ "/dev/null/includes/static_test_include.yml": """
+ - include: other_test_include.yml
+ - shell: echo 'hello static world'
+ """,
+ "/dev/null/includes/other_test_include.yml": """
+ - debug:
+ msg: other_test_include_debug
+ """})
+
+
+class TestLoadListOfTasks(unittest.TestCase, MixinForMocks):
+ def setUp(self):
+ self._setup()
+
+ def _assert_is_task_list(self, results):
+ for result in results:
+ self.assertIsInstance(result, Task)
+
+ def _assert_is_task_list_or_blocks(self, results):
+ self.assertIsInstance(results, list)
+ for result in results:
+ self.assertIsInstance(result, (Task, Block))
+
+ def test_ds_not_list(self):
+ ds = {}
+ self.assertRaises(AssertionError, helpers.load_list_of_tasks,
+ ds, self.mock_play, block=None, role=None, task_include=None, use_handlers=False, variable_manager=None, loader=None)
+
+ def test_ds_not_dict(self):
+ ds = [[]]
+ self.assertRaises(AssertionError, helpers.load_list_of_tasks,
+ ds, self.mock_play, block=None, role=None, task_include=None, use_handlers=False, variable_manager=None, loader=None)
+
+ def test_empty_task(self):
+ ds = [{}]
+ self.assertRaisesRegex(errors.AnsibleParserError,
+ "no module/action detected in task",
+ helpers.load_list_of_tasks,
+ ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+
+ def test_empty_task_use_handlers(self):
+ ds = [{}]
+ self.assertRaisesRegex(errors.AnsibleParserError,
+ "no module/action detected in task.",
+ helpers.load_list_of_tasks,
+ ds,
+ use_handlers=True,
+ play=self.mock_play,
+ variable_manager=self.mock_variable_manager,
+ loader=self.fake_loader)
+
+ def test_one_bogus_block(self):
+ ds = [{'block': None}]
+ self.assertRaisesRegex(errors.AnsibleParserError,
+ "A malformed block was encountered",
+ helpers.load_list_of_tasks,
+ ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+
+ def test_unknown_action(self):
+ action_name = 'foo_test_unknown_action'
+ ds = [{'action': action_name}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+ self._assert_is_task_list_or_blocks(res)
+ self.assertEqual(res[0].action, action_name)
+
+ def test_block_unknown_action(self):
+ action_name = 'foo_test_block_unknown_action'
+ ds = [{
+ 'block': [{'action': action_name}]
+ }]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+ self._assert_is_task_list_or_blocks(res)
+ self.assertIsInstance(res[0], Block)
+ self._assert_default_block(res[0])
+
+ def _assert_default_block(self, block):
+ # the expected defaults
+ self.assertIsInstance(block.block, list)
+ self.assertEqual(len(block.block), 1)
+ self.assertIsInstance(block.rescue, list)
+ self.assertEqual(len(block.rescue), 0)
+ self.assertIsInstance(block.always, list)
+ self.assertEqual(len(block.always), 0)
+
+ def test_block_use_handlers(self):
+ ds = [{'block': True}]
+ self.assertRaisesRegex(errors.AnsibleParserError,
+ "Using a block as a handler is not supported.",
+ helpers.load_list_of_tasks,
+ ds, play=self.mock_play, use_handlers=True,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+
+ def test_one_bogus_include(self):
+ ds = [{'include': 'somefile.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+ self.assertIsInstance(res, list)
+ self.assertEqual(len(res), 0)
+
+ def test_one_bogus_include_use_handlers(self):
+ ds = [{'include': 'somefile.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play, use_handlers=True,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+ self.assertIsInstance(res, list)
+ self.assertEqual(len(res), 0)
+
+ def test_one_bogus_include_static(self):
+ ds = [{'import_tasks': 'somefile.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_loader)
+ self.assertIsInstance(res, list)
+ self.assertEqual(len(res), 0)
+
+ def test_one_include(self):
+ ds = [{'include': '/dev/null/includes/other_test_include.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self.assertEqual(len(res), 1)
+ self._assert_is_task_list_or_blocks(res)
+
+ def test_one_parent_include(self):
+ ds = [{'include': '/dev/null/includes/test_include.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self._assert_is_task_list_or_blocks(res)
+ self.assertIsInstance(res[0], Block)
+ self.assertIsInstance(res[0]._parent, TaskInclude)
+
+ # TODO/FIXME: do this non deprecated way
+ def test_one_include_tags(self):
+ ds = [{'include': '/dev/null/includes/other_test_include.yml',
+ 'tags': ['test_one_include_tags_tag1', 'and_another_tagB']
+ }]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self._assert_is_task_list_or_blocks(res)
+ self.assertIsInstance(res[0], Block)
+ self.assertIn('test_one_include_tags_tag1', res[0].tags)
+ self.assertIn('and_another_tagB', res[0].tags)
+
+ # TODO/FIXME: do this non deprecated way
+ def test_one_parent_include_tags(self):
+ ds = [{'include': '/dev/null/includes/test_include.yml',
+ # 'vars': {'tags': ['test_one_parent_include_tags_tag1', 'and_another_tag2']}
+ 'tags': ['test_one_parent_include_tags_tag1', 'and_another_tag2']
+ }
+ ]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self._assert_is_task_list_or_blocks(res)
+ self.assertIsInstance(res[0], Block)
+ self.assertIn('test_one_parent_include_tags_tag1', res[0].tags)
+ self.assertIn('and_another_tag2', res[0].tags)
+
+ def test_one_include_use_handlers(self):
+ ds = [{'include': '/dev/null/includes/other_test_include.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ use_handlers=True,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self._assert_is_task_list_or_blocks(res)
+ self.assertIsInstance(res[0], Handler)
+
+ def test_one_parent_include_use_handlers(self):
+ ds = [{'include': '/dev/null/includes/test_include.yml'}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ use_handlers=True,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self._assert_is_task_list_or_blocks(res)
+ self.assertIsInstance(res[0], Handler)
+
+ # default for Handler
+ self.assertEqual(res[0].listen, [])
+
+ # TODO/FIXME: this doesn't seen right
+ # figure out how to get the non-static errors to be raised, this seems to just ignore everything
+ def test_one_include_not_static(self):
+ ds = [{
+ 'include_tasks': '/dev/null/includes/static_test_include.yml',
+ }]
+ # a_block = Block()
+ ti_ds = {'include_tasks': '/dev/null/includes/ssdftatic_test_include.yml'}
+ a_task_include = TaskInclude()
+ ti = a_task_include.load(ti_ds)
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ block=ti,
+ variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+ self._assert_is_task_list_or_blocks(res)
+ self.assertIsInstance(res[0], Task)
+ self.assertEqual(res[0].args['_raw_params'], '/dev/null/includes/static_test_include.yml')
+
+ # TODO/FIXME: This two get stuck trying to make a mock_block into a TaskInclude
+# def test_one_include(self):
+# ds = [{'include': 'other_test_include.yml'}]
+# res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+# block=self.mock_block,
+# variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+# print(res)
+
+# def test_one_parent_include(self):
+# ds = [{'include': 'test_include.yml'}]
+# res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+# block=self.mock_block,
+# variable_manager=self.mock_variable_manager, loader=self.fake_include_loader)
+# print(res)
+
+ def test_one_bogus_include_role(self):
+ ds = [{'include_role': {'name': 'bogus_role'}, 'collections': []}]
+ res = helpers.load_list_of_tasks(ds, play=self.mock_play,
+ block=self.mock_block,
+ variable_manager=self.mock_variable_manager, loader=self.fake_role_loader)
+ self.assertEqual(len(res), 1)
+ self._assert_is_task_list_or_blocks(res)
+
+ def test_one_bogus_include_role_use_handlers(self):
+ ds = [{'include_role': {'name': 'bogus_role'}, 'collections': []}]
+
+ self.assertRaises(errors.AnsibleError, helpers.load_list_of_tasks,
+ ds,
+ self.mock_play,
+ True, # use_handlers
+ self.mock_block,
+ self.mock_variable_manager,
+ self.fake_role_loader)
+
+
+class TestLoadListOfRoles(unittest.TestCase, MixinForMocks):
+ def setUp(self):
+ self._setup()
+
+ def test_ds_not_list(self):
+ ds = {}
+ self.assertRaises(AssertionError, helpers.load_list_of_roles,
+ ds, self.mock_play)
+
+ def test_empty_role(self):
+ ds = [{}]
+ self.assertRaisesRegex(errors.AnsibleError,
+ "role definitions must contain a role name",
+ helpers.load_list_of_roles,
+ ds, self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_role_loader)
+
+ def test_empty_role_just_name(self):
+ ds = [{'name': 'bogus_role'}]
+ res = helpers.load_list_of_roles(ds, self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_role_loader)
+ self.assertIsInstance(res, list)
+ for r in res:
+ self.assertIsInstance(r, RoleInclude)
+
+ def test_block_unknown_action(self):
+ ds = [{
+ 'block': [{'action': 'foo_test_block_unknown_action'}]
+ }]
+ ds = [{'name': 'bogus_role'}]
+ res = helpers.load_list_of_roles(ds, self.mock_play,
+ variable_manager=self.mock_variable_manager, loader=self.fake_role_loader)
+ self.assertIsInstance(res, list)
+ for r in res:
+ self.assertIsInstance(r, RoleInclude)
+
+
+class TestLoadListOfBlocks(unittest.TestCase, MixinForMocks):
+ def setUp(self):
+ self._setup()
+
+ def test_ds_not_list(self):
+ ds = {}
+ mock_play = MagicMock(name='MockPlay')
+ self.assertRaises(AssertionError, helpers.load_list_of_blocks,
+ ds, mock_play, parent_block=None, role=None, task_include=None, use_handlers=False, variable_manager=None, loader=None)
+
+ def test_empty_block(self):
+ ds = [{}]
+ mock_play = MagicMock(name='MockPlay')
+ self.assertRaisesRegex(errors.AnsibleParserError,
+ "no module/action detected in task",
+ helpers.load_list_of_blocks,
+ ds, mock_play,
+ parent_block=None,
+ role=None,
+ task_include=None,
+ use_handlers=False,
+ variable_manager=None,
+ loader=None)
+
+ def test_block_unknown_action(self):
+ ds = [{'action': 'foo', 'collections': []}]
+ mock_play = MagicMock(name='MockPlay')
+ res = helpers.load_list_of_blocks(ds, mock_play, parent_block=None, role=None, task_include=None, use_handlers=False, variable_manager=None,
+ loader=None)
+
+ self.assertIsInstance(res, list)
+ for block in res:
+ self.assertIsInstance(block, Block)
diff --git a/test/units/playbook/test_included_file.py b/test/units/playbook/test_included_file.py
new file mode 100644
index 0000000..7341dff
--- /dev/null
+++ b/test/units/playbook/test_included_file.py
@@ -0,0 +1,332 @@
+# (c) 2016, Adrian Likins <alikins@redhat.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+import pytest
+
+from unittest.mock import MagicMock
+from units.mock.loader import DictDataLoader
+
+from ansible.playbook.block import Block
+from ansible.playbook.task import Task
+from ansible.playbook.task_include import TaskInclude
+from ansible.playbook.role_include import IncludeRole
+from ansible.executor import task_result
+
+from ansible.playbook.included_file import IncludedFile
+from ansible.errors import AnsibleParserError
+
+
+@pytest.fixture
+def mock_iterator():
+ mock_iterator = MagicMock(name='MockIterator')
+ mock_iterator._play = MagicMock(name='MockPlay')
+ return mock_iterator
+
+
+@pytest.fixture
+def mock_variable_manager():
+ # TODO: can we use a real VariableManager?
+ mock_variable_manager = MagicMock(name='MockVariableManager')
+ mock_variable_manager.get_vars.return_value = dict()
+ return mock_variable_manager
+
+
+def test_equals_ok():
+ uuid = '111-111'
+ parent = MagicMock(name='MockParent')
+ parent._uuid = uuid
+ task = MagicMock(name='MockTask')
+ task._uuid = uuid
+ task._parent = parent
+ inc_a = IncludedFile('a.yml', {}, {}, task)
+ inc_b = IncludedFile('a.yml', {}, {}, task)
+ assert inc_a == inc_b
+
+
+def test_equals_different_tasks():
+ parent = MagicMock(name='MockParent')
+ parent._uuid = '111-111'
+ task_a = MagicMock(name='MockTask')
+ task_a._uuid = '11-11'
+ task_a._parent = parent
+ task_b = MagicMock(name='MockTask')
+ task_b._uuid = '22-22'
+ task_b._parent = parent
+ inc_a = IncludedFile('a.yml', {}, {}, task_a)
+ inc_b = IncludedFile('a.yml', {}, {}, task_b)
+ assert inc_a != inc_b
+
+
+def test_equals_different_parents():
+ parent_a = MagicMock(name='MockParent')
+ parent_a._uuid = '111-111'
+ parent_b = MagicMock(name='MockParent')
+ parent_b._uuid = '222-222'
+ task_a = MagicMock(name='MockTask')
+ task_a._uuid = '11-11'
+ task_a._parent = parent_a
+ task_b = MagicMock(name='MockTask')
+ task_b._uuid = '11-11'
+ task_b._parent = parent_b
+ inc_a = IncludedFile('a.yml', {}, {}, task_a)
+ inc_b = IncludedFile('a.yml', {}, {}, task_b)
+ assert inc_a != inc_b
+
+
+def test_included_file_instantiation():
+ filename = 'somefile.yml'
+
+ inc_file = IncludedFile(filename=filename, args={}, vars={}, task=None)
+
+ assert isinstance(inc_file, IncludedFile)
+ assert inc_file._filename == filename
+ assert inc_file._args == {}
+ assert inc_file._vars == {}
+ assert inc_file._task is None
+
+
+def test_process_include_results(mock_iterator, mock_variable_manager):
+ hostname = "testhost1"
+ hostname2 = "testhost2"
+
+ parent_task_ds = {'debug': 'msg=foo'}
+ parent_task = Task.load(parent_task_ds)
+ parent_task._play = None
+
+ task_ds = {'include': 'include_test.yml'}
+ loaded_task = TaskInclude.load(task_ds, task_include=parent_task)
+
+ return_data = {'include': 'include_test.yml'}
+ # The task in the TaskResult has to be a TaskInclude so it has a .static attr
+ result1 = task_result.TaskResult(host=hostname, task=loaded_task, return_data=return_data)
+ result2 = task_result.TaskResult(host=hostname2, task=loaded_task, return_data=return_data)
+ results = [result1, result2]
+
+ fake_loader = DictDataLoader({'include_test.yml': ""})
+
+ res = IncludedFile.process_include_results(results, mock_iterator, fake_loader, mock_variable_manager)
+ assert isinstance(res, list)
+ assert len(res) == 1
+ assert res[0]._filename == os.path.join(os.getcwd(), 'include_test.yml')
+ assert res[0]._hosts == ['testhost1', 'testhost2']
+ assert res[0]._args == {}
+ assert res[0]._vars == {}
+
+
+def test_process_include_diff_files(mock_iterator, mock_variable_manager):
+ hostname = "testhost1"
+ hostname2 = "testhost2"
+
+ parent_task_ds = {'debug': 'msg=foo'}
+ parent_task = Task.load(parent_task_ds)
+ parent_task._play = None
+
+ task_ds = {'include': 'include_test.yml'}
+ loaded_task = TaskInclude.load(task_ds, task_include=parent_task)
+ loaded_task._play = None
+
+ child_task_ds = {'include': 'other_include_test.yml'}
+ loaded_child_task = TaskInclude.load(child_task_ds, task_include=loaded_task)
+ loaded_child_task._play = None
+
+ return_data = {'include': 'include_test.yml'}
+ # The task in the TaskResult has to be a TaskInclude so it has a .static attr
+ result1 = task_result.TaskResult(host=hostname, task=loaded_task, return_data=return_data)
+
+ return_data = {'include': 'other_include_test.yml'}
+ result2 = task_result.TaskResult(host=hostname2, task=loaded_child_task, return_data=return_data)
+ results = [result1, result2]
+
+ fake_loader = DictDataLoader({'include_test.yml': "",
+ 'other_include_test.yml': ""})
+
+ res = IncludedFile.process_include_results(results, mock_iterator, fake_loader, mock_variable_manager)
+ assert isinstance(res, list)
+ assert res[0]._filename == os.path.join(os.getcwd(), 'include_test.yml')
+ assert res[1]._filename == os.path.join(os.getcwd(), 'other_include_test.yml')
+
+ assert res[0]._hosts == ['testhost1']
+ assert res[1]._hosts == ['testhost2']
+
+ assert res[0]._args == {}
+ assert res[1]._args == {}
+
+ assert res[0]._vars == {}
+ assert res[1]._vars == {}
+
+
+def test_process_include_simulate_free(mock_iterator, mock_variable_manager):
+ hostname = "testhost1"
+ hostname2 = "testhost2"
+
+ parent_task_ds = {'debug': 'msg=foo'}
+ parent_task1 = Task.load(parent_task_ds)
+ parent_task2 = Task.load(parent_task_ds)
+
+ parent_task1._play = None
+ parent_task2._play = None
+
+ task_ds = {'include': 'include_test.yml'}
+ loaded_task1 = TaskInclude.load(task_ds, task_include=parent_task1)
+ loaded_task2 = TaskInclude.load(task_ds, task_include=parent_task2)
+
+ return_data = {'include': 'include_test.yml'}
+ # The task in the TaskResult has to be a TaskInclude so it has a .static attr
+ result1 = task_result.TaskResult(host=hostname, task=loaded_task1, return_data=return_data)
+ result2 = task_result.TaskResult(host=hostname2, task=loaded_task2, return_data=return_data)
+ results = [result1, result2]
+
+ fake_loader = DictDataLoader({'include_test.yml': ""})
+
+ res = IncludedFile.process_include_results(results, mock_iterator, fake_loader, mock_variable_manager)
+ assert isinstance(res, list)
+ assert len(res) == 2
+ assert res[0]._filename == os.path.join(os.getcwd(), 'include_test.yml')
+ assert res[1]._filename == os.path.join(os.getcwd(), 'include_test.yml')
+
+ assert res[0]._hosts == ['testhost1']
+ assert res[1]._hosts == ['testhost2']
+
+ assert res[0]._args == {}
+ assert res[1]._args == {}
+
+ assert res[0]._vars == {}
+ assert res[1]._vars == {}
+
+
+def test_process_include_simulate_free_block_role_tasks(mock_iterator,
+ mock_variable_manager):
+ """Test loading the same role returns different included files
+
+ In the case of free, we may end up with included files from roles that
+ have the same parent but are different tasks. Previously the comparison
+ for equality did not check if the tasks were the same and only checked
+ that the parents were the same. This lead to some tasks being run
+ incorrectly and some tasks being silient dropped."""
+
+ fake_loader = DictDataLoader({
+ 'include_test.yml': "",
+ '/etc/ansible/roles/foo_role/tasks/task1.yml': """
+ - debug: msg=task1
+ """,
+ '/etc/ansible/roles/foo_role/tasks/task2.yml': """
+ - debug: msg=task2
+ """,
+ })
+
+ hostname = "testhost1"
+ hostname2 = "testhost2"
+
+ role1_ds = {
+ 'name': 'task1 include',
+ 'include_role': {
+ 'name': 'foo_role',
+ 'tasks_from': 'task1.yml'
+ }
+ }
+ role2_ds = {
+ 'name': 'task2 include',
+ 'include_role': {
+ 'name': 'foo_role',
+ 'tasks_from': 'task2.yml'
+ }
+ }
+ parent_task_ds = {
+ 'block': [
+ role1_ds,
+ role2_ds
+ ]
+ }
+ parent_block = Block.load(parent_task_ds, loader=fake_loader)
+
+ parent_block._play = None
+
+ include_role1_ds = {
+ 'include_args': {
+ 'name': 'foo_role',
+ 'tasks_from': 'task1.yml'
+ }
+ }
+ include_role2_ds = {
+ 'include_args': {
+ 'name': 'foo_role',
+ 'tasks_from': 'task2.yml'
+ }
+ }
+
+ include_role1 = IncludeRole.load(role1_ds,
+ block=parent_block,
+ loader=fake_loader)
+ include_role2 = IncludeRole.load(role2_ds,
+ block=parent_block,
+ loader=fake_loader)
+
+ result1 = task_result.TaskResult(host=hostname,
+ task=include_role1,
+ return_data=include_role1_ds)
+ result2 = task_result.TaskResult(host=hostname2,
+ task=include_role2,
+ return_data=include_role2_ds)
+ results = [result1, result2]
+
+ res = IncludedFile.process_include_results(results,
+ mock_iterator,
+ fake_loader,
+ mock_variable_manager)
+ assert isinstance(res, list)
+ # we should get two different includes
+ assert len(res) == 2
+ assert res[0]._filename == 'foo_role'
+ assert res[1]._filename == 'foo_role'
+ # with different tasks
+ assert res[0]._task != res[1]._task
+
+ assert res[0]._hosts == ['testhost1']
+ assert res[1]._hosts == ['testhost2']
+
+ assert res[0]._args == {}
+ assert res[1]._args == {}
+
+ assert res[0]._vars == {}
+ assert res[1]._vars == {}
+
+
+def test_empty_raw_params():
+ parent_task_ds = {'debug': 'msg=foo'}
+ parent_task = Task.load(parent_task_ds)
+ parent_task._play = None
+
+ task_ds_list = [
+ {
+ 'include': ''
+ },
+ {
+ 'include_tasks': ''
+ },
+ {
+ 'import_tasks': ''
+ }
+ ]
+ for task_ds in task_ds_list:
+ with pytest.raises(AnsibleParserError):
+ TaskInclude.load(task_ds, task_include=parent_task)
diff --git a/test/units/playbook/test_play.py b/test/units/playbook/test_play.py
new file mode 100644
index 0000000..bcc1e5e
--- /dev/null
+++ b/test/units/playbook/test_play.py
@@ -0,0 +1,291 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible.errors import AnsibleAssertionError, AnsibleParserError
+from ansible.parsing.yaml.objects import AnsibleVaultEncryptedUnicode
+from ansible.playbook.block import Block
+from ansible.playbook.play import Play
+from ansible.playbook.role import Role
+from ansible.playbook.task import Task
+
+from units.mock.loader import DictDataLoader
+
+
+def test_empty_play():
+ p = Play.load({})
+
+ assert str(p) == ''
+
+
+def test_play_with_hosts_string():
+ p = Play.load({'hosts': 'foo'})
+
+ assert str(p) == 'foo'
+
+ # Test the caching since self.name should be set by previous call.
+ assert p.get_name() == 'foo'
+
+
+def test_basic_play():
+ p = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ connection='local',
+ remote_user="root",
+ become=True,
+ become_user="testing",
+ ))
+
+ assert p.name == 'test play'
+ assert p.hosts == ['foo']
+ assert p.connection == 'local'
+
+
+def test_play_with_remote_user():
+ p = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ user="testing",
+ gather_facts=False,
+ ))
+
+ assert p.remote_user == "testing"
+
+
+def test_play_with_user_conflict():
+ play_data = dict(
+ name="test play",
+ hosts=['foo'],
+ user="testing",
+ remote_user="testing",
+ )
+
+ with pytest.raises(AnsibleParserError):
+ Play.load(play_data)
+
+
+def test_play_with_bad_ds_type():
+ play_data = []
+ with pytest.raises(AnsibleAssertionError, match=r"while preprocessing data \(\[\]\), ds should be a dict but was a <(?:class|type) 'list'>"):
+ Play.load(play_data)
+
+
+def test_play_with_tasks():
+ p = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ tasks=[dict(action='shell echo "hello world"')],
+ ))
+
+ assert len(p.tasks) == 1
+ assert isinstance(p.tasks[0], Block)
+ assert p.tasks[0].has_tasks() is True
+
+
+def test_play_with_handlers():
+ p = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ handlers=[dict(action='shell echo "hello world"')],
+ ))
+
+ assert len(p.handlers) >= 1
+ assert len(p.get_handlers()) >= 1
+ assert isinstance(p.handlers[0], Block)
+ assert p.handlers[0].has_tasks() is True
+
+
+def test_play_with_pre_tasks():
+ p = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ pre_tasks=[dict(action='shell echo "hello world"')],
+ ))
+
+ assert len(p.pre_tasks) >= 1
+ assert isinstance(p.pre_tasks[0], Block)
+ assert p.pre_tasks[0].has_tasks() is True
+
+ assert len(p.get_tasks()) >= 1
+ assert isinstance(p.get_tasks()[0][0], Task)
+ assert p.get_tasks()[0][0].action == 'shell'
+
+
+def test_play_with_post_tasks():
+ p = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ post_tasks=[dict(action='shell echo "hello world"')],
+ ))
+
+ assert len(p.post_tasks) >= 1
+ assert isinstance(p.post_tasks[0], Block)
+ assert p.post_tasks[0].has_tasks() is True
+
+
+def test_play_with_roles(mocker):
+ mocker.patch('ansible.playbook.role.definition.RoleDefinition._load_role_path', return_value=('foo', '/etc/ansible/roles/foo'))
+ fake_loader = DictDataLoader({
+ '/etc/ansible/roles/foo/tasks.yml': """
+ - name: role task
+ shell: echo "hello world"
+ """,
+ })
+
+ mock_var_manager = mocker.MagicMock()
+ mock_var_manager.get_vars.return_value = {}
+
+ p = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ roles=['foo'],
+ ), loader=fake_loader, variable_manager=mock_var_manager)
+
+ blocks = p.compile()
+ assert len(blocks) > 1
+ assert all(isinstance(block, Block) for block in blocks)
+ assert isinstance(p.get_roles()[0], Role)
+
+
+def test_play_compile():
+ p = Play.load(dict(
+ name="test play",
+ hosts=['foo'],
+ gather_facts=False,
+ tasks=[dict(action='shell echo "hello world"')],
+ ))
+
+ blocks = p.compile()
+
+ # with a single block, there will still be three
+ # implicit meta flush_handler blocks inserted
+ assert len(blocks) == 4
+
+
+@pytest.mark.parametrize(
+ 'value, expected',
+ (
+ ('my_vars.yml', ['my_vars.yml']),
+ (['my_vars.yml'], ['my_vars.yml']),
+ (['my_vars1.yml', 'my_vars2.yml'], ['my_vars1.yml', 'my_vars2.yml']),
+ (None, []),
+ )
+)
+def test_play_with_vars_files(value, expected):
+ play = Play.load({
+ 'name': 'Play with vars_files',
+ 'hosts': ['testhost1'],
+ 'vars_files': value,
+ })
+
+ assert play.vars_files == value
+ assert play.get_vars_files() == expected
+
+
+@pytest.mark.parametrize('value', ([], tuple(), set(), {}, '', None, False, 0))
+def test_play_empty_hosts(value):
+ with pytest.raises(AnsibleParserError, match='Hosts list cannot be empty'):
+ Play.load({'hosts': value})
+
+
+@pytest.mark.parametrize('value', ([None], (None,), ['one', None]))
+def test_play_none_hosts(value):
+ with pytest.raises(AnsibleParserError, match="Hosts list cannot contain values of 'None'"):
+ Play.load({'hosts': value})
+
+
+@pytest.mark.parametrize(
+ 'value',
+ (
+ {'one': None},
+ {'one': 'two'},
+ True,
+ 1,
+ 1.75,
+ AnsibleVaultEncryptedUnicode('secret'),
+ )
+)
+def test_play_invalid_hosts_sequence(value):
+ with pytest.raises(AnsibleParserError, match='Hosts list must be a sequence or string'):
+ Play.load({'hosts': value})
+
+
+@pytest.mark.parametrize(
+ 'value',
+ (
+ [[1, 'two']],
+ [{'one': None}],
+ [set((None, 'one'))],
+ ['one', 'two', {'three': None}],
+ ['one', 'two', {'three': 'four'}],
+ [AnsibleVaultEncryptedUnicode('secret')],
+ )
+)
+def test_play_invalid_hosts_value(value):
+ with pytest.raises(AnsibleParserError, match='Hosts list contains an invalid host value'):
+ Play.load({'hosts': value})
+
+
+def test_play_with_vars():
+ play = Play.load({}, vars={'var1': 'val1'})
+
+ assert play.get_name() == ''
+ assert play.vars == {'var1': 'val1'}
+ assert play.get_vars() == {'var1': 'val1'}
+
+
+def test_play_no_name_hosts_sequence():
+ play = Play.load({'hosts': ['host1', 'host2']})
+
+ assert play.get_name() == 'host1,host2'
+
+
+def test_play_hosts_template_expression():
+ play = Play.load({'hosts': "{{ target_hosts }}"})
+
+ assert play.get_name() == '{{ target_hosts }}'
+
+
+@pytest.mark.parametrize(
+ 'call',
+ (
+ '_load_tasks',
+ '_load_pre_tasks',
+ '_load_post_tasks',
+ '_load_handlers',
+ '_load_roles',
+ )
+)
+def test_bad_blocks_roles(mocker, call):
+ mocker.patch('ansible.playbook.play.load_list_of_blocks', side_effect=AssertionError('Raised intentionally'))
+ mocker.patch('ansible.playbook.play.load_list_of_roles', side_effect=AssertionError('Raised intentionally'))
+
+ play = Play.load({})
+ with pytest.raises(AnsibleParserError, match='A malformed (block|(role declaration)) was encountered'):
+ getattr(play, call)('', None)
diff --git a/test/units/playbook/test_play_context.py b/test/units/playbook/test_play_context.py
new file mode 100644
index 0000000..7c24de5
--- /dev/null
+++ b/test/units/playbook/test_play_context.py
@@ -0,0 +1,94 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (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
+
+import pytest
+
+from ansible import constants as C
+from ansible import context
+from ansible.cli.arguments import option_helpers as opt_help
+from ansible.errors import AnsibleError
+from ansible.playbook.play_context import PlayContext
+from ansible.playbook.play import Play
+from ansible.plugins.loader import become_loader
+from ansible.utils import context_objects as co
+
+
+@pytest.fixture
+def parser():
+ parser = opt_help.create_base_parser('testparser')
+
+ opt_help.add_runas_options(parser)
+ opt_help.add_meta_options(parser)
+ opt_help.add_runtask_options(parser)
+ opt_help.add_vault_options(parser)
+ opt_help.add_async_options(parser)
+ opt_help.add_connect_options(parser)
+ opt_help.add_subset_options(parser)
+ opt_help.add_check_options(parser)
+ opt_help.add_inventory_options(parser)
+
+ return parser
+
+
+@pytest.fixture
+def reset_cli_args():
+ co.GlobalCLIArgs._Singleton__instance = None
+ yield
+ co.GlobalCLIArgs._Singleton__instance = None
+
+
+def test_play_context(mocker, parser, reset_cli_args):
+ options = parser.parse_args(['-vv', '--check'])
+ context._init_global_context(options)
+ play = Play.load({})
+ play_context = PlayContext(play=play)
+
+ assert play_context.remote_addr is None
+ assert play_context.remote_user is None
+ assert play_context.password == ''
+ assert play_context.private_key_file == C.DEFAULT_PRIVATE_KEY_FILE
+ assert play_context.timeout == C.DEFAULT_TIMEOUT
+ assert play_context.verbosity == 2
+ assert play_context.check_mode is True
+
+ mock_play = mocker.MagicMock()
+ mock_play.force_handlers = True
+
+ play_context = PlayContext(play=mock_play)
+ assert play_context.force_handlers is True
+
+ mock_task = mocker.MagicMock()
+ mock_task.connection = 'mocktask'
+ mock_task.remote_user = 'mocktask'
+ mock_task.port = 1234
+ mock_task.no_log = True
+ mock_task.become = True
+ mock_task.become_method = 'mocktask'
+ mock_task.become_user = 'mocktaskroot'
+ mock_task.become_pass = 'mocktaskpass'
+ mock_task._local_action = False
+ mock_task.delegate_to = None
+
+ all_vars = dict(
+ ansible_connection='mock_inventory',
+ ansible_ssh_port=4321,
+ )
+
+ mock_templar = mocker.MagicMock()
+
+ play_context = PlayContext()
+ play_context = play_context.set_task_and_variable_override(task=mock_task, variables=all_vars, templar=mock_templar)
+
+ assert play_context.connection == 'mock_inventory'
+ assert play_context.remote_user == 'mocktask'
+ assert play_context.no_log is True
+
+ mock_task.no_log = False
+ play_context = play_context.set_task_and_variable_override(task=mock_task, variables=all_vars, templar=mock_templar)
+ assert play_context.no_log is False
diff --git a/test/units/playbook/test_playbook.py b/test/units/playbook/test_playbook.py
new file mode 100644
index 0000000..68a9fb7
--- /dev/null
+++ b/test/units/playbook/test_playbook.py
@@ -0,0 +1,61 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from ansible.errors import AnsibleParserError
+from ansible.playbook import Playbook
+from ansible.vars.manager import VariableManager
+
+from units.mock.loader import DictDataLoader
+
+
+class TestPlaybook(unittest.TestCase):
+
+ def test_empty_playbook(self):
+ fake_loader = DictDataLoader({})
+ p = Playbook(loader=fake_loader)
+
+ def test_basic_playbook(self):
+ fake_loader = DictDataLoader({
+ "test_file.yml": """
+ - hosts: all
+ """,
+ })
+ p = Playbook.load("test_file.yml", loader=fake_loader)
+ plays = p.get_plays()
+
+ def test_bad_playbook_files(self):
+ fake_loader = DictDataLoader({
+ # represents a playbook which is not a list of plays
+ "bad_list.yml": """
+ foo: bar
+
+ """,
+ # represents a playbook where a play entry is mis-formatted
+ "bad_entry.yml": """
+ -
+ - "This should be a mapping..."
+
+ """,
+ })
+ vm = VariableManager()
+ self.assertRaises(AnsibleParserError, Playbook.load, "bad_list.yml", vm, fake_loader)
+ self.assertRaises(AnsibleParserError, Playbook.load, "bad_entry.yml", vm, fake_loader)
diff --git a/test/units/playbook/test_taggable.py b/test/units/playbook/test_taggable.py
new file mode 100644
index 0000000..3881e17
--- /dev/null
+++ b/test/units/playbook/test_taggable.py
@@ -0,0 +1,105 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from ansible.playbook.taggable import Taggable
+from units.mock.loader import DictDataLoader
+
+
+class TaggableTestObj(Taggable):
+
+ def __init__(self):
+ self._loader = DictDataLoader({})
+ self.tags = []
+
+
+class TestTaggable(unittest.TestCase):
+
+ def assert_evaluate_equal(self, test_value, tags, only_tags, skip_tags):
+ taggable_obj = TaggableTestObj()
+ taggable_obj.tags = tags
+
+ evaluate = taggable_obj.evaluate_tags(only_tags, skip_tags, {})
+
+ self.assertEqual(test_value, evaluate)
+
+ def test_evaluate_tags_tag_in_only_tags(self):
+ self.assert_evaluate_equal(True, ['tag1', 'tag2'], ['tag1'], [])
+
+ def test_evaluate_tags_tag_in_skip_tags(self):
+ self.assert_evaluate_equal(False, ['tag1', 'tag2'], [], ['tag1'])
+
+ def test_evaluate_tags_special_always_in_object_tags(self):
+ self.assert_evaluate_equal(True, ['tag', 'always'], ['random'], [])
+
+ def test_evaluate_tags_tag_in_skip_tags_special_always_in_object_tags(self):
+ self.assert_evaluate_equal(False, ['tag', 'always'], ['random'], ['tag'])
+
+ def test_evaluate_tags_special_always_in_skip_tags_and_always_in_tags(self):
+ self.assert_evaluate_equal(False, ['tag', 'always'], [], ['always'])
+
+ def test_evaluate_tags_special_tagged_in_only_tags_and_object_tagged(self):
+ self.assert_evaluate_equal(True, ['tag'], ['tagged'], [])
+
+ def test_evaluate_tags_special_tagged_in_only_tags_and_object_untagged(self):
+ self.assert_evaluate_equal(False, [], ['tagged'], [])
+
+ def test_evaluate_tags_special_tagged_in_skip_tags_and_object_tagged(self):
+ self.assert_evaluate_equal(False, ['tag'], [], ['tagged'])
+
+ def test_evaluate_tags_special_tagged_in_skip_tags_and_object_untagged(self):
+ self.assert_evaluate_equal(True, [], [], ['tagged'])
+
+ def test_evaluate_tags_special_untagged_in_only_tags_and_object_tagged(self):
+ self.assert_evaluate_equal(False, ['tag'], ['untagged'], [])
+
+ def test_evaluate_tags_special_untagged_in_only_tags_and_object_untagged(self):
+ self.assert_evaluate_equal(True, [], ['untagged'], [])
+
+ def test_evaluate_tags_special_untagged_in_skip_tags_and_object_tagged(self):
+ self.assert_evaluate_equal(True, ['tag'], [], ['untagged'])
+
+ def test_evaluate_tags_special_untagged_in_skip_tags_and_object_untagged(self):
+ self.assert_evaluate_equal(False, [], [], ['untagged'])
+
+ def test_evaluate_tags_special_all_in_only_tags(self):
+ self.assert_evaluate_equal(True, ['tag'], ['all'], ['untagged'])
+
+ def test_evaluate_tags_special_all_in_only_tags_and_object_untagged(self):
+ self.assert_evaluate_equal(True, [], ['all'], [])
+
+ def test_evaluate_tags_special_all_in_skip_tags(self):
+ self.assert_evaluate_equal(False, ['tag'], ['tag'], ['all'])
+
+ def test_evaluate_tags_special_all_in_only_tags_and_special_all_in_skip_tags(self):
+ self.assert_evaluate_equal(False, ['tag'], ['all'], ['all'])
+
+ def test_evaluate_tags_special_all_in_skip_tags_and_always_in_object_tags(self):
+ self.assert_evaluate_equal(True, ['tag', 'always'], [], ['all'])
+
+ def test_evaluate_tags_special_all_in_skip_tags_and_special_always_in_skip_tags_and_always_in_object_tags(self):
+ self.assert_evaluate_equal(False, ['tag', 'always'], [], ['all', 'always'])
+
+ def test_evaluate_tags_accepts_lists(self):
+ self.assert_evaluate_equal(True, ['tag1', 'tag2'], ['tag2'], [])
+
+ def test_evaluate_tags_with_repeated_tags(self):
+ self.assert_evaluate_equal(False, ['tag', 'tag'], [], ['tag'])
diff --git a/test/units/playbook/test_task.py b/test/units/playbook/test_task.py
new file mode 100644
index 0000000..070d7aa
--- /dev/null
+++ b/test/units/playbook/test_task.py
@@ -0,0 +1,114 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from unittest.mock import patch
+from ansible.playbook.task import Task
+from ansible.parsing.yaml import objects
+from ansible import errors
+
+
+basic_command_task = dict(
+ name='Test Task',
+ command='echo hi'
+)
+
+kv_command_task = dict(
+ action='command echo hi'
+)
+
+# See #36848
+kv_bad_args_str = '- apk: sdfs sf sdf 37'
+kv_bad_args_ds = {'apk': 'sdfs sf sdf 37'}
+
+
+class TestTask(unittest.TestCase):
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ def test_construct_empty_task(self):
+ Task()
+
+ def test_construct_task_with_role(self):
+ pass
+
+ def test_construct_task_with_block(self):
+ pass
+
+ def test_construct_task_with_role_and_block(self):
+ pass
+
+ def test_load_task_simple(self):
+ t = Task.load(basic_command_task)
+ assert t is not None
+ self.assertEqual(t.name, basic_command_task['name'])
+ self.assertEqual(t.action, 'command')
+ self.assertEqual(t.args, dict(_raw_params='echo hi'))
+
+ def test_load_task_kv_form(self):
+ t = Task.load(kv_command_task)
+ self.assertEqual(t.action, 'command')
+ self.assertEqual(t.args, dict(_raw_params='echo hi'))
+
+ @patch.object(errors.AnsibleError, '_get_error_lines_from_file')
+ def test_load_task_kv_form_error_36848(self, mock_get_err_lines):
+ ds = objects.AnsibleMapping(kv_bad_args_ds)
+ ds.ansible_pos = ('test_task_faux_playbook.yml', 1, 1)
+ mock_get_err_lines.return_value = (kv_bad_args_str, '')
+
+ with self.assertRaises(errors.AnsibleParserError) as cm:
+ Task.load(ds)
+
+ self.assertIsInstance(cm.exception, errors.AnsibleParserError)
+ self.assertEqual(cm.exception.obj, ds)
+ self.assertEqual(cm.exception.obj, kv_bad_args_ds)
+ self.assertIn("The error appears to be in 'test_task_faux_playbook.yml", cm.exception.message)
+ self.assertIn(kv_bad_args_str, cm.exception.message)
+ self.assertIn('apk', cm.exception.message)
+ self.assertEqual(cm.exception.message.count('The offending line'), 1)
+ self.assertEqual(cm.exception.message.count('The error appears to be in'), 1)
+
+ def test_task_auto_name(self):
+ assert 'name' not in kv_command_task
+ Task.load(kv_command_task)
+ # self.assertEqual(t.name, 'shell echo hi')
+
+ def test_task_auto_name_with_role(self):
+ pass
+
+ def test_load_task_complex_form(self):
+ pass
+
+ def test_can_load_module_complex_form(self):
+ pass
+
+ def test_local_action_implies_delegate(self):
+ pass
+
+ def test_local_action_conflicts_with_delegate(self):
+ pass
+
+ def test_delegate_to_parses(self):
+ pass
diff --git a/test/units/plugins/__init__.py b/test/units/plugins/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/__init__.py
diff --git a/test/units/plugins/action/__init__.py b/test/units/plugins/action/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/action/__init__.py
diff --git a/test/units/plugins/action/test_action.py b/test/units/plugins/action/test_action.py
new file mode 100644
index 0000000..f2bbe19
--- /dev/null
+++ b/test/units/plugins/action/test_action.py
@@ -0,0 +1,912 @@
+# -*- coding: utf-8 -*-
+# (c) 2015, Florian Apolloner <florian@apolloner.eu>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import re
+
+from ansible import constants as C
+from units.compat import unittest
+from unittest.mock import patch, MagicMock, mock_open
+
+from ansible.errors import AnsibleError, AnsibleAuthenticationFailure
+from ansible.module_utils.six import text_type
+from ansible.module_utils.six.moves import shlex_quote, builtins
+from ansible.module_utils._text import to_bytes
+from ansible.playbook.play_context import PlayContext
+from ansible.plugins.action import ActionBase
+from ansible.template import Templar
+from ansible.vars.clean import clean_facts
+
+from units.mock.loader import DictDataLoader
+
+
+python_module_replacers = br"""
+#!/usr/bin/python
+
+#ANSIBLE_VERSION = "<<ANSIBLE_VERSION>>"
+#MODULE_COMPLEX_ARGS = "<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"
+#SELINUX_SPECIAL_FS="<<SELINUX_SPECIAL_FILESYSTEMS>>"
+
+test = u'Toshio \u304f\u3089\u3068\u307f'
+from ansible.module_utils.basic import *
+"""
+
+powershell_module_replacers = b"""
+WINDOWS_ARGS = "<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>"
+# POWERSHELL_COMMON
+"""
+
+
+def _action_base():
+ fake_loader = DictDataLoader({
+ })
+ mock_module_loader = MagicMock()
+ mock_shared_loader_obj = MagicMock()
+ mock_shared_loader_obj.module_loader = mock_module_loader
+ mock_connection_loader = MagicMock()
+
+ mock_shared_loader_obj.connection_loader = mock_connection_loader
+ mock_connection = MagicMock()
+
+ play_context = MagicMock()
+
+ action_base = DerivedActionBase(task=None,
+ connection=mock_connection,
+ play_context=play_context,
+ loader=fake_loader,
+ templar=None,
+ shared_loader_obj=mock_shared_loader_obj)
+ return action_base
+
+
+class DerivedActionBase(ActionBase):
+ TRANSFERS_FILES = False
+
+ def run(self, tmp=None, task_vars=None):
+ # We're not testing the plugin run() method, just the helper
+ # methods ActionBase defines
+ return super(DerivedActionBase, self).run(tmp=tmp, task_vars=task_vars)
+
+
+class TestActionBase(unittest.TestCase):
+
+ def test_action_base_run(self):
+ mock_task = MagicMock()
+ mock_task.action = "foo"
+ mock_task.args = dict(a=1, b=2, c=3)
+
+ mock_connection = MagicMock()
+
+ play_context = PlayContext()
+
+ mock_task.async_val = None
+ action_base = DerivedActionBase(mock_task, mock_connection, play_context, None, None, None)
+ results = action_base.run()
+ self.assertEqual(results, dict())
+
+ mock_task.async_val = 0
+ action_base = DerivedActionBase(mock_task, mock_connection, play_context, None, None, None)
+ results = action_base.run()
+ self.assertEqual(results, {})
+
+ def test_action_base__configure_module(self):
+ fake_loader = DictDataLoader({
+ })
+
+ # create our fake task
+ mock_task = MagicMock()
+ mock_task.action = "copy"
+ mock_task.async_val = 0
+ mock_task.delegate_to = None
+
+ # create a mock connection, so we don't actually try and connect to things
+ mock_connection = MagicMock()
+
+ # create a mock shared loader object
+ def mock_find_plugin_with_context(name, options, collection_list=None):
+ mockctx = MagicMock()
+ if name == 'badmodule':
+ mockctx.resolved = False
+ mockctx.plugin_resolved_path = None
+ elif '.ps1' in options:
+ mockctx.resolved = True
+ mockctx.plugin_resolved_path = '/fake/path/to/%s.ps1' % name
+ else:
+ mockctx.resolved = True
+ mockctx.plugin_resolved_path = '/fake/path/to/%s' % name
+ return mockctx
+
+ mock_module_loader = MagicMock()
+ mock_module_loader.find_plugin_with_context.side_effect = mock_find_plugin_with_context
+ mock_shared_obj_loader = MagicMock()
+ mock_shared_obj_loader.module_loader = mock_module_loader
+
+ # we're using a real play context here
+ play_context = PlayContext()
+
+ # our test class
+ action_base = DerivedActionBase(
+ task=mock_task,
+ connection=mock_connection,
+ play_context=play_context,
+ loader=fake_loader,
+ templar=Templar(loader=fake_loader),
+ shared_loader_obj=mock_shared_obj_loader,
+ )
+
+ # test python module formatting
+ with patch.object(builtins, 'open', mock_open(read_data=to_bytes(python_module_replacers.strip(), encoding='utf-8'))):
+ with patch.object(os, 'rename'):
+ mock_task.args = dict(a=1, foo='fö〩')
+ mock_connection.module_implementation_preferences = ('',)
+ (style, shebang, data, path) = action_base._configure_module(mock_task.action, mock_task.args,
+ task_vars=dict(ansible_python_interpreter='/usr/bin/python',
+ ansible_playbook_python='/usr/bin/python'))
+ self.assertEqual(style, "new")
+ self.assertEqual(shebang, u"#!/usr/bin/python")
+
+ # test module not found
+ self.assertRaises(AnsibleError, action_base._configure_module, 'badmodule', mock_task.args, {})
+
+ # test powershell module formatting
+ with patch.object(builtins, 'open', mock_open(read_data=to_bytes(powershell_module_replacers.strip(), encoding='utf-8'))):
+ mock_task.action = 'win_copy'
+ mock_task.args = dict(b=2)
+ mock_connection.module_implementation_preferences = ('.ps1',)
+ (style, shebang, data, path) = action_base._configure_module('stat', mock_task.args, {})
+ self.assertEqual(style, "new")
+ self.assertEqual(shebang, u'#!powershell')
+
+ # test module not found
+ self.assertRaises(AnsibleError, action_base._configure_module, 'badmodule', mock_task.args, {})
+
+ def test_action_base__compute_environment_string(self):
+ fake_loader = DictDataLoader({
+ })
+
+ # create our fake task
+ mock_task = MagicMock()
+ mock_task.action = "copy"
+ mock_task.args = dict(a=1)
+
+ # create a mock connection, so we don't actually try and connect to things
+ def env_prefix(**args):
+ return ' '.join(['%s=%s' % (k, shlex_quote(text_type(v))) for k, v in args.items()])
+ mock_connection = MagicMock()
+ mock_connection._shell.env_prefix.side_effect = env_prefix
+
+ # we're using a real play context here
+ play_context = PlayContext()
+
+ # and we're using a real templar here too
+ templar = Templar(loader=fake_loader)
+
+ # our test class
+ action_base = DerivedActionBase(
+ task=mock_task,
+ connection=mock_connection,
+ play_context=play_context,
+ loader=fake_loader,
+ templar=templar,
+ shared_loader_obj=None,
+ )
+
+ # test standard environment setup
+ mock_task.environment = [dict(FOO='foo'), None]
+ env_string = action_base._compute_environment_string()
+ self.assertEqual(env_string, "FOO=foo")
+
+ # test where environment is not a list
+ mock_task.environment = dict(FOO='foo')
+ env_string = action_base._compute_environment_string()
+ self.assertEqual(env_string, "FOO=foo")
+
+ # test environment with a variable in it
+ templar.available_variables = dict(the_var='bar')
+ mock_task.environment = [dict(FOO='{{the_var}}')]
+ env_string = action_base._compute_environment_string()
+ self.assertEqual(env_string, "FOO=bar")
+
+ # test with a bad environment set
+ mock_task.environment = dict(FOO='foo')
+ mock_task.environment = ['hi there']
+ self.assertRaises(AnsibleError, action_base._compute_environment_string)
+
+ def test_action_base__early_needs_tmp_path(self):
+ # create our fake task
+ mock_task = MagicMock()
+
+ # create a mock connection, so we don't actually try and connect to things
+ mock_connection = MagicMock()
+
+ # we're using a real play context here
+ play_context = PlayContext()
+
+ # our test class
+ action_base = DerivedActionBase(
+ task=mock_task,
+ connection=mock_connection,
+ play_context=play_context,
+ loader=None,
+ templar=None,
+ shared_loader_obj=None,
+ )
+
+ self.assertFalse(action_base._early_needs_tmp_path())
+
+ action_base.TRANSFERS_FILES = True
+ self.assertTrue(action_base._early_needs_tmp_path())
+
+ def test_action_base__make_tmp_path(self):
+ # create our fake task
+ mock_task = MagicMock()
+
+ def get_shell_opt(opt):
+
+ ret = None
+ if opt == 'admin_users':
+ ret = ['root', 'toor', 'Administrator']
+ elif opt == 'remote_tmp':
+ ret = '~/.ansible/tmp'
+
+ return ret
+
+ # create a mock connection, so we don't actually try and connect to things
+ mock_connection = MagicMock()
+ mock_connection.transport = 'ssh'
+ mock_connection._shell.mkdtemp.return_value = 'mkdir command'
+ mock_connection._shell.join_path.side_effect = os.path.join
+ mock_connection._shell.get_option = get_shell_opt
+ mock_connection._shell.HOMES_RE = re.compile(r'(\'|\")?(~|\$HOME)(.*)')
+
+ # we're using a real play context here
+ play_context = PlayContext()
+ play_context.become = True
+ play_context.become_user = 'foo'
+
+ mock_task.become = True
+ mock_task.become_user = True
+
+ # our test class
+ action_base = DerivedActionBase(
+ task=mock_task,
+ connection=mock_connection,
+ play_context=play_context,
+ loader=None,
+ templar=None,
+ shared_loader_obj=None,
+ )
+
+ action_base._low_level_execute_command = MagicMock()
+ action_base._low_level_execute_command.return_value = dict(rc=0, stdout='/some/path')
+ self.assertEqual(action_base._make_tmp_path('root'), '/some/path/')
+
+ # empty path fails
+ action_base._low_level_execute_command.return_value = dict(rc=0, stdout='')
+ self.assertRaises(AnsibleError, action_base._make_tmp_path, 'root')
+
+ # authentication failure
+ action_base._low_level_execute_command.return_value = dict(rc=5, stdout='')
+ self.assertRaises(AnsibleError, action_base._make_tmp_path, 'root')
+
+ # ssh error
+ action_base._low_level_execute_command.return_value = dict(rc=255, stdout='', stderr='')
+ self.assertRaises(AnsibleError, action_base._make_tmp_path, 'root')
+ self.assertRaises(AnsibleError, action_base._make_tmp_path, 'root')
+
+ # general error
+ action_base._low_level_execute_command.return_value = dict(rc=1, stdout='some stuff here', stderr='')
+ self.assertRaises(AnsibleError, action_base._make_tmp_path, 'root')
+ action_base._low_level_execute_command.return_value = dict(rc=1, stdout='some stuff here', stderr='No space left on device')
+ self.assertRaises(AnsibleError, action_base._make_tmp_path, 'root')
+
+ def test_action_base__fixup_perms2(self):
+ mock_task = MagicMock()
+ mock_connection = MagicMock()
+ play_context = PlayContext()
+ action_base = DerivedActionBase(
+ task=mock_task,
+ connection=mock_connection,
+ play_context=play_context,
+ loader=None,
+ templar=None,
+ shared_loader_obj=None,
+ )
+ action_base._low_level_execute_command = MagicMock()
+ remote_paths = ['/tmp/foo/bar.txt', '/tmp/baz.txt']
+ remote_user = 'remoteuser1'
+
+ # Used for skipping down to common group dir.
+ CHMOD_ACL_FLAGS = ('+a', 'A+user:remoteuser2:r:allow')
+
+ def runWithNoExpectation(execute=False):
+ return action_base._fixup_perms2(
+ remote_paths,
+ remote_user=remote_user,
+ execute=execute)
+
+ def assertSuccess(execute=False):
+ self.assertEqual(runWithNoExpectation(execute), remote_paths)
+
+ def assertThrowRegex(regex, execute=False):
+ self.assertRaisesRegex(
+ AnsibleError,
+ regex,
+ action_base._fixup_perms2,
+ remote_paths,
+ remote_user=remote_user,
+ execute=execute)
+
+ def get_shell_option_for_arg(args_kv, default):
+ '''A helper for get_shell_option. Returns a function that, if
+ called with ``option`` that exists in args_kv, will return the
+ value, else will return ``default`` for every other given arg'''
+ def _helper(option, *args, **kwargs):
+ return args_kv.get(option, default)
+ return _helper
+
+ action_base.get_become_option = MagicMock()
+ action_base.get_become_option.return_value = 'remoteuser2'
+
+ # Step 1: On windows, we just return remote_paths
+ action_base._connection._shell._IS_WINDOWS = True
+ assertSuccess(execute=False)
+ assertSuccess(execute=True)
+
+ # But if we're not on windows....we have more work to do.
+ action_base._connection._shell._IS_WINDOWS = False
+
+ # Step 2: We're /not/ becoming an unprivileged user
+ action_base._remote_chmod = MagicMock()
+ action_base._is_become_unprivileged = MagicMock()
+ action_base._is_become_unprivileged.return_value = False
+ # Two subcases:
+ # - _remote_chmod rc is 0
+ # - _remote-chmod rc is not 0, something failed
+ action_base._remote_chmod.return_value = {
+ 'rc': 0,
+ 'stdout': 'some stuff here',
+ 'stderr': '',
+ }
+ assertSuccess(execute=True)
+
+ # When execute=False, we just get the list back. But add it here for
+ # completion. chmod is never called.
+ assertSuccess()
+
+ action_base._remote_chmod.return_value = {
+ 'rc': 1,
+ 'stdout': 'some stuff here',
+ 'stderr': 'and here',
+ }
+ assertThrowRegex(
+ 'Failed to set execute bit on remote files',
+ execute=True)
+
+ # Step 3: we are becoming unprivileged
+ action_base._is_become_unprivileged.return_value = True
+
+ # Step 3a: setfacl
+ action_base._remote_set_user_facl = MagicMock()
+ action_base._remote_set_user_facl.return_value = {
+ 'rc': 0,
+ 'stdout': '',
+ 'stderr': '',
+ }
+ assertSuccess()
+
+ # Step 3b: chmod +x if we need to
+ # To get here, setfacl failed, so mock it as such.
+ action_base._remote_set_user_facl.return_value = {
+ 'rc': 1,
+ 'stdout': '',
+ 'stderr': '',
+ }
+ action_base._remote_chmod.return_value = {
+ 'rc': 1,
+ 'stdout': 'some stuff here',
+ 'stderr': '',
+ }
+ assertThrowRegex(
+ 'Failed to set file mode or acl on remote temporary files',
+ execute=True)
+ action_base._remote_chmod.return_value = {
+ 'rc': 0,
+ 'stdout': 'some stuff here',
+ 'stderr': '',
+ }
+ assertSuccess(execute=True)
+
+ # Step 3c: chown
+ action_base._remote_chown = MagicMock()
+ action_base._remote_chown.return_value = {
+ 'rc': 0,
+ 'stdout': '',
+ 'stderr': '',
+ }
+ assertSuccess()
+ action_base._remote_chown.return_value = {
+ 'rc': 1,
+ 'stdout': '',
+ 'stderr': '',
+ }
+ remote_user = 'root'
+ action_base._get_admin_users = MagicMock()
+ action_base._get_admin_users.return_value = ['root']
+ assertThrowRegex('user would be unable to read the file.')
+ remote_user = 'remoteuser1'
+
+ # Step 3d: chmod +a on osx
+ assertSuccess()
+ action_base._remote_chmod.assert_called_with(
+ ['remoteuser2 allow read'] + remote_paths,
+ '+a')
+
+ # This case can cause Solaris chmod to return 5 which the ssh plugin
+ # treats as failure. To prevent a regression and ensure we still try the
+ # rest of the cases below, we mock the thrown exception here.
+ # This function ensures that only the macOS case (+a) throws this.
+ def raise_if_plus_a(definitely_not_underscore, mode):
+ if mode == '+a':
+ raise AnsibleAuthenticationFailure()
+ return {'rc': 0, 'stdout': '', 'stderr': ''}
+
+ action_base._remote_chmod.side_effect = raise_if_plus_a
+ assertSuccess()
+
+ # Step 3e: chmod A+ on Solaris
+ # We threw AnsibleAuthenticationFailure above, try Solaris fallback.
+ # Based on our lambda above, it should be successful.
+ action_base._remote_chmod.assert_called_with(
+ remote_paths,
+ 'A+user:remoteuser2:r:allow')
+ assertSuccess()
+
+ # Step 3f: Common group
+ def rc_1_if_chmod_acl(definitely_not_underscore, mode):
+ rc = 0
+ if mode in CHMOD_ACL_FLAGS:
+ rc = 1
+ return {'rc': rc, 'stdout': '', 'stderr': ''}
+
+ action_base._remote_chmod = MagicMock()
+ action_base._remote_chmod.side_effect = rc_1_if_chmod_acl
+
+ get_shell_option = action_base.get_shell_option
+ action_base.get_shell_option = MagicMock()
+ action_base.get_shell_option.side_effect = get_shell_option_for_arg(
+ {
+ 'common_remote_group': 'commongroup',
+ },
+ None)
+ action_base._remote_chgrp = MagicMock()
+ action_base._remote_chgrp.return_value = {
+ 'rc': 0,
+ 'stdout': '',
+ 'stderr': '',
+ }
+ # TODO: Add test to assert warning is shown if
+ # world_readable_temp is set in this case.
+ assertSuccess()
+ action_base._remote_chgrp.assert_called_once_with(
+ remote_paths,
+ 'commongroup')
+
+ # Step 4: world-readable tmpdir
+ action_base.get_shell_option.side_effect = get_shell_option_for_arg(
+ {
+ 'world_readable_temp': True,
+ 'common_remote_group': None,
+ },
+ None)
+ action_base._remote_chmod.return_value = {
+ 'rc': 0,
+ 'stdout': 'some stuff here',
+ 'stderr': '',
+ }
+ assertSuccess()
+ action_base._remote_chmod = MagicMock()
+ action_base._remote_chmod.return_value = {
+ 'rc': 1,
+ 'stdout': 'some stuff here',
+ 'stderr': '',
+ }
+ assertThrowRegex('Failed to set file mode on remote files')
+
+ # Otherwise if we make it here in this state, we hit the catch-all
+ action_base.get_shell_option.side_effect = get_shell_option_for_arg(
+ {},
+ None)
+ assertThrowRegex('on the temporary files Ansible needs to create')
+
+ def test_action_base__remove_tmp_path(self):
+ # create our fake task
+ mock_task = MagicMock()
+
+ # create a mock connection, so we don't actually try and connect to things
+ mock_connection = MagicMock()
+ mock_connection._shell.remove.return_value = 'rm some stuff'
+
+ # we're using a real play context here
+ play_context = PlayContext()
+
+ # our test class
+ action_base = DerivedActionBase(
+ task=mock_task,
+ connection=mock_connection,
+ play_context=play_context,
+ loader=None,
+ templar=None,
+ shared_loader_obj=None,
+ )
+
+ action_base._low_level_execute_command = MagicMock()
+ # these don't really return anything or raise errors, so
+ # we're pretty much calling these for coverage right now
+ action_base._remove_tmp_path('/bad/path/dont/remove')
+ action_base._remove_tmp_path('/good/path/to/ansible-tmp-thing')
+
+ @patch('os.unlink')
+ @patch('os.fdopen')
+ @patch('tempfile.mkstemp')
+ def test_action_base__transfer_data(self, mock_mkstemp, mock_fdopen, mock_unlink):
+ # create our fake task
+ mock_task = MagicMock()
+
+ # create a mock connection, so we don't actually try and connect to things
+ mock_connection = MagicMock()
+ mock_connection.put_file.return_value = None
+
+ # we're using a real play context here
+ play_context = PlayContext()
+
+ # our test class
+ action_base = DerivedActionBase(
+ task=mock_task,
+ connection=mock_connection,
+ play_context=play_context,
+ loader=None,
+ templar=None,
+ shared_loader_obj=None,
+ )
+
+ mock_afd = MagicMock()
+ mock_afile = MagicMock()
+ mock_mkstemp.return_value = (mock_afd, mock_afile)
+
+ mock_unlink.return_value = None
+
+ mock_afo = MagicMock()
+ mock_afo.write.return_value = None
+ mock_afo.flush.return_value = None
+ mock_afo.close.return_value = None
+ mock_fdopen.return_value = mock_afo
+
+ self.assertEqual(action_base._transfer_data('/path/to/remote/file', 'some data'), '/path/to/remote/file')
+ self.assertEqual(action_base._transfer_data('/path/to/remote/file', 'some mixed data: fö〩'), '/path/to/remote/file')
+ self.assertEqual(action_base._transfer_data('/path/to/remote/file', dict(some_key='some value')), '/path/to/remote/file')
+ self.assertEqual(action_base._transfer_data('/path/to/remote/file', dict(some_key='fö〩')), '/path/to/remote/file')
+
+ mock_afo.write.side_effect = Exception()
+ self.assertRaises(AnsibleError, action_base._transfer_data, '/path/to/remote/file', '')
+
+ def test_action_base__execute_remote_stat(self):
+ # create our fake task
+ mock_task = MagicMock()
+
+ # create a mock connection, so we don't actually try and connect to things
+ mock_connection = MagicMock()
+
+ # we're using a real play context here
+ play_context = PlayContext()
+
+ # our test class
+ action_base = DerivedActionBase(
+ task=mock_task,
+ connection=mock_connection,
+ play_context=play_context,
+ loader=None,
+ templar=None,
+ shared_loader_obj=None,
+ )
+
+ action_base._execute_module = MagicMock()
+
+ # test normal case
+ action_base._execute_module.return_value = dict(stat=dict(checksum='1111111111111111111111111111111111', exists=True))
+ res = action_base._execute_remote_stat(path='/path/to/file', all_vars=dict(), follow=False)
+ self.assertEqual(res['checksum'], '1111111111111111111111111111111111')
+
+ # test does not exist
+ action_base._execute_module.return_value = dict(stat=dict(exists=False))
+ res = action_base._execute_remote_stat(path='/path/to/file', all_vars=dict(), follow=False)
+ self.assertFalse(res['exists'])
+ self.assertEqual(res['checksum'], '1')
+
+ # test no checksum in result from _execute_module
+ action_base._execute_module.return_value = dict(stat=dict(exists=True))
+ res = action_base._execute_remote_stat(path='/path/to/file', all_vars=dict(), follow=False)
+ self.assertTrue(res['exists'])
+ self.assertEqual(res['checksum'], '')
+
+ # test stat call failed
+ action_base._execute_module.return_value = dict(failed=True, msg="because I said so")
+ self.assertRaises(AnsibleError, action_base._execute_remote_stat, path='/path/to/file', all_vars=dict(), follow=False)
+
+ def test_action_base__execute_module(self):
+ # create our fake task
+ mock_task = MagicMock()
+ mock_task.action = 'copy'
+ mock_task.args = dict(a=1, b=2, c=3)
+ mock_task.diff = False
+ mock_task.check_mode = False
+ mock_task.no_log = False
+
+ # create a mock connection, so we don't actually try and connect to things
+ def build_module_command(env_string, shebang, cmd, arg_path=None):
+ to_run = [env_string, cmd]
+ if arg_path:
+ to_run.append(arg_path)
+ return " ".join(to_run)
+
+ def get_option(option):
+ return {'admin_users': ['root', 'toor']}.get(option)
+
+ mock_connection = MagicMock()
+ mock_connection.build_module_command.side_effect = build_module_command
+ mock_connection.socket_path = None
+ mock_connection._shell.get_remote_filename.return_value = 'copy.py'
+ mock_connection._shell.join_path.side_effect = os.path.join
+ mock_connection._shell.tmpdir = '/var/tmp/mytempdir'
+ mock_connection._shell.get_option = get_option
+
+ # we're using a real play context here
+ play_context = PlayContext()
+
+ # our test class
+ action_base = DerivedActionBase(
+ task=mock_task,
+ connection=mock_connection,
+ play_context=play_context,
+ loader=None,
+ templar=None,
+ shared_loader_obj=None,
+ )
+
+ # fake a lot of methods as we test those elsewhere
+ action_base._configure_module = MagicMock()
+ action_base._supports_check_mode = MagicMock()
+ action_base._is_pipelining_enabled = MagicMock()
+ action_base._make_tmp_path = MagicMock()
+ action_base._transfer_data = MagicMock()
+ action_base._compute_environment_string = MagicMock()
+ action_base._low_level_execute_command = MagicMock()
+ action_base._fixup_perms2 = MagicMock()
+
+ action_base._configure_module.return_value = ('new', '#!/usr/bin/python', 'this is the module data', 'path')
+ action_base._is_pipelining_enabled.return_value = False
+ action_base._compute_environment_string.return_value = ''
+ action_base._connection.has_pipelining = False
+ action_base._make_tmp_path.return_value = '/the/tmp/path'
+ action_base._low_level_execute_command.return_value = dict(stdout='{"rc": 0, "stdout": "ok"}')
+ self.assertEqual(action_base._execute_module(module_name=None, module_args=None), dict(_ansible_parsed=True, rc=0, stdout="ok", stdout_lines=['ok']))
+ self.assertEqual(
+ action_base._execute_module(
+ module_name='foo',
+ module_args=dict(z=9, y=8, x=7),
+ task_vars=dict(a=1)
+ ),
+ dict(
+ _ansible_parsed=True,
+ rc=0,
+ stdout="ok",
+ stdout_lines=['ok'],
+ )
+ )
+
+ # test with needing/removing a remote tmp path
+ action_base._configure_module.return_value = ('old', '#!/usr/bin/python', 'this is the module data', 'path')
+ action_base._is_pipelining_enabled.return_value = False
+ action_base._make_tmp_path.return_value = '/the/tmp/path'
+ self.assertEqual(action_base._execute_module(), dict(_ansible_parsed=True, rc=0, stdout="ok", stdout_lines=['ok']))
+
+ action_base._configure_module.return_value = ('non_native_want_json', '#!/usr/bin/python', 'this is the module data', 'path')
+ self.assertEqual(action_base._execute_module(), dict(_ansible_parsed=True, rc=0, stdout="ok", stdout_lines=['ok']))
+
+ play_context.become = True
+ play_context.become_user = 'foo'
+ mock_task.become = True
+ mock_task.become_user = True
+ self.assertEqual(action_base._execute_module(), dict(_ansible_parsed=True, rc=0, stdout="ok", stdout_lines=['ok']))
+
+ # test an invalid shebang return
+ action_base._configure_module.return_value = ('new', '', 'this is the module data', 'path')
+ action_base._is_pipelining_enabled.return_value = False
+ action_base._make_tmp_path.return_value = '/the/tmp/path'
+ self.assertRaises(AnsibleError, action_base._execute_module)
+
+ # test with check mode enabled, once with support for check
+ # mode and once with support disabled to raise an error
+ play_context.check_mode = True
+ mock_task.check_mode = True
+ action_base._configure_module.return_value = ('new', '#!/usr/bin/python', 'this is the module data', 'path')
+ self.assertEqual(action_base._execute_module(), dict(_ansible_parsed=True, rc=0, stdout="ok", stdout_lines=['ok']))
+ action_base._supports_check_mode = False
+ self.assertRaises(AnsibleError, action_base._execute_module)
+
+ def test_action_base_sudo_only_if_user_differs(self):
+ fake_loader = MagicMock()
+ fake_loader.get_basedir.return_value = os.getcwd()
+ play_context = PlayContext()
+
+ action_base = DerivedActionBase(None, None, play_context, fake_loader, None, None)
+ action_base.get_become_option = MagicMock(return_value='root')
+ action_base._get_remote_user = MagicMock(return_value='root')
+
+ action_base._connection = MagicMock(exec_command=MagicMock(return_value=(0, '', '')))
+
+ action_base._connection._shell = shell = MagicMock(append_command=MagicMock(return_value=('JOINED CMD')))
+
+ action_base._connection.become = become = MagicMock()
+ become.build_become_command.return_value = 'foo'
+
+ action_base._low_level_execute_command('ECHO', sudoable=True)
+ become.build_become_command.assert_not_called()
+
+ action_base._get_remote_user.return_value = 'apo'
+ action_base._low_level_execute_command('ECHO', sudoable=True, executable='/bin/csh')
+ become.build_become_command.assert_called_once_with("ECHO", shell)
+
+ become.build_become_command.reset_mock()
+
+ with patch.object(C, 'BECOME_ALLOW_SAME_USER', new=True):
+ action_base._get_remote_user.return_value = 'root'
+ action_base._low_level_execute_command('ECHO SAME', sudoable=True)
+ become.build_become_command.assert_called_once_with("ECHO SAME", shell)
+
+ def test__remote_expand_user_relative_pathing(self):
+ action_base = _action_base()
+ action_base._play_context.remote_addr = 'bar'
+ action_base._connection.get_option.return_value = 'bar'
+ action_base._low_level_execute_command = MagicMock(return_value={'stdout': b'../home/user'})
+ action_base._connection._shell.join_path.return_value = '../home/user/foo'
+ with self.assertRaises(AnsibleError) as cm:
+ action_base._remote_expand_user('~/foo')
+ self.assertEqual(
+ cm.exception.message,
+ "'bar' returned an invalid relative home directory path containing '..'"
+ )
+
+
+class TestActionBaseCleanReturnedData(unittest.TestCase):
+ def test(self):
+
+ fake_loader = DictDataLoader({
+ })
+ mock_module_loader = MagicMock()
+ mock_shared_loader_obj = MagicMock()
+ mock_shared_loader_obj.module_loader = mock_module_loader
+ connection_loader_paths = ['/tmp/asdfadf', '/usr/lib64/whatever',
+ 'dfadfasf',
+ 'foo.py',
+ '.*',
+ # FIXME: a path with parans breaks the regex
+ # '(.*)',
+ '/path/to/ansible/lib/ansible/plugins/connection/custom_connection.py',
+ '/path/to/ansible/lib/ansible/plugins/connection/ssh.py']
+
+ def fake_all(path_only=None):
+ for path in connection_loader_paths:
+ yield path
+
+ mock_connection_loader = MagicMock()
+ mock_connection_loader.all = fake_all
+
+ mock_shared_loader_obj.connection_loader = mock_connection_loader
+ mock_connection = MagicMock()
+ # mock_connection._shell.env_prefix.side_effect = env_prefix
+
+ # action_base = DerivedActionBase(mock_task, mock_connection, play_context, None, None, None)
+ action_base = DerivedActionBase(task=None,
+ connection=mock_connection,
+ play_context=None,
+ loader=fake_loader,
+ templar=None,
+ shared_loader_obj=mock_shared_loader_obj)
+ data = {'ansible_playbook_python': '/usr/bin/python',
+ # 'ansible_rsync_path': '/usr/bin/rsync',
+ 'ansible_python_interpreter': '/usr/bin/python',
+ 'ansible_ssh_some_var': 'whatever',
+ 'ansible_ssh_host_key_somehost': 'some key here',
+ 'some_other_var': 'foo bar'}
+ data = clean_facts(data)
+ self.assertNotIn('ansible_playbook_python', data)
+ self.assertNotIn('ansible_python_interpreter', data)
+ self.assertIn('ansible_ssh_host_key_somehost', data)
+ self.assertIn('some_other_var', data)
+
+
+class TestActionBaseParseReturnedData(unittest.TestCase):
+
+ def test_fail_no_json(self):
+ action_base = _action_base()
+ rc = 0
+ stdout = 'foo\nbar\n'
+ err = 'oopsy'
+ returned_data = {'rc': rc,
+ 'stdout': stdout,
+ 'stdout_lines': stdout.splitlines(),
+ 'stderr': err}
+ res = action_base._parse_returned_data(returned_data)
+ self.assertFalse(res['_ansible_parsed'])
+ self.assertTrue(res['failed'])
+ self.assertEqual(res['module_stderr'], err)
+
+ def test_json_empty(self):
+ action_base = _action_base()
+ rc = 0
+ stdout = '{}\n'
+ err = ''
+ returned_data = {'rc': rc,
+ 'stdout': stdout,
+ 'stdout_lines': stdout.splitlines(),
+ 'stderr': err}
+ res = action_base._parse_returned_data(returned_data)
+ del res['_ansible_parsed'] # we always have _ansible_parsed
+ self.assertEqual(len(res), 0)
+ self.assertFalse(res)
+
+ def test_json_facts(self):
+ action_base = _action_base()
+ rc = 0
+ stdout = '{"ansible_facts": {"foo": "bar", "ansible_blip": "blip_value"}}\n'
+ err = ''
+
+ returned_data = {'rc': rc,
+ 'stdout': stdout,
+ 'stdout_lines': stdout.splitlines(),
+ 'stderr': err}
+ res = action_base._parse_returned_data(returned_data)
+ self.assertTrue(res['ansible_facts'])
+ self.assertIn('ansible_blip', res['ansible_facts'])
+ # TODO: Should this be an AnsibleUnsafe?
+ # self.assertIsInstance(res['ansible_facts'], AnsibleUnsafe)
+
+ def test_json_facts_add_host(self):
+ action_base = _action_base()
+ rc = 0
+ stdout = '''{"ansible_facts": {"foo": "bar", "ansible_blip": "blip_value"},
+ "add_host": {"host_vars": {"some_key": ["whatever the add_host object is"]}
+ }
+ }\n'''
+ err = ''
+
+ returned_data = {'rc': rc,
+ 'stdout': stdout,
+ 'stdout_lines': stdout.splitlines(),
+ 'stderr': err}
+ res = action_base._parse_returned_data(returned_data)
+ self.assertTrue(res['ansible_facts'])
+ self.assertIn('ansible_blip', res['ansible_facts'])
+ self.assertIn('add_host', res)
+ # TODO: Should this be an AnsibleUnsafe?
+ # self.assertIsInstance(res['ansible_facts'], AnsibleUnsafe)
diff --git a/test/units/plugins/action/test_gather_facts.py b/test/units/plugins/action/test_gather_facts.py
new file mode 100644
index 0000000..20225aa
--- /dev/null
+++ b/test/units/plugins/action/test_gather_facts.py
@@ -0,0 +1,98 @@
+# (c) 2016, Saran Ahluwalia <ahlusar.ahluwalia@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from unittest.mock import MagicMock, patch
+
+from ansible import constants as C
+from ansible.playbook.task import Task
+from ansible.plugins.action.gather_facts import ActionModule as GatherFactsAction
+from ansible.template import Templar
+from ansible.executor import module_common
+
+from units.mock.loader import DictDataLoader
+
+
+class TestNetworkFacts(unittest.TestCase):
+ task = MagicMock(Task)
+ play_context = MagicMock()
+ play_context.check_mode = False
+ connection = MagicMock()
+ fake_loader = DictDataLoader({
+ })
+ templar = Templar(loader=fake_loader)
+
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ @patch.object(module_common, '_get_collection_metadata', return_value={})
+ def test_network_gather_facts_smart_facts_module(self, mock_collection_metadata):
+ self.fqcn_task_vars = {'ansible_network_os': 'ios'}
+ self.task.action = 'gather_facts'
+ self.task.async_val = False
+ self.task.args = {}
+
+ plugin = GatherFactsAction(self.task, self.connection, self.play_context, loader=None, templar=self.templar, shared_loader_obj=None)
+ get_module_args = MagicMock()
+ plugin._get_module_args = get_module_args
+ plugin._execute_module = MagicMock()
+
+ res = plugin.run(task_vars=self.fqcn_task_vars)
+
+ # assert the gather_facts config is 'smart'
+ facts_modules = C.config.get_config_value('FACTS_MODULES', variables=self.fqcn_task_vars)
+ self.assertEqual(facts_modules, ['smart'])
+
+ # assert the correct module was found
+ self.assertEqual(get_module_args.call_count, 1)
+
+ self.assertEqual(
+ get_module_args.call_args.args,
+ ('ansible.legacy.ios_facts', {'ansible_network_os': 'ios'},)
+ )
+
+ @patch.object(module_common, '_get_collection_metadata', return_value={})
+ def test_network_gather_facts_smart_facts_module_fqcn(self, mock_collection_metadata):
+ self.fqcn_task_vars = {'ansible_network_os': 'cisco.ios.ios'}
+ self.task.action = 'gather_facts'
+ self.task.async_val = False
+ self.task.args = {}
+
+ plugin = GatherFactsAction(self.task, self.connection, self.play_context, loader=None, templar=self.templar, shared_loader_obj=None)
+ get_module_args = MagicMock()
+ plugin._get_module_args = get_module_args
+ plugin._execute_module = MagicMock()
+
+ res = plugin.run(task_vars=self.fqcn_task_vars)
+
+ # assert the gather_facts config is 'smart'
+ facts_modules = C.config.get_config_value('FACTS_MODULES', variables=self.fqcn_task_vars)
+ self.assertEqual(facts_modules, ['smart'])
+
+ # assert the correct module was found
+ self.assertEqual(get_module_args.call_count, 1)
+
+ self.assertEqual(
+ get_module_args.call_args.args,
+ ('cisco.ios.ios_facts', {'ansible_network_os': 'cisco.ios.ios'},)
+ )
diff --git a/test/units/plugins/action/test_pause.py b/test/units/plugins/action/test_pause.py
new file mode 100644
index 0000000..8ad6db7
--- /dev/null
+++ b/test/units/plugins/action/test_pause.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# 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 curses
+import importlib
+import io
+import pytest
+import sys
+
+from ansible.plugins.action import pause # noqa: F401
+from ansible.module_utils.six import PY2
+
+builtin_import = 'builtins.__import__'
+if PY2:
+ builtin_import = '__builtin__.__import__'
+
+
+def test_pause_curses_tigetstr_none(mocker, monkeypatch):
+ monkeypatch.delitem(sys.modules, 'ansible.plugins.action.pause')
+
+ dunder_import = __import__
+
+ def _import(*args, **kwargs):
+ if args[0] == 'curses':
+ mock_curses = mocker.Mock()
+ mock_curses.setupterm = mocker.Mock(return_value=True)
+ mock_curses.tigetstr = mocker.Mock(return_value=None)
+ return mock_curses
+ else:
+ return dunder_import(*args, **kwargs)
+
+ mocker.patch(builtin_import, _import)
+
+ mod = importlib.import_module('ansible.plugins.action.pause')
+
+ assert mod.HAS_CURSES is True
+ assert mod.MOVE_TO_BOL == b'\r'
+ assert mod.CLEAR_TO_EOL == b'\x1b[K'
+
+
+def test_pause_missing_curses(mocker, monkeypatch):
+ monkeypatch.delitem(sys.modules, 'ansible.plugins.action.pause')
+
+ dunder_import = __import__
+
+ def _import(*args, **kwargs):
+ if args[0] == 'curses':
+ raise ImportError
+ else:
+ return dunder_import(*args, **kwargs)
+
+ mocker.patch(builtin_import, _import)
+
+ mod = importlib.import_module('ansible.plugins.action.pause')
+
+ with pytest.raises(AttributeError):
+ mod.curses
+
+ assert mod.HAS_CURSES is False
+ assert mod.MOVE_TO_BOL == b'\r'
+ assert mod.CLEAR_TO_EOL == b'\x1b[K'
+
+
+@pytest.mark.parametrize('exc', (curses.error, TypeError, io.UnsupportedOperation))
+def test_pause_curses_setupterm_error(mocker, monkeypatch, exc):
+ monkeypatch.delitem(sys.modules, 'ansible.plugins.action.pause')
+
+ dunder_import = __import__
+
+ def _import(*args, **kwargs):
+ if args[0] == 'curses':
+ mock_curses = mocker.Mock()
+ mock_curses.setupterm = mocker.Mock(side_effect=exc)
+ mock_curses.error = curses.error
+ return mock_curses
+ else:
+ return dunder_import(*args, **kwargs)
+
+ mocker.patch(builtin_import, _import)
+
+ mod = importlib.import_module('ansible.plugins.action.pause')
+
+ assert mod.HAS_CURSES is False
+ assert mod.MOVE_TO_BOL == b'\r'
+ assert mod.CLEAR_TO_EOL == b'\x1b[K'
diff --git a/test/units/plugins/action/test_raw.py b/test/units/plugins/action/test_raw.py
new file mode 100644
index 0000000..3348051
--- /dev/null
+++ b/test/units/plugins/action/test_raw.py
@@ -0,0 +1,105 @@
+# (c) 2016, Saran Ahluwalia <ahlusar.ahluwalia@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+from ansible.errors import AnsibleActionFail
+from units.compat import unittest
+from unittest.mock import MagicMock, Mock
+from ansible.plugins.action.raw import ActionModule
+from ansible.playbook.task import Task
+from ansible.plugins.loader import connection_loader
+
+
+class TestCopyResultExclude(unittest.TestCase):
+
+ def setUp(self):
+ self.play_context = Mock()
+ self.play_context.shell = 'sh'
+ self.connection = connection_loader.get('local', self.play_context, os.devnull)
+
+ def tearDown(self):
+ pass
+
+ # The current behavior of the raw aciton in regards to executable is currently in question;
+ # the test_raw_executable_is_not_empty_string verifies the current behavior (whether it is desireed or not.
+ # Please refer to the following for context:
+ # Issue: https://github.com/ansible/ansible/issues/16054
+ # PR: https://github.com/ansible/ansible/pull/16085
+
+ def test_raw_executable_is_not_empty_string(self):
+
+ task = MagicMock(Task)
+ task.async_val = False
+
+ task.args = {'_raw_params': 'Args1'}
+ self.play_context.check_mode = False
+
+ self.mock_am = ActionModule(task, self.connection, self.play_context, loader=None, templar=None, shared_loader_obj=None)
+ self.mock_am._low_level_execute_command = Mock(return_value={})
+ self.mock_am.display = Mock()
+ self.mock_am._admin_users = ['root', 'toor']
+
+ self.mock_am.run()
+ self.mock_am._low_level_execute_command.assert_called_with('Args1', executable=False)
+
+ def test_raw_check_mode_is_True(self):
+
+ task = MagicMock(Task)
+ task.async_val = False
+
+ task.args = {'_raw_params': 'Args1'}
+ self.play_context.check_mode = True
+
+ try:
+ self.mock_am = ActionModule(task, self.connection, self.play_context, loader=None, templar=None, shared_loader_obj=None)
+ except AnsibleActionFail:
+ pass
+
+ def test_raw_test_environment_is_None(self):
+
+ task = MagicMock(Task)
+ task.async_val = False
+
+ task.args = {'_raw_params': 'Args1'}
+ task.environment = None
+ self.play_context.check_mode = False
+
+ self.mock_am = ActionModule(task, self.connection, self.play_context, loader=None, templar=None, shared_loader_obj=None)
+ self.mock_am._low_level_execute_command = Mock(return_value={})
+ self.mock_am.display = Mock()
+
+ self.assertEqual(task.environment, None)
+
+ def test_raw_task_vars_is_not_None(self):
+
+ task = MagicMock(Task)
+ task.async_val = False
+
+ task.args = {'_raw_params': 'Args1'}
+ task.environment = None
+ self.play_context.check_mode = False
+
+ self.mock_am = ActionModule(task, self.connection, self.play_context, loader=None, templar=None, shared_loader_obj=None)
+ self.mock_am._low_level_execute_command = Mock(return_value={})
+ self.mock_am.display = Mock()
+
+ self.mock_am.run(task_vars={'a': 'b'})
+ self.assertEqual(task.environment, None)
diff --git a/test/units/plugins/become/__init__.py b/test/units/plugins/become/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/become/__init__.py
diff --git a/test/units/plugins/become/conftest.py b/test/units/plugins/become/conftest.py
new file mode 100644
index 0000000..a04a5e2
--- /dev/null
+++ b/test/units/plugins/become/conftest.py
@@ -0,0 +1,37 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (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
+
+import pytest
+
+from ansible.cli.arguments import option_helpers as opt_help
+from ansible.utils import context_objects as co
+
+
+@pytest.fixture
+def parser():
+ parser = opt_help.create_base_parser('testparser')
+
+ opt_help.add_runas_options(parser)
+ opt_help.add_meta_options(parser)
+ opt_help.add_runtask_options(parser)
+ opt_help.add_vault_options(parser)
+ opt_help.add_async_options(parser)
+ opt_help.add_connect_options(parser)
+ opt_help.add_subset_options(parser)
+ opt_help.add_check_options(parser)
+ opt_help.add_inventory_options(parser)
+
+ return parser
+
+
+@pytest.fixture
+def reset_cli_args():
+ co.GlobalCLIArgs._Singleton__instance = None
+ yield
+ co.GlobalCLIArgs._Singleton__instance = None
diff --git a/test/units/plugins/become/test_su.py b/test/units/plugins/become/test_su.py
new file mode 100644
index 0000000..bf74a4c
--- /dev/null
+++ b/test/units/plugins/become/test_su.py
@@ -0,0 +1,30 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (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
+
+import re
+
+from ansible import context
+from ansible.plugins.loader import become_loader, shell_loader
+
+
+def test_su(mocker, parser, reset_cli_args):
+ options = parser.parse_args([])
+ context._init_global_context(options)
+
+ su = become_loader.get('su')
+ sh = shell_loader.get('sh')
+ sh.executable = "/bin/bash"
+
+ su.set_options(direct={
+ 'become_user': 'foo',
+ 'become_flags': '',
+ })
+
+ cmd = su.build_become_command('/bin/foo', sh)
+ assert re.match(r"""su\s+foo -c '/bin/bash -c '"'"'echo BECOME-SUCCESS-.+?; /bin/foo'"'"''""", cmd)
diff --git a/test/units/plugins/become/test_sudo.py b/test/units/plugins/become/test_sudo.py
new file mode 100644
index 0000000..67eb9a4
--- /dev/null
+++ b/test/units/plugins/become/test_sudo.py
@@ -0,0 +1,67 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (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
+
+import re
+
+from ansible import context
+from ansible.plugins.loader import become_loader, shell_loader
+
+
+def test_sudo(mocker, parser, reset_cli_args):
+ options = parser.parse_args([])
+ context._init_global_context(options)
+
+ sudo = become_loader.get('sudo')
+ sh = shell_loader.get('sh')
+ sh.executable = "/bin/bash"
+
+ sudo.set_options(direct={
+ 'become_user': 'foo',
+ 'become_flags': '-n -s -H',
+ })
+
+ cmd = sudo.build_become_command('/bin/foo', sh)
+
+ assert re.match(r"""sudo\s+-n -s -H\s+-u foo /bin/bash -c 'echo BECOME-SUCCESS-.+? ; /bin/foo'""", cmd), cmd
+
+ sudo.set_options(direct={
+ 'become_user': 'foo',
+ 'become_flags': '-n -s -H',
+ 'become_pass': 'testpass',
+ })
+
+ cmd = sudo.build_become_command('/bin/foo', sh)
+ assert re.match(r"""sudo\s+-s\s-H\s+-p "\[sudo via ansible, key=.+?\] password:" -u foo /bin/bash -c 'echo BECOME-SUCCESS-.+? ; /bin/foo'""", cmd), cmd
+
+ sudo.set_options(direct={
+ 'become_user': 'foo',
+ 'become_flags': '-snH',
+ 'become_pass': 'testpass',
+ })
+
+ cmd = sudo.build_become_command('/bin/foo', sh)
+ assert re.match(r"""sudo\s+-sH\s+-p "\[sudo via ansible, key=.+?\] password:" -u foo /bin/bash -c 'echo BECOME-SUCCESS-.+? ; /bin/foo'""", cmd), cmd
+
+ sudo.set_options(direct={
+ 'become_user': 'foo',
+ 'become_flags': '--non-interactive -s -H',
+ 'become_pass': 'testpass',
+ })
+
+ cmd = sudo.build_become_command('/bin/foo', sh)
+ assert re.match(r"""sudo\s+-s\s-H\s+-p "\[sudo via ansible, key=.+?\] password:" -u foo /bin/bash -c 'echo BECOME-SUCCESS-.+? ; /bin/foo'""", cmd), cmd
+
+ sudo.set_options(direct={
+ 'become_user': 'foo',
+ 'become_flags': '--non-interactive -nC5 -s -H',
+ 'become_pass': 'testpass',
+ })
+
+ cmd = sudo.build_become_command('/bin/foo', sh)
+ assert re.match(r"""sudo\s+-C5\s-s\s-H\s+-p "\[sudo via ansible, key=.+?\] password:" -u foo /bin/bash -c 'echo BECOME-SUCCESS-.+? ; /bin/foo'""", cmd), cmd
diff --git a/test/units/plugins/cache/__init__.py b/test/units/plugins/cache/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/cache/__init__.py
diff --git a/test/units/plugins/cache/test_cache.py b/test/units/plugins/cache/test_cache.py
new file mode 100644
index 0000000..25b84c0
--- /dev/null
+++ b/test/units/plugins/cache/test_cache.py
@@ -0,0 +1,199 @@
+# (c) 2012-2015, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import shutil
+import tempfile
+
+from unittest import mock
+
+from units.compat import unittest
+from ansible.errors import AnsibleError
+from ansible.plugins.cache import CachePluginAdjudicator
+from ansible.plugins.cache.memory import CacheModule as MemoryCache
+from ansible.plugins.loader import cache_loader
+from ansible.vars.fact_cache import FactCache
+
+import pytest
+
+
+class TestCachePluginAdjudicator(unittest.TestCase):
+ def setUp(self):
+ # memory plugin cache
+ self.cache = CachePluginAdjudicator()
+ self.cache['cache_key'] = {'key1': 'value1', 'key2': 'value2'}
+ self.cache['cache_key_2'] = {'key': 'value'}
+
+ def test___setitem__(self):
+ self.cache['new_cache_key'] = {'new_key1': ['new_value1', 'new_value2']}
+ assert self.cache['new_cache_key'] == {'new_key1': ['new_value1', 'new_value2']}
+
+ def test_inner___setitem__(self):
+ self.cache['new_cache_key'] = {'new_key1': ['new_value1', 'new_value2']}
+ self.cache['new_cache_key']['new_key1'][0] = 'updated_value1'
+ assert self.cache['new_cache_key'] == {'new_key1': ['updated_value1', 'new_value2']}
+
+ def test___contains__(self):
+ assert 'cache_key' in self.cache
+ assert 'not_cache_key' not in self.cache
+
+ def test_get(self):
+ assert self.cache.get('cache_key') == {'key1': 'value1', 'key2': 'value2'}
+
+ def test_get_with_default(self):
+ assert self.cache.get('foo', 'bar') == 'bar'
+
+ def test_get_without_default(self):
+ assert self.cache.get('foo') is None
+
+ def test___getitem__(self):
+ with pytest.raises(KeyError):
+ self.cache['foo']
+
+ def test_pop_with_default(self):
+ assert self.cache.pop('foo', 'bar') == 'bar'
+
+ def test_pop_without_default(self):
+ with pytest.raises(KeyError):
+ assert self.cache.pop('foo')
+
+ def test_pop(self):
+ v = self.cache.pop('cache_key_2')
+ assert v == {'key': 'value'}
+ assert 'cache_key_2' not in self.cache
+
+ def test_update(self):
+ self.cache.update({'cache_key': {'key2': 'updatedvalue'}})
+ assert self.cache['cache_key']['key2'] == 'updatedvalue'
+
+ def test_update_cache_if_changed(self):
+ # Changes are stored in the CachePluginAdjudicator and will be
+ # persisted to the plugin when calling update_cache_if_changed()
+ # The exception is flush which flushes the plugin immediately.
+ assert len(self.cache.keys()) == 2
+ assert len(self.cache._plugin.keys()) == 0
+ self.cache.update_cache_if_changed()
+ assert len(self.cache._plugin.keys()) == 2
+
+ def test_flush(self):
+ # Fake that the cache already has some data in it but the adjudicator
+ # hasn't loaded it in.
+ self.cache._plugin.set('monkey', 'animal')
+ self.cache._plugin.set('wolf', 'animal')
+ self.cache._plugin.set('another wolf', 'another animal')
+
+ # The adjudicator does't know about the new entries
+ assert len(self.cache.keys()) == 2
+ # But the cache itself does
+ assert len(self.cache._plugin.keys()) == 3
+
+ # If we call flush, both the adjudicator and the cache should flush
+ self.cache.flush()
+ assert len(self.cache.keys()) == 0
+ assert len(self.cache._plugin.keys()) == 0
+
+
+class TestJsonFileCache(TestCachePluginAdjudicator):
+ cache_prefix = ''
+
+ def setUp(self):
+ self.cache_dir = tempfile.mkdtemp(prefix='ansible-plugins-cache-')
+ self.cache = CachePluginAdjudicator(
+ plugin_name='jsonfile', _uri=self.cache_dir,
+ _prefix=self.cache_prefix)
+ self.cache['cache_key'] = {'key1': 'value1', 'key2': 'value2'}
+ self.cache['cache_key_2'] = {'key': 'value'}
+
+ def test_keys(self):
+ # A cache without a prefix will consider all files in the cache
+ # directory as valid cache entries.
+ self.cache._plugin._dump(
+ 'no prefix', os.path.join(self.cache_dir, 'no_prefix'))
+ self.cache._plugin._dump(
+ 'special cache', os.path.join(self.cache_dir, 'special_test'))
+
+ # The plugin does not know the CachePluginAdjudicator entries.
+ assert sorted(self.cache._plugin.keys()) == [
+ 'no_prefix', 'special_test']
+
+ assert 'no_prefix' in self.cache
+ assert 'special_test' in self.cache
+ assert 'test' not in self.cache
+ assert self.cache['no_prefix'] == 'no prefix'
+ assert self.cache['special_test'] == 'special cache'
+
+ def tearDown(self):
+ shutil.rmtree(self.cache_dir)
+
+
+class TestJsonFileCachePrefix(TestJsonFileCache):
+ cache_prefix = 'special_'
+
+ def test_keys(self):
+ # For caches with a prefix only files that match the prefix are
+ # considered. The prefix is removed from the key name.
+ self.cache._plugin._dump(
+ 'no prefix', os.path.join(self.cache_dir, 'no_prefix'))
+ self.cache._plugin._dump(
+ 'special cache', os.path.join(self.cache_dir, 'special_test'))
+
+ # The plugin does not know the CachePluginAdjudicator entries.
+ assert sorted(self.cache._plugin.keys()) == ['test']
+
+ assert 'no_prefix' not in self.cache
+ assert 'special_test' not in self.cache
+ assert 'test' in self.cache
+ assert self.cache['test'] == 'special cache'
+
+
+class TestFactCache(unittest.TestCase):
+ def setUp(self):
+ with mock.patch('ansible.constants.CACHE_PLUGIN', 'memory'):
+ self.cache = FactCache()
+
+ def test_copy(self):
+ self.cache['avocado'] = 'fruit'
+ self.cache['daisy'] = 'flower'
+ a_copy = self.cache.copy()
+ self.assertEqual(type(a_copy), dict)
+ self.assertEqual(a_copy, dict(avocado='fruit', daisy='flower'))
+
+ def test_flush(self):
+ self.cache['motorcycle'] = 'vehicle'
+ self.cache['sock'] = 'clothing'
+ self.cache.flush()
+ assert len(self.cache.keys()) == 0
+
+ def test_plugin_load_failure(self):
+ # See https://github.com/ansible/ansible/issues/18751
+ # Note no fact_connection config set, so this will fail
+ with mock.patch('ansible.constants.CACHE_PLUGIN', 'json'):
+ self.assertRaisesRegex(AnsibleError,
+ "Unable to load the facts cache plugin.*json.*",
+ FactCache)
+
+ def test_update(self):
+ self.cache.update({'cache_key': {'key2': 'updatedvalue'}})
+ assert self.cache['cache_key']['key2'] == 'updatedvalue'
+
+
+def test_memory_cachemodule_with_loader():
+ assert isinstance(cache_loader.get('memory'), MemoryCache)
diff --git a/test/units/plugins/callback/__init__.py b/test/units/plugins/callback/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/callback/__init__.py
diff --git a/test/units/plugins/callback/test_callback.py b/test/units/plugins/callback/test_callback.py
new file mode 100644
index 0000000..ccfa465
--- /dev/null
+++ b/test/units/plugins/callback/test_callback.py
@@ -0,0 +1,416 @@
+# (c) 2012-2014, Chris Meyers <chris.meyers.fsu@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import re
+import textwrap
+import types
+
+from units.compat import unittest
+from unittest.mock import MagicMock
+
+from ansible.executor.task_result import TaskResult
+from ansible.inventory.host import Host
+from ansible.plugins.callback import CallbackBase
+
+
+mock_task = MagicMock()
+mock_task.delegate_to = None
+
+
+class TestCallback(unittest.TestCase):
+ # FIXME: This doesn't really test anything...
+ def test_init(self):
+ CallbackBase()
+
+ def test_display(self):
+ display_mock = MagicMock()
+ display_mock.verbosity = 0
+ cb = CallbackBase(display=display_mock)
+ self.assertIs(cb._display, display_mock)
+
+ def test_display_verbose(self):
+ display_mock = MagicMock()
+ display_mock.verbosity = 5
+ cb = CallbackBase(display=display_mock)
+ self.assertIs(cb._display, display_mock)
+
+ def test_host_label(self):
+ result = TaskResult(host=Host('host1'), task=mock_task, return_data={})
+
+ self.assertEqual(CallbackBase.host_label(result), 'host1')
+
+ def test_host_label_delegated(self):
+ mock_task.delegate_to = 'host2'
+ result = TaskResult(
+ host=Host('host1'),
+ task=mock_task,
+ return_data={'_ansible_delegated_vars': {'ansible_host': 'host2'}},
+ )
+ self.assertEqual(CallbackBase.host_label(result), 'host1 -> host2')
+
+ # TODO: import callback module so we can patch callback.cli/callback.C
+
+
+class TestCallbackResults(unittest.TestCase):
+
+ def test_get_item_label(self):
+ cb = CallbackBase()
+ results = {'item': 'some_item'}
+ res = cb._get_item_label(results)
+ self.assertEqual(res, 'some_item')
+
+ def test_get_item_label_no_log(self):
+ cb = CallbackBase()
+ results = {'item': 'some_item', '_ansible_no_log': True}
+ res = cb._get_item_label(results)
+ self.assertEqual(res, "(censored due to no_log)")
+
+ results = {'item': 'some_item', '_ansible_no_log': False}
+ res = cb._get_item_label(results)
+ self.assertEqual(res, "some_item")
+
+ def test_clean_results_debug_task(self):
+ cb = CallbackBase()
+ result = {'item': 'some_item',
+ 'invocation': 'foo --bar whatever [some_json]',
+ 'a': 'a single a in result note letter a is in invocation',
+ 'b': 'a single b in result note letter b is not in invocation',
+ 'changed': True}
+
+ cb._clean_results(result, 'debug')
+
+ # See https://github.com/ansible/ansible/issues/33723
+ self.assertTrue('a' in result)
+ self.assertTrue('b' in result)
+ self.assertFalse('invocation' in result)
+ self.assertFalse('changed' in result)
+
+ def test_clean_results_debug_task_no_invocation(self):
+ cb = CallbackBase()
+ result = {'item': 'some_item',
+ 'a': 'a single a in result note letter a is in invocation',
+ 'b': 'a single b in result note letter b is not in invocation',
+ 'changed': True}
+
+ cb._clean_results(result, 'debug')
+ self.assertTrue('a' in result)
+ self.assertTrue('b' in result)
+ self.assertFalse('changed' in result)
+ self.assertFalse('invocation' in result)
+
+ def test_clean_results_debug_task_empty_results(self):
+ cb = CallbackBase()
+ result = {}
+ cb._clean_results(result, 'debug')
+ self.assertFalse('invocation' in result)
+ self.assertEqual(len(result), 0)
+
+ def test_clean_results(self):
+ cb = CallbackBase()
+ result = {'item': 'some_item',
+ 'invocation': 'foo --bar whatever [some_json]',
+ 'a': 'a single a in result note letter a is in invocation',
+ 'b': 'a single b in result note letter b is not in invocation',
+ 'changed': True}
+
+ expected_result = result.copy()
+ cb._clean_results(result, 'ebug')
+ self.assertEqual(result, expected_result)
+
+
+class TestCallbackDumpResults(object):
+ def test_internal_keys(self):
+ cb = CallbackBase()
+ result = {'item': 'some_item',
+ '_ansible_some_var': 'SENTINEL',
+ 'testing_ansible_out': 'should_be_left_in LEFTIN',
+ 'invocation': 'foo --bar whatever [some_json]',
+ 'some_dict_key': {'a_sub_dict_for_key': 'baz'},
+ 'bad_dict_key': {'_ansible_internal_blah': 'SENTINEL'},
+ 'changed': True}
+ json_out = cb._dump_results(result)
+ assert '"_ansible_' not in json_out
+ assert 'SENTINEL' not in json_out
+ assert 'LEFTIN' in json_out
+
+ def test_exception(self):
+ cb = CallbackBase()
+ result = {'item': 'some_item LEFTIN',
+ 'exception': ['frame1', 'SENTINEL']}
+ json_out = cb._dump_results(result)
+ assert 'SENTINEL' not in json_out
+ assert 'exception' not in json_out
+ assert 'LEFTIN' in json_out
+
+ def test_verbose(self):
+ cb = CallbackBase()
+ result = {'item': 'some_item LEFTIN',
+ '_ansible_verbose_always': 'chicane'}
+ json_out = cb._dump_results(result)
+ assert 'SENTINEL' not in json_out
+ assert 'LEFTIN' in json_out
+
+ def test_diff(self):
+ cb = CallbackBase()
+ result = {'item': 'some_item LEFTIN',
+ 'diff': ['remove stuff', 'added LEFTIN'],
+ '_ansible_verbose_always': 'chicane'}
+ json_out = cb._dump_results(result)
+ assert 'SENTINEL' not in json_out
+ assert 'LEFTIN' in json_out
+
+ def test_mixed_keys(self):
+ cb = CallbackBase()
+ result = {3: 'pi',
+ 'tau': 6}
+ json_out = cb._dump_results(result)
+ round_trip_result = json.loads(json_out)
+ assert len(round_trip_result) == 2
+ assert '3' in round_trip_result
+ assert 'tau' in round_trip_result
+ assert round_trip_result['3'] == 'pi'
+ assert round_trip_result['tau'] == 6
+
+
+class TestCallbackDiff(unittest.TestCase):
+
+ def setUp(self):
+ self.cb = CallbackBase()
+
+ def _strip_color(self, s):
+ return re.sub('\033\\[[^m]*m', '', s)
+
+ def test_difflist(self):
+ # TODO: split into smaller tests?
+ difflist = [{'before': u'preface\nThe Before String\npostscript',
+ 'after': u'preface\nThe After String\npostscript',
+ 'before_header': u'just before',
+ 'after_header': u'just after'
+ },
+ {'before': u'preface\nThe Before String\npostscript',
+ 'after': u'preface\nThe After String\npostscript',
+ },
+ {'src_binary': 'chicane'},
+ {'dst_binary': 'chicanery'},
+ {'dst_larger': 1},
+ {'src_larger': 2},
+ {'prepared': u'what does prepared do?'},
+ {'before_header': u'just before'},
+ {'after_header': u'just after'}]
+
+ res = self.cb._get_diff(difflist)
+
+ self.assertIn(u'Before String', res)
+ self.assertIn(u'After String', res)
+ self.assertIn(u'just before', res)
+ self.assertIn(u'just after', res)
+
+ def test_simple_diff(self):
+ self.assertMultiLineEqual(
+ self._strip_color(self.cb._get_diff({
+ 'before_header': 'somefile.txt',
+ 'after_header': 'generated from template somefile.j2',
+ 'before': 'one\ntwo\nthree\n',
+ 'after': 'one\nthree\nfour\n',
+ })),
+ textwrap.dedent('''\
+ --- before: somefile.txt
+ +++ after: generated from template somefile.j2
+ @@ -1,3 +1,3 @@
+ one
+ -two
+ three
+ +four
+
+ '''))
+
+ def test_new_file(self):
+ self.assertMultiLineEqual(
+ self._strip_color(self.cb._get_diff({
+ 'before_header': 'somefile.txt',
+ 'after_header': 'generated from template somefile.j2',
+ 'before': '',
+ 'after': 'one\ntwo\nthree\n',
+ })),
+ textwrap.dedent('''\
+ --- before: somefile.txt
+ +++ after: generated from template somefile.j2
+ @@ -0,0 +1,3 @@
+ +one
+ +two
+ +three
+
+ '''))
+
+ def test_clear_file(self):
+ self.assertMultiLineEqual(
+ self._strip_color(self.cb._get_diff({
+ 'before_header': 'somefile.txt',
+ 'after_header': 'generated from template somefile.j2',
+ 'before': 'one\ntwo\nthree\n',
+ 'after': '',
+ })),
+ textwrap.dedent('''\
+ --- before: somefile.txt
+ +++ after: generated from template somefile.j2
+ @@ -1,3 +0,0 @@
+ -one
+ -two
+ -three
+
+ '''))
+
+ def test_no_trailing_newline_before(self):
+ self.assertMultiLineEqual(
+ self._strip_color(self.cb._get_diff({
+ 'before_header': 'somefile.txt',
+ 'after_header': 'generated from template somefile.j2',
+ 'before': 'one\ntwo\nthree',
+ 'after': 'one\ntwo\nthree\n',
+ })),
+ textwrap.dedent('''\
+ --- before: somefile.txt
+ +++ after: generated from template somefile.j2
+ @@ -1,3 +1,3 @@
+ one
+ two
+ -three
+ \\ No newline at end of file
+ +three
+
+ '''))
+
+ def test_no_trailing_newline_after(self):
+ self.assertMultiLineEqual(
+ self._strip_color(self.cb._get_diff({
+ 'before_header': 'somefile.txt',
+ 'after_header': 'generated from template somefile.j2',
+ 'before': 'one\ntwo\nthree\n',
+ 'after': 'one\ntwo\nthree',
+ })),
+ textwrap.dedent('''\
+ --- before: somefile.txt
+ +++ after: generated from template somefile.j2
+ @@ -1,3 +1,3 @@
+ one
+ two
+ -three
+ +three
+ \\ No newline at end of file
+
+ '''))
+
+ def test_no_trailing_newline_both(self):
+ self.assertMultiLineEqual(
+ self.cb._get_diff({
+ 'before_header': 'somefile.txt',
+ 'after_header': 'generated from template somefile.j2',
+ 'before': 'one\ntwo\nthree',
+ 'after': 'one\ntwo\nthree',
+ }),
+ '')
+
+ def test_no_trailing_newline_both_with_some_changes(self):
+ self.assertMultiLineEqual(
+ self._strip_color(self.cb._get_diff({
+ 'before_header': 'somefile.txt',
+ 'after_header': 'generated from template somefile.j2',
+ 'before': 'one\ntwo\nthree',
+ 'after': 'one\nfive\nthree',
+ })),
+ textwrap.dedent('''\
+ --- before: somefile.txt
+ +++ after: generated from template somefile.j2
+ @@ -1,3 +1,3 @@
+ one
+ -two
+ +five
+ three
+ \\ No newline at end of file
+
+ '''))
+
+ def test_diff_dicts(self):
+ self.assertMultiLineEqual(
+ self._strip_color(self.cb._get_diff({
+ 'before': dict(one=1, two=2, three=3),
+ 'after': dict(one=1, three=3, four=4),
+ })),
+ textwrap.dedent('''\
+ --- before
+ +++ after
+ @@ -1,5 +1,5 @@
+ {
+ + "four": 4,
+ "one": 1,
+ - "three": 3,
+ - "two": 2
+ + "three": 3
+ }
+
+ '''))
+
+ def test_diff_before_none(self):
+ self.assertMultiLineEqual(
+ self._strip_color(self.cb._get_diff({
+ 'before': None,
+ 'after': 'one line\n',
+ })),
+ textwrap.dedent('''\
+ --- before
+ +++ after
+ @@ -0,0 +1 @@
+ +one line
+
+ '''))
+
+ def test_diff_after_none(self):
+ self.assertMultiLineEqual(
+ self._strip_color(self.cb._get_diff({
+ 'before': 'one line\n',
+ 'after': None,
+ })),
+ textwrap.dedent('''\
+ --- before
+ +++ after
+ @@ -1 +0,0 @@
+ -one line
+
+ '''))
+
+
+class TestCallbackOnMethods(unittest.TestCase):
+ def _find_on_methods(self, callback):
+ cb_dir = dir(callback)
+ method_names = [x for x in cb_dir if '_on_' in x]
+ methods = [getattr(callback, mn) for mn in method_names]
+ return methods
+
+ def test_are_methods(self):
+ cb = CallbackBase()
+ for method in self._find_on_methods(cb):
+ self.assertIsInstance(method, types.MethodType)
+
+ def test_on_any(self):
+ cb = CallbackBase()
+ cb.v2_on_any('whatever', some_keyword='blippy')
+ cb.on_any('whatever', some_keyword='blippy')
diff --git a/test/units/plugins/connection/__init__.py b/test/units/plugins/connection/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/connection/__init__.py
diff --git a/test/units/plugins/connection/test_connection.py b/test/units/plugins/connection/test_connection.py
new file mode 100644
index 0000000..38d6691
--- /dev/null
+++ b/test/units/plugins/connection/test_connection.py
@@ -0,0 +1,163 @@
+# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from io import StringIO
+
+from units.compat import unittest
+from ansible.playbook.play_context import PlayContext
+from ansible.plugins.connection import ConnectionBase
+from ansible.plugins.loader import become_loader
+
+
+class TestConnectionBaseClass(unittest.TestCase):
+
+ def setUp(self):
+ self.play_context = PlayContext()
+ self.play_context.prompt = (
+ '[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: '
+ )
+ self.in_stream = StringIO()
+
+ def tearDown(self):
+ pass
+
+ def test_subclass_error(self):
+ class ConnectionModule1(ConnectionBase):
+ pass
+ with self.assertRaises(TypeError):
+ ConnectionModule1() # pylint: disable=abstract-class-instantiated
+
+ class ConnectionModule2(ConnectionBase):
+ def get(self, key):
+ super(ConnectionModule2, self).get(key)
+
+ with self.assertRaises(TypeError):
+ ConnectionModule2() # pylint: disable=abstract-class-instantiated
+
+ def test_subclass_success(self):
+ class ConnectionModule3(ConnectionBase):
+
+ @property
+ def transport(self):
+ pass
+
+ def _connect(self):
+ pass
+
+ def exec_command(self):
+ pass
+
+ def put_file(self):
+ pass
+
+ def fetch_file(self):
+ pass
+
+ def close(self):
+ pass
+
+ self.assertIsInstance(ConnectionModule3(self.play_context, self.in_stream), ConnectionModule3)
+
+ def test_check_password_prompt(self):
+ local = (
+ b'[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: \n'
+ b'BECOME-SUCCESS-ouzmdnewuhucvuaabtjmweasarviygqq\n'
+ )
+
+ ssh_pipelining_vvvv = b'''
+debug3: mux_master_read_cb: channel 1 packet type 0x10000002 len 251
+debug2: process_mux_new_session: channel 1: request tty 0, X 1, agent 1, subsys 0, term "xterm-256color", cmd "/bin/sh -c 'sudo -H -S -p "[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: " -u root /bin/sh -c '"'"'echo BECOME-SUCCESS-ouzmdnewuhucvuaabtjmweasarviygqq; /bin/true'"'"' && sleep 0'", env 0
+debug3: process_mux_new_session: got fds stdin 9, stdout 10, stderr 11
+debug2: client_session2_setup: id 2
+debug1: Sending command: /bin/sh -c 'sudo -H -S -p "[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: " -u root /bin/sh -c '"'"'echo BECOME-SUCCESS-ouzmdnewuhucvuaabtjmweasarviygqq; /bin/true'"'"' && sleep 0'
+debug2: channel 2: request exec confirm 1
+debug2: channel 2: rcvd ext data 67
+[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: debug2: channel 2: written 67 to efd 11
+BECOME-SUCCESS-ouzmdnewuhucvuaabtjmweasarviygqq
+debug3: receive packet: type 98
+''' # noqa
+
+ ssh_nopipelining_vvvv = b'''
+debug3: mux_master_read_cb: channel 1 packet type 0x10000002 len 251
+debug2: process_mux_new_session: channel 1: request tty 1, X 1, agent 1, subsys 0, term "xterm-256color", cmd "/bin/sh -c 'sudo -H -S -p "[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: " -u root /bin/sh -c '"'"'echo BECOME-SUCCESS-ouzmdnewuhucvuaabtjmweasarviygqq; /bin/true'"'"' && sleep 0'", env 0
+debug3: mux_client_request_session: session request sent
+debug3: send packet: type 98
+debug1: Sending command: /bin/sh -c 'sudo -H -S -p "[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: " -u root /bin/sh -c '"'"'echo BECOME-SUCCESS-ouzmdnewuhucvuaabtjmweasarviygqq; /bin/true'"'"' && sleep 0'
+debug2: channel 2: request exec confirm 1
+debug2: exec request accepted on channel 2
+[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: debug3: receive packet: type 2
+debug3: Received SSH2_MSG_IGNORE
+debug3: Received SSH2_MSG_IGNORE
+
+BECOME-SUCCESS-ouzmdnewuhucvuaabtjmweasarviygqq
+debug3: receive packet: type 98
+''' # noqa
+
+ ssh_novvvv = (
+ b'[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: \n'
+ b'BECOME-SUCCESS-ouzmdnewuhucvuaabtjmweasarviygqq\n'
+ )
+
+ dns_issue = (
+ b'timeout waiting for privilege escalation password prompt:\n'
+ b'sudo: sudo: unable to resolve host tcloud014\n'
+ b'[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: \n'
+ b'BECOME-SUCCESS-ouzmdnewuhucvuaabtjmweasarviygqq\n'
+ )
+
+ nothing = b''
+
+ in_front = b'''
+debug1: Sending command: /bin/sh -c 'sudo -H -S -p "[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: " -u root /bin/sh -c '"'"'echo
+'''
+
+ class ConnectionFoo(ConnectionBase):
+
+ @property
+ def transport(self):
+ pass
+
+ def _connect(self):
+ pass
+
+ def exec_command(self):
+ pass
+
+ def put_file(self):
+ pass
+
+ def fetch_file(self):
+ pass
+
+ def close(self):
+ pass
+
+ c = ConnectionFoo(self.play_context, self.in_stream)
+ c.set_become_plugin(become_loader.get('sudo'))
+ c.become.prompt = '[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: '
+
+ self.assertTrue(c.become.check_password_prompt(local))
+ self.assertTrue(c.become.check_password_prompt(ssh_pipelining_vvvv))
+ self.assertTrue(c.become.check_password_prompt(ssh_nopipelining_vvvv))
+ self.assertTrue(c.become.check_password_prompt(ssh_novvvv))
+ self.assertTrue(c.become.check_password_prompt(dns_issue))
+ self.assertFalse(c.become.check_password_prompt(nothing))
+ self.assertFalse(c.become.check_password_prompt(in_front))
diff --git a/test/units/plugins/connection/test_local.py b/test/units/plugins/connection/test_local.py
new file mode 100644
index 0000000..e552585
--- /dev/null
+++ b/test/units/plugins/connection/test_local.py
@@ -0,0 +1,40 @@
+#
+# (c) 2020 Red Hat Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from io import StringIO
+import pytest
+
+from units.compat import unittest
+from ansible.plugins.connection import local
+from ansible.playbook.play_context import PlayContext
+
+
+class TestLocalConnectionClass(unittest.TestCase):
+
+ def test_local_connection_module(self):
+ play_context = PlayContext()
+ play_context.prompt = (
+ '[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: '
+ )
+ in_stream = StringIO()
+
+ self.assertIsInstance(local.Connection(play_context, in_stream), local.Connection)
diff --git a/test/units/plugins/connection/test_paramiko.py b/test/units/plugins/connection/test_paramiko.py
new file mode 100644
index 0000000..dcf3177
--- /dev/null
+++ b/test/units/plugins/connection/test_paramiko.py
@@ -0,0 +1,56 @@
+#
+# (c) 2020 Red Hat Inc.
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from io import StringIO
+import pytest
+
+from ansible.plugins.connection import paramiko_ssh
+from ansible.playbook.play_context import PlayContext
+
+
+@pytest.fixture
+def play_context():
+ play_context = PlayContext()
+ play_context.prompt = (
+ '[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: '
+ )
+
+ return play_context
+
+
+@pytest.fixture()
+def in_stream():
+ return StringIO()
+
+
+def test_paramiko_connection_module(play_context, in_stream):
+ assert isinstance(
+ paramiko_ssh.Connection(play_context, in_stream),
+ paramiko_ssh.Connection)
+
+
+def test_paramiko_connect(play_context, in_stream, mocker):
+ mocker.patch.object(paramiko_ssh.Connection, '_connect_uncached')
+ connection = paramiko_ssh.Connection(play_context, in_stream)._connect()
+
+ assert isinstance(connection, paramiko_ssh.Connection)
+ assert connection._connected is True
diff --git a/test/units/plugins/connection/test_psrp.py b/test/units/plugins/connection/test_psrp.py
new file mode 100644
index 0000000..38052e8
--- /dev/null
+++ b/test/units/plugins/connection/test_psrp.py
@@ -0,0 +1,233 @@
+# -*- coding: utf-8 -*-
+# (c) 2018, Jordan Borean <jborean@redhat.com>
+# 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 pytest
+import sys
+
+from io import StringIO
+from unittest.mock import MagicMock
+
+from ansible.playbook.play_context import PlayContext
+from ansible.plugins.loader import connection_loader
+from ansible.utils.display import Display
+
+
+@pytest.fixture(autouse=True)
+def psrp_connection():
+ """Imports the psrp connection plugin with a mocked pypsrp module for testing"""
+
+ # Take a snapshot of sys.modules before we manipulate it
+ orig_modules = sys.modules.copy()
+ try:
+ fake_pypsrp = MagicMock()
+ fake_pypsrp.FEATURES = [
+ 'wsman_locale',
+ 'wsman_read_timeout',
+ 'wsman_reconnections',
+ ]
+
+ fake_wsman = MagicMock()
+ fake_wsman.AUTH_KWARGS = {
+ "certificate": ["certificate_key_pem", "certificate_pem"],
+ "credssp": ["credssp_auth_mechanism", "credssp_disable_tlsv1_2",
+ "credssp_minimum_version"],
+ "negotiate": ["negotiate_delegate", "negotiate_hostname_override",
+ "negotiate_send_cbt", "negotiate_service"],
+ "mock": ["mock_test1", "mock_test2"],
+ }
+
+ sys.modules["pypsrp"] = fake_pypsrp
+ sys.modules["pypsrp.complex_objects"] = MagicMock()
+ sys.modules["pypsrp.exceptions"] = MagicMock()
+ sys.modules["pypsrp.host"] = MagicMock()
+ sys.modules["pypsrp.powershell"] = MagicMock()
+ sys.modules["pypsrp.shell"] = MagicMock()
+ sys.modules["pypsrp.wsman"] = fake_wsman
+ sys.modules["requests.exceptions"] = MagicMock()
+
+ from ansible.plugins.connection import psrp
+
+ # Take a copy of the original import state vars before we set to an ok import
+ orig_has_psrp = psrp.HAS_PYPSRP
+ orig_psrp_imp_err = psrp.PYPSRP_IMP_ERR
+
+ yield psrp
+
+ psrp.HAS_PYPSRP = orig_has_psrp
+ psrp.PYPSRP_IMP_ERR = orig_psrp_imp_err
+ finally:
+ # Restore sys.modules back to our pre-shenanigans
+ sys.modules = orig_modules
+
+
+class TestConnectionPSRP(object):
+
+ OPTIONS_DATA = (
+ # default options
+ (
+ {'_extras': {}},
+ {
+ '_psrp_auth': 'negotiate',
+ '_psrp_cert_validation': True,
+ '_psrp_configuration_name': 'Microsoft.PowerShell',
+ '_psrp_connection_timeout': 30,
+ '_psrp_message_encryption': 'auto',
+ '_psrp_host': 'inventory_hostname',
+ '_psrp_conn_kwargs': {
+ 'server': 'inventory_hostname',
+ 'port': 5986,
+ 'username': None,
+ 'password': None,
+ 'ssl': True,
+ 'path': 'wsman',
+ 'auth': 'negotiate',
+ 'cert_validation': True,
+ 'connection_timeout': 30,
+ 'encryption': 'auto',
+ 'proxy': None,
+ 'no_proxy': False,
+ 'max_envelope_size': 153600,
+ 'operation_timeout': 20,
+ 'certificate_key_pem': None,
+ 'certificate_pem': None,
+ 'credssp_auth_mechanism': 'auto',
+ 'credssp_disable_tlsv1_2': False,
+ 'credssp_minimum_version': 2,
+ 'negotiate_delegate': None,
+ 'negotiate_hostname_override': None,
+ 'negotiate_send_cbt': True,
+ 'negotiate_service': 'WSMAN',
+ 'read_timeout': 30,
+ 'reconnection_backoff': 2.0,
+ 'reconnection_retries': 0,
+ },
+ '_psrp_max_envelope_size': 153600,
+ '_psrp_ignore_proxy': False,
+ '_psrp_operation_timeout': 20,
+ '_psrp_pass': None,
+ '_psrp_path': 'wsman',
+ '_psrp_port': 5986,
+ '_psrp_proxy': None,
+ '_psrp_protocol': 'https',
+ '_psrp_user': None
+ },
+ ),
+ # ssl=False when port defined to 5985
+ (
+ {'_extras': {}, 'ansible_port': '5985'},
+ {
+ '_psrp_port': 5985,
+ '_psrp_protocol': 'http'
+ },
+ ),
+ # ssl=True when port defined to not 5985
+ (
+ {'_extras': {}, 'ansible_port': 1234},
+ {
+ '_psrp_port': 1234,
+ '_psrp_protocol': 'https'
+ },
+ ),
+ # port 5986 when ssl=True
+ (
+ {'_extras': {}, 'ansible_psrp_protocol': 'https'},
+ {
+ '_psrp_port': 5986,
+ '_psrp_protocol': 'https'
+ },
+ ),
+ # port 5985 when ssl=False
+ (
+ {'_extras': {}, 'ansible_psrp_protocol': 'http'},
+ {
+ '_psrp_port': 5985,
+ '_psrp_protocol': 'http'
+ },
+ ),
+ # psrp extras
+ (
+ {'_extras': {'ansible_psrp_mock_test1': True}},
+ {
+ '_psrp_conn_kwargs': {
+ 'server': 'inventory_hostname',
+ 'port': 5986,
+ 'username': None,
+ 'password': None,
+ 'ssl': True,
+ 'path': 'wsman',
+ 'auth': 'negotiate',
+ 'cert_validation': True,
+ 'connection_timeout': 30,
+ 'encryption': 'auto',
+ 'proxy': None,
+ 'no_proxy': False,
+ 'max_envelope_size': 153600,
+ 'operation_timeout': 20,
+ 'certificate_key_pem': None,
+ 'certificate_pem': None,
+ 'credssp_auth_mechanism': 'auto',
+ 'credssp_disable_tlsv1_2': False,
+ 'credssp_minimum_version': 2,
+ 'negotiate_delegate': None,
+ 'negotiate_hostname_override': None,
+ 'negotiate_send_cbt': True,
+ 'negotiate_service': 'WSMAN',
+ 'read_timeout': 30,
+ 'reconnection_backoff': 2.0,
+ 'reconnection_retries': 0,
+ 'mock_test1': True
+ },
+ },
+ ),
+ # cert validation through string repr of bool
+ (
+ {'_extras': {}, 'ansible_psrp_cert_validation': 'ignore'},
+ {
+ '_psrp_cert_validation': False
+ },
+ ),
+ # cert validation path
+ (
+ {'_extras': {}, 'ansible_psrp_cert_trust_path': '/path/cert.pem'},
+ {
+ '_psrp_cert_validation': '/path/cert.pem'
+ },
+ ),
+ )
+
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ # pylint: disable=undefined-variable
+ @pytest.mark.parametrize('options, expected',
+ ((o, e) for o, e in OPTIONS_DATA))
+ def test_set_options(self, options, expected):
+ pc = PlayContext()
+ new_stdin = StringIO()
+
+ conn = connection_loader.get('psrp', pc, new_stdin)
+ conn.set_options(var_options=options)
+ conn._build_kwargs()
+
+ for attr, expected in expected.items():
+ actual = getattr(conn, attr)
+ assert actual == expected, \
+ "psrp attr '%s', actual '%s' != expected '%s'"\
+ % (attr, actual, expected)
+
+ def test_set_invalid_extras_options(self, monkeypatch):
+ pc = PlayContext()
+ new_stdin = StringIO()
+
+ conn = connection_loader.get('psrp', pc, new_stdin)
+ conn.set_options(var_options={'_extras': {'ansible_psrp_mock_test3': True}})
+
+ mock_display = MagicMock()
+ monkeypatch.setattr(Display, "warning", mock_display)
+ conn._build_kwargs()
+
+ assert mock_display.call_args[0][0] == \
+ 'ansible_psrp_mock_test3 is unsupported by the current psrp version installed'
diff --git a/test/units/plugins/connection/test_ssh.py b/test/units/plugins/connection/test_ssh.py
new file mode 100644
index 0000000..662dff9
--- /dev/null
+++ b/test/units/plugins/connection/test_ssh.py
@@ -0,0 +1,696 @@
+# -*- coding: utf-8 -*-
+# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from io import StringIO
+import pytest
+
+
+from ansible import constants as C
+from ansible.errors import AnsibleAuthenticationFailure
+from units.compat import unittest
+from unittest.mock import patch, MagicMock, PropertyMock
+from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
+from ansible.module_utils.compat.selectors import SelectorKey, EVENT_READ
+from ansible.module_utils.six.moves import shlex_quote
+from ansible.module_utils._text import to_bytes
+from ansible.playbook.play_context import PlayContext
+from ansible.plugins.connection import ssh
+from ansible.plugins.loader import connection_loader, become_loader
+
+
+class TestConnectionBaseClass(unittest.TestCase):
+
+ def test_plugins_connection_ssh_module(self):
+ play_context = PlayContext()
+ play_context.prompt = (
+ '[sudo via ansible, key=ouzmdnewuhucvuaabtjmweasarviygqq] password: '
+ )
+ in_stream = StringIO()
+
+ self.assertIsInstance(ssh.Connection(play_context, in_stream), ssh.Connection)
+
+ def test_plugins_connection_ssh_basic(self):
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = ssh.Connection(pc, new_stdin)
+
+ # connect just returns self, so assert that
+ res = conn._connect()
+ self.assertEqual(conn, res)
+
+ ssh.SSHPASS_AVAILABLE = False
+ self.assertFalse(conn._sshpass_available())
+
+ ssh.SSHPASS_AVAILABLE = True
+ self.assertTrue(conn._sshpass_available())
+
+ with patch('subprocess.Popen') as p:
+ ssh.SSHPASS_AVAILABLE = None
+ p.return_value = MagicMock()
+ self.assertTrue(conn._sshpass_available())
+
+ ssh.SSHPASS_AVAILABLE = None
+ p.return_value = None
+ p.side_effect = OSError()
+ self.assertFalse(conn._sshpass_available())
+
+ conn.close()
+ self.assertFalse(conn._connected)
+
+ def test_plugins_connection_ssh__build_command(self):
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('ssh', pc, new_stdin)
+ conn.get_option = MagicMock()
+ conn.get_option.return_value = ""
+ conn._build_command('ssh', 'ssh')
+
+ def test_plugins_connection_ssh_exec_command(self):
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('ssh', pc, new_stdin)
+
+ conn._build_command = MagicMock()
+ conn._build_command.return_value = 'ssh something something'
+ conn._run = MagicMock()
+ conn._run.return_value = (0, 'stdout', 'stderr')
+ conn.get_option = MagicMock()
+ conn.get_option.return_value = True
+
+ res, stdout, stderr = conn.exec_command('ssh')
+ res, stdout, stderr = conn.exec_command('ssh', 'this is some data')
+
+ def test_plugins_connection_ssh__examine_output(self):
+ pc = PlayContext()
+ new_stdin = StringIO()
+ become_success_token = b'BECOME-SUCCESS-abcdefghijklmnopqrstuvxyz'
+
+ conn = connection_loader.get('ssh', pc, new_stdin)
+ conn.set_become_plugin(become_loader.get('sudo'))
+
+ conn.become.check_password_prompt = MagicMock()
+ conn.become.check_success = MagicMock()
+ conn.become.check_incorrect_password = MagicMock()
+ conn.become.check_missing_password = MagicMock()
+
+ def _check_password_prompt(line):
+ return b'foo' in line
+
+ def _check_become_success(line):
+ return become_success_token in line
+
+ def _check_incorrect_password(line):
+ return b'incorrect password' in line
+
+ def _check_missing_password(line):
+ return b'bad password' in line
+
+ # test examining output for prompt
+ conn._flags = dict(
+ become_prompt=False,
+ become_success=False,
+ become_error=False,
+ become_nopasswd_error=False,
+ )
+
+ pc.prompt = True
+
+ # override become plugin
+ conn.become.prompt = True
+ conn.become.check_password_prompt = MagicMock(side_effect=_check_password_prompt)
+ conn.become.check_success = MagicMock(side_effect=_check_become_success)
+ conn.become.check_incorrect_password = MagicMock(side_effect=_check_incorrect_password)
+ conn.become.check_missing_password = MagicMock(side_effect=_check_missing_password)
+
+ def get_option(option):
+ if option == 'become_pass':
+ return 'password'
+ return None
+
+ conn.become.get_option = get_option
+ output, unprocessed = conn._examine_output(u'source', u'state', b'line 1\nline 2\nfoo\nline 3\nthis should be the remainder', False)
+ self.assertEqual(output, b'line 1\nline 2\nline 3\n')
+ self.assertEqual(unprocessed, b'this should be the remainder')
+ self.assertTrue(conn._flags['become_prompt'])
+ self.assertFalse(conn._flags['become_success'])
+ self.assertFalse(conn._flags['become_error'])
+ self.assertFalse(conn._flags['become_nopasswd_error'])
+
+ # test examining output for become prompt
+ conn._flags = dict(
+ become_prompt=False,
+ become_success=False,
+ become_error=False,
+ become_nopasswd_error=False,
+ )
+
+ pc.prompt = False
+ conn.become.prompt = False
+ pc.success_key = str(become_success_token)
+ conn.become.success = str(become_success_token)
+ output, unprocessed = conn._examine_output(u'source', u'state', b'line 1\nline 2\n%s\nline 3\n' % become_success_token, False)
+ self.assertEqual(output, b'line 1\nline 2\nline 3\n')
+ self.assertEqual(unprocessed, b'')
+ self.assertFalse(conn._flags['become_prompt'])
+ self.assertTrue(conn._flags['become_success'])
+ self.assertFalse(conn._flags['become_error'])
+ self.assertFalse(conn._flags['become_nopasswd_error'])
+
+ # test we dont detect become success from ssh debug: lines
+ conn._flags = dict(
+ become_prompt=False,
+ become_success=False,
+ become_error=False,
+ become_nopasswd_error=False,
+ )
+
+ pc.prompt = False
+ conn.become.prompt = True
+ pc.success_key = str(become_success_token)
+ conn.become.success = str(become_success_token)
+ output, unprocessed = conn._examine_output(u'source', u'state', b'line 1\nline 2\ndebug1: %s\nline 3\n' % become_success_token, False)
+ self.assertEqual(output, b'line 1\nline 2\ndebug1: %s\nline 3\n' % become_success_token)
+ self.assertEqual(unprocessed, b'')
+ self.assertFalse(conn._flags['become_success'])
+
+ # test examining output for become failure
+ conn._flags = dict(
+ become_prompt=False,
+ become_success=False,
+ become_error=False,
+ become_nopasswd_error=False,
+ )
+
+ pc.prompt = False
+ conn.become.prompt = False
+ pc.success_key = None
+ output, unprocessed = conn._examine_output(u'source', u'state', b'line 1\nline 2\nincorrect password\n', True)
+ self.assertEqual(output, b'line 1\nline 2\nincorrect password\n')
+ self.assertEqual(unprocessed, b'')
+ self.assertFalse(conn._flags['become_prompt'])
+ self.assertFalse(conn._flags['become_success'])
+ self.assertTrue(conn._flags['become_error'])
+ self.assertFalse(conn._flags['become_nopasswd_error'])
+
+ # test examining output for missing password
+ conn._flags = dict(
+ become_prompt=False,
+ become_success=False,
+ become_error=False,
+ become_nopasswd_error=False,
+ )
+
+ pc.prompt = False
+ conn.become.prompt = False
+ pc.success_key = None
+ output, unprocessed = conn._examine_output(u'source', u'state', b'line 1\nbad password\n', True)
+ self.assertEqual(output, b'line 1\nbad password\n')
+ self.assertEqual(unprocessed, b'')
+ self.assertFalse(conn._flags['become_prompt'])
+ self.assertFalse(conn._flags['become_success'])
+ self.assertFalse(conn._flags['become_error'])
+ self.assertTrue(conn._flags['become_nopasswd_error'])
+
+ @patch('time.sleep')
+ @patch('os.path.exists')
+ def test_plugins_connection_ssh_put_file(self, mock_ospe, mock_sleep):
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('ssh', pc, new_stdin)
+ conn._build_command = MagicMock()
+ conn._bare_run = MagicMock()
+
+ mock_ospe.return_value = True
+ conn._build_command.return_value = 'some command to run'
+ conn._bare_run.return_value = (0, '', '')
+ conn.host = "some_host"
+
+ conn.set_option('reconnection_retries', 9)
+ conn.set_option('ssh_transfer_method', None) # unless set to None scp_if_ssh is ignored
+
+ # Test with SCP_IF_SSH set to smart
+ # Test when SFTP works
+ conn.set_option('scp_if_ssh', 'smart')
+ expected_in_data = b' '.join((b'put', to_bytes(shlex_quote('/path/to/in/file')), to_bytes(shlex_quote('/path/to/dest/file')))) + b'\n'
+ conn.put_file('/path/to/in/file', '/path/to/dest/file')
+ conn._bare_run.assert_called_with('some command to run', expected_in_data, checkrc=False)
+
+ # Test when SFTP doesn't work but SCP does
+ conn._bare_run.side_effect = [(1, 'stdout', 'some errors'), (0, '', '')]
+ conn.put_file('/path/to/in/file', '/path/to/dest/file')
+ conn._bare_run.assert_called_with('some command to run', None, checkrc=False)
+ conn._bare_run.side_effect = None
+
+ # test with SCP_IF_SSH enabled
+ conn.set_option('scp_if_ssh', True)
+ conn.put_file('/path/to/in/file', '/path/to/dest/file')
+ conn._bare_run.assert_called_with('some command to run', None, checkrc=False)
+
+ conn.put_file(u'/path/to/in/file/with/unicode-fö〩', u'/path/to/dest/file/with/unicode-fö〩')
+ conn._bare_run.assert_called_with('some command to run', None, checkrc=False)
+
+ # test with SCPP_IF_SSH disabled
+ conn.set_option('scp_if_ssh', False)
+ expected_in_data = b' '.join((b'put', to_bytes(shlex_quote('/path/to/in/file')), to_bytes(shlex_quote('/path/to/dest/file')))) + b'\n'
+ conn.put_file('/path/to/in/file', '/path/to/dest/file')
+ conn._bare_run.assert_called_with('some command to run', expected_in_data, checkrc=False)
+
+ expected_in_data = b' '.join((b'put',
+ to_bytes(shlex_quote('/path/to/in/file/with/unicode-fö〩')),
+ to_bytes(shlex_quote('/path/to/dest/file/with/unicode-fö〩')))) + b'\n'
+ conn.put_file(u'/path/to/in/file/with/unicode-fö〩', u'/path/to/dest/file/with/unicode-fö〩')
+ conn._bare_run.assert_called_with('some command to run', expected_in_data, checkrc=False)
+
+ # test that a non-zero rc raises an error
+ conn._bare_run.return_value = (1, 'stdout', 'some errors')
+ self.assertRaises(AnsibleError, conn.put_file, '/path/to/bad/file', '/remote/path/to/file')
+
+ # test that a not-found path raises an error
+ mock_ospe.return_value = False
+ conn._bare_run.return_value = (0, 'stdout', '')
+ self.assertRaises(AnsibleFileNotFound, conn.put_file, '/path/to/bad/file', '/remote/path/to/file')
+
+ @patch('time.sleep')
+ def test_plugins_connection_ssh_fetch_file(self, mock_sleep):
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('ssh', pc, new_stdin)
+ conn._build_command = MagicMock()
+ conn._bare_run = MagicMock()
+ conn._load_name = 'ssh'
+
+ conn._build_command.return_value = 'some command to run'
+ conn._bare_run.return_value = (0, '', '')
+ conn.host = "some_host"
+
+ conn.set_option('reconnection_retries', 9)
+ conn.set_option('ssh_transfer_method', None) # unless set to None scp_if_ssh is ignored
+
+ # Test with SCP_IF_SSH set to smart
+ # Test when SFTP works
+ conn.set_option('scp_if_ssh', 'smart')
+ expected_in_data = b' '.join((b'get', to_bytes(shlex_quote('/path/to/in/file')), to_bytes(shlex_quote('/path/to/dest/file')))) + b'\n'
+ conn.set_options({})
+ conn.fetch_file('/path/to/in/file', '/path/to/dest/file')
+ conn._bare_run.assert_called_with('some command to run', expected_in_data, checkrc=False)
+
+ # Test when SFTP doesn't work but SCP does
+ conn._bare_run.side_effect = [(1, 'stdout', 'some errors'), (0, '', '')]
+ conn.fetch_file('/path/to/in/file', '/path/to/dest/file')
+ conn._bare_run.assert_called_with('some command to run', None, checkrc=False)
+
+ # test with SCP_IF_SSH enabled
+ conn._bare_run.side_effect = None
+ conn.set_option('ssh_transfer_method', None) # unless set to None scp_if_ssh is ignored
+ conn.set_option('scp_if_ssh', 'True')
+ conn.fetch_file('/path/to/in/file', '/path/to/dest/file')
+ conn._bare_run.assert_called_with('some command to run', None, checkrc=False)
+
+ conn.fetch_file(u'/path/to/in/file/with/unicode-fö〩', u'/path/to/dest/file/with/unicode-fö〩')
+ conn._bare_run.assert_called_with('some command to run', None, checkrc=False)
+
+ # test with SCP_IF_SSH disabled
+ conn.set_option('scp_if_ssh', False)
+ expected_in_data = b' '.join((b'get', to_bytes(shlex_quote('/path/to/in/file')), to_bytes(shlex_quote('/path/to/dest/file')))) + b'\n'
+ conn.fetch_file('/path/to/in/file', '/path/to/dest/file')
+ conn._bare_run.assert_called_with('some command to run', expected_in_data, checkrc=False)
+
+ expected_in_data = b' '.join((b'get',
+ to_bytes(shlex_quote('/path/to/in/file/with/unicode-fö〩')),
+ to_bytes(shlex_quote('/path/to/dest/file/with/unicode-fö〩')))) + b'\n'
+ conn.fetch_file(u'/path/to/in/file/with/unicode-fö〩', u'/path/to/dest/file/with/unicode-fö〩')
+ conn._bare_run.assert_called_with('some command to run', expected_in_data, checkrc=False)
+
+ # test that a non-zero rc raises an error
+ conn._bare_run.return_value = (1, 'stdout', 'some errors')
+ self.assertRaises(AnsibleError, conn.fetch_file, '/path/to/bad/file', '/remote/path/to/file')
+
+
+class MockSelector(object):
+ def __init__(self):
+ self.files_watched = 0
+ self.register = MagicMock(side_effect=self._register)
+ self.unregister = MagicMock(side_effect=self._unregister)
+ self.close = MagicMock()
+ self.get_map = MagicMock(side_effect=self._get_map)
+ self.select = MagicMock()
+
+ def _register(self, *args, **kwargs):
+ self.files_watched += 1
+
+ def _unregister(self, *args, **kwargs):
+ self.files_watched -= 1
+
+ def _get_map(self, *args, **kwargs):
+ return self.files_watched
+
+
+@pytest.fixture
+def mock_run_env(request, mocker):
+ pc = PlayContext()
+ new_stdin = StringIO()
+
+ conn = connection_loader.get('ssh', pc, new_stdin)
+ conn.set_become_plugin(become_loader.get('sudo'))
+ conn._send_initial_data = MagicMock()
+ conn._examine_output = MagicMock()
+ conn._terminate_process = MagicMock()
+ conn._load_name = 'ssh'
+ conn.sshpass_pipe = [MagicMock(), MagicMock()]
+
+ request.cls.pc = pc
+ request.cls.conn = conn
+
+ mock_popen_res = MagicMock()
+ mock_popen_res.poll = MagicMock()
+ mock_popen_res.wait = MagicMock()
+ mock_popen_res.stdin = MagicMock()
+ mock_popen_res.stdin.fileno.return_value = 1000
+ mock_popen_res.stdout = MagicMock()
+ mock_popen_res.stdout.fileno.return_value = 1001
+ mock_popen_res.stderr = MagicMock()
+ mock_popen_res.stderr.fileno.return_value = 1002
+ mock_popen_res.returncode = 0
+ request.cls.mock_popen_res = mock_popen_res
+
+ mock_popen = mocker.patch('subprocess.Popen', return_value=mock_popen_res)
+ request.cls.mock_popen = mock_popen
+
+ request.cls.mock_selector = MockSelector()
+ mocker.patch('ansible.module_utils.compat.selectors.DefaultSelector', lambda: request.cls.mock_selector)
+
+ request.cls.mock_openpty = mocker.patch('pty.openpty')
+
+ mocker.patch('fcntl.fcntl')
+ mocker.patch('os.write')
+ mocker.patch('os.close')
+
+
+@pytest.mark.usefixtures('mock_run_env')
+class TestSSHConnectionRun(object):
+ # FIXME:
+ # These tests are little more than a smoketest. Need to enhance them
+ # a bit to check that they're calling the relevant functions and making
+ # complete coverage of the code paths
+ def test_no_escalation(self):
+ self.mock_popen_res.stdout.read.side_effect = [b"my_stdout\n", b"second_line"]
+ self.mock_popen_res.stderr.read.side_effect = [b"my_stderr"]
+ self.mock_selector.select.side_effect = [
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ []]
+ self.mock_selector.get_map.side_effect = lambda: True
+
+ return_code, b_stdout, b_stderr = self.conn._run("ssh", "this is input data")
+ assert return_code == 0
+ assert b_stdout == b'my_stdout\nsecond_line'
+ assert b_stderr == b'my_stderr'
+ assert self.mock_selector.register.called is True
+ assert self.mock_selector.register.call_count == 2
+ assert self.conn._send_initial_data.called is True
+ assert self.conn._send_initial_data.call_count == 1
+ assert self.conn._send_initial_data.call_args[0][1] == 'this is input data'
+
+ def test_with_password(self):
+ # test with a password set to trigger the sshpass write
+ self.pc.password = '12345'
+ self.mock_popen_res.stdout.read.side_effect = [b"some data", b"", b""]
+ self.mock_popen_res.stderr.read.side_effect = [b""]
+ self.mock_selector.select.side_effect = [
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ []]
+ self.mock_selector.get_map.side_effect = lambda: True
+
+ return_code, b_stdout, b_stderr = self.conn._run(["ssh", "is", "a", "cmd"], "this is more data")
+ assert return_code == 0
+ assert b_stdout == b'some data'
+ assert b_stderr == b''
+ assert self.mock_selector.register.called is True
+ assert self.mock_selector.register.call_count == 2
+ assert self.conn._send_initial_data.called is True
+ assert self.conn._send_initial_data.call_count == 1
+ assert self.conn._send_initial_data.call_args[0][1] == 'this is more data'
+
+ def _password_with_prompt_examine_output(self, sourice, state, b_chunk, sudoable):
+ if state == 'awaiting_prompt':
+ self.conn._flags['become_prompt'] = True
+ elif state == 'awaiting_escalation':
+ self.conn._flags['become_success'] = True
+ return (b'', b'')
+
+ def test_password_with_prompt(self):
+ # test with password prompting enabled
+ self.pc.password = None
+ self.conn.become.prompt = b'Password:'
+ self.conn._examine_output.side_effect = self._password_with_prompt_examine_output
+ self.mock_popen_res.stdout.read.side_effect = [b"Password:", b"Success", b""]
+ self.mock_popen_res.stderr.read.side_effect = [b""]
+ self.mock_selector.select.side_effect = [
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ),
+ (SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ []]
+ self.mock_selector.get_map.side_effect = lambda: True
+
+ return_code, b_stdout, b_stderr = self.conn._run("ssh", "this is input data")
+ assert return_code == 0
+ assert b_stdout == b''
+ assert b_stderr == b''
+ assert self.mock_selector.register.called is True
+ assert self.mock_selector.register.call_count == 2
+ assert self.conn._send_initial_data.called is True
+ assert self.conn._send_initial_data.call_count == 1
+ assert self.conn._send_initial_data.call_args[0][1] == 'this is input data'
+
+ def test_password_with_become(self):
+ # test with some become settings
+ self.pc.prompt = b'Password:'
+ self.conn.become.prompt = b'Password:'
+ self.pc.become = True
+ self.pc.success_key = 'BECOME-SUCCESS-abcdefg'
+ self.conn.become._id = 'abcdefg'
+ self.conn._examine_output.side_effect = self._password_with_prompt_examine_output
+ self.mock_popen_res.stdout.read.side_effect = [b"Password:", b"BECOME-SUCCESS-abcdefg", b"abc"]
+ self.mock_popen_res.stderr.read.side_effect = [b"123"]
+ self.mock_selector.select.side_effect = [
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ []]
+ self.mock_selector.get_map.side_effect = lambda: True
+
+ return_code, b_stdout, b_stderr = self.conn._run("ssh", "this is input data")
+ self.mock_popen_res.stdin.flush.assert_called_once_with()
+ assert return_code == 0
+ assert b_stdout == b'abc'
+ assert b_stderr == b'123'
+ assert self.mock_selector.register.called is True
+ assert self.mock_selector.register.call_count == 2
+ assert self.conn._send_initial_data.called is True
+ assert self.conn._send_initial_data.call_count == 1
+ assert self.conn._send_initial_data.call_args[0][1] == 'this is input data'
+
+ def test_pasword_without_data(self):
+ # simulate no data input but Popen using new pty's fails
+ self.mock_popen.return_value = None
+ self.mock_popen.side_effect = [OSError(), self.mock_popen_res]
+
+ # simulate no data input
+ self.mock_openpty.return_value = (98, 99)
+ self.mock_popen_res.stdout.read.side_effect = [b"some data", b"", b""]
+ self.mock_popen_res.stderr.read.side_effect = [b""]
+ self.mock_selector.select.side_effect = [
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ []]
+ self.mock_selector.get_map.side_effect = lambda: True
+
+ return_code, b_stdout, b_stderr = self.conn._run("ssh", "")
+ assert return_code == 0
+ assert b_stdout == b'some data'
+ assert b_stderr == b''
+ assert self.mock_selector.register.called is True
+ assert self.mock_selector.register.call_count == 2
+ assert self.conn._send_initial_data.called is False
+
+
+@pytest.mark.usefixtures('mock_run_env')
+class TestSSHConnectionRetries(object):
+ def test_incorrect_password(self, monkeypatch):
+ self.conn.set_option('host_key_checking', False)
+ self.conn.set_option('reconnection_retries', 5)
+ monkeypatch.setattr('time.sleep', lambda x: None)
+
+ self.mock_popen_res.stdout.read.side_effect = [b'']
+ self.mock_popen_res.stderr.read.side_effect = [b'Permission denied, please try again.\r\n']
+ type(self.mock_popen_res).returncode = PropertyMock(side_effect=[5] * 4)
+
+ self.mock_selector.select.side_effect = [
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ [],
+ ]
+
+ self.mock_selector.get_map.side_effect = lambda: True
+
+ self.conn._build_command = MagicMock()
+ self.conn._build_command.return_value = [b'sshpass', b'-d41', b'ssh', b'-C']
+
+ exception_info = pytest.raises(AnsibleAuthenticationFailure, self.conn.exec_command, 'sshpass', 'some data')
+ assert exception_info.value.message == ('Invalid/incorrect username/password. Skipping remaining 5 retries to prevent account lockout: '
+ 'Permission denied, please try again.')
+ assert self.mock_popen.call_count == 1
+
+ def test_retry_then_success(self, monkeypatch):
+ self.conn.set_option('host_key_checking', False)
+ self.conn.set_option('reconnection_retries', 3)
+
+ monkeypatch.setattr('time.sleep', lambda x: None)
+
+ self.mock_popen_res.stdout.read.side_effect = [b"", b"my_stdout\n", b"second_line"]
+ self.mock_popen_res.stderr.read.side_effect = [b"", b"my_stderr"]
+ type(self.mock_popen_res).returncode = PropertyMock(side_effect=[255] * 3 + [0] * 4)
+
+ self.mock_selector.select.side_effect = [
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ [],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ []
+ ]
+ self.mock_selector.get_map.side_effect = lambda: True
+
+ self.conn._build_command = MagicMock()
+ self.conn._build_command.return_value = 'ssh'
+
+ return_code, b_stdout, b_stderr = self.conn.exec_command('ssh', 'some data')
+ assert return_code == 0
+ assert b_stdout == b'my_stdout\nsecond_line'
+ assert b_stderr == b'my_stderr'
+
+ def test_multiple_failures(self, monkeypatch):
+ self.conn.set_option('host_key_checking', False)
+ self.conn.set_option('reconnection_retries', 9)
+
+ monkeypatch.setattr('time.sleep', lambda x: None)
+
+ self.mock_popen_res.stdout.read.side_effect = [b""] * 10
+ self.mock_popen_res.stderr.read.side_effect = [b""] * 10
+ type(self.mock_popen_res).returncode = PropertyMock(side_effect=[255] * 30)
+
+ self.mock_selector.select.side_effect = [
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ [],
+ ] * 10
+ self.mock_selector.get_map.side_effect = lambda: True
+
+ self.conn._build_command = MagicMock()
+ self.conn._build_command.return_value = 'ssh'
+
+ pytest.raises(AnsibleConnectionFailure, self.conn.exec_command, 'ssh', 'some data')
+ assert self.mock_popen.call_count == 10
+
+ def test_abitrary_exceptions(self, monkeypatch):
+ self.conn.set_option('host_key_checking', False)
+ self.conn.set_option('reconnection_retries', 9)
+
+ monkeypatch.setattr('time.sleep', lambda x: None)
+
+ self.conn._build_command = MagicMock()
+ self.conn._build_command.return_value = 'ssh'
+
+ self.mock_popen.side_effect = [Exception('bad')] * 10
+ pytest.raises(Exception, self.conn.exec_command, 'ssh', 'some data')
+ assert self.mock_popen.call_count == 10
+
+ def test_put_file_retries(self, monkeypatch):
+ self.conn.set_option('host_key_checking', False)
+ self.conn.set_option('reconnection_retries', 3)
+
+ monkeypatch.setattr('time.sleep', lambda x: None)
+ monkeypatch.setattr('ansible.plugins.connection.ssh.os.path.exists', lambda x: True)
+
+ self.mock_popen_res.stdout.read.side_effect = [b"", b"my_stdout\n", b"second_line"]
+ self.mock_popen_res.stderr.read.side_effect = [b"", b"my_stderr"]
+ type(self.mock_popen_res).returncode = PropertyMock(side_effect=[255] * 4 + [0] * 4)
+
+ self.mock_selector.select.side_effect = [
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ [],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ []
+ ]
+ self.mock_selector.get_map.side_effect = lambda: True
+
+ self.conn._build_command = MagicMock()
+ self.conn._build_command.return_value = 'sftp'
+
+ return_code, b_stdout, b_stderr = self.conn.put_file('/path/to/in/file', '/path/to/dest/file')
+ assert return_code == 0
+ assert b_stdout == b"my_stdout\nsecond_line"
+ assert b_stderr == b"my_stderr"
+ assert self.mock_popen.call_count == 2
+
+ def test_fetch_file_retries(self, monkeypatch):
+ self.conn.set_option('host_key_checking', False)
+ self.conn.set_option('reconnection_retries', 3)
+
+ monkeypatch.setattr('time.sleep', lambda x: None)
+ monkeypatch.setattr('ansible.plugins.connection.ssh.os.path.exists', lambda x: True)
+
+ self.mock_popen_res.stdout.read.side_effect = [b"", b"my_stdout\n", b"second_line"]
+ self.mock_popen_res.stderr.read.side_effect = [b"", b"my_stderr"]
+ type(self.mock_popen_res).returncode = PropertyMock(side_effect=[255] * 4 + [0] * 4)
+
+ self.mock_selector.select.side_effect = [
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ [],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stdout, 1001, [EVENT_READ], None), EVENT_READ)],
+ [(SelectorKey(self.mock_popen_res.stderr, 1002, [EVENT_READ], None), EVENT_READ)],
+ []
+ ]
+ self.mock_selector.get_map.side_effect = lambda: True
+
+ self.conn._build_command = MagicMock()
+ self.conn._build_command.return_value = 'sftp'
+
+ return_code, b_stdout, b_stderr = self.conn.fetch_file('/path/to/in/file', '/path/to/dest/file')
+ assert return_code == 0
+ assert b_stdout == b"my_stdout\nsecond_line"
+ assert b_stderr == b"my_stderr"
+ assert self.mock_popen.call_count == 2
diff --git a/test/units/plugins/connection/test_winrm.py b/test/units/plugins/connection/test_winrm.py
new file mode 100644
index 0000000..cb52814
--- /dev/null
+++ b/test/units/plugins/connection/test_winrm.py
@@ -0,0 +1,443 @@
+# -*- coding: utf-8 -*-
+# (c) 2018, Jordan Borean <jborean@redhat.com>
+# 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 pytest
+
+from io import StringIO
+
+from unittest.mock import MagicMock
+from ansible.errors import AnsibleConnectionFailure
+from ansible.module_utils._text import to_bytes
+from ansible.playbook.play_context import PlayContext
+from ansible.plugins.loader import connection_loader
+from ansible.plugins.connection import winrm
+
+pytest.importorskip("winrm")
+
+
+class TestConnectionWinRM(object):
+
+ OPTIONS_DATA = (
+ # default options
+ (
+ {'_extras': {}},
+ {},
+ {
+ '_kerb_managed': False,
+ '_kinit_cmd': 'kinit',
+ '_winrm_connection_timeout': None,
+ '_winrm_host': 'inventory_hostname',
+ '_winrm_kwargs': {'username': None, 'password': None},
+ '_winrm_pass': None,
+ '_winrm_path': '/wsman',
+ '_winrm_port': 5986,
+ '_winrm_scheme': 'https',
+ '_winrm_transport': ['ssl'],
+ '_winrm_user': None
+ },
+ False
+ ),
+ # http through port
+ (
+ {'_extras': {}, 'ansible_port': 5985},
+ {},
+ {
+ '_winrm_kwargs': {'username': None, 'password': None},
+ '_winrm_port': 5985,
+ '_winrm_scheme': 'http',
+ '_winrm_transport': ['plaintext'],
+ },
+ False
+ ),
+ # kerberos user with kerb present
+ (
+ {'_extras': {}, 'ansible_user': 'user@domain.com'},
+ {},
+ {
+ '_kerb_managed': False,
+ '_kinit_cmd': 'kinit',
+ '_winrm_kwargs': {'username': 'user@domain.com',
+ 'password': None},
+ '_winrm_pass': None,
+ '_winrm_transport': ['kerberos', 'ssl'],
+ '_winrm_user': 'user@domain.com'
+ },
+ True
+ ),
+ # kerberos user without kerb present
+ (
+ {'_extras': {}, 'ansible_user': 'user@domain.com'},
+ {},
+ {
+ '_kerb_managed': False,
+ '_kinit_cmd': 'kinit',
+ '_winrm_kwargs': {'username': 'user@domain.com',
+ 'password': None},
+ '_winrm_pass': None,
+ '_winrm_transport': ['ssl'],
+ '_winrm_user': 'user@domain.com'
+ },
+ False
+ ),
+ # kerberos user with managed ticket (implicit)
+ (
+ {'_extras': {}, 'ansible_user': 'user@domain.com'},
+ {'remote_password': 'pass'},
+ {
+ '_kerb_managed': True,
+ '_kinit_cmd': 'kinit',
+ '_winrm_kwargs': {'username': 'user@domain.com',
+ 'password': 'pass'},
+ '_winrm_pass': 'pass',
+ '_winrm_transport': ['kerberos', 'ssl'],
+ '_winrm_user': 'user@domain.com'
+ },
+ True
+ ),
+ # kerb with managed ticket (explicit)
+ (
+ {'_extras': {}, 'ansible_user': 'user@domain.com',
+ 'ansible_winrm_kinit_mode': 'managed'},
+ {'password': 'pass'},
+ {
+ '_kerb_managed': True,
+ },
+ True
+ ),
+ # kerb with unmanaged ticket (explicit))
+ (
+ {'_extras': {}, 'ansible_user': 'user@domain.com',
+ 'ansible_winrm_kinit_mode': 'manual'},
+ {'password': 'pass'},
+ {
+ '_kerb_managed': False,
+ },
+ True
+ ),
+ # transport override (single)
+ (
+ {'_extras': {}, 'ansible_user': 'user@domain.com',
+ 'ansible_winrm_transport': 'ntlm'},
+ {},
+ {
+ '_winrm_kwargs': {'username': 'user@domain.com',
+ 'password': None},
+ '_winrm_pass': None,
+ '_winrm_transport': ['ntlm'],
+ },
+ False
+ ),
+ # transport override (list)
+ (
+ {'_extras': {}, 'ansible_user': 'user@domain.com',
+ 'ansible_winrm_transport': ['ntlm', 'certificate']},
+ {},
+ {
+ '_winrm_kwargs': {'username': 'user@domain.com',
+ 'password': None},
+ '_winrm_pass': None,
+ '_winrm_transport': ['ntlm', 'certificate'],
+ },
+ False
+ ),
+ # winrm extras
+ (
+ {'_extras': {'ansible_winrm_server_cert_validation': 'ignore',
+ 'ansible_winrm_service': 'WSMAN'}},
+ {},
+ {
+ '_winrm_kwargs': {'username': None, 'password': None,
+ 'server_cert_validation': 'ignore',
+ 'service': 'WSMAN'},
+ },
+ False
+ ),
+ # direct override
+ (
+ {'_extras': {}, 'ansible_winrm_connection_timeout': 5},
+ {'connection_timeout': 10},
+ {
+ '_winrm_connection_timeout': 10,
+ },
+ False
+ ),
+ # password as ansible_password
+ (
+ {'_extras': {}, 'ansible_password': 'pass'},
+ {},
+ {
+ '_winrm_pass': 'pass',
+ '_winrm_kwargs': {'username': None, 'password': 'pass'}
+ },
+ False
+ ),
+ # password as ansible_winrm_pass
+ (
+ {'_extras': {}, 'ansible_winrm_pass': 'pass'},
+ {},
+ {
+ '_winrm_pass': 'pass',
+ '_winrm_kwargs': {'username': None, 'password': 'pass'}
+ },
+ False
+ ),
+
+ # password as ansible_winrm_password
+ (
+ {'_extras': {}, 'ansible_winrm_password': 'pass'},
+ {},
+ {
+ '_winrm_pass': 'pass',
+ '_winrm_kwargs': {'username': None, 'password': 'pass'}
+ },
+ False
+ ),
+ )
+
+ # pylint bug: https://github.com/PyCQA/pylint/issues/511
+ # pylint: disable=undefined-variable
+ @pytest.mark.parametrize('options, direct, expected, kerb',
+ ((o, d, e, k) for o, d, e, k in OPTIONS_DATA))
+ def test_set_options(self, options, direct, expected, kerb):
+ winrm.HAVE_KERBEROS = kerb
+
+ pc = PlayContext()
+ new_stdin = StringIO()
+
+ conn = connection_loader.get('winrm', pc, new_stdin)
+ conn.set_options(var_options=options, direct=direct)
+ conn._build_winrm_kwargs()
+
+ for attr, expected in expected.items():
+ actual = getattr(conn, attr)
+ assert actual == expected, \
+ "winrm attr '%s', actual '%s' != expected '%s'"\
+ % (attr, actual, expected)
+
+
+class TestWinRMKerbAuth(object):
+
+ @pytest.mark.parametrize('options, expected', [
+ [{"_extras": {}},
+ (["kinit", "user@domain"],)],
+ [{"_extras": {}, 'ansible_winrm_kinit_cmd': 'kinit2'},
+ (["kinit2", "user@domain"],)],
+ [{"_extras": {'ansible_winrm_kerberos_delegation': True}},
+ (["kinit", "-f", "user@domain"],)],
+ [{"_extras": {}, 'ansible_winrm_kinit_args': '-f -p'},
+ (["kinit", "-f", "-p", "user@domain"],)],
+ [{"_extras": {}, 'ansible_winrm_kerberos_delegation': True, 'ansible_winrm_kinit_args': '-p'},
+ (["kinit", "-p", "user@domain"],)]
+ ])
+ def test_kinit_success_subprocess(self, monkeypatch, options, expected):
+ def mock_communicate(input=None, timeout=None):
+ return b"", b""
+
+ mock_popen = MagicMock()
+ mock_popen.return_value.communicate = mock_communicate
+ mock_popen.return_value.returncode = 0
+ monkeypatch.setattr("subprocess.Popen", mock_popen)
+
+ winrm.HAS_PEXPECT = False
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('winrm', pc, new_stdin)
+ conn.set_options(var_options=options)
+ conn._build_winrm_kwargs()
+
+ conn._kerb_auth("user@domain", "pass")
+ mock_calls = mock_popen.mock_calls
+ assert len(mock_calls) == 1
+ assert mock_calls[0][1] == expected
+ actual_env = mock_calls[0][2]['env']
+ assert sorted(list(actual_env.keys())) == ['KRB5CCNAME', 'PATH']
+ assert actual_env['KRB5CCNAME'].startswith("FILE:/")
+ assert actual_env['PATH'] == os.environ['PATH']
+
+ @pytest.mark.parametrize('options, expected', [
+ [{"_extras": {}},
+ ("kinit", ["user@domain"],)],
+ [{"_extras": {}, 'ansible_winrm_kinit_cmd': 'kinit2'},
+ ("kinit2", ["user@domain"],)],
+ [{"_extras": {'ansible_winrm_kerberos_delegation': True}},
+ ("kinit", ["-f", "user@domain"],)],
+ [{"_extras": {}, 'ansible_winrm_kinit_args': '-f -p'},
+ ("kinit", ["-f", "-p", "user@domain"],)],
+ [{"_extras": {}, 'ansible_winrm_kerberos_delegation': True, 'ansible_winrm_kinit_args': '-p'},
+ ("kinit", ["-p", "user@domain"],)]
+ ])
+ def test_kinit_success_pexpect(self, monkeypatch, options, expected):
+ pytest.importorskip("pexpect")
+ mock_pexpect = MagicMock()
+ mock_pexpect.return_value.exitstatus = 0
+ monkeypatch.setattr("pexpect.spawn", mock_pexpect)
+
+ winrm.HAS_PEXPECT = True
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('winrm', pc, new_stdin)
+ conn.set_options(var_options=options)
+ conn._build_winrm_kwargs()
+
+ conn._kerb_auth("user@domain", "pass")
+ mock_calls = mock_pexpect.mock_calls
+ assert mock_calls[0][1] == expected
+ actual_env = mock_calls[0][2]['env']
+ assert sorted(list(actual_env.keys())) == ['KRB5CCNAME', 'PATH']
+ assert actual_env['KRB5CCNAME'].startswith("FILE:/")
+ assert actual_env['PATH'] == os.environ['PATH']
+ assert mock_calls[0][2]['echo'] is False
+ assert mock_calls[1][0] == "().expect"
+ assert mock_calls[1][1] == (".*:",)
+ assert mock_calls[2][0] == "().sendline"
+ assert mock_calls[2][1] == ("pass",)
+ assert mock_calls[3][0] == "().read"
+ assert mock_calls[4][0] == "().wait"
+
+ def test_kinit_with_missing_executable_subprocess(self, monkeypatch):
+ expected_err = "[Errno 2] No such file or directory: " \
+ "'/fake/kinit': '/fake/kinit'"
+ mock_popen = MagicMock(side_effect=OSError(expected_err))
+
+ monkeypatch.setattr("subprocess.Popen", mock_popen)
+
+ winrm.HAS_PEXPECT = False
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('winrm', pc, new_stdin)
+ options = {"_extras": {}, "ansible_winrm_kinit_cmd": "/fake/kinit"}
+ conn.set_options(var_options=options)
+ conn._build_winrm_kwargs()
+
+ with pytest.raises(AnsibleConnectionFailure) as err:
+ conn._kerb_auth("user@domain", "pass")
+ assert str(err.value) == "Kerberos auth failure when calling " \
+ "kinit cmd '/fake/kinit': %s" % expected_err
+
+ def test_kinit_with_missing_executable_pexpect(self, monkeypatch):
+ pexpect = pytest.importorskip("pexpect")
+
+ expected_err = "The command was not found or was not " \
+ "executable: /fake/kinit"
+ mock_pexpect = \
+ MagicMock(side_effect=pexpect.ExceptionPexpect(expected_err))
+
+ monkeypatch.setattr("pexpect.spawn", mock_pexpect)
+
+ winrm.HAS_PEXPECT = True
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('winrm', pc, new_stdin)
+ options = {"_extras": {}, "ansible_winrm_kinit_cmd": "/fake/kinit"}
+ conn.set_options(var_options=options)
+ conn._build_winrm_kwargs()
+
+ with pytest.raises(AnsibleConnectionFailure) as err:
+ conn._kerb_auth("user@domain", "pass")
+ assert str(err.value) == "Kerberos auth failure when calling " \
+ "kinit cmd '/fake/kinit': %s" % expected_err
+
+ def test_kinit_error_subprocess(self, monkeypatch):
+ expected_err = "kinit: krb5_parse_name: " \
+ "Configuration file does not specify default realm"
+
+ def mock_communicate(input=None, timeout=None):
+ return b"", to_bytes(expected_err)
+
+ mock_popen = MagicMock()
+ mock_popen.return_value.communicate = mock_communicate
+ mock_popen.return_value.returncode = 1
+ monkeypatch.setattr("subprocess.Popen", mock_popen)
+
+ winrm.HAS_PEXPECT = False
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('winrm', pc, new_stdin)
+ conn.set_options(var_options={"_extras": {}})
+ conn._build_winrm_kwargs()
+
+ with pytest.raises(AnsibleConnectionFailure) as err:
+ conn._kerb_auth("invaliduser", "pass")
+
+ assert str(err.value) == \
+ "Kerberos auth failure for principal invaliduser with " \
+ "subprocess: %s" % (expected_err)
+
+ def test_kinit_error_pexpect(self, monkeypatch):
+ pytest.importorskip("pexpect")
+
+ expected_err = "Configuration file does not specify default realm"
+ mock_pexpect = MagicMock()
+ mock_pexpect.return_value.expect = MagicMock(side_effect=OSError)
+ mock_pexpect.return_value.read.return_value = to_bytes(expected_err)
+ mock_pexpect.return_value.exitstatus = 1
+
+ monkeypatch.setattr("pexpect.spawn", mock_pexpect)
+
+ winrm.HAS_PEXPECT = True
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('winrm', pc, new_stdin)
+ conn.set_options(var_options={"_extras": {}})
+ conn._build_winrm_kwargs()
+
+ with pytest.raises(AnsibleConnectionFailure) as err:
+ conn._kerb_auth("invaliduser", "pass")
+
+ assert str(err.value) == \
+ "Kerberos auth failure for principal invaliduser with " \
+ "pexpect: %s" % (expected_err)
+
+ def test_kinit_error_pass_in_output_subprocess(self, monkeypatch):
+ def mock_communicate(input=None, timeout=None):
+ return b"", b"Error with kinit\n" + input
+
+ mock_popen = MagicMock()
+ mock_popen.return_value.communicate = mock_communicate
+ mock_popen.return_value.returncode = 1
+ monkeypatch.setattr("subprocess.Popen", mock_popen)
+
+ winrm.HAS_PEXPECT = False
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('winrm', pc, new_stdin)
+ conn.set_options(var_options={"_extras": {}})
+ conn._build_winrm_kwargs()
+
+ with pytest.raises(AnsibleConnectionFailure) as err:
+ conn._kerb_auth("username", "password")
+ assert str(err.value) == \
+ "Kerberos auth failure for principal username with subprocess: " \
+ "Error with kinit\n<redacted>"
+
+ def test_kinit_error_pass_in_output_pexpect(self, monkeypatch):
+ pytest.importorskip("pexpect")
+
+ mock_pexpect = MagicMock()
+ mock_pexpect.return_value.expect = MagicMock()
+ mock_pexpect.return_value.read.return_value = \
+ b"Error with kinit\npassword\n"
+ mock_pexpect.return_value.exitstatus = 1
+
+ monkeypatch.setattr("pexpect.spawn", mock_pexpect)
+
+ winrm.HAS_PEXPECT = True
+ pc = PlayContext()
+ pc = PlayContext()
+ new_stdin = StringIO()
+ conn = connection_loader.get('winrm', pc, new_stdin)
+ conn.set_options(var_options={"_extras": {}})
+ conn._build_winrm_kwargs()
+
+ with pytest.raises(AnsibleConnectionFailure) as err:
+ conn._kerb_auth("username", "password")
+ assert str(err.value) == \
+ "Kerberos auth failure for principal username with pexpect: " \
+ "Error with kinit\n<redacted>"
diff --git a/test/units/plugins/filter/__init__.py b/test/units/plugins/filter/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/filter/__init__.py
diff --git a/test/units/plugins/filter/test_core.py b/test/units/plugins/filter/test_core.py
new file mode 100644
index 0000000..df4e472
--- /dev/null
+++ b/test/units/plugins/filter/test_core.py
@@ -0,0 +1,43 @@
+# -*- 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
+from jinja2.runtime import Undefined
+from jinja2.exceptions import UndefinedError
+__metaclass__ = type
+
+import pytest
+
+from ansible.module_utils._text import to_native
+from ansible.plugins.filter.core import to_uuid
+from ansible.errors import AnsibleFilterError
+
+
+UUID_DEFAULT_NAMESPACE_TEST_CASES = (
+ ('example.com', 'ae780c3a-a3ab-53c2-bfb4-098da300b3fe'),
+ ('test.example', '8e437a35-c7c5-50ea-867c-5c254848dbc2'),
+ ('café.example', '8a99d6b1-fb8f-5f78-af86-879768589f56'),
+)
+
+UUID_TEST_CASES = (
+ ('361E6D51-FAEC-444A-9079-341386DA8E2E', 'example.com', 'ae780c3a-a3ab-53c2-bfb4-098da300b3fe'),
+ ('361E6D51-FAEC-444A-9079-341386DA8E2E', 'test.example', '8e437a35-c7c5-50ea-867c-5c254848dbc2'),
+ ('11111111-2222-3333-4444-555555555555', 'example.com', 'e776faa5-5299-55dc-9057-7a00e6be2364'),
+)
+
+
+@pytest.mark.parametrize('value, expected', UUID_DEFAULT_NAMESPACE_TEST_CASES)
+def test_to_uuid_default_namespace(value, expected):
+ assert expected == to_uuid(value)
+
+
+@pytest.mark.parametrize('namespace, value, expected', UUID_TEST_CASES)
+def test_to_uuid(namespace, value, expected):
+ assert expected == to_uuid(value, namespace=namespace)
+
+
+def test_to_uuid_invalid_namespace():
+ with pytest.raises(AnsibleFilterError) as e:
+ to_uuid('example.com', namespace='11111111-2222-3333-4444-555555555')
+ assert 'Invalid value' in to_native(e.value)
diff --git a/test/units/plugins/filter/test_mathstuff.py b/test/units/plugins/filter/test_mathstuff.py
new file mode 100644
index 0000000..f793871
--- /dev/null
+++ b/test/units/plugins/filter/test_mathstuff.py
@@ -0,0 +1,162 @@
+# Copyright: (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
+import pytest
+
+from jinja2 import Environment
+
+import ansible.plugins.filter.mathstuff as ms
+from ansible.errors import AnsibleFilterError, AnsibleFilterTypeError
+
+
+UNIQUE_DATA = (([1, 3, 4, 2], [1, 3, 4, 2]),
+ ([1, 3, 2, 4, 2, 3], [1, 3, 2, 4]),
+ (['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd']),
+ (['a', 'a', 'd', 'b', 'a', 'd', 'c', 'b'], ['a', 'd', 'b', 'c']),
+ )
+
+TWO_SETS_DATA = (([1, 2], [3, 4], ([], sorted([1, 2]), sorted([1, 2, 3, 4]), sorted([1, 2, 3, 4]))),
+ ([1, 2, 3], [5, 3, 4], ([3], sorted([1, 2]), sorted([1, 2, 5, 4]), sorted([1, 2, 3, 4, 5]))),
+ (['a', 'b', 'c'], ['d', 'c', 'e'], (['c'], sorted(['a', 'b']), sorted(['a', 'b', 'd', 'e']), sorted(['a', 'b', 'c', 'e', 'd']))),
+ )
+
+env = Environment()
+
+
+@pytest.mark.parametrize('data, expected', UNIQUE_DATA)
+class TestUnique:
+ def test_unhashable(self, data, expected):
+ assert ms.unique(env, list(data)) == expected
+
+ def test_hashable(self, data, expected):
+ assert ms.unique(env, tuple(data)) == expected
+
+
+@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA)
+class TestIntersect:
+ def test_unhashable(self, dataset1, dataset2, expected):
+ assert sorted(ms.intersect(env, list(dataset1), list(dataset2))) == expected[0]
+
+ def test_hashable(self, dataset1, dataset2, expected):
+ assert sorted(ms.intersect(env, tuple(dataset1), tuple(dataset2))) == expected[0]
+
+
+@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA)
+class TestDifference:
+ def test_unhashable(self, dataset1, dataset2, expected):
+ assert sorted(ms.difference(env, list(dataset1), list(dataset2))) == expected[1]
+
+ def test_hashable(self, dataset1, dataset2, expected):
+ assert sorted(ms.difference(env, tuple(dataset1), tuple(dataset2))) == expected[1]
+
+
+@pytest.mark.parametrize('dataset1, dataset2, expected', TWO_SETS_DATA)
+class TestSymmetricDifference:
+ def test_unhashable(self, dataset1, dataset2, expected):
+ assert sorted(ms.symmetric_difference(env, list(dataset1), list(dataset2))) == expected[2]
+
+ def test_hashable(self, dataset1, dataset2, expected):
+ assert sorted(ms.symmetric_difference(env, tuple(dataset1), tuple(dataset2))) == expected[2]
+
+
+class TestLogarithm:
+ def test_log_non_number(self):
+ # Message changed in python3.6
+ with pytest.raises(AnsibleFilterTypeError, match='log\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'):
+ ms.logarithm('a')
+ with pytest.raises(AnsibleFilterTypeError, match='log\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'):
+ ms.logarithm(10, base='a')
+
+ def test_log_ten(self):
+ assert ms.logarithm(10, 10) == 1.0
+ assert ms.logarithm(69, 10) * 1000 // 1 == 1838
+
+ def test_log_natural(self):
+ assert ms.logarithm(69) * 1000 // 1 == 4234
+
+ def test_log_two(self):
+ assert ms.logarithm(69, 2) * 1000 // 1 == 6108
+
+
+class TestPower:
+ def test_power_non_number(self):
+ # Message changed in python3.6
+ with pytest.raises(AnsibleFilterTypeError, match='pow\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'):
+ ms.power('a', 10)
+
+ with pytest.raises(AnsibleFilterTypeError, match='pow\\(\\) can only be used on numbers: (a float is required|must be real number, not str)'):
+ ms.power(10, 'a')
+
+ def test_power_squared(self):
+ assert ms.power(10, 2) == 100
+
+ def test_power_cubed(self):
+ assert ms.power(10, 3) == 1000
+
+
+class TestInversePower:
+ def test_root_non_number(self):
+ # Messages differed in python-2.6, python-2.7-3.5, and python-3.6+
+ with pytest.raises(AnsibleFilterTypeError, match="root\\(\\) can only be used on numbers:"
+ " (invalid literal for float\\(\\): a"
+ "|could not convert string to float: a"
+ "|could not convert string to float: 'a')"):
+ ms.inversepower(10, 'a')
+
+ with pytest.raises(AnsibleFilterTypeError, match="root\\(\\) can only be used on numbers: (a float is required|must be real number, not str)"):
+ ms.inversepower('a', 10)
+
+ def test_square_root(self):
+ assert ms.inversepower(100) == 10
+ assert ms.inversepower(100, 2) == 10
+
+ def test_cube_root(self):
+ assert ms.inversepower(27, 3) == 3
+
+
+class TestRekeyOnMember():
+ # (Input data structure, member to rekey on, expected return)
+ VALID_ENTRIES = (
+ ([{"proto": "eigrp", "state": "enabled"}, {"proto": "ospf", "state": "enabled"}],
+ 'proto',
+ {'eigrp': {'state': 'enabled', 'proto': 'eigrp'}, 'ospf': {'state': 'enabled', 'proto': 'ospf'}}),
+ ({'eigrp': {"proto": "eigrp", "state": "enabled"}, 'ospf': {"proto": "ospf", "state": "enabled"}},
+ 'proto',
+ {'eigrp': {'state': 'enabled', 'proto': 'eigrp'}, 'ospf': {'state': 'enabled', 'proto': 'ospf'}}),
+ )
+
+ # (Input data structure, member to rekey on, expected error message)
+ INVALID_ENTRIES = (
+ # Fail when key is not found
+ (AnsibleFilterError, [{"proto": "eigrp", "state": "enabled"}], 'invalid_key', "Key invalid_key was not found"),
+ (AnsibleFilterError, {"eigrp": {"proto": "eigrp", "state": "enabled"}}, 'invalid_key', "Key invalid_key was not found"),
+ # Fail when key is duplicated
+ (AnsibleFilterError, [{"proto": "eigrp"}, {"proto": "ospf"}, {"proto": "ospf"}],
+ 'proto', 'Key ospf is not unique, cannot correctly turn into dict'),
+ # Fail when value is not a dict
+ (AnsibleFilterTypeError, ["string"], 'proto', "List item is not a valid dict"),
+ (AnsibleFilterTypeError, [123], 'proto', "List item is not a valid dict"),
+ (AnsibleFilterTypeError, [[{'proto': 1}]], 'proto', "List item is not a valid dict"),
+ # Fail when we do not send a dict or list
+ (AnsibleFilterTypeError, "string", 'proto', "Type is not a valid list, set, or dict"),
+ (AnsibleFilterTypeError, 123, 'proto', "Type is not a valid list, set, or dict"),
+ )
+
+ @pytest.mark.parametrize("list_original, key, expected", VALID_ENTRIES)
+ def test_rekey_on_member_success(self, list_original, key, expected):
+ assert ms.rekey_on_member(list_original, key) == expected
+
+ @pytest.mark.parametrize("expected_exception_type, list_original, key, expected", INVALID_ENTRIES)
+ def test_fail_rekey_on_member(self, expected_exception_type, list_original, key, expected):
+ with pytest.raises(expected_exception_type) as err:
+ ms.rekey_on_member(list_original, key)
+
+ assert err.value.message == expected
+
+ def test_duplicate_strategy_overwrite(self):
+ list_original = ({'proto': 'eigrp', 'id': 1}, {'proto': 'ospf', 'id': 2}, {'proto': 'eigrp', 'id': 3})
+ expected = {'eigrp': {'proto': 'eigrp', 'id': 3}, 'ospf': {'proto': 'ospf', 'id': 2}}
+ assert ms.rekey_on_member(list_original, 'proto', duplicates='overwrite') == expected
diff --git a/test/units/plugins/inventory/__init__.py b/test/units/plugins/inventory/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/inventory/__init__.py
diff --git a/test/units/plugins/inventory/test_constructed.py b/test/units/plugins/inventory/test_constructed.py
new file mode 100644
index 0000000..581e025
--- /dev/null
+++ b/test/units/plugins/inventory/test_constructed.py
@@ -0,0 +1,337 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Alan Rominger <arominge@redhat.net>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible.errors import AnsibleParserError
+from ansible.plugins.inventory.constructed import InventoryModule
+from ansible.inventory.data import InventoryData
+from ansible.template import Templar
+
+
+@pytest.fixture()
+def inventory_module():
+ r = InventoryModule()
+ r.inventory = InventoryData()
+ r.templar = Templar(None)
+ r._options = {'leading_separator': True}
+ return r
+
+
+def test_group_by_value_only(inventory_module):
+ inventory_module.inventory.add_host('foohost')
+ inventory_module.inventory.set_variable('foohost', 'bar', 'my_group_name')
+ host = inventory_module.inventory.get_host('foohost')
+ keyed_groups = [
+ {
+ 'prefix': '',
+ 'separator': '',
+ 'key': 'bar'
+ }
+ ]
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=False
+ )
+ assert 'my_group_name' in inventory_module.inventory.groups
+ group = inventory_module.inventory.groups['my_group_name']
+ assert group.hosts == [host]
+
+
+def test_keyed_group_separator(inventory_module):
+ inventory_module.inventory.add_host('farm')
+ inventory_module.inventory.set_variable('farm', 'farmer', 'mcdonald')
+ inventory_module.inventory.set_variable('farm', 'barn', {'cow': 'betsy'})
+ host = inventory_module.inventory.get_host('farm')
+ keyed_groups = [
+ {
+ 'prefix': 'farmer',
+ 'separator': '_old_',
+ 'key': 'farmer'
+ },
+ {
+ 'separator': 'mmmmmmmmmm',
+ 'key': 'barn'
+ }
+ ]
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=False
+ )
+ for group_name in ('farmer_old_mcdonald', 'mmmmmmmmmmcowmmmmmmmmmmbetsy'):
+ assert group_name in inventory_module.inventory.groups
+ group = inventory_module.inventory.groups[group_name]
+ assert group.hosts == [host]
+
+
+def test_keyed_group_empty_construction(inventory_module):
+ inventory_module.inventory.add_host('farm')
+ inventory_module.inventory.set_variable('farm', 'barn', {})
+ host = inventory_module.inventory.get_host('farm')
+ keyed_groups = [
+ {
+ 'separator': 'mmmmmmmmmm',
+ 'key': 'barn'
+ }
+ ]
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=True
+ )
+ assert host.groups == []
+
+
+def test_keyed_group_host_confusion(inventory_module):
+ inventory_module.inventory.add_host('cow')
+ inventory_module.inventory.add_group('cow')
+ host = inventory_module.inventory.get_host('cow')
+ host.vars['species'] = 'cow'
+ keyed_groups = [
+ {
+ 'separator': '',
+ 'prefix': '',
+ 'key': 'species'
+ }
+ ]
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=True
+ )
+ group = inventory_module.inventory.groups['cow']
+ # group cow has host of cow
+ assert group.hosts == [host]
+
+
+def test_keyed_parent_groups(inventory_module):
+ inventory_module.inventory.add_host('web1')
+ inventory_module.inventory.add_host('web2')
+ inventory_module.inventory.set_variable('web1', 'region', 'japan')
+ inventory_module.inventory.set_variable('web2', 'region', 'japan')
+ host1 = inventory_module.inventory.get_host('web1')
+ host2 = inventory_module.inventory.get_host('web2')
+ keyed_groups = [
+ {
+ 'prefix': 'region',
+ 'key': 'region',
+ 'parent_group': 'region_list'
+ }
+ ]
+ for host in [host1, host2]:
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=False
+ )
+ assert 'region_japan' in inventory_module.inventory.groups
+ assert 'region_list' in inventory_module.inventory.groups
+ region_group = inventory_module.inventory.groups['region_japan']
+ all_regions = inventory_module.inventory.groups['region_list']
+ assert all_regions.child_groups == [region_group]
+ assert region_group.hosts == [host1, host2]
+
+
+def test_parent_group_templating(inventory_module):
+ inventory_module.inventory.add_host('cow')
+ inventory_module.inventory.set_variable('cow', 'sound', 'mmmmmmmmmm')
+ inventory_module.inventory.set_variable('cow', 'nickname', 'betsy')
+ host = inventory_module.inventory.get_host('cow')
+ keyed_groups = [
+ {
+ 'key': 'sound',
+ 'prefix': 'sound',
+ 'parent_group': '{{ nickname }}'
+ },
+ {
+ 'key': 'nickname',
+ 'prefix': '',
+ 'separator': '',
+ 'parent_group': 'nickname' # statically-named parent group, conflicting with hostvar
+ },
+ {
+ 'key': 'nickname',
+ 'separator': '',
+ 'parent_group': '{{ location | default("field") }}'
+ }
+ ]
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=True
+ )
+ # first keyed group, "betsy" is a parent group name dynamically generated
+ betsys_group = inventory_module.inventory.groups['betsy']
+ assert [child.name for child in betsys_group.child_groups] == ['sound_mmmmmmmmmm']
+ # second keyed group, "nickname" is a statically-named root group
+ nicknames_group = inventory_module.inventory.groups['nickname']
+ assert [child.name for child in nicknames_group.child_groups] == ['betsy']
+ # second keyed group actually generated the parent group of the first keyed group
+ # assert that these are, in fact, the same object
+ assert nicknames_group.child_groups[0] == betsys_group
+ # second keyed group has two parents
+ locations_group = inventory_module.inventory.groups['field']
+ assert [child.name for child in locations_group.child_groups] == ['betsy']
+
+
+def test_parent_group_templating_error(inventory_module):
+ inventory_module.inventory.add_host('cow')
+ inventory_module.inventory.set_variable('cow', 'nickname', 'betsy')
+ host = inventory_module.inventory.get_host('cow')
+ keyed_groups = [
+ {
+ 'key': 'nickname',
+ 'separator': '',
+ 'parent_group': '{{ location.barn-yard }}'
+ }
+ ]
+ with pytest.raises(AnsibleParserError) as err_message:
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=True
+ )
+ assert 'Could not generate parent group' in err_message
+ # invalid parent group did not raise an exception with strict=False
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=False
+ )
+ # assert group was never added with invalid parent
+ assert 'betsy' not in inventory_module.inventory.groups
+
+
+def test_keyed_group_exclusive_argument(inventory_module):
+ inventory_module.inventory.add_host('cow')
+ inventory_module.inventory.set_variable('cow', 'nickname', 'betsy')
+ host = inventory_module.inventory.get_host('cow')
+ keyed_groups = [
+ {
+ 'key': 'tag',
+ 'separator': '_',
+ 'default_value': 'default_value_name',
+ 'trailing_separator': True
+ }
+ ]
+ with pytest.raises(AnsibleParserError) as err_message:
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=True
+ )
+ assert 'parameters are mutually exclusive' in err_message
+
+
+def test_keyed_group_empty_value(inventory_module):
+ inventory_module.inventory.add_host('server0')
+ inventory_module.inventory.set_variable('server0', 'tags', {'environment': 'prod', 'status': ''})
+ host = inventory_module.inventory.get_host('server0')
+ keyed_groups = [
+ {
+ 'prefix': 'tag',
+ 'separator': '_',
+ 'key': 'tags'
+ }
+ ]
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=False
+ )
+ for group_name in ('tag_environment_prod', 'tag_status_'):
+ assert group_name in inventory_module.inventory.groups
+
+
+def test_keyed_group_dict_with_default_value(inventory_module):
+ inventory_module.inventory.add_host('server0')
+ inventory_module.inventory.set_variable('server0', 'tags', {'environment': 'prod', 'status': ''})
+ host = inventory_module.inventory.get_host('server0')
+ keyed_groups = [
+ {
+ 'prefix': 'tag',
+ 'separator': '_',
+ 'key': 'tags',
+ 'default_value': 'running'
+ }
+ ]
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=False
+ )
+ for group_name in ('tag_environment_prod', 'tag_status_running'):
+ assert group_name in inventory_module.inventory.groups
+
+
+def test_keyed_group_str_no_default_value(inventory_module):
+ inventory_module.inventory.add_host('server0')
+ inventory_module.inventory.set_variable('server0', 'tags', '')
+ host = inventory_module.inventory.get_host('server0')
+ keyed_groups = [
+ {
+ 'prefix': 'tag',
+ 'separator': '_',
+ 'key': 'tags'
+ }
+ ]
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=False
+ )
+ # when the value is an empty string. this group is not generated
+ assert "tag_" not in inventory_module.inventory.groups
+
+
+def test_keyed_group_str_with_default_value(inventory_module):
+ inventory_module.inventory.add_host('server0')
+ inventory_module.inventory.set_variable('server0', 'tags', '')
+ host = inventory_module.inventory.get_host('server0')
+ keyed_groups = [
+ {
+ 'prefix': 'tag',
+ 'separator': '_',
+ 'key': 'tags',
+ 'default_value': 'running'
+ }
+ ]
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=False
+ )
+ assert "tag_running" in inventory_module.inventory.groups
+
+
+def test_keyed_group_list_with_default_value(inventory_module):
+ inventory_module.inventory.add_host('server0')
+ inventory_module.inventory.set_variable('server0', 'tags', ['test', ''])
+ host = inventory_module.inventory.get_host('server0')
+ keyed_groups = [
+ {
+ 'prefix': 'tag',
+ 'separator': '_',
+ 'key': 'tags',
+ 'default_value': 'prod'
+ }
+ ]
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=False
+ )
+ for group_name in ('tag_test', 'tag_prod'):
+ assert group_name in inventory_module.inventory.groups
+
+
+def test_keyed_group_with_trailing_separator(inventory_module):
+ inventory_module.inventory.add_host('server0')
+ inventory_module.inventory.set_variable('server0', 'tags', {'environment': 'prod', 'status': ''})
+ host = inventory_module.inventory.get_host('server0')
+ keyed_groups = [
+ {
+ 'prefix': 'tag',
+ 'separator': '_',
+ 'key': 'tags',
+ 'trailing_separator': False
+ }
+ ]
+ inventory_module._add_host_to_keyed_groups(
+ keyed_groups, host.vars, host.name, strict=False
+ )
+ for group_name in ('tag_environment_prod', 'tag_status'):
+ assert group_name in inventory_module.inventory.groups
diff --git a/test/units/plugins/inventory/test_inventory.py b/test/units/plugins/inventory/test_inventory.py
new file mode 100644
index 0000000..df24607
--- /dev/null
+++ b/test/units/plugins/inventory/test_inventory.py
@@ -0,0 +1,208 @@
+# Copyright 2015 Abhijit Menon-Sen <ams@2ndQuadrant.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import string
+import textwrap
+
+from unittest import mock
+
+from ansible import constants as C
+from units.compat import unittest
+from ansible.module_utils.six import string_types
+from ansible.module_utils._text import to_text
+from units.mock.path import mock_unfrackpath_noop
+
+from ansible.inventory.manager import InventoryManager, split_host_pattern
+
+from units.mock.loader import DictDataLoader
+
+
+class TestInventory(unittest.TestCase):
+
+ patterns = {
+ 'a': ['a'],
+ 'a, b': ['a', 'b'],
+ 'a , b': ['a', 'b'],
+ ' a,b ,c[1:2] ': ['a', 'b', 'c[1:2]'],
+ '9a01:7f8:191:7701::9': ['9a01:7f8:191:7701::9'],
+ '9a01:7f8:191:7701::9,9a01:7f8:191:7701::9': ['9a01:7f8:191:7701::9', '9a01:7f8:191:7701::9'],
+ '9a01:7f8:191:7701::9,9a01:7f8:191:7701::9,foo': ['9a01:7f8:191:7701::9', '9a01:7f8:191:7701::9', 'foo'],
+ 'foo[1:2]': ['foo[1:2]'],
+ 'a::b': ['a::b'],
+ 'a:b': ['a', 'b'],
+ ' a : b ': ['a', 'b'],
+ 'foo:bar:baz[1:2]': ['foo', 'bar', 'baz[1:2]'],
+ 'a,,b': ['a', 'b'],
+ 'a, ,b,,c, ,': ['a', 'b', 'c'],
+ ',': [],
+ '': [],
+ }
+
+ pattern_lists = [
+ [['a'], ['a']],
+ [['a', 'b'], ['a', 'b']],
+ [['a, b'], ['a', 'b']],
+ [['9a01:7f8:191:7701::9', '9a01:7f8:191:7701::9,foo'],
+ ['9a01:7f8:191:7701::9', '9a01:7f8:191:7701::9', 'foo']]
+ ]
+
+ # pattern_string: [ ('base_pattern', (a,b)), ['x','y','z'] ]
+ # a,b are the bounds of the subscript; x..z are the results of the subscript
+ # when applied to string.ascii_letters.
+
+ subscripts = {
+ 'a': [('a', None), list(string.ascii_letters)],
+ 'a[0]': [('a', (0, None)), ['a']],
+ 'a[1]': [('a', (1, None)), ['b']],
+ 'a[2:3]': [('a', (2, 3)), ['c', 'd']],
+ 'a[-1]': [('a', (-1, None)), ['Z']],
+ 'a[-2]': [('a', (-2, None)), ['Y']],
+ 'a[48:]': [('a', (48, -1)), ['W', 'X', 'Y', 'Z']],
+ 'a[49:]': [('a', (49, -1)), ['X', 'Y', 'Z']],
+ 'a[1:]': [('a', (1, -1)), list(string.ascii_letters[1:])],
+ }
+
+ ranges_to_expand = {
+ 'a[1:2]': ['a1', 'a2'],
+ 'a[1:10:2]': ['a1', 'a3', 'a5', 'a7', 'a9'],
+ 'a[a:b]': ['aa', 'ab'],
+ 'a[a:i:3]': ['aa', 'ad', 'ag'],
+ 'a[a:b][c:d]': ['aac', 'aad', 'abc', 'abd'],
+ 'a[0:1][2:3]': ['a02', 'a03', 'a12', 'a13'],
+ 'a[a:b][2:3]': ['aa2', 'aa3', 'ab2', 'ab3'],
+ }
+
+ def setUp(self):
+ fake_loader = DictDataLoader({})
+
+ self.i = InventoryManager(loader=fake_loader, sources=[None])
+
+ def test_split_patterns(self):
+
+ for p in self.patterns:
+ r = self.patterns[p]
+ self.assertEqual(r, split_host_pattern(p))
+
+ for p, r in self.pattern_lists:
+ self.assertEqual(r, split_host_pattern(p))
+
+ def test_ranges(self):
+
+ for s in self.subscripts:
+ r = self.subscripts[s]
+ self.assertEqual(r[0], self.i._split_subscript(s))
+ self.assertEqual(
+ r[1],
+ self.i._apply_subscript(
+ list(string.ascii_letters),
+ r[0][1]
+ )
+ )
+
+
+class TestInventoryPlugins(unittest.TestCase):
+
+ def test_empty_inventory(self):
+ inventory = self._get_inventory('')
+
+ self.assertIn('all', inventory.groups)
+ self.assertIn('ungrouped', inventory.groups)
+ self.assertFalse(inventory.groups['all'].get_hosts())
+ self.assertFalse(inventory.groups['ungrouped'].get_hosts())
+
+ def test_ini(self):
+ self._test_default_groups("""
+ host1
+ host2
+ host3
+ [servers]
+ host3
+ host4
+ host5
+ """)
+
+ def test_ini_explicit_ungrouped(self):
+ self._test_default_groups("""
+ [ungrouped]
+ host1
+ host2
+ host3
+ [servers]
+ host3
+ host4
+ host5
+ """)
+
+ def test_ini_variables_stringify(self):
+ values = ['string', 'no', 'No', 'false', 'FALSE', [], False, 0]
+
+ inventory_content = "host1 "
+ inventory_content += ' '.join(['var%s=%s' % (i, to_text(x)) for i, x in enumerate(values)])
+ inventory = self._get_inventory(inventory_content)
+
+ variables = inventory.get_host('host1').vars
+ for i in range(len(values)):
+ if isinstance(values[i], string_types):
+ self.assertIsInstance(variables['var%s' % i], string_types)
+ else:
+ self.assertIsInstance(variables['var%s' % i], type(values[i]))
+
+ @mock.patch('ansible.inventory.manager.unfrackpath', mock_unfrackpath_noop)
+ @mock.patch('os.path.exists', lambda x: True)
+ @mock.patch('os.access', lambda x, y: True)
+ def test_yaml_inventory(self, filename="test.yaml"):
+ inventory_content = {filename: textwrap.dedent("""\
+ ---
+ all:
+ hosts:
+ test1:
+ test2:
+ """)}
+ C.INVENTORY_ENABLED = ['yaml']
+ fake_loader = DictDataLoader(inventory_content)
+ im = InventoryManager(loader=fake_loader, sources=filename)
+ self.assertTrue(im._inventory.hosts)
+ self.assertIn('test1', im._inventory.hosts)
+ self.assertIn('test2', im._inventory.hosts)
+ self.assertIn(im._inventory.get_host('test1'), im._inventory.groups['all'].hosts)
+ self.assertIn(im._inventory.get_host('test2'), im._inventory.groups['all'].hosts)
+ self.assertEqual(len(im._inventory.groups['all'].hosts), 2)
+ self.assertIn(im._inventory.get_host('test1'), im._inventory.groups['ungrouped'].hosts)
+ self.assertIn(im._inventory.get_host('test2'), im._inventory.groups['ungrouped'].hosts)
+ self.assertEqual(len(im._inventory.groups['ungrouped'].hosts), 2)
+
+ def _get_inventory(self, inventory_content):
+
+ fake_loader = DictDataLoader({__file__: inventory_content})
+
+ return InventoryManager(loader=fake_loader, sources=[__file__])
+
+ def _test_default_groups(self, inventory_content):
+ inventory = self._get_inventory(inventory_content)
+
+ self.assertIn('all', inventory.groups)
+ self.assertIn('ungrouped', inventory.groups)
+ all_hosts = set(host.name for host in inventory.groups['all'].get_hosts())
+ self.assertEqual(set(['host1', 'host2', 'host3', 'host4', 'host5']), all_hosts)
+ ungrouped_hosts = set(host.name for host in inventory.groups['ungrouped'].get_hosts())
+ self.assertEqual(set(['host1', 'host2']), ungrouped_hosts)
+ servers_hosts = set(host.name for host in inventory.groups['servers'].get_hosts())
+ self.assertEqual(set(['host3', 'host4', 'host5']), servers_hosts)
diff --git a/test/units/plugins/inventory/test_script.py b/test/units/plugins/inventory/test_script.py
new file mode 100644
index 0000000..9f75199
--- /dev/null
+++ b/test/units/plugins/inventory/test_script.py
@@ -0,0 +1,105 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2017 Chris Meyers <cmeyers@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+from unittest import mock
+
+from ansible import constants as C
+from ansible.errors import AnsibleError
+from ansible.plugins.loader import PluginLoader
+from units.compat import unittest
+from ansible.module_utils._text import to_bytes, to_native
+
+
+class TestInventoryModule(unittest.TestCase):
+
+ def setUp(self):
+
+ class Inventory():
+ cache = dict()
+
+ class PopenResult():
+ returncode = 0
+ stdout = b""
+ stderr = b""
+
+ def communicate(self):
+ return (self.stdout, self.stderr)
+
+ self.popen_result = PopenResult()
+ self.inventory = Inventory()
+ self.loader = mock.MagicMock()
+ self.loader.load = mock.MagicMock()
+
+ inv_loader = PluginLoader('InventoryModule', 'ansible.plugins.inventory', C.DEFAULT_INVENTORY_PLUGIN_PATH, 'inventory_plugins')
+ self.inventory_module = inv_loader.get('script')
+ self.inventory_module.set_options()
+
+ def register_patch(name):
+ patcher = mock.patch(name)
+ self.addCleanup(patcher.stop)
+ return patcher.start()
+
+ self.popen = register_patch('subprocess.Popen')
+ self.popen.return_value = self.popen_result
+
+ self.BaseInventoryPlugin = register_patch('ansible.plugins.inventory.BaseInventoryPlugin')
+ self.BaseInventoryPlugin.get_cache_prefix.return_value = 'abc123'
+
+ def test_parse_subprocess_path_not_found_fail(self):
+ self.popen.side_effect = OSError("dummy text")
+
+ with pytest.raises(AnsibleError) as e:
+ self.inventory_module.parse(self.inventory, self.loader, '/foo/bar/foobar.py')
+ assert e.value.message == "problem running /foo/bar/foobar.py --list (dummy text)"
+
+ def test_parse_subprocess_err_code_fail(self):
+ self.popen_result.stdout = to_bytes(u"fooébar", errors='surrogate_escape')
+ self.popen_result.stderr = to_bytes(u"dummyédata")
+
+ self.popen_result.returncode = 1
+
+ with pytest.raises(AnsibleError) as e:
+ self.inventory_module.parse(self.inventory, self.loader, '/foo/bar/foobar.py')
+ assert e.value.message == to_native("Inventory script (/foo/bar/foobar.py) had an execution error: "
+ "dummyédata\n ")
+
+ def test_parse_utf8_fail(self):
+ self.popen_result.returncode = 0
+ self.popen_result.stderr = to_bytes("dummyédata")
+ self.loader.load.side_effect = TypeError('obj must be string')
+
+ with pytest.raises(AnsibleError) as e:
+ self.inventory_module.parse(self.inventory, self.loader, '/foo/bar/foobar.py')
+ assert e.value.message == to_native("failed to parse executable inventory script results from "
+ "/foo/bar/foobar.py: obj must be string\ndummyédata\n")
+
+ def test_parse_dict_fail(self):
+ self.popen_result.returncode = 0
+ self.popen_result.stderr = to_bytes("dummyédata")
+ self.loader.load.return_value = 'i am not a dict'
+
+ with pytest.raises(AnsibleError) as e:
+ self.inventory_module.parse(self.inventory, self.loader, '/foo/bar/foobar.py')
+ assert e.value.message == to_native("failed to parse executable inventory script results from "
+ "/foo/bar/foobar.py: needs to be a json dict\ndummyédata\n")
diff --git a/test/units/plugins/loader_fixtures/__init__.py b/test/units/plugins/loader_fixtures/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/loader_fixtures/__init__.py
diff --git a/test/units/plugins/loader_fixtures/import_fixture.py b/test/units/plugins/loader_fixtures/import_fixture.py
new file mode 100644
index 0000000..8112733
--- /dev/null
+++ b/test/units/plugins/loader_fixtures/import_fixture.py
@@ -0,0 +1,9 @@
+# Nothing to see here, this file is just empty to support a imp.load_source
+# without doing anything
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class test:
+ def __init__(self, *args, **kwargs):
+ pass
diff --git a/test/units/plugins/lookup/__init__.py b/test/units/plugins/lookup/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/lookup/__init__.py
diff --git a/test/units/plugins/lookup/test_env.py b/test/units/plugins/lookup/test_env.py
new file mode 100644
index 0000000..5d9713f
--- /dev/null
+++ b/test/units/plugins/lookup/test_env.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2019, Abhay Kadam <abhaykadam88@gmail.com>
+# 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 pytest
+
+from ansible.plugins.loader import lookup_loader
+
+
+@pytest.mark.parametrize('env_var,exp_value', [
+ ('foo', 'bar'),
+ ('equation', 'a=b*100')
+])
+def test_env_var_value(monkeypatch, env_var, exp_value):
+ monkeypatch.setattr('ansible.utils.py3compat.environ.get', lambda x, y: exp_value)
+
+ env_lookup = lookup_loader.get('env')
+ retval = env_lookup.run([env_var], None)
+ assert retval == [exp_value]
+
+
+@pytest.mark.parametrize('env_var,exp_value', [
+ ('simple_var', 'alpha-β-gamma'),
+ ('the_var', 'ãnˈsiβle')
+])
+def test_utf8_env_var_value(monkeypatch, env_var, exp_value):
+ monkeypatch.setattr('ansible.utils.py3compat.environ.get', lambda x, y: exp_value)
+
+ env_lookup = lookup_loader.get('env')
+ retval = env_lookup.run([env_var], None)
+ assert retval == [exp_value]
diff --git a/test/units/plugins/lookup/test_ini.py b/test/units/plugins/lookup/test_ini.py
new file mode 100644
index 0000000..b2d883c
--- /dev/null
+++ b/test/units/plugins/lookup/test_ini.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from ansible.plugins.lookup.ini import _parse_params
+
+
+class TestINILookup(unittest.TestCase):
+
+ # Currently there isn't a new-style
+ old_style_params_data = (
+ # Simple case
+ dict(
+ term=u'keyA section=sectionA file=/path/to/file',
+ expected=[u'file=/path/to/file', u'keyA', u'section=sectionA'],
+ ),
+ dict(
+ term=u'keyB section=sectionB with space file=/path/with/embedded spaces and/file',
+ expected=[u'file=/path/with/embedded spaces and/file', u'keyB', u'section=sectionB with space'],
+ ),
+ dict(
+ term=u'keyC section=sectionC file=/path/with/equals/cn=com.ansible',
+ expected=[u'file=/path/with/equals/cn=com.ansible', u'keyC', u'section=sectionC'],
+ ),
+ dict(
+ term=u'keyD section=sectionD file=/path/with space and/equals/cn=com.ansible',
+ expected=[u'file=/path/with space and/equals/cn=com.ansible', u'keyD', u'section=sectionD'],
+ ),
+ dict(
+ term=u'keyE section=sectionE file=/path/with/unicode/くらとみ/file',
+ expected=[u'file=/path/with/unicode/くらとみ/file', u'keyE', u'section=sectionE'],
+ ),
+ dict(
+ term=u'keyF section=sectionF file=/path/with/utf 8 and spaces/くらとみ/file',
+ expected=[u'file=/path/with/utf 8 and spaces/くらとみ/file', u'keyF', u'section=sectionF'],
+ ),
+ )
+
+ def test_parse_parameters(self):
+ pvals = {'file': '', 'section': '', 'key': '', 'type': '', 're': '', 'default': '', 'encoding': ''}
+ for testcase in self.old_style_params_data:
+ # print(testcase)
+ params = _parse_params(testcase['term'], pvals)
+ params.sort()
+ self.assertEqual(params, testcase['expected'])
diff --git a/test/units/plugins/lookup/test_password.py b/test/units/plugins/lookup/test_password.py
new file mode 100644
index 0000000..15207b2
--- /dev/null
+++ b/test/units/plugins/lookup/test_password.py
@@ -0,0 +1,577 @@
+# -*- coding: utf-8 -*-
+# (c) 2015, Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+try:
+ import passlib
+ from passlib.handlers import pbkdf2
+except ImportError:
+ passlib = None
+ pbkdf2 = None
+
+import pytest
+
+from units.mock.loader import DictDataLoader
+
+from units.compat import unittest
+from unittest.mock import mock_open, patch
+from ansible.errors import AnsibleError
+from ansible.module_utils.six import text_type
+from ansible.module_utils.six.moves import builtins
+from ansible.module_utils._text import to_bytes
+from ansible.plugins.loader import PluginLoader, lookup_loader
+from ansible.plugins.lookup import password
+
+
+DEFAULT_LENGTH = 20
+DEFAULT_CHARS = sorted([u'ascii_letters', u'digits', u".,:-_"])
+DEFAULT_CANDIDATE_CHARS = u'.,:-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
+
+# Currently there isn't a new-style
+old_style_params_data = (
+ # Simple case
+ dict(
+ term=u'/path/to/file',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None),
+ candidate_chars=DEFAULT_CANDIDATE_CHARS,
+ ),
+
+ # Special characters in path
+ dict(
+ term=u'/path/with/embedded spaces and/file',
+ filename=u'/path/with/embedded spaces and/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None),
+ candidate_chars=DEFAULT_CANDIDATE_CHARS,
+ ),
+ dict(
+ term=u'/path/with/equals/cn=com.ansible',
+ filename=u'/path/with/equals/cn=com.ansible',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None),
+ candidate_chars=DEFAULT_CANDIDATE_CHARS,
+ ),
+ dict(
+ term=u'/path/with/unicode/くらとみ/file',
+ filename=u'/path/with/unicode/くらとみ/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None),
+ candidate_chars=DEFAULT_CANDIDATE_CHARS,
+ ),
+
+ # Mix several special chars
+ dict(
+ term=u'/path/with/utf 8 and spaces/くらとみ/file',
+ filename=u'/path/with/utf 8 and spaces/くらとみ/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None),
+ candidate_chars=DEFAULT_CANDIDATE_CHARS,
+ ),
+ dict(
+ term=u'/path/with/encoding=unicode/くらとみ/file',
+ filename=u'/path/with/encoding=unicode/くらとみ/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None),
+ candidate_chars=DEFAULT_CANDIDATE_CHARS,
+ ),
+ dict(
+ term=u'/path/with/encoding=unicode/くらとみ/and spaces file',
+ filename=u'/path/with/encoding=unicode/くらとみ/and spaces file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None),
+ candidate_chars=DEFAULT_CANDIDATE_CHARS,
+ ),
+
+ # Simple parameters
+ dict(
+ term=u'/path/to/file length=42',
+ filename=u'/path/to/file',
+ params=dict(length=42, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed=None),
+ candidate_chars=DEFAULT_CANDIDATE_CHARS,
+ ),
+ dict(
+ term=u'/path/to/file encrypt=pbkdf2_sha256',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt='pbkdf2_sha256', ident=None, chars=DEFAULT_CHARS, seed=None),
+ candidate_chars=DEFAULT_CANDIDATE_CHARS,
+ ),
+ dict(
+ term=u'/path/to/file chars=abcdefghijklmnop',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'abcdefghijklmnop'], seed=None),
+ candidate_chars=u'abcdefghijklmnop',
+ ),
+ dict(
+ term=u'/path/to/file chars=digits,abc,def',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None,
+ chars=sorted([u'digits', u'abc', u'def']), seed=None),
+ candidate_chars=u'abcdef0123456789',
+ ),
+ dict(
+ term=u'/path/to/file seed=1',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=DEFAULT_CHARS, seed='1'),
+ candidate_chars=DEFAULT_CANDIDATE_CHARS,
+ ),
+
+ # Including comma in chars
+ dict(
+ term=u'/path/to/file chars=abcdefghijklmnop,,digits',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None,
+ chars=sorted([u'abcdefghijklmnop', u',', u'digits']), seed=None),
+ candidate_chars=u',abcdefghijklmnop0123456789',
+ ),
+ dict(
+ term=u'/path/to/file chars=,,',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None,
+ chars=[u','], seed=None),
+ candidate_chars=u',',
+ ),
+
+ # Including = in chars
+ dict(
+ term=u'/path/to/file chars=digits,=,,',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None,
+ chars=sorted([u'digits', u'=', u',']), seed=None),
+ candidate_chars=u',=0123456789',
+ ),
+ dict(
+ term=u'/path/to/file chars=digits,abc=def',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None,
+ chars=sorted([u'digits', u'abc=def']), seed=None),
+ candidate_chars=u'abc=def0123456789',
+ ),
+
+ # Including unicode in chars
+ dict(
+ term=u'/path/to/file chars=digits,くらとみ,,',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None,
+ chars=sorted([u'digits', u'くらとみ', u',']), seed=None),
+ candidate_chars=u',0123456789くらとみ',
+ ),
+ # Including only unicode in chars
+ dict(
+ term=u'/path/to/file chars=くらとみ',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None,
+ chars=sorted([u'くらとみ']), seed=None),
+ candidate_chars=u'くらとみ',
+ ),
+
+ # Include ':' in path
+ dict(
+ term=u'/path/to/file_with:colon chars=ascii_letters,digits',
+ filename=u'/path/to/file_with:colon',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None,
+ chars=sorted([u'ascii_letters', u'digits']), seed=None),
+ candidate_chars=u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789',
+ ),
+
+ # Including special chars in both path and chars
+ # Special characters in path
+ dict(
+ term=u'/path/with/embedded spaces and/file chars=abc=def',
+ filename=u'/path/with/embedded spaces and/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'abc=def'], seed=None),
+ candidate_chars=u'abc=def',
+ ),
+ dict(
+ term=u'/path/with/equals/cn=com.ansible chars=abc=def',
+ filename=u'/path/with/equals/cn=com.ansible',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'abc=def'], seed=None),
+ candidate_chars=u'abc=def',
+ ),
+ dict(
+ term=u'/path/with/unicode/くらとみ/file chars=くらとみ',
+ filename=u'/path/with/unicode/くらとみ/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, ident=None, chars=[u'くらとみ'], seed=None),
+ candidate_chars=u'くらとみ',
+ ),
+)
+
+
+class TestParseParameters(unittest.TestCase):
+
+ def setUp(self):
+ self.fake_loader = DictDataLoader({'/path/to/somewhere': 'sdfsdf'})
+ self.password_lookup = lookup_loader.get('password')
+ self.password_lookup._loader = self.fake_loader
+
+ def test(self):
+ for testcase in old_style_params_data:
+ filename, params = self.password_lookup._parse_parameters(testcase['term'])
+ params['chars'].sort()
+ self.assertEqual(filename, testcase['filename'])
+ self.assertEqual(params, testcase['params'])
+
+ def test_unrecognized_value(self):
+ testcase = dict(term=u'/path/to/file chars=くらとみi sdfsdf',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, chars=[u'くらとみ']),
+ candidate_chars=u'くらとみ')
+ self.assertRaises(AnsibleError, self.password_lookup._parse_parameters, testcase['term'])
+
+ def test_invalid_params(self):
+ testcase = dict(term=u'/path/to/file chars=くらとみi somethign_invalid=123',
+ filename=u'/path/to/file',
+ params=dict(length=DEFAULT_LENGTH, encrypt=None, chars=[u'くらとみ']),
+ candidate_chars=u'くらとみ')
+ self.assertRaises(AnsibleError, self.password_lookup._parse_parameters, testcase['term'])
+
+
+class TestReadPasswordFile(unittest.TestCase):
+ def setUp(self):
+ self.os_path_exists = password.os.path.exists
+
+ def tearDown(self):
+ password.os.path.exists = self.os_path_exists
+
+ def test_no_password_file(self):
+ password.os.path.exists = lambda x: False
+ self.assertEqual(password._read_password_file(b'/nonexistent'), None)
+
+ def test_with_password_file(self):
+ password.os.path.exists = lambda x: True
+ with patch.object(builtins, 'open', mock_open(read_data=b'Testing\n')) as m:
+ self.assertEqual(password._read_password_file(b'/etc/motd'), u'Testing')
+
+
+class TestGenCandidateChars(unittest.TestCase):
+ def _assert_gen_candidate_chars(self, testcase):
+ expected_candidate_chars = testcase['candidate_chars']
+ params = testcase['params']
+ chars_spec = params['chars']
+ res = password._gen_candidate_chars(chars_spec)
+ self.assertEqual(res, expected_candidate_chars)
+
+ def test_gen_candidate_chars(self):
+ for testcase in old_style_params_data:
+ self._assert_gen_candidate_chars(testcase)
+
+
+class TestRandomPassword(unittest.TestCase):
+ def _assert_valid_chars(self, res, chars):
+ for res_char in res:
+ self.assertIn(res_char, chars)
+
+ def test_default(self):
+ res = password.random_password()
+ self.assertEqual(len(res), DEFAULT_LENGTH)
+ self.assertTrue(isinstance(res, text_type))
+ self._assert_valid_chars(res, DEFAULT_CANDIDATE_CHARS)
+
+ def test_zero_length(self):
+ res = password.random_password(length=0)
+ self.assertEqual(len(res), 0)
+ self.assertTrue(isinstance(res, text_type))
+ self._assert_valid_chars(res, u',')
+
+ def test_just_a_common(self):
+ res = password.random_password(length=1, chars=u',')
+ self.assertEqual(len(res), 1)
+ self.assertEqual(res, u',')
+
+ def test_free_will(self):
+ # A Rush and Spinal Tap reference twofer
+ res = password.random_password(length=11, chars=u'a')
+ self.assertEqual(len(res), 11)
+ self.assertEqual(res, 'aaaaaaaaaaa')
+ self._assert_valid_chars(res, u'a')
+
+ def test_unicode(self):
+ res = password.random_password(length=11, chars=u'くらとみ')
+ self._assert_valid_chars(res, u'くらとみ')
+ self.assertEqual(len(res), 11)
+
+ def test_seed(self):
+ pw1 = password.random_password(seed=1)
+ pw2 = password.random_password(seed=1)
+ pw3 = password.random_password(seed=2)
+ self.assertEqual(pw1, pw2)
+ self.assertNotEqual(pw1, pw3)
+
+ def test_gen_password(self):
+ for testcase in old_style_params_data:
+ params = testcase['params']
+ candidate_chars = testcase['candidate_chars']
+ params_chars_spec = password._gen_candidate_chars(params['chars'])
+ password_string = password.random_password(length=params['length'],
+ chars=params_chars_spec)
+ self.assertEqual(len(password_string),
+ params['length'],
+ msg='generated password=%s has length (%s) instead of expected length (%s)' %
+ (password_string, len(password_string), params['length']))
+
+ for char in password_string:
+ self.assertIn(char, candidate_chars,
+ msg='%s not found in %s from chars spect %s' %
+ (char, candidate_chars, params['chars']))
+
+
+class TestParseContent(unittest.TestCase):
+
+ def test_empty_password_file(self):
+ plaintext_password, salt = password._parse_content(u'')
+ self.assertEqual(plaintext_password, u'')
+ self.assertEqual(salt, None)
+
+ def test(self):
+ expected_content = u'12345678'
+ file_content = expected_content
+ plaintext_password, salt = password._parse_content(file_content)
+ self.assertEqual(plaintext_password, expected_content)
+ self.assertEqual(salt, None)
+
+ def test_with_salt(self):
+ expected_content = u'12345678 salt=87654321'
+ file_content = expected_content
+ plaintext_password, salt = password._parse_content(file_content)
+ self.assertEqual(plaintext_password, u'12345678')
+ self.assertEqual(salt, u'87654321')
+
+
+class TestFormatContent(unittest.TestCase):
+ def test_no_encrypt(self):
+ self.assertEqual(
+ password._format_content(password=u'hunter42',
+ salt=u'87654321',
+ encrypt=False),
+ u'hunter42 salt=87654321')
+
+ def test_no_encrypt_no_salt(self):
+ self.assertEqual(
+ password._format_content(password=u'hunter42',
+ salt=None,
+ encrypt=None),
+ u'hunter42')
+
+ def test_encrypt(self):
+ self.assertEqual(
+ password._format_content(password=u'hunter42',
+ salt=u'87654321',
+ encrypt='pbkdf2_sha256'),
+ u'hunter42 salt=87654321')
+
+ def test_encrypt_no_salt(self):
+ self.assertRaises(AssertionError, password._format_content, u'hunter42', None, 'pbkdf2_sha256')
+
+
+class TestWritePasswordFile(unittest.TestCase):
+ def setUp(self):
+ self.makedirs_safe = password.makedirs_safe
+ self.os_chmod = password.os.chmod
+ password.makedirs_safe = lambda path, mode: None
+ password.os.chmod = lambda path, mode: None
+
+ def tearDown(self):
+ password.makedirs_safe = self.makedirs_safe
+ password.os.chmod = self.os_chmod
+
+ def test_content_written(self):
+
+ with patch.object(builtins, 'open', mock_open()) as m:
+ password._write_password_file(b'/this/is/a/test/caf\xc3\xa9', u'Testing Café')
+
+ m.assert_called_once_with(b'/this/is/a/test/caf\xc3\xa9', 'wb')
+ m().write.assert_called_once_with(u'Testing Café\n'.encode('utf-8'))
+
+
+class BaseTestLookupModule(unittest.TestCase):
+ def setUp(self):
+ self.fake_loader = DictDataLoader({'/path/to/somewhere': 'sdfsdf'})
+ self.password_lookup = lookup_loader.get('password')
+ self.password_lookup._loader = self.fake_loader
+ self.os_path_exists = password.os.path.exists
+ self.os_open = password.os.open
+ password.os.open = lambda path, flag: None
+ self.os_close = password.os.close
+ password.os.close = lambda fd: None
+ self.os_remove = password.os.remove
+ password.os.remove = lambda path: None
+ self.makedirs_safe = password.makedirs_safe
+ password.makedirs_safe = lambda path, mode: None
+
+ def tearDown(self):
+ password.os.path.exists = self.os_path_exists
+ password.os.open = self.os_open
+ password.os.close = self.os_close
+ password.os.remove = self.os_remove
+ password.makedirs_safe = self.makedirs_safe
+
+
+class TestLookupModuleWithoutPasslib(BaseTestLookupModule):
+ @patch.object(PluginLoader, '_get_paths')
+ @patch('ansible.plugins.lookup.password._write_password_file')
+ def test_no_encrypt(self, mock_get_paths, mock_write_file):
+ mock_get_paths.return_value = ['/path/one', '/path/two', '/path/three']
+
+ results = self.password_lookup.run([u'/path/to/somewhere'], None)
+
+ # FIXME: assert something useful
+ for result in results:
+ assert len(result) == DEFAULT_LENGTH
+ assert isinstance(result, text_type)
+
+ @patch.object(PluginLoader, '_get_paths')
+ @patch('ansible.plugins.lookup.password._write_password_file')
+ def test_password_already_created_no_encrypt(self, mock_get_paths, mock_write_file):
+ mock_get_paths.return_value = ['/path/one', '/path/two', '/path/three']
+ password.os.path.exists = lambda x: x == to_bytes('/path/to/somewhere')
+
+ with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m:
+ results = self.password_lookup.run([u'/path/to/somewhere chars=anything'], None)
+
+ for result in results:
+ self.assertEqual(result, u'hunter42')
+
+ @patch.object(PluginLoader, '_get_paths')
+ @patch('ansible.plugins.lookup.password._write_password_file')
+ def test_only_a(self, mock_get_paths, mock_write_file):
+ mock_get_paths.return_value = ['/path/one', '/path/two', '/path/three']
+
+ results = self.password_lookup.run([u'/path/to/somewhere chars=a'], None)
+ for result in results:
+ self.assertEqual(result, u'a' * DEFAULT_LENGTH)
+
+ @patch('time.sleep')
+ def test_lock_been_held(self, mock_sleep):
+ # pretend the lock file is here
+ password.os.path.exists = lambda x: True
+ try:
+ with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m:
+ # should timeout here
+ results = self.password_lookup.run([u'/path/to/somewhere chars=anything'], None)
+ self.fail("Lookup didn't timeout when lock already been held")
+ except AnsibleError:
+ pass
+
+ def test_lock_not_been_held(self):
+ # pretend now there is password file but no lock
+ password.os.path.exists = lambda x: x == to_bytes('/path/to/somewhere')
+ try:
+ with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m:
+ # should not timeout here
+ results = self.password_lookup.run([u'/path/to/somewhere chars=anything'], None)
+ except AnsibleError:
+ self.fail('Lookup timeouts when lock is free')
+
+ for result in results:
+ self.assertEqual(result, u'hunter42')
+
+
+@pytest.mark.skipif(passlib is None, reason='passlib must be installed to run these tests')
+class TestLookupModuleWithPasslib(BaseTestLookupModule):
+ def setUp(self):
+ super(TestLookupModuleWithPasslib, self).setUp()
+
+ # Different releases of passlib default to a different number of rounds
+ self.sha256 = passlib.registry.get_crypt_handler('pbkdf2_sha256')
+ sha256_for_tests = pbkdf2.create_pbkdf2_hash("sha256", 32, 20000)
+ passlib.registry.register_crypt_handler(sha256_for_tests, force=True)
+
+ def tearDown(self):
+ super(TestLookupModuleWithPasslib, self).tearDown()
+
+ passlib.registry.register_crypt_handler(self.sha256, force=True)
+
+ @patch.object(PluginLoader, '_get_paths')
+ @patch('ansible.plugins.lookup.password._write_password_file')
+ def test_encrypt(self, mock_get_paths, mock_write_file):
+ mock_get_paths.return_value = ['/path/one', '/path/two', '/path/three']
+
+ results = self.password_lookup.run([u'/path/to/somewhere encrypt=pbkdf2_sha256'], None)
+
+ # pbkdf2 format plus hash
+ expected_password_length = 76
+
+ for result in results:
+ self.assertEqual(len(result), expected_password_length)
+ # result should have 5 parts split by '$'
+ str_parts = result.split('$', 5)
+
+ # verify the result is parseable by the passlib
+ crypt_parts = passlib.hash.pbkdf2_sha256.parsehash(result)
+
+ # verify it used the right algo type
+ self.assertEqual(str_parts[1], 'pbkdf2-sha256')
+
+ self.assertEqual(len(str_parts), 5)
+
+ # verify the string and parsehash agree on the number of rounds
+ self.assertEqual(int(str_parts[2]), crypt_parts['rounds'])
+ self.assertIsInstance(result, text_type)
+
+ @patch.object(PluginLoader, '_get_paths')
+ @patch('ansible.plugins.lookup.password._write_password_file')
+ def test_password_already_created_encrypt(self, mock_get_paths, mock_write_file):
+ mock_get_paths.return_value = ['/path/one', '/path/two', '/path/three']
+ password.os.path.exists = lambda x: x == to_bytes('/path/to/somewhere')
+
+ with patch.object(builtins, 'open', mock_open(read_data=b'hunter42 salt=87654321\n')) as m:
+ results = self.password_lookup.run([u'/path/to/somewhere chars=anything encrypt=pbkdf2_sha256'], None)
+ for result in results:
+ self.assertEqual(result, u'$pbkdf2-sha256$20000$ODc2NTQzMjE$Uikde0cv0BKaRaAXMrUQB.zvG4GmnjClwjghwIRf2gU')
+
+
+@pytest.mark.skipif(passlib is None, reason='passlib must be installed to run these tests')
+class TestLookupModuleWithPasslibWrappedAlgo(BaseTestLookupModule):
+ def setUp(self):
+ super(TestLookupModuleWithPasslibWrappedAlgo, self).setUp()
+ self.os_path_exists = password.os.path.exists
+
+ def tearDown(self):
+ super(TestLookupModuleWithPasslibWrappedAlgo, self).tearDown()
+ password.os.path.exists = self.os_path_exists
+
+ @patch('ansible.plugins.lookup.password._write_password_file')
+ def test_encrypt_wrapped_crypt_algo(self, mock_write_file):
+
+ password.os.path.exists = self.password_lookup._loader.path_exists
+ with patch.object(builtins, 'open', mock_open(read_data=self.password_lookup._loader._get_file_contents('/path/to/somewhere')[0])) as m:
+ results = self.password_lookup.run([u'/path/to/somewhere encrypt=ldap_sha256_crypt'], None)
+
+ wrapper = getattr(passlib.hash, 'ldap_sha256_crypt')
+
+ self.assertEqual(len(results), 1)
+ result = results[0]
+ self.assertIsInstance(result, text_type)
+
+ expected_password_length = 76
+ self.assertEqual(len(result), expected_password_length)
+
+ # result should have 5 parts split by '$'
+ str_parts = result.split('$')
+ self.assertEqual(len(str_parts), 5)
+
+ # verify the string and passlib agree on the number of rounds
+ self.assertEqual(str_parts[2], "rounds=%s" % wrapper.default_rounds)
+
+ # verify it used the right algo type
+ self.assertEqual(str_parts[0], '{CRYPT}')
+
+ # verify it used the right algo type
+ self.assertTrue(wrapper.verify(self.password_lookup._loader._get_file_contents('/path/to/somewhere')[0], result))
+
+ # verify a password with a non default rounds value
+ # generated with: echo test | mkpasswd -s --rounds 660000 -m sha-256 --salt testansiblepass.
+ hashpw = '{CRYPT}$5$rounds=660000$testansiblepass.$KlRSdA3iFXoPI.dEwh7AixiXW3EtCkLrlQvlYA2sluD'
+ self.assertTrue(wrapper.verify('test', hashpw))
diff --git a/test/units/plugins/lookup/test_url.py b/test/units/plugins/lookup/test_url.py
new file mode 100644
index 0000000..2aa77b3
--- /dev/null
+++ b/test/units/plugins/lookup/test_url.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2020, Sam Doran <sdoran@redhat.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible.plugins.loader import lookup_loader
+
+
+@pytest.mark.parametrize(
+ ('kwargs', 'agent'),
+ (
+ ({}, 'ansible-httpget'),
+ ({'http_agent': 'SuperFox'}, 'SuperFox'),
+ )
+)
+def test_user_agent(mocker, kwargs, agent):
+ mock_open_url = mocker.patch('ansible.plugins.lookup.url.open_url', side_effect=AttributeError('raised intentionally'))
+ url_lookup = lookup_loader.get('url')
+ with pytest.raises(AttributeError):
+ url_lookup.run(['https://nourl'], **kwargs)
+ assert 'http_agent' in mock_open_url.call_args.kwargs
+ assert mock_open_url.call_args.kwargs['http_agent'] == agent
diff --git a/test/units/plugins/shell/__init__.py b/test/units/plugins/shell/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/shell/__init__.py
diff --git a/test/units/plugins/shell/test_cmd.py b/test/units/plugins/shell/test_cmd.py
new file mode 100644
index 0000000..4c1a654
--- /dev/null
+++ b/test/units/plugins/shell/test_cmd.py
@@ -0,0 +1,19 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible.plugins.shell.cmd import ShellModule
+
+
+@pytest.mark.parametrize('s, expected', [
+ ['arg1', 'arg1'],
+ [None, '""'],
+ ['arg1 and 2', '^"arg1 and 2^"'],
+ ['malicious argument\\"&whoami', '^"malicious argument\\\\^"^&whoami^"'],
+ ['C:\\temp\\some ^%file% > nul', '^"C:\\temp\\some ^^^%file^% ^> nul^"']
+])
+def test_quote_args(s, expected):
+ cmd = ShellModule()
+ actual = cmd.quote(s)
+ assert actual == expected
diff --git a/test/units/plugins/shell/test_powershell.py b/test/units/plugins/shell/test_powershell.py
new file mode 100644
index 0000000..c94baab
--- /dev/null
+++ b/test/units/plugins/shell/test_powershell.py
@@ -0,0 +1,83 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ansible.plugins.shell.powershell import _parse_clixml, ShellModule
+
+
+def test_parse_clixml_empty():
+ empty = b'#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"></Objs>'
+ expected = b''
+ actual = _parse_clixml(empty)
+ assert actual == expected
+
+
+def test_parse_clixml_with_progress():
+ progress = b'#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">' \
+ b'<Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS>' \
+ b'<I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil />' \
+ b'<PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj></Objs>'
+ expected = b''
+ actual = _parse_clixml(progress)
+ assert actual == expected
+
+
+def test_parse_clixml_single_stream():
+ single_stream = b'#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">' \
+ b'<S S="Error">fake : The term \'fake\' is not recognized as the name of a cmdlet. Check _x000D__x000A_</S>' \
+ b'<S S="Error">the spelling of the name, or if a path was included._x000D__x000A_</S>' \
+ b'<S S="Error">At line:1 char:1_x000D__x000A_</S>' \
+ b'<S S="Error">+ fake cmdlet_x000D__x000A_</S><S S="Error">+ ~~~~_x000D__x000A_</S>' \
+ b'<S S="Error"> + CategoryInfo : ObjectNotFound: (fake:String) [], CommandNotFoundException_x000D__x000A_</S>' \
+ b'<S S="Error"> + FullyQualifiedErrorId : CommandNotFoundException_x000D__x000A_</S><S S="Error"> _x000D__x000A_</S>' \
+ b'</Objs>'
+ expected = b"fake : The term 'fake' is not recognized as the name of a cmdlet. Check \r\n" \
+ b"the spelling of the name, or if a path was included.\r\n" \
+ b"At line:1 char:1\r\n" \
+ b"+ fake cmdlet\r\n" \
+ b"+ ~~~~\r\n" \
+ b" + CategoryInfo : ObjectNotFound: (fake:String) [], CommandNotFoundException\r\n" \
+ b" + FullyQualifiedErrorId : CommandNotFoundException\r\n "
+ actual = _parse_clixml(single_stream)
+ assert actual == expected
+
+
+def test_parse_clixml_multiple_streams():
+ multiple_stream = b'#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">' \
+ b'<S S="Error">fake : The term \'fake\' is not recognized as the name of a cmdlet. Check _x000D__x000A_</S>' \
+ b'<S S="Error">the spelling of the name, or if a path was included._x000D__x000A_</S>' \
+ b'<S S="Error">At line:1 char:1_x000D__x000A_</S>' \
+ b'<S S="Error">+ fake cmdlet_x000D__x000A_</S><S S="Error">+ ~~~~_x000D__x000A_</S>' \
+ b'<S S="Error"> + CategoryInfo : ObjectNotFound: (fake:String) [], CommandNotFoundException_x000D__x000A_</S>' \
+ b'<S S="Error"> + FullyQualifiedErrorId : CommandNotFoundException_x000D__x000A_</S><S S="Error"> _x000D__x000A_</S>' \
+ b'<S S="Info">hi info</S>' \
+ b'</Objs>'
+ expected = b"hi info"
+ actual = _parse_clixml(multiple_stream, stream="Info")
+ assert actual == expected
+
+
+def test_parse_clixml_multiple_elements():
+ multiple_elements = b'#< CLIXML\r\n#< CLIXML\r\n<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">' \
+ b'<Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS>' \
+ b'<I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil />' \
+ b'<PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj>' \
+ b'<S S="Error">Error 1</S></Objs>' \
+ b'<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><Obj S="progress" RefId="0">' \
+ b'<TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS>' \
+ b'<I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil />' \
+ b'<PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj>' \
+ b'<Obj S="progress" RefId="1"><TNRef RefId="0" /><MS><I64 N="SourceId">2</I64>' \
+ b'<PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil />' \
+ b'<PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj>' \
+ b'<S S="Error">Error 2</S></Objs>'
+ expected = b"Error 1\r\nError 2"
+ actual = _parse_clixml(multiple_elements)
+ assert actual == expected
+
+
+def test_join_path_unc():
+ pwsh = ShellModule()
+ unc_path_parts = ['\\\\host\\share\\dir1\\\\dir2\\', '\\dir3/dir4', 'dir5', 'dir6\\']
+ expected = '\\\\host\\share\\dir1\\dir2\\dir3\\dir4\\dir5\\dir6'
+ actual = pwsh.join_path(*unc_path_parts)
+ assert actual == expected
diff --git a/test/units/plugins/strategy/__init__.py b/test/units/plugins/strategy/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/plugins/strategy/__init__.py
diff --git a/test/units/plugins/strategy/test_linear.py b/test/units/plugins/strategy/test_linear.py
new file mode 100644
index 0000000..b39c142
--- /dev/null
+++ b/test/units/plugins/strategy/test_linear.py
@@ -0,0 +1,320 @@
+# Copyright (c) 2018 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 units.compat import unittest
+from unittest.mock import patch, MagicMock
+
+from ansible.executor.play_iterator import PlayIterator
+from ansible.playbook import Playbook
+from ansible.playbook.play_context import PlayContext
+from ansible.plugins.strategy.linear import StrategyModule
+from ansible.executor.task_queue_manager import TaskQueueManager
+
+from units.mock.loader import DictDataLoader
+from units.mock.path import mock_unfrackpath_noop
+
+
+class TestStrategyLinear(unittest.TestCase):
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_noop(self):
+ fake_loader = DictDataLoader({
+ "test_play.yml": """
+ - hosts: all
+ gather_facts: no
+ tasks:
+ - block:
+ - block:
+ - name: task1
+ debug: msg='task1'
+ failed_when: inventory_hostname == 'host01'
+
+ - name: task2
+ debug: msg='task2'
+
+ rescue:
+ - name: rescue1
+ debug: msg='rescue1'
+
+ - name: rescue2
+ debug: msg='rescue2'
+ """,
+ })
+
+ mock_var_manager = MagicMock()
+ mock_var_manager._fact_cache = dict()
+ mock_var_manager.get_vars.return_value = dict()
+
+ p = Playbook.load('test_play.yml', loader=fake_loader, variable_manager=mock_var_manager)
+
+ inventory = MagicMock()
+ inventory.hosts = {}
+ hosts = []
+ for i in range(0, 2):
+ host = MagicMock()
+ host.name = host.get_name.return_value = 'host%02d' % i
+ hosts.append(host)
+ inventory.hosts[host.name] = host
+ inventory.get_hosts.return_value = hosts
+ inventory.filter_hosts.return_value = hosts
+
+ mock_var_manager._fact_cache['host00'] = dict()
+
+ play_context = PlayContext(play=p._entries[0])
+
+ itr = PlayIterator(
+ inventory=inventory,
+ play=p._entries[0],
+ play_context=play_context,
+ variable_manager=mock_var_manager,
+ all_vars=dict(),
+ )
+
+ tqm = TaskQueueManager(
+ inventory=inventory,
+ variable_manager=mock_var_manager,
+ loader=fake_loader,
+ passwords=None,
+ forks=5,
+ )
+ tqm._initialize_processes(3)
+ strategy = StrategyModule(tqm)
+ strategy._hosts_cache = [h.name for h in hosts]
+ strategy._hosts_cache_all = [h.name for h in hosts]
+
+ # implicit meta: flush_handlers
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'meta')
+ self.assertEqual(host2_task.action, 'meta')
+
+ # debug: task1, debug: task1
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'debug')
+ self.assertEqual(host2_task.action, 'debug')
+ self.assertEqual(host1_task.name, 'task1')
+ self.assertEqual(host2_task.name, 'task1')
+
+ # mark the second host failed
+ itr.mark_host_failed(hosts[1])
+
+ # debug: task2, meta: noop
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'debug')
+ self.assertEqual(host2_task.action, 'meta')
+ self.assertEqual(host1_task.name, 'task2')
+ self.assertEqual(host2_task.name, '')
+
+ # meta: noop, debug: rescue1
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'meta')
+ self.assertEqual(host2_task.action, 'debug')
+ self.assertEqual(host1_task.name, '')
+ self.assertEqual(host2_task.name, 'rescue1')
+
+ # meta: noop, debug: rescue2
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'meta')
+ self.assertEqual(host2_task.action, 'debug')
+ self.assertEqual(host1_task.name, '')
+ self.assertEqual(host2_task.name, 'rescue2')
+
+ # implicit meta: flush_handlers
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'meta')
+ self.assertEqual(host2_task.action, 'meta')
+
+ # implicit meta: flush_handlers
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'meta')
+ self.assertEqual(host2_task.action, 'meta')
+
+ # end of iteration
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNone(host1_task)
+ self.assertIsNone(host2_task)
+
+ def test_noop_64999(self):
+ fake_loader = DictDataLoader({
+ "test_play.yml": """
+ - hosts: all
+ gather_facts: no
+ tasks:
+ - name: block1
+ block:
+ - name: block2
+ block:
+ - name: block3
+ block:
+ - name: task1
+ debug:
+ failed_when: inventory_hostname == 'host01'
+ rescue:
+ - name: rescue1
+ debug:
+ msg: "rescue"
+ - name: after_rescue1
+ debug:
+ msg: "after_rescue1"
+ """,
+ })
+
+ mock_var_manager = MagicMock()
+ mock_var_manager._fact_cache = dict()
+ mock_var_manager.get_vars.return_value = dict()
+
+ p = Playbook.load('test_play.yml', loader=fake_loader, variable_manager=mock_var_manager)
+
+ inventory = MagicMock()
+ inventory.hosts = {}
+ hosts = []
+ for i in range(0, 2):
+ host = MagicMock()
+ host.name = host.get_name.return_value = 'host%02d' % i
+ hosts.append(host)
+ inventory.hosts[host.name] = host
+ inventory.get_hosts.return_value = hosts
+ inventory.filter_hosts.return_value = hosts
+
+ mock_var_manager._fact_cache['host00'] = dict()
+
+ play_context = PlayContext(play=p._entries[0])
+
+ itr = PlayIterator(
+ inventory=inventory,
+ play=p._entries[0],
+ play_context=play_context,
+ variable_manager=mock_var_manager,
+ all_vars=dict(),
+ )
+
+ tqm = TaskQueueManager(
+ inventory=inventory,
+ variable_manager=mock_var_manager,
+ loader=fake_loader,
+ passwords=None,
+ forks=5,
+ )
+ tqm._initialize_processes(3)
+ strategy = StrategyModule(tqm)
+ strategy._hosts_cache = [h.name for h in hosts]
+ strategy._hosts_cache_all = [h.name for h in hosts]
+
+ # implicit meta: flush_handlers
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'meta')
+ self.assertEqual(host2_task.action, 'meta')
+
+ # debug: task1, debug: task1
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'debug')
+ self.assertEqual(host2_task.action, 'debug')
+ self.assertEqual(host1_task.name, 'task1')
+ self.assertEqual(host2_task.name, 'task1')
+
+ # mark the second host failed
+ itr.mark_host_failed(hosts[1])
+
+ # meta: noop, debug: rescue1
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'meta')
+ self.assertEqual(host2_task.action, 'debug')
+ self.assertEqual(host1_task.name, '')
+ self.assertEqual(host2_task.name, 'rescue1')
+
+ # debug: after_rescue1, debug: after_rescue1
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'debug')
+ self.assertEqual(host2_task.action, 'debug')
+ self.assertEqual(host1_task.name, 'after_rescue1')
+ self.assertEqual(host2_task.name, 'after_rescue1')
+
+ # implicit meta: flush_handlers
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'meta')
+ self.assertEqual(host2_task.action, 'meta')
+
+ # implicit meta: flush_handlers
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNotNone(host1_task)
+ self.assertIsNotNone(host2_task)
+ self.assertEqual(host1_task.action, 'meta')
+ self.assertEqual(host2_task.action, 'meta')
+
+ # end of iteration
+ hosts_left = strategy.get_hosts_left(itr)
+ hosts_tasks = strategy._get_next_task_lockstep(hosts_left, itr)
+ host1_task = hosts_tasks[0][1]
+ host2_task = hosts_tasks[1][1]
+ self.assertIsNone(host1_task)
+ self.assertIsNone(host2_task)
diff --git a/test/units/plugins/strategy/test_strategy.py b/test/units/plugins/strategy/test_strategy.py
new file mode 100644
index 0000000..f935f4b
--- /dev/null
+++ b/test/units/plugins/strategy/test_strategy.py
@@ -0,0 +1,492 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.mock.loader import DictDataLoader
+import uuid
+
+from units.compat import unittest
+from unittest.mock import patch, MagicMock
+from ansible.executor.process.worker import WorkerProcess
+from ansible.executor.task_queue_manager import TaskQueueManager
+from ansible.executor.task_result import TaskResult
+from ansible.inventory.host import Host
+from ansible.module_utils.six.moves import queue as Queue
+from ansible.playbook.block import Block
+from ansible.playbook.handler import Handler
+from ansible.plugins.strategy import StrategyBase
+
+import pytest
+
+pytestmark = pytest.mark.skipif(True, reason="Temporarily disabled due to fragile tests that need rewritten")
+
+
+class TestStrategyBase(unittest.TestCase):
+
+ def test_strategy_base_init(self):
+ queue_items = []
+
+ def _queue_empty(*args, **kwargs):
+ return len(queue_items) == 0
+
+ def _queue_get(*args, **kwargs):
+ if len(queue_items) == 0:
+ raise Queue.Empty
+ else:
+ return queue_items.pop()
+
+ def _queue_put(item, *args, **kwargs):
+ queue_items.append(item)
+
+ mock_queue = MagicMock()
+ mock_queue.empty.side_effect = _queue_empty
+ mock_queue.get.side_effect = _queue_get
+ mock_queue.put.side_effect = _queue_put
+
+ mock_tqm = MagicMock(TaskQueueManager)
+ mock_tqm._final_q = mock_queue
+ mock_tqm._workers = []
+ strategy_base = StrategyBase(tqm=mock_tqm)
+ strategy_base.cleanup()
+
+ def test_strategy_base_run(self):
+ queue_items = []
+
+ def _queue_empty(*args, **kwargs):
+ return len(queue_items) == 0
+
+ def _queue_get(*args, **kwargs):
+ if len(queue_items) == 0:
+ raise Queue.Empty
+ else:
+ return queue_items.pop()
+
+ def _queue_put(item, *args, **kwargs):
+ queue_items.append(item)
+
+ mock_queue = MagicMock()
+ mock_queue.empty.side_effect = _queue_empty
+ mock_queue.get.side_effect = _queue_get
+ mock_queue.put.side_effect = _queue_put
+
+ mock_tqm = MagicMock(TaskQueueManager)
+ mock_tqm._final_q = mock_queue
+ mock_tqm._stats = MagicMock()
+ mock_tqm.send_callback.return_value = None
+
+ for attr in ('RUN_OK', 'RUN_ERROR', 'RUN_FAILED_HOSTS', 'RUN_UNREACHABLE_HOSTS'):
+ setattr(mock_tqm, attr, getattr(TaskQueueManager, attr))
+
+ mock_iterator = MagicMock()
+ mock_iterator._play = MagicMock()
+ mock_iterator._play.handlers = []
+
+ mock_play_context = MagicMock()
+
+ mock_tqm._failed_hosts = dict()
+ mock_tqm._unreachable_hosts = dict()
+ mock_tqm._workers = []
+ strategy_base = StrategyBase(tqm=mock_tqm)
+
+ mock_host = MagicMock()
+ mock_host.name = 'host1'
+
+ self.assertEqual(strategy_base.run(iterator=mock_iterator, play_context=mock_play_context), mock_tqm.RUN_OK)
+ self.assertEqual(strategy_base.run(iterator=mock_iterator, play_context=mock_play_context, result=TaskQueueManager.RUN_ERROR), mock_tqm.RUN_ERROR)
+ mock_tqm._failed_hosts = dict(host1=True)
+ mock_iterator.get_failed_hosts.return_value = [mock_host]
+ self.assertEqual(strategy_base.run(iterator=mock_iterator, play_context=mock_play_context, result=False), mock_tqm.RUN_FAILED_HOSTS)
+ mock_tqm._unreachable_hosts = dict(host1=True)
+ mock_iterator.get_failed_hosts.return_value = []
+ self.assertEqual(strategy_base.run(iterator=mock_iterator, play_context=mock_play_context, result=False), mock_tqm.RUN_UNREACHABLE_HOSTS)
+ strategy_base.cleanup()
+
+ def test_strategy_base_get_hosts(self):
+ queue_items = []
+
+ def _queue_empty(*args, **kwargs):
+ return len(queue_items) == 0
+
+ def _queue_get(*args, **kwargs):
+ if len(queue_items) == 0:
+ raise Queue.Empty
+ else:
+ return queue_items.pop()
+
+ def _queue_put(item, *args, **kwargs):
+ queue_items.append(item)
+
+ mock_queue = MagicMock()
+ mock_queue.empty.side_effect = _queue_empty
+ mock_queue.get.side_effect = _queue_get
+ mock_queue.put.side_effect = _queue_put
+
+ mock_hosts = []
+ for i in range(0, 5):
+ mock_host = MagicMock()
+ mock_host.name = "host%02d" % (i + 1)
+ mock_host.has_hostkey = True
+ mock_hosts.append(mock_host)
+
+ mock_hosts_names = [h.name for h in mock_hosts]
+
+ mock_inventory = MagicMock()
+ mock_inventory.get_hosts.return_value = mock_hosts
+
+ mock_tqm = MagicMock()
+ mock_tqm._final_q = mock_queue
+ mock_tqm.get_inventory.return_value = mock_inventory
+
+ mock_play = MagicMock()
+ mock_play.hosts = ["host%02d" % (i + 1) for i in range(0, 5)]
+
+ strategy_base = StrategyBase(tqm=mock_tqm)
+ strategy_base._hosts_cache = strategy_base._hosts_cache_all = mock_hosts_names
+
+ mock_tqm._failed_hosts = []
+ mock_tqm._unreachable_hosts = []
+ self.assertEqual(strategy_base.get_hosts_remaining(play=mock_play), [h.name for h in mock_hosts])
+
+ mock_tqm._failed_hosts = ["host01"]
+ self.assertEqual(strategy_base.get_hosts_remaining(play=mock_play), [h.name for h in mock_hosts[1:]])
+ self.assertEqual(strategy_base.get_failed_hosts(play=mock_play), [mock_hosts[0].name])
+
+ mock_tqm._unreachable_hosts = ["host02"]
+ self.assertEqual(strategy_base.get_hosts_remaining(play=mock_play), [h.name for h in mock_hosts[2:]])
+ strategy_base.cleanup()
+
+ @patch.object(WorkerProcess, 'run')
+ def test_strategy_base_queue_task(self, mock_worker):
+ def fake_run(self):
+ return
+
+ mock_worker.run.side_effect = fake_run
+
+ fake_loader = DictDataLoader()
+ mock_var_manager = MagicMock()
+ mock_host = MagicMock()
+ mock_host.get_vars.return_value = dict()
+ mock_host.has_hostkey = True
+ mock_inventory = MagicMock()
+ mock_inventory.get.return_value = mock_host
+
+ tqm = TaskQueueManager(
+ inventory=mock_inventory,
+ variable_manager=mock_var_manager,
+ loader=fake_loader,
+ passwords=None,
+ forks=3,
+ )
+ tqm._initialize_processes(3)
+ tqm.hostvars = dict()
+
+ mock_task = MagicMock()
+ mock_task._uuid = 'abcd'
+ mock_task.throttle = 0
+
+ try:
+ strategy_base = StrategyBase(tqm=tqm)
+ strategy_base._queue_task(host=mock_host, task=mock_task, task_vars=dict(), play_context=MagicMock())
+ self.assertEqual(strategy_base._cur_worker, 1)
+ self.assertEqual(strategy_base._pending_results, 1)
+ strategy_base._queue_task(host=mock_host, task=mock_task, task_vars=dict(), play_context=MagicMock())
+ self.assertEqual(strategy_base._cur_worker, 2)
+ self.assertEqual(strategy_base._pending_results, 2)
+ strategy_base._queue_task(host=mock_host, task=mock_task, task_vars=dict(), play_context=MagicMock())
+ self.assertEqual(strategy_base._cur_worker, 0)
+ self.assertEqual(strategy_base._pending_results, 3)
+ finally:
+ tqm.cleanup()
+
+ def test_strategy_base_process_pending_results(self):
+ mock_tqm = MagicMock()
+ mock_tqm._terminated = False
+ mock_tqm._failed_hosts = dict()
+ mock_tqm._unreachable_hosts = dict()
+ mock_tqm.send_callback.return_value = None
+
+ queue_items = []
+
+ def _queue_empty(*args, **kwargs):
+ return len(queue_items) == 0
+
+ def _queue_get(*args, **kwargs):
+ if len(queue_items) == 0:
+ raise Queue.Empty
+ else:
+ return queue_items.pop()
+
+ def _queue_put(item, *args, **kwargs):
+ queue_items.append(item)
+
+ mock_queue = MagicMock()
+ mock_queue.empty.side_effect = _queue_empty
+ mock_queue.get.side_effect = _queue_get
+ mock_queue.put.side_effect = _queue_put
+ mock_tqm._final_q = mock_queue
+
+ mock_tqm._stats = MagicMock()
+ mock_tqm._stats.increment.return_value = None
+
+ mock_play = MagicMock()
+
+ mock_host = MagicMock()
+ mock_host.name = 'test01'
+ mock_host.vars = dict()
+ mock_host.get_vars.return_value = dict()
+ mock_host.has_hostkey = True
+
+ mock_task = MagicMock()
+ mock_task._role = None
+ mock_task._parent = None
+ mock_task.ignore_errors = False
+ mock_task.ignore_unreachable = False
+ mock_task._uuid = str(uuid.uuid4())
+ mock_task.loop = None
+ mock_task.copy.return_value = mock_task
+
+ mock_handler_task = Handler()
+ mock_handler_task.name = 'test handler'
+ mock_handler_task.action = 'foo'
+ mock_handler_task._parent = None
+ mock_handler_task._uuid = 'xxxxxxxxxxxxx'
+
+ mock_iterator = MagicMock()
+ mock_iterator._play = mock_play
+ mock_iterator.mark_host_failed.return_value = None
+ mock_iterator.get_next_task_for_host.return_value = (None, None)
+
+ mock_handler_block = MagicMock()
+ mock_handler_block.name = '' # implicit unnamed block
+ mock_handler_block.block = [mock_handler_task]
+ mock_handler_block.rescue = []
+ mock_handler_block.always = []
+ mock_play.handlers = [mock_handler_block]
+
+ mock_group = MagicMock()
+ mock_group.add_host.return_value = None
+
+ def _get_host(host_name):
+ if host_name == 'test01':
+ return mock_host
+ return None
+
+ def _get_group(group_name):
+ if group_name in ('all', 'foo'):
+ return mock_group
+ return None
+
+ mock_inventory = MagicMock()
+ mock_inventory._hosts_cache = dict()
+ mock_inventory.hosts.return_value = mock_host
+ mock_inventory.get_host.side_effect = _get_host
+ mock_inventory.get_group.side_effect = _get_group
+ mock_inventory.clear_pattern_cache.return_value = None
+ mock_inventory.get_host_vars.return_value = {}
+ mock_inventory.hosts.get.return_value = mock_host
+
+ mock_var_mgr = MagicMock()
+ mock_var_mgr.set_host_variable.return_value = None
+ mock_var_mgr.set_host_facts.return_value = None
+ mock_var_mgr.get_vars.return_value = dict()
+
+ strategy_base = StrategyBase(tqm=mock_tqm)
+ strategy_base._inventory = mock_inventory
+ strategy_base._variable_manager = mock_var_mgr
+ strategy_base._blocked_hosts = dict()
+
+ def _has_dead_workers():
+ return False
+
+ strategy_base._tqm.has_dead_workers.side_effect = _has_dead_workers
+ results = strategy_base._wait_on_pending_results(iterator=mock_iterator)
+ self.assertEqual(len(results), 0)
+
+ task_result = TaskResult(host=mock_host.name, task=mock_task._uuid, return_data=dict(changed=True))
+ queue_items.append(task_result)
+ strategy_base._blocked_hosts['test01'] = True
+ strategy_base._pending_results = 1
+
+ def mock_queued_task_cache():
+ return {
+ (mock_host.name, mock_task._uuid): {
+ 'task': mock_task,
+ 'host': mock_host,
+ 'task_vars': {},
+ 'play_context': {},
+ }
+ }
+
+ strategy_base._queued_task_cache = mock_queued_task_cache()
+ results = strategy_base._wait_on_pending_results(iterator=mock_iterator)
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0], task_result)
+ self.assertEqual(strategy_base._pending_results, 0)
+ self.assertNotIn('test01', strategy_base._blocked_hosts)
+
+ task_result = TaskResult(host=mock_host.name, task=mock_task._uuid, return_data='{"failed":true}')
+ queue_items.append(task_result)
+ strategy_base._blocked_hosts['test01'] = True
+ strategy_base._pending_results = 1
+ mock_iterator.is_failed.return_value = True
+ strategy_base._queued_task_cache = mock_queued_task_cache()
+ results = strategy_base._wait_on_pending_results(iterator=mock_iterator)
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0], task_result)
+ self.assertEqual(strategy_base._pending_results, 0)
+ self.assertNotIn('test01', strategy_base._blocked_hosts)
+ # self.assertIn('test01', mock_tqm._failed_hosts)
+ # del mock_tqm._failed_hosts['test01']
+ mock_iterator.is_failed.return_value = False
+
+ task_result = TaskResult(host=mock_host.name, task=mock_task._uuid, return_data='{"unreachable": true}')
+ queue_items.append(task_result)
+ strategy_base._blocked_hosts['test01'] = True
+ strategy_base._pending_results = 1
+ strategy_base._queued_task_cache = mock_queued_task_cache()
+ results = strategy_base._wait_on_pending_results(iterator=mock_iterator)
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0], task_result)
+ self.assertEqual(strategy_base._pending_results, 0)
+ self.assertNotIn('test01', strategy_base._blocked_hosts)
+ self.assertIn('test01', mock_tqm._unreachable_hosts)
+ del mock_tqm._unreachable_hosts['test01']
+
+ task_result = TaskResult(host=mock_host.name, task=mock_task._uuid, return_data='{"skipped": true}')
+ queue_items.append(task_result)
+ strategy_base._blocked_hosts['test01'] = True
+ strategy_base._pending_results = 1
+ strategy_base._queued_task_cache = mock_queued_task_cache()
+ results = strategy_base._wait_on_pending_results(iterator=mock_iterator)
+ self.assertEqual(len(results), 1)
+ self.assertEqual(results[0], task_result)
+ self.assertEqual(strategy_base._pending_results, 0)
+ self.assertNotIn('test01', strategy_base._blocked_hosts)
+
+ queue_items.append(TaskResult(host=mock_host.name, task=mock_task._uuid, return_data=dict(add_host=dict(host_name='newhost01', new_groups=['foo']))))
+ strategy_base._blocked_hosts['test01'] = True
+ strategy_base._pending_results = 1
+ strategy_base._queued_task_cache = mock_queued_task_cache()
+ results = strategy_base._wait_on_pending_results(iterator=mock_iterator)
+ self.assertEqual(len(results), 1)
+ self.assertEqual(strategy_base._pending_results, 0)
+ self.assertNotIn('test01', strategy_base._blocked_hosts)
+
+ queue_items.append(TaskResult(host=mock_host.name, task=mock_task._uuid, return_data=dict(add_group=dict(group_name='foo'))))
+ strategy_base._blocked_hosts['test01'] = True
+ strategy_base._pending_results = 1
+ strategy_base._queued_task_cache = mock_queued_task_cache()
+ results = strategy_base._wait_on_pending_results(iterator=mock_iterator)
+ self.assertEqual(len(results), 1)
+ self.assertEqual(strategy_base._pending_results, 0)
+ self.assertNotIn('test01', strategy_base._blocked_hosts)
+
+ queue_items.append(TaskResult(host=mock_host.name, task=mock_task._uuid, return_data=dict(changed=True, _ansible_notify=['test handler'])))
+ strategy_base._blocked_hosts['test01'] = True
+ strategy_base._pending_results = 1
+ strategy_base._queued_task_cache = mock_queued_task_cache()
+ results = strategy_base._wait_on_pending_results(iterator=mock_iterator)
+ self.assertEqual(len(results), 1)
+ self.assertEqual(strategy_base._pending_results, 0)
+ self.assertNotIn('test01', strategy_base._blocked_hosts)
+ self.assertEqual(mock_iterator._play.handlers[0].block[0], mock_handler_task)
+
+ # queue_items.append(('set_host_var', mock_host, mock_task, None, 'foo', 'bar'))
+ # results = strategy_base._process_pending_results(iterator=mock_iterator)
+ # self.assertEqual(len(results), 0)
+ # self.assertEqual(strategy_base._pending_results, 1)
+
+ # queue_items.append(('set_host_facts', mock_host, mock_task, None, 'foo', dict()))
+ # results = strategy_base._process_pending_results(iterator=mock_iterator)
+ # self.assertEqual(len(results), 0)
+ # self.assertEqual(strategy_base._pending_results, 1)
+
+ # queue_items.append(('bad'))
+ # self.assertRaises(AnsibleError, strategy_base._process_pending_results, iterator=mock_iterator)
+ strategy_base.cleanup()
+
+ def test_strategy_base_load_included_file(self):
+ fake_loader = DictDataLoader({
+ "test.yml": """
+ - debug: msg='foo'
+ """,
+ "bad.yml": """
+ """,
+ })
+
+ queue_items = []
+
+ def _queue_empty(*args, **kwargs):
+ return len(queue_items) == 0
+
+ def _queue_get(*args, **kwargs):
+ if len(queue_items) == 0:
+ raise Queue.Empty
+ else:
+ return queue_items.pop()
+
+ def _queue_put(item, *args, **kwargs):
+ queue_items.append(item)
+
+ mock_queue = MagicMock()
+ mock_queue.empty.side_effect = _queue_empty
+ mock_queue.get.side_effect = _queue_get
+ mock_queue.put.side_effect = _queue_put
+
+ mock_tqm = MagicMock()
+ mock_tqm._final_q = mock_queue
+
+ strategy_base = StrategyBase(tqm=mock_tqm)
+ strategy_base._loader = fake_loader
+ strategy_base.cleanup()
+
+ mock_play = MagicMock()
+
+ mock_block = MagicMock()
+ mock_block._play = mock_play
+ mock_block.vars = dict()
+
+ mock_task = MagicMock()
+ mock_task._block = mock_block
+ mock_task._role = None
+
+ # NOTE Mocking calls below to account for passing parent_block=ti_copy.build_parent_block()
+ # into load_list_of_blocks() in _load_included_file. Not doing so meant that retrieving
+ # `collection` attr from parent would result in getting MagicMock instance
+ # instead of an empty list.
+ mock_task._parent = MagicMock()
+ mock_task.copy.return_value = mock_task
+ mock_task.build_parent_block.return_value = mock_block
+ mock_block._get_parent_attribute.return_value = None
+
+ mock_iterator = MagicMock()
+ mock_iterator.mark_host_failed.return_value = None
+
+ mock_inc_file = MagicMock()
+ mock_inc_file._task = mock_task
+
+ mock_inc_file._filename = "test.yml"
+ res = strategy_base._load_included_file(included_file=mock_inc_file, iterator=mock_iterator)
+ self.assertEqual(len(res), 1)
+ self.assertTrue(isinstance(res[0], Block))
+
+ mock_inc_file._filename = "bad.yml"
+ res = strategy_base._load_included_file(included_file=mock_inc_file, iterator=mock_iterator)
+ self.assertEqual(res, [])
diff --git a/test/units/plugins/test_plugins.py b/test/units/plugins/test_plugins.py
new file mode 100644
index 0000000..be123b1
--- /dev/null
+++ b/test/units/plugins/test_plugins.py
@@ -0,0 +1,133 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+from units.compat import unittest
+from unittest.mock import patch, MagicMock
+from ansible.plugins.loader import PluginLoader, PluginPathContext
+
+
+class TestErrors(unittest.TestCase):
+
+ @patch.object(PluginLoader, '_get_paths')
+ def test_print_paths(self, mock_method):
+ mock_method.return_value = ['/path/one', '/path/two', '/path/three']
+ pl = PluginLoader('foo', 'foo', '', 'test_plugins')
+ paths = pl.print_paths()
+ expected_paths = os.pathsep.join(['/path/one', '/path/two', '/path/three'])
+ self.assertEqual(paths, expected_paths)
+
+ def test_plugins__get_package_paths_no_package(self):
+ pl = PluginLoader('test', '', 'test', 'test_plugin')
+ self.assertEqual(pl._get_package_paths(), [])
+
+ def test_plugins__get_package_paths_with_package(self):
+ # the _get_package_paths() call uses __import__ to load a
+ # python library, and then uses the __file__ attribute of
+ # the result for that to get the library path, so we mock
+ # that here and patch the builtin to use our mocked result
+ foo = MagicMock()
+ bar = MagicMock()
+ bam = MagicMock()
+ bam.__file__ = '/path/to/my/foo/bar/bam/__init__.py'
+ bar.bam = bam
+ foo.return_value.bar = bar
+ pl = PluginLoader('test', 'foo.bar.bam', 'test', 'test_plugin')
+ with patch('builtins.__import__', foo):
+ self.assertEqual(pl._get_package_paths(), ['/path/to/my/foo/bar/bam'])
+
+ def test_plugins__get_paths(self):
+ pl = PluginLoader('test', '', 'test', 'test_plugin')
+ pl._paths = [PluginPathContext('/path/one', False),
+ PluginPathContext('/path/two', True)]
+ self.assertEqual(pl._get_paths(), ['/path/one', '/path/two'])
+
+ # NOT YET WORKING
+ # def fake_glob(path):
+ # if path == 'test/*':
+ # return ['test/foo', 'test/bar', 'test/bam']
+ # elif path == 'test/*/*'
+ # m._paths = None
+ # mock_glob = MagicMock()
+ # mock_glob.return_value = []
+ # with patch('glob.glob', mock_glob):
+ # pass
+
+ def assertPluginLoaderConfigBecomes(self, arg, expected):
+ pl = PluginLoader('test', '', arg, 'test_plugin')
+ self.assertEqual(pl.config, expected)
+
+ def test_plugin__init_config_list(self):
+ config = ['/one', '/two']
+ self.assertPluginLoaderConfigBecomes(config, config)
+
+ def test_plugin__init_config_str(self):
+ self.assertPluginLoaderConfigBecomes('test', ['test'])
+
+ def test_plugin__init_config_none(self):
+ self.assertPluginLoaderConfigBecomes(None, [])
+
+ def test__load_module_source_no_duplicate_names(self):
+ '''
+ This test simulates importing 2 plugins with the same name,
+ and validating that the import is short circuited if a file with the same name
+ has already been imported
+ '''
+
+ fixture_path = os.path.join(os.path.dirname(__file__), 'loader_fixtures')
+
+ pl = PluginLoader('test', '', 'test', 'test_plugin')
+ one = pl._load_module_source('import_fixture', os.path.join(fixture_path, 'import_fixture.py'))
+ # This line wouldn't even succeed if we didn't short circuit on finding a duplicate name
+ two = pl._load_module_source('import_fixture', '/path/to/import_fixture.py')
+
+ self.assertEqual(one, two)
+
+ @patch('ansible.plugins.loader.glob')
+ @patch.object(PluginLoader, '_get_paths_with_context')
+ def test_all_no_duplicate_names(self, gp_mock, glob_mock):
+ '''
+ This test goes along with ``test__load_module_source_no_duplicate_names``
+ and ensures that we ignore duplicate imports on multiple paths
+ '''
+
+ fixture_path = os.path.join(os.path.dirname(__file__), 'loader_fixtures')
+
+ gp_mock.return_value = [
+ MagicMock(path=fixture_path),
+ MagicMock(path='/path/to'),
+ ]
+
+ glob_mock.glob.side_effect = [
+ [os.path.join(fixture_path, 'import_fixture.py')],
+ ['/path/to/import_fixture.py']
+ ]
+
+ pl = PluginLoader('test', '', 'test', 'test_plugins')
+ # Aside from needing ``list()`` so we can do a len, ``PluginLoader.all`` returns a generator
+ # so ``list()`` actually causes ``PluginLoader.all`` to run.
+ plugins = list(pl.all())
+ self.assertEqual(len(plugins), 1)
+
+ self.assertIn(os.path.join(fixture_path, 'import_fixture.py'), pl._module_cache)
+ self.assertNotIn('/path/to/import_fixture.py', pl._module_cache)
diff --git a/test/units/regex/test_invalid_var_names.py b/test/units/regex/test_invalid_var_names.py
new file mode 100644
index 0000000..d47e68d
--- /dev/null
+++ b/test/units/regex/test_invalid_var_names.py
@@ -0,0 +1,27 @@
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+
+from ansible import constants as C
+
+
+test_cases = (('not-valid', ['-'], 'not_valid'), ('not!valid@either', ['!', '@'], 'not_valid_either'), ('1_nor_This', ['1'], '__nor_This'))
+
+
+class TestInvalidVars(unittest.TestCase):
+
+ def test_positive_matches(self):
+
+ for name, invalid, sanitized in test_cases:
+ self.assertEqual(C.INVALID_VARIABLE_NAMES.findall(name), invalid)
+
+ def test_negative_matches(self):
+ for name in ('this_is_valid', 'Also_1_valid', 'noproblem'):
+ self.assertEqual(C.INVALID_VARIABLE_NAMES.findall(name), [])
+
+ def test_get_setting(self):
+
+ for name, invalid, sanitized in test_cases:
+ self.assertEqual(C.INVALID_VARIABLE_NAMES.sub('_', name), sanitized)
diff --git a/test/units/requirements.txt b/test/units/requirements.txt
new file mode 100644
index 0000000..1822ada
--- /dev/null
+++ b/test/units/requirements.txt
@@ -0,0 +1,4 @@
+bcrypt ; python_version >= '3.9' # controller only
+passlib ; python_version >= '3.9' # controller only
+pexpect ; python_version >= '3.9' # controller only
+pywinrm ; python_version >= '3.9' # controller only
diff --git a/test/units/template/__init__.py b/test/units/template/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/template/__init__.py
diff --git a/test/units/template/test_native_concat.py b/test/units/template/test_native_concat.py
new file mode 100644
index 0000000..ee1b7df
--- /dev/null
+++ b/test/units/template/test_native_concat.py
@@ -0,0 +1,25 @@
+# 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.playbook.conditional import Conditional
+from ansible.template import Templar
+
+from units.mock.loader import DictDataLoader
+
+
+def test_cond_eval():
+ fake_loader = DictDataLoader({})
+ # True must be stored in a variable to trigger templating. Using True
+ # directly would be caught by optimization for bools to short-circuit
+ # templating.
+ variables = {"foo": True}
+ templar = Templar(loader=fake_loader, variables=variables)
+ cond = Conditional(loader=fake_loader)
+ cond.when = ["foo"]
+
+ with templar.set_temporary_context(jinja2_native=True):
+ assert cond.evaluate_conditional(templar, variables)
diff --git a/test/units/template/test_templar.py b/test/units/template/test_templar.py
new file mode 100644
index 0000000..6747f76
--- /dev/null
+++ b/test/units/template/test_templar.py
@@ -0,0 +1,470 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from jinja2.runtime import Context
+
+from units.compat import unittest
+from unittest.mock import patch
+
+from ansible import constants as C
+from ansible.errors import AnsibleError, AnsibleUndefinedVariable
+from ansible.module_utils.six import string_types
+from ansible.template import Templar, AnsibleContext, AnsibleEnvironment, AnsibleUndefined
+from ansible.utils.unsafe_proxy import AnsibleUnsafe, wrap_var
+from units.mock.loader import DictDataLoader
+
+
+class BaseTemplar(object):
+ def setUp(self):
+ self.test_vars = dict(
+ foo="bar",
+ bam="{{foo}}",
+ num=1,
+ var_true=True,
+ var_false=False,
+ var_dict=dict(a="b"),
+ bad_dict="{a='b'",
+ var_list=[1],
+ recursive="{{recursive}}",
+ some_var="blip",
+ some_static_var="static_blip",
+ some_keyword="{{ foo }}",
+ some_unsafe_var=wrap_var("unsafe_blip"),
+ some_static_unsafe_var=wrap_var("static_unsafe_blip"),
+ some_unsafe_keyword=wrap_var("{{ foo }}"),
+ str_with_error="{{ 'str' | from_json }}",
+ )
+ self.fake_loader = DictDataLoader({
+ "/path/to/my_file.txt": "foo\n",
+ })
+ self.templar = Templar(loader=self.fake_loader, variables=self.test_vars)
+ self._ansible_context = AnsibleContext(self.templar.environment, {}, {}, {})
+
+ def is_unsafe(self, obj):
+ return self._ansible_context._is_unsafe(obj)
+
+
+# class used for testing arbitrary objects passed to template
+class SomeClass(object):
+ foo = 'bar'
+
+ def __init__(self):
+ self.blip = 'blip'
+
+
+class SomeUnsafeClass(AnsibleUnsafe):
+ def __init__(self):
+ super(SomeUnsafeClass, self).__init__()
+ self.blip = 'unsafe blip'
+
+
+class TestTemplarTemplate(BaseTemplar, unittest.TestCase):
+ def test_lookup_jinja_dict_key_in_static_vars(self):
+ res = self.templar.template("{'some_static_var': '{{ some_var }}'}",
+ static_vars=['some_static_var'])
+ # self.assertEqual(res['{{ a_keyword }}'], "blip")
+ print(res)
+
+ def test_is_possibly_template_true(self):
+ tests = [
+ '{{ foo }}',
+ '{% foo %}',
+ '{# foo #}',
+ '{# {{ foo }} #}',
+ '{# {{ nothing }} {# #}',
+ '{# {{ nothing }} {# #} #}',
+ '{% raw %}{{ foo }}{% endraw %}',
+ '{{',
+ '{%',
+ '{#',
+ '{% raw',
+ ]
+ for test in tests:
+ self.assertTrue(self.templar.is_possibly_template(test))
+
+ def test_is_possibly_template_false(self):
+ tests = [
+ '{',
+ '%',
+ '#',
+ 'foo',
+ '}}',
+ '%}',
+ 'raw %}',
+ '#}',
+ ]
+ for test in tests:
+ self.assertFalse(self.templar.is_possibly_template(test))
+
+ def test_is_possible_template(self):
+ """This test ensures that a broken template still gets templated"""
+ # Purposefully invalid jinja
+ self.assertRaises(AnsibleError, self.templar.template, '{{ foo|default(False)) }}')
+
+ def test_is_template_true(self):
+ tests = [
+ '{{ foo }}',
+ '{% foo %}',
+ '{# foo #}',
+ '{# {{ foo }} #}',
+ '{# {{ nothing }} {# #}',
+ '{# {{ nothing }} {# #} #}',
+ '{% raw %}{{ foo }}{% endraw %}',
+ ]
+ for test in tests:
+ self.assertTrue(self.templar.is_template(test))
+
+ def test_is_template_false(self):
+ tests = [
+ 'foo',
+ '{{ foo',
+ '{% foo',
+ '{# foo',
+ '{{ foo %}',
+ '{{ foo #}',
+ '{% foo }}',
+ '{% foo #}',
+ '{# foo %}',
+ '{# foo }}',
+ '{{ foo {{',
+ '{% raw %}{% foo %}',
+ ]
+ for test in tests:
+ self.assertFalse(self.templar.is_template(test))
+
+ def test_is_template_raw_string(self):
+ res = self.templar.is_template('foo')
+ self.assertFalse(res)
+
+ def test_is_template_none(self):
+ res = self.templar.is_template(None)
+ self.assertFalse(res)
+
+ def test_template_convert_bare_string(self):
+ res = self.templar.template('foo', convert_bare=True)
+ self.assertEqual(res, 'bar')
+
+ def test_template_convert_bare_nested(self):
+ res = self.templar.template('bam', convert_bare=True)
+ self.assertEqual(res, 'bar')
+
+ def test_template_convert_bare_unsafe(self):
+ res = self.templar.template('some_unsafe_var', convert_bare=True)
+ self.assertEqual(res, 'unsafe_blip')
+ # self.assertIsInstance(res, AnsibleUnsafe)
+ self.assertTrue(self.is_unsafe(res), 'returned value from template.template (%s) is not marked unsafe' % res)
+
+ def test_template_convert_bare_filter(self):
+ res = self.templar.template('bam|capitalize', convert_bare=True)
+ self.assertEqual(res, 'Bar')
+
+ def test_template_convert_bare_filter_unsafe(self):
+ res = self.templar.template('some_unsafe_var|capitalize', convert_bare=True)
+ self.assertEqual(res, 'Unsafe_blip')
+ # self.assertIsInstance(res, AnsibleUnsafe)
+ self.assertTrue(self.is_unsafe(res), 'returned value from template.template (%s) is not marked unsafe' % res)
+
+ def test_template_convert_data(self):
+ res = self.templar.template('{{foo}}', convert_data=True)
+ self.assertTrue(res)
+ self.assertEqual(res, 'bar')
+
+ def test_template_convert_data_template_in_data(self):
+ res = self.templar.template('{{bam}}', convert_data=True)
+ self.assertTrue(res)
+ self.assertEqual(res, 'bar')
+
+ def test_template_convert_data_bare(self):
+ res = self.templar.template('bam', convert_data=True)
+ self.assertTrue(res)
+ self.assertEqual(res, 'bam')
+
+ def test_template_convert_data_to_json(self):
+ res = self.templar.template('{{bam|to_json}}', convert_data=True)
+ self.assertTrue(res)
+ self.assertEqual(res, '"bar"')
+
+ def test_template_convert_data_convert_bare_data_bare(self):
+ res = self.templar.template('bam', convert_data=True, convert_bare=True)
+ self.assertTrue(res)
+ self.assertEqual(res, 'bar')
+
+ def test_template_unsafe_non_string(self):
+ unsafe_obj = AnsibleUnsafe()
+ res = self.templar.template(unsafe_obj)
+ self.assertTrue(self.is_unsafe(res), 'returned value from template.template (%s) is not marked unsafe' % res)
+
+ def test_template_unsafe_non_string_subclass(self):
+ unsafe_obj = SomeUnsafeClass()
+ res = self.templar.template(unsafe_obj)
+ self.assertTrue(self.is_unsafe(res), 'returned value from template.template (%s) is not marked unsafe' % res)
+
+ def test_weird(self):
+ data = u'''1 2 #}huh{# %}ddfg{% }}dfdfg{{ {%what%} {{#foo#}} {%{bar}%} {#%blip%#} {{asdfsd%} 3 4 {{foo}} 5 6 7'''
+ self.assertRaisesRegex(AnsibleError,
+ 'template error while templating string',
+ self.templar.template,
+ data)
+
+ def test_template_with_error(self):
+ """Check that AnsibleError is raised, fail if an unhandled exception is raised"""
+ self.assertRaises(AnsibleError, self.templar.template, "{{ str_with_error }}")
+
+
+class TestTemplarMisc(BaseTemplar, unittest.TestCase):
+ def test_templar_simple(self):
+
+ templar = self.templar
+ # test some basic templating
+ self.assertEqual(templar.template("{{foo}}"), "bar")
+ self.assertEqual(templar.template("{{foo}}\n"), "bar\n")
+ self.assertEqual(templar.template("{{foo}}\n", preserve_trailing_newlines=True), "bar\n")
+ self.assertEqual(templar.template("{{foo}}\n", preserve_trailing_newlines=False), "bar")
+ self.assertEqual(templar.template("{{bam}}"), "bar")
+ self.assertEqual(templar.template("{{num}}"), 1)
+ self.assertEqual(templar.template("{{var_true}}"), True)
+ self.assertEqual(templar.template("{{var_false}}"), False)
+ self.assertEqual(templar.template("{{var_dict}}"), dict(a="b"))
+ self.assertEqual(templar.template("{{bad_dict}}"), "{a='b'")
+ self.assertEqual(templar.template("{{var_list}}"), [1])
+ self.assertEqual(templar.template(1, convert_bare=True), 1)
+
+ # force errors
+ self.assertRaises(AnsibleUndefinedVariable, templar.template, "{{bad_var}}")
+ self.assertRaises(AnsibleUndefinedVariable, templar.template, "{{lookup('file', bad_var)}}")
+ self.assertRaises(AnsibleError, templar.template, "{{lookup('bad_lookup')}}")
+ self.assertRaises(AnsibleError, templar.template, "{{recursive}}")
+ self.assertRaises(AnsibleUndefinedVariable, templar.template, "{{foo-bar}}")
+
+ # test with fail_on_undefined=False
+ self.assertEqual(templar.template("{{bad_var}}", fail_on_undefined=False), "{{bad_var}}")
+
+ # test setting available_variables
+ templar.available_variables = dict(foo="bam")
+ self.assertEqual(templar.template("{{foo}}"), "bam")
+ # variables must be a dict() for available_variables setter
+ # FIXME Use assertRaises() as a context manager (added in 2.7) once we do not run tests on Python 2.6 anymore.
+ try:
+ templar.available_variables = "foo=bam"
+ except AssertionError:
+ pass
+ except Exception as e:
+ self.fail(e)
+
+ def test_templar_escape_backslashes(self):
+ # Rule of thumb: If escape backslashes is True you should end up with
+ # the same number of backslashes as when you started.
+ self.assertEqual(self.templar.template("\t{{foo}}", escape_backslashes=True), "\tbar")
+ self.assertEqual(self.templar.template("\t{{foo}}", escape_backslashes=False), "\tbar")
+ self.assertEqual(self.templar.template("\\{{foo}}", escape_backslashes=True), "\\bar")
+ self.assertEqual(self.templar.template("\\{{foo}}", escape_backslashes=False), "\\bar")
+ self.assertEqual(self.templar.template("\\{{foo + '\t' }}", escape_backslashes=True), "\\bar\t")
+ self.assertEqual(self.templar.template("\\{{foo + '\t' }}", escape_backslashes=False), "\\bar\t")
+ self.assertEqual(self.templar.template("\\{{foo + '\\t' }}", escape_backslashes=True), "\\bar\\t")
+ self.assertEqual(self.templar.template("\\{{foo + '\\t' }}", escape_backslashes=False), "\\bar\t")
+ self.assertEqual(self.templar.template("\\{{foo + '\\\\t' }}", escape_backslashes=True), "\\bar\\\\t")
+ self.assertEqual(self.templar.template("\\{{foo + '\\\\t' }}", escape_backslashes=False), "\\bar\\t")
+
+ def test_template_jinja2_extensions(self):
+ fake_loader = DictDataLoader({})
+ templar = Templar(loader=fake_loader)
+
+ old_exts = C.DEFAULT_JINJA2_EXTENSIONS
+ try:
+ C.DEFAULT_JINJA2_EXTENSIONS = "foo,bar"
+ self.assertEqual(templar._get_extensions(), ['foo', 'bar'])
+ finally:
+ C.DEFAULT_JINJA2_EXTENSIONS = old_exts
+
+
+class TestTemplarLookup(BaseTemplar, unittest.TestCase):
+ def test_lookup_missing_plugin(self):
+ self.assertRaisesRegex(AnsibleError,
+ r'lookup plugin \(not_a_real_lookup_plugin\) not found',
+ self.templar._lookup,
+ 'not_a_real_lookup_plugin',
+ 'an_arg', a_keyword_arg='a_keyword_arg_value')
+
+ def test_lookup_list(self):
+ res = self.templar._lookup('list', 'an_arg', 'another_arg')
+ self.assertEqual(res, 'an_arg,another_arg')
+
+ def test_lookup_jinja_undefined(self):
+ self.assertRaisesRegex(AnsibleUndefinedVariable,
+ "'an_undefined_jinja_var' is undefined",
+ self.templar._lookup,
+ 'list', '{{ an_undefined_jinja_var }}')
+
+ def test_lookup_jinja_defined(self):
+ res = self.templar._lookup('list', '{{ some_var }}')
+ self.assertTrue(self.is_unsafe(res))
+ # self.assertIsInstance(res, AnsibleUnsafe)
+
+ def test_lookup_jinja_dict_string_passed(self):
+ self.assertRaisesRegex(AnsibleError,
+ "with_dict expects a dict",
+ self.templar._lookup,
+ 'dict',
+ '{{ some_var }}')
+
+ def test_lookup_jinja_dict_list_passed(self):
+ self.assertRaisesRegex(AnsibleError,
+ "with_dict expects a dict",
+ self.templar._lookup,
+ 'dict',
+ ['foo', 'bar'])
+
+ def test_lookup_jinja_kwargs(self):
+ res = self.templar._lookup('list', 'blip', random_keyword='12345')
+ self.assertTrue(self.is_unsafe(res))
+ # self.assertIsInstance(res, AnsibleUnsafe)
+
+ def test_lookup_jinja_list_wantlist(self):
+ res = self.templar._lookup('list', '{{ some_var }}', wantlist=True)
+ self.assertEqual(res, ["blip"])
+
+ def test_lookup_jinja_list_wantlist_undefined(self):
+ self.assertRaisesRegex(AnsibleUndefinedVariable,
+ "'some_undefined_var' is undefined",
+ self.templar._lookup,
+ 'list',
+ '{{ some_undefined_var }}',
+ wantlist=True)
+
+ def test_lookup_jinja_list_wantlist_unsafe(self):
+ res = self.templar._lookup('list', '{{ some_unsafe_var }}', wantlist=True)
+ for lookup_result in res:
+ self.assertTrue(self.is_unsafe(lookup_result))
+ # self.assertIsInstance(lookup_result, AnsibleUnsafe)
+
+ # Should this be an AnsibleUnsafe
+ # self.assertIsInstance(res, AnsibleUnsafe)
+
+ def test_lookup_jinja_dict(self):
+ res = self.templar._lookup('list', {'{{ a_keyword }}': '{{ some_var }}'})
+ self.assertEqual(res['{{ a_keyword }}'], "blip")
+ # TODO: Should this be an AnsibleUnsafe
+ # self.assertIsInstance(res['{{ a_keyword }}'], AnsibleUnsafe)
+ # self.assertIsInstance(res, AnsibleUnsafe)
+
+ def test_lookup_jinja_dict_unsafe(self):
+ res = self.templar._lookup('list', {'{{ some_unsafe_key }}': '{{ some_unsafe_var }}'})
+ self.assertTrue(self.is_unsafe(res['{{ some_unsafe_key }}']))
+ # self.assertIsInstance(res['{{ some_unsafe_key }}'], AnsibleUnsafe)
+ # TODO: Should this be an AnsibleUnsafe
+ # self.assertIsInstance(res, AnsibleUnsafe)
+
+ def test_lookup_jinja_dict_unsafe_value(self):
+ res = self.templar._lookup('list', {'{{ a_keyword }}': '{{ some_unsafe_var }}'})
+ self.assertTrue(self.is_unsafe(res['{{ a_keyword }}']))
+ # self.assertIsInstance(res['{{ a_keyword }}'], AnsibleUnsafe)
+ # TODO: Should this be an AnsibleUnsafe
+ # self.assertIsInstance(res, AnsibleUnsafe)
+
+ def test_lookup_jinja_none(self):
+ res = self.templar._lookup('list', None)
+ self.assertIsNone(res)
+
+
+class TestAnsibleContext(BaseTemplar, unittest.TestCase):
+ def _context(self, variables=None):
+ variables = variables or {}
+
+ env = AnsibleEnvironment()
+ context = AnsibleContext(env, parent={}, name='some_context',
+ blocks={})
+
+ for key, value in variables.items():
+ context.vars[key] = value
+
+ return context
+
+ def test(self):
+ context = self._context()
+ self.assertIsInstance(context, AnsibleContext)
+ self.assertIsInstance(context, Context)
+
+ def test_resolve_unsafe(self):
+ context = self._context(variables={'some_unsafe_key': wrap_var('some_unsafe_string')})
+ res = context.resolve('some_unsafe_key')
+ # self.assertIsInstance(res, AnsibleUnsafe)
+ self.assertTrue(self.is_unsafe(res),
+ 'return of AnsibleContext.resolve (%s) was expected to be marked unsafe but was not' % res)
+
+ def test_resolve_unsafe_list(self):
+ context = self._context(variables={'some_unsafe_key': [wrap_var('some unsafe string 1')]})
+ res = context.resolve('some_unsafe_key')
+ # self.assertIsInstance(res[0], AnsibleUnsafe)
+ self.assertTrue(self.is_unsafe(res),
+ 'return of AnsibleContext.resolve (%s) was expected to be marked unsafe but was not' % res)
+
+ def test_resolve_unsafe_dict(self):
+ context = self._context(variables={'some_unsafe_key':
+ {'an_unsafe_dict': wrap_var('some unsafe string 1')}
+ })
+ res = context.resolve('some_unsafe_key')
+ self.assertTrue(self.is_unsafe(res['an_unsafe_dict']),
+ 'return of AnsibleContext.resolve (%s) was expected to be marked unsafe but was not' % res['an_unsafe_dict'])
+
+ def test_resolve(self):
+ context = self._context(variables={'some_key': 'some_string'})
+ res = context.resolve('some_key')
+ self.assertEqual(res, 'some_string')
+ # self.assertNotIsInstance(res, AnsibleUnsafe)
+ self.assertFalse(self.is_unsafe(res),
+ 'return of AnsibleContext.resolve (%s) was not expected to be marked unsafe but was' % res)
+
+ def test_resolve_none(self):
+ context = self._context(variables={'some_key': None})
+ res = context.resolve('some_key')
+ self.assertEqual(res, None)
+ # self.assertNotIsInstance(res, AnsibleUnsafe)
+ self.assertFalse(self.is_unsafe(res),
+ 'return of AnsibleContext.resolve (%s) was not expected to be marked unsafe but was' % res)
+
+ def test_is_unsafe(self):
+ context = self._context()
+ self.assertFalse(context._is_unsafe(AnsibleUndefined()))
+
+
+def test_unsafe_lookup():
+ res = Templar(
+ None,
+ variables={
+ 'var0': '{{ var1 }}',
+ 'var1': ['unsafe'],
+ }
+ ).template('{{ lookup("list", var0) }}')
+ assert getattr(res[0], '__UNSAFE__', False)
+
+
+def test_unsafe_lookup_no_conversion():
+ res = Templar(
+ None,
+ variables={
+ 'var0': '{{ var1 }}',
+ 'var1': ['unsafe'],
+ }
+ ).template(
+ '{{ lookup("list", var0) }}',
+ convert_data=False,
+ )
+ assert getattr(res, '__UNSAFE__', False)
diff --git a/test/units/template/test_template_utilities.py b/test/units/template/test_template_utilities.py
new file mode 100644
index 0000000..1044895
--- /dev/null
+++ b/test/units/template/test_template_utilities.py
@@ -0,0 +1,117 @@
+# (c) 2015 Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import jinja2
+from units.compat import unittest
+
+from ansible.template import AnsibleUndefined, _escape_backslashes, _count_newlines_from_end
+
+# These are internal utility functions only needed for templating. They're
+# algorithmic so good candidates for unittesting by themselves
+
+
+class TestBackslashEscape(unittest.TestCase):
+
+ test_data = (
+ # Test backslashes in a filter arg are double escaped
+ dict(
+ template=u"{{ 'test2 %s' | format('\\1') }}",
+ intermediate=u"{{ 'test2 %s' | format('\\\\1') }}",
+ expectation=u"test2 \\1",
+ args=dict()
+ ),
+ # Test backslashes inside the jinja2 var itself are double
+ # escaped
+ dict(
+ template=u"Test 2\\3: {{ '\\1 %s' | format('\\2') }}",
+ intermediate=u"Test 2\\3: {{ '\\\\1 %s' | format('\\\\2') }}",
+ expectation=u"Test 2\\3: \\1 \\2",
+ args=dict()
+ ),
+ # Test backslashes outside of the jinja2 var are not double
+ # escaped
+ dict(
+ template=u"Test 2\\3: {{ 'test2 %s' | format('\\1') }}; \\done",
+ intermediate=u"Test 2\\3: {{ 'test2 %s' | format('\\\\1') }}; \\done",
+ expectation=u"Test 2\\3: test2 \\1; \\done",
+ args=dict()
+ ),
+ # Test backslashes in a variable sent to a filter are handled
+ dict(
+ template=u"{{ 'test2 %s' | format(var1) }}",
+ intermediate=u"{{ 'test2 %s' | format(var1) }}",
+ expectation=u"test2 \\1",
+ args=dict(var1=u'\\1')
+ ),
+ # Test backslashes in a variable expanded by jinja2 are double
+ # escaped
+ dict(
+ template=u"Test 2\\3: {{ var1 | format('\\2') }}",
+ intermediate=u"Test 2\\3: {{ var1 | format('\\\\2') }}",
+ expectation=u"Test 2\\3: \\1 \\2",
+ args=dict(var1=u'\\1 %s')
+ ),
+ )
+
+ def setUp(self):
+ self.env = jinja2.Environment()
+
+ def test_backslash_escaping(self):
+
+ for test in self.test_data:
+ intermediate = _escape_backslashes(test['template'], self.env)
+ self.assertEqual(intermediate, test['intermediate'])
+ template = jinja2.Template(intermediate)
+ args = test['args']
+ self.assertEqual(template.render(**args), test['expectation'])
+
+
+class TestCountNewlines(unittest.TestCase):
+
+ def test_zero_length_string(self):
+ self.assertEqual(_count_newlines_from_end(u''), 0)
+
+ def test_short_string(self):
+ self.assertEqual(_count_newlines_from_end(u'The quick\n'), 1)
+
+ def test_one_newline(self):
+ self.assertEqual(_count_newlines_from_end(u'The quick brown fox jumped over the lazy dog' * 1000 + u'\n'), 1)
+
+ def test_multiple_newlines(self):
+ self.assertEqual(_count_newlines_from_end(u'The quick brown fox jumped over the lazy dog' * 1000 + u'\n\n\n'), 3)
+
+ def test_zero_newlines(self):
+ self.assertEqual(_count_newlines_from_end(u'The quick brown fox jumped over the lazy dog' * 1000), 0)
+
+ def test_all_newlines(self):
+ self.assertEqual(_count_newlines_from_end(u'\n' * 10), 10)
+
+ def test_mostly_newlines(self):
+ self.assertEqual(_count_newlines_from_end(u'The quick brown fox jumped over the lazy dog' + u'\n' * 1000), 1000)
+
+
+class TestAnsibleUndefined(unittest.TestCase):
+ def test_getattr(self):
+ val = AnsibleUndefined()
+
+ self.assertIs(getattr(val, 'foo'), val)
+
+ self.assertRaises(AttributeError, getattr, val, '__UNSAFE__')
diff --git a/test/units/template/test_vars.py b/test/units/template/test_vars.py
new file mode 100644
index 0000000..514104f
--- /dev/null
+++ b/test/units/template/test_vars.py
@@ -0,0 +1,41 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat import unittest
+from unittest.mock import MagicMock
+
+from ansible.template.vars import AnsibleJ2Vars
+
+
+class TestVars(unittest.TestCase):
+ def setUp(self):
+ self.mock_templar = MagicMock(name='mock_templar')
+
+ def test_globals_empty(self):
+ ajvars = AnsibleJ2Vars(self.mock_templar, {})
+ res = dict(ajvars)
+ self.assertIsInstance(res, dict)
+
+ def test_globals(self):
+ res = dict(AnsibleJ2Vars(self.mock_templar, {'foo': 'bar', 'blip': [1, 2, 3]}))
+ self.assertIsInstance(res, dict)
+ self.assertIn('foo', res)
+ self.assertEqual(res['foo'], 'bar')
diff --git a/test/units/test_constants.py b/test/units/test_constants.py
new file mode 100644
index 0000000..a206d23
--- /dev/null
+++ b/test/units/test_constants.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+# (c) 2017 Toshio Kuratomi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pwd
+import os
+
+import pytest
+
+from ansible import constants
+from ansible.module_utils.six import StringIO
+from ansible.module_utils.six.moves import configparser
+from ansible.module_utils._text import to_text
+
+
+@pytest.fixture
+def cfgparser():
+ CFGDATA = StringIO("""
+[defaults]
+defaults_one = 'data_defaults_one'
+
+[level1]
+level1_one = 'data_level1_one'
+ """)
+ p = configparser.ConfigParser()
+ p.readfp(CFGDATA)
+ return p
+
+
+@pytest.fixture
+def user():
+ user = {}
+ user['uid'] = os.geteuid()
+
+ pwd_entry = pwd.getpwuid(user['uid'])
+ user['username'] = pwd_entry.pw_name
+ user['home'] = pwd_entry.pw_dir
+
+ return user
+
+
+@pytest.fixture
+def cfg_file():
+ data = '/ansible/test/cfg/path'
+ old_cfg_file = constants.CONFIG_FILE
+ constants.CONFIG_FILE = os.path.join(data, 'ansible.cfg')
+ yield data
+
+ constants.CONFIG_FILE = old_cfg_file
+
+
+@pytest.fixture
+def null_cfg_file():
+ old_cfg_file = constants.CONFIG_FILE
+ del constants.CONFIG_FILE
+ yield
+
+ constants.CONFIG_FILE = old_cfg_file
+
+
+@pytest.fixture
+def cwd():
+ data = '/ansible/test/cwd/'
+ old_cwd = os.getcwd
+ os.getcwd = lambda: data
+
+ old_cwdu = None
+ if hasattr(os, 'getcwdu'):
+ old_cwdu = os.getcwdu
+ os.getcwdu = lambda: to_text(data)
+
+ yield data
+
+ os.getcwd = old_cwd
+ if hasattr(os, 'getcwdu'):
+ os.getcwdu = old_cwdu
diff --git a/test/units/test_context.py b/test/units/test_context.py
new file mode 100644
index 0000000..24e2376
--- /dev/null
+++ b/test/units/test_context.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Toshio Kuratomi <tkuratomi@ansible.com>
+# 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 import context
+
+
+class FakeOptions:
+ pass
+
+
+def test_set_global_context():
+ options = FakeOptions()
+ options.tags = [u'production', u'webservers']
+ options.check_mode = True
+ options.start_at_task = u'Start with くらとみ'
+
+ expected = frozenset((('tags', (u'production', u'webservers')),
+ ('check_mode', True),
+ ('start_at_task', u'Start with くらとみ')))
+
+ context._init_global_context(options)
+ assert frozenset(context.CLIARGS.items()) == expected
diff --git a/test/units/test_no_tty.py b/test/units/test_no_tty.py
new file mode 100644
index 0000000..290c0b9
--- /dev/null
+++ b/test/units/test_no_tty.py
@@ -0,0 +1,7 @@
+import sys
+
+
+def test_no_tty():
+ assert not sys.stdin.isatty()
+ assert not sys.stdout.isatty()
+ assert not sys.stderr.isatty()
diff --git a/test/units/utils/__init__.py b/test/units/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/utils/__init__.py
diff --git a/test/units/utils/collection_loader/__init__.py b/test/units/utils/collection_loader/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/utils/collection_loader/__init__.py
diff --git a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/ansible/builtin/plugins/modules/shouldnotload.py b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/ansible/builtin/plugins/modules/shouldnotload.py
new file mode 100644
index 0000000..4041a33
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/ansible/builtin/plugins/modules/shouldnotload.py
@@ -0,0 +1,4 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+raise Exception('this module should never be loaded')
diff --git a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/meta/runtime.yml b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/meta/runtime.yml
new file mode 100644
index 0000000..f2e2fde
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/meta/runtime.yml
@@ -0,0 +1,4 @@
+plugin_routing:
+ modules:
+ rerouted_module:
+ redirect: ansible.builtin.ping
diff --git a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py
new file mode 100644
index 0000000..9d30580
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/action/my_action.py
@@ -0,0 +1,8 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from ..module_utils.my_util import question
+
+
+def action_code():
+ return "hello from my_action.py"
diff --git a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/__init__.py b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/__init__.py
diff --git a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_other_util.py b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_other_util.py
new file mode 100644
index 0000000..35e1381
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_other_util.py
@@ -0,0 +1,4 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from .my_util import question
diff --git a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_util.py b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_util.py
new file mode 100644
index 0000000..c431c34
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/module_utils/my_util.py
@@ -0,0 +1,6 @@
+# WARNING: Changing line numbers of code in this file will break collection tests that use tracing to check paths and line numbers.
+# Also, do not import division from __future__ as this will break detection of __future__ inheritance on Python 2.
+
+
+def question():
+ return 3 / 2
diff --git a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/__init__.py b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/__init__.py
new file mode 100644
index 0000000..6d69703
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/__init__.py
@@ -0,0 +1,5 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+raise Exception('this should never run')
diff --git a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/amodule.py b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/amodule.py
new file mode 100644
index 0000000..99320a0
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/plugins/modules/amodule.py
@@ -0,0 +1,6 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+def module_code():
+ return "hello from amodule.py"
diff --git a/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/roles/some_role/.gitkeep b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/roles/some_role/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections/ansible_collections/testns/testcoll/roles/some_role/.gitkeep
diff --git a/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/__init__.py b/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/__init__.py
new file mode 100644
index 0000000..6068ac1
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/__init__.py
@@ -0,0 +1,5 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+raise Exception('this code should never execute')
diff --git a/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/ansible/__init__.py b/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/ansible/__init__.py
new file mode 100644
index 0000000..6068ac1
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/ansible/__init__.py
@@ -0,0 +1,5 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+raise Exception('this code should never execute')
diff --git a/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/__init__.py b/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/__init__.py
new file mode 100644
index 0000000..6068ac1
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/__init__.py
@@ -0,0 +1,5 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+raise Exception('this code should never execute')
diff --git a/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/testcoll/__init__.py b/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/testcoll/__init__.py
new file mode 100644
index 0000000..6068ac1
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/collections_masked/ansible_collections/testns/testcoll/__init__.py
@@ -0,0 +1,5 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+raise Exception('this code should never execute')
diff --git a/test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/ansible/playbook_adj_other/.gitkeep b/test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/ansible/playbook_adj_other/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/ansible/playbook_adj_other/.gitkeep
diff --git a/test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/freshns/playbook_adj_other/.gitkeep b/test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/freshns/playbook_adj_other/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/freshns/playbook_adj_other/.gitkeep
diff --git a/test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/testns/playbook_adj_other/.gitkeep b/test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/testns/playbook_adj_other/.gitkeep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/utils/collection_loader/fixtures/playbook_path/collections/ansible_collections/testns/playbook_adj_other/.gitkeep
diff --git a/test/units/utils/collection_loader/test_collection_loader.py b/test/units/utils/collection_loader/test_collection_loader.py
new file mode 100644
index 0000000..f7050dc
--- /dev/null
+++ b/test/units/utils/collection_loader/test_collection_loader.py
@@ -0,0 +1,868 @@
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+import pkgutil
+import pytest
+import re
+import sys
+
+from ansible.module_utils.six import PY3, string_types
+from ansible.module_utils.compat.importlib import import_module
+from ansible.modules import ping as ping_module
+from ansible.utils.collection_loader import AnsibleCollectionConfig, AnsibleCollectionRef
+from ansible.utils.collection_loader._collection_finder import (
+ _AnsibleCollectionFinder, _AnsibleCollectionLoader, _AnsibleCollectionNSPkgLoader, _AnsibleCollectionPkgLoader,
+ _AnsibleCollectionPkgLoaderBase, _AnsibleCollectionRootPkgLoader, _AnsiblePathHookFinder,
+ _get_collection_name_from_path, _get_collection_role_path, _get_collection_metadata, _iter_modules_impl
+)
+from ansible.utils.collection_loader._collection_config import _EventSource
+from unittest.mock import MagicMock, NonCallableMagicMock, patch
+
+
+# fixture to ensure we always clean up the import stuff when we're done
+@pytest.fixture(autouse=True, scope='function')
+def teardown(*args, **kwargs):
+ yield
+ reset_collections_loader_state()
+
+# BEGIN STANDALONE TESTS - these exercise behaviors of the individual components without the import machinery
+
+
+@pytest.mark.skipif(not PY3, reason='Testing Python 2 codepath (find_module) on Python 3')
+def test_find_module_py3():
+ dir_to_a_file = os.path.dirname(ping_module.__file__)
+ path_hook_finder = _AnsiblePathHookFinder(_AnsibleCollectionFinder(), dir_to_a_file)
+
+ # setuptools may fall back to find_module on Python 3 if find_spec returns None
+ # see https://github.com/pypa/setuptools/pull/2918
+ assert path_hook_finder.find_spec('missing') is None
+ assert path_hook_finder.find_module('missing') is None
+
+
+def test_finder_setup():
+ # ensure scalar path is listified
+ f = _AnsibleCollectionFinder(paths='/bogus/bogus')
+ assert isinstance(f._n_collection_paths, list)
+
+ # ensure sys.path paths that have an ansible_collections dir are added to the end of the collections paths
+ with patch.object(sys, 'path', ['/bogus', default_test_collection_paths[1], '/morebogus', default_test_collection_paths[0]]):
+ with patch('os.path.isdir', side_effect=lambda x: b'bogus' not in x):
+ f = _AnsibleCollectionFinder(paths=['/explicit', '/other'])
+ assert f._n_collection_paths == ['/explicit', '/other', default_test_collection_paths[1], default_test_collection_paths[0]]
+
+ configured_paths = ['/bogus']
+ playbook_paths = ['/playbookdir']
+ with patch.object(sys, 'path', ['/bogus', '/playbookdir']) and patch('os.path.isdir', side_effect=lambda x: b'bogus' in x):
+ f = _AnsibleCollectionFinder(paths=configured_paths)
+ assert f._n_collection_paths == configured_paths
+
+ f.set_playbook_paths(playbook_paths)
+ assert f._n_collection_paths == extend_paths(playbook_paths, 'collections') + configured_paths
+
+ # ensure scalar playbook_paths gets listified
+ f.set_playbook_paths(playbook_paths[0])
+ assert f._n_collection_paths == extend_paths(playbook_paths, 'collections') + configured_paths
+
+
+def test_finder_not_interested():
+ f = get_default_finder()
+ assert f.find_module('nothanks') is None
+ assert f.find_module('nothanks.sub', path=['/bogus/dir']) is None
+
+
+def test_finder_ns():
+ # ensure we can still load ansible_collections and ansible_collections.ansible when they don't exist on disk
+ f = _AnsibleCollectionFinder(paths=['/bogus/bogus'])
+ loader = f.find_module('ansible_collections')
+ assert isinstance(loader, _AnsibleCollectionRootPkgLoader)
+
+ loader = f.find_module('ansible_collections.ansible', path=['/bogus/bogus'])
+ assert isinstance(loader, _AnsibleCollectionNSPkgLoader)
+
+ f = get_default_finder()
+ loader = f.find_module('ansible_collections')
+ assert isinstance(loader, _AnsibleCollectionRootPkgLoader)
+
+ # path is not allowed for top-level
+ with pytest.raises(ValueError):
+ f.find_module('ansible_collections', path=['whatever'])
+
+ # path is required for subpackages
+ with pytest.raises(ValueError):
+ f.find_module('ansible_collections.whatever', path=None)
+
+ paths = [os.path.join(p, 'ansible_collections/nonexistns') for p in default_test_collection_paths]
+
+ # test missing
+ loader = f.find_module('ansible_collections.nonexistns', paths)
+ assert loader is None
+
+
+# keep these up top to make sure the loader install/remove are working, since we rely on them heavily in the tests
+def test_loader_remove():
+ fake_mp = [MagicMock(), _AnsibleCollectionFinder(), MagicMock(), _AnsibleCollectionFinder()]
+ fake_ph = [MagicMock().m1, MagicMock().m2, _AnsibleCollectionFinder()._ansible_collection_path_hook, NonCallableMagicMock]
+ # must nest until 2.6 compilation is totally donezo
+ with patch.object(sys, 'meta_path', fake_mp):
+ with patch.object(sys, 'path_hooks', fake_ph):
+ _AnsibleCollectionFinder()._remove()
+ assert len(sys.meta_path) == 2
+ # no AnsibleCollectionFinders on the meta path after remove is called
+ assert all((not isinstance(mpf, _AnsibleCollectionFinder) for mpf in sys.meta_path))
+ assert len(sys.path_hooks) == 3
+ # none of the remaining path hooks should point at an AnsibleCollectionFinder
+ assert all((not isinstance(ph.__self__, _AnsibleCollectionFinder) for ph in sys.path_hooks if hasattr(ph, '__self__')))
+ assert AnsibleCollectionConfig.collection_finder is None
+
+
+def test_loader_install():
+ fake_mp = [MagicMock(), _AnsibleCollectionFinder(), MagicMock(), _AnsibleCollectionFinder()]
+ fake_ph = [MagicMock().m1, MagicMock().m2, _AnsibleCollectionFinder()._ansible_collection_path_hook, NonCallableMagicMock]
+ # must nest until 2.6 compilation is totally donezo
+ with patch.object(sys, 'meta_path', fake_mp):
+ with patch.object(sys, 'path_hooks', fake_ph):
+ f = _AnsibleCollectionFinder()
+ f._install()
+ assert len(sys.meta_path) == 3 # should have removed the existing ACFs and installed a new one
+ assert sys.meta_path[0] is f # at the front
+ # the rest of the meta_path should not be AnsibleCollectionFinders
+ assert all((not isinstance(mpf, _AnsibleCollectionFinder) for mpf in sys.meta_path[1:]))
+ assert len(sys.path_hooks) == 4 # should have removed the existing ACF path hooks and installed a new one
+ # the first path hook should be ours, make sure it's pointing at the right instance
+ assert hasattr(sys.path_hooks[0], '__self__') and sys.path_hooks[0].__self__ is f
+ # the rest of the path_hooks should not point at an AnsibleCollectionFinder
+ assert all((not isinstance(ph.__self__, _AnsibleCollectionFinder) for ph in sys.path_hooks[1:] if hasattr(ph, '__self__')))
+ assert AnsibleCollectionConfig.collection_finder is f
+ with pytest.raises(ValueError):
+ AnsibleCollectionConfig.collection_finder = f
+
+
+def test_finder_coll():
+ f = get_default_finder()
+
+ tests = [
+ {'name': 'ansible_collections.testns.testcoll', 'test_paths': [default_test_collection_paths]},
+ {'name': 'ansible_collections.ansible.builtin', 'test_paths': [['/bogus'], default_test_collection_paths]},
+ ]
+ # ensure finder works for legit paths and bogus paths
+ for test_dict in tests:
+ # splat the dict values to our locals
+ globals().update(test_dict)
+ parent_pkg = name.rpartition('.')[0]
+ for paths in test_paths:
+ paths = [os.path.join(p, parent_pkg.replace('.', '/')) for p in paths]
+ loader = f.find_module(name, path=paths)
+ assert isinstance(loader, _AnsibleCollectionPkgLoader)
+
+
+def test_root_loader_not_interested():
+ with pytest.raises(ImportError):
+ _AnsibleCollectionRootPkgLoader('not_ansible_collections_toplevel', path_list=[])
+
+ with pytest.raises(ImportError):
+ _AnsibleCollectionRootPkgLoader('ansible_collections.somens', path_list=['/bogus'])
+
+
+def test_root_loader():
+ name = 'ansible_collections'
+ # ensure this works even when ansible_collections doesn't exist on disk
+ for paths in [], default_test_collection_paths:
+ if name in sys.modules:
+ del sys.modules[name]
+ loader = _AnsibleCollectionRootPkgLoader(name, paths)
+ assert repr(loader).startswith('_AnsibleCollectionRootPkgLoader(path=')
+ module = loader.load_module(name)
+ assert module.__name__ == name
+ assert module.__path__ == [p for p in extend_paths(paths, name) if os.path.isdir(p)]
+ # even if the dir exists somewhere, this loader doesn't support get_data, so make __file__ a non-file
+ assert module.__file__ == '<ansible_synthetic_collection_package>'
+ assert module.__package__ == name
+ assert sys.modules.get(name) == module
+
+
+def test_nspkg_loader_not_interested():
+ with pytest.raises(ImportError):
+ _AnsibleCollectionNSPkgLoader('not_ansible_collections_toplevel.something', path_list=[])
+
+ with pytest.raises(ImportError):
+ _AnsibleCollectionNSPkgLoader('ansible_collections.somens.somecoll', path_list=[])
+
+
+def test_nspkg_loader_load_module():
+ # ensure the loader behaves on the toplevel and ansible packages for both legit and missing/bogus paths
+ for name in ['ansible_collections.ansible', 'ansible_collections.testns']:
+ parent_pkg = name.partition('.')[0]
+ module_to_load = name.rpartition('.')[2]
+ paths = extend_paths(default_test_collection_paths, parent_pkg)
+ existing_child_paths = [p for p in extend_paths(paths, module_to_load) if os.path.exists(p)]
+ if name in sys.modules:
+ del sys.modules[name]
+ loader = _AnsibleCollectionNSPkgLoader(name, path_list=paths)
+ assert repr(loader).startswith('_AnsibleCollectionNSPkgLoader(path=')
+ module = loader.load_module(name)
+ assert module.__name__ == name
+ assert isinstance(module.__loader__, _AnsibleCollectionNSPkgLoader)
+ assert module.__path__ == existing_child_paths
+ assert module.__package__ == name
+ assert module.__file__ == '<ansible_synthetic_collection_package>'
+ assert sys.modules.get(name) == module
+
+
+def test_collpkg_loader_not_interested():
+ with pytest.raises(ImportError):
+ _AnsibleCollectionPkgLoader('not_ansible_collections', path_list=[])
+
+ with pytest.raises(ImportError):
+ _AnsibleCollectionPkgLoader('ansible_collections.ns', path_list=['/bogus/bogus'])
+
+
+def test_collpkg_loader_load_module():
+ reset_collections_loader_state()
+ with patch('ansible.utils.collection_loader.AnsibleCollectionConfig') as p:
+ for name in ['ansible_collections.ansible.builtin', 'ansible_collections.testns.testcoll']:
+ parent_pkg = name.rpartition('.')[0]
+ module_to_load = name.rpartition('.')[2]
+ paths = extend_paths(default_test_collection_paths, parent_pkg)
+ existing_child_paths = [p for p in extend_paths(paths, module_to_load) if os.path.exists(p)]
+ is_builtin = 'ansible.builtin' in name
+ if name in sys.modules:
+ del sys.modules[name]
+ loader = _AnsibleCollectionPkgLoader(name, path_list=paths)
+ assert repr(loader).startswith('_AnsibleCollectionPkgLoader(path=')
+ module = loader.load_module(name)
+ assert module.__name__ == name
+ assert isinstance(module.__loader__, _AnsibleCollectionPkgLoader)
+ if is_builtin:
+ assert module.__path__ == []
+ else:
+ assert module.__path__ == [existing_child_paths[0]]
+
+ assert module.__package__ == name
+ if is_builtin:
+ assert module.__file__ == '<ansible_synthetic_collection_package>'
+ else:
+ assert module.__file__.endswith('__synthetic__') and os.path.isdir(os.path.dirname(module.__file__))
+ assert sys.modules.get(name) == module
+
+ assert hasattr(module, '_collection_meta') and isinstance(module._collection_meta, dict)
+
+ # FIXME: validate _collection_meta contents match what's on disk (or not)
+
+ # if the module has metadata, try loading it with busted metadata
+ if module._collection_meta:
+ _collection_finder = import_module('ansible.utils.collection_loader._collection_finder')
+ with patch.object(_collection_finder, '_meta_yml_to_dict', side_effect=Exception('bang')):
+ with pytest.raises(Exception) as ex:
+ _AnsibleCollectionPkgLoader(name, path_list=paths).load_module(name)
+ assert 'error parsing collection metadata' in str(ex.value)
+
+
+def test_coll_loader():
+ with patch('ansible.utils.collection_loader.AnsibleCollectionConfig'):
+ with pytest.raises(ValueError):
+ # not a collection
+ _AnsibleCollectionLoader('ansible_collections')
+
+ with pytest.raises(ValueError):
+ # bogus paths
+ _AnsibleCollectionLoader('ansible_collections.testns.testcoll', path_list=[])
+
+ # FIXME: more
+
+
+def test_path_hook_setup():
+ with patch.object(sys, 'path_hooks', []):
+ found_hook = None
+ pathhook_exc = None
+ try:
+ found_hook = _AnsiblePathHookFinder._get_filefinder_path_hook()
+ except Exception as phe:
+ pathhook_exc = phe
+
+ if PY3:
+ assert str(pathhook_exc) == 'need exactly one FileFinder import hook (found 0)'
+ else:
+ assert found_hook is None
+
+ assert repr(_AnsiblePathHookFinder(object(), '/bogus/path')) == "_AnsiblePathHookFinder(path='/bogus/path')"
+
+
+def test_path_hook_importerror():
+ # ensure that AnsiblePathHookFinder.find_module swallows ImportError from path hook delegation on Py3, eg if the delegated
+ # path hook gets passed a file on sys.path (python36.zip)
+ reset_collections_loader_state()
+ path_to_a_file = os.path.join(default_test_collection_paths[0], 'ansible_collections/testns/testcoll/plugins/action/my_action.py')
+ # it's a bug if the following pops an ImportError...
+ assert _AnsiblePathHookFinder(_AnsibleCollectionFinder(), path_to_a_file).find_module('foo.bar.my_action') is None
+
+
+def test_new_or_existing_module():
+ module_name = 'blar.test.module'
+ pkg_name = module_name.rpartition('.')[0]
+
+ # create new module case
+ nuke_module_prefix(module_name)
+ with _AnsibleCollectionPkgLoaderBase._new_or_existing_module(module_name, __package__=pkg_name) as new_module:
+ # the module we just created should now exist in sys.modules
+ assert sys.modules.get(module_name) is new_module
+ assert new_module.__name__ == module_name
+
+ # the module should stick since we didn't raise an exception in the contextmgr
+ assert sys.modules.get(module_name) is new_module
+
+ # reuse existing module case
+ with _AnsibleCollectionPkgLoaderBase._new_or_existing_module(module_name, __attr1__=42, blar='yo') as existing_module:
+ assert sys.modules.get(module_name) is new_module # should be the same module we created earlier
+ assert hasattr(existing_module, '__package__') and existing_module.__package__ == pkg_name
+ assert hasattr(existing_module, '__attr1__') and existing_module.__attr1__ == 42
+ assert hasattr(existing_module, 'blar') and existing_module.blar == 'yo'
+
+ # exception during update existing shouldn't zap existing module from sys.modules
+ with pytest.raises(ValueError) as ve:
+ with _AnsibleCollectionPkgLoaderBase._new_or_existing_module(module_name) as existing_module:
+ err_to_raise = ValueError('bang')
+ raise err_to_raise
+ # make sure we got our error
+ assert ve.value is err_to_raise
+ # and that the module still exists
+ assert sys.modules.get(module_name) is existing_module
+
+ # test module removal after exception during creation
+ nuke_module_prefix(module_name)
+ with pytest.raises(ValueError) as ve:
+ with _AnsibleCollectionPkgLoaderBase._new_or_existing_module(module_name) as new_module:
+ err_to_raise = ValueError('bang')
+ raise err_to_raise
+ # make sure we got our error
+ assert ve.value is err_to_raise
+ # and that the module was removed
+ assert sys.modules.get(module_name) is None
+
+
+def test_iter_modules_impl():
+ modules_trailer = 'ansible_collections/testns/testcoll/plugins'
+ modules_pkg_prefix = modules_trailer.replace('/', '.') + '.'
+ modules_path = os.path.join(default_test_collection_paths[0], modules_trailer)
+ modules = list(_iter_modules_impl([modules_path], modules_pkg_prefix))
+
+ assert modules
+ assert set([('ansible_collections.testns.testcoll.plugins.action', True),
+ ('ansible_collections.testns.testcoll.plugins.module_utils', True),
+ ('ansible_collections.testns.testcoll.plugins.modules', True)]) == set(modules)
+
+ modules_trailer = 'ansible_collections/testns/testcoll/plugins/modules'
+ modules_pkg_prefix = modules_trailer.replace('/', '.') + '.'
+ modules_path = os.path.join(default_test_collection_paths[0], modules_trailer)
+ modules = list(_iter_modules_impl([modules_path], modules_pkg_prefix))
+
+ assert modules
+ assert len(modules) == 1
+ assert modules[0][0] == 'ansible_collections.testns.testcoll.plugins.modules.amodule' # name
+ assert modules[0][1] is False # is_pkg
+
+ # FIXME: more
+
+
+# BEGIN IN-CIRCUIT TESTS - these exercise behaviors of the loader when wired up to the import machinery
+
+
+def test_import_from_collection(monkeypatch):
+ collection_root = os.path.join(os.path.dirname(__file__), 'fixtures', 'collections')
+ collection_path = os.path.join(collection_root, 'ansible_collections/testns/testcoll/plugins/module_utils/my_util.py')
+
+ # THIS IS UNSTABLE UNDER A DEBUGGER
+ # the trace we're expecting to be generated when running the code below:
+ # answer = question()
+ expected_trace_log = [
+ (collection_path, 5, 'call'),
+ (collection_path, 6, 'line'),
+ (collection_path, 6, 'return'),
+ ]
+
+ # define the collection root before any ansible code has been loaded
+ # otherwise config will have already been loaded and changing the environment will have no effect
+ monkeypatch.setenv('ANSIBLE_COLLECTIONS_PATH', collection_root)
+
+ finder = _AnsibleCollectionFinder(paths=[collection_root])
+ reset_collections_loader_state(finder)
+
+ from ansible_collections.testns.testcoll.plugins.module_utils.my_util import question
+
+ original_trace_function = sys.gettrace()
+ trace_log = []
+
+ if original_trace_function:
+ # enable tracing while preserving the existing trace function (coverage)
+ def my_trace_function(frame, event, arg):
+ trace_log.append((frame.f_code.co_filename, frame.f_lineno, event))
+
+ # the original trace function expects to have itself set as the trace function
+ sys.settrace(original_trace_function)
+ # call the original trace function
+ original_trace_function(frame, event, arg)
+ # restore our trace function
+ sys.settrace(my_trace_function)
+
+ return my_trace_function
+ else:
+ # no existing trace function, so our trace function is much simpler
+ def my_trace_function(frame, event, arg):
+ trace_log.append((frame.f_code.co_filename, frame.f_lineno, event))
+
+ return my_trace_function
+
+ sys.settrace(my_trace_function)
+
+ try:
+ # run a minimal amount of code while the trace is running
+ # adding more code here, including use of a context manager, will add more to our trace
+ answer = question()
+ finally:
+ sys.settrace(original_trace_function)
+
+ # make sure 'import ... as ...' works on builtin synthetic collections
+ # the following import is not supported (it tries to find module_utils in ansible.plugins)
+ # import ansible_collections.ansible.builtin.plugins.module_utils as c1
+ import ansible_collections.ansible.builtin.plugins.action as c2
+ import ansible_collections.ansible.builtin.plugins as c3
+ import ansible_collections.ansible.builtin as c4
+ import ansible_collections.ansible as c5
+ import ansible_collections as c6
+
+ # make sure 'import ...' works on builtin synthetic collections
+ import ansible_collections.ansible.builtin.plugins.module_utils
+
+ import ansible_collections.ansible.builtin.plugins.action
+ assert ansible_collections.ansible.builtin.plugins.action == c3.action == c2
+
+ import ansible_collections.ansible.builtin.plugins
+ assert ansible_collections.ansible.builtin.plugins == c4.plugins == c3
+
+ import ansible_collections.ansible.builtin
+ assert ansible_collections.ansible.builtin == c5.builtin == c4
+
+ import ansible_collections.ansible
+ assert ansible_collections.ansible == c6.ansible == c5
+
+ import ansible_collections
+ assert ansible_collections == c6
+
+ # make sure 'from ... import ...' works on builtin synthetic collections
+ from ansible_collections.ansible import builtin
+ from ansible_collections.ansible.builtin import plugins
+ assert builtin.plugins == plugins
+
+ from ansible_collections.ansible.builtin.plugins import action
+ from ansible_collections.ansible.builtin.plugins.action import command
+ assert action.command == command
+
+ from ansible_collections.ansible.builtin.plugins.module_utils import basic
+ from ansible_collections.ansible.builtin.plugins.module_utils.basic import AnsibleModule
+ assert basic.AnsibleModule == AnsibleModule
+
+ # make sure relative imports work from collections code
+ # these require __package__ to be set correctly
+ import ansible_collections.testns.testcoll.plugins.module_utils.my_other_util
+ import ansible_collections.testns.testcoll.plugins.action.my_action
+
+ # verify that code loaded from a collection does not inherit __future__ statements from the collection loader
+ if sys.version_info[0] == 2:
+ # if the collection code inherits the division future feature from the collection loader this will fail
+ assert answer == 1
+ else:
+ assert answer == 1.5
+
+ # verify that the filename and line number reported by the trace is correct
+ # this makes sure that collection loading preserves file paths and line numbers
+ assert trace_log == expected_trace_log
+
+
+def test_eventsource():
+ es = _EventSource()
+ # fire when empty should succeed
+ es.fire(42)
+ handler1 = MagicMock()
+ handler2 = MagicMock()
+ es += handler1
+ es.fire(99, my_kwarg='blah')
+ handler1.assert_called_with(99, my_kwarg='blah')
+ es += handler2
+ es.fire(123, foo='bar')
+ handler1.assert_called_with(123, foo='bar')
+ handler2.assert_called_with(123, foo='bar')
+ es -= handler2
+ handler1.reset_mock()
+ handler2.reset_mock()
+ es.fire(123, foo='bar')
+ handler1.assert_called_with(123, foo='bar')
+ handler2.assert_not_called()
+ es -= handler1
+ handler1.reset_mock()
+ es.fire('blah', kwarg=None)
+ handler1.assert_not_called()
+ handler2.assert_not_called()
+ es -= handler1 # should succeed silently
+ handler_bang = MagicMock(side_effect=Exception('bang'))
+ es += handler_bang
+ with pytest.raises(Exception) as ex:
+ es.fire(123)
+ assert 'bang' in str(ex.value)
+ handler_bang.assert_called_with(123)
+ with pytest.raises(ValueError):
+ es += 42
+
+
+def test_on_collection_load():
+ finder = get_default_finder()
+ reset_collections_loader_state(finder)
+
+ load_handler = MagicMock()
+ AnsibleCollectionConfig.on_collection_load += load_handler
+
+ m = import_module('ansible_collections.testns.testcoll')
+ load_handler.assert_called_once_with(collection_name='testns.testcoll', collection_path=os.path.dirname(m.__file__))
+
+ _meta = _get_collection_metadata('testns.testcoll')
+ assert _meta
+ # FIXME: compare to disk
+
+ finder = get_default_finder()
+ reset_collections_loader_state(finder)
+
+ AnsibleCollectionConfig.on_collection_load += MagicMock(side_effect=Exception('bang'))
+ with pytest.raises(Exception) as ex:
+ import_module('ansible_collections.testns.testcoll')
+ assert 'bang' in str(ex.value)
+
+
+def test_default_collection_config():
+ finder = get_default_finder()
+ reset_collections_loader_state(finder)
+ assert AnsibleCollectionConfig.default_collection is None
+ AnsibleCollectionConfig.default_collection = 'foo.bar'
+ assert AnsibleCollectionConfig.default_collection == 'foo.bar'
+
+
+def test_default_collection_detection():
+ finder = get_default_finder()
+ reset_collections_loader_state(finder)
+
+ # we're clearly not under a collection path
+ assert _get_collection_name_from_path('/') is None
+
+ # something that looks like a collection path but isn't importable by our finder
+ assert _get_collection_name_from_path('/foo/ansible_collections/bogusns/boguscoll/bar') is None
+
+ # legit, at the top of the collection
+ live_collection_path = os.path.join(os.path.dirname(__file__), 'fixtures/collections/ansible_collections/testns/testcoll')
+ assert _get_collection_name_from_path(live_collection_path) == 'testns.testcoll'
+
+ # legit, deeper inside the collection
+ live_collection_deep_path = os.path.join(live_collection_path, 'plugins/modules')
+ assert _get_collection_name_from_path(live_collection_deep_path) == 'testns.testcoll'
+
+ # this one should be hidden by the real testns.testcoll, so should not resolve
+ masked_collection_path = os.path.join(os.path.dirname(__file__), 'fixtures/collections_masked/ansible_collections/testns/testcoll')
+ assert _get_collection_name_from_path(masked_collection_path) is None
+
+
+@pytest.mark.parametrize(
+ 'role_name,collection_list,expected_collection_name,expected_path_suffix',
+ [
+ ('some_role', ['testns.testcoll', 'ansible.bogus'], 'testns.testcoll', 'testns/testcoll/roles/some_role'),
+ ('testns.testcoll.some_role', ['ansible.bogus', 'testns.testcoll'], 'testns.testcoll', 'testns/testcoll/roles/some_role'),
+ ('testns.testcoll.some_role', [], 'testns.testcoll', 'testns/testcoll/roles/some_role'),
+ ('testns.testcoll.some_role', None, 'testns.testcoll', 'testns/testcoll/roles/some_role'),
+ ('some_role', [], None, None),
+ ('some_role', None, None, None),
+ ])
+def test_collection_role_name_location(role_name, collection_list, expected_collection_name, expected_path_suffix):
+ finder = get_default_finder()
+ reset_collections_loader_state(finder)
+
+ expected_path = None
+ if expected_path_suffix:
+ expected_path = os.path.join(os.path.dirname(__file__), 'fixtures/collections/ansible_collections', expected_path_suffix)
+
+ found = _get_collection_role_path(role_name, collection_list)
+
+ if found:
+ assert found[0] == role_name.rpartition('.')[2]
+ assert found[1] == expected_path
+ assert found[2] == expected_collection_name
+ else:
+ assert expected_collection_name is None and expected_path_suffix is None
+
+
+def test_bogus_imports():
+ finder = get_default_finder()
+ reset_collections_loader_state(finder)
+
+ # ensure ImportError on known-bogus imports
+ bogus_imports = ['bogus_toplevel', 'ansible_collections.bogusns', 'ansible_collections.testns.boguscoll',
+ 'ansible_collections.testns.testcoll.bogussub', 'ansible_collections.ansible.builtin.bogussub']
+ for bogus_import in bogus_imports:
+ with pytest.raises(ImportError):
+ import_module(bogus_import)
+
+
+def test_empty_vs_no_code():
+ finder = get_default_finder()
+ reset_collections_loader_state(finder)
+
+ from ansible_collections.testns import testcoll # synthetic package with no code on disk
+ from ansible_collections.testns.testcoll.plugins import module_utils # real package with empty code file
+
+ # ensure synthetic packages have no code object at all (prevent bogus coverage entries)
+ assert testcoll.__loader__.get_source(testcoll.__name__) is None
+ assert testcoll.__loader__.get_code(testcoll.__name__) is None
+
+ # ensure empty package inits do have a code object
+ assert module_utils.__loader__.get_source(module_utils.__name__) == b''
+ assert module_utils.__loader__.get_code(module_utils.__name__) is not None
+
+
+def test_finder_playbook_paths():
+ finder = get_default_finder()
+ reset_collections_loader_state(finder)
+
+ import ansible_collections
+ import ansible_collections.ansible
+ import ansible_collections.testns
+
+ # ensure the package modules look like we expect
+ assert hasattr(ansible_collections, '__path__') and len(ansible_collections.__path__) > 0
+ assert hasattr(ansible_collections.ansible, '__path__') and len(ansible_collections.ansible.__path__) > 0
+ assert hasattr(ansible_collections.testns, '__path__') and len(ansible_collections.testns.__path__) > 0
+
+ # these shouldn't be visible yet, since we haven't added the playbook dir
+ with pytest.raises(ImportError):
+ import ansible_collections.ansible.playbook_adj_other
+
+ with pytest.raises(ImportError):
+ import ansible_collections.testns.playbook_adj_other
+
+ assert AnsibleCollectionConfig.playbook_paths == []
+ playbook_path_fixture_dir = os.path.join(os.path.dirname(__file__), 'fixtures/playbook_path')
+
+ # configure the playbook paths
+ AnsibleCollectionConfig.playbook_paths = [playbook_path_fixture_dir]
+
+ # playbook paths go to the front of the line
+ assert AnsibleCollectionConfig.collection_paths[0] == os.path.join(playbook_path_fixture_dir, 'collections')
+
+ # playbook paths should be updated on the existing root ansible_collections path, as well as on the 'ansible' namespace (but no others!)
+ assert ansible_collections.__path__[0] == os.path.join(playbook_path_fixture_dir, 'collections/ansible_collections')
+ assert ansible_collections.ansible.__path__[0] == os.path.join(playbook_path_fixture_dir, 'collections/ansible_collections/ansible')
+ assert all('playbook_path' not in p for p in ansible_collections.testns.__path__)
+
+ # should succeed since we fixed up the package path
+ import ansible_collections.ansible.playbook_adj_other
+ # should succeed since we didn't import freshns before hacking in the path
+ import ansible_collections.freshns.playbook_adj_other
+ # should fail since we've already imported something from this path and didn't fix up its package path
+ with pytest.raises(ImportError):
+ import ansible_collections.testns.playbook_adj_other
+
+
+def test_toplevel_iter_modules():
+ finder = get_default_finder()
+ reset_collections_loader_state(finder)
+
+ modules = list(pkgutil.iter_modules(default_test_collection_paths, ''))
+ assert len(modules) == 1
+ assert modules[0][1] == 'ansible_collections'
+
+
+def test_iter_modules_namespaces():
+ finder = get_default_finder()
+ reset_collections_loader_state(finder)
+
+ paths = extend_paths(default_test_collection_paths, 'ansible_collections')
+ modules = list(pkgutil.iter_modules(paths, 'ansible_collections.'))
+ assert len(modules) == 2
+ assert all(m[2] is True for m in modules)
+ assert all(isinstance(m[0], _AnsiblePathHookFinder) for m in modules)
+ assert set(['ansible_collections.testns', 'ansible_collections.ansible']) == set(m[1] for m in modules)
+
+
+def test_collection_get_data():
+ finder = get_default_finder()
+ reset_collections_loader_state(finder)
+
+ # something that's there
+ d = pkgutil.get_data('ansible_collections.testns.testcoll', 'plugins/action/my_action.py')
+ assert b'hello from my_action.py' in d
+
+ # something that's not there
+ d = pkgutil.get_data('ansible_collections.testns.testcoll', 'bogus/bogus')
+ assert d is None
+
+ with pytest.raises(ValueError):
+ plugins_pkg = import_module('ansible_collections.ansible.builtin')
+ assert not os.path.exists(os.path.dirname(plugins_pkg.__file__))
+ d = pkgutil.get_data('ansible_collections.ansible.builtin', 'plugins/connection/local.py')
+
+
+@pytest.mark.parametrize(
+ 'ref,ref_type,expected_collection,expected_subdirs,expected_resource,expected_python_pkg_name',
+ [
+ ('ns.coll.myaction', 'action', 'ns.coll', '', 'myaction', 'ansible_collections.ns.coll.plugins.action'),
+ ('ns.coll.subdir1.subdir2.myaction', 'action', 'ns.coll', 'subdir1.subdir2', 'myaction', 'ansible_collections.ns.coll.plugins.action.subdir1.subdir2'),
+ ('ns.coll.myrole', 'role', 'ns.coll', '', 'myrole', 'ansible_collections.ns.coll.roles.myrole'),
+ ('ns.coll.subdir1.subdir2.myrole', 'role', 'ns.coll', 'subdir1.subdir2', 'myrole', 'ansible_collections.ns.coll.roles.subdir1.subdir2.myrole'),
+ ])
+def test_fqcr_parsing_valid(ref, ref_type, expected_collection,
+ expected_subdirs, expected_resource, expected_python_pkg_name):
+ assert AnsibleCollectionRef.is_valid_fqcr(ref, ref_type)
+
+ r = AnsibleCollectionRef.from_fqcr(ref, ref_type)
+ assert r.collection == expected_collection
+ assert r.subdirs == expected_subdirs
+ assert r.resource == expected_resource
+ assert r.n_python_package_name == expected_python_pkg_name
+
+ r = AnsibleCollectionRef.try_parse_fqcr(ref, ref_type)
+ assert r.collection == expected_collection
+ assert r.subdirs == expected_subdirs
+ assert r.resource == expected_resource
+ assert r.n_python_package_name == expected_python_pkg_name
+
+
+@pytest.mark.parametrize(
+ ('fqcn', 'expected'),
+ (
+ ('ns1.coll2', True),
+ ('ns1#coll2', False),
+ ('def.coll3', False),
+ ('ns4.return', False),
+ ('assert.this', False),
+ ('import.that', False),
+ ('.that', False),
+ ('this.', False),
+ ('.', False),
+ ('', False),
+ ),
+)
+def test_fqcn_validation(fqcn, expected):
+ """Vefiry that is_valid_collection_name validates FQCN correctly."""
+ assert AnsibleCollectionRef.is_valid_collection_name(fqcn) is expected
+
+
+@pytest.mark.parametrize(
+ 'ref,ref_type,expected_error_type,expected_error_expression',
+ [
+ ('no_dots_at_all_action', 'action', ValueError, 'is not a valid collection reference'),
+ ('no_nscoll.myaction', 'action', ValueError, 'is not a valid collection reference'),
+ ('no_nscoll%myaction', 'action', ValueError, 'is not a valid collection reference'),
+ ('ns.coll.myaction', 'bogus', ValueError, 'invalid collection ref_type'),
+ ])
+def test_fqcr_parsing_invalid(ref, ref_type, expected_error_type, expected_error_expression):
+ assert not AnsibleCollectionRef.is_valid_fqcr(ref, ref_type)
+
+ with pytest.raises(expected_error_type) as curerr:
+ AnsibleCollectionRef.from_fqcr(ref, ref_type)
+
+ assert re.search(expected_error_expression, str(curerr.value))
+
+ r = AnsibleCollectionRef.try_parse_fqcr(ref, ref_type)
+ assert r is None
+
+
+@pytest.mark.parametrize(
+ 'name,subdirs,resource,ref_type,python_pkg_name',
+ [
+ ('ns.coll', None, 'res', 'doc_fragments', 'ansible_collections.ns.coll.plugins.doc_fragments'),
+ ('ns.coll', 'subdir1', 'res', 'doc_fragments', 'ansible_collections.ns.coll.plugins.doc_fragments.subdir1'),
+ ('ns.coll', 'subdir1.subdir2', 'res', 'action', 'ansible_collections.ns.coll.plugins.action.subdir1.subdir2'),
+ ])
+def test_collectionref_components_valid(name, subdirs, resource, ref_type, python_pkg_name):
+ x = AnsibleCollectionRef(name, subdirs, resource, ref_type)
+
+ assert x.collection == name
+ if subdirs:
+ assert x.subdirs == subdirs
+ else:
+ assert x.subdirs == ''
+
+ assert x.resource == resource
+ assert x.ref_type == ref_type
+ assert x.n_python_package_name == python_pkg_name
+
+
+@pytest.mark.parametrize(
+ 'dirname,expected_result',
+ [
+ ('become_plugins', 'become'),
+ ('cache_plugins', 'cache'),
+ ('connection_plugins', 'connection'),
+ ('library', 'modules'),
+ ('filter_plugins', 'filter'),
+ ('bogus_plugins', ValueError),
+ (None, ValueError)
+ ]
+)
+def test_legacy_plugin_dir_to_plugin_type(dirname, expected_result):
+ if isinstance(expected_result, string_types):
+ assert AnsibleCollectionRef.legacy_plugin_dir_to_plugin_type(dirname) == expected_result
+ else:
+ with pytest.raises(expected_result):
+ AnsibleCollectionRef.legacy_plugin_dir_to_plugin_type(dirname)
+
+
+@pytest.mark.parametrize(
+ 'name,subdirs,resource,ref_type,expected_error_type,expected_error_expression',
+ [
+ ('bad_ns', '', 'resource', 'action', ValueError, 'invalid collection name'),
+ ('ns.coll.', '', 'resource', 'action', ValueError, 'invalid collection name'),
+ ('ns.coll', 'badsubdir#', 'resource', 'action', ValueError, 'invalid subdirs entry'),
+ ('ns.coll', 'badsubdir.', 'resource', 'action', ValueError, 'invalid subdirs entry'),
+ ('ns.coll', '.badsubdir', 'resource', 'action', ValueError, 'invalid subdirs entry'),
+ ('ns.coll', '', 'resource', 'bogus', ValueError, 'invalid collection ref_type'),
+ ])
+def test_collectionref_components_invalid(name, subdirs, resource, ref_type, expected_error_type, expected_error_expression):
+ with pytest.raises(expected_error_type) as curerr:
+ AnsibleCollectionRef(name, subdirs, resource, ref_type)
+
+ assert re.search(expected_error_expression, str(curerr.value))
+
+
+# BEGIN TEST SUPPORT
+
+default_test_collection_paths = [
+ os.path.join(os.path.dirname(__file__), 'fixtures', 'collections'),
+ os.path.join(os.path.dirname(__file__), 'fixtures', 'collections_masked'),
+ '/bogus/bogussub'
+]
+
+
+def get_default_finder():
+ return _AnsibleCollectionFinder(paths=default_test_collection_paths)
+
+
+def extend_paths(path_list, suffix):
+ suffix = suffix.replace('.', '/')
+ return [os.path.join(p, suffix) for p in path_list]
+
+
+def nuke_module_prefix(prefix):
+ for module_to_nuke in [m for m in sys.modules if m.startswith(prefix)]:
+ sys.modules.pop(module_to_nuke)
+
+
+def reset_collections_loader_state(metapath_finder=None):
+ _AnsibleCollectionFinder._remove()
+
+ nuke_module_prefix('ansible_collections')
+ nuke_module_prefix('ansible.modules')
+ nuke_module_prefix('ansible.plugins')
+
+ # FIXME: better to move this someplace else that gets cleaned up automatically?
+ _AnsibleCollectionLoader._redirected_package_map = {}
+
+ AnsibleCollectionConfig._default_collection = None
+ AnsibleCollectionConfig._on_collection_load = _EventSource()
+
+ if metapath_finder:
+ metapath_finder._install()
diff --git a/test/units/utils/display/test_broken_cowsay.py b/test/units/utils/display/test_broken_cowsay.py
new file mode 100644
index 0000000..d888010
--- /dev/null
+++ b/test/units/utils/display/test_broken_cowsay.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# 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.utils.display import Display
+from unittest.mock import MagicMock
+
+
+def test_display_with_fake_cowsay_binary(capsys, mocker):
+ mocker.patch("ansible.constants.ANSIBLE_COW_PATH", "./cowsay.sh")
+
+ def mock_communicate(input=None, timeout=None):
+ return b"", b""
+
+ mock_popen = MagicMock()
+ mock_popen.return_value.communicate = mock_communicate
+ mock_popen.return_value.returncode = 1
+ mocker.patch("subprocess.Popen", mock_popen)
+
+ display = Display()
+ assert not hasattr(display, "cows_available")
+ assert display.b_cowsay is None
diff --git a/test/units/utils/display/test_display.py b/test/units/utils/display/test_display.py
new file mode 100644
index 0000000..cdeb496
--- /dev/null
+++ b/test/units/utils/display/test_display.py
@@ -0,0 +1,20 @@
+# -*- 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
+
+
+from ansible.utils.display import Display
+
+
+def test_display_basic_message(capsys, mocker):
+ # Disable logging
+ mocker.patch('ansible.utils.display.logger', return_value=None)
+
+ d = Display()
+ d.display(u'Some displayed message')
+ out, err = capsys.readouterr()
+ assert out == 'Some displayed message\n'
+ assert err == ''
diff --git a/test/units/utils/display/test_logger.py b/test/units/utils/display/test_logger.py
new file mode 100644
index 0000000..ed69393
--- /dev/null
+++ b/test/units/utils/display/test_logger.py
@@ -0,0 +1,31 @@
+# -*- 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
+
+
+import logging
+import sys
+
+
+def test_logger():
+ '''
+ Avoid CVE-2019-14846 as 3rd party libs will disclose secrets when
+ logging is set to DEBUG
+ '''
+
+ # clear loaded modules to have unadultered test.
+ for loaded in list(sys.modules.keys()):
+ if 'ansible' in loaded:
+ del sys.modules[loaded]
+
+ # force logger to exist via config
+ from ansible import constants as C
+ C.DEFAULT_LOG_PATH = '/dev/null'
+
+ # initialize logger
+ from ansible.utils.display import logger
+
+ assert logger.root.level != logging.DEBUG
diff --git a/test/units/utils/display/test_warning.py b/test/units/utils/display/test_warning.py
new file mode 100644
index 0000000..be63c34
--- /dev/null
+++ b/test/units/utils/display/test_warning.py
@@ -0,0 +1,42 @@
+# -*- 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
+
+import pytest
+
+from ansible.utils.display import Display
+
+
+@pytest.fixture
+def warning_message():
+ warning_message = 'bad things will happen'
+ expected_warning_message = '[WARNING]: {0}\n'.format(warning_message)
+ return warning_message, expected_warning_message
+
+
+def test_warning(capsys, mocker, warning_message):
+ warning_message, expected_warning_message = warning_message
+
+ mocker.patch('ansible.utils.color.ANSIBLE_COLOR', True)
+ mocker.patch('ansible.utils.color.parsecolor', return_value=u'1;35') # value for 'bright purple'
+
+ d = Display()
+ d.warning(warning_message)
+ out, err = capsys.readouterr()
+ assert d._warns == {expected_warning_message: 1}
+ assert err == '\x1b[1;35m{0}\x1b[0m\n'.format(expected_warning_message.rstrip('\n'))
+
+
+def test_warning_no_color(capsys, mocker, warning_message):
+ warning_message, expected_warning_message = warning_message
+
+ mocker.patch('ansible.utils.color.ANSIBLE_COLOR', False)
+
+ d = Display()
+ d.warning(warning_message)
+ out, err = capsys.readouterr()
+ assert d._warns == {expected_warning_message: 1}
+ assert err == expected_warning_message
diff --git a/test/units/utils/test_cleanup_tmp_file.py b/test/units/utils/test_cleanup_tmp_file.py
new file mode 100644
index 0000000..2a44a55
--- /dev/null
+++ b/test/units/utils/test_cleanup_tmp_file.py
@@ -0,0 +1,48 @@
+# -*- 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
+
+import os
+import pytest
+import tempfile
+
+from ansible.utils.path import cleanup_tmp_file
+
+
+def raise_error():
+ raise OSError
+
+
+def test_cleanup_tmp_file_file():
+ tmp_fd, tmp = tempfile.mkstemp()
+ cleanup_tmp_file(tmp)
+
+ assert not os.path.exists(tmp)
+
+
+def test_cleanup_tmp_file_dir():
+ tmp = tempfile.mkdtemp()
+ cleanup_tmp_file(tmp)
+
+ assert not os.path.exists(tmp)
+
+
+def test_cleanup_tmp_file_nonexistant():
+ assert None is cleanup_tmp_file('nope')
+
+
+def test_cleanup_tmp_file_failure(mocker):
+ tmp = tempfile.mkdtemp()
+ with pytest.raises(Exception):
+ mocker.patch('shutil.rmtree', side_effect=raise_error())
+ cleanup_tmp_file(tmp)
+
+
+def test_cleanup_tmp_file_failure_warning(mocker, capsys):
+ tmp = tempfile.mkdtemp()
+ with pytest.raises(Exception):
+ mocker.patch('shutil.rmtree', side_effect=raise_error())
+ cleanup_tmp_file(tmp, warn=True)
diff --git a/test/units/utils/test_context_objects.py b/test/units/utils/test_context_objects.py
new file mode 100644
index 0000000..c56a41d
--- /dev/null
+++ b/test/units/utils/test_context_objects.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+# Copyright: (c) 2018, Toshio Kuratomi <tkuratomi@ansible.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import argparse
+
+import pytest
+
+from ansible.module_utils.common.collections import ImmutableDict
+from ansible.utils import context_objects as co
+
+
+MAKE_IMMUTABLE_DATA = ((u'くらとみ', u'くらとみ'),
+ (42, 42),
+ ({u'café': u'くらとみ'}, ImmutableDict({u'café': u'くらとみ'})),
+ ([1, u'café', u'くらとみ'], (1, u'café', u'くらとみ')),
+ (set((1, u'café', u'くらとみ')), frozenset((1, u'café', u'くらとみ'))),
+ ({u'café': [1, set(u'ñ')]},
+ ImmutableDict({u'café': (1, frozenset(u'ñ'))})),
+ ([set((1, 2)), {u'くらとみ': 3}],
+ (frozenset((1, 2)), ImmutableDict({u'くらとみ': 3}))),
+ )
+
+
+@pytest.mark.parametrize('data, expected', MAKE_IMMUTABLE_DATA)
+def test_make_immutable(data, expected):
+ assert co._make_immutable(data) == expected
+
+
+def test_cliargs_from_dict():
+ old_dict = {'tags': [u'production', u'webservers'],
+ 'check_mode': True,
+ 'start_at_task': u'Start with くらとみ'}
+ expected = frozenset((('tags', (u'production', u'webservers')),
+ ('check_mode', True),
+ ('start_at_task', u'Start with くらとみ')))
+
+ assert frozenset(co.CLIArgs(old_dict).items()) == expected
+
+
+def test_cliargs():
+ class FakeOptions:
+ pass
+ options = FakeOptions()
+ options.tags = [u'production', u'webservers']
+ options.check_mode = True
+ options.start_at_task = u'Start with くらとみ'
+
+ expected = frozenset((('tags', (u'production', u'webservers')),
+ ('check_mode', True),
+ ('start_at_task', u'Start with くらとみ')))
+
+ assert frozenset(co.CLIArgs.from_options(options).items()) == expected
+
+
+def test_cliargs_argparse():
+ parser = argparse.ArgumentParser(description='Process some integers.')
+ parser.add_argument('integers', metavar='N', type=int, nargs='+',
+ help='an integer for the accumulator')
+ parser.add_argument('--sum', dest='accumulate', action='store_const',
+ const=sum, default=max,
+ help='sum the integers (default: find the max)')
+ args = parser.parse_args([u'--sum', u'1', u'2'])
+
+ expected = frozenset((('accumulate', sum), ('integers', (1, 2))))
+
+ assert frozenset(co.CLIArgs.from_options(args).items()) == expected
diff --git a/test/units/utils/test_display.py b/test/units/utils/test_display.py
new file mode 100644
index 0000000..6b1914b
--- /dev/null
+++ b/test/units/utils/test_display.py
@@ -0,0 +1,135 @@
+# -*- coding: utf-8 -*-
+# (c) 2020 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import locale
+import sys
+import unicodedata
+from unittest.mock import MagicMock
+
+import pytest
+
+from ansible.utils.display import _LIBC, _MAX_INT, Display, get_text_width
+from ansible.utils.multiprocessing import context as multiprocessing_context
+
+
+@pytest.fixture
+def problematic_wcswidth_chars():
+ problematic = []
+ try:
+ locale.setlocale(locale.LC_ALL, 'C.UTF-8')
+ except Exception:
+ return problematic
+
+ candidates = set(chr(c) for c in range(sys.maxunicode) if unicodedata.category(chr(c)) == 'Cf')
+ for c in candidates:
+ if _LIBC.wcswidth(c, _MAX_INT) == -1:
+ problematic.append(c)
+
+ return problematic
+
+
+def test_get_text_width():
+ locale.setlocale(locale.LC_ALL, '')
+ assert get_text_width(u'コンニチハ') == 10
+ assert get_text_width(u'abコcd') == 6
+ assert get_text_width(u'café') == 4
+ assert get_text_width(u'four') == 4
+ assert get_text_width(u'\u001B') == 0
+ assert get_text_width(u'ab\u0000') == 2
+ assert get_text_width(u'abコ\u0000') == 4
+ assert get_text_width(u'🚀🐮') == 4
+ assert get_text_width(u'\x08') == 0
+ assert get_text_width(u'\x08\x08') == 0
+ assert get_text_width(u'ab\x08cd') == 3
+ assert get_text_width(u'ab\x1bcd') == 3
+ assert get_text_width(u'ab\x7fcd') == 3
+ assert get_text_width(u'ab\x94cd') == 3
+
+ pytest.raises(TypeError, get_text_width, 1)
+ pytest.raises(TypeError, get_text_width, b'four')
+
+
+def test_get_text_width_no_locale(problematic_wcswidth_chars):
+ if not problematic_wcswidth_chars:
+ pytest.skip("No problmatic wcswidth chars")
+ locale.setlocale(locale.LC_ALL, 'C.UTF-8')
+ pytest.raises(EnvironmentError, get_text_width, problematic_wcswidth_chars[0])
+
+
+def test_Display_banner_get_text_width(monkeypatch):
+ locale.setlocale(locale.LC_ALL, '')
+ display = Display()
+ display_mock = MagicMock()
+ monkeypatch.setattr(display, 'display', display_mock)
+
+ display.banner(u'🚀🐮', color=False, cows=False)
+ args, kwargs = display_mock.call_args
+ msg = args[0]
+ stars = u' %s' % (75 * u'*')
+ assert msg.endswith(stars)
+
+
+def test_Display_banner_get_text_width_fallback(monkeypatch):
+ locale.setlocale(locale.LC_ALL, 'C.UTF-8')
+ display = Display()
+ display_mock = MagicMock()
+ monkeypatch.setattr(display, 'display', display_mock)
+
+ display.banner(u'\U000110cd', color=False, cows=False)
+ args, kwargs = display_mock.call_args
+ msg = args[0]
+ stars = u' %s' % (78 * u'*')
+ assert msg.endswith(stars)
+
+
+def test_Display_set_queue_parent():
+ display = Display()
+ pytest.raises(RuntimeError, display.set_queue, 'foo')
+
+
+def test_Display_set_queue_fork():
+ def test():
+ display = Display()
+ display.set_queue('foo')
+ assert display._final_q == 'foo'
+ p = multiprocessing_context.Process(target=test)
+ p.start()
+ p.join()
+ assert p.exitcode == 0
+
+
+def test_Display_display_fork():
+ def test():
+ queue = MagicMock()
+ display = Display()
+ display.set_queue(queue)
+ display.display('foo')
+ queue.send_display.assert_called_once_with(
+ 'foo', color=None, stderr=False, screen_only=False, log_only=False, newline=True
+ )
+
+ p = multiprocessing_context.Process(target=test)
+ p.start()
+ p.join()
+ assert p.exitcode == 0
+
+
+def test_Display_display_lock(monkeypatch):
+ lock = MagicMock()
+ display = Display()
+ monkeypatch.setattr(display, '_lock', lock)
+ display.display('foo')
+ lock.__enter__.assert_called_once_with()
+
+
+def test_Display_display_lock_fork(monkeypatch):
+ lock = MagicMock()
+ display = Display()
+ monkeypatch.setattr(display, '_lock', lock)
+ monkeypatch.setattr(display, '_final_q', MagicMock())
+ display.display('foo')
+ lock.__enter__.assert_not_called()
diff --git a/test/units/utils/test_encrypt.py b/test/units/utils/test_encrypt.py
new file mode 100644
index 0000000..72fe3b0
--- /dev/null
+++ b/test/units/utils/test_encrypt.py
@@ -0,0 +1,220 @@
+# (c) 2018, Matthias Fuchs <matthias.s.fuchs@gmail.com>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import sys
+
+import pytest
+
+from ansible.errors import AnsibleError, AnsibleFilterError
+from ansible.plugins.filter.core import get_encrypted_password
+from ansible.utils import encrypt
+
+
+class passlib_off(object):
+ def __init__(self):
+ self.orig = encrypt.PASSLIB_AVAILABLE
+
+ def __enter__(self):
+ encrypt.PASSLIB_AVAILABLE = False
+ return self
+
+ def __exit__(self, exception_type, exception_value, traceback):
+ encrypt.PASSLIB_AVAILABLE = self.orig
+
+
+def assert_hash(expected, secret, algorithm, **settings):
+
+ if encrypt.PASSLIB_AVAILABLE:
+ assert encrypt.passlib_or_crypt(secret, algorithm, **settings) == expected
+ assert encrypt.PasslibHash(algorithm).hash(secret, **settings) == expected
+ else:
+ assert encrypt.passlib_or_crypt(secret, algorithm, **settings) == expected
+ with pytest.raises(AnsibleError) as excinfo:
+ encrypt.PasslibHash(algorithm).hash(secret, **settings)
+ assert excinfo.value.args[0] == "passlib must be installed and usable to hash with '%s'" % algorithm
+
+
+@pytest.mark.skipif(sys.platform.startswith('darwin'), reason='macOS requires passlib')
+def test_encrypt_with_rounds_no_passlib():
+ with passlib_off():
+ assert_hash("$5$rounds=5000$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7",
+ secret="123", algorithm="sha256_crypt", salt="12345678", rounds=5000)
+ assert_hash("$5$rounds=10000$12345678$JBinliYMFEcBeAXKZnLjenhgEhTmJBvZn3aR8l70Oy/",
+ secret="123", algorithm="sha256_crypt", salt="12345678", rounds=10000)
+ assert_hash("$6$rounds=5000$12345678$LcV9LQiaPekQxZ.OfkMADjFdSO2k9zfbDQrHPVcYjSLqSdjLYpsgqviYvTEP/R41yPmhH3CCeEDqVhW1VHr3L.",
+ secret="123", algorithm="sha512_crypt", salt="12345678", rounds=5000)
+
+
+@pytest.mark.skipif(not encrypt.PASSLIB_AVAILABLE, reason='passlib must be installed to run this test')
+def test_encrypt_with_ident():
+ assert_hash("$2$12$123456789012345678901ufd3hZRrev.WXCbemqGIV/gmWaTGLImm",
+ secret="123", algorithm="bcrypt", salt='1234567890123456789012', ident='2')
+ assert_hash("$2y$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
+ secret="123", algorithm="bcrypt", salt='1234567890123456789012', ident='2y')
+ assert_hash("$2a$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
+ secret="123", algorithm="bcrypt", salt='1234567890123456789012', ident='2a')
+ assert_hash("$2b$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
+ secret="123", algorithm="bcrypt", salt='1234567890123456789012', ident='2b')
+ assert_hash("$2b$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
+ secret="123", algorithm="bcrypt", salt='1234567890123456789012')
+ # negative test: sha256_crypt does not take ident as parameter so ignore it
+ assert_hash("$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7",
+ secret="123", algorithm="sha256_crypt", salt="12345678", rounds=5000, ident='invalid_ident')
+
+
+# If passlib is not installed. this is identical to the test_encrypt_with_rounds_no_passlib() test
+@pytest.mark.skipif(not encrypt.PASSLIB_AVAILABLE, reason='passlib must be installed to run this test')
+def test_encrypt_with_rounds():
+ assert_hash("$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7",
+ secret="123", algorithm="sha256_crypt", salt="12345678", rounds=5000)
+ assert_hash("$5$rounds=10000$12345678$JBinliYMFEcBeAXKZnLjenhgEhTmJBvZn3aR8l70Oy/",
+ secret="123", algorithm="sha256_crypt", salt="12345678", rounds=10000)
+ assert_hash("$6$12345678$LcV9LQiaPekQxZ.OfkMADjFdSO2k9zfbDQrHPVcYjSLqSdjLYpsgqviYvTEP/R41yPmhH3CCeEDqVhW1VHr3L.",
+ secret="123", algorithm="sha512_crypt", salt="12345678", rounds=5000)
+
+
+@pytest.mark.skipif(sys.platform.startswith('darwin'), reason='macOS requires passlib')
+def test_encrypt_default_rounds_no_passlib():
+ with passlib_off():
+ assert_hash("$1$12345678$tRy4cXc3kmcfRZVj4iFXr/",
+ secret="123", algorithm="md5_crypt", salt="12345678")
+ assert_hash("$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7",
+ secret="123", algorithm="sha256_crypt", salt="12345678")
+ assert_hash("$6$12345678$LcV9LQiaPekQxZ.OfkMADjFdSO2k9zfbDQrHPVcYjSLqSdjLYpsgqviYvTEP/R41yPmhH3CCeEDqVhW1VHr3L.",
+ secret="123", algorithm="sha512_crypt", salt="12345678")
+
+ assert encrypt.CryptHash("md5_crypt").hash("123")
+
+
+# If passlib is not installed. this is identical to the test_encrypt_default_rounds_no_passlib() test
+@pytest.mark.skipif(not encrypt.PASSLIB_AVAILABLE, reason='passlib must be installed to run this test')
+def test_encrypt_default_rounds():
+ assert_hash("$1$12345678$tRy4cXc3kmcfRZVj4iFXr/",
+ secret="123", algorithm="md5_crypt", salt="12345678")
+ assert_hash("$5$rounds=535000$12345678$uy3TurUPaY71aioJi58HvUY8jkbhSQU8HepbyaNngv.",
+ secret="123", algorithm="sha256_crypt", salt="12345678")
+ assert_hash("$6$rounds=656000$12345678$InMy49UwxyCh2pGJU1NpOhVSElDDzKeyuC6n6E9O34BCUGVNYADnI.rcA3m.Vro9BiZpYmjEoNhpREqQcbvQ80",
+ secret="123", algorithm="sha512_crypt", salt="12345678")
+
+ assert encrypt.PasslibHash("md5_crypt").hash("123")
+
+
+@pytest.mark.skipif(sys.platform.startswith('darwin'), reason='macOS requires passlib')
+def test_password_hash_filter_no_passlib():
+ with passlib_off():
+ assert not encrypt.PASSLIB_AVAILABLE
+ assert get_encrypted_password("123", "md5", salt="12345678") == "$1$12345678$tRy4cXc3kmcfRZVj4iFXr/"
+
+ with pytest.raises(AnsibleFilterError):
+ get_encrypted_password("123", "crypt16", salt="12")
+
+
+@pytest.mark.skipif(not encrypt.PASSLIB_AVAILABLE, reason='passlib must be installed to run this test')
+def test_password_hash_filter_passlib():
+
+ with pytest.raises(AnsibleFilterError):
+ get_encrypted_password("123", "sha257", salt="12345678")
+
+ # Uses passlib default rounds value for sha256 matching crypt behaviour
+ assert get_encrypted_password("123", "sha256", salt="12345678") == "$5$rounds=535000$12345678$uy3TurUPaY71aioJi58HvUY8jkbhSQU8HepbyaNngv."
+ assert get_encrypted_password("123", "sha256", salt="12345678", rounds=5000) == "$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7"
+
+ assert (get_encrypted_password("123", "sha256", salt="12345678", rounds=10000) ==
+ "$5$rounds=10000$12345678$JBinliYMFEcBeAXKZnLjenhgEhTmJBvZn3aR8l70Oy/")
+
+ assert (get_encrypted_password("123", "sha512", salt="12345678", rounds=6000) ==
+ "$6$rounds=6000$12345678$l/fC67BdJwZrJ7qneKGP1b6PcatfBr0dI7W6JLBrsv8P1wnv/0pu4WJsWq5p6WiXgZ2gt9Aoir3MeORJxg4.Z/")
+
+ assert (get_encrypted_password("123", "sha512", salt="12345678", rounds=5000) ==
+ "$6$12345678$LcV9LQiaPekQxZ.OfkMADjFdSO2k9zfbDQrHPVcYjSLqSdjLYpsgqviYvTEP/R41yPmhH3CCeEDqVhW1VHr3L.")
+
+ assert get_encrypted_password("123", "crypt16", salt="12") == "12pELHK2ME3McUFlHxel6uMM"
+
+ # Try algorithm that uses a raw salt
+ assert get_encrypted_password("123", "pbkdf2_sha256")
+ # Try algorithm with ident
+ assert get_encrypted_password("123", "pbkdf2_sha256", ident='invalid_ident')
+
+
+@pytest.mark.skipif(sys.platform.startswith('darwin'), reason='macOS requires passlib')
+def test_do_encrypt_no_passlib():
+ with passlib_off():
+ assert not encrypt.PASSLIB_AVAILABLE
+ assert encrypt.do_encrypt("123", "md5_crypt", salt="12345678") == "$1$12345678$tRy4cXc3kmcfRZVj4iFXr/"
+
+ with pytest.raises(AnsibleError):
+ encrypt.do_encrypt("123", "crypt16", salt="12")
+
+
+@pytest.mark.skipif(not encrypt.PASSLIB_AVAILABLE, reason='passlib must be installed to run this test')
+def test_do_encrypt_passlib():
+ with pytest.raises(AnsibleError):
+ encrypt.do_encrypt("123", "sha257_crypt", salt="12345678")
+
+ # Uses passlib default rounds value for sha256 matching crypt behaviour.
+ assert encrypt.do_encrypt("123", "sha256_crypt", salt="12345678") == "$5$rounds=535000$12345678$uy3TurUPaY71aioJi58HvUY8jkbhSQU8HepbyaNngv."
+
+ assert encrypt.do_encrypt("123", "md5_crypt", salt="12345678") == "$1$12345678$tRy4cXc3kmcfRZVj4iFXr/"
+
+ assert encrypt.do_encrypt("123", "crypt16", salt="12") == "12pELHK2ME3McUFlHxel6uMM"
+
+ assert encrypt.do_encrypt("123", "bcrypt",
+ salt='1234567890123456789012',
+ ident='2a') == "$2a$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu"
+
+
+def test_random_salt():
+ res = encrypt.random_salt()
+ expected_salt_candidate_chars = u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./'
+ assert len(res) == 8
+ for res_char in res:
+ assert res_char in expected_salt_candidate_chars
+
+
+@pytest.mark.skipif(sys.platform.startswith('darwin'), reason='macOS requires passlib')
+def test_invalid_crypt_salt():
+ pytest.raises(
+ AnsibleError,
+ encrypt.CryptHash('bcrypt')._salt,
+ '_',
+ None
+ )
+ encrypt.CryptHash('bcrypt')._salt('1234567890123456789012', None)
+ pytest.raises(
+ AnsibleError,
+ encrypt.CryptHash('bcrypt')._salt,
+ 'kljsdf',
+ None
+ )
+ encrypt.CryptHash('sha256_crypt')._salt('123456', None)
+ pytest.raises(
+ AnsibleError,
+ encrypt.CryptHash('sha256_crypt')._salt,
+ '1234567890123456789012',
+ None
+ )
+
+
+def test_passlib_bcrypt_salt(recwarn):
+ passlib_exc = pytest.importorskip("passlib.exc")
+
+ secret = 'foo'
+ salt = '1234567890123456789012'
+ repaired_salt = '123456789012345678901u'
+ expected = '$2b$12$123456789012345678901uMv44x.2qmQeefEGb3bcIRc1mLuO7bqa'
+ ident = '2b'
+
+ p = encrypt.PasslibHash('bcrypt')
+
+ result = p.hash(secret, salt=salt, ident=ident)
+ passlib_warnings = [w.message for w in recwarn if isinstance(w.message, passlib_exc.PasslibHashWarning)]
+ assert len(passlib_warnings) == 0
+ assert result == expected
+
+ recwarn.clear()
+
+ result = p.hash(secret, salt=repaired_salt, ident=ident)
+ assert result == expected
diff --git a/test/units/utils/test_helpers.py b/test/units/utils/test_helpers.py
new file mode 100644
index 0000000..ec37b39
--- /dev/null
+++ b/test/units/utils/test_helpers.py
@@ -0,0 +1,34 @@
+# (c) 2015, Marius Gedminas <marius@gedmin.as>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import unittest
+
+from ansible.utils.helpers import pct_to_int
+
+
+class TestHelpers(unittest.TestCase):
+
+ def test_pct_to_int(self):
+ self.assertEqual(pct_to_int(1, 100), 1)
+ self.assertEqual(pct_to_int(-1, 100), -1)
+ self.assertEqual(pct_to_int("1%", 10), 1)
+ self.assertEqual(pct_to_int("1%", 10, 0), 0)
+ self.assertEqual(pct_to_int("1", 100), 1)
+ self.assertEqual(pct_to_int("10%", 100), 10)
diff --git a/test/units/utils/test_isidentifier.py b/test/units/utils/test_isidentifier.py
new file mode 100644
index 0000000..de6de64
--- /dev/null
+++ b/test/units/utils/test_isidentifier.py
@@ -0,0 +1,49 @@
+# -*- 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)
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import pytest
+
+from ansible.utils.vars import isidentifier
+
+
+# Originally posted at: http://stackoverflow.com/a/29586366
+
+
+@pytest.mark.parametrize(
+ "identifier", [
+ "foo", "foo1_23",
+ ]
+)
+def test_valid_identifier(identifier):
+ assert isidentifier(identifier)
+
+
+@pytest.mark.parametrize(
+ "identifier", [
+ "pass", "foo ", " foo", "1234", "1234abc", "", " ", "foo bar", "no-dashed-names-for-you",
+ ]
+)
+def test_invalid_identifier(identifier):
+ assert not isidentifier(identifier)
+
+
+def test_keywords_not_in_PY2():
+ """In Python 2 ("True", "False", "None") are not keywords. The isidentifier
+ method ensures that those are treated as keywords on both Python 2 and 3.
+ """
+ assert not isidentifier("True")
+ assert not isidentifier("False")
+ assert not isidentifier("None")
+
+
+def test_non_ascii():
+ """In Python 3 non-ascii characters are allowed as opposed to Python 2. The
+ isidentifier method ensures that those are treated as keywords on both
+ Python 2 and 3.
+ """
+ assert not isidentifier("křížek")
diff --git a/test/units/utils/test_plugin_docs.py b/test/units/utils/test_plugin_docs.py
new file mode 100644
index 0000000..ff973b1
--- /dev/null
+++ b/test/units/utils/test_plugin_docs.py
@@ -0,0 +1,333 @@
+# -*- coding: utf-8 -*-
+# (c) 2020 Felix Fontein <felix@fontein.de>
+# 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 copy
+
+import pytest
+
+from ansible.utils.plugin_docs import (
+ add_collection_to_versions_and_dates,
+)
+
+
+ADD_TESTS = [
+ (
+ # Module options
+ True,
+ False,
+ {
+ 'author': 'x',
+ 'version_added': '1.0.0',
+ 'deprecated': {
+ 'removed_in': '2.0.0',
+ },
+ 'options': {
+ 'test': {
+ 'description': '',
+ 'type': 'str',
+ 'version_added': '1.1.0',
+ 'deprecated': {
+ # should not be touched since this isn't a plugin
+ 'removed_in': '2.0.0',
+ },
+ 'env': [
+ # should not be touched since this isn't a plugin
+ {
+ 'version_added': '1.3.0',
+ 'deprecated': {
+ 'version': '2.0.0',
+ },
+ },
+ ],
+ 'ini': [
+ # should not be touched since this isn't a plugin
+ {
+ 'version_added': '1.3.0',
+ 'deprecated': {
+ 'version': '2.0.0',
+ },
+ },
+ ],
+ 'vars': [
+ # should not be touched since this isn't a plugin
+ {
+ 'version_added': '1.3.0',
+ 'deprecated': {
+ 'removed_at_date': '2020-01-01',
+ },
+ },
+ ],
+ },
+ 'subtest': {
+ 'description': '',
+ 'type': 'dict',
+ 'deprecated': {
+ # should not be touched since this isn't a plugin
+ 'version': '2.0.0',
+ },
+ 'suboptions': {
+ 'suboption': {
+ 'description': '',
+ 'type': 'int',
+ 'version_added': '1.2.0',
+ }
+ },
+ }
+ },
+ },
+ {
+ 'author': 'x',
+ 'version_added': '1.0.0',
+ 'version_added_collection': 'foo.bar',
+ 'deprecated': {
+ 'removed_in': '2.0.0',
+ 'removed_from_collection': 'foo.bar',
+ },
+ 'options': {
+ 'test': {
+ 'description': '',
+ 'type': 'str',
+ 'version_added': '1.1.0',
+ 'version_added_collection': 'foo.bar',
+ 'deprecated': {
+ # should not be touched since this isn't a plugin
+ 'removed_in': '2.0.0',
+ },
+ 'env': [
+ # should not be touched since this isn't a plugin
+ {
+ 'version_added': '1.3.0',
+ 'deprecated': {
+ 'version': '2.0.0',
+ },
+ },
+ ],
+ 'ini': [
+ # should not be touched since this isn't a plugin
+ {
+ 'version_added': '1.3.0',
+ 'deprecated': {
+ 'version': '2.0.0',
+ },
+ },
+ ],
+ 'vars': [
+ # should not be touched since this isn't a plugin
+ {
+ 'version_added': '1.3.0',
+ 'deprecated': {
+ 'removed_at_date': '2020-01-01',
+ },
+ },
+ ],
+ },
+ 'subtest': {
+ 'description': '',
+ 'type': 'dict',
+ 'deprecated': {
+ # should not be touched since this isn't a plugin
+ 'version': '2.0.0',
+ },
+ 'suboptions': {
+ 'suboption': {
+ 'description': '',
+ 'type': 'int',
+ 'version_added': '1.2.0',
+ 'version_added_collection': 'foo.bar',
+ }
+ },
+ }
+ },
+ },
+ ),
+ (
+ # Module options
+ True,
+ False,
+ {
+ 'author': 'x',
+ 'deprecated': {
+ 'removed_at_date': '2020-01-01',
+ },
+ },
+ {
+ 'author': 'x',
+ 'deprecated': {
+ 'removed_at_date': '2020-01-01',
+ 'removed_from_collection': 'foo.bar',
+ },
+ },
+ ),
+ (
+ # Plugin options
+ False,
+ False,
+ {
+ 'author': 'x',
+ 'version_added': '1.0.0',
+ 'deprecated': {
+ 'removed_in': '2.0.0',
+ },
+ 'options': {
+ 'test': {
+ 'description': '',
+ 'type': 'str',
+ 'version_added': '1.1.0',
+ 'deprecated': {
+ # should not be touched since this is the wrong name
+ 'removed_in': '2.0.0',
+ },
+ 'env': [
+ {
+ 'version_added': '1.3.0',
+ 'deprecated': {
+ 'version': '2.0.0',
+ },
+ },
+ ],
+ 'ini': [
+ {
+ 'version_added': '1.3.0',
+ 'deprecated': {
+ 'version': '2.0.0',
+ },
+ },
+ ],
+ 'vars': [
+ {
+ 'version_added': '1.3.0',
+ 'deprecated': {
+ 'removed_at_date': '2020-01-01',
+ },
+ },
+ ],
+ },
+ 'subtest': {
+ 'description': '',
+ 'type': 'dict',
+ 'deprecated': {
+ 'version': '2.0.0',
+ },
+ 'suboptions': {
+ 'suboption': {
+ 'description': '',
+ 'type': 'int',
+ 'version_added': '1.2.0',
+ }
+ },
+ }
+ },
+ },
+ {
+ 'author': 'x',
+ 'version_added': '1.0.0',
+ 'version_added_collection': 'foo.bar',
+ 'deprecated': {
+ 'removed_in': '2.0.0',
+ 'removed_from_collection': 'foo.bar',
+ },
+ 'options': {
+ 'test': {
+ 'description': '',
+ 'type': 'str',
+ 'version_added': '1.1.0',
+ 'version_added_collection': 'foo.bar',
+ 'deprecated': {
+ # should not be touched since this is the wrong name
+ 'removed_in': '2.0.0',
+ },
+ 'env': [
+ {
+ 'version_added': '1.3.0',
+ 'version_added_collection': 'foo.bar',
+ 'deprecated': {
+ 'version': '2.0.0',
+ 'collection_name': 'foo.bar',
+ },
+ },
+ ],
+ 'ini': [
+ {
+ 'version_added': '1.3.0',
+ 'version_added_collection': 'foo.bar',
+ 'deprecated': {
+ 'version': '2.0.0',
+ 'collection_name': 'foo.bar',
+ },
+ },
+ ],
+ 'vars': [
+ {
+ 'version_added': '1.3.0',
+ 'version_added_collection': 'foo.bar',
+ 'deprecated': {
+ 'removed_at_date': '2020-01-01',
+ 'collection_name': 'foo.bar',
+ },
+ },
+ ],
+ },
+ 'subtest': {
+ 'description': '',
+ 'type': 'dict',
+ 'deprecated': {
+ 'version': '2.0.0',
+ 'collection_name': 'foo.bar',
+ },
+ 'suboptions': {
+ 'suboption': {
+ 'description': '',
+ 'type': 'int',
+ 'version_added': '1.2.0',
+ 'version_added_collection': 'foo.bar',
+ }
+ },
+ }
+ },
+ },
+ ),
+ (
+ # Return values
+ True, # this value is is ignored
+ True,
+ {
+ 'rv1': {
+ 'version_added': '1.0.0',
+ 'type': 'dict',
+ 'contains': {
+ 'srv1': {
+ 'version_added': '1.1.0',
+ },
+ 'srv2': {
+ },
+ }
+ },
+ },
+ {
+ 'rv1': {
+ 'version_added': '1.0.0',
+ 'version_added_collection': 'foo.bar',
+ 'type': 'dict',
+ 'contains': {
+ 'srv1': {
+ 'version_added': '1.1.0',
+ 'version_added_collection': 'foo.bar',
+ },
+ 'srv2': {
+ },
+ }
+ },
+ },
+ ),
+]
+
+
+@pytest.mark.parametrize('is_module,return_docs,fragment,expected_fragment', ADD_TESTS)
+def test_add(is_module, return_docs, fragment, expected_fragment):
+ fragment_copy = copy.deepcopy(fragment)
+ add_collection_to_versions_and_dates(fragment_copy, 'foo.bar', is_module, return_docs)
+ assert fragment_copy == expected_fragment
diff --git a/test/units/utils/test_shlex.py b/test/units/utils/test_shlex.py
new file mode 100644
index 0000000..e13d302
--- /dev/null
+++ b/test/units/utils/test_shlex.py
@@ -0,0 +1,41 @@
+# (c) 2015, Marius Gedminas <marius@gedmin.as>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import unittest
+
+from ansible.utils.shlex import shlex_split
+
+
+class TestSplit(unittest.TestCase):
+
+ def test_trivial(self):
+ self.assertEqual(shlex_split("a b c"), ["a", "b", "c"])
+
+ def test_unicode(self):
+ self.assertEqual(shlex_split(u"a b \u010D"), [u"a", u"b", u"\u010D"])
+
+ def test_quoted(self):
+ self.assertEqual(shlex_split('"a b" c'), ["a b", "c"])
+
+ def test_comments(self):
+ self.assertEqual(shlex_split('"a b" c # d', comments=True), ["a b", "c"])
+
+ def test_error(self):
+ self.assertRaises(ValueError, shlex_split, 'a "b')
diff --git a/test/units/utils/test_unsafe_proxy.py b/test/units/utils/test_unsafe_proxy.py
new file mode 100644
index 0000000..ea653cf
--- /dev/null
+++ b/test/units/utils/test_unsafe_proxy.py
@@ -0,0 +1,121 @@
+# -*- coding: utf-8 -*-
+# (c) 2018 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.six import PY3
+from ansible.utils.unsafe_proxy import AnsibleUnsafe, AnsibleUnsafeBytes, AnsibleUnsafeText, wrap_var
+from ansible.module_utils.common.text.converters import to_text, to_bytes
+
+
+def test_wrap_var_text():
+ assert isinstance(wrap_var(u'foo'), AnsibleUnsafeText)
+
+
+def test_wrap_var_bytes():
+ assert isinstance(wrap_var(b'foo'), AnsibleUnsafeBytes)
+
+
+def test_wrap_var_string():
+ if PY3:
+ assert isinstance(wrap_var('foo'), AnsibleUnsafeText)
+ else:
+ assert isinstance(wrap_var('foo'), AnsibleUnsafeBytes)
+
+
+def test_wrap_var_dict():
+ assert isinstance(wrap_var(dict(foo='bar')), dict)
+ assert not isinstance(wrap_var(dict(foo='bar')), AnsibleUnsafe)
+ assert isinstance(wrap_var(dict(foo=u'bar'))['foo'], AnsibleUnsafeText)
+
+
+def test_wrap_var_dict_None():
+ assert wrap_var(dict(foo=None))['foo'] is None
+ assert not isinstance(wrap_var(dict(foo=None))['foo'], AnsibleUnsafe)
+
+
+def test_wrap_var_list():
+ assert isinstance(wrap_var(['foo']), list)
+ assert not isinstance(wrap_var(['foo']), AnsibleUnsafe)
+ assert isinstance(wrap_var([u'foo'])[0], AnsibleUnsafeText)
+
+
+def test_wrap_var_list_None():
+ assert wrap_var([None])[0] is None
+ assert not isinstance(wrap_var([None])[0], AnsibleUnsafe)
+
+
+def test_wrap_var_set():
+ assert isinstance(wrap_var(set(['foo'])), set)
+ assert not isinstance(wrap_var(set(['foo'])), AnsibleUnsafe)
+ for item in wrap_var(set([u'foo'])):
+ assert isinstance(item, AnsibleUnsafeText)
+
+
+def test_wrap_var_set_None():
+ for item in wrap_var(set([None])):
+ assert item is None
+ assert not isinstance(item, AnsibleUnsafe)
+
+
+def test_wrap_var_tuple():
+ assert isinstance(wrap_var(('foo',)), tuple)
+ assert not isinstance(wrap_var(('foo',)), AnsibleUnsafe)
+ assert isinstance(wrap_var(('foo',))[0], AnsibleUnsafe)
+
+
+def test_wrap_var_tuple_None():
+ assert wrap_var((None,))[0] is None
+ assert not isinstance(wrap_var((None,))[0], AnsibleUnsafe)
+
+
+def test_wrap_var_None():
+ assert wrap_var(None) is None
+ assert not isinstance(wrap_var(None), AnsibleUnsafe)
+
+
+def test_wrap_var_unsafe_text():
+ assert isinstance(wrap_var(AnsibleUnsafeText(u'foo')), AnsibleUnsafeText)
+
+
+def test_wrap_var_unsafe_bytes():
+ assert isinstance(wrap_var(AnsibleUnsafeBytes(b'foo')), AnsibleUnsafeBytes)
+
+
+def test_wrap_var_no_ref():
+ thing = {
+ 'foo': {
+ 'bar': 'baz'
+ },
+ 'bar': ['baz', 'qux'],
+ 'baz': ('qux',),
+ 'none': None,
+ 'text': 'text',
+ }
+ wrapped_thing = wrap_var(thing)
+ thing is not wrapped_thing
+ thing['foo'] is not wrapped_thing['foo']
+ thing['bar'][0] is not wrapped_thing['bar'][0]
+ thing['baz'][0] is not wrapped_thing['baz'][0]
+ thing['none'] is not wrapped_thing['none']
+ thing['text'] is not wrapped_thing['text']
+
+
+def test_AnsibleUnsafeText():
+ assert isinstance(AnsibleUnsafeText(u'foo'), AnsibleUnsafe)
+
+
+def test_AnsibleUnsafeBytes():
+ assert isinstance(AnsibleUnsafeBytes(b'foo'), AnsibleUnsafe)
+
+
+def test_to_text_unsafe():
+ assert isinstance(to_text(AnsibleUnsafeBytes(b'foo')), AnsibleUnsafeText)
+ assert to_text(AnsibleUnsafeBytes(b'foo')) == AnsibleUnsafeText(u'foo')
+
+
+def test_to_bytes_unsafe():
+ assert isinstance(to_bytes(AnsibleUnsafeText(u'foo')), AnsibleUnsafeBytes)
+ assert to_bytes(AnsibleUnsafeText(u'foo')) == AnsibleUnsafeBytes(b'foo')
diff --git a/test/units/utils/test_vars.py b/test/units/utils/test_vars.py
new file mode 100644
index 0000000..9be33de
--- /dev/null
+++ b/test/units/utils/test_vars.py
@@ -0,0 +1,284 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+# (c) 2015, Toshio Kuraotmi <tkuratomi@ansible.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from collections import defaultdict
+
+from unittest import mock
+
+from units.compat import unittest
+from ansible.errors import AnsibleError
+from ansible.utils.vars import combine_vars, merge_hash
+
+
+class TestVariableUtils(unittest.TestCase):
+ def setUp(self):
+ pass
+
+ def tearDown(self):
+ pass
+
+ combine_vars_merge_data = (
+ dict(
+ a=dict(a=1),
+ b=dict(b=2),
+ result=dict(a=1, b=2),
+ ),
+ dict(
+ a=dict(a=1, c=dict(foo='bar')),
+ b=dict(b=2, c=dict(baz='bam')),
+ result=dict(a=1, b=2, c=dict(foo='bar', baz='bam'))
+ ),
+ dict(
+ a=defaultdict(a=1, c=defaultdict(foo='bar')),
+ b=dict(b=2, c=dict(baz='bam')),
+ result=defaultdict(a=1, b=2, c=defaultdict(foo='bar', baz='bam'))
+ ),
+ )
+ combine_vars_replace_data = (
+ dict(
+ a=dict(a=1),
+ b=dict(b=2),
+ result=dict(a=1, b=2)
+ ),
+ dict(
+ a=dict(a=1, c=dict(foo='bar')),
+ b=dict(b=2, c=dict(baz='bam')),
+ result=dict(a=1, b=2, c=dict(baz='bam'))
+ ),
+ dict(
+ a=defaultdict(a=1, c=dict(foo='bar')),
+ b=dict(b=2, c=defaultdict(baz='bam')),
+ result=defaultdict(a=1, b=2, c=defaultdict(baz='bam'))
+ ),
+ )
+
+ def test_combine_vars_improper_args(self):
+ with mock.patch('ansible.constants.DEFAULT_HASH_BEHAVIOUR', 'replace'):
+ with self.assertRaises(AnsibleError):
+ combine_vars([1, 2, 3], dict(a=1))
+ with self.assertRaises(AnsibleError):
+ combine_vars(dict(a=1), [1, 2, 3])
+
+ with mock.patch('ansible.constants.DEFAULT_HASH_BEHAVIOUR', 'merge'):
+ with self.assertRaises(AnsibleError):
+ combine_vars([1, 2, 3], dict(a=1))
+ with self.assertRaises(AnsibleError):
+ combine_vars(dict(a=1), [1, 2, 3])
+
+ def test_combine_vars_replace(self):
+ with mock.patch('ansible.constants.DEFAULT_HASH_BEHAVIOUR', 'replace'):
+ for test in self.combine_vars_replace_data:
+ self.assertEqual(combine_vars(test['a'], test['b']), test['result'])
+
+ def test_combine_vars_merge(self):
+ with mock.patch('ansible.constants.DEFAULT_HASH_BEHAVIOUR', 'merge'):
+ for test in self.combine_vars_merge_data:
+ self.assertEqual(combine_vars(test['a'], test['b']), test['result'])
+
+ merge_hash_data = {
+ "low_prio": {
+ "a": {
+ "a'": {
+ "x": "low_value",
+ "y": "low_value",
+ "list": ["low_value"]
+ }
+ },
+ "b": [1, 1, 2, 3]
+ },
+ "high_prio": {
+ "a": {
+ "a'": {
+ "y": "high_value",
+ "z": "high_value",
+ "list": ["high_value"]
+ }
+ },
+ "b": [3, 4, 4, {"5": "value"}]
+ }
+ }
+
+ def test_merge_hash_simple(self):
+ for test in self.combine_vars_merge_data:
+ self.assertEqual(merge_hash(test['a'], test['b']), test['result'])
+
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": {
+ "a'": {
+ "x": "low_value",
+ "y": "high_value",
+ "z": "high_value",
+ "list": ["high_value"]
+ }
+ },
+ "b": high['b']
+ }
+ self.assertEqual(merge_hash(low, high), expected)
+
+ def test_merge_hash_non_recursive_and_list_replace(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = high
+ self.assertEqual(merge_hash(low, high, False, 'replace'), expected)
+
+ def test_merge_hash_non_recursive_and_list_keep(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": high['a'],
+ "b": low['b']
+ }
+ self.assertEqual(merge_hash(low, high, False, 'keep'), expected)
+
+ def test_merge_hash_non_recursive_and_list_append(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": high['a'],
+ "b": low['b'] + high['b']
+ }
+ self.assertEqual(merge_hash(low, high, False, 'append'), expected)
+
+ def test_merge_hash_non_recursive_and_list_prepend(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": high['a'],
+ "b": high['b'] + low['b']
+ }
+ self.assertEqual(merge_hash(low, high, False, 'prepend'), expected)
+
+ def test_merge_hash_non_recursive_and_list_append_rp(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": high['a'],
+ "b": [1, 1, 2] + high['b']
+ }
+ self.assertEqual(merge_hash(low, high, False, 'append_rp'), expected)
+
+ def test_merge_hash_non_recursive_and_list_prepend_rp(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": high['a'],
+ "b": high['b'] + [1, 1, 2]
+ }
+ self.assertEqual(merge_hash(low, high, False, 'prepend_rp'), expected)
+
+ def test_merge_hash_recursive_and_list_replace(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": {
+ "a'": {
+ "x": "low_value",
+ "y": "high_value",
+ "z": "high_value",
+ "list": ["high_value"]
+ }
+ },
+ "b": high['b']
+ }
+ self.assertEqual(merge_hash(low, high, True, 'replace'), expected)
+
+ def test_merge_hash_recursive_and_list_keep(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": {
+ "a'": {
+ "x": "low_value",
+ "y": "high_value",
+ "z": "high_value",
+ "list": ["low_value"]
+ }
+ },
+ "b": low['b']
+ }
+ self.assertEqual(merge_hash(low, high, True, 'keep'), expected)
+
+ def test_merge_hash_recursive_and_list_append(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": {
+ "a'": {
+ "x": "low_value",
+ "y": "high_value",
+ "z": "high_value",
+ "list": ["low_value", "high_value"]
+ }
+ },
+ "b": low['b'] + high['b']
+ }
+ self.assertEqual(merge_hash(low, high, True, 'append'), expected)
+
+ def test_merge_hash_recursive_and_list_prepend(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": {
+ "a'": {
+ "x": "low_value",
+ "y": "high_value",
+ "z": "high_value",
+ "list": ["high_value", "low_value"]
+ }
+ },
+ "b": high['b'] + low['b']
+ }
+ self.assertEqual(merge_hash(low, high, True, 'prepend'), expected)
+
+ def test_merge_hash_recursive_and_list_append_rp(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": {
+ "a'": {
+ "x": "low_value",
+ "y": "high_value",
+ "z": "high_value",
+ "list": ["low_value", "high_value"]
+ }
+ },
+ "b": [1, 1, 2] + high['b']
+ }
+ self.assertEqual(merge_hash(low, high, True, 'append_rp'), expected)
+
+ def test_merge_hash_recursive_and_list_prepend_rp(self):
+ low = self.merge_hash_data['low_prio']
+ high = self.merge_hash_data['high_prio']
+ expected = {
+ "a": {
+ "a'": {
+ "x": "low_value",
+ "y": "high_value",
+ "z": "high_value",
+ "list": ["high_value", "low_value"]
+ }
+ },
+ "b": high['b'] + [1, 1, 2]
+ }
+ self.assertEqual(merge_hash(low, high, True, 'prepend_rp'), expected)
diff --git a/test/units/utils/test_version.py b/test/units/utils/test_version.py
new file mode 100644
index 0000000..3c2cbaf
--- /dev/null
+++ b/test/units/utils/test_version.py
@@ -0,0 +1,335 @@
+# -*- coding: utf-8 -*-
+# (c) 2020 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.compat.version import LooseVersion, StrictVersion
+
+import pytest
+
+from ansible.utils.version import _Alpha, _Numeric, SemanticVersion
+
+
+EQ = [
+ ('1.0.0', '1.0.0', True),
+ ('1.0.0', '1.0.0-beta', False),
+ ('1.0.0-beta2+build1', '1.0.0-beta.2+build.1', False),
+ ('1.0.0-beta+build', '1.0.0-beta+build', True),
+ ('1.0.0-beta+build1', '1.0.0-beta+build2', True),
+ ('1.0.0-beta+a', '1.0.0-alpha+bar', False),
+]
+
+NE = [
+ ('1.0.0', '1.0.0', False),
+ ('1.0.0', '1.0.0-beta', True),
+ ('1.0.0-beta2+build1', '1.0.0-beta.2+build.1', True),
+ ('1.0.0-beta+build', '1.0.0-beta+build', False),
+ ('1.0.0-beta+a', '1.0.0-alpha+bar', True),
+]
+
+LT = [
+ ('1.0.0', '2.0.0', True),
+ ('1.0.0-beta', '2.0.0-alpha', True),
+ ('1.0.0-alpha', '2.0.0-beta', True),
+ ('1.0.0-alpha', '1.0.0', True),
+ ('1.0.0-beta', '1.0.0-alpha3', False),
+ ('1.0.0+foo', '1.0.0-alpha', False),
+ ('1.0.0-beta.1', '1.0.0-beta.a', True),
+ ('1.0.0-beta+a', '1.0.0-alpha+bar', False),
+]
+
+GT = [
+ ('1.0.0', '2.0.0', False),
+ ('1.0.0-beta', '2.0.0-alpha', False),
+ ('1.0.0-alpha', '2.0.0-beta', False),
+ ('1.0.0-alpha', '1.0.0', False),
+ ('1.0.0-beta', '1.0.0-alpha3', True),
+ ('1.0.0+foo', '1.0.0-alpha', True),
+ ('1.0.0-beta.1', '1.0.0-beta.a', False),
+ ('1.0.0-beta+a', '1.0.0-alpha+bar', True),
+]
+
+LE = [
+ ('1.0.0', '1.0.0', True),
+ ('1.0.0', '2.0.0', True),
+ ('1.0.0-alpha', '1.0.0-beta', True),
+ ('1.0.0-beta', '1.0.0-alpha', False),
+]
+
+GE = [
+ ('1.0.0', '1.0.0', True),
+ ('1.0.0', '2.0.0', False),
+ ('1.0.0-alpha', '1.0.0-beta', False),
+ ('1.0.0-beta', '1.0.0-alpha', True),
+]
+
+VALID = [
+ "0.0.4",
+ "1.2.3",
+ "10.20.30",
+ "1.1.2-prerelease+meta",
+ "1.1.2+meta",
+ "1.1.2+meta-valid",
+ "1.0.0-alpha",
+ "1.0.0-beta",
+ "1.0.0-alpha.beta",
+ "1.0.0-alpha.beta.1",
+ "1.0.0-alpha.1",
+ "1.0.0-alpha0.valid",
+ "1.0.0-alpha.0valid",
+ "1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay",
+ "1.0.0-rc.1+build.1",
+ "2.0.0-rc.1+build.123",
+ "1.2.3-beta",
+ "10.2.3-DEV-SNAPSHOT",
+ "1.2.3-SNAPSHOT-123",
+ "1.0.0",
+ "2.0.0",
+ "1.1.7",
+ "2.0.0+build.1848",
+ "2.0.1-alpha.1227",
+ "1.0.0-alpha+beta",
+ "1.2.3----RC-SNAPSHOT.12.9.1--.12+788",
+ "1.2.3----R-S.12.9.1--.12+meta",
+ "1.2.3----RC-SNAPSHOT.12.9.1--.12",
+ "1.0.0+0.build.1-rc.10000aaa-kk-0.1",
+ "99999999999999999999999.999999999999999999.99999999999999999",
+ "1.0.0-0A.is.legal",
+]
+
+INVALID = [
+ "1",
+ "1.2",
+ "1.2.3-0123",
+ "1.2.3-0123.0123",
+ "1.1.2+.123",
+ "+invalid",
+ "-invalid",
+ "-invalid+invalid",
+ "-invalid.01",
+ "alpha",
+ "alpha.beta",
+ "alpha.beta.1",
+ "alpha.1",
+ "alpha+beta",
+ "alpha_beta",
+ "alpha.",
+ "alpha..",
+ "beta",
+ "1.0.0-alpha_beta",
+ "-alpha.",
+ "1.0.0-alpha..",
+ "1.0.0-alpha..1",
+ "1.0.0-alpha...1",
+ "1.0.0-alpha....1",
+ "1.0.0-alpha.....1",
+ "1.0.0-alpha......1",
+ "1.0.0-alpha.......1",
+ "01.1.1",
+ "1.01.1",
+ "1.1.01",
+ "1.2",
+ "1.2.3.DEV",
+ "1.2-SNAPSHOT",
+ "1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788",
+ "1.2-RC-SNAPSHOT",
+ "-1.0.3-gamma+b7718",
+ "+justmeta",
+ "9.8.7+meta+meta",
+ "9.8.7-whatever+meta+meta",
+]
+
+PRERELEASE = [
+ ('1.0.0-alpha', True),
+ ('1.0.0-alpha.1', True),
+ ('1.0.0-0.3.7', True),
+ ('1.0.0-x.7.z.92', True),
+ ('0.1.2', False),
+ ('0.1.2+bob', False),
+ ('1.0.0', False),
+]
+
+STABLE = [
+ ('1.0.0-alpha', False),
+ ('1.0.0-alpha.1', False),
+ ('1.0.0-0.3.7', False),
+ ('1.0.0-x.7.z.92', False),
+ ('0.1.2', False),
+ ('0.1.2+bob', False),
+ ('1.0.0', True),
+ ('1.0.0+bob', True),
+]
+
+LOOSE_VERSION = [
+ (LooseVersion('1'), SemanticVersion('1.0.0')),
+ (LooseVersion('1-alpha'), SemanticVersion('1.0.0-alpha')),
+ (LooseVersion('1.0.0-alpha+build'), SemanticVersion('1.0.0-alpha+build')),
+]
+
+LOOSE_VERSION_INVALID = [
+ LooseVersion('1.a.3'),
+ LooseVersion(),
+ 'bar',
+ StrictVersion('1.2.3'),
+]
+
+
+def test_semanticversion_none():
+ assert SemanticVersion().major is None
+
+
+@pytest.mark.parametrize('left,right,expected', EQ)
+def test_eq(left, right, expected):
+ assert (SemanticVersion(left) == SemanticVersion(right)) is expected
+
+
+@pytest.mark.parametrize('left,right,expected', NE)
+def test_ne(left, right, expected):
+ assert (SemanticVersion(left) != SemanticVersion(right)) is expected
+
+
+@pytest.mark.parametrize('left,right,expected', LT)
+def test_lt(left, right, expected):
+ assert (SemanticVersion(left) < SemanticVersion(right)) is expected
+
+
+@pytest.mark.parametrize('left,right,expected', LE)
+def test_le(left, right, expected):
+ assert (SemanticVersion(left) <= SemanticVersion(right)) is expected
+
+
+@pytest.mark.parametrize('left,right,expected', GT)
+def test_gt(left, right, expected):
+ assert (SemanticVersion(left) > SemanticVersion(right)) is expected
+
+
+@pytest.mark.parametrize('left,right,expected', GE)
+def test_ge(left, right, expected):
+ assert (SemanticVersion(left) >= SemanticVersion(right)) is expected
+
+
+@pytest.mark.parametrize('value', VALID)
+def test_valid(value):
+ SemanticVersion(value)
+
+
+@pytest.mark.parametrize('value', INVALID)
+def test_invalid(value):
+ pytest.raises(ValueError, SemanticVersion, value)
+
+
+def test_example_precedence():
+ # https://semver.org/#spec-item-11
+ sv = SemanticVersion
+ assert sv('1.0.0') < sv('2.0.0') < sv('2.1.0') < sv('2.1.1')
+ assert sv('1.0.0-alpha') < sv('1.0.0')
+ assert sv('1.0.0-alpha') < sv('1.0.0-alpha.1') < sv('1.0.0-alpha.beta')
+ assert sv('1.0.0-beta') < sv('1.0.0-beta.2') < sv('1.0.0-beta.11') < sv('1.0.0-rc.1') < sv('1.0.0')
+
+
+@pytest.mark.parametrize('value,expected', PRERELEASE)
+def test_prerelease(value, expected):
+ assert SemanticVersion(value).is_prerelease is expected
+
+
+@pytest.mark.parametrize('value,expected', STABLE)
+def test_stable(value, expected):
+ assert SemanticVersion(value).is_stable is expected
+
+
+@pytest.mark.parametrize('value,expected', LOOSE_VERSION)
+def test_from_loose_version(value, expected):
+ assert SemanticVersion.from_loose_version(value) == expected
+
+
+@pytest.mark.parametrize('value', LOOSE_VERSION_INVALID)
+def test_from_loose_version_invalid(value):
+ pytest.raises((AttributeError, ValueError), SemanticVersion.from_loose_version, value)
+
+
+def test_comparison_with_string():
+ assert SemanticVersion('1.0.0') > '0.1.0'
+
+
+def test_alpha():
+ assert _Alpha('a') == _Alpha('a')
+ assert _Alpha('a') == 'a'
+ assert _Alpha('a') != _Alpha('b')
+ assert _Alpha('a') != 1
+ assert _Alpha('a') < _Alpha('b')
+ assert _Alpha('a') < 'c'
+ assert _Alpha('a') > _Numeric(1)
+ with pytest.raises(ValueError):
+ _Alpha('a') < None
+ assert _Alpha('a') <= _Alpha('a')
+ assert _Alpha('a') <= _Alpha('b')
+ assert _Alpha('b') >= _Alpha('a')
+ assert _Alpha('b') >= _Alpha('b')
+
+ # The following 3*6 tests check that all comparison operators perform
+ # as expected. DO NOT remove any of them, or reformulate them (to remove
+ # the explicit `not`)!
+
+ assert _Alpha('a') == _Alpha('a')
+ assert not _Alpha('a') != _Alpha('a') # pylint: disable=unneeded-not
+ assert not _Alpha('a') < _Alpha('a') # pylint: disable=unneeded-not
+ assert _Alpha('a') <= _Alpha('a')
+ assert not _Alpha('a') > _Alpha('a') # pylint: disable=unneeded-not
+ assert _Alpha('a') >= _Alpha('a')
+
+ assert not _Alpha('a') == _Alpha('b') # pylint: disable=unneeded-not
+ assert _Alpha('a') != _Alpha('b')
+ assert _Alpha('a') < _Alpha('b')
+ assert _Alpha('a') <= _Alpha('b')
+ assert not _Alpha('a') > _Alpha('b') # pylint: disable=unneeded-not
+ assert not _Alpha('a') >= _Alpha('b') # pylint: disable=unneeded-not
+
+ assert not _Alpha('b') == _Alpha('a') # pylint: disable=unneeded-not
+ assert _Alpha('b') != _Alpha('a')
+ assert not _Alpha('b') < _Alpha('a') # pylint: disable=unneeded-not
+ assert not _Alpha('b') <= _Alpha('a') # pylint: disable=unneeded-not
+ assert _Alpha('b') > _Alpha('a')
+ assert _Alpha('b') >= _Alpha('a')
+
+
+def test_numeric():
+ assert _Numeric(1) == _Numeric(1)
+ assert _Numeric(1) == 1
+ assert _Numeric(1) != _Numeric(2)
+ assert _Numeric(1) != 'a'
+ assert _Numeric(1) < _Numeric(2)
+ assert _Numeric(1) < 3
+ assert _Numeric(1) < _Alpha('b')
+ with pytest.raises(ValueError):
+ _Numeric(1) < None
+ assert _Numeric(1) <= _Numeric(1)
+ assert _Numeric(1) <= _Numeric(2)
+ assert _Numeric(2) >= _Numeric(1)
+ assert _Numeric(2) >= _Numeric(2)
+
+ # The following 3*6 tests check that all comparison operators perform
+ # as expected. DO NOT remove any of them, or reformulate them (to remove
+ # the explicit `not`)!
+
+ assert _Numeric(1) == _Numeric(1)
+ assert not _Numeric(1) != _Numeric(1) # pylint: disable=unneeded-not
+ assert not _Numeric(1) < _Numeric(1) # pylint: disable=unneeded-not
+ assert _Numeric(1) <= _Numeric(1)
+ assert not _Numeric(1) > _Numeric(1) # pylint: disable=unneeded-not
+ assert _Numeric(1) >= _Numeric(1)
+
+ assert not _Numeric(1) == _Numeric(2) # pylint: disable=unneeded-not
+ assert _Numeric(1) != _Numeric(2)
+ assert _Numeric(1) < _Numeric(2)
+ assert _Numeric(1) <= _Numeric(2)
+ assert not _Numeric(1) > _Numeric(2) # pylint: disable=unneeded-not
+ assert not _Numeric(1) >= _Numeric(2) # pylint: disable=unneeded-not
+
+ assert not _Numeric(2) == _Numeric(1) # pylint: disable=unneeded-not
+ assert _Numeric(2) != _Numeric(1)
+ assert not _Numeric(2) < _Numeric(1) # pylint: disable=unneeded-not
+ assert not _Numeric(2) <= _Numeric(1) # pylint: disable=unneeded-not
+ assert _Numeric(2) > _Numeric(1)
+ assert _Numeric(2) >= _Numeric(1)
diff --git a/test/units/vars/__init__.py b/test/units/vars/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/units/vars/__init__.py
diff --git a/test/units/vars/test_module_response_deepcopy.py b/test/units/vars/test_module_response_deepcopy.py
new file mode 100644
index 0000000..78f9de0
--- /dev/null
+++ b/test/units/vars/test_module_response_deepcopy.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# (c) 2018 Matt Martz <matt@sivel.net>
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.vars.clean import module_response_deepcopy
+
+import pytest
+
+
+def test_module_response_deepcopy_basic():
+ x = 42
+ y = module_response_deepcopy(x)
+ assert y == x
+
+
+def test_module_response_deepcopy_atomic():
+ tests = [None, 42, 2**100, 3.14, True, False, 1j,
+ "hello", u"hello\u1234"]
+ for x in tests:
+ assert module_response_deepcopy(x) is x
+
+
+def test_module_response_deepcopy_list():
+ x = [[1, 2], 3]
+ y = module_response_deepcopy(x)
+ assert y == x
+ assert x is not y
+ assert x[0] is not y[0]
+
+
+def test_module_response_deepcopy_empty_tuple():
+ x = ()
+ y = module_response_deepcopy(x)
+ assert x is y
+
+
+@pytest.mark.skip(reason='No current support for this situation')
+def test_module_response_deepcopy_tuple():
+ x = ([1, 2], 3)
+ y = module_response_deepcopy(x)
+ assert y == x
+ assert x is not y
+ assert x[0] is not y[0]
+
+
+def test_module_response_deepcopy_tuple_of_immutables():
+ x = ((1, 2), 3)
+ y = module_response_deepcopy(x)
+ assert x is y
+
+
+def test_module_response_deepcopy_dict():
+ x = {"foo": [1, 2], "bar": 3}
+ y = module_response_deepcopy(x)
+ assert y == x
+ assert x is not y
+ assert x["foo"] is not y["foo"]
diff --git a/test/units/vars/test_variable_manager.py b/test/units/vars/test_variable_manager.py
new file mode 100644
index 0000000..67ec120
--- /dev/null
+++ b/test/units/vars/test_variable_manager.py
@@ -0,0 +1,307 @@
+# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import os
+
+from units.compat import unittest
+from unittest.mock import MagicMock, patch
+from ansible.inventory.manager import InventoryManager
+from ansible.module_utils.six import iteritems
+from ansible.playbook.play import Play
+
+
+from units.mock.loader import DictDataLoader
+from units.mock.path import mock_unfrackpath_noop
+
+from ansible.vars.manager import VariableManager
+
+
+class TestVariableManager(unittest.TestCase):
+
+ def test_basic_manager(self):
+ fake_loader = DictDataLoader({})
+
+ mock_inventory = MagicMock()
+ v = VariableManager(loader=fake_loader, inventory=mock_inventory)
+ variables = v.get_vars(use_cache=False)
+
+ # Check var manager expected values, never check: ['omit', 'vars']
+ # FIXME: add the following ['ansible_version', 'ansible_playbook_python', 'groups']
+ for varname, value in (('playbook_dir', os.path.abspath('.')), ):
+ self.assertEqual(variables[varname], value)
+
+ def test_variable_manager_extra_vars(self):
+ fake_loader = DictDataLoader({})
+
+ extra_vars = dict(a=1, b=2, c=3)
+ mock_inventory = MagicMock()
+ v = VariableManager(loader=fake_loader, inventory=mock_inventory)
+
+ # override internal extra_vars loading
+ v._extra_vars = extra_vars
+
+ myvars = v.get_vars(use_cache=False)
+ for (key, val) in iteritems(extra_vars):
+ self.assertEqual(myvars.get(key), val)
+
+ def test_variable_manager_options_vars(self):
+ fake_loader = DictDataLoader({})
+
+ options_vars = dict(a=1, b=2, c=3)
+ mock_inventory = MagicMock()
+ v = VariableManager(loader=fake_loader, inventory=mock_inventory)
+
+ # override internal options_vars loading
+ v._extra_vars = options_vars
+
+ myvars = v.get_vars(use_cache=False)
+ for (key, val) in iteritems(options_vars):
+ self.assertEqual(myvars.get(key), val)
+
+ def test_variable_manager_play_vars(self):
+ fake_loader = DictDataLoader({})
+
+ mock_play = MagicMock()
+ mock_play.get_vars.return_value = dict(foo="bar")
+ mock_play.get_roles.return_value = []
+ mock_play.get_vars_files.return_value = []
+
+ mock_inventory = MagicMock()
+ v = VariableManager(loader=fake_loader, inventory=mock_inventory)
+ self.assertEqual(v.get_vars(play=mock_play, use_cache=False).get("foo"), "bar")
+
+ def test_variable_manager_play_vars_files(self):
+ fake_loader = DictDataLoader({
+ __file__: """
+ foo: bar
+ """
+ })
+
+ mock_play = MagicMock()
+ mock_play.get_vars.return_value = dict()
+ mock_play.get_roles.return_value = []
+ mock_play.get_vars_files.return_value = [__file__]
+
+ mock_inventory = MagicMock()
+ v = VariableManager(inventory=mock_inventory, loader=fake_loader)
+ self.assertEqual(v.get_vars(play=mock_play, use_cache=False).get("foo"), "bar")
+
+ def test_variable_manager_task_vars(self):
+ # FIXME: BCS make this work
+ return
+
+ # pylint: disable=unreachable
+ fake_loader = DictDataLoader({})
+
+ mock_task = MagicMock()
+ mock_task._role = None
+ mock_task.loop = None
+ mock_task.get_vars.return_value = dict(foo="bar")
+ mock_task.get_include_params.return_value = dict()
+
+ mock_all = MagicMock()
+ mock_all.get_vars.return_value = {}
+ mock_all.get_file_vars.return_value = {}
+
+ mock_host = MagicMock()
+ mock_host.get.name.return_value = 'test01'
+ mock_host.get_vars.return_value = {}
+ mock_host.get_host_vars.return_value = {}
+
+ mock_inventory = MagicMock()
+ mock_inventory.hosts.get.return_value = mock_host
+ mock_inventory.hosts.get.name.return_value = 'test01'
+ mock_inventory.get_host.return_value = mock_host
+ mock_inventory.groups.__getitem__.return_value = mock_all
+
+ v = VariableManager(loader=fake_loader, inventory=mock_inventory)
+ self.assertEqual(v.get_vars(task=mock_task, use_cache=False).get("foo"), "bar")
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_variable_manager_precedence(self):
+ # FIXME: this needs to be redone as dataloader is not the automatic source of data anymore
+ return
+
+ # pylint: disable=unreachable
+ '''
+ Tests complex variations and combinations of get_vars() with different
+ objects to modify the context under which variables are merged.
+ '''
+ # FIXME: BCS makethiswork
+ # return True
+
+ mock_inventory = MagicMock()
+
+ inventory1_filedata = """
+ [group2:children]
+ group1
+
+ [group1]
+ host1 host_var=host_var_from_inventory_host1
+
+ [group1:vars]
+ group_var = group_var_from_inventory_group1
+
+ [group2:vars]
+ group_var = group_var_from_inventory_group2
+ """
+
+ fake_loader = DictDataLoader({
+ # inventory1
+ '/etc/ansible/inventory1': inventory1_filedata,
+ # role defaults_only1
+ '/etc/ansible/roles/defaults_only1/defaults/main.yml': """
+ default_var: "default_var_from_defaults_only1"
+ host_var: "host_var_from_defaults_only1"
+ group_var: "group_var_from_defaults_only1"
+ group_var_all: "group_var_all_from_defaults_only1"
+ extra_var: "extra_var_from_defaults_only1"
+ """,
+ '/etc/ansible/roles/defaults_only1/tasks/main.yml': """
+ - debug: msg="here i am"
+ """,
+
+ # role defaults_only2
+ '/etc/ansible/roles/defaults_only2/defaults/main.yml': """
+ default_var: "default_var_from_defaults_only2"
+ host_var: "host_var_from_defaults_only2"
+ group_var: "group_var_from_defaults_only2"
+ group_var_all: "group_var_all_from_defaults_only2"
+ extra_var: "extra_var_from_defaults_only2"
+ """,
+ })
+
+ inv1 = InventoryManager(loader=fake_loader, sources=['/etc/ansible/inventory1'])
+ v = VariableManager(inventory=mock_inventory, loader=fake_loader)
+
+ play1 = Play.load(dict(
+ hosts=['all'],
+ roles=['defaults_only1', 'defaults_only2'],
+ ), loader=fake_loader, variable_manager=v)
+
+ # first we assert that the defaults as viewed as a whole are the merged results
+ # of the defaults from each role, with the last role defined "winning" when
+ # there is a variable naming conflict
+ res = v.get_vars(play=play1)
+ self.assertEqual(res['default_var'], 'default_var_from_defaults_only2')
+
+ # next, we assert that when vars are viewed from the context of a task within a
+ # role, that task will see its own role defaults before any other role's
+ blocks = play1.compile()
+ task = blocks[1].block[0]
+ res = v.get_vars(play=play1, task=task)
+ self.assertEqual(res['default_var'], 'default_var_from_defaults_only1')
+
+ # next we assert the precedence of inventory variables
+ v.set_inventory(inv1)
+ h1 = inv1.get_host('host1')
+
+ res = v.get_vars(play=play1, host=h1)
+ self.assertEqual(res['group_var'], 'group_var_from_inventory_group1')
+ self.assertEqual(res['host_var'], 'host_var_from_inventory_host1')
+
+ # next we test with group_vars/ files loaded
+ fake_loader.push("/etc/ansible/group_vars/all", """
+ group_var_all: group_var_all_from_group_vars_all
+ """)
+ fake_loader.push("/etc/ansible/group_vars/group1", """
+ group_var: group_var_from_group_vars_group1
+ """)
+ fake_loader.push("/etc/ansible/group_vars/group3", """
+ # this is a dummy, which should not be used anywhere
+ group_var: group_var_from_group_vars_group3
+ """)
+ fake_loader.push("/etc/ansible/host_vars/host1", """
+ host_var: host_var_from_host_vars_host1
+ """)
+ fake_loader.push("group_vars/group1", """
+ playbook_group_var: playbook_group_var
+ """)
+ fake_loader.push("host_vars/host1", """
+ playbook_host_var: playbook_host_var
+ """)
+
+ res = v.get_vars(play=play1, host=h1)
+ # self.assertEqual(res['group_var'], 'group_var_from_group_vars_group1')
+ # self.assertEqual(res['group_var_all'], 'group_var_all_from_group_vars_all')
+ # self.assertEqual(res['playbook_group_var'], 'playbook_group_var')
+ # self.assertEqual(res['host_var'], 'host_var_from_host_vars_host1')
+ # self.assertEqual(res['playbook_host_var'], 'playbook_host_var')
+
+ # add in the fact cache
+ v._fact_cache['host1'] = dict(fact_cache_var="fact_cache_var_from_fact_cache")
+
+ res = v.get_vars(play=play1, host=h1)
+ self.assertEqual(res['fact_cache_var'], 'fact_cache_var_from_fact_cache')
+
+ @patch('ansible.playbook.role.definition.unfrackpath', mock_unfrackpath_noop)
+ def test_variable_manager_role_vars_dependencies(self):
+ '''
+ Tests vars from role dependencies with duplicate dependencies.
+ '''
+ mock_inventory = MagicMock()
+
+ fake_loader = DictDataLoader({
+ # role common-role
+ '/etc/ansible/roles/common-role/tasks/main.yml': """
+ - debug: msg="{{role_var}}"
+ """,
+ # We do not need allow_duplicates: yes for this role
+ # because eliminating duplicates is done by the execution
+ # strategy, which we do not test here.
+
+ # role role1
+ '/etc/ansible/roles/role1/vars/main.yml': """
+ role_var: "role_var_from_role1"
+ """,
+ '/etc/ansible/roles/role1/meta/main.yml': """
+ dependencies:
+ - { role: common-role }
+ """,
+
+ # role role2
+ '/etc/ansible/roles/role2/vars/main.yml': """
+ role_var: "role_var_from_role2"
+ """,
+ '/etc/ansible/roles/role2/meta/main.yml': """
+ dependencies:
+ - { role: common-role }
+ """,
+ })
+
+ v = VariableManager(loader=fake_loader, inventory=mock_inventory)
+
+ play1 = Play.load(dict(
+ hosts=['all'],
+ roles=['role1', 'role2'],
+ ), loader=fake_loader, variable_manager=v)
+
+ # The task defined by common-role exists twice because role1
+ # and role2 depend on common-role. Check that the tasks see
+ # different values of role_var.
+ blocks = play1.compile()
+ task = blocks[1].block[0]
+ res = v.get_vars(play=play1, task=task)
+ self.assertEqual(res['role_var'], 'role_var_from_role1')
+
+ task = blocks[2].block[0]
+ res = v.get_vars(play=play1, task=task)
+ self.assertEqual(res['role_var'], 'role_var_from_role2')