diff options
author | Chad Smith <chad.smith@canonical.com> | 2021-12-03 15:44:06 -0700 |
---|---|---|
committer | git-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com> | 2021-12-06 17:15:09 +0000 |
commit | 2fe7d2a5227c55b79182cd1eac831a63e6d7006b (patch) | |
tree | 5dea8a911dca291b74d94b271beaece84775fb1c | |
parent | 5552b6be6680af032bcf1fc02d4af96736c741b9 (diff) | |
download | cloud-init-git-2fe7d2a5227c55b79182cd1eac831a63e6d7006b.tar.gz |
21.4-25-g039c40f9-0ubuntu1~22.04.1 (patches unapplied)
Imported using git-ubuntu import.
-rw-r--r-- | .github/PULL_REQUEST_TEMPLATE.md | 2 | ||||
-rw-r--r-- | CONTRIBUTING.rst (renamed from HACKING.rst) | 58 | ||||
-rw-r--r-- | cloudinit/cmd/query.py | 134 | ||||
-rwxr-xr-x | cloudinit/config/cc_ssh_authkey_fingerprints.py | 2 | ||||
-rw-r--r-- | cloudinit/config/tests/test_mounts.py | 61 | ||||
-rw-r--r-- | cloudinit/config/tests/test_resolv_conf.py | 92 | ||||
-rwxr-xr-x | cloudinit/distros/__init__.py | 7 | ||||
-rw-r--r-- | cloudinit/distros/alpine.py | 3 | ||||
-rw-r--r-- | cloudinit/distros/debian.py | 90 | ||||
-rw-r--r-- | cloudinit/handlers/jinja_template.py | 48 | ||||
-rw-r--r-- | cloudinit/net/activators.py | 6 | ||||
-rw-r--r-- | cloudinit/net/networkd.py | 17 | ||||
-rwxr-xr-x | cloudinit/sources/DataSourceAzure.py | 6 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceGCE.py | 25 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceLXD.py | 101 | ||||
-rw-r--r-- | cloudinit/sources/DataSourceVultr.py | 4 | ||||
-rw-r--r-- | cloudinit/sources/helpers/vmware/imc/config_nic.py | 2 | ||||
-rw-r--r-- | cloudinit/sources/helpers/vultr.py | 52 | ||||
-rw-r--r-- | cloudinit/sources/tests/test_lxd.py | 185 | ||||
-rw-r--r-- | cloudinit/tests/test_gpg.py | 55 | ||||
-rw-r--r-- | cloudinit/tests/test_util.py | 1149 | ||||
-rw-r--r-- | cloudinit/util.py | 41 | ||||
-rw-r--r-- | conftest.py | 18 | ||||
-rw-r--r-- | debian/changelog | 39 | ||||
-rw-r--r-- | doc/examples/cloud-config-datasources.txt | 1 | ||||
-rw-r--r-- | doc/rtd/index.rst | 2 | ||||
-rw-r--r-- | doc/rtd/topics/contributing.rst | 2 | ||||
-rw-r--r-- | doc/rtd/topics/hacking.rst | 2 | ||||
-rw-r--r-- | doc/rtd/topics/instancedata.rst | 16 | ||||
-rw-r--r-- | doc/rtd/topics/testing.rst | 13 | ||||
-rwxr-xr-x | setup.py | 2 | ||||
-rw-r--r-- | tests/integration_tests/bugs/test_gh868.py | 6 | ||||
-rw-r--r-- | tests/integration_tests/conftest.py | 10 | ||||
-rw-r--r-- | tests/integration_tests/datasources/test_lxd_discovery.py | 39 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_apt.py | 52 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_combined.py | 148 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_command_output.py | 1 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_growpart.py | 62 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_jinja_templating.py | 12 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_keys_to_console.py | 43 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_ntp_servers.py | 3 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_puppet.py | 39 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_seed_random_data.py | 30 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_snap.py | 30 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py | 5 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_ssh_import_id.py | 40 | ||||
-rw-r--r-- | tests/integration_tests/modules/test_timezone.py | 25 | ||||
-rw-r--r-- | tests/integration_tests/util.py | 9 | ||||
-rw-r--r-- | tests/unittests/analyze/test_boot.py (renamed from cloudinit/analyze/tests/test_boot.py) | 15 | ||||
-rw-r--r-- | tests/unittests/analyze/test_dump.py (renamed from cloudinit/analyze/tests/test_dump.py) | 2 | ||||
-rw-r--r-- | tests/unittests/cloudinit/__init__py (renamed from cloudinit/cmd/devel/tests/__init__.py) | 0 | ||||
-rw-r--r-- | tests/unittests/cmd/__init__.py (renamed from cloudinit/cmd/tests/__init__.py) | 0 | ||||
-rw-r--r-- | tests/unittests/cmd/devel/__init__.py (renamed from cloudinit/distros/tests/__init__.py) | 0 | ||||
-rw-r--r-- | tests/unittests/cmd/devel/test_logs.py (renamed from cloudinit/cmd/devel/tests/test_logs.py) | 2 | ||||
-rw-r--r-- | tests/unittests/cmd/devel/test_render.py (renamed from cloudinit/cmd/devel/tests/test_render.py) | 2 | ||||
-rw-r--r-- | tests/unittests/cmd/test_clean.py (renamed from cloudinit/cmd/tests/test_clean.py) | 2 | ||||
-rw-r--r-- | tests/unittests/cmd/test_cloud_id.py (renamed from cloudinit/cmd/tests/test_cloud_id.py) | 2 | ||||
-rw-r--r-- | tests/unittests/cmd/test_main.py (renamed from cloudinit/cmd/tests/test_main.py) | 2 | ||||
-rw-r--r-- | tests/unittests/cmd/test_query.py (renamed from cloudinit/cmd/tests/test_query.py) | 73 | ||||
-rw-r--r-- | tests/unittests/cmd/test_status.py (renamed from cloudinit/cmd/tests/test_status.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/__init__.py (renamed from cloudinit/net/tests/__init__.py) | 0 | ||||
-rw-r--r-- | tests/unittests/config/test_apt_conf_v1.py (renamed from tests/unittests/test_handler/test_handler_apt_conf_v1.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_apt_configure_sources_list_v1.py (renamed from tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_apt_configure_sources_list_v3.py (renamed from tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_apt_key.py (renamed from tests/unittests/test_handler/test_handler_apt_key.py) | 0 | ||||
-rw-r--r-- | tests/unittests/config/test_apt_source_v1.py (renamed from tests/unittests/test_handler/test_handler_apt_source_v1.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_apt_source_v3.py (renamed from tests/unittests/test_handler/test_handler_apt_source_v3.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_apk_configure.py (renamed from tests/unittests/test_handler/test_handler_apk_configure.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_apt_pipelining.py (renamed from cloudinit/config/tests/test_apt_pipelining.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_bootcmd.py (renamed from tests/unittests/test_handler/test_handler_bootcmd.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_ca_certs.py (renamed from tests/unittests/test_handler/test_handler_ca_certs.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_chef.py (renamed from tests/unittests/test_handler/test_handler_chef.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_debug.py (renamed from tests/unittests/test_handler/test_handler_debug.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_disable_ec2_metadata.py (renamed from cloudinit/config/tests/test_disable_ec2_metadata.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_disk_setup.py (renamed from tests/unittests/test_handler/test_handler_disk_setup.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_final_message.py (renamed from cloudinit/config/tests/test_final_message.py) | 0 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_growpart.py (renamed from tests/unittests/test_handler/test_handler_growpart.py) | 71 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_grub_dpkg.py (renamed from cloudinit/config/tests/test_grub_dpkg.py) | 0 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_install_hotplug.py (renamed from tests/unittests/test_handler/test_handler_install_hotplug.py) | 0 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_keys_to_console.py (renamed from cloudinit/config/tests/test_keys_to_console.py) | 0 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_landscape.py (renamed from tests/unittests/test_handler/test_handler_landscape.py) | 6 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_locale.py (renamed from tests/unittests/test_handler/test_handler_locale.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_lxd.py (renamed from tests/unittests/test_handler/test_handler_lxd.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_mcollective.py (renamed from tests/unittests/test_handler/test_handler_mcollective.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_mounts.py (renamed from tests/unittests/test_handler/test_handler_mounts.py) | 57 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_ntp.py (renamed from tests/unittests/test_handler/test_handler_ntp.py) | 29 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_power_state_change.py (renamed from tests/unittests/test_handler/test_handler_power_state.py) | 4 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_puppet.py (renamed from tests/unittests/test_handler/test_handler_puppet.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_refresh_rmc_and_interface.py (renamed from tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py) | 4 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_resizefs.py (renamed from tests/unittests/test_handler/test_handler_resizefs.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_resolv_conf.py (renamed from tests/unittests/test_handler/test_handler_resolv_conf.py) | 106 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_rh_subscription.py (renamed from tests/unittests/test_rh_subscription.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_rsyslog.py (renamed from tests/unittests/test_handler/test_handler_rsyslog.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_runcmd.py (renamed from tests/unittests/test_handler/test_handler_runcmd.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_seed_random.py (renamed from tests/unittests/test_handler/test_handler_seed_random.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_set_hostname.py (renamed from tests/unittests/test_handler/test_handler_set_hostname.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_set_passwords.py (renamed from cloudinit/config/tests/test_set_passwords.py) | 30 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_snap.py (renamed from cloudinit/config/tests/test_snap.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_spacewalk.py (renamed from tests/unittests/test_handler/test_handler_spacewalk.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_ssh.py (renamed from cloudinit/config/tests/test_ssh.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_timezone.py (renamed from tests/unittests/test_handler/test_handler_timezone.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_ubuntu_advantage.py (renamed from cloudinit/config/tests/test_ubuntu_advantage.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_ubuntu_drivers.py (renamed from cloudinit/config/tests/test_ubuntu_drivers.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_update_etc_hosts.py (renamed from tests/unittests/test_handler/test_handler_etc_hosts.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_users_groups.py (renamed from cloudinit/config/tests/test_users_groups.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_write_files.py (renamed from tests/unittests/test_handler/test_handler_write_files.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_write_files_deferred.py (renamed from tests/unittests/test_handler/test_handler_write_files_deferred.py) | 4 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_yum_add_repo.py (renamed from tests/unittests/test_handler/test_handler_yum_add_repo.py) | 2 | ||||
-rw-r--r-- | tests/unittests/config/test_cc_zypper_add_repo.py (renamed from tests/unittests/test_handler/test_handler_zypper_add_repo.py) | 4 | ||||
-rw-r--r-- | tests/unittests/config/test_schema.py (renamed from tests/unittests/test_handler/test_schema.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/__init__.py (renamed from tests/unittests/test_distros/__init__.py) | 0 | ||||
-rw-r--r-- | tests/unittests/distros/test_arch.py (renamed from tests/unittests/test_distros/test_arch.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/test_bsd_utils.py (renamed from tests/unittests/test_distros/test_bsd_utils.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/test_create_users.py (renamed from tests/unittests/test_distros/test_create_users.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/test_debian.py (renamed from tests/unittests/test_distros/test_debian.py) | 80 | ||||
-rw-r--r-- | tests/unittests/distros/test_dragonflybsd.py (renamed from tests/unittests/test_distros/test_dragonflybsd.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/test_freebsd.py (renamed from tests/unittests/test_distros/test_freebsd.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/test_generic.py (renamed from tests/unittests/test_distros/test_generic.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/test_gentoo.py (renamed from tests/unittests/test_distros/test_gentoo.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/test_hostname.py (renamed from tests/unittests/test_distros/test_hostname.py) | 0 | ||||
-rw-r--r-- | tests/unittests/distros/test_hosts.py (renamed from tests/unittests/test_distros/test_hosts.py) | 0 | ||||
-rw-r--r-- | tests/unittests/distros/test_init.py (renamed from cloudinit/distros/tests/test_init.py) | 0 | ||||
-rw-r--r-- | tests/unittests/distros/test_manage_service.py (renamed from tests/unittests/test_distros/test_manage_service.py) | 12 | ||||
-rw-r--r-- | tests/unittests/distros/test_netbsd.py (renamed from tests/unittests/test_distros/test_netbsd.py) | 0 | ||||
-rw-r--r-- | tests/unittests/distros/test_netconfig.py (renamed from tests/unittests/test_distros/test_netconfig.py) | 9 | ||||
-rw-r--r-- | tests/unittests/distros/test_networking.py (renamed from cloudinit/distros/tests/test_networking.py) | 0 | ||||
-rw-r--r-- | tests/unittests/distros/test_opensuse.py (renamed from tests/unittests/test_distros/test_opensuse.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/test_photon.py (renamed from tests/unittests/test_distros/test_photon.py) | 4 | ||||
-rw-r--r-- | tests/unittests/distros/test_resolv.py (renamed from tests/unittests/test_distros/test_resolv.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/test_sles.py (renamed from tests/unittests/test_distros/test_sles.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/test_sysconfig.py (renamed from tests/unittests/test_distros/test_sysconfig.py) | 2 | ||||
-rw-r--r-- | tests/unittests/distros/test_user_data_normalize.py (renamed from tests/unittests/test_distros/test_user_data_normalize.py) | 4 | ||||
-rw-r--r-- | tests/unittests/filters/__init__.py (renamed from cloudinit/sources/tests/__init__.py) | 0 | ||||
-rw-r--r-- | tests/unittests/filters/test_launch_index.py (renamed from tests/unittests/test_filters/test_launch_index.py) | 2 | ||||
-rw-r--r-- | tests/unittests/helpers.py (renamed from cloudinit/tests/helpers.py) | 0 | ||||
-rw-r--r-- | tests/unittests/net/__init__.py (renamed from cloudinit/tests/__init__.py) | 0 | ||||
-rw-r--r-- | tests/unittests/net/test_dhcp.py (renamed from cloudinit/net/tests/test_dhcp.py) | 2 | ||||
-rw-r--r-- | tests/unittests/net/test_init.py (renamed from cloudinit/net/tests/test_init.py) | 2 | ||||
-rw-r--r-- | tests/unittests/net/test_network_state.py (renamed from cloudinit/net/tests/test_network_state.py) | 2 | ||||
-rw-r--r-- | tests/unittests/net/test_networkd.py | 64 | ||||
-rw-r--r-- | tests/unittests/runs/__init__.py (renamed from tests/unittests/test_datasource/__init__.py) | 0 | ||||
-rw-r--r-- | tests/unittests/runs/test_merge_run.py (renamed from tests/unittests/test_runs/test_merge_run.py) | 2 | ||||
-rw-r--r-- | tests/unittests/runs/test_simple_run.py (renamed from tests/unittests/test_runs/test_simple_run.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/__init__.py (renamed from tests/unittests/test_filters/__init__.py) | 0 | ||||
-rw-r--r-- | tests/unittests/sources/helpers/test_netlink.py (renamed from cloudinit/sources/helpers/tests/test_netlink.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/helpers/test_openstack.py (renamed from cloudinit/sources/helpers/tests/test_openstack.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_aliyun.py (renamed from tests/unittests/test_datasource/test_aliyun.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_altcloud.py (renamed from tests/unittests/test_datasource/test_altcloud.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_azure.py (renamed from tests/unittests/test_datasource/test_azure.py) | 61 | ||||
-rw-r--r-- | tests/unittests/sources/test_azure_helper.py (renamed from tests/unittests/test_datasource/test_azure_helper.py) | 6 | ||||
-rw-r--r-- | tests/unittests/sources/test_cloudsigma.py (renamed from tests/unittests/test_datasource/test_cloudsigma.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_cloudstack.py (renamed from tests/unittests/test_datasource/test_cloudstack.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_common.py (renamed from tests/unittests/test_datasource/test_common.py) | 3 | ||||
-rw-r--r-- | tests/unittests/sources/test_configdrive.py (renamed from tests/unittests/test_datasource/test_configdrive.py) | 11 | ||||
-rw-r--r-- | tests/unittests/sources/test_digitalocean.py (renamed from tests/unittests/test_datasource/test_digitalocean.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_ec2.py (renamed from tests/unittests/test_datasource/test_ec2.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_exoscale.py (renamed from tests/unittests/test_datasource/test_exoscale.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_gce.py (renamed from tests/unittests/test_datasource/test_gce.py) | 26 | ||||
-rw-r--r-- | tests/unittests/sources/test_hetzner.py (renamed from tests/unittests/test_datasource/test_hetzner.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_ibmcloud.py (renamed from tests/unittests/test_datasource/test_ibmcloud.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_init.py (renamed from cloudinit/sources/tests/test_init.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_lxd.py | 376 | ||||
-rw-r--r-- | tests/unittests/sources/test_maas.py (renamed from tests/unittests/test_datasource/test_maas.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_nocloud.py (renamed from tests/unittests/test_datasource/test_nocloud.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_opennebula.py (renamed from tests/unittests/test_datasource/test_opennebula.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_openstack.py (renamed from tests/unittests/test_datasource/test_openstack.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_oracle.py (renamed from cloudinit/sources/tests/test_oracle.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_ovf.py (renamed from tests/unittests/test_datasource/test_ovf.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_rbx.py (renamed from tests/unittests/test_datasource/test_rbx.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_scaleway.py (renamed from tests/unittests/test_datasource/test_scaleway.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_smartos.py (renamed from tests/unittests/test_datasource/test_smartos.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_upcloud.py (renamed from tests/unittests/test_datasource/test_upcloud.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_vmware.py (renamed from tests/unittests/test_datasource/test_vmware.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/test_vultr.py (renamed from tests/unittests/test_datasource/test_vultr.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/vmware/__init__.py (renamed from tests/unittests/test_handler/__init__.py) | 0 | ||||
-rw-r--r-- | tests/unittests/sources/vmware/test_custom_script.py (renamed from tests/unittests/test_vmware/test_custom_script.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/vmware/test_guestcust_util.py (renamed from tests/unittests/test_vmware/test_guestcust_util.py) | 2 | ||||
-rw-r--r-- | tests/unittests/sources/vmware/test_vmware_config_file.py (renamed from tests/unittests/test_vmware_config_file.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test__init__.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_atomic_helper.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_builtin_handlers.py | 70 | ||||
-rw-r--r-- | tests/unittests/test_cli.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_conftest.py (renamed from cloudinit/tests/test_conftest.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test_cs_util.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_data.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_dhclient_hook.py (renamed from cloudinit/tests/test_dhclient_hook.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test_dmi.py (renamed from cloudinit/tests/test_dmi.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test_ds_identify.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_ec2_util.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_event.py (renamed from cloudinit/tests/test_event.py) | 0 | ||||
-rw-r--r-- | tests/unittests/test_features.py (renamed from cloudinit/tests/test_features.py) | 0 | ||||
-rw-r--r-- | tests/unittests/test_gpg.py | 49 | ||||
-rw-r--r-- | tests/unittests/test_helpers.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_log.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_merging.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_net.py | 27 | ||||
-rw-r--r-- | tests/unittests/test_net_activators.py | 5 | ||||
-rw-r--r-- | tests/unittests/test_net_freebsd.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_netinfo.py (renamed from cloudinit/tests/test_netinfo.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test_pathprefix2dict.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_persistence.py (renamed from cloudinit/tests/test_persistence.py) | 0 | ||||
-rw-r--r-- | tests/unittests/test_registry.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_reporting.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_reporting_hyperv.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_runs/__init__.py | 0 | ||||
-rw-r--r-- | tests/unittests/test_simpletable.py (renamed from cloudinit/tests/test_simpletable.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test_sshutil.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_stages.py (renamed from cloudinit/tests/test_stages.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test_subp.py (renamed from cloudinit/tests/test_subp.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test_temp_utils.py (renamed from cloudinit/tests/test_temp_utils.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test_templating.py | 2 | ||||
-rw-r--r-- | tests/unittests/test_upgrade.py (renamed from cloudinit/tests/test_upgrade.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test_url_helper.py (renamed from cloudinit/tests/test_url_helper.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test_util.py | 1660 | ||||
-rw-r--r-- | tests/unittests/test_version.py (renamed from cloudinit/tests/test_version.py) | 2 | ||||
-rw-r--r-- | tests/unittests/test_vmware/__init__.py | 0 | ||||
-rw-r--r-- | tests/unittests/util.py | 8 | ||||
-rw-r--r-- | tools/.github-cla-signers | 1 | ||||
-rw-r--r-- | tox.ini | 10 |
219 files changed, 3582 insertions, 2322 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0aa97dd4..017e82e4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -27,6 +27,6 @@ setup, and teardown. Scripts used may be attached directly to this PR. --> ## Checklist: <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - - [ ] My code follows the process laid out in [the documentation](https://cloudinit.readthedocs.io/en/latest/topics/hacking.html) + - [ ] My code follows the process laid out in [the documentation](https://cloudinit.readthedocs.io/en/latest/topics/contributing.html) - [ ] I have updated or added any unit tests accordingly - [ ] I have updated or added any documentation accordingly diff --git a/HACKING.rst b/CONTRIBUTING.rst index 6b7dae5a..06b31497 100644 --- a/HACKING.rst +++ b/CONTRIBUTING.rst @@ -1,6 +1,5 @@ -********************* -Hacking on cloud-init -********************* +Contributing to cloud-init +************************** This document describes how to contribute changes to cloud-init. It assumes you have a `GitHub`_ account, and refers to your GitHub user @@ -9,6 +8,26 @@ as ``GH_USER`` throughout. Submitting your first pull request ================================== +Summary +------- + +Before any pull request can be accepted, you must do the following: + +* Sign the Canonical `contributor license agreement`_ +* Add yourself (alphabetically) to the in-repository list that we use + to track CLA signatures: + `tools/.github-cla-signers`_ +* Add or update any `unit tests`_ accordingly +* Add or update any `integration tests`_ (if applicable) +* Ensure unit tests and linting pass using `tox`_ +* Submit a PR against the `main` branch of the `cloud-init` repository + +.. _unit tests: https://cloudinit.readthedocs.io/en/latest/topics/testing.html +.. _integration tests: https://cloudinit.readthedocs.io/en/latest/topics/integration_tests.html + +The detailed instructions +------------------------- + Follow these steps to submit your first pull request to cloud-init: * To contribute to cloud-init, you must sign the Canonical `contributor @@ -78,7 +97,6 @@ Follow these steps to submit your first pull request to cloud-init: .. _repository: https://github.com/canonical/cloud-init .. _contributor license agreement: https://ubuntu.com/legal/contributors .. _contributor-agreement-canonical: https://launchpad.net/%7Econtributor-agreement-canonical/+members -.. _tools/.github-cla-signers: https://github.com/canonical/cloud-init/blob/main/tools/.github-cla-signers .. _PR #344: https://github.com/canonical/cloud-init/pull/344 .. _PR #345: https://github.com/canonical/cloud-init/pull/345 @@ -123,7 +141,7 @@ Do these things for each feature or bug git push -u GH_USER my-topic-branch -* Use your browser to create a merge request: +* Use your browser to create a pull request: - Open the branch on GitHub @@ -142,20 +160,23 @@ Do these things for each feature or bug as footers with syntax as shown here. The commit message should be one summary line of less than - 74 characters followed by a blank line, and then one or more - paragraphs describing the change and why it was needed. + 70 characters followed by a blank line, and then one or more + paragraphs wrapped at 72 characters describing the change and why + it was needed. This is the message that will be used on the commit when it - is sqaushed and merged into trunk. + is sqaushed and merged into main. If there is a related launchpad + bug, specify it at the bottom of the commit message. - LP: #1 + LP: #NNNNNNN (replace with the appropriate bug reference or remove + this line entirely if there is no associated bug) Note that the project continues to use LP: #NNNNN format for closing launchpad bugs rather than GitHub Issues. - Click 'Create Pull Request` -Then, someone in the `Ubuntu Server`_ team will review your changes and +Then, a cloud-init committer will review your changes and follow up in the pull request. Look at the `Code Review Process`_ doc to understand the following steps. @@ -163,7 +184,6 @@ Feel free to ping and/or join ``#cloud-init`` on Libera irc if you have any questions. .. _tox: https://tox.readthedocs.io/en/latest/ -.. _Ubuntu Server: https://github.com/orgs/canonical/teams/ubuntu-server .. _Code Review Process: https://cloudinit.readthedocs.io/en/latest/topics/code_review.html Design @@ -188,18 +208,9 @@ Type Annotations ---------------- The cloud-init codebase uses Python's annotation support for storing -type annotations in the style specified by `PEP-484`_. Their use in -the codebase is encouraged but with one important caveat: types from -the ``typing`` module cannot be used. - -cloud-init still supports Python 3.4, which doesn't have the ``typing`` -module in the stdlib. This means that the use of any types from the -``typing`` module in the codebase would require installation of an -additional Python module on platforms using Python 3.4. As such -platforms are generally in maintenance mode, the introduction of a new -dependency may act as a break in compatibility in practical terms. - -Similarly, only function annotations are appropriate for use, as the +type annotations in the style specified by `PEP-484`_. Their use in +the codebase is encouraged but with one important caveat: only +function annotations or comment annotations are supported, as the variable annotations specified in `PEP-526`_ were introduced in Python 3.6. @@ -517,6 +528,7 @@ References * `PR #363`_, the discussion which prompted finally starting this refactor (and where a lot of the above details were hashed out) +.. _tools/.github-cla-signers: https://github.com/canonical/cloud-init/blob/main/tools/.github-cla-signers .. _get_interfaces_by_mac: https://github.com/canonical/cloud-init/blob/961239749106daead88da483e7319e9268c67cde/cloudinit/net/__init__.py#L810-L818 .. _Mina Galić's email the the cloud-init ML in 2018: https://lists.launchpad.net/cloud-init/msg00185.html .. _Mina Galić's email to the cloud-init ML in 2019: https://lists.launchpad.net/cloud-init/msg00237.html diff --git a/cloudinit/cmd/query.py b/cloudinit/cmd/query.py index 07db9552..e53cd855 100644 --- a/cloudinit/cmd/query.py +++ b/cloudinit/cmd/query.py @@ -19,7 +19,10 @@ import os import sys from cloudinit.handlers.jinja_template import ( - convert_jinja_instance_data, render_jinja_payload) + convert_jinja_instance_data, + get_jinja_variable_alias, + render_jinja_payload +) from cloudinit.cmd.devel import addLogHandlerCLI, read_cfg_paths from cloudinit import log from cloudinit.sources import ( @@ -93,22 +96,24 @@ def load_userdata(ud_file_path): return util.decomp_gzip(bdata, quiet=False, decode=True) -def handle_args(name, args): - """Handle calls to 'cloud-init query' as a subcommand.""" - paths = None - addLogHandlerCLI(LOG, log.DEBUG if args.debug else log.WARNING) - if not any([args.list_keys, args.varname, args.format, args.dump_all]): - LOG.error( - 'Expected one of the options: --all, --format,' - ' --list-keys or varname') - get_parser().print_help() - return 1 +def _read_instance_data(instance_data, user_data, vendor_data) -> dict: + """Return a dict of merged instance-data, vendordata and userdata. + The dict will contain supplemental userdata and vendordata keys sourced + from default user-data and vendor-data files. + + Non-root users will have redacted INSTANCE_JSON_FILE content and redacted + vendordata and userdata values. + + :raise: IOError/OSError on absence of instance-data.json file or invalid + access perms. + """ + paths = None uid = os.getuid() - if not all([args.instance_data, args.user_data, args.vendor_data]): + if not all([instance_data, user_data, vendor_data]): paths = read_cfg_paths() - if args.instance_data: - instance_data_fn = args.instance_data + if instance_data: + instance_data_fn = instance_data else: redacted_data_fn = os.path.join(paths.run_dir, INSTANCE_JSON_FILE) if uid == 0: @@ -124,12 +129,12 @@ def handle_args(name, args): instance_data_fn = redacted_data_fn else: instance_data_fn = redacted_data_fn - if args.user_data: - user_data_fn = args.user_data + if user_data: + user_data_fn = user_data else: user_data_fn = os.path.join(paths.instance_link, 'user-data.txt') - if args.vendor_data: - vendor_data_fn = args.vendor_data + if vendor_data: + vendor_data_fn = vendor_data else: vendor_data_fn = os.path.join(paths.instance_link, 'vendor-data.txt') @@ -140,7 +145,7 @@ def handle_args(name, args): LOG.error("No read permission on '%s'. Try sudo", instance_data_fn) else: LOG.error('Missing instance-data file: %s', instance_data_fn) - return 1 + raise instance_data = util.load_json(instance_json) if uid != 0: @@ -151,6 +156,65 @@ def handle_args(name, args): else: instance_data['userdata'] = load_userdata(user_data_fn) instance_data['vendordata'] = load_userdata(vendor_data_fn) + return instance_data + + +def _find_instance_data_leaf_by_varname_path( + jinja_vars_without_aliases: dict, jinja_vars_with_aliases: dict, + varname: str, list_keys: bool +): + """Return the value of the dot-delimited varname path in instance-data + + Split a dot-delimited jinja variable name path into components, walk the + path components into the instance_data and look up a matching jinja + variable name or cloud-init's underscore-delimited key aliases. + + :raises: ValueError when varname represents an invalid key name or path or + if list-keys is provided by varname isn't a dict object. + """ + walked_key_path = "" + response = jinja_vars_without_aliases + for key_path_part in varname.split('.'): + try: + # Walk key path using complete aliases dict, yet response + # should only contain jinja_without_aliases + jinja_vars_with_aliases = jinja_vars_with_aliases[key_path_part] + except KeyError as e: + if walked_key_path: + msg = "instance-data '{key_path}' has no '{leaf}'".format( + leaf=key_path_part, key_path=walked_key_path + ) + else: + msg = "Undefined instance-data key '{}'".format(varname) + raise ValueError(msg) from e + if key_path_part in response: + response = response[key_path_part] + else: # We are an underscore_delimited key alias + for key in response: + if get_jinja_variable_alias(key) == key_path_part: + response = response[key] + break + if walked_key_path: + walked_key_path += "." + walked_key_path += key_path_part + return response + + +def handle_args(name, args): + """Handle calls to 'cloud-init query' as a subcommand.""" + addLogHandlerCLI(LOG, log.DEBUG if args.debug else log.WARNING) + if not any([args.list_keys, args.varname, args.format, args.dump_all]): + LOG.error( + 'Expected one of the options: --all, --format,' + ' --list-keys or varname') + get_parser().print_help() + return 1 + try: + instance_data = _read_instance_data( + args.instance_data, args.user_data, args.vendor_data + ) + except (IOError, OSError): + return 1 if args.format: payload = '## template: jinja\n{fmt}'.format(fmt=args.format) rendered_payload = render_jinja_payload( @@ -162,20 +226,32 @@ def handle_args(name, args): return 0 return 1 + # If not rendering a structured format above, query output will be either: + # - JSON dump of all instance-data/jinja variables + # - JSON dump of a value at an dict path into the instance-data dict. + # - a list of keys for a specific dict path into the instance-data dict. response = convert_jinja_instance_data(instance_data) if args.varname: + jinja_vars_with_aliases = convert_jinja_instance_data( + instance_data, include_key_aliases=True + ) try: - for var in args.varname.split('.'): - response = response[var] - except KeyError: - LOG.error('Undefined instance-data key %s', args.varname) + response = _find_instance_data_leaf_by_varname_path( + jinja_vars_without_aliases=response, + jinja_vars_with_aliases=jinja_vars_with_aliases, + varname=args.varname, + list_keys=args.list_keys + ) + except (KeyError, ValueError) as e: + LOG.error(e) + return 1 + if args.list_keys: + if not isinstance(response, dict): + LOG.error( + "--list-keys provided but '%s' is not a dict", + args.varname + ) return 1 - if args.list_keys: - if not isinstance(response, dict): - LOG.error("--list-keys provided but '%s' is not a dict", var) - return 1 - response = '\n'.join(sorted(response.keys())) - elif args.list_keys: response = '\n'.join(sorted(response.keys())) if not isinstance(response, str): response = util.json_dumps(response) diff --git a/cloudinit/config/cc_ssh_authkey_fingerprints.py b/cloudinit/config/cc_ssh_authkey_fingerprints.py index 05d30ad1..5323522c 100755 --- a/cloudinit/config/cc_ssh_authkey_fingerprints.py +++ b/cloudinit/config/cc_ssh_authkey_fingerprints.py @@ -70,7 +70,7 @@ def _pprint_key_entries(user, key_fn, key_entries, hash_meth='sha256', if not key_entries: message = ("%sno authorized SSH keys fingerprints found for user %s.\n" % (prefix, user)) - util.multi_log(message) + util.multi_log(message, console=True, stderr=False) return tbl_fields = ['Keytype', 'Fingerprint (%s)' % (hash_meth), 'Options', 'Comment'] diff --git a/cloudinit/config/tests/test_mounts.py b/cloudinit/config/tests/test_mounts.py deleted file mode 100644 index 56510fd6..00000000 --- a/cloudinit/config/tests/test_mounts.py +++ /dev/null @@ -1,61 +0,0 @@ -# This file is part of cloud-init. See LICENSE file for license information. -from unittest import mock - -import pytest - -from cloudinit.config.cc_mounts import create_swapfile -from cloudinit.subp import ProcessExecutionError - - -M_PATH = 'cloudinit.config.cc_mounts.' - - -class TestCreateSwapfile: - - @pytest.mark.parametrize('fstype', ('xfs', 'btrfs', 'ext4', 'other')) - @mock.patch(M_PATH + 'util.get_mount_info') - @mock.patch(M_PATH + 'subp.subp') - def test_happy_path(self, m_subp, m_get_mount_info, fstype, tmpdir): - swap_file = tmpdir.join("swap-file") - fname = str(swap_file) - - # Some of the calls to subp.subp should create the swap file; this - # roughly approximates that - m_subp.side_effect = lambda *args, **kwargs: swap_file.write('') - - m_get_mount_info.return_value = (mock.ANY, fstype) - - create_swapfile(fname, '') - assert mock.call(['mkswap', fname]) in m_subp.call_args_list - - @mock.patch(M_PATH + "util.get_mount_info") - @mock.patch(M_PATH + "subp.subp") - def test_fallback_from_fallocate_to_dd( - self, m_subp, m_get_mount_info, caplog, tmpdir - ): - swap_file = tmpdir.join("swap-file") - fname = str(swap_file) - - def subp_side_effect(cmd, *args, **kwargs): - # Mock fallocate failing, to initiate fallback - if cmd[0] == "fallocate": - raise ProcessExecutionError() - - m_subp.side_effect = subp_side_effect - # Use ext4 so both fallocate and dd are valid swap creation methods - m_get_mount_info.return_value = (mock.ANY, "ext4") - - create_swapfile(fname, "") - - cmds = [args[0][0] for args, _kwargs in m_subp.call_args_list] - assert "fallocate" in cmds, "fallocate was not called" - assert "dd" in cmds, "fallocate failure did not fallback to dd" - - assert cmds.index("dd") > cmds.index( - "fallocate" - ), "dd ran before fallocate" - - assert mock.call(["mkswap", fname]) in m_subp.call_args_list - - msg = "fallocate swap creation failed, will attempt with dd" - assert msg in caplog.text diff --git a/cloudinit/config/tests/test_resolv_conf.py b/cloudinit/config/tests/test_resolv_conf.py deleted file mode 100644 index aff110e5..00000000 --- a/cloudinit/config/tests/test_resolv_conf.py +++ /dev/null @@ -1,92 +0,0 @@ -import pytest - -from unittest import mock -from cloudinit.config.cc_resolv_conf import generate_resolv_conf -from tests.unittests.util import TestingDistro - -EXPECTED_HEADER = """\ -# Your system has been configured with 'manage-resolv-conf' set to true. -# As a result, cloud-init has written this file with configuration data -# that it has been provided. Cloud-init, by default, will write this file -# a single time (PER_ONCE). -#\n\n""" - - -class TestGenerateResolvConf: - - dist = TestingDistro() - tmpl_fn = "templates/resolv.conf.tmpl" - - @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") - def test_dist_resolv_conf_fn(self, m_render_to_file): - self.dist.resolve_conf_fn = "/tmp/resolv-test.conf" - generate_resolv_conf(self.tmpl_fn, - mock.MagicMock(), - self.dist.resolve_conf_fn) - - assert [ - mock.call(mock.ANY, self.dist.resolve_conf_fn, mock.ANY) - ] == m_render_to_file.call_args_list - - @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") - def test_target_fname_is_used_if_passed(self, m_render_to_file): - path = "/use/this/path" - generate_resolv_conf(self.tmpl_fn, mock.MagicMock(), path) - - assert [ - mock.call(mock.ANY, path, mock.ANY) - ] == m_render_to_file.call_args_list - - # Patch in templater so we can assert on the actual generated content - @mock.patch("cloudinit.templater.util.write_file") - # Parameterise with the value to be passed to generate_resolv_conf as the - # params parameter, and the expected line after the header as - # expected_extra_line. - @pytest.mark.parametrize( - "params,expected_extra_line", - [ - # No options - ({}, None), - # Just a true flag - ({"options": {"foo": True}}, "options foo"), - # Just a false flag - ({"options": {"foo": False}}, None), - # Just an option - ({"options": {"foo": "some_value"}}, "options foo:some_value"), - # A true flag and an option - ( - {"options": {"foo": "some_value", "bar": True}}, - "options bar foo:some_value", - ), - # Two options - ( - {"options": {"foo": "some_value", "bar": "other_value"}}, - "options bar:other_value foo:some_value", - ), - # Everything - ( - { - "options": { - "foo": "some_value", - "bar": "other_value", - "baz": False, - "spam": True, - } - }, - "options spam bar:other_value foo:some_value", - ), - ], - ) - def test_flags_and_options( - self, m_write_file, params, expected_extra_line - ): - target_fn = "/etc/resolv.conf" - generate_resolv_conf(self.tmpl_fn, params, target_fn) - - expected_content = EXPECTED_HEADER - if expected_extra_line is not None: - # If we have any extra lines, expect a trailing newline - expected_content += "\n".join([expected_extra_line, ""]) - assert [ - mock.call(mock.ANY, expected_content, mode=mock.ANY) - ] == m_write_file.call_args_list diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index cf6aad14..fe44f20e 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -228,7 +228,12 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): # Now try to bring them up if bring_up: LOG.debug('Bringing up newly configured network interfaces') - network_activator = activators.select_activator() + try: + network_activator = activators.select_activator() + except activators.NoActivatorException: + LOG.warning("No network activator found, not bringing up " + "network interfaces") + return True network_activator.bring_up_all_interfaces(network_state) else: LOG.debug("Not bringing up newly configured network interfaces") diff --git a/cloudinit/distros/alpine.py b/cloudinit/distros/alpine.py index 73b68baf..e82965fd 100644 --- a/cloudinit/distros/alpine.py +++ b/cloudinit/distros/alpine.py @@ -128,6 +128,9 @@ class Distro(distros.Distro): if command: cmd.append(command) + if command == 'upgrade': + cmd.extend(["--update-cache", "--available"]) + pkglist = util.expand_package_list('%s-%s', pkgs) cmd.extend(pkglist) diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index f2b4dfc9..f3901470 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -7,8 +7,9 @@ # Author: Joshua Harlow <harlowja@yahoo-inc.com> # # This file is part of cloud-init. See LICENSE file for license information. - +import fcntl import os +import time from cloudinit import distros from cloudinit import helpers @@ -22,6 +23,7 @@ from cloudinit.settings import PER_INSTANCE LOG = logging.getLogger(__name__) +APT_LOCK_WAIT_TIMEOUT = 30 APT_GET_COMMAND = ('apt-get', '--option=Dpkg::Options::=--force-confold', '--option=Dpkg::options::=--force-unsafe-io', '--assume-yes', '--quiet') @@ -41,6 +43,12 @@ NETWORK_FILE_HEADER = """\ NETWORK_CONF_FN = "/etc/network/interfaces.d/50-cloud-init" LOCALE_CONF_FN = "/etc/default/locale" +APT_LOCK_FILES = [ + '/var/lib/dpkg/lock', + '/var/lib/apt/lists/lock', + '/var/cache/apt/archives/lock', +] + class Distro(distros.Distro): hostname_conf_fn = "/etc/hostname" @@ -155,7 +163,78 @@ class Distro(distros.Distro): def set_timezone(self, tz): distros.set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz)) + def _apt_lock_available(self, lock_files=None): + """Determines if another process holds any apt locks. + + If all locks are clear, return True else False. + """ + if lock_files is None: + lock_files = APT_LOCK_FILES + for lock in lock_files: + if not os.path.exists(lock): + # Only wait for lock files that already exist + continue + with open(lock, 'w') as handle: + try: + fcntl.lockf(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError: + return False + return True + + def _wait_for_apt_command( + self, short_cmd, subp_kwargs, timeout=APT_LOCK_WAIT_TIMEOUT + ): + """Wait for apt install to complete. + + short_cmd: Name of command like "upgrade" or "install" + subp_kwargs: kwargs to pass to subp + """ + start_time = time.time() + LOG.debug('Waiting for apt lock') + while time.time() - start_time < timeout: + if not self._apt_lock_available(): + time.sleep(1) + continue + LOG.debug('apt lock available') + try: + # Allow the output of this to flow outwards (not be captured) + log_msg = "apt-%s [%s]" % ( + short_cmd, + ' '.join(subp_kwargs['args']) + ) + return util.log_time( + logfunc=LOG.debug, + msg=log_msg, + func=subp.subp, + kwargs=subp_kwargs, + ) + except subp.ProcessExecutionError: + # Even though we have already waited for the apt lock to be + # available, it is possible that the lock was acquired by + # another process since the check. Since apt doesn't provide + # a meaningful error code to check and checking the error + # text is fragile and subject to internationalization, we + # can instead check the apt lock again. If the apt lock is + # still available, given the length of an average apt + # transaction, it is extremely unlikely that another process + # raced us when we tried to acquire it, so raise the apt + # error received. If the lock is unavailable, just keep waiting + if self._apt_lock_available(): + raise + LOG.debug('Another process holds apt lock. Waiting...') + time.sleep(1) + raise TimeoutError('Could not get apt lock') + def package_command(self, command, args=None, pkgs=None): + """Run the given package command. + + On Debian, this will run apt-get (unless APT_GET_COMMAND is set). + + command: The command to run, like "upgrade" or "install" + args: Arguments passed to apt itself in addition to + any specified in APT_GET_COMMAND + pkgs: Apt packages that the command will apply to + """ if pkgs is None: pkgs = [] @@ -185,11 +264,10 @@ class Distro(distros.Distro): pkglist = util.expand_package_list('%s=%s', pkgs) cmd.extend(pkglist) - # Allow the output of this to flow outwards (ie not be captured) - util.log_time(logfunc=LOG.debug, - msg="apt-%s [%s]" % (command, ' '.join(cmd)), - func=subp.subp, - args=(cmd,), kwargs={'env': e, 'capture': False}) + self._wait_for_apt_command( + short_cmd=command, + subp_kwargs={'args': cmd, 'env': e, 'capture': False} + ) def update_package_sources(self): self._runner.run("update-sources", self.package_command, diff --git a/cloudinit/handlers/jinja_template.py b/cloudinit/handlers/jinja_template.py index 5033abbb..de88a5ea 100644 --- a/cloudinit/handlers/jinja_template.py +++ b/cloudinit/handlers/jinja_template.py @@ -1,14 +1,18 @@ # This file is part of cloud-init. See LICENSE file for license information. +import copy from errno import EACCES import os import re +from typing import Optional try: from jinja2.exceptions import UndefinedError as JUndefinedError + from jinja2.lexer import operator_re except ImportError: # No jinja2 dependency JUndefinedError = Exception + operator_re = re.compile(r'[-.]') from cloudinit import handlers from cloudinit import log as logging @@ -97,7 +101,9 @@ def render_jinja_payload_from_file( def render_jinja_payload(payload, payload_fn, instance_data, debug=False): instance_jinja_vars = convert_jinja_instance_data( instance_data, - decode_paths=instance_data.get('base64-encoded-keys', [])) + decode_paths=instance_data.get('base64-encoded-keys', []), + include_key_aliases=True + ) if debug: LOG.debug('Converted jinja variables\n%s', json_dumps(instance_jinja_vars)) @@ -118,7 +124,30 @@ def render_jinja_payload(payload, payload_fn, instance_data, debug=False): return rendered_payload -def convert_jinja_instance_data(data, prefix='', sep='/', decode_paths=()): +def get_jinja_variable_alias(orig_name: str) -> Optional[str]: + """Return a jinja variable alias, replacing any operators with underscores. + + Provide underscore-delimited key aliases to simplify dot-notation + attribute references for keys which contain operators "." or "-". + This provides for simpler short-hand jinja attribute notation + allowing one to avoid quoting keys which contain operators. + {{ ds.v1_0.config.user_network_config }} instead of + {{ ds['v1.0'].config["user.network-config"] }}. + + :param orig_name: String representing a jinja variable name to scrub/alias. + + :return: A string with any jinja operators replaced if needed. Otherwise, + none if no alias required. + """ + alias_name = re.sub(operator_re, '_', orig_name) + if alias_name != orig_name: + return alias_name + return None + + +def convert_jinja_instance_data( + data, prefix='', sep='/', decode_paths=(), include_key_aliases=False +): """Process instance-data.json dict for use in jinja templates. Replace hyphens with underscores for jinja templates and decode any @@ -127,21 +156,24 @@ def convert_jinja_instance_data(data, prefix='', sep='/', decode_paths=()): result = {} decode_paths = [path.replace('-', '_') for path in decode_paths] for key, value in sorted(data.items()): - if '-' in key: - # Standardize keys for use in #cloud-config/shell templates - key = key.replace('-', '_') key_path = '{0}{1}{2}'.format(prefix, sep, key) if prefix else key if key_path in decode_paths: value = b64d(value) if isinstance(value, dict): result[key] = convert_jinja_instance_data( - value, key_path, sep=sep, decode_paths=decode_paths) - if re.match(r'v\d+', key): + value, key_path, sep=sep, decode_paths=decode_paths, + include_key_aliases=include_key_aliases + ) + if re.match(r'v\d+$', key): # Copy values to top-level aliases for subkey, subvalue in result[key].items(): - result[subkey] = subvalue + result[subkey] = copy.deepcopy(subvalue) else: result[key] = value + if include_key_aliases: + alias_name = get_jinja_variable_alias(key) + if alias_name: + result[alias_name] = copy.deepcopy(result[key]) return result # vi: ts=4 expandtab diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py index 11149548..137338d8 100644 --- a/cloudinit/net/activators.py +++ b/cloudinit/net/activators.py @@ -16,6 +16,10 @@ from cloudinit.net.sysconfig import NM_CFG_FILE LOG = logging.getLogger(__name__) +class NoActivatorException(Exception): + pass + + def _alter_interface(cmd, device_name) -> bool: LOG.debug("Attempting command %s for device %s", cmd, device_name) try: @@ -271,7 +275,7 @@ def select_activator(priority=None, target=None) -> Type[NetworkActivator]: tmsg = "" if target and target != "/": tmsg = " in target=%s" % target - raise RuntimeError( + raise NoActivatorException( "No available network activators found%s. Searched " "through list: %s" % (tmsg, priority)) selected = found[0] diff --git a/cloudinit/net/networkd.py b/cloudinit/net/networkd.py index ee6fd2ad..c97c18f6 100644 --- a/cloudinit/net/networkd.py +++ b/cloudinit/net/networkd.py @@ -41,11 +41,11 @@ class CfgParser: def get_final_conf(self): contents = '' - for k, v in self.conf_dict.items(): + for k, v in sorted(self.conf_dict.items()): if not v: continue contents += '['+k+']\n' - for e in v: + for e in sorted(v): contents += e + '\n' contents += '\n' @@ -242,6 +242,19 @@ class Renderer(renderer.Renderer): name = iface['name'] # network state doesn't give dhcp domain info # using ns.config as a workaround here + + # Check to see if this interface matches against an interface + # from the network state that specified a set-name directive. + # If there is a device with a set-name directive and it has + # set-name value that matches the current name, then update the + # current name to the device's name. That will be the value in + # the ns.config['ethernets'] dict below. + for dev_name, dev_cfg in ns.config['ethernets'].items(): + if 'set-name' in dev_cfg: + if dev_cfg.get('set-name') == name: + name = dev_name + break + self.dhcp_domain(ns.config['ethernets'][name], cfg) ret_dict.update({link: cfg.get_final_conf()}) diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py index 93493fa0..6c1bc085 100755 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -52,8 +52,6 @@ LOG = logging.getLogger(__name__) DS_NAME = 'Azure' DEFAULT_METADATA = {"instance-id": "iid-AZURE-NODE"} -AGENT_START = ['service', 'walinuxagent', 'start'] -AGENT_START_BUILTIN = "__builtin__" BOUNCE_COMMAND_IFUP = [ 'sh', '-xc', "i=$interface; x=0; ifdown $i || x=$?; ifup $i || x=$?; exit $x" @@ -262,7 +260,6 @@ if util.is_FreeBSD(): PLATFORM_ENTROPY_SOURCE = None BUILTIN_DS_CONFIG = { - 'agent_command': AGENT_START_BUILTIN, 'data_dir': AGENT_SEED_DIR, 'set_hostname': True, 'hostname_bounce': { @@ -1525,8 +1522,7 @@ class DataSourceAzure(sources.DataSource): dhclient_lease_file, pubkey_info=pubkey_info) - LOG.debug("negotiating with fabric via agent command %s", - self.ds_cfg['agent_command']) + LOG.debug("negotiating with fabric") try: fabric_data = metadata_func() except Exception as e: diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py index 9f838bd4..b82fa410 100644 --- a/cloudinit/sources/DataSourceGCE.py +++ b/cloudinit/sources/DataSourceGCE.py @@ -4,6 +4,7 @@ import datetime import json +from contextlib import suppress as noop from base64 import b64decode @@ -13,6 +14,7 @@ from cloudinit import log as logging from cloudinit import sources from cloudinit import url_helper from cloudinit import util +from cloudinit.net.dhcp import EphemeralDHCPv4 LOG = logging.getLogger(__name__) @@ -58,6 +60,7 @@ class GoogleMetadataFetcher(object): class DataSourceGCE(sources.DataSource): dsname = 'GCE' + perform_dhcp_setup = False def __init__(self, sys_cfg, distro, paths): sources.DataSource.__init__(self, sys_cfg, distro, paths) @@ -73,10 +76,19 @@ class DataSourceGCE(sources.DataSource): def _get_data(self): url_params = self.get_url_params() - ret = util.log_time( - LOG.debug, 'Crawl of GCE metadata service', - read_md, kwargs={'address': self.metadata_address, - 'url_params': url_params}) + network_context = noop() + if self.perform_dhcp_setup: + network_context = EphemeralDHCPv4(self.fallback_interface) + with network_context: + ret = util.log_time( + LOG.debug, + "Crawl of GCE metadata service", + read_md, + kwargs={ + "address": self.metadata_address, + "url_params": url_params, + }, + ) if not ret['success']: if ret['platform_reports_gce']: @@ -117,6 +129,10 @@ class DataSourceGCE(sources.DataSource): return self.availability_zone.rsplit('-', 1)[0] +class DataSourceGCELocal(DataSourceGCE): + perform_dhcp_setup = True + + def _write_host_key_to_guest_attributes(key_type, key_value): url = '%s/%s/%s' % (GUEST_ATTRIBUTES_URL, HOSTKEY_NAMESPACE, key_type) key_value = key_value.encode('utf-8') @@ -272,6 +288,7 @@ def platform_reports_gce(): # Used to match classes to dependencies. datasources = [ + (DataSourceGCELocal, (sources.DEP_FILESYSTEM,)), (DataSourceGCE, (sources.DEP_FILESYSTEM, sources.DEP_NETWORK)), ] diff --git a/cloudinit/sources/DataSourceLXD.py b/cloudinit/sources/DataSourceLXD.py index 732b32ff..469707d2 100644 --- a/cloudinit/sources/DataSourceLXD.py +++ b/cloudinit/sources/DataSourceLXD.py @@ -10,6 +10,7 @@ Notes: * TODO( Hotplug support using websockets API 1.0/events ) """ +from json.decoder import JSONDecodeError import os import requests @@ -41,14 +42,16 @@ LXD_SOCKET_API_VERSION = "1.0" # Config key mappings to alias as top-level instance data keys CONFIG_KEY_ALIASES = { + "cloud-init.user-data": "user-data", + "cloud-init.network-config": "network-config", + "cloud-init.vendor-data": "vendor-data", "user.user-data": "user-data", "user.network-config": "network-config", - "user.network_mode": "network_mode", "user.vendor-data": "vendor-data" } -def generate_fallback_network_config(network_mode: str = "") -> dict: +def generate_fallback_network_config() -> dict: """Return network config V1 dict representing instance network config.""" network_v1 = { "version": 1, @@ -76,12 +79,6 @@ def generate_fallback_network_config(network_mode: str = "") -> dict: network_v1["config"][0]["name"] = "enc9" else: network_v1["config"][0]["name"] = "enp5s0" - if network_mode == "link-local": - network_v1["config"][0]["subnets"][0]["control"] = "manual" - elif network_mode not in ("", "dhcp"): - LOG.warning( - "Ignoring unexpected value user.network_mode: %s", network_mode - ) return network_v1 @@ -193,19 +190,16 @@ class DataSourceLXD(sources.DataSource): self.metadata = _raw_instance_data_to_dict( "meta-data", self._crawled_metadata.get("meta-data") ) - if LXD_SOCKET_API_VERSION in self._crawled_metadata: - config = self._crawled_metadata[LXD_SOCKET_API_VERSION].get( - "config", {} + config = self._crawled_metadata.get("config", {}) + user_metadata = config.get("user.meta-data", {}) + if user_metadata: + user_metadata = _raw_instance_data_to_dict( + "user.meta-data", user_metadata + ) + if not isinstance(self.metadata, dict): + self.metadata = util.mergemanydict( + [util.load_yaml(self.metadata), user_metadata] ) - user_metadata = config.get("user.meta-data", {}) - if user_metadata: - user_metadata = _raw_instance_data_to_dict( - "user.meta-data", user_metadata - ) - if not isinstance(self.metadata, dict): - self.metadata = util.mergemanydict( - [util.load_yaml(self.metadata), user_metadata] - ) if "user-data" in self._crawled_metadata: self.userdata_raw = self._crawled_metadata["user-data"] if "network-config" in self._crawled_metadata: @@ -244,10 +238,7 @@ class DataSourceLXD(sources.DataSource): "network-config" ) else: - network_mode = self._crawled_metadata.get("network_mode", "") - self._network_config = generate_fallback_network_config( - network_mode - ) + self._network_config = generate_fallback_network_config() return self._network_config @@ -294,7 +285,7 @@ def read_metadata( with requests.Session() as session: session.mount(version_url, LXDSocketAdapter()) # Raw meta-data as text - md_route = "{route}/meta-data".format(route=version_url) + md_route = "{route}meta-data".format(route=version_url) response = session.get(md_route) LOG.debug("[GET] [HTTP:%d] %s", response.status_code, md_route) if not response.ok: @@ -302,7 +293,7 @@ def read_metadata( "Invalid HTTP response [{code}] from {route}: {resp}".format( code=response.status_code, route=md_route, - resp=response.txt + resp=response.text ) ) @@ -310,30 +301,66 @@ def read_metadata( if metadata_only: return md # Skip network-data, vendor-data, user-data - config_url = version_url + "config" - # Represent all advertized/available config routes under - # the dict path {LXD_SOCKET_API_VERSION: {config: {...}}. - LOG.debug("[GET] %s", config_url) - config_routes = session.get(config_url).json() - md[LXD_SOCKET_API_VERSION] = { + md = { + "_metadata_api_version": api_version, # Document API version read "config": {}, "meta-data": md["meta-data"] } - for config_route in config_routes: + + config_url = version_url + "config" + # Represent all advertized/available config routes under + # the dict path {LXD_SOCKET_API_VERSION: {config: {...}}. + response = session.get(config_url) + LOG.debug("[GET] [HTTP:%d] %s", response.status_code, config_url) + if not response.ok: + raise sources.InvalidMetaDataException( + "Invalid HTTP response [{code}] from {route}: {resp}".format( + code=response.status_code, + route=config_url, + resp=response.text + ) + ) + try: + config_routes = response.json() + except JSONDecodeError as exc: + raise sources.InvalidMetaDataException( + "Unable to determine cloud-init config from {route}." + " Expected JSON but found: {resp}".format( + route=config_url, + resp=response.text + ) + ) from exc + + # Sorting keys to ensure we always process in alphabetical order. + # cloud-init.* keys will sort before user.* keys which is preferred + # precedence. + for config_route in sorted(config_routes): url = "http://lxd{route}".format(route=config_route) - LOG.debug("[GET] %s", url) response = session.get(url) + LOG.debug("[GET] [HTTP:%d] %s", response.status_code, url) if response.ok: cfg_key = config_route.rpartition("/")[-1] # Leave raw data values/format unchanged to represent it in # instance-data.json for cloud-init query or jinja template # use. - md[LXD_SOCKET_API_VERSION]["config"][cfg_key] = response.text + md["config"][cfg_key] = response.text # Promote common CONFIG_KEY_ALIASES to top-level keys. if cfg_key in CONFIG_KEY_ALIASES: - md[CONFIG_KEY_ALIASES[cfg_key]] = response.text + # Due to sort of config_routes, promote cloud-init.* + # aliases before user.*. This allows user.* keys to act as + # fallback config on old LXD, with new cloud-init images. + if CONFIG_KEY_ALIASES[cfg_key] not in md: + md[CONFIG_KEY_ALIASES[cfg_key]] = response.text + else: + LOG.warning( + "Ignoring LXD config %s in favor of %s value.", + cfg_key, cfg_key.replace("user", "cloud-init", 1) + ) else: - LOG.debug("Skipping %s on invalid response", url) + LOG.debug( + "Skipping %s on [HTTP:%d]:%s", + url, response.status_code, response.text + ) return md diff --git a/cloudinit/sources/DataSourceVultr.py b/cloudinit/sources/DataSourceVultr.py index 68e1ff0b..abeefbc5 100644 --- a/cloudinit/sources/DataSourceVultr.py +++ b/cloudinit/sources/DataSourceVultr.py @@ -16,8 +16,8 @@ LOG = log.getLogger(__name__) BUILTIN_DS_CONFIG = { 'url': 'http://169.254.169.254', 'retries': 30, - 'timeout': 2, - 'wait': 2, + 'timeout': 10, + 'wait': 5, 'user-agent': 'Cloud-Init/%s - OS: %s Variant: %s' % (version.version_string(), util.system_info()['system'], diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py index 9cd2c0c0..f5a0ebe4 100644 --- a/cloudinit/sources/helpers/vmware/imc/config_nic.py +++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py @@ -274,7 +274,7 @@ class NicConfigurator(object): lines = [ "# DO NOT EDIT THIS FILE BY HAND --" " AUTOMATICALLY GENERATED BY cloud-init", - "source /etc/network/interfaces.d/*.cfg", + "source /etc/network/interfaces.d/*", "source-directory /etc/network/interfaces.d", ] diff --git a/cloudinit/sources/helpers/vultr.py b/cloudinit/sources/helpers/vultr.py index 55487ac3..ad347bea 100644 --- a/cloudinit/sources/helpers/vultr.py +++ b/cloudinit/sources/helpers/vultr.py @@ -9,6 +9,8 @@ from cloudinit import url_helper from cloudinit import dmi from cloudinit import util from cloudinit import net +from cloudinit import netinfo +from cloudinit import subp from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError from functools import lru_cache @@ -21,6 +23,9 @@ def get_metadata(url, timeout, retries, sec_between, agent): # Bring up interface try: with EphemeralDHCPv4(connectivity_url_data={"url": url}): + # Set metadata route + set_route() + # Fetch the metadata v1 = read_metadata(url, timeout, retries, sec_between, agent) except (NoDHCPLeaseError) as exc: @@ -30,6 +35,53 @@ def get_metadata(url, timeout, retries, sec_between, agent): return json.loads(v1) +# Set route for metadata +def set_route(): + # Get routes, confirm entry does not exist + routes = netinfo.route_info() + + # If no tools exist and empty dict is returned + if 'ipv4' not in routes: + return + + # We only care about IPv4 + routes = routes['ipv4'] + + # Searchable list + dests = [] + + # Parse each route into a more searchable format + for route in routes: + dests.append(route['destination']) + + gw_present = '100.64.0.0' in dests or '100.64.0.0/10' in dests + dest_present = '169.254.169.254' in dests + + # If not IPv6 only (No link local) + # or the route is already present + if not gw_present or dest_present: + return + + # Set metadata route + if subp.which('ip'): + subp.subp([ + 'ip', + 'route', + 'add', + '169.254.169.254/32', + 'dev', + net.find_fallback_nic() + ]) + elif subp.which('route'): + subp.subp([ + 'route', + 'add', + '-net', + '169.254.169.254/32', + '100.64.0.1' + ]) + + # Read the system information from SMBIOS def get_sysinfo(): return { diff --git a/cloudinit/sources/tests/test_lxd.py b/cloudinit/sources/tests/test_lxd.py deleted file mode 100644 index c2027616..00000000 --- a/cloudinit/sources/tests/test_lxd.py +++ /dev/null @@ -1,185 +0,0 @@ -# This file is part of cloud-init. See LICENSE file for license information. - -from collections import namedtuple -from copy import deepcopy -import stat -from unittest import mock -import yaml - -import pytest - -from cloudinit.sources import DataSourceLXD as lxd, UNSET -DS_PATH = "cloudinit.sources.DataSourceLXD." - - -LStatResponse = namedtuple("lstatresponse", "st_mode") - - -NETWORK_V1 = { - "version": 1, - "config": [ - { - "type": "physical", "name": "eth0", - "subnets": [{"type": "dhcp", "control": "auto"}] - } - ] -} -NETWORK_V1_MANUAL = deepcopy(NETWORK_V1) -NETWORK_V1_MANUAL["config"][0]["subnets"][0]["control"] = "manual" - - -def _add_network_v1_device(devname) -> dict: - """Helper to inject device name into default network v1 config.""" - network_cfg = deepcopy(NETWORK_V1) - network_cfg["config"][0]["name"] = devname - return network_cfg - - -LXD_V1_METADATA = { - "meta-data": "instance-id: my-lxc\nlocal-hostname: my-lxc\n\n", - "network-config": NETWORK_V1, - "user-data": "#cloud-config\npackages: [sl]\n", - "vendor-data": "#cloud-config\nruncmd: ['echo vendor-data']\n", - "1.0": { - "meta-data": "instance-id: my-lxc\nlocal-hostname: my-lxc\n\n", - "config": { - "user.user-data": - "instance-id: my-lxc\nlocal-hostname: my-lxc\n\n", - "user.vendor-data": - "#cloud-config\nruncmd: ['echo vendor-data']\n", - "user.network-config": yaml.safe_dump(NETWORK_V1), - } - } -} - - -@pytest.fixture -def lxd_metadata(): - return LXD_V1_METADATA - - -@pytest.yield_fixture -def lxd_ds(request, paths, lxd_metadata): - """ - Return an instantiated DataSourceLXD. - - This also performs the mocking required for the default test case: - * ``is_platform_viable`` returns True, - * ``read_metadata`` returns ``LXD_V1_METADATA`` - - (This uses the paths fixture for the required helpers.Paths object) - """ - with mock.patch(DS_PATH + "is_platform_viable", return_value=True): - with mock.patch(DS_PATH + "read_metadata", return_value=lxd_metadata): - yield lxd.DataSourceLXD( - sys_cfg={}, distro=mock.Mock(), paths=paths - ) - - -class TestGenerateFallbackNetworkConfig: - - @pytest.mark.parametrize( - "uname_machine,systemd_detect_virt,network_mode,expected", ( - # None for systemd_detect_virt returns None from which - ({}, None, "", NETWORK_V1), - ({}, None, "dhcp", NETWORK_V1), - # invalid network_mode logs warning - ({}, None, "bogus", NETWORK_V1), - ({}, None, "link-local", NETWORK_V1_MANUAL), - ("anything", "lxc\n", "", NETWORK_V1), - # `uname -m` on kvm determines devname - ("x86_64", "kvm\n", "", _add_network_v1_device("enp5s0")), - ("ppc64le", "kvm\n", "", _add_network_v1_device("enp0s5")), - ("s390x", "kvm\n", "", _add_network_v1_device("enc9")) - ) - ) - @mock.patch(DS_PATH + "util.system_info") - @mock.patch(DS_PATH + "subp.subp") - @mock.patch(DS_PATH + "subp.which") - def test_net_v2_based_on_network_mode_virt_type_and_uname_machine( - self, - m_which, - m_subp, - m_system_info, - uname_machine, - systemd_detect_virt, - network_mode, - expected, - caplog - ): - """Return network config v2 based on uname -m, systemd-detect-virt. - - LXC config network_mode of "link-local" will determine whether to set - "activation-mode: manual", leaving the interface down. - """ - if systemd_detect_virt is None: - m_which.return_value = None - m_system_info.return_value = {"uname": ["", "", "", "", uname_machine]} - m_subp.return_value = (systemd_detect_virt, "") - assert expected == lxd.generate_fallback_network_config( - network_mode=network_mode - ) - if systemd_detect_virt is None: - assert 0 == m_subp.call_count - assert 0 == m_system_info.call_count - else: - assert [ - mock.call(["systemd-detect-virt"]) - ] == m_subp.call_args_list - if systemd_detect_virt != "kvm\n": - assert 0 == m_system_info.call_count - else: - assert 1 == m_system_info.call_count - if network_mode not in ("dhcp", "", "link-local"): - assert "Ignoring unexpected value user.network_mode: {}".format( - network_mode - ) in caplog.text - - -class TestDataSourceLXD: - def test_platform_info(self, lxd_ds): - assert "LXD" == lxd_ds.dsname - assert "lxd" == lxd_ds.cloud_name - assert "lxd" == lxd_ds.platform_type - - def test_subplatform(self, lxd_ds): - assert "LXD socket API v. 1.0 (/dev/lxd/sock)" == lxd_ds.subplatform - - def test__get_data(self, lxd_ds): - """get_data calls read_metadata, setting appropiate instance attrs.""" - assert UNSET == lxd_ds._crawled_metadata - assert UNSET == lxd_ds._network_config - assert None is lxd_ds.userdata_raw - assert True is lxd_ds._get_data() - assert LXD_V1_METADATA == lxd_ds._crawled_metadata - # network-config is dumped from YAML - assert NETWORK_V1 == lxd_ds._network_config - # Any user-data and vendor-data are saved as raw - assert LXD_V1_METADATA["user-data"] == lxd_ds.userdata_raw - assert LXD_V1_METADATA["vendor-data"] == lxd_ds.vendordata_raw - - -class TestIsPlatformViable: - @pytest.mark.parametrize( - "exists,lstat_mode,expected", ( - (False, None, False), - (True, stat.S_IFREG, False), - (True, stat.S_IFSOCK, True), - ) - ) - @mock.patch(DS_PATH + "os.lstat") - @mock.patch(DS_PATH + "os.path.exists") - def test_expected_viable( - self, m_exists, m_lstat, exists, lstat_mode, expected - ): - """Return True only when LXD_SOCKET_PATH exists and is a socket.""" - m_exists.return_value = exists - m_lstat.return_value = LStatResponse(lstat_mode) - assert expected is lxd.is_platform_viable() - m_exists.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)]) - if exists: - m_lstat.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)]) - else: - assert 0 == m_lstat.call_count - -# vi: ts=4 expandtab diff --git a/cloudinit/tests/test_gpg.py b/cloudinit/tests/test_gpg.py deleted file mode 100644 index 311dfad6..00000000 --- a/cloudinit/tests/test_gpg.py +++ /dev/null @@ -1,55 +0,0 @@ -# This file is part of cloud-init. See LICENSE file for license information. -"""Test gpg module.""" - -from unittest import mock - -from cloudinit import gpg -from cloudinit import subp -from cloudinit.tests.helpers import CiTestCase - - -@mock.patch("cloudinit.gpg.time.sleep") -@mock.patch("cloudinit.gpg.subp.subp") -class TestReceiveKeys(CiTestCase): - """Test the recv_key method.""" - - def test_retries_on_subp_exc(self, m_subp, m_sleep): - """retry should be done on gpg receive keys failure.""" - retries = (1, 2, 4) - my_exc = subp.ProcessExecutionError( - stdout='', stderr='', exit_code=2, cmd=['mycmd']) - m_subp.side_effect = (my_exc, my_exc, ('', '')) - gpg.recv_key("ABCD", "keyserver.example.com", retries=retries) - self.assertEqual([mock.call(1), mock.call(2)], m_sleep.call_args_list) - - def test_raises_error_after_retries(self, m_subp, m_sleep): - """If the final run fails, error should be raised.""" - naplen = 1 - keyid, keyserver = ("ABCD", "keyserver.example.com") - m_subp.side_effect = subp.ProcessExecutionError( - stdout='', stderr='', exit_code=2, cmd=['mycmd']) - with self.assertRaises(ValueError) as rcm: - gpg.recv_key(keyid, keyserver, retries=(naplen,)) - self.assertIn(keyid, str(rcm.exception)) - self.assertIn(keyserver, str(rcm.exception)) - m_sleep.assert_called_with(naplen) - - def test_no_retries_on_none(self, m_subp, m_sleep): - """retry should not be done if retries is None.""" - m_subp.side_effect = subp.ProcessExecutionError( - stdout='', stderr='', exit_code=2, cmd=['mycmd']) - with self.assertRaises(ValueError): - gpg.recv_key("ABCD", "keyserver.example.com", retries=None) - m_sleep.assert_not_called() - - def test_expected_gpg_command(self, m_subp, m_sleep): - """Verify gpg is called with expected args.""" - key, keyserver = ("DEADBEEF", "keyserver.example.com") - retries = (1, 2, 4) - m_subp.return_value = ('', '') - gpg.recv_key(key, keyserver, retries=retries) - m_subp.assert_called_once_with( - ['gpg', '--no-tty', - '--keyserver=%s' % keyserver, '--recv-keys', key], - capture=True) - m_sleep.assert_not_called() diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py deleted file mode 100644 index ab5eb35c..00000000 --- a/cloudinit/tests/test_util.py +++ /dev/null @@ -1,1149 +0,0 @@ -# This file is part of cloud-init. See LICENSE file for license information. - -"""Tests for cloudinit.util""" - -import base64 -import logging -import json -import platform -import pytest - -import cloudinit.util as util -from cloudinit import subp - -from cloudinit.tests.helpers import CiTestCase, mock -from textwrap import dedent - -LOG = logging.getLogger(__name__) - -MOUNT_INFO = [ - '68 0 8:3 / / ro,relatime shared:1 - btrfs /dev/sda1 ro,attr2,inode64', - '153 68 254:0 / /home rw,relatime shared:101 - xfs /dev/sda2 rw,attr2' -] - -OS_RELEASE_SLES = dedent("""\ - NAME="SLES" - VERSION="12-SP3" - VERSION_ID="12.3" - PRETTY_NAME="SUSE Linux Enterprise Server 12 SP3" - ID="sles" - ANSI_COLOR="0;32" - CPE_NAME="cpe:/o:suse:sles:12:sp3" -""") - -OS_RELEASE_OPENSUSE = dedent("""\ - NAME="openSUSE Leap" - VERSION="42.3" - ID=opensuse - ID_LIKE="suse" - VERSION_ID="42.3" - PRETTY_NAME="openSUSE Leap 42.3" - ANSI_COLOR="0;32" - CPE_NAME="cpe:/o:opensuse:leap:42.3" - BUG_REPORT_URL="https://bugs.opensuse.org" - HOME_URL="https://www.opensuse.org/" -""") - -OS_RELEASE_OPENSUSE_L15 = dedent("""\ - NAME="openSUSE Leap" - VERSION="15.0" - ID="opensuse-leap" - ID_LIKE="suse opensuse" - VERSION_ID="15.0" - PRETTY_NAME="openSUSE Leap 15.0" - ANSI_COLOR="0;32" - CPE_NAME="cpe:/o:opensuse:leap:15.0" - BUG_REPORT_URL="https://bugs.opensuse.org" - HOME_URL="https://www.opensuse.org/" -""") - -OS_RELEASE_OPENSUSE_TW = dedent("""\ - NAME="openSUSE Tumbleweed" - ID="opensuse-tumbleweed" - ID_LIKE="opensuse suse" - VERSION_ID="20180920" - PRETTY_NAME="openSUSE Tumbleweed" - ANSI_COLOR="0;32" - CPE_NAME="cpe:/o:opensuse:tumbleweed:20180920" - BUG_REPORT_URL="https://bugs.opensuse.org" - HOME_URL="https://www.opensuse.org/" -""") - -OS_RELEASE_CENTOS = dedent("""\ - NAME="CentOS Linux" - VERSION="7 (Core)" - ID="centos" - ID_LIKE="rhel fedora" - VERSION_ID="7" - PRETTY_NAME="CentOS Linux 7 (Core)" - ANSI_COLOR="0;31" - CPE_NAME="cpe:/o:centos:centos:7" - HOME_URL="https://www.centos.org/" - BUG_REPORT_URL="https://bugs.centos.org/" - - CENTOS_MANTISBT_PROJECT="CentOS-7" - CENTOS_MANTISBT_PROJECT_VERSION="7" - REDHAT_SUPPORT_PRODUCT="centos" - REDHAT_SUPPORT_PRODUCT_VERSION="7" -""") - -OS_RELEASE_REDHAT_7 = dedent("""\ - NAME="Red Hat Enterprise Linux Server" - VERSION="7.5 (Maipo)" - ID="rhel" - ID_LIKE="fedora" - VARIANT="Server" - VARIANT_ID="server" - VERSION_ID="7.5" - PRETTY_NAME="Red Hat" - ANSI_COLOR="0;31" - CPE_NAME="cpe:/o:redhat:enterprise_linux:7.5: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.5 - REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux" - REDHAT_SUPPORT_PRODUCT_VERSION="7.5" -""") - -OS_RELEASE_ALMALINUX_8 = dedent("""\ - 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 (Purple Manul)" - ANSI_COLOR="0;34" - CPE_NAME="cpe:/o:almalinux:almalinux:8.3:GA" - HOME_URL="https://almalinux.org/" - BUG_REPORT_URL="https://bugs.almalinux.org/" - - ALMALINUX_MANTISBT_PROJECT="AlmaLinux-8" - ALMALINUX_MANTISBT_PROJECT_VERSION="8.3" -""") - -OS_RELEASE_EUROLINUX_7 = dedent("""\ - VERSION="7.9 (Minsk)" - ID="eurolinux" - ID_LIKE="rhel scientific centos fedora" - VERSION_ID="7.9" - PRETTY_NAME="EuroLinux 7.9 (Minsk)" - ANSI_COLOR="0;31" - CPE_NAME="cpe:/o:eurolinux:eurolinux:7.9:GA" - HOME_URL="http://www.euro-linux.com/" - BUG_REPORT_URL="mailto:support@euro-linux.com" - REDHAT_BUGZILLA_PRODUCT="EuroLinux 7" - REDHAT_BUGZILLA_PRODUCT_VERSION=7.9 - REDHAT_SUPPORT_PRODUCT="EuroLinux" - REDHAT_SUPPORT_PRODUCT_VERSION="7.9" -""") - -OS_RELEASE_EUROLINUX_8 = dedent("""\ - NAME="EuroLinux" - VERSION="8.4 (Vaduz)" - ID="eurolinux" - ID_LIKE="rhel fedora centos" - VERSION_ID="8.4" - PLATFORM_ID="platform:el8" - PRETTY_NAME="EuroLinux 8.4 (Vaduz)" - 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" -""") - -OS_RELEASE_ROCKY_8 = dedent("""\ - NAME="Rocky Linux" - VERSION="8.3 (Green Obsidian)" - ID="rocky" - ID_LIKE="rhel fedora" - VERSION_ID="8.3" - PLATFORM_ID="platform:el8" - PRETTY_NAME="Rocky Linux 8.3 (Green Obsidian)" - 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" -""") - -OS_RELEASE_VIRTUOZZO_8 = dedent("""\ - NAME="Virtuozzo Linux" - VERSION="8" - ID="virtuozzo" - ID_LIKE="rhel fedora" - VERSION_ID="8" - PLATFORM_ID="platform:el8" - PRETTY_NAME="Virtuozzo Linux" - ANSI_COLOR="0;31" - CPE_NAME="cpe:/o:virtuozzoproject:vzlinux:8" - HOME_URL="https://www.vzlinux.org" - BUG_REPORT_URL="https://bugs.openvz.org" -""") - -OS_RELEASE_CLOUDLINUX_8 = dedent("""\ - NAME="CloudLinux" - VERSION="8.4 (Valery Rozhdestvensky)" - ID="cloudlinux" - ID_LIKE="rhel fedora centos" - VERSION_ID="8.4" - PLATFORM_ID="platform:el8" - PRETTY_NAME="CloudLinux 8.4 (Valery Rozhdestvensky)" - ANSI_COLOR="0;31" - CPE_NAME="cpe:/o:cloudlinux:cloudlinux:8.4:GA:server" - HOME_URL="https://www.cloudlinux.com/" - BUG_REPORT_URL="https://www.cloudlinux.com/support" -""") - -OS_RELEASE_OPENEULER_20 = dedent("""\ - NAME="openEuler" - VERSION="20.03 (LTS-SP2)" - ID="openEuler" - VERSION_ID="20.03" - PRETTY_NAME="openEuler 20.03 (LTS-SP2)" - ANSI_COLOR="0;31" -""") - -REDHAT_RELEASE_CENTOS_6 = "CentOS release 6.10 (Final)" -REDHAT_RELEASE_CENTOS_7 = "CentOS Linux release 7.5.1804 (Core)" -REDHAT_RELEASE_REDHAT_6 = ( - "Red Hat Enterprise Linux Server release 6.10 (Santiago)") -REDHAT_RELEASE_REDHAT_7 = ( - "Red Hat Enterprise Linux Server release 7.5 (Maipo)") -REDHAT_RELEASE_ALMALINUX_8 = ( - "AlmaLinux release 8.3 (Purple Manul)") -REDHAT_RELEASE_EUROLINUX_7 = "EuroLinux release 7.9 (Minsk)" -REDHAT_RELEASE_EUROLINUX_8 = "EuroLinux release 8.4 (Vaduz)" -REDHAT_RELEASE_ROCKY_8 = ( - "Rocky Linux release 8.3 (Green Obsidian)") -REDHAT_RELEASE_VIRTUOZZO_8 = ( - "Virtuozzo Linux release 8") -REDHAT_RELEASE_CLOUDLINUX_8 = ( - "CloudLinux release 8.4 (Valery Rozhdestvensky)") -OS_RELEASE_DEBIAN = dedent("""\ - PRETTY_NAME="Debian GNU/Linux 9 (stretch)" - NAME="Debian GNU/Linux" - VERSION_ID="9" - VERSION="9 (stretch)" - ID=debian - HOME_URL="https://www.debian.org/" - SUPPORT_URL="https://www.debian.org/support" - BUG_REPORT_URL="https://bugs.debian.org/" -""") - -OS_RELEASE_UBUNTU = dedent("""\ - NAME="Ubuntu"\n - # comment test - VERSION="16.04.3 LTS (Xenial Xerus)"\n - ID=ubuntu\n - ID_LIKE=debian\n - PRETTY_NAME="Ubuntu 16.04.3 LTS"\n - VERSION_ID="16.04"\n - HOME_URL="http://www.ubuntu.com/"\n - SUPPORT_URL="http://help.ubuntu.com/"\n - BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"\n - VERSION_CODENAME=xenial\n - UBUNTU_CODENAME=xenial\n -""") - -OS_RELEASE_PHOTON = ("""\ - NAME="VMware Photon OS" - VERSION="4.0" - ID=photon - VERSION_ID=4.0 - PRETTY_NAME="VMware Photon OS/Linux" - ANSI_COLOR="1;34" - HOME_URL="https://vmware.github.io/photon/" - BUG_REPORT_URL="https://github.com/vmware/photon/issues" -""") - - -class FakeCloud(object): - - def __init__(self, hostname, fqdn): - self.hostname = hostname - self.fqdn = fqdn - self.calls = [] - - def get_hostname(self, fqdn=None, metadata_only=None): - myargs = {} - if fqdn is not None: - myargs['fqdn'] = fqdn - if metadata_only is not None: - myargs['metadata_only'] = metadata_only - self.calls.append(myargs) - if fqdn: - return self.fqdn - return self.hostname - - -class TestUtil(CiTestCase): - - def test_parse_mount_info_no_opts_no_arg(self): - result = util.parse_mount_info('/home', MOUNT_INFO, LOG) - self.assertEqual(('/dev/sda2', 'xfs', '/home'), result) - - def test_parse_mount_info_no_opts_arg(self): - result = util.parse_mount_info('/home', MOUNT_INFO, LOG, False) - self.assertEqual(('/dev/sda2', 'xfs', '/home'), result) - - def test_parse_mount_info_with_opts(self): - result = util.parse_mount_info('/', MOUNT_INFO, LOG, True) - self.assertEqual( - ('/dev/sda1', 'btrfs', '/', 'ro,relatime'), - result - ) - - @mock.patch('cloudinit.util.get_mount_info') - def test_mount_is_rw(self, m_mount_info): - m_mount_info.return_value = ('/dev/sda1', 'btrfs', '/', 'rw,relatime') - is_rw = util.mount_is_read_write('/') - self.assertEqual(is_rw, True) - - @mock.patch('cloudinit.util.get_mount_info') - def test_mount_is_ro(self, m_mount_info): - m_mount_info.return_value = ('/dev/sda1', 'btrfs', '/', 'ro,relatime') - is_rw = util.mount_is_read_write('/') - self.assertEqual(is_rw, False) - - -class TestUptime(CiTestCase): - - @mock.patch('cloudinit.util.boottime') - @mock.patch('cloudinit.util.os.path.exists') - @mock.patch('cloudinit.util.time.time') - def test_uptime_non_linux_path(self, m_time, m_exists, m_boottime): - boottime = 1000.0 - uptime = 10.0 - m_boottime.return_value = boottime - m_time.return_value = boottime + uptime - m_exists.return_value = False - result = util.uptime() - self.assertEqual(str(uptime), result) - - -class TestShellify(CiTestCase): - - def test_input_dict_raises_type_error(self): - self.assertRaisesRegex( - TypeError, 'Input.*was.*dict.*xpected', - util.shellify, {'mykey': 'myval'}) - - def test_input_str_raises_type_error(self): - self.assertRaisesRegex( - TypeError, 'Input.*was.*str.*xpected', util.shellify, "foobar") - - def test_value_with_int_raises_type_error(self): - self.assertRaisesRegex( - TypeError, 'shellify.*int', util.shellify, ["foo", 1]) - - def test_supports_strings_and_lists(self): - self.assertEqual( - '\n'.join(["#!/bin/sh", "echo hi mom", "'echo' 'hi dad'", - "'echo' 'hi' 'sis'", ""]), - util.shellify(["echo hi mom", ["echo", "hi dad"], - ('echo', 'hi', 'sis')])) - - def test_supports_comments(self): - self.assertEqual( - '\n'.join(["#!/bin/sh", "echo start", "echo end", ""]), - util.shellify(["echo start", None, "echo end"])) - - -class TestGetHostnameFqdn(CiTestCase): - - def test_get_hostname_fqdn_from_only_cfg_fqdn(self): - """When cfg only has the fqdn key, derive hostname and fqdn from it.""" - hostname, fqdn = util.get_hostname_fqdn( - cfg={'fqdn': 'myhost.domain.com'}, cloud=None) - self.assertEqual('myhost', hostname) - self.assertEqual('myhost.domain.com', fqdn) - - def test_get_hostname_fqdn_from_cfg_fqdn_and_hostname(self): - """When cfg has both fqdn and hostname keys, return them.""" - hostname, fqdn = util.get_hostname_fqdn( - cfg={'fqdn': 'myhost.domain.com', 'hostname': 'other'}, cloud=None) - self.assertEqual('other', hostname) - self.assertEqual('myhost.domain.com', fqdn) - - def test_get_hostname_fqdn_from_cfg_hostname_with_domain(self): - """When cfg has only hostname key which represents a fqdn, use that.""" - hostname, fqdn = util.get_hostname_fqdn( - cfg={'hostname': 'myhost.domain.com'}, cloud=None) - self.assertEqual('myhost', hostname) - self.assertEqual('myhost.domain.com', fqdn) - - def test_get_hostname_fqdn_from_cfg_hostname_without_domain(self): - """When cfg has a hostname without a '.' query cloud.get_hostname.""" - mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') - hostname, fqdn = util.get_hostname_fqdn( - cfg={'hostname': 'myhost'}, cloud=mycloud) - self.assertEqual('myhost', hostname) - self.assertEqual('cloudhost.mycloud.com', fqdn) - self.assertEqual( - [{'fqdn': True, 'metadata_only': False}], mycloud.calls) - - def test_get_hostname_fqdn_from_without_fqdn_or_hostname(self): - """When cfg has neither hostname nor fqdn cloud.get_hostname.""" - mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') - hostname, fqdn = util.get_hostname_fqdn(cfg={}, cloud=mycloud) - self.assertEqual('cloudhost', hostname) - self.assertEqual('cloudhost.mycloud.com', fqdn) - self.assertEqual( - [{'fqdn': True, 'metadata_only': False}, - {'metadata_only': False}], mycloud.calls) - - def test_get_hostname_fqdn_from_passes_metadata_only_to_cloud(self): - """Calls to cloud.get_hostname pass the metadata_only parameter.""" - mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') - _hn, _fqdn = util.get_hostname_fqdn( - cfg={}, cloud=mycloud, metadata_only=True) - self.assertEqual( - [{'fqdn': True, 'metadata_only': True}, - {'metadata_only': True}], mycloud.calls) - - -class TestBlkid(CiTestCase): - ids = { - "id01": "1111-1111", - "id02": "22222222-2222", - "id03": "33333333-3333", - "id04": "44444444-4444", - "id05": "55555555-5555-5555-5555-555555555555", - "id06": "66666666-6666-6666-6666-666666666666", - "id07": "52894610484658920398", - "id08": "86753098675309867530", - "id09": "99999999-9999-9999-9999-999999999999", - } - - blkid_out = dedent("""\ - /dev/loop0: TYPE="squashfs" - /dev/loop1: TYPE="squashfs" - /dev/loop2: TYPE="squashfs" - /dev/loop3: TYPE="squashfs" - /dev/sda1: UUID="{id01}" TYPE="vfat" PARTUUID="{id02}" - /dev/sda2: UUID="{id03}" TYPE="ext4" PARTUUID="{id04}" - /dev/sda3: UUID="{id05}" TYPE="ext4" PARTUUID="{id06}" - /dev/sda4: LABEL="default" UUID="{id07}" UUID_SUB="{id08}" """ - """TYPE="zfs_member" PARTUUID="{id09}" - /dev/loop4: TYPE="squashfs" - """) - - maxDiff = None - - def _get_expected(self): - return ({ - "/dev/loop0": {"DEVNAME": "/dev/loop0", "TYPE": "squashfs"}, - "/dev/loop1": {"DEVNAME": "/dev/loop1", "TYPE": "squashfs"}, - "/dev/loop2": {"DEVNAME": "/dev/loop2", "TYPE": "squashfs"}, - "/dev/loop3": {"DEVNAME": "/dev/loop3", "TYPE": "squashfs"}, - "/dev/loop4": {"DEVNAME": "/dev/loop4", "TYPE": "squashfs"}, - "/dev/sda1": {"DEVNAME": "/dev/sda1", "TYPE": "vfat", - "UUID": self.ids["id01"], - "PARTUUID": self.ids["id02"]}, - "/dev/sda2": {"DEVNAME": "/dev/sda2", "TYPE": "ext4", - "UUID": self.ids["id03"], - "PARTUUID": self.ids["id04"]}, - "/dev/sda3": {"DEVNAME": "/dev/sda3", "TYPE": "ext4", - "UUID": self.ids["id05"], - "PARTUUID": self.ids["id06"]}, - "/dev/sda4": {"DEVNAME": "/dev/sda4", "TYPE": "zfs_member", - "LABEL": "default", - "UUID": self.ids["id07"], - "UUID_SUB": self.ids["id08"], - "PARTUUID": self.ids["id09"]}, - }) - - @mock.patch("cloudinit.subp.subp") - def test_functional_blkid(self, m_subp): - m_subp.return_value = ( - self.blkid_out.format(**self.ids), "") - self.assertEqual(self._get_expected(), util.blkid()) - m_subp.assert_called_with(["blkid", "-o", "full"], capture=True, - decode="replace") - - @mock.patch("cloudinit.subp.subp") - def test_blkid_no_cache_uses_no_cache(self, m_subp): - """blkid should turn off cache if disable_cache is true.""" - m_subp.return_value = ( - self.blkid_out.format(**self.ids), "") - self.assertEqual(self._get_expected(), - util.blkid(disable_cache=True)) - m_subp.assert_called_with(["blkid", "-o", "full", "-c", "/dev/null"], - capture=True, decode="replace") - - -@mock.patch('cloudinit.subp.subp') -class TestUdevadmSettle(CiTestCase): - def test_with_no_params(self, m_subp): - """called with no parameters.""" - util.udevadm_settle() - m_subp.called_once_with(mock.call(['udevadm', 'settle'])) - - def test_with_exists_and_not_exists(self, m_subp): - """with exists=file where file does not exist should invoke subp.""" - mydev = self.tmp_path("mydev") - util.udevadm_settle(exists=mydev) - m_subp.called_once_with( - ['udevadm', 'settle', '--exit-if-exists=%s' % mydev]) - - def test_with_exists_and_file_exists(self, m_subp): - """with exists=file where file does exist should not invoke subp.""" - mydev = self.tmp_path("mydev") - util.write_file(mydev, "foo\n") - util.udevadm_settle(exists=mydev) - self.assertIsNone(m_subp.call_args) - - def test_with_timeout_int(self, m_subp): - """timeout can be an integer.""" - timeout = 9 - util.udevadm_settle(timeout=timeout) - m_subp.called_once_with( - ['udevadm', 'settle', '--timeout=%s' % timeout]) - - def test_with_timeout_string(self, m_subp): - """timeout can be a string.""" - timeout = "555" - util.udevadm_settle(timeout=timeout) - m_subp.assert_called_once_with( - ['udevadm', 'settle', '--timeout=%s' % timeout]) - - def test_with_exists_and_timeout(self, m_subp): - """test call with both exists and timeout.""" - mydev = self.tmp_path("mydev") - timeout = "3" - util.udevadm_settle(exists=mydev) - m_subp.called_once_with( - ['udevadm', 'settle', '--exit-if-exists=%s' % mydev, - '--timeout=%s' % timeout]) - - def test_subp_exception_raises_to_caller(self, m_subp): - m_subp.side_effect = subp.ProcessExecutionError("BOOM") - self.assertRaises(subp.ProcessExecutionError, util.udevadm_settle) - - -@mock.patch('os.path.exists') -class TestGetLinuxDistro(CiTestCase): - - def setUp(self): - # python2 has no lru_cache, and therefore, no cache_clear() - if hasattr(util.get_linux_distro, "cache_clear"): - util.get_linux_distro.cache_clear() - - @classmethod - def os_release_exists(self, path): - """Side effect function""" - if path == '/etc/os-release': - return 1 - - @classmethod - def redhat_release_exists(self, path): - """Side effect function """ - if path == '/etc/redhat-release': - return 1 - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_distro_quoted_name(self, m_os_release, m_path_exists): - """Verify we get the correct name if the os-release file has - the distro name in quotes""" - m_os_release.return_value = OS_RELEASE_SLES - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('sles', '12.3', platform.machine()), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_distro_bare_name(self, m_os_release, m_path_exists): - """Verify we get the correct name if the os-release file does not - have the distro name in quotes""" - m_os_release.return_value = OS_RELEASE_UBUNTU - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('ubuntu', '16.04', 'xenial'), dist) - - @mock.patch('platform.system') - @mock.patch('platform.release') - @mock.patch('cloudinit.util._parse_redhat_release') - def test_get_linux_freebsd(self, m_parse_redhat_release, - m_platform_release, - m_platform_system, m_path_exists): - """Verify we get the correct name and release name on FreeBSD.""" - m_path_exists.return_value = False - m_platform_release.return_value = '12.0-RELEASE-p10' - m_platform_system.return_value = 'FreeBSD' - m_parse_redhat_release.return_value = {} - util.is_BSD.cache_clear() - dist = util.get_linux_distro() - self.assertEqual(('freebsd', '12.0-RELEASE-p10', ''), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_centos6(self, m_os_release, m_path_exists): - """Verify we get the correct name and release name on CentOS 6.""" - m_os_release.return_value = REDHAT_RELEASE_CENTOS_6 - m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists - dist = util.get_linux_distro() - self.assertEqual(('centos', '6.10', 'Final'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_centos7_redhat_release(self, m_os_release, m_exists): - """Verify the correct release info on CentOS 7 without os-release.""" - m_os_release.return_value = REDHAT_RELEASE_CENTOS_7 - m_exists.side_effect = TestGetLinuxDistro.redhat_release_exists - dist = util.get_linux_distro() - self.assertEqual(('centos', '7.5.1804', 'Core'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_redhat7_osrelease(self, m_os_release, m_path_exists): - """Verify redhat 7 read from os-release.""" - m_os_release.return_value = OS_RELEASE_REDHAT_7 - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('redhat', '7.5', 'Maipo'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_redhat7_rhrelease(self, m_os_release, m_path_exists): - """Verify redhat 7 read from redhat-release.""" - m_os_release.return_value = REDHAT_RELEASE_REDHAT_7 - m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists - dist = util.get_linux_distro() - self.assertEqual(('redhat', '7.5', 'Maipo'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_redhat6_rhrelease(self, m_os_release, m_path_exists): - """Verify redhat 6 read from redhat-release.""" - m_os_release.return_value = REDHAT_RELEASE_REDHAT_6 - m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists - dist = util.get_linux_distro() - self.assertEqual(('redhat', '6.10', 'Santiago'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_copr_centos(self, m_os_release, m_path_exists): - """Verify we get the correct name and release name on COPR CentOS.""" - m_os_release.return_value = OS_RELEASE_CENTOS - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('centos', '7', 'Core'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_almalinux8_rhrelease(self, m_os_release, m_path_exists): - """Verify almalinux 8 read from redhat-release.""" - m_os_release.return_value = REDHAT_RELEASE_ALMALINUX_8 - m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists - dist = util.get_linux_distro() - self.assertEqual(('almalinux', '8.3', 'Purple Manul'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_almalinux8_osrelease(self, m_os_release, m_path_exists): - """Verify almalinux 8 read from os-release.""" - m_os_release.return_value = OS_RELEASE_ALMALINUX_8 - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('almalinux', '8.3', 'Purple Manul'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_eurolinux7_rhrelease(self, m_os_release, m_path_exists): - """Verify eurolinux 7 read from redhat-release.""" - m_os_release.return_value = REDHAT_RELEASE_EUROLINUX_7 - m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists - dist = util.get_linux_distro() - self.assertEqual(('eurolinux', '7.9', 'Minsk'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_eurolinux7_osrelease(self, m_os_release, m_path_exists): - """Verify eurolinux 7 read from os-release.""" - m_os_release.return_value = OS_RELEASE_EUROLINUX_7 - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('eurolinux', '7.9', 'Minsk'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_eurolinux8_rhrelease(self, m_os_release, m_path_exists): - """Verify eurolinux 8 read from redhat-release.""" - m_os_release.return_value = REDHAT_RELEASE_EUROLINUX_8 - m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists - dist = util.get_linux_distro() - self.assertEqual(('eurolinux', '8.4', 'Vaduz'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_eurolinux8_osrelease(self, m_os_release, m_path_exists): - """Verify eurolinux 8 read from os-release.""" - m_os_release.return_value = OS_RELEASE_EUROLINUX_8 - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('eurolinux', '8.4', 'Vaduz'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_rocky8_rhrelease(self, m_os_release, m_path_exists): - """Verify rocky linux 8 read from redhat-release.""" - m_os_release.return_value = REDHAT_RELEASE_ROCKY_8 - m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists - dist = util.get_linux_distro() - self.assertEqual(('rocky', '8.3', 'Green Obsidian'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_rocky8_osrelease(self, m_os_release, m_path_exists): - """Verify rocky linux 8 read from os-release.""" - m_os_release.return_value = OS_RELEASE_ROCKY_8 - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('rocky', '8.3', 'Green Obsidian'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_virtuozzo8_rhrelease(self, m_os_release, m_path_exists): - """Verify virtuozzo linux 8 read from redhat-release.""" - m_os_release.return_value = REDHAT_RELEASE_VIRTUOZZO_8 - m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists - dist = util.get_linux_distro() - self.assertEqual(('virtuozzo', '8', 'Virtuozzo Linux'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_virtuozzo8_osrelease(self, m_os_release, m_path_exists): - """Verify virtuozzo linux 8 read from os-release.""" - m_os_release.return_value = OS_RELEASE_VIRTUOZZO_8 - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('virtuozzo', '8', 'Virtuozzo Linux'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_cloud8_rhrelease(self, m_os_release, m_path_exists): - """Verify cloudlinux 8 read from redhat-release.""" - m_os_release.return_value = REDHAT_RELEASE_CLOUDLINUX_8 - m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists - dist = util.get_linux_distro() - self.assertEqual(('cloudlinux', '8.4', 'Valery Rozhdestvensky'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_cloud8_osrelease(self, m_os_release, m_path_exists): - """Verify cloudlinux 8 read from os-release.""" - m_os_release.return_value = OS_RELEASE_CLOUDLINUX_8 - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('cloudlinux', '8.4', 'Valery Rozhdestvensky'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_debian(self, m_os_release, m_path_exists): - """Verify we get the correct name and release name on Debian.""" - m_os_release.return_value = OS_RELEASE_DEBIAN - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('debian', '9', 'stretch'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_openeuler(self, m_os_release, m_path_exists): - """Verify get the correct name and release name on Openeuler.""" - m_os_release.return_value = OS_RELEASE_OPENEULER_20 - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('openEuler', '20.03', 'LTS-SP2'), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_opensuse(self, m_os_release, m_path_exists): - """Verify we get the correct name and machine arch on openSUSE - prior to openSUSE Leap 15. - """ - m_os_release.return_value = OS_RELEASE_OPENSUSE - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('opensuse', '42.3', platform.machine()), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_opensuse_l15(self, m_os_release, m_path_exists): - """Verify we get the correct name and machine arch on openSUSE - for openSUSE Leap 15.0 and later. - """ - m_os_release.return_value = OS_RELEASE_OPENSUSE_L15 - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual(('opensuse-leap', '15.0', platform.machine()), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_opensuse_tw(self, m_os_release, m_path_exists): - """Verify we get the correct name and machine arch on openSUSE - for openSUSE Tumbleweed - """ - m_os_release.return_value = OS_RELEASE_OPENSUSE_TW - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual( - ('opensuse-tumbleweed', '20180920', platform.machine()), dist) - - @mock.patch('cloudinit.util.load_file') - def test_get_linux_photon_os_release(self, m_os_release, m_path_exists): - """Verify we get the correct name and machine arch on PhotonOS""" - m_os_release.return_value = OS_RELEASE_PHOTON - m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists - dist = util.get_linux_distro() - self.assertEqual( - ('photon', '4.0', 'VMware Photon OS/Linux'), dist) - - @mock.patch('platform.system') - @mock.patch('platform.dist', create=True) - def test_get_linux_distro_no_data(self, m_platform_dist, - m_platform_system, m_path_exists): - """Verify we get no information if os-release does not exist""" - m_platform_dist.return_value = ('', '', '') - m_platform_system.return_value = "Linux" - m_path_exists.return_value = 0 - dist = util.get_linux_distro() - self.assertEqual(('', '', ''), dist) - - @mock.patch('platform.system') - @mock.patch('platform.dist', create=True) - def test_get_linux_distro_no_impl(self, m_platform_dist, - m_platform_system, m_path_exists): - """Verify we get an empty tuple when no information exists and - Exceptions are not propagated""" - m_platform_dist.side_effect = Exception() - m_platform_system.return_value = "Linux" - m_path_exists.return_value = 0 - dist = util.get_linux_distro() - self.assertEqual(('', '', ''), dist) - - @mock.patch('platform.system') - @mock.patch('platform.dist', create=True) - def test_get_linux_distro_plat_data(self, m_platform_dist, - m_platform_system, m_path_exists): - """Verify we get the correct platform information""" - m_platform_dist.return_value = ('foo', '1.1', 'aarch64') - m_platform_system.return_value = "Linux" - m_path_exists.return_value = 0 - dist = util.get_linux_distro() - self.assertEqual(('foo', '1.1', 'aarch64'), dist) - - -class TestJsonDumps(CiTestCase): - def test_is_str(self): - """json_dumps should return a string.""" - self.assertTrue(isinstance(util.json_dumps({'abc': '123'}), str)) - - def test_utf8(self): - smiley = '\\ud83d\\ude03' - self.assertEqual( - {'smiley': smiley}, - json.loads(util.json_dumps({'smiley': smiley}))) - - def test_non_utf8(self): - blob = b'\xba\x03Qx-#y\xea' - self.assertEqual( - {'blob': 'ci-b64:' + base64.b64encode(blob).decode('utf-8')}, - json.loads(util.json_dumps({'blob': blob}))) - - -@mock.patch('os.path.exists') -class TestIsLXD(CiTestCase): - - def test_is_lxd_true_on_sock_device(self, m_exists): - """When lxd's /dev/lxd/sock exists, is_lxd returns true.""" - m_exists.return_value = True - self.assertTrue(util.is_lxd()) - m_exists.assert_called_once_with('/dev/lxd/sock') - - def test_is_lxd_false_when_sock_device_absent(self, m_exists): - """When lxd's /dev/lxd/sock is absent, is_lxd returns false.""" - m_exists.return_value = False - self.assertFalse(util.is_lxd()) - m_exists.assert_called_once_with('/dev/lxd/sock') - - -class TestReadCcFromCmdline: - - @pytest.mark.parametrize( - "cmdline,expected_cfg", - [ - # Return None if cmdline has no cc:<YAML>end_cc content. - (CiTestCase.random_string(), None), - # Return None if YAML content is empty string. - ('foo cc: end_cc bar', None), - # Return expected dictionary without trailing end_cc marker. - ('foo cc: ssh_pwauth: true', {'ssh_pwauth': True}), - # Return expected dictionary w escaped newline and no end_cc. - ('foo cc: ssh_pwauth: true\\n', {'ssh_pwauth': True}), - # Return expected dictionary of yaml between cc: and end_cc. - ('foo cc: ssh_pwauth: true end_cc bar', {'ssh_pwauth': True}), - # Return dict with list value w escaped newline, no end_cc. - ( - 'cc: ssh_import_id: [smoser, kirkland]\\n', - {'ssh_import_id': ['smoser', 'kirkland']} - ), - # Parse urlencoded brackets in yaml content. - ( - 'cc: ssh_import_id: %5Bsmoser, kirkland%5D end_cc', - {'ssh_import_id': ['smoser', 'kirkland']} - ), - # Parse complete urlencoded yaml content. - ( - 'cc: ssh_import_id%3A%20%5Buser1%2C%20user2%5D end_cc', - {'ssh_import_id': ['user1', 'user2']} - ), - # Parse nested dictionary in yaml content. - ( - 'cc: ntp: {enabled: true, ntp_client: myclient} end_cc', - {'ntp': {'enabled': True, 'ntp_client': 'myclient'}} - ), - # Parse single mapping value in yaml content. - ('cc: ssh_import_id: smoser end_cc', {'ssh_import_id': 'smoser'}), - # Parse multiline content with multiple mapping and nested lists. - ( - ('cc: ssh_import_id: [smoser, bob]\\n' - 'runcmd: [ [ ls, -l ], echo hi ] end_cc'), - {'ssh_import_id': ['smoser', 'bob'], - 'runcmd': [['ls', '-l'], 'echo hi']} - ), - # Parse multiline encoded content w/ mappings and nested lists. - ( - ('cc: ssh_import_id: %5Bsmoser, bob%5D\\n' - 'runcmd: [ [ ls, -l ], echo hi ] end_cc'), - {'ssh_import_id': ['smoser', 'bob'], - 'runcmd': [['ls', '-l'], 'echo hi']} - ), - # test encoded escaped newlines work. - # - # unquote(encoded_content) - # 'ssh_import_id: [smoser, bob]\\nruncmd: [ [ ls, -l ], echo hi ]' - ( - ('cc: ' + - ('ssh_import_id%3A%20%5Bsmoser%2C%20bob%5D%5Cn' - 'runcmd%3A%20%5B%20%5B%20ls%2C%20-l%20%5D%2C' - '%20echo%20hi%20%5D') + ' end_cc'), - {'ssh_import_id': ['smoser', 'bob'], - 'runcmd': [['ls', '-l'], 'echo hi']} - ), - # test encoded newlines work. - # - # unquote(encoded_content) - # 'ssh_import_id: [smoser, bob]\nruncmd: [ [ ls, -l ], echo hi ]' - ( - ("cc: " + - ('ssh_import_id%3A%20%5Bsmoser%2C%20bob%5D%0A' - 'runcmd%3A%20%5B%20%5B%20ls%2C%20-l%20%5D%2C' - '%20echo%20hi%20%5D') + ' end_cc'), - {'ssh_import_id': ['smoser', 'bob'], - 'runcmd': [['ls', '-l'], 'echo hi']} - ), - # Parse and merge multiple yaml content sections. - ( - ('cc:ssh_import_id: [smoser, bob] end_cc ' - 'cc: runcmd: [ [ ls, -l ] ] end_cc'), - {'ssh_import_id': ['smoser', 'bob'], - 'runcmd': [['ls', '-l']]} - ), - # Parse and merge multiple encoded yaml content sections. - ( - ('cc:ssh_import_id%3A%20%5Bsmoser%5D end_cc ' - 'cc:runcmd%3A%20%5B%20%5B%20ls%2C%20-l%20%5D%20%5D end_cc'), - {'ssh_import_id': ['smoser'], 'runcmd': [['ls', '-l']]} - ), - ] - ) - def test_read_conf_from_cmdline_config(self, expected_cfg, cmdline): - assert expected_cfg == util.read_conf_from_cmdline(cmdline=cmdline) - - -class TestMountCb: - """Tests for ``util.mount_cb``. - - These tests consider the "unit" under test to be ``util.mount_cb`` and - ``util.unmounter``, which is only used by ``mount_cb``. - - TODO: Test default mtype determination - TODO: Test the if/else branch that actually performs the mounting operation - """ - - @pytest.yield_fixture - def already_mounted_device_and_mountdict(self): - """Mock an already-mounted device, and yield (device, mount dict)""" - device = "/dev/fake0" - mountpoint = "/mnt/fake" - with mock.patch("cloudinit.util.subp.subp"): - with mock.patch("cloudinit.util.mounts") as m_mounts: - mounts = {device: {"mountpoint": mountpoint}} - m_mounts.return_value = mounts - yield device, mounts[device] - - @pytest.fixture - def already_mounted_device(self, already_mounted_device_and_mountdict): - """already_mounted_device_and_mountdict, but return only the device""" - return already_mounted_device_and_mountdict[0] - - @pytest.mark.parametrize( - "mtype,expected", - [ - # While the filesystem is called iso9660, the mount type is cd9660 - ("iso9660", "cd9660"), - # vfat is generally called "msdos" on BSD - ("vfat", "msdos"), - # judging from man pages, only FreeBSD has this alias - ("msdosfs", "msdos"), - # Test happy path - ("ufs", "ufs") - ], - ) - @mock.patch("cloudinit.util.is_Linux", autospec=True) - @mock.patch("cloudinit.util.is_BSD", autospec=True) - @mock.patch("cloudinit.util.subp.subp") - @mock.patch("cloudinit.temp_utils.tempdir", autospec=True) - def test_normalize_mtype_on_bsd( - self, m_tmpdir, m_subp, m_is_BSD, m_is_Linux, mtype, expected - ): - m_is_BSD.return_value = True - m_is_Linux.return_value = False - m_tmpdir.return_value.__enter__ = mock.Mock( - autospec=True, return_value="/tmp/fake" - ) - m_tmpdir.return_value.__exit__ = mock.Mock( - autospec=True, return_value=True - ) - callback = mock.Mock(autospec=True) - - util.mount_cb('/dev/fake0', callback, mtype=mtype) - assert mock.call( - ["mount", "-o", "ro", "-t", expected, "/dev/fake0", "/tmp/fake"], - update_env=None) in m_subp.call_args_list - - @pytest.mark.parametrize("invalid_mtype", [int(0), float(0.0), dict()]) - def test_typeerror_raised_for_invalid_mtype(self, invalid_mtype): - with pytest.raises(TypeError): - util.mount_cb(mock.Mock(), mock.Mock(), mtype=invalid_mtype) - - @mock.patch("cloudinit.util.subp.subp") - def test_already_mounted_does_not_mount_or_umount_anything( - self, m_subp, already_mounted_device - ): - util.mount_cb(already_mounted_device, mock.Mock()) - - assert 0 == m_subp.call_count - - @pytest.mark.parametrize("trailing_slash_in_mounts", ["/", ""]) - def test_already_mounted_calls_callback( - self, trailing_slash_in_mounts, already_mounted_device_and_mountdict - ): - device, mount_dict = already_mounted_device_and_mountdict - mountpoint = mount_dict["mountpoint"] - mount_dict["mountpoint"] += trailing_slash_in_mounts - - callback = mock.Mock() - util.mount_cb(device, callback) - - # The mountpoint passed to callback should always have a trailing - # slash, regardless of the input - assert [mock.call(mountpoint + "/")] == callback.call_args_list - - def test_already_mounted_calls_callback_with_data( - self, already_mounted_device - ): - callback = mock.Mock() - util.mount_cb( - already_mounted_device, callback, data=mock.sentinel.data - ) - - assert [ - mock.call(mock.ANY, mock.sentinel.data) - ] == callback.call_args_list - - -@mock.patch("cloudinit.util.write_file") -class TestEnsureFile: - """Tests for ``cloudinit.util.ensure_file``.""" - - def test_parameters_passed_through(self, m_write_file): - """Test the parameters in the signature are passed to write_file.""" - util.ensure_file( - mock.sentinel.path, - mode=mock.sentinel.mode, - preserve_mode=mock.sentinel.preserve_mode, - ) - - assert 1 == m_write_file.call_count - args, kwargs = m_write_file.call_args - assert (mock.sentinel.path,) == args - assert mock.sentinel.mode == kwargs["mode"] - assert mock.sentinel.preserve_mode == kwargs["preserve_mode"] - - @pytest.mark.parametrize( - "kwarg,expected", - [ - # Files should be world-readable by default - ("mode", 0o644), - # The previous behaviour of not preserving mode should be retained - ("preserve_mode", False), - ], - ) - def test_defaults(self, m_write_file, kwarg, expected): - """Test that ensure_file defaults appropriately.""" - util.ensure_file(mock.sentinel.path) - - assert 1 == m_write_file.call_count - _args, kwargs = m_write_file.call_args - assert expected == kwargs[kwarg] - - def test_static_parameters_are_passed(self, m_write_file): - """Test that the static write_files parameters are passed correctly.""" - util.ensure_file(mock.sentinel.path) - - assert 1 == m_write_file.call_count - _args, kwargs = m_write_file.call_args - assert "" == kwargs["content"] - assert "ab" == kwargs["omode"] - - -@mock.patch("cloudinit.util.grp.getgrnam") -@mock.patch("cloudinit.util.os.setgid") -@mock.patch("cloudinit.util.os.umask") -class TestRedirectOutputPreexecFn: - """This tests specifically the preexec_fn used in redirect_output.""" - - @pytest.fixture(params=["outfmt", "errfmt"]) - def preexec_fn(self, request): - """A fixture to gather the preexec_fn used by redirect_output. - - This enables simpler direct testing of it, and parameterises any tests - using it to cover both the stdout and stderr code paths. - """ - test_string = "| piped output to invoke subprocess" - if request.param == "outfmt": - args = (test_string, None) - elif request.param == "errfmt": - args = (None, test_string) - with mock.patch("cloudinit.util.subprocess.Popen") as m_popen: - util.redirect_output(*args) - - assert 1 == m_popen.call_count - _args, kwargs = m_popen.call_args - assert "preexec_fn" in kwargs, "preexec_fn not passed to Popen" - return kwargs["preexec_fn"] - - def test_preexec_fn_sets_umask( - self, m_os_umask, _m_setgid, _m_getgrnam, preexec_fn - ): - """preexec_fn should set a mask that avoids world-readable files.""" - preexec_fn() - - assert [mock.call(0o037)] == m_os_umask.call_args_list - - def test_preexec_fn_sets_group_id_if_adm_group_present( - self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn - ): - """We should setgrp to adm if present, so files are owned by them.""" - fake_group = mock.Mock(gr_gid=mock.sentinel.gr_gid) - m_getgrnam.return_value = fake_group - - preexec_fn() - - assert [mock.call("adm")] == m_getgrnam.call_args_list - assert [mock.call(mock.sentinel.gr_gid)] == m_setgid.call_args_list - - def test_preexec_fn_handles_absent_adm_group_gracefully( - self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn - ): - """We should handle an absent adm group gracefully.""" - m_getgrnam.side_effect = KeyError("getgrnam(): name not found: 'adm'") - - preexec_fn() - - assert 0 == m_setgid.call_count - -# vi: ts=4 expandtab diff --git a/cloudinit/util.py b/cloudinit/util.py index 575a1fef..2045a6ab 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -533,42 +533,45 @@ def get_linux_distro(): return (distro_name, distro_version, flavor) -@lru_cache() -def system_info(): - info = { - 'platform': platform.platform(), - 'system': platform.system(), - 'release': platform.release(), - 'python': platform.python_version(), - 'uname': list(platform.uname()), - 'dist': get_linux_distro() - } +def _get_variant(info): system = info['system'].lower() - var = 'unknown' + variant = 'unknown' if system == "linux": linux_dist = info['dist'][0].lower() if linux_dist in ( 'almalinux', 'alpine', 'arch', 'centos', 'cloudlinux', - 'debian', 'eurolinux', 'fedora', 'openEuler', 'photon', + 'debian', 'eurolinux', 'fedora', 'openeuler', 'photon', 'rhel', 'rocky', 'suse', 'virtuozzo'): - var = linux_dist + variant = linux_dist elif linux_dist in ('ubuntu', 'linuxmint', 'mint'): - var = 'ubuntu' + variant = 'ubuntu' elif linux_dist == 'redhat': - var = 'rhel' + variant = 'rhel' elif linux_dist in ( 'opensuse', 'opensuse-tumbleweed', 'opensuse-leap', 'sles', 'sle_hpc'): - var = 'suse' + variant = 'suse' else: - var = 'linux' + variant = 'linux' elif system in ( 'windows', 'darwin', "freebsd", "netbsd", "openbsd", "dragonfly"): - var = system + variant = system + + return variant - info['variant'] = var +@lru_cache() +def system_info(): + info = { + 'platform': platform.platform(), + 'system': platform.system(), + 'release': platform.release(), + 'python': platform.python_version(), + 'uname': list(platform.uname()), + 'dist': get_linux_distro() + } + info['variant'] = _get_variant(info) return info diff --git a/conftest.py b/conftest.py index 9e9d9ff8..f3f8c036 100644 --- a/conftest.py +++ b/conftest.py @@ -12,7 +12,7 @@ from unittest import mock import pytest -from cloudinit import helpers, subp +from cloudinit import helpers, subp, util class _FixtureUtils: @@ -201,3 +201,19 @@ def paths(tmpdir): "run_dir": tmpdir.mkdir("run_dir").strpath, } return helpers.Paths(dirs) + + +@pytest.fixture(autouse=True, scope='session') +def monkeypatch_system_info(): + def my_system_info(): + return { + "platform": "invalid", + "system": "invalid", + "release": "invalid", + "python": "invalid", + "uname": ["invalid"] * 6, + "dist": ("Distro", "-1.1", "Codename"), + "variant": "ubuntu" + } + + util.system_info = my_system_info diff --git a/debian/changelog b/debian/changelog index 749b04a1..c4a217ff 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,42 @@ +cloud-init (21.4-25-g039c40f9-0ubuntu1~22.04.1) jammy; urgency=medium + + * New upstream snapshot. + - Reorganize unit test locations under tests/unittests (#1126) + [Brett Holman] + - Fix exception when no activator found (#1129) (LP: #1948681) + - jinja: provide and document jinja-safe key aliases in instance-data + (SC-622) (#1123) + - testing: Remove date from final_message test (SC-638) (#1127) + - Move GCE metadata fetch to init-local (SC-502) (#1122) + - Fix missing metadata routes for vultr (#1125) [eb3095] + - cc_ssh_authkey_fingerprints.py: prevent duplicate messages on console + (#1081) [dermotbradley] + - sources/azure: remove unused remnants related to agent command (#1119) + [Chris Patterson] + - github: update PR template's contributing URL (#1120) [Chris Patterson] + - docs: Rename HACKING.rst to CONTRIBUTING.rst (#1118) + - testing: monkeypatch system_info call in unit tests (SC-533) (#1117) + - Fix Vultr timeout and wait values (#1113) [eb3095] + - lxd: add preference for LXD cloud-init.* config keys over user keys + (#1108) + - VMware: source /etc/network/interfaces.d/* on Debian + [chengcheng-chcheng] (LP: #1950136) + - Add cjp256 as contributor (#1109) [Chris Patterson] + - integration_tests: Ensure log directory exists before symlinking to it + (#1110) + - testing: add growpart integration test (#1104) [Brett Holman] + - integration_test: Speed up CI run time (#1111) + - Some miscellaneous integration test fixes (SC-606) (#1103) + - tests: specialize lxd_discovery test for lxd_vm vendordata (#1106) + - Add convenience symlink to integration test output (#1105) [Brett Holman] + - Fix for set-name bug in networkd renderer (#1100) + [Andrew Kutz] (LP: #1949407) + - Wait for apt lock (#1034) (LP: #1944611) + - testing: stop chef test from running on openstack (#1102) + - alpine.py: add options to the apk upgrade command (#1089) [dermotbradley] + + -- Chad Smith <chad.smith@canonical.com> Fri, 03 Dec 2021 15:44:06 -0700 + cloud-init (21.4-0ubuntu1~22.04.1) jammy; urgency=medium * d/upstream/metadata: Change contact to James Falcon diff --git a/doc/examples/cloud-config-datasources.txt b/doc/examples/cloud-config-datasources.txt index 13bb687c..d1a4d79e 100644 --- a/doc/examples/cloud-config-datasources.txt +++ b/doc/examples/cloud-config-datasources.txt @@ -46,7 +46,6 @@ datasource: local-hostname: myhost.internal Azure: - agent_command: [service, walinuxagent, start] set_hostname: True hostname_bounce: interface: eth0 diff --git a/doc/rtd/index.rst b/doc/rtd/index.rst index 69cf2068..251a904d 100644 --- a/doc/rtd/index.rst +++ b/doc/rtd/index.rst @@ -68,7 +68,7 @@ Having trouble? We would like to help! :titlesonly: :caption: Development - topics/hacking.rst + topics/contributing.rst topics/code_review.rst topics/security.rst topics/debugging.rst diff --git a/doc/rtd/topics/contributing.rst b/doc/rtd/topics/contributing.rst new file mode 100644 index 00000000..c9e88dbb --- /dev/null +++ b/doc/rtd/topics/contributing.rst @@ -0,0 +1,2 @@ +.. include:: ../../../CONTRIBUTING.rst +.. vi: textwidth=78 diff --git a/doc/rtd/topics/hacking.rst b/doc/rtd/topics/hacking.rst deleted file mode 100644 index 5ec25bfb..00000000 --- a/doc/rtd/topics/hacking.rst +++ /dev/null @@ -1,2 +0,0 @@ -.. include:: ../../../HACKING.rst -.. vi: textwidth=78 diff --git a/doc/rtd/topics/instancedata.rst b/doc/rtd/topics/instancedata.rst index 6c17139f..c33b907a 100644 --- a/doc/rtd/topics/instancedata.rst +++ b/doc/rtd/topics/instancedata.rst @@ -530,12 +530,18 @@ Both user-data scripts and **#cloud-config** data support jinja template rendering. When the first line of the provided user-data begins with, **## template: jinja** cloud-init will use jinja to render that file. -Any instance-data-sensitive.json variables are surfaced as dot-delimited -jinja template variables because cloud-config modules are run as 'root' -user. +Any instance-data-sensitive.json variables are surfaced as jinja template +variables because cloud-config modules are run as 'root' user. - -Below are some examples of providing these types of user-data: +.. note:: + cloud-init also provides jinja-safe key aliases for any instance-data.json + keys which contain jinja operator characters such as +, -, ., /, etc. Any + jinja operator will be replaced with underscores in the jinja-safe key + alias. This allows for cloud-init templates to use aliased variable + references which allow for jinja's dot-notation reference such as + ``{{ ds.v1_0.my_safe_key }}`` instead of ``{{ ds["v1.0"]["my/safe-key"] }}``. + +Below are some other examples of using jinja templates in user-data: * Cloud config calling home with the ec2 public hostname and availability-zone diff --git a/doc/rtd/topics/testing.rst b/doc/rtd/topics/testing.rst index d882e036..7a1e3eec 100644 --- a/doc/rtd/topics/testing.rst +++ b/doc/rtd/topics/testing.rst @@ -3,8 +3,7 @@ Testing ******* cloud-init has both unit tests and integration tests. Unit tests can -be found in-tree alongside the source code, as well as -at ``tests/unittests``. Integration tests can be found at +be found at ``tests/unittests``. Integration tests can be found at ``tests/integration_tests``. Documentation specifically for integration tests can be found on the :ref:`integration_tests` page, but the guidelines specified below apply to both types of tests. @@ -36,6 +35,16 @@ Test Layout subclass (indirectly) from ``TestCase`` (e.g. `TestPrependBaseCommands`_) +* Unit tests and integration tests are located under cloud-init/tests + + * For consistency, unit test files should have a matching name and + directory location under `tests/unittests` + + * For example: the expected test file for code in + `cloudinit/path/to/file.py` is + `tests/unittests/path/to/test_file.py` + + ``pytest`` Tests ---------------- @@ -291,7 +291,7 @@ setuptools.setup( author='Scott Moser', author_email='scott.moser@canonical.com', url='http://launchpad.net/cloud-init/', - packages=setuptools.find_packages(exclude=['tests.*', '*.tests', 'tests']), + packages=setuptools.find_packages(exclude=['tests.*', 'tests']), scripts=['tools/cloud-init-per'], license='Dual-licensed under GPLv3 or Apache 2.0', data_files=data_files, diff --git a/tests/integration_tests/bugs/test_gh868.py b/tests/integration_tests/bugs/test_gh868.py index 73c03451..1119d461 100644 --- a/tests/integration_tests/bugs/test_gh868.py +++ b/tests/integration_tests/bugs/test_gh868.py @@ -16,6 +16,12 @@ chef: @pytest.mark.adhoc # Can't be regularly reaching out to chef install script +@pytest.mark.ec2 +@pytest.mark.gce +@pytest.mark.azure +@pytest.mark.oci +@pytest.mark.lxd_container +@pytest.mark.lxd_vm @pytest.mark.user_data(USERDATA) def test_chef_license(client: IntegrationInstance): log = client.read_from_file('/var/log/cloud-init.log') diff --git a/tests/integration_tests/conftest.py b/tests/integration_tests/conftest.py index 5a543e39..5eab5a45 100644 --- a/tests/integration_tests/conftest.py +++ b/tests/integration_tests/conftest.py @@ -190,8 +190,16 @@ def _collect_logs(instance: IntegrationInstance, node_id: str, integration_settings.LOCAL_LOG_PATH ) / session_start_time / node_id_path log.info("Writing logs to %s", log_dir) + if not log_dir.exists(): log_dir.mkdir(parents=True) + + # Add a symlink to the latest log output directory + last_symlink = Path(integration_settings.LOCAL_LOG_PATH) / 'last' + if os.path.islink(last_symlink): + os.unlink(last_symlink) + os.symlink(log_dir.parent, last_symlink) + tarball_path = log_dir / 'cloud-init.tar.gz' instance.pull_file('/var/tmp/cloud-init.tar.gz', tarball_path) @@ -242,7 +250,7 @@ def _client(request, fixture_utils, session_cloud: IntegrationCloud): local_launch_kwargs = {} if lxd_setup is not None: if not isinstance(session_cloud, _LxdIntegrationCloud): - pytest.skip('lxd_setup requres LXD') + pytest.skip('lxd_setup requires LXD') local_launch_kwargs['lxd_setup'] = lxd_setup with session_cloud.launch( diff --git a/tests/integration_tests/datasources/test_lxd_discovery.py b/tests/integration_tests/datasources/test_lxd_discovery.py index be76e179..3f05e906 100644 --- a/tests/integration_tests/datasources/test_lxd_discovery.py +++ b/tests/integration_tests/datasources/test_lxd_discovery.py @@ -2,6 +2,7 @@ import json import pytest import yaml +from tests.integration_tests.clouds import ImageSpecification from tests.integration_tests.instances import IntegrationInstance from tests.integration_tests.util import verify_clean_log @@ -52,11 +53,41 @@ def test_lxd_datasource_discovery(client: IntegrationInstance): assert "lxd" == v1["platform"] assert "LXD socket API v. 1.0 (/dev/lxd/sock)" == v1["subplatform"] ds_cfg = json.loads(client.execute('cloud-init query ds').stdout) - assert ["config", "meta_data"] == sorted(list(ds_cfg["1.0"].keys())) - assert ["user.meta_data"] == list(ds_cfg["1.0"]["config"].keys()) + assert ["_doc", "_metadata_api_version", "config", "meta-data"] == sorted( + list(ds_cfg.keys()) + ) + if ( + client.settings.PLATFORM == "lxd_vm" and + ImageSpecification.from_os_image().release in ("xenial", "bionic") + ): + # pycloudlib injects user.vendor_data for lxd_vm on bionic and xenial + # to start the lxd-agent. + # https://github.com/canonical/pycloudlib/blob/main/pycloudlib/\ + # lxd/defaults.py#L13-L27 + # Underscore-delimited aliases exist for any keys containing hyphens or + # dots. + lxd_config_keys = ["user.meta-data", "user.vendor-data"] + else: + lxd_config_keys = ["user.meta-data"] + assert "1.0" == ds_cfg["_metadata_api_version"] + assert lxd_config_keys == list(ds_cfg["config"].keys()) assert {"public-keys": v1["public_ssh_keys"][0]} == ( - yaml.safe_load(ds_cfg["1.0"]["config"]["user.meta_data"]) + yaml.safe_load(ds_cfg["config"]["user.meta-data"]) + ) + assert ( + "#cloud-config\ninstance-id" in ds_cfg["meta-data"] + ) + # Assert NoCloud seed data is still present in cloud image metadata + # This will start failing if we redact metadata templates from + # https://cloud-images.ubuntu.com/daily/server/jammy/current/\ + # jammy-server-cloudimg-amd64-lxd.tar.xz + nocloud_metadata = yaml.safe_load( + client.read_from_file( + "/var/lib/cloud/seed/nocloud-net/meta-data" + ) ) + assert client.instance.name == nocloud_metadata["instance-id"] assert ( - "#cloud-config\ninstance-id" in ds_cfg["1.0"]["meta_data"] + nocloud_metadata["instance-id"] == nocloud_metadata["local-hostname"] ) + assert v1["public_ssh_keys"][0] == nocloud_metadata["public-keys"] diff --git a/tests/integration_tests/modules/test_apt.py b/tests/integration_tests/modules/test_apt.py index 2c388047..f5f6c813 100644 --- a/tests/integration_tests/modules/test_apt.py +++ b/tests/integration_tests/modules/test_apt.py @@ -19,10 +19,6 @@ apt: Fix-Broken "true"; } } - proxy: "http://proxy.internal:3128" - http_proxy: "http://squid.internal:3128" - ftp_proxy: "ftp://squid.internal:3128" - https_proxy: "https://squid.internal:3128" primary: - arches: [default] uri: http://badarchive.ubuntu.com/ubuntu @@ -38,9 +34,9 @@ apt: deb-src $SECURITY $RELEASE-security multiverse sources: test_keyserver: - keyid: 72600DB15B8E4C8B1964B868038ACC97C660A937 - keyserver: keyserver.ubuntu.com - source: "deb http://ppa.launchpad.net/cloud-init-raharper/curtin-dev/ubuntu $RELEASE main" + keyid: 110E21D8B0E2A1F0243AF6820856F197B892ACEA + keyserver: keyserver.ubuntu.com + source: "deb http://ppa.launchpad.net/canonical-kernel-team/ppa/ubuntu $RELEASE main" test_ppa: keyid: 441614D8 keyserver: keyserver.ubuntu.com @@ -95,15 +91,12 @@ EXPECTED_REGEXES = [ r"deb-src http://badsecurity.ubuntu.com/ubuntu [a-z]+-security multiverse", ] -TEST_KEYSERVER_KEY = "7260 0DB1 5B8E 4C8B 1964 B868 038A CC97 C660 A937" - +TEST_KEYSERVER_KEY = "110E 21D8 B0E2 A1F0 243A F682 0856 F197 B892 ACEA" TEST_PPA_KEY = "3552 C902 B4DD F7BD 3842 1821 015D 28D7 4416 14D8" - TEST_KEY = "1FF0 D853 5EF7 E719 E5C8 1B9C 083D 06FB E4D3 04DF" TEST_SIGNED_BY_KEY = "A2EB 2DEC 0BD7 519B 7B38 BE38 376A 290E C806 8B11" -@pytest.mark.ci @pytest.mark.ubuntu @pytest.mark.user_data(USER_DATA) class TestApt: @@ -148,18 +141,6 @@ class TestApt: assert 'Assume-Yes "true";' in apt_config assert 'Fix-Broken "true";' in apt_config - def test_apt_proxy(self, class_client: IntegrationInstance): - """Test the apt proxy functionality. - - Ported from tests/cloud_tests/testcases/modules/apt_configure_proxy.py - """ - out = class_client.read_from_file( - '/etc/apt/apt.conf.d/90cloud-init-aptproxy') - assert 'Acquire::http::Proxy "http://proxy.internal:3128";' in out - assert 'Acquire::http::Proxy "http://squid.internal:3128";' in out - assert 'Acquire::ftp::Proxy "ftp://squid.internal:3128";' in out - assert 'Acquire::https::Proxy "https://squid.internal:3128";' in out - def test_ppa_source(self, class_client: IntegrationInstance): """Test the apt ppa functionality. @@ -186,7 +167,6 @@ class TestApt: "deb [signed-by=/etc/apt/cloud-init.gpg.d/test_signed_by.gpg] " "http://ppa.launchpad.net/juju/stable/ubuntu" " {} main".format(release)) - print(class_client.execute('cat /var/log/cloud-init.log')) path_contents = class_client.read_from_file( '/etc/apt/sources.list.d/test_signed_by.list') assert path_contents == source @@ -230,7 +210,7 @@ class TestApt: ) assert ( - 'http://ppa.launchpad.net/cloud-init-raharper/curtin-dev/ubuntu' + 'http://ppa.launchpad.net/canonical-kernel-team/ppa/ubuntu' ) in test_keyserver_contents assert TEST_KEYSERVER_KEY in self.get_keys(class_client) @@ -342,3 +322,25 @@ class TestDisabled: '/etc/apt/apt.conf.d/90cloud-init-pipelining' ) assert 'Acquire::http::Pipeline-Depth "0";' in conf + + +APT_PROXY_DATA = """\ +#cloud-config +apt: + proxy: "http://proxy.internal:3128" + http_proxy: "http://squid.internal:3128" + ftp_proxy: "ftp://squid.internal:3128" + https_proxy: "https://squid.internal:3128" +""" + + +@pytest.mark.ubuntu +@pytest.mark.user_data(APT_PROXY_DATA) +def test_apt_proxy(client: IntegrationInstance): + """Test the apt proxy data gets written correctly.""" + out = client.read_from_file( + '/etc/apt/apt.conf.d/90cloud-init-aptproxy') + assert 'Acquire::http::Proxy "http://proxy.internal:3128";' in out + assert 'Acquire::http::Proxy "http://squid.internal:3128";' in out + assert 'Acquire::ftp::Proxy "ftp://squid.internal:3128";' in out + assert 'Acquire::https::Proxy "https://squid.internal:3128";' in out diff --git a/tests/integration_tests/modules/test_combined.py b/tests/integration_tests/modules/test_combined.py index 9cd1648a..26a8397d 100644 --- a/tests/integration_tests/modules/test_combined.py +++ b/tests/integration_tests/modules/test_combined.py @@ -12,6 +12,7 @@ import re from tests.integration_tests.clouds import ImageSpecification from tests.integration_tests.instances import IntegrationInstance from tests.integration_tests.util import ( + retry, verify_clean_log, verify_ordered_items_in_text, ) @@ -33,8 +34,36 @@ locale: en_GB.UTF-8 locale_configfile: /etc/default/locale ntp: servers: ['ntp.ubuntu.com'] +package_update: true +random_seed: + data: 'MYUb34023nD:LFDK10913jk;dfnk:Df' + encoding: raw + file: /root/seed +rsyslog: + configs: + - "*.* @@127.0.0.1" + - filename: 0-basic-config.conf + content: | + module(load="imtcp") + input(type="imtcp" port="514") + $template RemoteLogs,"/var/tmp/rsyslog.log" + *.* ?RemoteLogs + & ~ + remotes: + me: "127.0.0.1" runcmd: - echo 'hello world' > /var/tmp/runcmd_output + + - # + - logger "My test log" +snap: + squashfuse_in_container: true + commands: + - snap install hello-world +ssh_import_id: + - gh:powersj + - lp:smoser +timezone: US/Aleutian """ @@ -45,23 +74,16 @@ class TestCombined: """Test that final_message module works as expected. Also tests LP 1511485: final_message is silent. - - It's possible that if this test is run within a minute or so of - midnight that we'll see a failure because the day in the logs - is different from the day specified in the test definition. """ client = class_client log = client.read_from_file('/var/log/cloud-init.log') - # Get date on host rather than locally as our host could be in a - # wildly different timezone (or more likely recording UTC) - today = client.execute('date "+%a, %d %b %Y"') expected = ( - 'This is my final message!\n' - r'\d+\.\d+.*\n' - '{}.*\n' - 'DataSource.*\n' - r'\d+\.\d+' - ).format(today) + "This is my final message!\n" + r"\d+\.\d+.*\n" + r"\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} \+\d{4}\n" # Datetime + "DataSource.*\n" + r"\d+\.\d+" + ) assert re.search(expected, log) @@ -102,11 +124,70 @@ class TestCombined: 'en_US.UTF-8' ], locale_gen) + def test_random_seed_data(self, class_client: IntegrationInstance): + """Integration test for the random seed module. + + This test specifies a command to be executed by the ``seed_random`` + module, by providing a different data to be used as seed data. We will + then check if that seed data was actually used. + """ + client = class_client + + # Only read the first 31 characters, because the rest could be + # binary data + result = client.execute("head -c 31 < /root/seed") + assert result.startswith("MYUb34023nD:LFDK10913jk;dfnk:Df") + + def test_rsyslog(self, class_client: IntegrationInstance): + """Test rsyslog is configured correctly.""" + client = class_client + assert 'My test log' in client.read_from_file('/var/tmp/rsyslog.log') + def test_runcmd(self, class_client: IntegrationInstance): """Test runcmd works as expected""" client = class_client assert 'hello world' == client.read_from_file('/var/tmp/runcmd_output') + @retry(tries=30, delay=1) + def test_ssh_import_id(self, class_client: IntegrationInstance): + """Integration test for the ssh_import_id module. + + This test specifies ssh keys to be imported by the ``ssh_import_id`` + module and then checks that if the ssh keys were successfully imported. + + TODO: + * This test assumes that SSH keys will be imported into the + /home/ubuntu; this will need modification to run on other OSes. + """ + client = class_client + ssh_output = client.read_from_file( + "/home/ubuntu/.ssh/authorized_keys") + + assert '# ssh-import-id gh:powersj' in ssh_output + assert '# ssh-import-id lp:smoser' in ssh_output + + def test_snap(self, class_client: IntegrationInstance): + """Integration test for the snap module. + + This test specifies a command to be executed by the ``snap`` module + and then checks that if that command was executed during boot. + """ + client = class_client + snap_output = client.execute("snap list") + assert "core " in snap_output + assert "hello-world " in snap_output + + def test_timezone(self, class_client: IntegrationInstance): + """Integration test for the timezone module. + + This test specifies a timezone to be used by the ``timezone`` module + and then checks that if that timezone was respected during boot. + """ + client = class_client + timezone_output = client.execute( + 'date "+%Z" --date="Thu, 03 Nov 2016 00:47:00 -0400"') + assert timezone_output.strip() == "HDT" + def test_no_problems(self, class_client: IntegrationInstance): """Test no errors, warnings, or tracebacks""" client = class_client @@ -121,6 +202,31 @@ class TestCombined: log = client.read_from_file('/var/log/cloud-init.log') verify_clean_log(log) + def test_correct_datasource_detected( + self, class_client: IntegrationInstance + ): + """Test datasource is detected at the proper boot stage.""" + client = class_client + status_file = client.read_from_file("/run/cloud-init/status.json") + + platform_datasources = { + "azure": "DataSourceAzure [seed=/dev/sr0]", + "ec2": "DataSourceEc2Local", + "gce": "DataSourceGCELocal", + "oci": "DataSourceOracle", + "openstack": "DataSourceOpenStackLocal [net,ver=2]", + "lxd_container": ( + "DataSourceNoCloud " + "[seed=/var/lib/cloud/seed/nocloud-net][dsmode=net]" + ), + "lxd_vm": "DataSourceNoCloud [seed=/dev/sr0][dsmode=net]", + } + + assert ( + platform_datasources[client.settings.PLATFORM] + == json.loads(status_file)["v1"]["datasource"] + ) + def _check_common_metadata(self, data): assert data['base64_encoded_keys'] == [] assert data['merged_cfg'] == 'redacted for non-root user' @@ -189,3 +295,19 @@ class TestCombined: assert v1_data['instance_id'] == client.instance.name assert v1_data['local_hostname'].startswith('ip-') assert v1_data['region'] == client.cloud.cloud_instance.region + + @pytest.mark.gce + def test_instance_json_gce(self, class_client: IntegrationInstance): + client = class_client + instance_json_file = client.read_from_file( + "/run/cloud-init/instance-data.json" + ) + data = json.loads(instance_json_file) + self._check_common_metadata(data) + v1_data = data["v1"] + assert v1_data["cloud_name"] == "gce" + assert v1_data["platform"] == "gce" + assert v1_data["subplatform"].startswith("metadata") + assert v1_data["availability_zone"] == client.instance.zone + assert v1_data["instance_id"] == client.instance.instance_id + assert v1_data["local_hostname"] == client.instance.name diff --git a/tests/integration_tests/modules/test_command_output.py b/tests/integration_tests/modules/test_command_output.py index 15033642..8429873f 100644 --- a/tests/integration_tests/modules/test_command_output.py +++ b/tests/integration_tests/modules/test_command_output.py @@ -16,7 +16,6 @@ final_message: "should be last line in cloud-init-test-output file" """ -@pytest.mark.ci @pytest.mark.user_data(USER_DATA) def test_runcmd(client: IntegrationInstance): log = client.read_from_file('/var/log/cloud-init-test-output') diff --git a/tests/integration_tests/modules/test_growpart.py b/tests/integration_tests/modules/test_growpart.py new file mode 100644 index 00000000..af1e3a15 --- /dev/null +++ b/tests/integration_tests/modules/test_growpart.py @@ -0,0 +1,62 @@ +import os +import pytest +import pathlib +import json +from uuid import uuid4 +from pycloudlib.lxd.instance import LXDInstance + +from cloudinit.subp import subp +from tests.integration_tests.instances import IntegrationInstance + +DISK_PATH = '/tmp/test_disk_setup_{}'.format(uuid4()) + + +def setup_and_mount_lxd_disk(instance: LXDInstance): + subp('lxc config device add {} test-disk-setup-disk disk source={}'.format( + instance.name, DISK_PATH).split()) + + +@pytest.fixture(scope='class', autouse=True) +def create_disk(): + """Create 16M sparse file""" + pathlib.Path(DISK_PATH).touch() + os.truncate(DISK_PATH, 1 << 24) + yield + os.remove(DISK_PATH) + + +# Create undersized partition in bootcmd +ALIAS_USERDATA = """\ +#cloud-config +bootcmd: + - parted /dev/sdb --script \ + mklabel gpt \ + mkpart primary 0 1MiB + - parted /dev/sdb --script print +growpart: + devices: + - "/" + - "/dev/sdb1" +runcmd: + - parted /dev/sdb --script print +""" + + +@pytest.mark.user_data(ALIAS_USERDATA) +@pytest.mark.lxd_setup.with_args(setup_and_mount_lxd_disk) +@pytest.mark.ubuntu +@pytest.mark.lxd_vm +class TestGrowPart: + """Test growpart""" + + def test_grow_part(self, client: IntegrationInstance): + """Verify """ + log = client.read_from_file('/var/log/cloud-init.log') + assert ("cc_growpart.py[INFO]: '/dev/sdb1' resized:" + " changed (/dev/sdb, 1) from") in log + + lsblk = json.loads(client.execute('lsblk --json')) + sdb = [x for x in lsblk['blockdevices'] if x['name'] == 'sdb'][0] + assert len(sdb['children']) == 1 + assert sdb['children'][0]['name'] == 'sdb1' + assert sdb['size'] == '16M' diff --git a/tests/integration_tests/modules/test_jinja_templating.py b/tests/integration_tests/modules/test_jinja_templating.py index 35b8ee2d..fe8eff1a 100644 --- a/tests/integration_tests/modules/test_jinja_templating.py +++ b/tests/integration_tests/modules/test_jinja_templating.py @@ -11,6 +11,7 @@ USER_DATA = """\ runcmd: - echo {{v1.local_hostname}} > /var/tmp/runcmd_output - echo {{merged_cfg._doc}} >> /var/tmp/runcmd_output + - echo {{v1['local-hostname']}} >> /var/tmp/runcmd_output """ @@ -18,13 +19,16 @@ runcmd: def test_runcmd_with_variable_substitution(client: IntegrationInstance): """Test jinja substitution. - Ensure we can also substitute variables from instance-data-sensitive - LP: #1931392 + Ensure underscore-delimited aliases exist for hyphenated key and + we can also substitute variables from instance-data-sensitive + LP: #1931392. """ + hostname = client.execute('hostname').stdout.strip() expected = [ - client.execute('hostname').stdout.strip(), + hostname, ('Merged cloud-init system config from /etc/cloud/cloud.cfg and ' - '/etc/cloud/cloud.cfg.d/') + '/etc/cloud/cloud.cfg.d/'), + hostname ] output = client.read_from_file('/var/tmp/runcmd_output') verify_ordered_items_in_text(expected, output) diff --git a/tests/integration_tests/modules/test_keys_to_console.py b/tests/integration_tests/modules/test_keys_to_console.py index 56dff9a0..e79db3c7 100644 --- a/tests/integration_tests/modules/test_keys_to_console.py +++ b/tests/integration_tests/modules/test_keys_to_console.py @@ -4,6 +4,8 @@ ``tests/cloud_tests/testcases/modules/keys_to_console.yaml``.)""" import pytest +from tests.integration_tests.util import retry + BLACKLIST_USER_DATA = """\ #cloud-config ssh_fp_console_blacklist: [ssh-dss, ssh-dsa, ecdsa-sha2-nistp256] @@ -21,6 +23,15 @@ ssh: emit_keys_to_console: false """ +ENABLE_KEYS_TO_CONSOLE_USER_DATA = """\ +#cloud-config +ssh: + emit_keys_to_console: true +users: + - default + - name: barfoo +""" + @pytest.mark.user_data(BLACKLIST_USER_DATA) class TestKeysToConsoleBlacklist: @@ -30,6 +41,9 @@ class TestKeysToConsoleBlacklist: syslog = class_client.read_from_file("/var/log/syslog") assert "({})".format(key_type) not in syslog + # retry decorator here because it can take some time to be reflected + # in syslog + @retry(tries=30, delay=1) @pytest.mark.parametrize("key_type", ["ED25519", "RSA"]) def test_included_keys(self, class_client, key_type): syslog = class_client.read_from_file("/var/log/syslog") @@ -65,3 +79,32 @@ class TestKeysToConsoleDisabled: def test_footer_excluded(self, class_client): syslog = class_client.read_from_file("/var/log/syslog") assert "END SSH HOST KEY FINGERPRINTS" not in syslog + + +@pytest.mark.user_data(ENABLE_KEYS_TO_CONSOLE_USER_DATA) +@pytest.mark.ec2 +@pytest.mark.lxd_container +@pytest.mark.oci +@pytest.mark.openstack +class TestKeysToConsoleEnabled: + """Test that output can be enabled disabled.""" + + def test_duplicate_messaging_console_log(self, class_client): + class_client.execute('cloud-init status --wait --long').ok + try: + console_log = class_client.instance.console_log() + except NotImplementedError: + # Assume that an exception here means that we can't use the console + # log + pytest.skip("NotImplementedError when requesting console log") + return + if console_log.lower() == 'no console output': + # This test retries because we might not have the full console log + # on the first fetch. However, if we have no console output + # at all, we don't want to keep retrying as that would trigger + # another 5 minute wait on the pycloudlib side, which could + # leave us waiting for a couple hours + pytest.fail('no console output') + return + msg = "no authorized SSH keys fingerprints found for user barfoo." + assert 1 == console_log.count(msg) diff --git a/tests/integration_tests/modules/test_ntp_servers.py b/tests/integration_tests/modules/test_ntp_servers.py index 59241faa..c777a641 100644 --- a/tests/integration_tests/modules/test_ntp_servers.py +++ b/tests/integration_tests/modules/test_ntp_servers.py @@ -31,7 +31,6 @@ EXPECTED_SERVERS = yaml.safe_load(USER_DATA)["ntp"]["servers"] EXPECTED_POOLS = yaml.safe_load(USER_DATA)["ntp"]["pools"] -@pytest.mark.ci @pytest.mark.user_data(USER_DATA) class TestNtpServers: @@ -83,7 +82,6 @@ ntp: """ -@pytest.mark.ci @pytest.mark.user_data(CHRONY_DATA) def test_chrony(client: IntegrationInstance): if client.execute('test -f /etc/chrony.conf').ok: @@ -104,7 +102,6 @@ ntp: """ -@pytest.mark.ci @pytest.mark.user_data(TIMESYNCD_DATA) def test_timesyncd(client: IntegrationInstance): contents = client.read_from_file( diff --git a/tests/integration_tests/modules/test_puppet.py b/tests/integration_tests/modules/test_puppet.py new file mode 100644 index 00000000..f40a6ca3 --- /dev/null +++ b/tests/integration_tests/modules/test_puppet.py @@ -0,0 +1,39 @@ +"""Test installation configuration of puppet module.""" +import pytest + +from tests.integration_tests.instances import IntegrationInstance +from tests.integration_tests.util import verify_clean_log + +SERVICE_DATA = """\ +#cloud-config +puppet: + install: true + install_type: packages +""" + + +@pytest.mark.user_data(SERVICE_DATA) +def test_puppet_service(client: IntegrationInstance): + """Basic test that puppet gets installed and runs.""" + log = client.read_from_file('/var/log/cloud-init.log') + verify_clean_log(log) + assert client.execute('systemctl is-active puppet').ok + assert "Running command ['puppet', 'agent'" not in log + + +EXEC_DATA = """\ +#cloud-config +puppet: + install: true + install_type: packages + exec: true + exec_args: ['--noop'] +""" + + +@pytest.mark.user_data +@pytest.mark.user_data(EXEC_DATA) +def test_pupet_exec(client: IntegrationInstance): + """Basic test that puppet gets installed and runs.""" + log = client.read_from_file('/var/log/cloud-init.log') + assert "Running command ['puppet', 'agent', '--noop']" in log diff --git a/tests/integration_tests/modules/test_seed_random_data.py b/tests/integration_tests/modules/test_seed_random_data.py deleted file mode 100644 index 94e982e0..00000000 --- a/tests/integration_tests/modules/test_seed_random_data.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Integration test for the random seed module. - -This test specifies a command to be executed by the ``seed_random`` module, by -providing a different data to be used as seed data. We will then check -if that seed data was actually used. - -(This is ported from -``tests/cloud_tests/testcases/modules/seed_random_data.yaml``.)""" - -import pytest - - -USER_DATA = """\ -#cloud-config -random_seed: - data: 'MYUb34023nD:LFDK10913jk;dfnk:Df' - encoding: raw - file: /root/seed -""" - - -@pytest.mark.ci -class TestSeedRandomData: - - @pytest.mark.user_data(USER_DATA) - def test_seed_random_data(self, client): - # Only read the first 31 characters, because the rest could be - # binary data - result = client.execute("head -c 31 < /root/seed") - assert result.startswith("MYUb34023nD:LFDK10913jk;dfnk:Df") diff --git a/tests/integration_tests/modules/test_snap.py b/tests/integration_tests/modules/test_snap.py deleted file mode 100644 index 652efa68..00000000 --- a/tests/integration_tests/modules/test_snap.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Integration test for the snap module. - -This test specifies a command to be executed by the ``snap`` module -and then checks that if that command was executed during boot. - -(This is ported from -``tests/cloud_tests/testcases/modules/snap.yaml``.)""" - -import pytest - - -USER_DATA = """\ -#cloud-config -package_update: true -snap: - squashfuse_in_container: true - commands: - - snap install hello-world -""" - - -@pytest.mark.ci -@pytest.mark.ubuntu -class TestSnap: - - @pytest.mark.user_data(USER_DATA) - def test_snap(self, client): - snap_output = client.execute("snap list") - assert "core " in snap_output - assert "hello-world " in snap_output diff --git a/tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py b/tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py index e1946cb1..cf14d0b0 100644 --- a/tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py +++ b/tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py @@ -12,6 +12,8 @@ import re import pytest +from tests.integration_tests.util import retry + USER_DATA_SSH_AUTHKEY_DISABLE = """\ #cloud-config @@ -38,6 +40,9 @@ class TestSshAuthkeyFingerprints: "Skipping module named ssh-authkey-fingerprints, " "logging of SSH fingerprints disabled") in cloudinit_output + # retry decorator here because it can take some time to be reflected + # in syslog + @retry(tries=30, delay=1) @pytest.mark.user_data(USER_DATA_SSH_AUTHKEY_ENABLE) def test_ssh_authkey_fingerprints_enable(self, client): syslog_output = client.read_from_file("/var/log/syslog") diff --git a/tests/integration_tests/modules/test_ssh_import_id.py b/tests/integration_tests/modules/test_ssh_import_id.py deleted file mode 100644 index b90fe95f..00000000 --- a/tests/integration_tests/modules/test_ssh_import_id.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Integration test for the ssh_import_id module. - -This test specifies ssh keys to be imported by the ``ssh_import_id`` module -and then checks that if the ssh keys were successfully imported. - -TODO: -* This test assumes that SSH keys will be imported into the /home/ubuntu; this - will need modification to run on other OSes. - -(This is ported from -``tests/cloud_tests/testcases/modules/ssh_import_id.yaml``.)""" - -import pytest - -from tests.integration_tests.util import retry - -USER_DATA = """\ -#cloud-config -ssh_import_id: - - gh:powersj - - lp:smoser -""" - - -@pytest.mark.ci -@pytest.mark.ubuntu -class TestSshImportId: - - @pytest.mark.user_data(USER_DATA) - # Retry is needed here because ssh import id is one of the last modules - # run, and it fires off a web request, then continues with the rest of - # cloud-init. It is possible cloud-init's status is "done" before the - # id's have been fully imported. - @retry(tries=30, delay=1) - def test_ssh_import_id(self, client): - ssh_output = client.read_from_file( - "/home/ubuntu/.ssh/authorized_keys") - - assert '# ssh-import-id gh:powersj' in ssh_output - assert '# ssh-import-id lp:smoser' in ssh_output diff --git a/tests/integration_tests/modules/test_timezone.py b/tests/integration_tests/modules/test_timezone.py deleted file mode 100644 index 111d53f7..00000000 --- a/tests/integration_tests/modules/test_timezone.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Integration test for the timezone module. - -This test specifies a timezone to be used by the ``timezone`` module -and then checks that if that timezone was respected during boot. - -(This is ported from -``tests/cloud_tests/testcases/modules/timezone.yaml``.)""" - -import pytest - - -USER_DATA = """\ -#cloud-config -timezone: US/Aleutian -""" - - -@pytest.mark.ci -class TestTimezone: - - @pytest.mark.user_data(USER_DATA) - def test_timezone(self, client): - timezone_output = client.execute( - 'date "+%Z" --date="Thu, 03 Nov 2016 00:47:00 -0400"') - assert timezone_output.strip() == "HDT" diff --git a/tests/integration_tests/util.py b/tests/integration_tests/util.py index 407096cd..e40d80fe 100644 --- a/tests/integration_tests/util.py +++ b/tests/integration_tests/util.py @@ -52,6 +52,15 @@ def verify_clean_log(log): 'http://169.254.169.254/latest/meta-data/') warning_texts.append(fetch_error_text) traceback_texts.append(fetch_error_text) + # Oracle has a file in /etc/cloud/cloud.cfg.d that contains + # users: + # - default + # - name: opc + # ssh_redirect_user: true + # This can trigger a warning about opc having no public key + warning_texts.append( + 'Unable to disable SSH logins for opc given ssh_redirect_user' + ) for warning_text in warning_texts: expected_warnings += log.count(warning_text) diff --git a/cloudinit/analyze/tests/test_boot.py b/tests/unittests/analyze/test_boot.py index f69423c3..fd878b44 100644 --- a/cloudinit/analyze/tests/test_boot.py +++ b/tests/unittests/analyze/test_boot.py @@ -1,6 +1,6 @@ import os from cloudinit.analyze.__main__ import (analyze_boot, get_parser) -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock from cloudinit.analyze.show import dist_check_timestamp, SystemctlReader, \ FAIL_CODE, CONTAINER_CODE @@ -9,20 +9,11 @@ err_code = (FAIL_CODE, -1, -1, -1) class TestDistroChecker(CiTestCase): - @mock.patch('cloudinit.util.system_info', return_value={'dist': ('', '', - ''), - 'system': ''}) - @mock.patch('cloudinit.util.get_linux_distro', return_value=('', '', '')) - @mock.patch('cloudinit.util.is_FreeBSD', return_value=False) - def test_blank_distro(self, m_sys_info, m_get_linux_distro, m_free_bsd): + def test_blank_distro(self): self.assertEqual(err_code, dist_check_timestamp()) - @mock.patch('cloudinit.util.system_info', return_value={'dist': ('', '', - '')}) - @mock.patch('cloudinit.util.get_linux_distro', return_value=('', '', '')) @mock.patch('cloudinit.util.is_FreeBSD', return_value=True) - def test_freebsd_gentoo_cant_find(self, m_sys_info, - m_get_linux_distro, m_is_FreeBSD): + def test_freebsd_gentoo_cant_find(self, m_is_FreeBSD): self.assertEqual(err_code, dist_check_timestamp()) @mock.patch('cloudinit.subp.subp', return_value=(0, 1)) diff --git a/cloudinit/analyze/tests/test_dump.py b/tests/unittests/analyze/test_dump.py index dac1efb6..e3683bbf 100644 --- a/cloudinit/analyze/tests/test_dump.py +++ b/tests/unittests/analyze/test_dump.py @@ -7,7 +7,7 @@ from cloudinit.analyze.dump import ( dump_events, parse_ci_logline, parse_timestamp) from cloudinit.util import write_file from cloudinit.subp import which -from cloudinit.tests.helpers import CiTestCase, mock, skipIf +from tests.unittests.helpers import CiTestCase, mock, skipIf class TestParseTimestamp(CiTestCase): diff --git a/cloudinit/cmd/devel/tests/__init__.py b/tests/unittests/cloudinit/__init__py index e69de29b..e69de29b 100644 --- a/cloudinit/cmd/devel/tests/__init__.py +++ b/tests/unittests/cloudinit/__init__py diff --git a/cloudinit/cmd/tests/__init__.py b/tests/unittests/cmd/__init__.py index e69de29b..e69de29b 100644 --- a/cloudinit/cmd/tests/__init__.py +++ b/tests/unittests/cmd/__init__.py diff --git a/cloudinit/distros/tests/__init__.py b/tests/unittests/cmd/devel/__init__.py index e69de29b..e69de29b 100644 --- a/cloudinit/distros/tests/__init__.py +++ b/tests/unittests/cmd/devel/__init__.py diff --git a/cloudinit/cmd/devel/tests/test_logs.py b/tests/unittests/cmd/devel/test_logs.py index ddfd58e1..18bdcdda 100644 --- a/cloudinit/cmd/devel/tests/test_logs.py +++ b/tests/unittests/cmd/devel/test_logs.py @@ -6,7 +6,7 @@ from io import StringIO from cloudinit.cmd.devel import logs from cloudinit.sources import INSTANCE_JSON_SENSITIVE_FILE -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( FilesystemMockingTestCase, mock, wrap_and_call) from cloudinit.subp import subp from cloudinit.util import ensure_dir, load_file, write_file diff --git a/cloudinit/cmd/devel/tests/test_render.py b/tests/unittests/cmd/devel/test_render.py index a7fcf2ce..c7ddca3d 100644 --- a/cloudinit/cmd/devel/tests/test_render.py +++ b/tests/unittests/cmd/devel/test_render.py @@ -7,7 +7,7 @@ from collections import namedtuple from cloudinit.cmd.devel import render from cloudinit.helpers import Paths from cloudinit.sources import INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE -from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJinja +from tests.unittests.helpers import CiTestCase, mock, skipUnlessJinja from cloudinit.util import ensure_dir, write_file diff --git a/cloudinit/cmd/tests/test_clean.py b/tests/unittests/cmd/test_clean.py index a848a810..81fc930e 100644 --- a/cloudinit/cmd/tests/test_clean.py +++ b/tests/unittests/cmd/test_clean.py @@ -2,7 +2,7 @@ from cloudinit.cmd import clean from cloudinit.util import ensure_dir, sym_link, write_file -from cloudinit.tests.helpers import CiTestCase, wrap_and_call, mock +from tests.unittests.helpers import CiTestCase, wrap_and_call, mock from collections import namedtuple import os from io import StringIO diff --git a/cloudinit/cmd/tests/test_cloud_id.py b/tests/unittests/cmd/test_cloud_id.py index 3f3727fd..12fc80e8 100644 --- a/cloudinit/cmd/tests/test_cloud_id.py +++ b/tests/unittests/cmd/test_cloud_id.py @@ -8,7 +8,7 @@ from io import StringIO from cloudinit.cmd import cloud_id -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock class TestCloudId(CiTestCase): diff --git a/cloudinit/cmd/tests/test_main.py b/tests/unittests/cmd/test_main.py index 2e380848..e1ce682b 100644 --- a/cloudinit/cmd/tests/test_main.py +++ b/tests/unittests/cmd/test_main.py @@ -12,7 +12,7 @@ from cloudinit.cmd import main from cloudinit import safeyaml from cloudinit.util import ( ensure_dir, load_file, write_file) -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( FilesystemMockingTestCase, wrap_and_call) mypaths = namedtuple('MyPaths', 'run_dir') diff --git a/cloudinit/cmd/tests/test_query.py b/tests/unittests/cmd/test_query.py index c258d321..b3f1d98d 100644 --- a/cloudinit/cmd/tests/test_query.py +++ b/tests/unittests/cmd/test_query.py @@ -13,7 +13,7 @@ from cloudinit.cmd import query from cloudinit.helpers import Paths from cloudinit.sources import ( REDACT_SENSITIVE_VALUE, INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE) -from cloudinit.tests.helpers import mock +from tests.unittests.helpers import mock from cloudinit.util import b64e, write_file @@ -75,6 +75,40 @@ class TestQuery: assert 'usage: query' in out assert 1 == m_cli_log.call_count + @pytest.mark.parametrize( + "inst_data,varname,expected_error", ( + ( + '{"v1": {"key-2": "value-2"}}', + 'v1.absent_leaf', + "instance-data 'v1' has no 'absent_leaf'\n" + ), + ( + '{"v1": {"key-2": "value-2"}}', + 'absent_key', + "Undefined instance-data key 'absent_key'\n" + ), + ) + ) + def test_handle_args_error_on_invalid_vaname_paths( + self, inst_data, varname, expected_error, caplog, tmpdir + ): + """Error when varname is not a valid instance-data variable path.""" + instance_data = tmpdir.join('instance-data') + instance_data.write(inst_data) + args = self.args( + debug=False, dump_all=False, format=None, + instance_data=instance_data.strpath, + list_keys=False, user_data=None, vendor_data=None, varname=varname + ) + paths, _, _, _ = self._setup_paths(tmpdir) + with mock.patch('cloudinit.cmd.query.read_cfg_paths') as m_paths: + m_paths.return_value = paths + with mock.patch( + "cloudinit.cmd.query.addLogHandlerCLI", return_value="" + ): + assert 1 == query.handle_args('anyname', args) + assert expected_error in caplog.text + def test_handle_args_error_on_missing_instance_data(self, caplog, tmpdir): """When instance_data file path does not exist, log an error.""" absent_fn = tmpdir.join('absent') @@ -166,7 +200,7 @@ class TestQuery: assert 0 == query.handle_args('anyname', args) out, _err = capsys.readouterr() cmd_output = json.loads(out) - assert "it worked" == cmd_output['my_var'] + assert "it worked" == cmd_output['my-var'] if ud_expected == "ci-b64:": ud_expected = "ci-b64:{}".format(b64e(ud_src)) if vd_expected == "ci-b64:": @@ -193,8 +227,8 @@ class TestQuery: m_getuid.return_value = 0 assert 0 == query.handle_args('anyname', args) expected = ( - '{\n "my_var": "it worked",\n "userdata": "ud",\n ' - '"vendordata": "vd"\n}\n' + '{\n "my-var": "it worked",\n ' + '"userdata": "ud",\n "vendordata": "vd"\n}\n' ) out, _err = capsys.readouterr() assert expected == out @@ -211,7 +245,7 @@ class TestQuery: m_getuid.return_value = 100 assert 0 == query.handle_args('anyname', args) expected = ( - '{\n "my_var": "it worked",\n "userdata": "<%s> file:ud",\n' + '{\n "my-var": "it worked",\n "userdata": "<%s> file:ud",\n' ' "vendordata": "<%s> file:vd"\n}\n' % ( REDACT_SENSITIVE_VALUE, REDACT_SENSITIVE_VALUE ) @@ -233,21 +267,38 @@ class TestQuery: out, _err = capsys.readouterr() assert 'it worked\n' == out - def test_handle_args_returns_nested_varname(self, capsys, tmpdir): + @pytest.mark.parametrize( + 'inst_data,varname,expected', + ( + ( + '{"v1": {"key-2": "value-2"}, "my-var": "it worked"}', + 'v1.key_2', + 'value-2\n' + ), + # Assert no jinja underscore-delimited aliases are reported on CLI + ( + '{"v1": {"something-hyphenated": {"no.underscores":"x",' + ' "no-alias": "y"}}, "my-var": "it worked"}', + 'v1.something_hyphenated', + '{\n "no-alias": "y",\n "no.underscores": "x"\n}\n' + ), + ) + ) + def test_handle_args_returns_nested_varname( + self, inst_data, varname, expected, capsys, tmpdir + ): """If user_data file is a jinja template render instance-data vars.""" instance_data = tmpdir.join('instance-data') - instance_data.write( - '{"v1": {"key-2": "value-2"}, "my-var": "it worked"}' - ) + instance_data.write(inst_data) args = self.args( debug=False, dump_all=False, format=None, instance_data=instance_data.strpath, user_data='ud', - vendor_data='vd', list_keys=False, varname='v1.key_2') + vendor_data='vd', list_keys=False, varname=varname) with mock.patch('os.getuid') as m_getuid: m_getuid.return_value = 100 assert 0 == query.handle_args('anyname', args) out, _err = capsys.readouterr() - assert 'value-2\n' == out + assert expected == out def test_handle_args_returns_standardized_vars_to_top_level_aliases( self, capsys, tmpdir diff --git a/cloudinit/cmd/tests/test_status.py b/tests/unittests/cmd/test_status.py index 1c9eec37..49eae043 100644 --- a/cloudinit/cmd/tests/test_status.py +++ b/tests/unittests/cmd/test_status.py @@ -8,7 +8,7 @@ from textwrap import dedent from cloudinit.atomic_helper import write_json from cloudinit.cmd import status from cloudinit.util import ensure_file -from cloudinit.tests.helpers import CiTestCase, wrap_and_call, mock +from tests.unittests.helpers import CiTestCase, wrap_and_call, mock mypaths = namedtuple('MyPaths', 'run_dir') myargs = namedtuple('MyArgs', 'long wait') diff --git a/cloudinit/net/tests/__init__.py b/tests/unittests/config/__init__.py index e69de29b..e69de29b 100644 --- a/cloudinit/net/tests/__init__.py +++ b/tests/unittests/config/__init__.py diff --git a/tests/unittests/test_handler/test_handler_apt_conf_v1.py b/tests/unittests/config/test_apt_conf_v1.py index 6a4b03ee..98d99945 100644 --- a/tests/unittests/test_handler/test_handler_apt_conf_v1.py +++ b/tests/unittests/config/test_apt_conf_v1.py @@ -3,7 +3,7 @@ from cloudinit.config import cc_apt_configure from cloudinit import util -from cloudinit.tests.helpers import TestCase +from tests.unittests.helpers import TestCase import copy import os diff --git a/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py b/tests/unittests/config/test_apt_configure_sources_list_v1.py index d69916f9..4aeaea24 100644 --- a/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py +++ b/tests/unittests/config/test_apt_configure_sources_list_v1.py @@ -17,7 +17,7 @@ from cloudinit.config import cc_apt_configure from cloudinit.distros.debian import Distro -from cloudinit.tests import helpers as t_help +from tests.unittests import helpers as t_help from tests.unittests.util import get_cloud LOG = logging.getLogger(__name__) diff --git a/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py b/tests/unittests/config/test_apt_configure_sources_list_v3.py index cd6f9239..a8087bd1 100644 --- a/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py +++ b/tests/unittests/config/test_apt_configure_sources_list_v3.py @@ -15,7 +15,7 @@ from cloudinit import subp from cloudinit import util from cloudinit.config import cc_apt_configure from cloudinit.distros.debian import Distro -from cloudinit.tests import helpers as t_help +from tests.unittests import helpers as t_help from tests.unittests.util import get_cloud diff --git a/tests/unittests/test_handler/test_handler_apt_key.py b/tests/unittests/config/test_apt_key.py index 00e5a38d..00e5a38d 100644 --- a/tests/unittests/test_handler/test_handler_apt_key.py +++ b/tests/unittests/config/test_apt_key.py diff --git a/tests/unittests/test_handler/test_handler_apt_source_v1.py b/tests/unittests/config/test_apt_source_v1.py index 2357d699..684c2495 100644 --- a/tests/unittests/test_handler/test_handler_apt_source_v1.py +++ b/tests/unittests/config/test_apt_source_v1.py @@ -18,7 +18,7 @@ from cloudinit import gpg from cloudinit import subp from cloudinit import util -from cloudinit.tests.helpers import TestCase +from tests.unittests.helpers import TestCase EXPECTEDKEY = """-----BEGIN PGP PUBLIC KEY BLOCK----- Version: GnuPG v1 diff --git a/tests/unittests/test_handler/test_handler_apt_source_v3.py b/tests/unittests/config/test_apt_source_v3.py index 20289121..0b78037e 100644 --- a/tests/unittests/test_handler/test_handler_apt_source_v3.py +++ b/tests/unittests/config/test_apt_source_v3.py @@ -19,7 +19,7 @@ from cloudinit import gpg from cloudinit import subp from cloudinit import util from cloudinit.config import cc_apt_configure -from cloudinit.tests import helpers as t_help +from tests.unittests import helpers as t_help from tests.unittests.util import get_cloud diff --git a/tests/unittests/test_handler/test_handler_apk_configure.py b/tests/unittests/config/test_cc_apk_configure.py index 8acc0b33..70139451 100644 --- a/tests/unittests/test_handler/test_handler_apk_configure.py +++ b/tests/unittests/config/test_cc_apk_configure.py @@ -11,7 +11,7 @@ import textwrap from cloudinit import (cloud, helpers, util) from cloudinit.config import cc_apk_configure -from cloudinit.tests.helpers import (FilesystemMockingTestCase, mock) +from tests.unittests.helpers import (FilesystemMockingTestCase, mock) REPO_FILE = "/etc/apk/repositories" DEFAULT_MIRROR_URL = "https://alpine.global.ssl.fastly.net/alpine" diff --git a/cloudinit/config/tests/test_apt_pipelining.py b/tests/unittests/config/test_cc_apt_pipelining.py index 2a6bb10b..d7589d35 100644 --- a/cloudinit/config/tests/test_apt_pipelining.py +++ b/tests/unittests/config/test_cc_apt_pipelining.py @@ -4,7 +4,7 @@ import cloudinit.config.cc_apt_pipelining as cc_apt_pipelining -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock class TestAptPipelining(CiTestCase): diff --git a/tests/unittests/test_handler/test_handler_bootcmd.py b/tests/unittests/config/test_cc_bootcmd.py index 8cd3a5e1..6f38f12a 100644 --- a/tests/unittests/test_handler/test_handler_bootcmd.py +++ b/tests/unittests/config/test_cc_bootcmd.py @@ -4,7 +4,7 @@ import tempfile from cloudinit.config.cc_bootcmd import handle, schema from cloudinit import (subp, util) -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, mock, SchemaTestCaseMixin, skipUnlessJsonSchema) from tests.unittests.util import get_cloud diff --git a/tests/unittests/test_handler/test_handler_ca_certs.py b/tests/unittests/config/test_cc_ca_certs.py index 2a4ab49e..91b005d0 100644 --- a/tests/unittests/test_handler/test_handler_ca_certs.py +++ b/tests/unittests/config/test_cc_ca_certs.py @@ -11,7 +11,7 @@ from cloudinit.config import cc_ca_certs from cloudinit import helpers from cloudinit import subp from cloudinit import util -from cloudinit.tests.helpers import TestCase +from tests.unittests.helpers import TestCase from tests.unittests.util import get_cloud diff --git a/tests/unittests/test_handler/test_handler_chef.py b/tests/unittests/config/test_cc_chef.py index 0672cebc..060293c8 100644 --- a/tests/unittests/test_handler/test_handler_chef.py +++ b/tests/unittests/config/test_cc_chef.py @@ -8,7 +8,7 @@ import os from cloudinit.config import cc_chef from cloudinit import util -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( HttprettyTestCase, FilesystemMockingTestCase, mock, skipIf) from tests.unittests.util import get_cloud diff --git a/tests/unittests/test_handler/test_handler_debug.py b/tests/unittests/config/test_cc_debug.py index 41e9d9bd..174f772f 100644 --- a/tests/unittests/test_handler/test_handler_debug.py +++ b/tests/unittests/config/test_cc_debug.py @@ -7,7 +7,7 @@ import tempfile from cloudinit import util from cloudinit.config import cc_debug -from cloudinit.tests.helpers import (FilesystemMockingTestCase, mock) +from tests.unittests.helpers import (FilesystemMockingTestCase, mock) from tests.unittests.util import get_cloud diff --git a/cloudinit/config/tests/test_disable_ec2_metadata.py b/tests/unittests/config/test_cc_disable_ec2_metadata.py index b00f2083..7a794845 100644 --- a/cloudinit/config/tests/test_disable_ec2_metadata.py +++ b/tests/unittests/config/test_cc_disable_ec2_metadata.py @@ -4,7 +4,7 @@ import cloudinit.config.cc_disable_ec2_metadata as ec2_meta -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock import logging diff --git a/tests/unittests/test_handler/test_handler_disk_setup.py b/tests/unittests/config/test_cc_disk_setup.py index 4f4a57fa..fa565559 100644 --- a/tests/unittests/test_handler/test_handler_disk_setup.py +++ b/tests/unittests/config/test_cc_disk_setup.py @@ -3,7 +3,7 @@ import random from cloudinit.config import cc_disk_setup -from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, TestCase +from tests.unittests.helpers import CiTestCase, ExitStack, mock, TestCase class TestIsDiskUsed(TestCase): diff --git a/cloudinit/config/tests/test_final_message.py b/tests/unittests/config/test_cc_final_message.py index 46ba99b2..46ba99b2 100644 --- a/cloudinit/config/tests/test_final_message.py +++ b/tests/unittests/config/test_cc_final_message.py diff --git a/tests/unittests/test_handler/test_handler_growpart.py b/tests/unittests/config/test_cc_growpart.py index 7f039b79..b007f24f 100644 --- a/tests/unittests/test_handler/test_handler_growpart.py +++ b/tests/unittests/config/test_cc_growpart.py @@ -3,16 +3,19 @@ from cloudinit import cloud from cloudinit.config import cc_growpart from cloudinit import subp +from cloudinit import temp_utils -from cloudinit.tests.helpers import TestCase +from tests.unittests.helpers import TestCase import errno import logging import os +import shutil import re import unittest from contextlib import ExitStack from unittest import mock +import stat # growpart: # mode: auto # off, on, auto, 'growpart' @@ -58,6 +61,28 @@ usage: gpart add -t type [-a alignment] [-b start] <SNIP> geom """ +class Dir: + '''Stub object''' + def __init__(self, name): + self.name = name + self.st_mode = name + + def is_dir(self, *args, **kwargs): + return True + + def stat(self, *args, **kwargs): + return self + + +class Scanner: + '''Stub object''' + def __enter__(self): + return (Dir(''), Dir(''),) + + def __exit__(self, *args): + pass + + class TestDisabled(unittest.TestCase): def setUp(self): super(TestDisabled, self).setUp() @@ -91,6 +116,13 @@ class TestConfig(TestCase): self.cloud_init = None self.handle = cc_growpart.handle + self.tmppath = '/tmp/cloudinit-test-file' + self.tmpdir = os.scandir('/tmp') + self.tmpfile = open(self.tmppath, 'w') + + def tearDown(self): + self.tmpfile.close() + os.remove(self.tmppath) @mock.patch.dict("os.environ", clear=True) def test_no_resizers_auto_is_fine(self): @@ -130,7 +162,42 @@ class TestConfig(TestCase): mockobj.assert_called_once_with( ['growpart', '--help'], env={'LANG': 'C'}) - @mock.patch.dict("os.environ", clear=True) + @mock.patch.dict("os.environ", {'LANG': 'cs_CZ.UTF-8'}, clear=True) + @mock.patch.object(temp_utils, 'mkdtemp', return_value='/tmp/much-random') + @mock.patch.object(stat, 'S_ISDIR', return_value=False) + @mock.patch.object(os.path, 'samestat', return_value=True) + @mock.patch.object(os.path, "join", return_value='/tmp') + @mock.patch.object(os, 'scandir', return_value=Scanner()) + @mock.patch.object(os, 'mkdir') + @mock.patch.object(os, 'unlink') + @mock.patch.object(os, 'rmdir') + @mock.patch.object(os, 'open', return_value=1) + @mock.patch.object(os, 'close') + @mock.patch.object(shutil, 'rmtree') + @mock.patch.object(os, 'lseek', return_value=1024) + @mock.patch.object(os, 'lstat', return_value='interesting metadata') + def test_force_lang_check_tempfile(self, *args, **kwargs): + with mock.patch.object( + subp, + 'subp', + return_value=(HELP_GROWPART_RESIZE, "")) as mockobj: + + ret = cc_growpart.resizer_factory(mode="auto") + self.assertIsInstance(ret, cc_growpart.ResizeGrowPart) + diskdev = '/dev/sdb' + partnum = 1 + partdev = '/dev/sdb' + ret.resize(diskdev, partnum, partdev) + mockobj.assert_has_calls([ + mock.call( + ["growpart", '--dry-run', diskdev, partnum], + env={'LANG': 'C', 'TMPDIR': '/tmp'}), + mock.call( + ["growpart", diskdev, partnum], + env={'LANG': 'C', 'TMPDIR': '/tmp'}), + ]) + + @mock.patch.dict("os.environ", {'LANG': 'cs_CZ.UTF-8'}, clear=True) def test_mode_auto_falls_back_to_gpart(self): with mock.patch.object( subp, 'subp', diff --git a/cloudinit/config/tests/test_grub_dpkg.py b/tests/unittests/config/test_cc_grub_dpkg.py index 99c05bb5..99c05bb5 100644 --- a/cloudinit/config/tests/test_grub_dpkg.py +++ b/tests/unittests/config/test_cc_grub_dpkg.py diff --git a/tests/unittests/test_handler/test_handler_install_hotplug.py b/tests/unittests/config/test_cc_install_hotplug.py index 5d6b1e77..5d6b1e77 100644 --- a/tests/unittests/test_handler/test_handler_install_hotplug.py +++ b/tests/unittests/config/test_cc_install_hotplug.py diff --git a/cloudinit/config/tests/test_keys_to_console.py b/tests/unittests/config/test_cc_keys_to_console.py index 4083fc54..4083fc54 100644 --- a/cloudinit/config/tests/test_keys_to_console.py +++ b/tests/unittests/config/test_cc_keys_to_console.py diff --git a/tests/unittests/test_handler/test_handler_landscape.py b/tests/unittests/config/test_cc_landscape.py index 00333985..07b3f899 100644 --- a/tests/unittests/test_handler/test_handler_landscape.py +++ b/tests/unittests/config/test_cc_landscape.py @@ -4,7 +4,7 @@ from configobj import ConfigObj from cloudinit.config import cc_landscape from cloudinit import util -from cloudinit.tests.helpers import (FilesystemMockingTestCase, mock, +from tests.unittests.helpers import (FilesystemMockingTestCase, mock, wrap_and_call) from tests.unittests.util import get_cloud @@ -22,6 +22,10 @@ class TestLandscape(FilesystemMockingTestCase): self.conf = self.tmp_path('client.conf', self.new_root) self.default_file = self.tmp_path('default_landscape', self.new_root) self.patchUtils(self.new_root) + self.add_patch( + 'cloudinit.distros.ubuntu.Distro.install_packages', + 'm_install_packages' + ) def test_handler_skips_empty_landscape_cloudconfig(self): """Empty landscape cloud-config section does no work.""" diff --git a/tests/unittests/test_handler/test_handler_locale.py b/tests/unittests/config/test_cc_locale.py index 3c17927e..6cd95a29 100644 --- a/tests/unittests/test_handler/test_handler_locale.py +++ b/tests/unittests/config/test_cc_locale.py @@ -13,7 +13,7 @@ from unittest import mock from cloudinit import util from cloudinit.config import cc_locale -from cloudinit.tests import helpers as t_help +from tests.unittests import helpers as t_help from tests.unittests.util import get_cloud diff --git a/tests/unittests/test_handler/test_handler_lxd.py b/tests/unittests/config/test_cc_lxd.py index ea8b6e90..887987c0 100644 --- a/tests/unittests/test_handler/test_handler_lxd.py +++ b/tests/unittests/config/test_cc_lxd.py @@ -2,7 +2,7 @@ from unittest import mock from cloudinit.config import cc_lxd -from cloudinit.tests import helpers as t_help +from tests.unittests import helpers as t_help from tests.unittests.util import get_cloud diff --git a/tests/unittests/test_handler/test_handler_mcollective.py b/tests/unittests/config/test_cc_mcollective.py index 9cda6fbe..fff777b6 100644 --- a/tests/unittests/test_handler/test_handler_mcollective.py +++ b/tests/unittests/config/test_cc_mcollective.py @@ -8,7 +8,7 @@ from io import BytesIO from cloudinit import (util) from cloudinit.config import cc_mcollective -from cloudinit.tests import helpers as t_help +from tests.unittests import helpers as t_help from tests.unittests.util import get_cloud diff --git a/tests/unittests/test_handler/test_handler_mounts.py b/tests/unittests/config/test_cc_mounts.py index 69e8b30d..fc65f108 100644 --- a/tests/unittests/test_handler/test_handler_mounts.py +++ b/tests/unittests/config/test_cc_mounts.py @@ -1,11 +1,15 @@ # This file is part of cloud-init. See LICENSE file for license information. +import pytest import os.path from unittest import mock +from tests.unittests import helpers as test_helpers from cloudinit.config import cc_mounts +from cloudinit.config.cc_mounts import create_swapfile +from cloudinit.subp import ProcessExecutionError -from cloudinit.tests import helpers as test_helpers +M_PATH = 'cloudinit.config.cc_mounts.' class TestSanitizeDevname(test_helpers.FilesystemMockingTestCase): @@ -403,4 +407,55 @@ class TestFstabHandling(test_helpers.FilesystemMockingTestCase): mock.call(['mount', '-a']), mock.call(['systemctl', 'daemon-reload'])]) + +class TestCreateSwapfile: + + @pytest.mark.parametrize('fstype', ('xfs', 'btrfs', 'ext4', 'other')) + @mock.patch(M_PATH + 'util.get_mount_info') + @mock.patch(M_PATH + 'subp.subp') + def test_happy_path(self, m_subp, m_get_mount_info, fstype, tmpdir): + swap_file = tmpdir.join("swap-file") + fname = str(swap_file) + + # Some of the calls to subp.subp should create the swap file; this + # roughly approximates that + m_subp.side_effect = lambda *args, **kwargs: swap_file.write('') + + m_get_mount_info.return_value = (mock.ANY, fstype) + + create_swapfile(fname, '') + assert mock.call(['mkswap', fname]) in m_subp.call_args_list + + @mock.patch(M_PATH + "util.get_mount_info") + @mock.patch(M_PATH + "subp.subp") + def test_fallback_from_fallocate_to_dd( + self, m_subp, m_get_mount_info, caplog, tmpdir + ): + swap_file = tmpdir.join("swap-file") + fname = str(swap_file) + + def subp_side_effect(cmd, *args, **kwargs): + # Mock fallocate failing, to initiate fallback + if cmd[0] == "fallocate": + raise ProcessExecutionError() + + m_subp.side_effect = subp_side_effect + # Use ext4 so both fallocate and dd are valid swap creation methods + m_get_mount_info.return_value = (mock.ANY, "ext4") + + create_swapfile(fname, "") + + cmds = [args[0][0] for args, _kwargs in m_subp.call_args_list] + assert "fallocate" in cmds, "fallocate was not called" + assert "dd" in cmds, "fallocate failure did not fallback to dd" + + assert cmds.index("dd") > cmds.index( + "fallocate" + ), "dd ran before fallocate" + + assert mock.call(["mkswap", fname]) in m_subp.call_args_list + + msg = "fallocate swap creation failed, will attempt with dd" + assert msg in caplog.text + # vi: ts=4 expandtab diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/config/test_cc_ntp.py index b9e2ba57..3426533a 100644 --- a/tests/unittests/test_handler/test_handler_ntp.py +++ b/tests/unittests/config/test_cc_ntp.py @@ -7,7 +7,7 @@ from os.path import dirname from cloudinit import (helpers, util) from cloudinit.config import cc_ntp -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, FilesystemMockingTestCase, mock, skipUnlessJsonSchema) from tests.unittests.util import get_cloud @@ -37,8 +37,6 @@ class TestNtp(FilesystemMockingTestCase): self.new_root = self.tmp_dir() self.add_patch('cloudinit.util.system_is_snappy', 'm_snappy') self.m_snappy.return_value = False - self.add_patch('cloudinit.util.system_info', 'm_sysinfo') - self.m_sysinfo.return_value = {'dist': ('Distro', '99.1', 'Codename')} self.new_root = self.reRoot() self._get_cloud = partial( get_cloud, @@ -510,35 +508,38 @@ class TestNtp(FilesystemMockingTestCase): self.assertEqual(expected_content, util.load_file(confpath)) - def test_opensuse_picks_chrony(self): + @mock.patch('cloudinit.util.system_info') + def test_opensuse_picks_chrony(self, m_sysinfo): """Test opensuse picks chrony or ntp on certain distro versions""" # < 15.0 => ntp - self.m_sysinfo.return_value = {'dist': - ('openSUSE', '13.2', 'Harlequin')} + m_sysinfo.return_value = { + 'dist': ('openSUSE', '13.2', 'Harlequin') + } mycloud = self._get_cloud('opensuse') expected_client = mycloud.distro.preferred_ntp_clients[0] self.assertEqual('ntp', expected_client) # >= 15.0 and not openSUSE => chrony - self.m_sysinfo.return_value = {'dist': - ('SLES', '15.0', - 'SUSE Linux Enterprise Server 15')} + m_sysinfo.return_value = { + 'dist': ('SLES', '15.0', 'SUSE Linux Enterprise Server 15') + } mycloud = self._get_cloud('sles') expected_client = mycloud.distro.preferred_ntp_clients[0] self.assertEqual('chrony', expected_client) # >= 15.0 and openSUSE and ver != 42 => chrony - self.m_sysinfo.return_value = {'dist': ('openSUSE Tumbleweed', - '20180326', - 'timbleweed')} + m_sysinfo.return_value = { + 'dist': ('openSUSE Tumbleweed', '20180326', 'timbleweed') + } mycloud = self._get_cloud('opensuse') expected_client = mycloud.distro.preferred_ntp_clients[0] self.assertEqual('chrony', expected_client) - def test_ubuntu_xenial_picks_ntp(self): + @mock.patch('cloudinit.util.system_info') + def test_ubuntu_xenial_picks_ntp(self, m_sysinfo): """Test Ubuntu picks ntp on xenial release""" - self.m_sysinfo.return_value = {'dist': ('Ubuntu', '16.04', 'xenial')} + m_sysinfo.return_value = {'dist': ('Ubuntu', '16.04', 'xenial')} mycloud = self._get_cloud('ubuntu') expected_client = mycloud.distro.preferred_ntp_clients[0] self.assertEqual('ntp', expected_client) diff --git a/tests/unittests/test_handler/test_handler_power_state.py b/tests/unittests/config/test_cc_power_state_change.py index 4ac49424..e699f424 100644 --- a/tests/unittests/test_handler/test_handler_power_state.py +++ b/tests/unittests/config/test_cc_power_state_change.py @@ -7,8 +7,8 @@ from cloudinit.config import cc_power_state_change as psc from cloudinit import distros from cloudinit import helpers -from cloudinit.tests import helpers as t_help -from cloudinit.tests.helpers import mock +from tests.unittests import helpers as t_help +from tests.unittests.helpers import mock class TestLoadPowerState(t_help.TestCase): diff --git a/tests/unittests/test_handler/test_handler_puppet.py b/tests/unittests/config/test_cc_puppet.py index 8d99f535..1f67dc4c 100644 --- a/tests/unittests/test_handler/test_handler_puppet.py +++ b/tests/unittests/config/test_cc_puppet.py @@ -4,7 +4,7 @@ import textwrap from cloudinit.config import cc_puppet from cloudinit import util -from cloudinit.tests.helpers import CiTestCase, HttprettyTestCase, mock +from tests.unittests.helpers import CiTestCase, HttprettyTestCase, mock from tests.unittests.util import get_cloud diff --git a/tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py b/tests/unittests/config/test_cc_refresh_rmc_and_interface.py index e13b7793..522de23d 100644 --- a/tests/unittests/test_handler/test_handler_refresh_rmc_and_interface.py +++ b/tests/unittests/config/test_cc_refresh_rmc_and_interface.py @@ -2,8 +2,8 @@ from cloudinit.config import cc_refresh_rmc_and_interface as ccrmci from cloudinit import util -from cloudinit.tests import helpers as t_help -from cloudinit.tests.helpers import mock +from tests.unittests import helpers as t_help +from tests.unittests.helpers import mock from textwrap import dedent import logging diff --git a/tests/unittests/test_handler/test_handler_resizefs.py b/tests/unittests/config/test_cc_resizefs.py index 28d55072..1f9e24da 100644 --- a/tests/unittests/test_handler/test_handler_resizefs.py +++ b/tests/unittests/config/test_cc_resizefs.py @@ -8,7 +8,7 @@ from collections import namedtuple import logging from cloudinit.subp import ProcessExecutionError -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, mock, skipUnlessJsonSchema, util, wrap_and_call) diff --git a/tests/unittests/test_handler/test_handler_resolv_conf.py b/tests/unittests/config/test_cc_resolv_conf.py index 96139001..0aa90a23 100644 --- a/tests/unittests/test_handler/test_handler_resolv_conf.py +++ b/tests/unittests/config/test_cc_resolv_conf.py @@ -1,22 +1,30 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.config import cc_resolv_conf +import logging +import os +import shutil +import tempfile +import pytest +from copy import deepcopy +from unittest import mock from cloudinit import cloud from cloudinit import distros from cloudinit import helpers from cloudinit import util -from copy import deepcopy -from cloudinit.tests import helpers as t_help - -import logging -import os -import shutil -import tempfile -from unittest import mock +from tests.unittests import helpers as t_help +from tests.unittests.util import MockDistro +from cloudinit.config import cc_resolv_conf +from cloudinit.config.cc_resolv_conf import generate_resolv_conf LOG = logging.getLogger(__name__) +EXPECTED_HEADER = """\ +# Your system has been configured with 'manage-resolv-conf' set to true. +# As a result, cloud-init has written this file with configuration data +# that it has been provided. Cloud-init, by default, will write this file +# a single time (PER_ONCE). +#\n\n""" class TestResolvConf(t_help.FilesystemMockingTestCase): @@ -102,4 +110,84 @@ class TestResolvConf(t_help.FilesystemMockingTestCase): mock.call(mock.ANY, '/etc/resolv.conf', mock.ANY) ] not in m_render_to_file.call_args_list + +class TestGenerateResolvConf: + + dist = MockDistro() + tmpl_fn = "templates/resolv.conf.tmpl" + + @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") + def test_dist_resolv_conf_fn(self, m_render_to_file): + self.dist.resolve_conf_fn = "/tmp/resolv-test.conf" + generate_resolv_conf(self.tmpl_fn, + mock.MagicMock(), + self.dist.resolve_conf_fn) + + assert [ + mock.call(mock.ANY, self.dist.resolve_conf_fn, mock.ANY) + ] == m_render_to_file.call_args_list + + @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") + def test_target_fname_is_used_if_passed(self, m_render_to_file): + path = "/use/this/path" + generate_resolv_conf(self.tmpl_fn, mock.MagicMock(), path) + + assert [ + mock.call(mock.ANY, path, mock.ANY) + ] == m_render_to_file.call_args_list + + # Patch in templater so we can assert on the actual generated content + @mock.patch("cloudinit.templater.util.write_file") + # Parameterise with the value to be passed to generate_resolv_conf as the + # params parameter, and the expected line after the header as + # expected_extra_line. + @pytest.mark.parametrize( + "params,expected_extra_line", + [ + # No options + ({}, None), + # Just a true flag + ({"options": {"foo": True}}, "options foo"), + # Just a false flag + ({"options": {"foo": False}}, None), + # Just an option + ({"options": {"foo": "some_value"}}, "options foo:some_value"), + # A true flag and an option + ( + {"options": {"foo": "some_value", "bar": True}}, + "options bar foo:some_value", + ), + # Two options + ( + {"options": {"foo": "some_value", "bar": "other_value"}}, + "options bar:other_value foo:some_value", + ), + # Everything + ( + { + "options": { + "foo": "some_value", + "bar": "other_value", + "baz": False, + "spam": True, + } + }, + "options spam bar:other_value foo:some_value", + ), + ], + ) + def test_flags_and_options( + self, m_write_file, params, expected_extra_line + ): + target_fn = "/etc/resolv.conf" + generate_resolv_conf(self.tmpl_fn, params, target_fn) + + expected_content = EXPECTED_HEADER + if expected_extra_line is not None: + # If we have any extra lines, expect a trailing newline + expected_content += "\n".join([expected_extra_line, ""]) + assert [ + mock.call(mock.ANY, expected_content, mode=mock.ANY) + ] == m_write_file.call_args_list + # vi: ts=4 expandtab diff --git a/tests/unittests/test_rh_subscription.py b/tests/unittests/config/test_cc_rh_subscription.py index 53d3cd5a..bd7ebc98 100644 --- a/tests/unittests/test_rh_subscription.py +++ b/tests/unittests/config/test_cc_rh_subscription.py @@ -8,7 +8,7 @@ import logging from cloudinit.config import cc_rh_subscription from cloudinit import subp -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock SUBMGR = cc_rh_subscription.SubscriptionManager SUB_MAN_CLI = 'cloudinit.config.cc_rh_subscription._sub_man_cli' diff --git a/tests/unittests/test_handler/test_handler_rsyslog.py b/tests/unittests/config/test_cc_rsyslog.py index 8c8e2838..bc147dac 100644 --- a/tests/unittests/test_handler/test_handler_rsyslog.py +++ b/tests/unittests/config/test_cc_rsyslog.py @@ -9,7 +9,7 @@ from cloudinit.config.cc_rsyslog import ( parse_remotes_line, remotes_to_rsyslog_cfg) from cloudinit import util -from cloudinit.tests import helpers as t_help +from tests.unittests import helpers as t_help class TestLoadConfig(t_help.TestCase): diff --git a/tests/unittests/test_handler/test_handler_runcmd.py b/tests/unittests/config/test_cc_runcmd.py index 672e8093..01de6af0 100644 --- a/tests/unittests/test_handler/test_handler_runcmd.py +++ b/tests/unittests/config/test_cc_runcmd.py @@ -6,7 +6,7 @@ from unittest.mock import patch from cloudinit.config.cc_runcmd import handle, schema from cloudinit import (helpers, subp, util) -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, FilesystemMockingTestCase, SchemaTestCaseMixin, skipUnlessJsonSchema) diff --git a/tests/unittests/test_handler/test_handler_seed_random.py b/tests/unittests/config/test_cc_seed_random.py index 2ab153d2..cfd67dce 100644 --- a/tests/unittests/test_handler/test_handler_seed_random.py +++ b/tests/unittests/config/test_cc_seed_random.py @@ -15,7 +15,7 @@ from io import BytesIO from cloudinit import subp from cloudinit import util from cloudinit.config import cc_seed_random -from cloudinit.tests import helpers as t_help +from tests.unittests import helpers as t_help from tests.unittests.util import get_cloud diff --git a/tests/unittests/test_handler/test_handler_set_hostname.py b/tests/unittests/config/test_cc_set_hostname.py index 1a524c7d..b9a783a7 100644 --- a/tests/unittests/test_handler/test_handler_set_hostname.py +++ b/tests/unittests/config/test_cc_set_hostname.py @@ -7,7 +7,7 @@ from cloudinit import distros from cloudinit import helpers from cloudinit import util -from cloudinit.tests import helpers as t_help +from tests.unittests import helpers as t_help from configobj import ConfigObj import logging diff --git a/cloudinit/config/tests/test_set_passwords.py b/tests/unittests/config/test_cc_set_passwords.py index 79118a12..9bcd0439 100644 --- a/cloudinit/config/tests/test_set_passwords.py +++ b/tests/unittests/config/test_cc_set_passwords.py @@ -3,7 +3,7 @@ from unittest import mock from cloudinit.config import cc_set_passwords as setpass -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase from cloudinit import util MODPATH = "cloudinit.config.cc_set_passwords." @@ -79,8 +79,7 @@ class TestSetPasswordsHandle(CiTestCase): 'ssh_pwauth=None\n', self.logs.getvalue()) - @mock.patch(MODPATH + "subp.subp") - def test_handle_on_chpasswd_list_parses_common_hashes(self, m_subp): + def test_handle_on_chpasswd_list_parses_common_hashes(self): """handle parses command password hashes.""" cloud = self.tmp_cloud(distro='ubuntu') valid_hashed_pwds = [ @@ -89,7 +88,7 @@ class TestSetPasswordsHandle(CiTestCase): 'ubuntu:$6$5hOurLPO$naywm3Ce0UlmZg9gG2Fl9acWCVEoakMMC7dR52q' 'SDexZbrN9z8yHxhUM2b.sxpguSwOlbOQSW/HpXazGGx3oo1'] cfg = {'chpasswd': {'list': valid_hashed_pwds}} - with mock.patch(MODPATH + 'subp.subp') as m_subp: + with mock.patch.object(setpass, 'chpasswd') as chpasswd: setpass.handle( 'IGNORED', cfg=cfg, cloud=cloud, log=self.logger, args=[]) self.assertIn( @@ -98,10 +97,9 @@ class TestSetPasswordsHandle(CiTestCase): self.assertIn( "DEBUG: Setting hashed password for ['root', 'ubuntu']", self.logs.getvalue()) - self.assertEqual( - [mock.call(['chpasswd', '-e'], - '\n'.join(valid_hashed_pwds) + '\n')], - m_subp.call_args_list) + valid = '\n'.join(valid_hashed_pwds) + '\n' + called = chpasswd.call_args[0][1] + self.assertEqual(valid, called) @mock.patch(MODPATH + "util.is_BSD") @mock.patch(MODPATH + "subp.subp") @@ -121,34 +119,28 @@ class TestSetPasswordsHandle(CiTestCase): m_subp.call_args_list) @mock.patch(MODPATH + "util.multi_log") - @mock.patch(MODPATH + "util.is_BSD") @mock.patch(MODPATH + "subp.subp") def test_handle_on_chpasswd_list_creates_random_passwords( - self, m_subp, m_is_bsd, m_multi_log + self, m_subp, m_multi_log ): """handle parses command set random passwords.""" - m_is_bsd.return_value = False cloud = self.tmp_cloud(distro='ubuntu') valid_random_pwds = [ 'root:R', 'ubuntu:RANDOM'] cfg = {'chpasswd': {'expire': 'false', 'list': valid_random_pwds}} - with mock.patch(MODPATH + 'subp.subp') as m_subp: + with mock.patch.object(setpass, 'chpasswd') as chpasswd: setpass.handle( 'IGNORED', cfg=cfg, cloud=cloud, log=self.logger, args=[]) self.assertIn( 'DEBUG: Handling input for chpasswd as list.', self.logs.getvalue()) - - self.assertEqual(1, m_subp.call_count) - args, _kwargs = m_subp.call_args - self.assertEqual(["chpasswd"], args[0]) - - stdin = args[1] + self.assertEqual(1, chpasswd.call_count) + passwords, _ = chpasswd.call_args user_pass = { user: password for user, password - in (line.split(":") for line in stdin.splitlines()) + in (line.split(":") for line in passwords[1].splitlines()) } self.assertEqual(1, m_multi_log.call_count) diff --git a/cloudinit/config/tests/test_snap.py b/tests/unittests/config/test_cc_snap.py index 6d4c014a..e8113eca 100644 --- a/cloudinit/config/tests/test_snap.py +++ b/tests/unittests/config/test_cc_snap.py @@ -8,7 +8,7 @@ from cloudinit.config.cc_snap import ( run_commands, schema) from cloudinit.config.schema import validate_cloudconfig_schema from cloudinit import util -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, SchemaTestCaseMixin, mock, wrap_and_call, skipUnlessJsonSchema) diff --git a/tests/unittests/test_handler/test_handler_spacewalk.py b/tests/unittests/config/test_cc_spacewalk.py index 26f7648f..96efccf0 100644 --- a/tests/unittests/test_handler/test_handler_spacewalk.py +++ b/tests/unittests/config/test_cc_spacewalk.py @@ -3,7 +3,7 @@ from cloudinit.config import cc_spacewalk from cloudinit import subp -from cloudinit.tests import helpers +from tests.unittests import helpers import logging from unittest import mock diff --git a/cloudinit/config/tests/test_ssh.py b/tests/unittests/config/test_cc_ssh.py index 87ccdb60..ba179bbf 100644 --- a/cloudinit/config/tests/test_ssh.py +++ b/tests/unittests/config/test_cc_ssh.py @@ -4,7 +4,7 @@ import os.path from cloudinit.config import cc_ssh from cloudinit import ssh_util -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock import logging LOG = logging.getLogger(__name__) diff --git a/tests/unittests/test_handler/test_handler_timezone.py b/tests/unittests/config/test_cc_timezone.py index 77cdb0c2..fb6aab5f 100644 --- a/tests/unittests/test_handler/test_handler_timezone.py +++ b/tests/unittests/config/test_cc_timezone.py @@ -15,7 +15,7 @@ import tempfile from configobj import ConfigObj from io import BytesIO -from cloudinit.tests import helpers as t_help +from tests.unittests import helpers as t_help from tests.unittests.util import get_cloud diff --git a/cloudinit/config/tests/test_ubuntu_advantage.py b/tests/unittests/config/test_cc_ubuntu_advantage.py index db7fb726..8d0c9665 100644 --- a/cloudinit/config/tests/test_ubuntu_advantage.py +++ b/tests/unittests/config/test_cc_ubuntu_advantage.py @@ -4,7 +4,7 @@ from cloudinit.config.cc_ubuntu_advantage import ( configure_ua, handle, maybe_install_ua_tools, schema) from cloudinit.config.schema import validate_cloudconfig_schema from cloudinit import subp -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, mock, SchemaTestCaseMixin, skipUnlessJsonSchema) diff --git a/cloudinit/config/tests/test_ubuntu_drivers.py b/tests/unittests/config/test_cc_ubuntu_drivers.py index 504ba356..d341fbfd 100644 --- a/cloudinit/config/tests/test_ubuntu_drivers.py +++ b/tests/unittests/config/test_cc_ubuntu_drivers.py @@ -3,7 +3,7 @@ import copy import os -from cloudinit.tests.helpers import CiTestCase, skipUnlessJsonSchema, mock +from tests.unittests.helpers import CiTestCase, skipUnlessJsonSchema, mock from cloudinit.config.schema import ( SchemaValidationError, validate_cloudconfig_schema) from cloudinit.config import cc_ubuntu_drivers as drivers diff --git a/tests/unittests/test_handler/test_handler_etc_hosts.py b/tests/unittests/config/test_cc_update_etc_hosts.py index e3778b11..77a7f78f 100644 --- a/tests/unittests/test_handler/test_handler_etc_hosts.py +++ b/tests/unittests/config/test_cc_update_etc_hosts.py @@ -7,7 +7,7 @@ from cloudinit import distros from cloudinit import helpers from cloudinit import util -from cloudinit.tests import helpers as t_help +from tests.unittests import helpers as t_help import logging import os diff --git a/cloudinit/config/tests/test_users_groups.py b/tests/unittests/config/test_cc_users_groups.py index df89ddb3..4ef844cb 100644 --- a/cloudinit/config/tests/test_users_groups.py +++ b/tests/unittests/config/test_cc_users_groups.py @@ -2,7 +2,7 @@ from cloudinit.config import cc_users_groups -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock MODPATH = "cloudinit.config.cc_users_groups" diff --git a/tests/unittests/test_handler/test_handler_write_files.py b/tests/unittests/config/test_cc_write_files.py index 0af92805..99248f74 100644 --- a/tests/unittests/test_handler/test_handler_write_files.py +++ b/tests/unittests/config/test_cc_write_files.py @@ -12,7 +12,7 @@ from cloudinit.config.cc_write_files import ( from cloudinit import log as logging from cloudinit import util -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, FilesystemMockingTestCase, mock, skipUnlessJsonSchema) LOG = logging.getLogger(__name__) diff --git a/tests/unittests/test_handler/test_handler_write_files_deferred.py b/tests/unittests/config/test_cc_write_files_deferred.py index 57b6934a..d33d250a 100644 --- a/tests/unittests/test_handler/test_handler_write_files_deferred.py +++ b/tests/unittests/config/test_cc_write_files_deferred.py @@ -4,11 +4,11 @@ import tempfile import shutil from cloudinit.config.cc_write_files_deferred import (handle) -from .test_handler_write_files import (VALID_SCHEMA) +from .test_cc_write_files import (VALID_SCHEMA) from cloudinit import log as logging from cloudinit import util -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, FilesystemMockingTestCase, mock, skipUnlessJsonSchema) LOG = logging.getLogger(__name__) diff --git a/tests/unittests/test_handler/test_handler_yum_add_repo.py b/tests/unittests/config/test_cc_yum_add_repo.py index 7c61bbf9..2f11b96a 100644 --- a/tests/unittests/test_handler/test_handler_yum_add_repo.py +++ b/tests/unittests/config/test_cc_yum_add_repo.py @@ -7,7 +7,7 @@ import tempfile from cloudinit import util from cloudinit.config import cc_yum_add_repo -from cloudinit.tests import helpers +from tests.unittests import helpers LOG = logging.getLogger(__name__) diff --git a/tests/unittests/test_handler/test_handler_zypper_add_repo.py b/tests/unittests/config/test_cc_zypper_add_repo.py index 0fb1de1a..4af04bee 100644 --- a/tests/unittests/test_handler/test_handler_zypper_add_repo.py +++ b/tests/unittests/config/test_cc_zypper_add_repo.py @@ -7,8 +7,8 @@ import os from cloudinit import util from cloudinit.config import cc_zypper_add_repo -from cloudinit.tests import helpers -from cloudinit.tests.helpers import mock +from tests.unittests import helpers +from tests.unittests.helpers import mock LOG = logging.getLogger(__name__) diff --git a/tests/unittests/test_handler/test_schema.py b/tests/unittests/config/test_schema.py index 1dae223d..b01f5eea 100644 --- a/tests/unittests/test_handler/test_schema.py +++ b/tests/unittests/config/test_schema.py @@ -6,7 +6,7 @@ from cloudinit.config.schema import ( validate_cloudconfig_schema, main) from cloudinit.util import write_file -from cloudinit.tests.helpers import CiTestCase, mock, skipUnlessJsonSchema +from tests.unittests.helpers import CiTestCase, mock, skipUnlessJsonSchema from copy import copy import itertools diff --git a/tests/unittests/test_distros/__init__.py b/tests/unittests/distros/__init__.py index 5394aa56..5394aa56 100644 --- a/tests/unittests/test_distros/__init__.py +++ b/tests/unittests/distros/__init__.py diff --git a/tests/unittests/test_distros/test_arch.py b/tests/unittests/distros/test_arch.py index a95ba3b5..590ba00e 100644 --- a/tests/unittests/test_distros/test_arch.py +++ b/tests/unittests/distros/test_arch.py @@ -3,7 +3,7 @@ from cloudinit.distros.arch import _render_network from cloudinit import util -from cloudinit.tests.helpers import (CiTestCase, dir2dict) +from tests.unittests.helpers import (CiTestCase, dir2dict) from . import _get_distro diff --git a/tests/unittests/test_distros/test_bsd_utils.py b/tests/unittests/distros/test_bsd_utils.py index 3a68f2a9..55686dc9 100644 --- a/tests/unittests/test_distros/test_bsd_utils.py +++ b/tests/unittests/distros/test_bsd_utils.py @@ -2,7 +2,7 @@ import cloudinit.distros.bsd_utils as bsd_utils -from cloudinit.tests.helpers import (CiTestCase, ExitStack, mock) +from tests.unittests.helpers import (CiTestCase, ExitStack, mock) RC_FILE = """ if something; then diff --git a/tests/unittests/test_distros/test_create_users.py b/tests/unittests/distros/test_create_users.py index 685f08ba..5baa8a4b 100644 --- a/tests/unittests/test_distros/test_create_users.py +++ b/tests/unittests/distros/test_create_users.py @@ -4,7 +4,7 @@ import re from cloudinit import distros from cloudinit import ssh_util -from cloudinit.tests.helpers import (CiTestCase, mock) +from tests.unittests.helpers import (CiTestCase, mock) from tests.unittests.util import abstract_to_concrete diff --git a/tests/unittests/test_distros/test_debian.py b/tests/unittests/distros/test_debian.py index 7ff8240b..3d0db145 100644 --- a/tests/unittests/test_distros/test_debian.py +++ b/tests/unittests/distros/test_debian.py @@ -1,8 +1,16 @@ # This file is part of cloud-init. See LICENSE file for license information. +from itertools import count, cycle +from unittest import mock -from cloudinit import distros -from cloudinit import util -from cloudinit.tests.helpers import (FilesystemMockingTestCase, mock) +import pytest + +from cloudinit import distros, util +from cloudinit.distros.debian import ( + APT_GET_COMMAND, + APT_GET_WRAPPER, +) +from tests.unittests.helpers import FilesystemMockingTestCase +from cloudinit import subp @mock.patch("cloudinit.distros.debian.subp.subp") @@ -98,3 +106,69 @@ class TestDebianApplyLocale(FilesystemMockingTestCase): m_subp.assert_not_called() self.assertEqual( 'Failed to provide locale value.', str(ctext_m.exception)) + + +@mock.patch.dict('os.environ', {}, clear=True) +@mock.patch("cloudinit.distros.debian.subp.which", return_value=True) +@mock.patch("cloudinit.distros.debian.subp.subp") +class TestPackageCommand: + distro = distros.fetch("debian")("debian", {}, None) + + @mock.patch("cloudinit.distros.debian.Distro._apt_lock_available", + return_value=True) + def test_simple_command(self, m_apt_avail, m_subp, m_which): + self.distro.package_command('update') + apt_args = [APT_GET_WRAPPER['command']] + apt_args.extend(APT_GET_COMMAND) + apt_args.append('update') + expected_call = { + 'args': apt_args, + 'capture': False, + 'env': {'DEBIAN_FRONTEND': 'noninteractive'}, + } + assert m_subp.call_args == mock.call(**expected_call) + + @mock.patch("cloudinit.distros.debian.Distro._apt_lock_available", + side_effect=[False, False, True]) + @mock.patch("cloudinit.distros.debian.time.sleep") + def test_wait_for_lock(self, m_sleep, m_apt_avail, m_subp, m_which): + self.distro._wait_for_apt_command("stub", {"args": "stub2"}) + assert m_sleep.call_args_list == [mock.call(1), mock.call(1)] + assert m_subp.call_args_list == [mock.call(args='stub2')] + + @mock.patch("cloudinit.distros.debian.Distro._apt_lock_available", + return_value=False) + @mock.patch("cloudinit.distros.debian.time.sleep") + @mock.patch("cloudinit.distros.debian.time.time", side_effect=count()) + def test_lock_wait_timeout( + self, m_time, m_sleep, m_apt_avail, m_subp, m_which + ): + with pytest.raises(TimeoutError): + self.distro._wait_for_apt_command("stub", "stub2", timeout=5) + assert m_subp.call_args_list == [] + + @mock.patch("cloudinit.distros.debian.Distro._apt_lock_available", + side_effect=cycle([True, False])) + @mock.patch("cloudinit.distros.debian.time.sleep") + def test_lock_exception_wait(self, m_sleep, m_apt_avail, m_subp, m_which): + exception = subp.ProcessExecutionError( + exit_code=100, stderr="Could not get apt lock" + ) + m_subp.side_effect = [exception, exception, "return_thing"] + ret = self.distro._wait_for_apt_command("stub", {"args": "stub2"}) + assert ret == "return_thing" + + @mock.patch("cloudinit.distros.debian.Distro._apt_lock_available", + side_effect=cycle([True, False])) + @mock.patch("cloudinit.distros.debian.time.sleep") + @mock.patch("cloudinit.distros.debian.time.time", side_effect=count()) + def test_lock_exception_timeout( + self, m_time, m_sleep, m_apt_avail, m_subp, m_which + ): + m_subp.side_effect = subp.ProcessExecutionError( + exit_code=100, stderr="Could not get apt lock" + ) + with pytest.raises(TimeoutError): + self.distro._wait_for_apt_command( + "stub", {"args": "stub2"}, timeout=5 + ) diff --git a/tests/unittests/test_distros/test_dragonflybsd.py b/tests/unittests/distros/test_dragonflybsd.py index df2c00f4..f0cd1b24 100644 --- a/tests/unittests/test_distros/test_dragonflybsd.py +++ b/tests/unittests/distros/test_dragonflybsd.py @@ -2,7 +2,7 @@ import cloudinit.util -from cloudinit.tests.helpers import mock +from tests.unittests.helpers import mock def test_find_dragonflybsd_part(): diff --git a/tests/unittests/test_distros/test_freebsd.py b/tests/unittests/distros/test_freebsd.py index be565b04..0279e86f 100644 --- a/tests/unittests/test_distros/test_freebsd.py +++ b/tests/unittests/distros/test_freebsd.py @@ -1,7 +1,7 @@ # This file is part of cloud-init. See LICENSE file for license information. from cloudinit.util import (find_freebsd_part, get_path_dev_freebsd) -from cloudinit.tests.helpers import (CiTestCase, mock) +from tests.unittests.helpers import (CiTestCase, mock) import os diff --git a/tests/unittests/test_distros/test_generic.py b/tests/unittests/distros/test_generic.py index 336150bc..e542c26f 100644 --- a/tests/unittests/test_distros/test_generic.py +++ b/tests/unittests/distros/test_generic.py @@ -3,7 +3,7 @@ from cloudinit import distros from cloudinit import util -from cloudinit.tests import helpers +from tests.unittests import helpers import os import pytest diff --git a/tests/unittests/test_distros/test_gentoo.py b/tests/unittests/distros/test_gentoo.py index 37a4f51f..4e4680b8 100644 --- a/tests/unittests/test_distros/test_gentoo.py +++ b/tests/unittests/distros/test_gentoo.py @@ -2,7 +2,7 @@ from cloudinit import util from cloudinit import atomic_helper -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase from . import _get_distro diff --git a/tests/unittests/test_distros/test_hostname.py b/tests/unittests/distros/test_hostname.py index f6d4dbe5..f6d4dbe5 100644 --- a/tests/unittests/test_distros/test_hostname.py +++ b/tests/unittests/distros/test_hostname.py diff --git a/tests/unittests/test_distros/test_hosts.py b/tests/unittests/distros/test_hosts.py index 8aaa6e48..8aaa6e48 100644 --- a/tests/unittests/test_distros/test_hosts.py +++ b/tests/unittests/distros/test_hosts.py diff --git a/cloudinit/distros/tests/test_init.py b/tests/unittests/distros/test_init.py index fd64a322..fd64a322 100644 --- a/cloudinit/distros/tests/test_init.py +++ b/tests/unittests/distros/test_init.py diff --git a/tests/unittests/test_distros/test_manage_service.py b/tests/unittests/distros/test_manage_service.py index 47e7cfb0..6f1bd0b1 100644 --- a/tests/unittests/test_distros/test_manage_service.py +++ b/tests/unittests/distros/test_manage_service.py @@ -1,7 +1,7 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.tests.helpers import (CiTestCase, mock) -from tests.unittests.util import TestingDistro +from tests.unittests.helpers import (CiTestCase, mock) +from tests.unittests.util import MockDistro class TestManageService(CiTestCase): @@ -10,9 +10,9 @@ class TestManageService(CiTestCase): def setUp(self): super(TestManageService, self).setUp() - self.dist = TestingDistro() + self.dist = MockDistro() - @mock.patch.object(TestingDistro, 'uses_systemd', return_value=False) + @mock.patch.object(MockDistro, 'uses_systemd', return_value=False) @mock.patch("cloudinit.distros.subp.subp") def test_manage_service_systemctl_initcmd(self, m_subp, m_sysd): self.dist.init_cmd = ['systemctl'] @@ -20,14 +20,14 @@ class TestManageService(CiTestCase): m_subp.assert_called_with(['systemctl', 'start', 'myssh'], capture=True) - @mock.patch.object(TestingDistro, 'uses_systemd', return_value=False) + @mock.patch.object(MockDistro, 'uses_systemd', return_value=False) @mock.patch("cloudinit.distros.subp.subp") def test_manage_service_service_initcmd(self, m_subp, m_sysd): self.dist.init_cmd = ['service'] self.dist.manage_service('start', 'myssh') m_subp.assert_called_with(['service', 'myssh', 'start'], capture=True) - @mock.patch.object(TestingDistro, 'uses_systemd', return_value=True) + @mock.patch.object(MockDistro, 'uses_systemd', return_value=True) @mock.patch("cloudinit.distros.subp.subp") def test_manage_service_systemctl(self, m_subp, m_sysd): self.dist.init_cmd = ['ignore'] diff --git a/tests/unittests/test_distros/test_netbsd.py b/tests/unittests/distros/test_netbsd.py index 11a68d2a..11a68d2a 100644 --- a/tests/unittests/test_distros/test_netbsd.py +++ b/tests/unittests/distros/test_netbsd.py diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/distros/test_netconfig.py index d09e46af..90ac5578 100644 --- a/tests/unittests/test_distros/test_netconfig.py +++ b/tests/unittests/distros/test_netconfig.py @@ -11,7 +11,7 @@ from cloudinit import distros from cloudinit.distros.parsers.sys_conf import SysConf from cloudinit import helpers from cloudinit import settings -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( FilesystemMockingTestCase, dir2dict) from cloudinit import subp from cloudinit import util @@ -241,8 +241,6 @@ class TestNetCfgDistroBase(FilesystemMockingTestCase): def setUp(self): super(TestNetCfgDistroBase, self).setUp() self.add_patch('cloudinit.util.system_is_snappy', 'm_snappy') - self.add_patch('cloudinit.util.system_info', 'm_sysinfo') - self.m_sysinfo.return_value = {'dist': ('Distro', '99.1', 'Codename')} def _get_distro(self, dname, renderers=None): cls = distros.fetch(dname) @@ -783,7 +781,10 @@ class TestNetCfgDistroArch(TestNetCfgDistroBase): """), } - with mock.patch('cloudinit.util.is_FreeBSD', return_value=False): + with mock.patch( + 'cloudinit.net.netplan.get_devicelist', + return_value=[] + ): self._apply_and_verify(self.distro.apply_network_config, V1_NET_CFG, expected_cfgs=expected_cfgs.copy(), diff --git a/cloudinit/distros/tests/test_networking.py b/tests/unittests/distros/test_networking.py index ec508f4d..ec508f4d 100644 --- a/cloudinit/distros/tests/test_networking.py +++ b/tests/unittests/distros/test_networking.py diff --git a/tests/unittests/test_distros/test_opensuse.py b/tests/unittests/distros/test_opensuse.py index b9bb9b3e..4ff26102 100644 --- a/tests/unittests/test_distros/test_opensuse.py +++ b/tests/unittests/distros/test_opensuse.py @@ -1,6 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase from . import _get_distro diff --git a/tests/unittests/test_distros/test_photon.py b/tests/unittests/distros/test_photon.py index 1c3145ca..3858f723 100644 --- a/tests/unittests/test_distros/test_photon.py +++ b/tests/unittests/distros/test_photon.py @@ -2,8 +2,8 @@ from . import _get_distro from cloudinit import util -from cloudinit.tests.helpers import mock -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import mock +from tests.unittests.helpers import CiTestCase SYSTEM_INFO = { 'paths': { diff --git a/tests/unittests/test_distros/test_resolv.py b/tests/unittests/distros/test_resolv.py index 7d940750..e7971627 100644 --- a/tests/unittests/test_distros/test_resolv.py +++ b/tests/unittests/distros/test_resolv.py @@ -2,7 +2,7 @@ from cloudinit.distros.parsers import resolv_conf -from cloudinit.tests.helpers import TestCase +from tests.unittests.helpers import TestCase import re diff --git a/tests/unittests/test_distros/test_sles.py b/tests/unittests/distros/test_sles.py index 33e3c457..04514a19 100644 --- a/tests/unittests/test_distros/test_sles.py +++ b/tests/unittests/distros/test_sles.py @@ -1,6 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase from . import _get_distro diff --git a/tests/unittests/test_distros/test_sysconfig.py b/tests/unittests/distros/test_sysconfig.py index c1d5b693..4368496d 100644 --- a/tests/unittests/test_distros/test_sysconfig.py +++ b/tests/unittests/distros/test_sysconfig.py @@ -4,7 +4,7 @@ import re from cloudinit.distros.parsers.sys_conf import SysConf -from cloudinit.tests.helpers import TestCase +from tests.unittests.helpers import TestCase # Lots of good examples @ diff --git a/tests/unittests/test_distros/test_user_data_normalize.py b/tests/unittests/distros/test_user_data_normalize.py index fa48410a..bd8f2adb 100644 --- a/tests/unittests/test_distros/test_user_data_normalize.py +++ b/tests/unittests/distros/test_user_data_normalize.py @@ -7,7 +7,7 @@ from cloudinit.distros import ug_util from cloudinit import helpers from cloudinit import settings -from cloudinit.tests.helpers import TestCase +from tests.unittests.helpers import TestCase bcfg = { @@ -26,8 +26,6 @@ class TestUGNormalize(TestCase): def setUp(self): super(TestUGNormalize, self).setUp() self.add_patch('cloudinit.util.system_is_snappy', 'm_snappy') - self.add_patch('cloudinit.util.system_info', 'm_sysinfo') - self.m_sysinfo.return_value = {'dist': ('Distro', '99.1', 'Codename')} def _make_distro(self, dtype, def_user=None): cfg = dict(settings.CFG_BUILTIN) diff --git a/cloudinit/sources/tests/__init__.py b/tests/unittests/filters/__init__.py index e69de29b..e69de29b 100644 --- a/cloudinit/sources/tests/__init__.py +++ b/tests/unittests/filters/__init__.py diff --git a/tests/unittests/test_filters/test_launch_index.py b/tests/unittests/filters/test_launch_index.py index 1492361e..0b1a7067 100644 --- a/tests/unittests/test_filters/test_launch_index.py +++ b/tests/unittests/filters/test_launch_index.py @@ -3,7 +3,7 @@ import copy from itertools import filterfalse -from cloudinit.tests import helpers +from tests.unittests import helpers from cloudinit.filters import launch_index from cloudinit import user_data as ud diff --git a/cloudinit/tests/helpers.py b/tests/unittests/helpers.py index ccd56793..ccd56793 100644 --- a/cloudinit/tests/helpers.py +++ b/tests/unittests/helpers.py diff --git a/cloudinit/tests/__init__.py b/tests/unittests/net/__init__.py index e69de29b..e69de29b 100644 --- a/cloudinit/tests/__init__.py +++ b/tests/unittests/net/__init__.py diff --git a/cloudinit/net/tests/test_dhcp.py b/tests/unittests/net/test_dhcp.py index 28b4ecf7..d3da3981 100644 --- a/cloudinit/net/tests/test_dhcp.py +++ b/tests/unittests/net/test_dhcp.py @@ -11,7 +11,7 @@ from cloudinit.net.dhcp import ( parse_dhcp_lease_file, dhcp_discovery, networkd_load_leases, parse_static_routes) from cloudinit.util import ensure_file, write_file -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, HttprettyTestCase, mock, populate_dir, wrap_and_call) diff --git a/cloudinit/net/tests/test_init.py b/tests/unittests/net/test_init.py index f9102f7b..666e8425 100644 --- a/cloudinit/net/tests/test_init.py +++ b/tests/unittests/net/test_init.py @@ -13,7 +13,7 @@ import requests import cloudinit.net as net from cloudinit import safeyaml as yaml -from cloudinit.tests.helpers import CiTestCase, HttprettyTestCase +from tests.unittests.helpers import CiTestCase, HttprettyTestCase from cloudinit.subp import ProcessExecutionError from cloudinit.util import ensure_file, write_file diff --git a/cloudinit/net/tests/test_network_state.py b/tests/unittests/net/test_network_state.py index 45e99171..fdcd5296 100644 --- a/cloudinit/net/tests/test_network_state.py +++ b/tests/unittests/net/test_network_state.py @@ -6,7 +6,7 @@ import pytest from cloudinit import safeyaml from cloudinit.net import network_state -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase netstate_path = 'cloudinit.net.network_state' diff --git a/tests/unittests/net/test_networkd.py b/tests/unittests/net/test_networkd.py new file mode 100644 index 00000000..8dc90b48 --- /dev/null +++ b/tests/unittests/net/test_networkd.py @@ -0,0 +1,64 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit import safeyaml +from cloudinit.net import networkd, network_state + +V2_CONFIG_SET_NAME = """\ +network: + version: 2 + ethernets: + eth0: + match: + macaddress: '00:11:22:33:44:55' + nameservers: + search: [spam.local, eggs.local] + addresses: [8.8.8.8] + eth1: + match: + macaddress: '66:77:88:99:00:11' + set-name: "ens92" + nameservers: + search: [foo.local, bar.local] + addresses: [4.4.4.4] +""" + +V2_CONFIG_SET_NAME_RENDERED_ETH0 = """[Match] +MACAddress=00:11:22:33:44:55 +Name=eth0 + +[Network] +DHCP=no +DNS=8.8.8.8 +Domains=spam.local eggs.local + +""" + +V2_CONFIG_SET_NAME_RENDERED_ETH1 = """[Match] +MACAddress=66:77:88:99:00:11 +Name=ens92 + +[Network] +DHCP=no +DNS=4.4.4.4 +Domains=foo.local bar.local + +""" + + +class TestNetworkdRenderState: + def _parse_network_state_from_config(self, config): + yaml = safeyaml.load(config) + return network_state.parse_net_config_data(yaml["network"]) + + def test_networkd_render_with_set_name(self): + ns = self._parse_network_state_from_config(V2_CONFIG_SET_NAME) + renderer = networkd.Renderer() + rendered_content = renderer._render_content(ns) + + assert "eth0" in rendered_content + assert rendered_content["eth0"] == V2_CONFIG_SET_NAME_RENDERED_ETH0 + assert "ens92" in rendered_content + assert rendered_content["ens92"] == V2_CONFIG_SET_NAME_RENDERED_ETH1 + + +# vi: ts=4 expandtab diff --git a/tests/unittests/test_datasource/__init__.py b/tests/unittests/runs/__init__.py index e69de29b..e69de29b 100644 --- a/tests/unittests/test_datasource/__init__.py +++ b/tests/unittests/runs/__init__.py diff --git a/tests/unittests/test_runs/test_merge_run.py b/tests/unittests/runs/test_merge_run.py index ff27a280..29439c8a 100644 --- a/tests/unittests/test_runs/test_merge_run.py +++ b/tests/unittests/runs/test_merge_run.py @@ -4,7 +4,7 @@ import os import shutil import tempfile -from cloudinit.tests import helpers +from tests.unittests import helpers from cloudinit.settings import PER_INSTANCE from cloudinit import safeyaml diff --git a/tests/unittests/test_runs/test_simple_run.py b/tests/unittests/runs/test_simple_run.py index cb3aae60..aa78dda3 100644 --- a/tests/unittests/test_runs/test_simple_run.py +++ b/tests/unittests/runs/test_simple_run.py @@ -7,7 +7,7 @@ import os from cloudinit.settings import PER_INSTANCE from cloudinit import safeyaml from cloudinit import stages -from cloudinit.tests import helpers +from tests.unittests import helpers from cloudinit import util diff --git a/tests/unittests/test_filters/__init__.py b/tests/unittests/sources/__init__.py index e69de29b..e69de29b 100644 --- a/tests/unittests/test_filters/__init__.py +++ b/tests/unittests/sources/__init__.py diff --git a/cloudinit/sources/helpers/tests/test_netlink.py b/tests/unittests/sources/helpers/test_netlink.py index cafe3961..478ce375 100644 --- a/cloudinit/sources/helpers/tests/test_netlink.py +++ b/tests/unittests/sources/helpers/test_netlink.py @@ -2,7 +2,7 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock import socket import struct import codecs diff --git a/cloudinit/sources/helpers/tests/test_openstack.py b/tests/unittests/sources/helpers/test_openstack.py index 95fb9743..74743e7c 100644 --- a/cloudinit/sources/helpers/tests/test_openstack.py +++ b/tests/unittests/sources/helpers/test_openstack.py @@ -3,7 +3,7 @@ from unittest import mock from cloudinit.sources.helpers import openstack -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers @mock.patch( diff --git a/tests/unittests/test_datasource/test_aliyun.py b/tests/unittests/sources/test_aliyun.py index cab1ac2b..00209913 100644 --- a/tests/unittests/test_datasource/test_aliyun.py +++ b/tests/unittests/sources/test_aliyun.py @@ -8,7 +8,7 @@ from unittest import mock from cloudinit import helpers from cloudinit.sources import DataSourceAliYun as ay from cloudinit.sources.DataSourceEc2 import convert_ec2_metadata_network_config -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers DEFAULT_METADATA = { 'instance-id': 'aliyun-test-vm-00', diff --git a/tests/unittests/test_datasource/test_altcloud.py b/tests/unittests/sources/test_altcloud.py index 7a5393ac..7384c104 100644 --- a/tests/unittests/test_datasource/test_altcloud.py +++ b/tests/unittests/sources/test_altcloud.py @@ -19,7 +19,7 @@ from cloudinit import helpers from cloudinit import subp from cloudinit import util -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock import cloudinit.sources.DataSourceAltCloud as dsac diff --git a/tests/unittests/test_datasource/test_azure.py b/tests/unittests/sources/test_azure.py index cbc9665d..b221a0d7 100644 --- a/tests/unittests/test_datasource/test_azure.py +++ b/tests/unittests/sources/test_azure.py @@ -8,7 +8,7 @@ from cloudinit.sources import ( from cloudinit.util import (b64e, decode_binary, load_file, write_file, MountFailedError, json_dumps, load_json) from cloudinit.version import version_string as vs -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( HttprettyTestCase, CiTestCase, populate_dir, mock, wrap_and_call, ExitStack, resourceLocation) from cloudinit.sources.helpers import netlink @@ -652,7 +652,7 @@ scbus-1 on xpt0 bus 0 ]) return dsaz - def _get_ds(self, data, agent_command=None, distro='ubuntu', + def _get_ds(self, data, distro='ubuntu', apply_network=None, instance_id=None): def _wait_for_files(flist, _maxwait=None, _naplen=None): @@ -722,8 +722,6 @@ scbus-1 on xpt0 bus 0 distro = distro_cls(distro, data.get('sys_cfg', {}), self.paths) dsrc = dsaz.DataSourceAzure( data.get('sys_cfg', {}), distro=distro, paths=self.paths) - if agent_command is not None: - dsrc.ds_cfg['agent_command'] = agent_command if apply_network is not None: dsrc.ds_cfg['apply_network_config'] = apply_network @@ -921,7 +919,7 @@ scbus-1 on xpt0 bus 0 def test_crawl_metadata_returns_structured_data_and_caches_nothing(self): """Return all structured metadata and cache no class attributes.""" - yaml_cfg = "{agent_command: my_command}\n" + yaml_cfg = "" odata = {'HostName': "myhost", 'UserName': "myuser", 'UserData': {'text': 'FOOBAR', 'encoding': 'plain'}, 'dscfg': {'text': yaml_cfg, 'encoding': 'plain'}} @@ -931,7 +929,7 @@ scbus-1 on xpt0 bus 0 expected_cfg = { 'PreprovisionedVMType': None, 'PreprovisionedVm': False, - 'datasource': {'Azure': {'agent_command': 'my_command'}}, + 'datasource': {'Azure': {}}, 'system_info': {'default_user': {'name': 'myuser'}}} expected_metadata = { 'azure_data': { @@ -1449,19 +1447,16 @@ scbus-1 on xpt0 bus 0 def test_dsaz_report_ready_returns_true_when_report_succeeds( self): dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - dsrc.ds_cfg['agent_command'] = '__builtin__' self.assertTrue(dsrc._report_ready(lease=mock.MagicMock())) def test_dsaz_report_ready_returns_false_and_does_not_propagate_exc( self): dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - dsrc.ds_cfg['agent_command'] = '__builtin__' self.m_get_metadata_from_fabric.side_effect = Exception self.assertFalse(dsrc._report_ready(lease=mock.MagicMock())) def test_dsaz_report_failure_returns_true_when_report_succeeds(self): dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - dsrc.ds_cfg['agent_command'] = '__builtin__' with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata: # mock crawl metadata failure to cause report failure @@ -1475,7 +1470,6 @@ scbus-1 on xpt0 bus 0 def test_dsaz_report_failure_returns_false_and_does_not_propagate_exc( self): dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - dsrc.ds_cfg['agent_command'] = '__builtin__' with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata, \ mock.patch.object(dsrc, '_ephemeral_dhcp_ctx') \ @@ -1505,7 +1499,6 @@ scbus-1 on xpt0 bus 0 def test_dsaz_report_failure_description_msg(self): dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - dsrc.ds_cfg['agent_command'] = '__builtin__' with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata: # mock crawl metadata failure to cause report failure @@ -1518,7 +1511,6 @@ scbus-1 on xpt0 bus 0 def test_dsaz_report_failure_no_description_msg(self): dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - dsrc.ds_cfg['agent_command'] = '__builtin__' with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata: m_crawl_metadata.side_effect = Exception @@ -1529,7 +1521,6 @@ scbus-1 on xpt0 bus 0 def test_dsaz_report_failure_uses_cached_ephemeral_dhcp_ctx_lease(self): dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - dsrc.ds_cfg['agent_command'] = '__builtin__' with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata, \ mock.patch.object(dsrc, '_ephemeral_dhcp_ctx') \ @@ -1558,7 +1549,6 @@ scbus-1 on xpt0 bus 0 def test_dsaz_report_failure_no_net_uses_new_ephemeral_dhcp_lease(self): dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - dsrc.ds_cfg['agent_command'] = '__builtin__' with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata, \ mock.patch.object(dsrc.distro.networking, 'is_up') \ @@ -1584,7 +1574,6 @@ scbus-1 on xpt0 bus 0 def test_dsaz_report_failure_no_net_and_no_dhcp_uses_fallback_lease( self): dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - dsrc.ds_cfg['agent_command'] = '__builtin__' with mock.patch.object(dsrc, 'crawl_metadata') as m_crawl_metadata, \ mock.patch.object(dsrc.distro.networking, 'is_up') \ @@ -1609,14 +1598,12 @@ scbus-1 on xpt0 bus 0 def test_exception_fetching_fabric_data_doesnt_propagate(self): """Errors communicating with fabric should warn, but return True.""" dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - dsrc.ds_cfg['agent_command'] = '__builtin__' self.m_get_metadata_from_fabric.side_effect = Exception ret = self._get_and_setup(dsrc) self.assertTrue(ret) def test_fabric_data_included_in_metadata(self): dsrc = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - dsrc.ds_cfg['agent_command'] = '__builtin__' self.m_get_metadata_from_fabric.return_value = {'test': 'value'} ret = self._get_and_setup(dsrc) self.assertTrue(ret) @@ -1672,7 +1659,6 @@ scbus-1 on xpt0 bus 0 def test_instance_id_from_dmidecode_used_for_builtin(self): ds = self._get_ds({'ovfcontent': construct_valid_ovf_env()}) - ds.ds_cfg['agent_command'] = '__builtin__' ds.get_data() self.assertEqual(self.instance_id, ds.metadata['instance-id']) @@ -1789,9 +1775,8 @@ scbus-1 on xpt0 bus 0 config_driver=True) @mock.patch(MOCKPATH + 'net.get_interfaces', autospec=True) - @mock.patch(MOCKPATH + 'util.is_FreeBSD') def test_blacklist_through_distro( - self, m_is_freebsd, m_net_get_interfaces): + self, m_net_get_interfaces): """Verify Azure DS updates blacklist drivers in the distro's networking object.""" odata = {'HostName': "myhost", 'UserName': "myuser"} @@ -1805,7 +1790,6 @@ scbus-1 on xpt0 bus 0 self.assertEqual(distro.networking.blacklist_drivers, dsaz.BLACKLIST_DRIVERS) - m_is_freebsd.return_value = False distro.networking.get_interfaces_by_mac() m_net_get_interfaces.assert_called_with( blacklist_drivers=dsaz.BLACKLIST_DRIVERS) @@ -2101,13 +2085,11 @@ class TestAzureBounce(CiTestCase): self.patches.close() super(TestAzureBounce, self).tearDown() - def _get_ds(self, ovfcontent=None, agent_command=None): + def _get_ds(self, ovfcontent=None): if ovfcontent is not None: populate_dir(os.path.join(self.paths.seed_dir, "azure"), {'ovf-env.xml': ovfcontent}) dsrc = dsaz.DataSourceAzure({}, distro=mock.Mock(), paths=self.paths) - if agent_command is not None: - dsrc.ds_cfg['agent_command'] = agent_command return dsrc def _get_and_setup(self, dsrc): @@ -2163,8 +2145,7 @@ class TestAzureBounce(CiTestCase): host_name = 'unchanged-host-name' self.get_hostname.return_value = host_name cfg = {'hostname_bounce': {'policy': 'force'}} - dsrc = self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg), - agent_command=['not', '__builtin__']) + dsrc = self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg)) ret = self._get_and_setup(dsrc) self.assertTrue(ret) self.assertEqual(1, perform_hostname_bounce.call_count) @@ -2173,8 +2154,7 @@ class TestAzureBounce(CiTestCase): host_name = 'unchanged-host-name' self.get_hostname.return_value = host_name cfg = {'hostname_bounce': {'policy': 'force'}} - dsrc = self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg), - agent_command=['not', '__builtin__']) + dsrc = self._get_ds(self.get_ovf_env_with_dscfg(host_name, cfg)) patch_path = MOCKPATH + 'subp.which' with mock.patch(patch_path) as m_which: m_which.return_value = None @@ -2189,8 +2169,7 @@ class TestAzureBounce(CiTestCase): expected_hostname = 'azure-expected-host-name' self.get_hostname.return_value = 'default-host-name' dsrc = self._get_ds( - self.get_ovf_env_with_dscfg(expected_hostname, {}), - agent_command=['not', '__builtin__']) + self.get_ovf_env_with_dscfg(expected_hostname, {})) ret = self._get_and_setup(dsrc) self.assertTrue(ret) self.assertEqual(expected_hostname, @@ -2202,8 +2181,7 @@ class TestAzureBounce(CiTestCase): expected_hostname = 'azure-expected-host-name' self.get_hostname.return_value = 'default-host-name' dsrc = self._get_ds( - self.get_ovf_env_with_dscfg(expected_hostname, {}), - agent_command=['not', '__builtin__']) + self.get_ovf_env_with_dscfg(expected_hostname, {})) ret = self._get_and_setup(dsrc) self.assertTrue(ret) self.assertEqual(1, perform_hostname_bounce.call_count) @@ -2212,8 +2190,7 @@ class TestAzureBounce(CiTestCase): initial_host_name = 'default-host-name' self.get_hostname.return_value = initial_host_name dsrc = self._get_ds( - self.get_ovf_env_with_dscfg('some-host-name', {}), - agent_command=['not', '__builtin__']) + self.get_ovf_env_with_dscfg('some-host-name', {})) ret = self._get_and_setup(dsrc) self.assertTrue(ret) self.assertEqual(initial_host_name, @@ -2226,8 +2203,7 @@ class TestAzureBounce(CiTestCase): initial_host_name = 'default-host-name' self.get_hostname.return_value = initial_host_name dsrc = self._get_ds( - self.get_ovf_env_with_dscfg('some-host-name', {}), - agent_command=['not', '__builtin__']) + self.get_ovf_env_with_dscfg('some-host-name', {})) ret = self._get_and_setup(dsrc) self.assertTrue(ret) self.assertEqual(initial_host_name, @@ -2242,7 +2218,7 @@ class TestAzureBounce(CiTestCase): self.get_hostname.return_value = old_hostname cfg = {'hostname_bounce': {'interface': interface, 'policy': 'force'}} data = self.get_ovf_env_with_dscfg(hostname, cfg) - dsrc = self._get_ds(data, agent_command=['not', '__builtin__']) + dsrc = self._get_ds(data) ret = self._get_and_setup(dsrc) self.assertTrue(ret) self.assertEqual(1, self.subp.call_count) @@ -2256,7 +2232,7 @@ class TestAzureBounce(CiTestCase): self, mock_get_boot_telemetry): cfg = {'hostname_bounce': {'policy': 'force'}} data = self.get_ovf_env_with_dscfg('some-hostname', cfg) - dsrc = self._get_ds(data, agent_command=['not', '__builtin__']) + dsrc = self._get_ds(data) ret = self._get_and_setup(dsrc) self.assertTrue(ret) self.assertEqual(1, self.subp.call_count) @@ -3219,7 +3195,6 @@ class TestPreprovisioningPollIMDS(CiTestCase): @mock.patch(MOCKPATH + 'DataSourceAzure._report_ready', mock.MagicMock()) @mock.patch(MOCKPATH + 'subp.subp', mock.MagicMock()) @mock.patch(MOCKPATH + 'util.write_file', mock.MagicMock()) -@mock.patch(MOCKPATH + 'util.is_FreeBSD') @mock.patch('cloudinit.sources.helpers.netlink.' 'wait_for_media_disconnect_connect') @mock.patch('cloudinit.net.dhcp.EphemeralIPv4Network', autospec=True) @@ -3236,10 +3211,8 @@ class TestAzureDataSourcePreprovisioning(CiTestCase): def test_poll_imds_returns_ovf_env(self, m_request, m_dhcp, m_net, - m_media_switch, - m_is_bsd): + m_media_switch): """The _poll_imds method should return the ovf_env.xml.""" - m_is_bsd.return_value = False m_media_switch.return_value = None m_dhcp.return_value = [{ 'interface': 'eth9', 'fixed-address': '192.168.2.9', @@ -3268,10 +3241,8 @@ class TestAzureDataSourcePreprovisioning(CiTestCase): def test__reprovision_calls__poll_imds(self, m_request, m_dhcp, m_net, - m_media_switch, - m_is_bsd): + m_media_switch): """The _reprovision method should call poll IMDS.""" - m_is_bsd.return_value = False m_media_switch.return_value = None m_dhcp.return_value = [{ 'interface': 'eth9', 'fixed-address': '192.168.2.9', diff --git a/tests/unittests/test_datasource/test_azure_helper.py b/tests/unittests/sources/test_azure_helper.py index 552c7905..24c582c2 100644 --- a/tests/unittests/test_datasource/test_azure_helper.py +++ b/tests/unittests/sources/test_azure_helper.py @@ -9,7 +9,7 @@ from xml.etree import ElementTree from xml.sax.saxutils import escape, unescape from cloudinit.sources.helpers import azure as azure_helper -from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, populate_dir +from tests.unittests.helpers import CiTestCase, ExitStack, mock, populate_dir from cloudinit.util import load_file from cloudinit.sources.helpers.azure import WALinuxAgentShim as wa_shim @@ -129,9 +129,7 @@ class TestFindEndpoint(CiTestCase): self.dhcp_options.return_value = {"eth0": {"unknown_245": "5:4:3:2"}} self.assertEqual('5.4.3.2', wa_shim.find_endpoint(None)) - @mock.patch('cloudinit.sources.helpers.azure.util.is_FreeBSD') - def test_latest_lease_used(self, m_is_freebsd): - m_is_freebsd.return_value = False # To avoid hitting load_file + def test_latest_lease_used(self): encoded_addresses = ['5:4:3:2', '4:3:2:1'] file_content = '\n'.join([self._build_lease_content(encoded_address) for encoded_address in encoded_addresses]) diff --git a/tests/unittests/test_datasource/test_cloudsigma.py b/tests/unittests/sources/test_cloudsigma.py index 7aa3b1d1..2eae16ee 100644 --- a/tests/unittests/test_datasource/test_cloudsigma.py +++ b/tests/unittests/sources/test_cloudsigma.py @@ -8,7 +8,7 @@ from cloudinit import helpers from cloudinit import sources from cloudinit.sources import DataSourceCloudSigma -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers SERVER_CONTEXT = { "cpu": 1000, diff --git a/tests/unittests/test_datasource/test_cloudstack.py b/tests/unittests/sources/test_cloudstack.py index e68168f2..2b1a1b70 100644 --- a/tests/unittests/test_datasource/test_cloudstack.py +++ b/tests/unittests/sources/test_cloudstack.py @@ -5,7 +5,7 @@ from cloudinit import util from cloudinit.sources.DataSourceCloudStack import ( DataSourceCloudStack, get_latest_lease) -from cloudinit.tests.helpers import CiTestCase, ExitStack, mock +from tests.unittests.helpers import CiTestCase, ExitStack, mock import os import time diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/sources/test_common.py index 17d53160..bb8fa530 100644 --- a/tests/unittests/test_datasource/test_common.py +++ b/tests/unittests/sources/test_common.py @@ -34,13 +34,14 @@ from cloudinit.sources import ( ) from cloudinit.sources import DataSourceNone as DSNone -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers DEFAULT_LOCAL = [ Azure.DataSourceAzure, CloudSigma.DataSourceCloudSigma, ConfigDrive.DataSourceConfigDrive, DigitalOcean.DataSourceDigitalOcean, + GCE.DataSourceGCELocal, Hetzner.DataSourceHetzner, IBMCloud.DataSourceIBMCloud, LXD.DataSourceLXD, diff --git a/tests/unittests/test_datasource/test_configdrive.py b/tests/unittests/sources/test_configdrive.py index 51097231..775d0622 100644 --- a/tests/unittests/test_datasource/test_configdrive.py +++ b/tests/unittests/sources/test_configdrive.py @@ -12,7 +12,7 @@ from cloudinit.sources import DataSourceConfigDrive as ds from cloudinit.sources.helpers import openstack from cloudinit import util -from cloudinit.tests.helpers import CiTestCase, ExitStack, mock, populate_dir +from tests.unittests.helpers import CiTestCase, ExitStack, mock, populate_dir PUBKEY = 'ssh-rsa AAAAB3NzaC1....sIkJhq8wdX+4I3A4cYbYP ubuntu@server-460\n' @@ -486,11 +486,10 @@ class TestConfigDriveDataSource(CiTestCase): None, helpers.Paths({})) with mock.patch(M_PATH + 'find_candidate_devs') as m_find_devs: - with mock.patch(M_PATH + 'util.is_FreeBSD', return_value=False): - with mock.patch(M_PATH + 'util.mount_cb'): - with mock.patch(M_PATH + 'on_first_boot'): - m_find_devs.return_value = ['/dev/anything'] - self.assertEqual(True, cfg_ds.get_data()) + with mock.patch(M_PATH + 'util.mount_cb'): + with mock.patch(M_PATH + 'on_first_boot'): + m_find_devs.return_value = ['/dev/anything'] + self.assertEqual(True, cfg_ds.get_data()) self.assertEqual('config-disk (/dev/anything)', cfg_ds.subplatform) diff --git a/tests/unittests/test_datasource/test_digitalocean.py b/tests/unittests/sources/test_digitalocean.py index 3127014b..351bf7ba 100644 --- a/tests/unittests/test_datasource/test_digitalocean.py +++ b/tests/unittests/sources/test_digitalocean.py @@ -13,7 +13,7 @@ from cloudinit import settings from cloudinit.sources import DataSourceDigitalOcean from cloudinit.sources.helpers import digitalocean -from cloudinit.tests.helpers import mock, CiTestCase +from tests.unittests.helpers import mock, CiTestCase DO_MULTIPLE_KEYS = ["ssh-rsa AAAAB3NzaC1yc2EAAAA... test1@do.co", "ssh-rsa AAAAB3NzaC1yc2EAAAA... test2@do.co"] diff --git a/tests/unittests/test_datasource/test_ec2.py b/tests/unittests/sources/test_ec2.py index a93f2195..19c2bbcd 100644 --- a/tests/unittests/test_datasource/test_ec2.py +++ b/tests/unittests/sources/test_ec2.py @@ -8,7 +8,7 @@ from unittest import mock from cloudinit import helpers from cloudinit.sources import DataSourceEc2 as ec2 -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers DYNAMIC_METADATA = { diff --git a/tests/unittests/test_datasource/test_exoscale.py b/tests/unittests/sources/test_exoscale.py index f0061199..b0ffb7a5 100644 --- a/tests/unittests/test_datasource/test_exoscale.py +++ b/tests/unittests/sources/test_exoscale.py @@ -10,7 +10,7 @@ from cloudinit.sources.DataSourceExoscale import ( get_password, PASSWORD_SERVER_PORT, read_metadata) -from cloudinit.tests.helpers import HttprettyTestCase, mock +from tests.unittests.helpers import HttprettyTestCase, mock from cloudinit import util import httpretty diff --git a/tests/unittests/test_datasource/test_gce.py b/tests/unittests/sources/test_gce.py index 80b38f9e..dc768e99 100644 --- a/tests/unittests/test_datasource/test_gce.py +++ b/tests/unittests/sources/test_gce.py @@ -18,7 +18,7 @@ from cloudinit import helpers from cloudinit import settings from cloudinit.sources import DataSourceGCE -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers GCE_META = { @@ -360,5 +360,29 @@ class TestDataSourceGCE(test_helpers.HttprettyTestCase): self.ds.publish_host_keys(hostkeys) m_readurl.assert_has_calls(readurl_expected_calls, any_order=True) + @mock.patch( + "cloudinit.sources.DataSourceGCE.EphemeralDHCPv4", + autospec=True, + ) + @mock.patch( + "cloudinit.sources.DataSourceGCE.DataSourceGCELocal.fallback_interface" + ) + def test_local_datasource_uses_ephemeral_dhcp(self, _m_fallback, m_dhcp): + _set_mock_metadata() + ds = DataSourceGCE.DataSourceGCELocal( + sys_cfg={}, distro=None, paths=None + ) + ds._get_data() + assert m_dhcp.call_count == 1 + + @mock.patch( + "cloudinit.sources.DataSourceGCE.EphemeralDHCPv4", + autospec=True, + ) + def test_datasource_doesnt_use_ephemeral_dhcp(self, m_dhcp): + _set_mock_metadata() + ds = DataSourceGCE.DataSourceGCE(sys_cfg={}, distro=None, paths=None) + ds._get_data() + assert m_dhcp.call_count == 0 # vi: ts=4 expandtab diff --git a/tests/unittests/test_datasource/test_hetzner.py b/tests/unittests/sources/test_hetzner.py index eadb92f1..5af0f3db 100644 --- a/tests/unittests/test_datasource/test_hetzner.py +++ b/tests/unittests/sources/test_hetzner.py @@ -8,7 +8,7 @@ from cloudinit.sources import DataSourceHetzner import cloudinit.sources.helpers.hetzner as hc_helper from cloudinit import util, settings, helpers -from cloudinit.tests.helpers import mock, CiTestCase +from tests.unittests.helpers import mock, CiTestCase import base64 import pytest diff --git a/tests/unittests/test_datasource/test_ibmcloud.py b/tests/unittests/sources/test_ibmcloud.py index 9013ae9f..38e8e892 100644 --- a/tests/unittests/test_datasource/test_ibmcloud.py +++ b/tests/unittests/sources/test_ibmcloud.py @@ -2,7 +2,7 @@ from cloudinit.helpers import Paths from cloudinit.sources import DataSourceIBMCloud as ibm -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers from cloudinit import util import base64 diff --git a/cloudinit/sources/tests/test_init.py b/tests/unittests/sources/test_init.py index ae09cb17..a1d19518 100644 --- a/cloudinit/sources/tests/test_init.py +++ b/tests/unittests/sources/test_init.py @@ -12,7 +12,7 @@ from cloudinit.sources import ( EXPERIMENTAL_TEXT, INSTANCE_JSON_FILE, INSTANCE_JSON_SENSITIVE_FILE, METADATA_UNKNOWN, REDACT_SENSITIVE_VALUE, UNSET, DataSource, canonical_cloud_id, redact_sensitive_keys) -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock from cloudinit.user_data import UserDataProcessor from cloudinit import util diff --git a/tests/unittests/sources/test_lxd.py b/tests/unittests/sources/test_lxd.py new file mode 100644 index 00000000..a6e51f3b --- /dev/null +++ b/tests/unittests/sources/test_lxd.py @@ -0,0 +1,376 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from collections import namedtuple +from copy import deepcopy +import json +import re +import stat +from unittest import mock +import yaml + +import pytest + +from cloudinit.sources import ( + DataSourceLXD as lxd, InvalidMetaDataException, UNSET +) +DS_PATH = "cloudinit.sources.DataSourceLXD." + + +LStatResponse = namedtuple("lstatresponse", "st_mode") + + +NETWORK_V1 = { + "version": 1, + "config": [ + { + "type": "physical", "name": "eth0", + "subnets": [{"type": "dhcp", "control": "auto"}] + } + ] +} + + +def _add_network_v1_device(devname) -> dict: + """Helper to inject device name into default network v1 config.""" + network_cfg = deepcopy(NETWORK_V1) + network_cfg["config"][0]["name"] = devname + return network_cfg + + +LXD_V1_METADATA = { + "meta-data": "instance-id: my-lxc\nlocal-hostname: my-lxc\n\n", + "network-config": NETWORK_V1, + "user-data": "#cloud-config\npackages: [sl]\n", + "vendor-data": "#cloud-config\nruncmd: ['echo vendor-data']\n", + "config": { + "user.user-data": + "instance-id: my-lxc\nlocal-hostname: my-lxc\n\n", + "user.vendor-data": + "#cloud-config\nruncmd: ['echo vendor-data']\n", + "user.network-config": yaml.safe_dump(NETWORK_V1), + } +} + + +@pytest.fixture +def lxd_metadata(): + return LXD_V1_METADATA + + +@pytest.yield_fixture +def lxd_ds(request, paths, lxd_metadata): + """ + Return an instantiated DataSourceLXD. + + This also performs the mocking required for the default test case: + * ``is_platform_viable`` returns True, + * ``read_metadata`` returns ``LXD_V1_METADATA`` + + (This uses the paths fixture for the required helpers.Paths object) + """ + with mock.patch(DS_PATH + "is_platform_viable", return_value=True): + with mock.patch(DS_PATH + "read_metadata", return_value=lxd_metadata): + yield lxd.DataSourceLXD( + sys_cfg={}, distro=mock.Mock(), paths=paths + ) + + +class TestGenerateFallbackNetworkConfig: + + @pytest.mark.parametrize( + "uname_machine,systemd_detect_virt,expected", ( + # None for systemd_detect_virt returns None from which + ({}, None, NETWORK_V1), + ({}, None, NETWORK_V1), + ("anything", "lxc\n", NETWORK_V1), + # `uname -m` on kvm determines devname + ("x86_64", "kvm\n", _add_network_v1_device("enp5s0")), + ("ppc64le", "kvm\n", _add_network_v1_device("enp0s5")), + ("s390x", "kvm\n", _add_network_v1_device("enc9")) + ) + ) + @mock.patch(DS_PATH + "util.system_info") + @mock.patch(DS_PATH + "subp.subp") + @mock.patch(DS_PATH + "subp.which") + def test_net_v2_based_on_network_mode_virt_type_and_uname_machine( + self, + m_which, + m_subp, + m_system_info, + uname_machine, + systemd_detect_virt, + expected, + ): + """Return network config v2 based on uname -m, systemd-detect-virt.""" + if systemd_detect_virt is None: + m_which.return_value = None + m_system_info.return_value = {"uname": ["", "", "", "", uname_machine]} + m_subp.return_value = (systemd_detect_virt, "") + assert expected == lxd.generate_fallback_network_config() + if systemd_detect_virt is None: + assert 0 == m_subp.call_count + assert 0 == m_system_info.call_count + else: + assert [ + mock.call(["systemd-detect-virt"]) + ] == m_subp.call_args_list + if systemd_detect_virt != "kvm\n": + assert 0 == m_system_info.call_count + else: + assert 1 == m_system_info.call_count + + +class TestDataSourceLXD: + def test_platform_info(self, lxd_ds): + assert "LXD" == lxd_ds.dsname + assert "lxd" == lxd_ds.cloud_name + assert "lxd" == lxd_ds.platform_type + + def test_subplatform(self, lxd_ds): + assert "LXD socket API v. 1.0 (/dev/lxd/sock)" == lxd_ds.subplatform + + def test__get_data(self, lxd_ds): + """get_data calls read_metadata, setting appropiate instance attrs.""" + assert UNSET == lxd_ds._crawled_metadata + assert UNSET == lxd_ds._network_config + assert None is lxd_ds.userdata_raw + assert True is lxd_ds._get_data() + assert LXD_V1_METADATA == lxd_ds._crawled_metadata + # network-config is dumped from YAML + assert NETWORK_V1 == lxd_ds._network_config + # Any user-data and vendor-data are saved as raw + assert LXD_V1_METADATA["user-data"] == lxd_ds.userdata_raw + assert LXD_V1_METADATA["vendor-data"] == lxd_ds.vendordata_raw + + +class TestIsPlatformViable: + @pytest.mark.parametrize( + "exists,lstat_mode,expected", ( + (False, None, False), + (True, stat.S_IFREG, False), + (True, stat.S_IFSOCK, True), + ) + ) + @mock.patch(DS_PATH + "os.lstat") + @mock.patch(DS_PATH + "os.path.exists") + def test_expected_viable( + self, m_exists, m_lstat, exists, lstat_mode, expected + ): + """Return True only when LXD_SOCKET_PATH exists and is a socket.""" + m_exists.return_value = exists + m_lstat.return_value = LStatResponse(lstat_mode) + assert expected is lxd.is_platform_viable() + m_exists.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)]) + if exists: + m_lstat.assert_has_calls([mock.call(lxd.LXD_SOCKET_PATH)]) + else: + assert 0 == m_lstat.call_count + + +class TestReadMetadata: + @pytest.mark.parametrize( + "url_responses,expected,logs", ( + ( # Assert non-JSON format from config route + { + "http://lxd/1.0/meta-data": "local-hostname: md\n", + "http://lxd/1.0/config": "[NOT_JSON", + }, + InvalidMetaDataException( + "Unable to determine cloud-init config from" + " http://lxd/1.0/config. Expected JSON but found:" + " [NOT_JSON"), + ["[GET] [HTTP:200] http://lxd/1.0/meta-data", + "[GET] [HTTP:200] http://lxd/1.0/config"], + ), + ( # Assert success on just meta-data + { + "http://lxd/1.0/meta-data": "local-hostname: md\n", + "http://lxd/1.0/config": "[]", + }, + { + "_metadata_api_version": lxd.LXD_SOCKET_API_VERSION, + "config": {}, "meta-data": "local-hostname: md\n" + }, + ["[GET] [HTTP:200] http://lxd/1.0/meta-data", + "[GET] [HTTP:200] http://lxd/1.0/config"], + ), + ( # Assert 404s for config routes log skipping + { + "http://lxd/1.0/meta-data": "local-hostname: md\n", + "http://lxd/1.0/config": + '["/1.0/config/user.custom1",' + ' "/1.0/config/user.meta-data",' + ' "/1.0/config/user.network-config",' + ' "/1.0/config/user.user-data",' + ' "/1.0/config/user.vendor-data"]', + "http://lxd/1.0/config/user.custom1": "custom1", + "http://lxd/1.0/config/user.meta-data": "", # 404 + "http://lxd/1.0/config/user.network-config": "net-config", + "http://lxd/1.0/config/user.user-data": "", # 404 + "http://lxd/1.0/config/user.vendor-data": "", # 404 + }, + { + "_metadata_api_version": lxd.LXD_SOCKET_API_VERSION, + "config": { + "user.custom1": "custom1", # Not promoted + "user.network-config": "net-config", + }, + "meta-data": "local-hostname: md\n", + "network-config": "net-config", + }, + [ + "Skipping http://lxd/1.0/config/user.vendor-data on" + " [HTTP:404]", + "Skipping http://lxd/1.0/config/user.meta-data on" + " [HTTP:404]", + "Skipping http://lxd/1.0/config/user.user-data on" + " [HTTP:404]", + "[GET] [HTTP:200] http://lxd/1.0/config", + "[GET] [HTTP:200] http://lxd/1.0/config/user.custom1", + "[GET] [HTTP:200]" + " http://lxd/1.0/config/user.network-config", + ], + ), + ( # Assert all CONFIG_KEY_ALIASES promoted to top-level keys + { + "http://lxd/1.0/meta-data": "local-hostname: md\n", + "http://lxd/1.0/config": + '["/1.0/config/user.custom1",' + ' "/1.0/config/user.meta-data",' + ' "/1.0/config/user.network-config",' + ' "/1.0/config/user.user-data",' + ' "/1.0/config/user.vendor-data"]', + "http://lxd/1.0/config/user.custom1": "custom1", + "http://lxd/1.0/config/user.meta-data": "meta-data", + "http://lxd/1.0/config/user.network-config": "net-config", + "http://lxd/1.0/config/user.user-data": "user-data", + "http://lxd/1.0/config/user.vendor-data": "vendor-data", + }, + { + "_metadata_api_version": lxd.LXD_SOCKET_API_VERSION, + "config": { + "user.custom1": "custom1", # Not promoted + "user.meta-data": "meta-data", + "user.network-config": "net-config", + "user.user-data": "user-data", + "user.vendor-data": "vendor-data", + }, + "meta-data": "local-hostname: md\n", + "network-config": "net-config", + "user-data": "user-data", + "vendor-data": "vendor-data", + }, + [ + "[GET] [HTTP:200] http://lxd/1.0/meta-data", + "[GET] [HTTP:200] http://lxd/1.0/config", + "[GET] [HTTP:200] http://lxd/1.0/config/user.custom1", + "[GET] [HTTP:200] http://lxd/1.0/config/user.meta-data", + "[GET] [HTTP:200]" + " http://lxd/1.0/config/user.network-config", + "[GET] [HTTP:200] http://lxd/1.0/config/user.user-data", + "[GET] [HTTP:200] http://lxd/1.0/config/user.vendor-data", + ], + ), + ( # Assert cloud-init.* config key values prefered over user.* + { + "http://lxd/1.0/meta-data": "local-hostname: md\n", + "http://lxd/1.0/config": + '["/1.0/config/user.meta-data",' + ' "/1.0/config/user.network-config",' + ' "/1.0/config/user.user-data",' + ' "/1.0/config/user.vendor-data",' + ' "/1.0/config/cloud-init.network-config",' + ' "/1.0/config/cloud-init.user-data",' + ' "/1.0/config/cloud-init.vendor-data"]', + "http://lxd/1.0/config/user.meta-data": "user.meta-data", + "http://lxd/1.0/config/user.network-config": + "user.network-config", + "http://lxd/1.0/config/user.user-data": "user.user-data", + "http://lxd/1.0/config/user.vendor-data": + "user.vendor-data", + "http://lxd/1.0/config/cloud-init.meta-data": + "cloud-init.meta-data", + "http://lxd/1.0/config/cloud-init.network-config": + "cloud-init.network-config", + "http://lxd/1.0/config/cloud-init.user-data": + "cloud-init.user-data", + "http://lxd/1.0/config/cloud-init.vendor-data": + "cloud-init.vendor-data", + }, + { + "_metadata_api_version": lxd.LXD_SOCKET_API_VERSION, + "config": { + "user.meta-data": "user.meta-data", + "user.network-config": "user.network-config", + "user.user-data": "user.user-data", + "user.vendor-data": "user.vendor-data", + "cloud-init.network-config": + "cloud-init.network-config", + "cloud-init.user-data": "cloud-init.user-data", + "cloud-init.vendor-data": + "cloud-init.vendor-data", + }, + "meta-data": "local-hostname: md\n", + "network-config": "cloud-init.network-config", + "user-data": "cloud-init.user-data", + "vendor-data": "cloud-init.vendor-data", + }, + [ + "[GET] [HTTP:200] http://lxd/1.0/meta-data", + "[GET] [HTTP:200] http://lxd/1.0/config", + "[GET] [HTTP:200] http://lxd/1.0/config/user.meta-data", + "[GET] [HTTP:200]" + " http://lxd/1.0/config/user.network-config", + "[GET] [HTTP:200] http://lxd/1.0/config/user.user-data", + "[GET] [HTTP:200] http://lxd/1.0/config/user.vendor-data", + "[GET] [HTTP:200]" + " http://lxd/1.0/config/cloud-init.network-config", + "[GET] [HTTP:200]" + " http://lxd/1.0/config/cloud-init.user-data", + "[GET] [HTTP:200]" + " http://lxd/1.0/config/cloud-init.vendor-data", + "Ignoring LXD config user.user-data in favor of" + " cloud-init.user-data value.", + "Ignoring LXD config user.network-config in favor of" + " cloud-init.network-config value.", + "Ignoring LXD config user.vendor-data in favor of" + " cloud-init.vendor-data value.", + ], + ), + ) + ) + @mock.patch.object(lxd.requests.Session, 'get') + def test_read_metadata_handles_unexpected_content_or_http_status( + self, session_get, url_responses, expected, logs, caplog + ): + """read_metadata handles valid and invalid content and status codes.""" + + def fake_get(url): + """Mock Response json, ok, status_code, text from url_responses.""" + m_resp = mock.MagicMock() + content = url_responses.get(url, '') + m_resp.json.side_effect = lambda: json.loads(content) + if content: + mock_ok = mock.PropertyMock(return_value=True) + mock_status_code = mock.PropertyMock(return_value=200) + else: + mock_ok = mock.PropertyMock(return_value=False) + mock_status_code = mock.PropertyMock(return_value=404) + type(m_resp).ok = mock_ok + type(m_resp).status_code = mock_status_code + mock_text = mock.PropertyMock(return_value=content) + type(m_resp).text = mock_text + return m_resp + + session_get.side_effect = fake_get + + if isinstance(expected, Exception): + with pytest.raises(type(expected), match=re.escape(str(expected))): + lxd.read_metadata() + else: + assert expected == lxd.read_metadata() + caplogs = caplog.text + for log in logs: + assert log in caplogs + +# vi: ts=4 expandtab diff --git a/tests/unittests/test_datasource/test_maas.py b/tests/unittests/sources/test_maas.py index 41b6c27b..34b79587 100644 --- a/tests/unittests/test_datasource/test_maas.py +++ b/tests/unittests/sources/test_maas.py @@ -9,7 +9,7 @@ from unittest import mock from cloudinit.sources import DataSourceMAAS from cloudinit import url_helper -from cloudinit.tests.helpers import CiTestCase, populate_dir +from tests.unittests.helpers import CiTestCase, populate_dir class TestMAASDataSource(CiTestCase): diff --git a/tests/unittests/test_datasource/test_nocloud.py b/tests/unittests/sources/test_nocloud.py index 02cc9b38..26f91054 100644 --- a/tests/unittests/test_datasource/test_nocloud.py +++ b/tests/unittests/sources/test_nocloud.py @@ -7,7 +7,7 @@ from cloudinit.sources.DataSourceNoCloud import ( _maybe_remove_top_network, parse_cmdline_data) from cloudinit import util -from cloudinit.tests.helpers import CiTestCase, populate_dir, mock, ExitStack +from tests.unittests.helpers import CiTestCase, populate_dir, mock, ExitStack import os import textwrap diff --git a/tests/unittests/test_datasource/test_opennebula.py b/tests/unittests/sources/test_opennebula.py index 283b65c2..e5963f5a 100644 --- a/tests/unittests/test_datasource/test_opennebula.py +++ b/tests/unittests/sources/test_opennebula.py @@ -3,7 +3,7 @@ from cloudinit import helpers from cloudinit.sources import DataSourceOpenNebula as ds from cloudinit import util -from cloudinit.tests.helpers import mock, populate_dir, CiTestCase +from tests.unittests.helpers import mock, populate_dir, CiTestCase import os import pwd diff --git a/tests/unittests/test_datasource/test_openstack.py b/tests/unittests/sources/test_openstack.py index a9829c75..0d6fb04a 100644 --- a/tests/unittests/test_datasource/test_openstack.py +++ b/tests/unittests/sources/test_openstack.py @@ -11,7 +11,7 @@ import re from io import StringIO from urllib.parse import urlparse -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers from cloudinit import helpers from cloudinit import settings diff --git a/cloudinit/sources/tests/test_oracle.py b/tests/unittests/sources/test_oracle.py index 5f608cbb..2aab097c 100644 --- a/cloudinit/sources/tests/test_oracle.py +++ b/tests/unittests/sources/test_oracle.py @@ -11,7 +11,7 @@ import pytest from cloudinit.sources import DataSourceOracle as oracle from cloudinit.sources import NetworkConfigSource from cloudinit.sources.DataSourceOracle import OpcMetadata -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers from cloudinit.url_helper import UrlError DS_PATH = "cloudinit.sources.DataSourceOracle" diff --git a/tests/unittests/test_datasource/test_ovf.py b/tests/unittests/sources/test_ovf.py index ad7446f8..da516731 100644 --- a/tests/unittests/test_datasource/test_ovf.py +++ b/tests/unittests/sources/test_ovf.py @@ -12,7 +12,7 @@ from textwrap import dedent from cloudinit import subp from cloudinit import util -from cloudinit.tests.helpers import CiTestCase, mock, wrap_and_call +from tests.unittests.helpers import CiTestCase, mock, wrap_and_call from cloudinit.helpers import Paths from cloudinit.sources import DataSourceOVF as dsovf from cloudinit.sources.helpers.vmware.imc.config_custom_script import ( diff --git a/tests/unittests/test_datasource/test_rbx.py b/tests/unittests/sources/test_rbx.py index d017510e..c1294c92 100644 --- a/tests/unittests/test_datasource/test_rbx.py +++ b/tests/unittests/sources/test_rbx.py @@ -3,7 +3,7 @@ import json from cloudinit import helpers from cloudinit import distros from cloudinit.sources import DataSourceRbxCloud as ds -from cloudinit.tests.helpers import mock, CiTestCase, populate_dir +from tests.unittests.helpers import mock, CiTestCase, populate_dir from cloudinit import subp DS_PATH = "cloudinit.sources.DataSourceRbxCloud" diff --git a/tests/unittests/test_datasource/test_scaleway.py b/tests/unittests/sources/test_scaleway.py index f9e968c5..33ae26b8 100644 --- a/tests/unittests/test_datasource/test_scaleway.py +++ b/tests/unittests/sources/test_scaleway.py @@ -10,7 +10,7 @@ from cloudinit import settings from cloudinit import sources from cloudinit.sources import DataSourceScaleway -from cloudinit.tests.helpers import mock, HttprettyTestCase, CiTestCase +from tests.unittests.helpers import mock, HttprettyTestCase, CiTestCase class DataResponses(object): diff --git a/tests/unittests/test_datasource/test_smartos.py b/tests/unittests/sources/test_smartos.py index 9c499672..e306eded 100644 --- a/tests/unittests/test_datasource/test_smartos.py +++ b/tests/unittests/sources/test_smartos.py @@ -35,7 +35,7 @@ from cloudinit import helpers as c_helpers from cloudinit.util import (b64e, write_file) from cloudinit.subp import (subp, ProcessExecutionError, which) -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, mock, FilesystemMockingTestCase, skipIf) diff --git a/tests/unittests/test_datasource/test_upcloud.py b/tests/unittests/sources/test_upcloud.py index cec48b4b..1d792066 100644 --- a/tests/unittests/test_datasource/test_upcloud.py +++ b/tests/unittests/sources/test_upcloud.py @@ -10,7 +10,7 @@ from cloudinit import sources from cloudinit.sources.DataSourceUpCloud import DataSourceUpCloud, \ DataSourceUpCloudLocal -from cloudinit.tests.helpers import mock, CiTestCase +from tests.unittests.helpers import mock, CiTestCase UC_METADATA = json.loads(""" { diff --git a/tests/unittests/test_datasource/test_vmware.py b/tests/unittests/sources/test_vmware.py index 52f910b5..d34d7782 100644 --- a/tests/unittests/test_datasource/test_vmware.py +++ b/tests/unittests/sources/test_vmware.py @@ -13,7 +13,7 @@ import pytest from cloudinit import dmi, helpers, safeyaml from cloudinit import settings from cloudinit.sources import DataSourceVMware -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( mock, CiTestCase, FilesystemMockingTestCase, diff --git a/tests/unittests/test_datasource/test_vultr.py b/tests/unittests/sources/test_vultr.py index 63235009..40594b95 100644 --- a/tests/unittests/test_datasource/test_vultr.py +++ b/tests/unittests/sources/test_vultr.py @@ -12,7 +12,7 @@ from cloudinit import settings from cloudinit.sources import DataSourceVultr from cloudinit.sources.helpers import vultr -from cloudinit.tests.helpers import mock, CiTestCase +from tests.unittests.helpers import mock, CiTestCase # Vultr metadata test data VULTR_V1_1 = { diff --git a/tests/unittests/test_handler/__init__.py b/tests/unittests/sources/vmware/__init__.py index e69de29b..e69de29b 100644 --- a/tests/unittests/test_handler/__init__.py +++ b/tests/unittests/sources/vmware/__init__.py diff --git a/tests/unittests/test_vmware/test_custom_script.py b/tests/unittests/sources/vmware/test_custom_script.py index f89f8157..fcbb9cd5 100644 --- a/tests/unittests/test_vmware/test_custom_script.py +++ b/tests/unittests/sources/vmware/test_custom_script.py @@ -14,7 +14,7 @@ from cloudinit.sources.helpers.vmware.imc.config_custom_script import ( PreCustomScript, PostCustomScript, ) -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock class TestVmwareCustomScript(CiTestCase): diff --git a/tests/unittests/test_vmware/test_guestcust_util.py b/tests/unittests/sources/vmware/test_guestcust_util.py index c8b59d83..9114f0b9 100644 --- a/tests/unittests/test_vmware/test_guestcust_util.py +++ b/tests/unittests/sources/vmware/test_guestcust_util.py @@ -12,7 +12,7 @@ from cloudinit.sources.helpers.vmware.imc.guestcust_util import ( get_tools_config, set_gc_status, ) -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock class TestGuestCustUtil(CiTestCase): diff --git a/tests/unittests/test_vmware_config_file.py b/tests/unittests/sources/vmware/test_vmware_config_file.py index 430cc69f..54de113e 100644 --- a/tests/unittests/test_vmware_config_file.py +++ b/tests/unittests/sources/vmware/test_vmware_config_file.py @@ -19,7 +19,7 @@ from cloudinit.sources.helpers.vmware.imc.config import Config from cloudinit.sources.helpers.vmware.imc.config_file import ConfigFile from cloudinit.sources.helpers.vmware.imc.config_nic import gen_subnet from cloudinit.sources.helpers.vmware.imc.config_nic import NicConfigurator -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase logging.basicConfig(level=logging.DEBUG, stream=sys.stdout) logger = logging.getLogger(__name__) diff --git a/tests/unittests/test__init__.py b/tests/unittests/test__init__.py index 739bbebf..4382a078 100644 --- a/tests/unittests/test__init__.py +++ b/tests/unittests/test__init__.py @@ -12,7 +12,7 @@ from cloudinit import settings from cloudinit import url_helper from cloudinit import util -from cloudinit.tests.helpers import TestCase, CiTestCase, ExitStack, mock +from tests.unittests.helpers import TestCase, CiTestCase, ExitStack, mock class FakeModule(handlers.Handler): diff --git a/tests/unittests/test_atomic_helper.py b/tests/unittests/test_atomic_helper.py index 0101b0e3..0c8b8e53 100644 --- a/tests/unittests/test_atomic_helper.py +++ b/tests/unittests/test_atomic_helper.py @@ -6,7 +6,7 @@ import stat from cloudinit import atomic_helper -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase class TestAtomicHelper(CiTestCase): diff --git a/tests/unittests/test_builtin_handlers.py b/tests/unittests/test_builtin_handlers.py index 30293e9e..cf2c0a4d 100644 --- a/tests/unittests/test_builtin_handlers.py +++ b/tests/unittests/test_builtin_handlers.py @@ -5,12 +5,13 @@ import copy import errno import os +import pytest import shutil import tempfile from textwrap import dedent -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( FilesystemMockingTestCase, CiTestCase, mock, skipUnlessJinja) from cloudinit import handlers @@ -281,17 +282,44 @@ class TestJinjaTemplatePartHandler(CiTestCase): self.logs.getvalue()) -class TestConvertJinjaInstanceData(CiTestCase): - - def test_convert_instance_data_hyphens_to_underscores(self): - """Replace hyphenated keys with underscores in instance-data.""" - data = {'hyphenated-key': 'hyphenated-val', - 'underscore_delim_key': 'underscore_delimited_val'} - expected_data = {'hyphenated_key': 'hyphenated-val', - 'underscore_delim_key': 'underscore_delimited_val'} - self.assertEqual( - expected_data, - convert_jinja_instance_data(data=data)) +class TestConvertJinjaInstanceData: + + @pytest.mark.parametrize( + "include_key_aliases,data,expected", ( + ( + False, + {'my-key': 'my-val'}, + {'my-key': 'my-val'} + ), + ( + True, + {'my-key': 'my-val'}, + {'my-key': 'my-val', 'my_key': 'my-val'} + ), + ( + False, + {'my.key': 'my.val'}, + {'my.key': 'my.val'} + ), + ( + True, + {'my.key': 'my.val'}, + {'my.key': 'my.val', 'my_key': 'my.val'} + ), + ( + True, + {'my/key': 'my/val'}, + {'my/key': 'my/val', 'my_key': 'my/val'} + ), + ) + ) + def test_convert_instance_data_operators_to_underscores( + self, include_key_aliases, data, expected + ): + """Replace Jinja operators keys with underscores in instance-data.""" + assert expected == convert_jinja_instance_data( + data=data, include_key_aliases=include_key_aliases + ) def test_convert_instance_data_promotes_versioned_keys_to_top_level(self): """Any versioned keys are promoted as top-level keys @@ -307,11 +335,10 @@ class TestConvertJinjaInstanceData(CiTestCase): expected_data.update({'v1key1': 'v1.1', 'v2key1': 'v2.1'}) converted_data = convert_jinja_instance_data(data=data) - self.assertCountEqual( - ['ds', 'v1', 'v2', 'v1key1', 'v2key1'], converted_data.keys()) - self.assertEqual( - expected_data, - converted_data) + assert sorted(['ds', 'v1', 'v2', 'v1key1', 'v2key1']) == sorted( + converted_data.keys() + ) + assert expected_data == converted_data def test_convert_instance_data_most_recent_version_of_promoted_keys(self): """The most-recent versioned key value is promoted to top-level.""" @@ -324,9 +351,7 @@ class TestConvertJinjaInstanceData(CiTestCase): 'key3': 'newer v2 key3'}) converted_data = convert_jinja_instance_data(data=data) - self.assertEqual( - expected_data, - converted_data) + assert expected_data == converted_data def test_convert_instance_data_decodes_decode_paths(self): """Any decode_paths provided are decoded by convert_instance_data.""" @@ -336,9 +361,7 @@ class TestConvertJinjaInstanceData(CiTestCase): converted_data = convert_jinja_instance_data( data=data, decode_paths=('key1/subkey1',)) - self.assertEqual( - expected_data, - converted_data) + assert expected_data == converted_data class TestRenderJinjaPayload(CiTestCase): @@ -355,6 +378,7 @@ class TestRenderJinjaPayload(CiTestCase): DEBUG: Converted jinja variables { "hostname": "foo", + "instance-id": "iid", "instance_id": "iid", "v1": { "hostname": "foo" diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py index 1459fd9c..fd717f34 100644 --- a/tests/unittests/test_cli.py +++ b/tests/unittests/test_cli.py @@ -5,7 +5,7 @@ import io from collections import namedtuple from cloudinit.cmd import main as cli -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers from cloudinit.util import load_file, load_json diff --git a/cloudinit/tests/test_conftest.py b/tests/unittests/test_conftest.py index 6f1263a5..2e02b7a7 100644 --- a/cloudinit/tests/test_conftest.py +++ b/tests/unittests/test_conftest.py @@ -1,7 +1,7 @@ import pytest from cloudinit import subp -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase class TestDisableSubpUsage: diff --git a/tests/unittests/test_cs_util.py b/tests/unittests/test_cs_util.py index bfd07ecf..be9da40c 100644 --- a/tests/unittests/test_cs_util.py +++ b/tests/unittests/test_cs_util.py @@ -1,6 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers from cloudinit.cs_utils import Cepko diff --git a/tests/unittests/test_data.py b/tests/unittests/test_data.py index 8c968ae9..2ee09bbb 100644 --- a/tests/unittests/test_data.py +++ b/tests/unittests/test_data.py @@ -25,7 +25,7 @@ from cloudinit import user_data as ud from cloudinit import safeyaml from cloudinit import util -from cloudinit.tests import helpers +from tests.unittests import helpers INSTANCE_ID = "i-testing" diff --git a/cloudinit/tests/test_dhclient_hook.py b/tests/unittests/test_dhclient_hook.py index eadae81c..14549111 100644 --- a/cloudinit/tests/test_dhclient_hook.py +++ b/tests/unittests/test_dhclient_hook.py @@ -3,7 +3,7 @@ """Tests for cloudinit.dhclient_hook.""" from cloudinit import dhclient_hook as dhc -from cloudinit.tests.helpers import CiTestCase, dir2dict, populate_dir +from tests.unittests.helpers import CiTestCase, dir2dict, populate_dir import argparse import json diff --git a/cloudinit/tests/test_dmi.py b/tests/unittests/test_dmi.py index 78a72122..674e7b98 100644 --- a/cloudinit/tests/test_dmi.py +++ b/tests/unittests/test_dmi.py @@ -1,4 +1,4 @@ -from cloudinit.tests import helpers +from tests.unittests import helpers from cloudinit import dmi from cloudinit import util from cloudinit import subp diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py index 43603ea5..62c3e403 100644 --- a/tests/unittests/test_ds_identify.py +++ b/tests/unittests/test_ds_identify.py @@ -8,7 +8,7 @@ from uuid import uuid4 from cloudinit import safeyaml from cloudinit import subp from cloudinit import util -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, dir2dict, populate_dir, populate_dir_with_ts) from cloudinit.sources import DataSourceIBMCloud as ds_ibm diff --git a/tests/unittests/test_ec2_util.py b/tests/unittests/test_ec2_util.py index 3f50f57d..e8e0b5b1 100644 --- a/tests/unittests/test_ec2_util.py +++ b/tests/unittests/test_ec2_util.py @@ -2,7 +2,7 @@ import httpretty as hp -from cloudinit.tests import helpers +from tests.unittests import helpers from cloudinit import ec2_utils as eu from cloudinit import url_helper as uh diff --git a/cloudinit/tests/test_event.py b/tests/unittests/test_event.py index 3da4c70c..3da4c70c 100644 --- a/cloudinit/tests/test_event.py +++ b/tests/unittests/test_event.py diff --git a/cloudinit/tests/test_features.py b/tests/unittests/test_features.py index d7a7226d..d7a7226d 100644 --- a/cloudinit/tests/test_features.py +++ b/tests/unittests/test_features.py diff --git a/tests/unittests/test_gpg.py b/tests/unittests/test_gpg.py index 451ffa91..ceada49a 100644 --- a/tests/unittests/test_gpg.py +++ b/tests/unittests/test_gpg.py @@ -4,6 +4,8 @@ from unittest import mock from cloudinit import gpg from cloudinit import subp +from tests.unittests.helpers import CiTestCase + TEST_KEY_HUMAN = ''' /etc/apt/cloud-init.gpg.d/my_key.gpg -------------------------------------------- @@ -79,3 +81,50 @@ class TestGPGCommands: test_call = mock.call( ["gpg", "--dearmor"], data='key', decode=False) assert test_call == m_subp.call_args + + @mock.patch("cloudinit.gpg.time.sleep") + @mock.patch("cloudinit.gpg.subp.subp") + class TestReceiveKeys(CiTestCase): + """Test the recv_key method.""" + + def test_retries_on_subp_exc(self, m_subp, m_sleep): + """retry should be done on gpg receive keys failure.""" + retries = (1, 2, 4) + my_exc = subp.ProcessExecutionError( + stdout='', stderr='', exit_code=2, cmd=['mycmd']) + m_subp.side_effect = (my_exc, my_exc, ('', '')) + gpg.recv_key("ABCD", "keyserver.example.com", retries=retries) + self.assertEqual( + [mock.call(1), mock.call(2)], m_sleep.call_args_list) + + def test_raises_error_after_retries(self, m_subp, m_sleep): + """If the final run fails, error should be raised.""" + naplen = 1 + keyid, keyserver = ("ABCD", "keyserver.example.com") + m_subp.side_effect = subp.ProcessExecutionError( + stdout='', stderr='', exit_code=2, cmd=['mycmd']) + with self.assertRaises(ValueError) as rcm: + gpg.recv_key(keyid, keyserver, retries=(naplen,)) + self.assertIn(keyid, str(rcm.exception)) + self.assertIn(keyserver, str(rcm.exception)) + m_sleep.assert_called_with(naplen) + + def test_no_retries_on_none(self, m_subp, m_sleep): + """retry should not be done if retries is None.""" + m_subp.side_effect = subp.ProcessExecutionError( + stdout='', stderr='', exit_code=2, cmd=['mycmd']) + with self.assertRaises(ValueError): + gpg.recv_key("ABCD", "keyserver.example.com", retries=None) + m_sleep.assert_not_called() + + def test_expected_gpg_command(self, m_subp, m_sleep): + """Verify gpg is called with expected args.""" + key, keyserver = ("DEADBEEF", "keyserver.example.com") + retries = (1, 2, 4) + m_subp.return_value = ('', '') + gpg.recv_key(key, keyserver, retries=retries) + m_subp.assert_called_once_with( + ['gpg', '--no-tty', + '--keyserver=%s' % keyserver, '--recv-keys', key], + capture=True) + m_sleep.assert_not_called() diff --git a/tests/unittests/test_helpers.py b/tests/unittests/test_helpers.py index 2e4582a0..c6f9b94a 100644 --- a/tests/unittests/test_helpers.py +++ b/tests/unittests/test_helpers.py @@ -4,7 +4,7 @@ import os -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers from cloudinit import sources diff --git a/tests/unittests/test_log.py b/tests/unittests/test_log.py index e069a487..3d1b9582 100644 --- a/tests/unittests/test_log.py +++ b/tests/unittests/test_log.py @@ -9,7 +9,7 @@ import time from cloudinit import log as ci_logging from cloudinit.analyze.dump import CLOUD_INIT_ASCTIME_FMT -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase class TestCloudInitLogger(CiTestCase): diff --git a/tests/unittests/test_merging.py b/tests/unittests/test_merging.py index 10871bcf..48ab6602 100644 --- a/tests/unittests/test_merging.py +++ b/tests/unittests/test_merging.py @@ -1,6 +1,6 @@ # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.tests import helpers +from tests.unittests import helpers from cloudinit.handlers import cloud_config from cloudinit.handlers import (CONTENT_START, CONTENT_END) diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 094450b4..b5c38c55 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -12,7 +12,7 @@ from cloudinit import subp from cloudinit import util from cloudinit import safeyaml as yaml -from cloudinit.tests.helpers import ( +from tests.unittests.helpers import ( CiTestCase, FilesystemMockingTestCase, dir2dict, mock, populate_dir) import base64 @@ -5373,21 +5373,20 @@ class TestNetRenderers(CiTestCase): priority=['sysconfig', 'eni']) @mock.patch("cloudinit.net.sysconfig.available_sysconfig") - @mock.patch("cloudinit.util.get_linux_distro") - def test_sysconfig_available_uses_variant_mapping(self, m_distro, m_avail): + @mock.patch("cloudinit.util.system_info") + def test_sysconfig_available_uses_variant_mapping(self, m_info, m_avail): m_avail.return_value = True - distro_values = [ - ('opensuse', '', ''), - ('opensuse-leap', '', ''), - ('opensuse-tumbleweed', '', ''), - ('sles', '', ''), - ('centos', '', ''), - ('eurolinux', '', ''), - ('fedora', '', ''), - ('redhat', '', ''), + variants = [ + 'suse', + 'centos', + 'eurolinux', + 'fedora', + 'rhel', ] - for (distro_name, distro_version, flavor) in distro_values: - m_distro.return_value = (distro_name, distro_version, flavor) + for distro_name in variants: + m_info.return_value = { + "variant": distro_name + } if hasattr(util.system_info, "cache_clear"): util.system_info.cache_clear() result = sysconfig.available() diff --git a/tests/unittests/test_net_activators.py b/tests/unittests/test_net_activators.py index f63a8b74..9da21195 100644 --- a/tests/unittests/test_net_activators.py +++ b/tests/unittests/test_net_activators.py @@ -12,7 +12,8 @@ from cloudinit.net.activators import ( IfUpDownActivator, NetplanActivator, NetworkManagerActivator, - NetworkdActivator + NetworkdActivator, + NoActivatorException, ) from cloudinit.net.network_state import parse_net_config_data from cloudinit.safeyaml import load @@ -99,7 +100,7 @@ class TestSearchAndSelect: resp = search_activator() assert resp == [] - with pytest.raises(RuntimeError): + with pytest.raises(NoActivatorException): select_activator() diff --git a/tests/unittests/test_net_freebsd.py b/tests/unittests/test_net_freebsd.py index e339e132..f0dde097 100644 --- a/tests/unittests/test_net_freebsd.py +++ b/tests/unittests/test_net_freebsd.py @@ -3,7 +3,7 @@ import os import cloudinit.net import cloudinit.net.network_state from cloudinit import safeyaml -from cloudinit.tests.helpers import (CiTestCase, mock, readResource, dir2dict) +from tests.unittests.helpers import (CiTestCase, mock, readResource, dir2dict) SAMPLE_FREEBSD_IFCONFIG_OUT = readResource("netinfo/freebsd-ifconfig-output") diff --git a/cloudinit/tests/test_netinfo.py b/tests/unittests/test_netinfo.py index e44b16d8..238f7b0a 100644 --- a/cloudinit/tests/test_netinfo.py +++ b/tests/unittests/test_netinfo.py @@ -5,7 +5,7 @@ from copy import copy from cloudinit.netinfo import netdev_info, netdev_pformat, route_pformat -from cloudinit.tests.helpers import CiTestCase, mock, readResource +from tests.unittests.helpers import CiTestCase, mock, readResource # Example ifconfig and route output diff --git a/tests/unittests/test_pathprefix2dict.py b/tests/unittests/test_pathprefix2dict.py index abbb29b8..4e737ad7 100644 --- a/tests/unittests/test_pathprefix2dict.py +++ b/tests/unittests/test_pathprefix2dict.py @@ -2,7 +2,7 @@ from cloudinit import util -from cloudinit.tests.helpers import TestCase, populate_dir +from tests.unittests.helpers import TestCase, populate_dir import shutil import tempfile diff --git a/cloudinit/tests/test_persistence.py b/tests/unittests/test_persistence.py index ec1152a9..ec1152a9 100644 --- a/cloudinit/tests/test_persistence.py +++ b/tests/unittests/test_persistence.py diff --git a/tests/unittests/test_registry.py b/tests/unittests/test_registry.py index 2b625026..4c7df186 100644 --- a/tests/unittests/test_registry.py +++ b/tests/unittests/test_registry.py @@ -2,7 +2,7 @@ from cloudinit.registry import DictRegistry -from cloudinit.tests.helpers import (mock, TestCase) +from tests.unittests.helpers import (mock, TestCase) class TestDictRegistry(TestCase): diff --git a/tests/unittests/test_reporting.py b/tests/unittests/test_reporting.py index b78a6939..3aaeea43 100644 --- a/tests/unittests/test_reporting.py +++ b/tests/unittests/test_reporting.py @@ -8,7 +8,7 @@ from cloudinit import reporting from cloudinit.reporting import events from cloudinit.reporting import handlers -from cloudinit.tests.helpers import TestCase +from tests.unittests.helpers import TestCase def _fake_registry(): diff --git a/tests/unittests/test_reporting_hyperv.py b/tests/unittests/test_reporting_hyperv.py index 9324b78d..24a1dcc7 100644 --- a/tests/unittests/test_reporting_hyperv.py +++ b/tests/unittests/test_reporting_hyperv.py @@ -13,7 +13,7 @@ import re from unittest import mock from cloudinit import util -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase from cloudinit.sources.helpers import azure diff --git a/tests/unittests/test_runs/__init__.py b/tests/unittests/test_runs/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/tests/unittests/test_runs/__init__.py +++ /dev/null diff --git a/cloudinit/tests/test_simpletable.py b/tests/unittests/test_simpletable.py index a12a62a0..69b30f0e 100644 --- a/cloudinit/tests/test_simpletable.py +++ b/tests/unittests/test_simpletable.py @@ -10,7 +10,7 @@ reimplement the entire library, only the minimal parts we actually use. """ from cloudinit.simpletable import SimpleTable -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase # Examples rendered by cloud-init using PrettyTable NET_DEVICE_FIELDS = ( diff --git a/tests/unittests/test_sshutil.py b/tests/unittests/test_sshutil.py index 08e20050..b210bd3b 100644 --- a/tests/unittests/test_sshutil.py +++ b/tests/unittests/test_sshutil.py @@ -7,7 +7,7 @@ from functools import partial from unittest.mock import patch from cloudinit import ssh_util -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers from cloudinit import util # https://stackoverflow.com/questions/11351032/ diff --git a/cloudinit/tests/test_stages.py b/tests/unittests/test_stages.py index a50836a4..a722f03f 100644 --- a/cloudinit/tests/test_stages.py +++ b/tests/unittests/test_stages.py @@ -13,7 +13,7 @@ from cloudinit.sources import NetworkConfigSource from cloudinit.event import EventScope, EventType from cloudinit.util import write_file -from cloudinit.tests.helpers import CiTestCase, mock +from tests.unittests.helpers import CiTestCase, mock TEST_INSTANCE_ID = 'i-testing' diff --git a/cloudinit/tests/test_subp.py b/tests/unittests/test_subp.py index 515d5d64..ec513d01 100644 --- a/cloudinit/tests/test_subp.py +++ b/tests/unittests/test_subp.py @@ -10,7 +10,7 @@ import stat from unittest import mock from cloudinit import subp, util -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase BASH = subp.which('bash') diff --git a/cloudinit/tests/test_temp_utils.py b/tests/unittests/test_temp_utils.py index 4a52ef89..9d56d0d0 100644 --- a/cloudinit/tests/test_temp_utils.py +++ b/tests/unittests/test_temp_utils.py @@ -3,7 +3,7 @@ """Tests for cloudinit.temp_utils""" from cloudinit.temp_utils import mkdtemp, mkstemp, tempdir -from cloudinit.tests.helpers import CiTestCase, wrap_and_call +from tests.unittests.helpers import CiTestCase, wrap_and_call import os diff --git a/tests/unittests/test_templating.py b/tests/unittests/test_templating.py index cba09830..459e017b 100644 --- a/tests/unittests/test_templating.py +++ b/tests/unittests/test_templating.py @@ -4,7 +4,7 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.tests import helpers as test_helpers +from tests.unittests import helpers as test_helpers import textwrap from cloudinit import templater diff --git a/cloudinit/tests/test_upgrade.py b/tests/unittests/test_upgrade.py index da3ab23b..d7a721a2 100644 --- a/cloudinit/tests/test_upgrade.py +++ b/tests/unittests/test_upgrade.py @@ -19,7 +19,7 @@ import pathlib import pytest from cloudinit.stages import _pkl_load -from cloudinit.tests.helpers import resourceLocation +from tests.unittests.helpers import resourceLocation class TestUpgrade: diff --git a/cloudinit/tests/test_url_helper.py b/tests/unittests/test_url_helper.py index c3918f80..501d9533 100644 --- a/cloudinit/tests/test_url_helper.py +++ b/tests/unittests/test_url_helper.py @@ -3,7 +3,7 @@ from cloudinit.url_helper import ( NOT_FOUND, UrlError, REDACTED, oauth_headers, read_file_or_url, retry_on_url_exc) -from cloudinit.tests.helpers import CiTestCase, mock, skipIf +from tests.unittests.helpers import CiTestCase, mock, skipIf from cloudinit import util from cloudinit import version diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py index bc30c90b..1290cbc6 100644 --- a/tests/unittests/test_util.py +++ b/tests/unittests/test_util.py @@ -1,23 +1,1311 @@ # This file is part of cloud-init. See LICENSE file for license information. -import io +"""Tests for cloudinit.util""" + +import base64 import logging +import json +import platform +import pytest + +import io import os import re import shutil import stat import tempfile -import pytest import yaml from unittest import mock from cloudinit import subp from cloudinit import importer, util -from cloudinit.tests import helpers +from tests.unittests import helpers + + +from tests.unittests.helpers import CiTestCase +from textwrap import dedent + +LOG = logging.getLogger(__name__) + +MOUNT_INFO = [ + '68 0 8:3 / / ro,relatime shared:1 - btrfs /dev/sda1 ro,attr2,inode64', + '153 68 254:0 / /home rw,relatime shared:101 - xfs /dev/sda2 rw,attr2', +] + +OS_RELEASE_SLES = dedent( + """\ + NAME="SLES" + VERSION="12-SP3" + VERSION_ID="12.3" + PRETTY_NAME="SUSE Linux Enterprise Server 12 SP3" + ID="sles" + ANSI_COLOR="0;32" + CPE_NAME="cpe:/o:suse:sles:12:sp3" +""" +) + +OS_RELEASE_OPENSUSE = dedent( + """\ + NAME="openSUSE Leap" + VERSION="42.3" + ID=opensuse + ID_LIKE="suse" + VERSION_ID="42.3" + PRETTY_NAME="openSUSE Leap 42.3" + ANSI_COLOR="0;32" + CPE_NAME="cpe:/o:opensuse:leap:42.3" + BUG_REPORT_URL="https://bugs.opensuse.org" + HOME_URL="https://www.opensuse.org/" +""" +) + +OS_RELEASE_OPENSUSE_L15 = dedent( + """\ + NAME="openSUSE Leap" + VERSION="15.0" + ID="opensuse-leap" + ID_LIKE="suse opensuse" + VERSION_ID="15.0" + PRETTY_NAME="openSUSE Leap 15.0" + ANSI_COLOR="0;32" + CPE_NAME="cpe:/o:opensuse:leap:15.0" + BUG_REPORT_URL="https://bugs.opensuse.org" + HOME_URL="https://www.opensuse.org/" +""" +) + +OS_RELEASE_OPENSUSE_TW = dedent( + """\ + NAME="openSUSE Tumbleweed" + ID="opensuse-tumbleweed" + ID_LIKE="opensuse suse" + VERSION_ID="20180920" + PRETTY_NAME="openSUSE Tumbleweed" + ANSI_COLOR="0;32" + CPE_NAME="cpe:/o:opensuse:tumbleweed:20180920" + BUG_REPORT_URL="https://bugs.opensuse.org" + HOME_URL="https://www.opensuse.org/" +""" +) + +OS_RELEASE_CENTOS = dedent( + """\ + NAME="CentOS Linux" + VERSION="7 (Core)" + ID="centos" + ID_LIKE="rhel fedora" + VERSION_ID="7" + PRETTY_NAME="CentOS Linux 7 (Core)" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:centos:centos:7" + HOME_URL="https://www.centos.org/" + BUG_REPORT_URL="https://bugs.centos.org/" + + CENTOS_MANTISBT_PROJECT="CentOS-7" + CENTOS_MANTISBT_PROJECT_VERSION="7" + REDHAT_SUPPORT_PRODUCT="centos" + REDHAT_SUPPORT_PRODUCT_VERSION="7" +""" +) + +OS_RELEASE_REDHAT_7 = dedent( + """\ + NAME="Red Hat Enterprise Linux Server" + VERSION="7.5 (Maipo)" + ID="rhel" + ID_LIKE="fedora" + VARIANT="Server" + VARIANT_ID="server" + VERSION_ID="7.5" + PRETTY_NAME="Red Hat" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:redhat:enterprise_linux:7.5: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.5 + REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux" + REDHAT_SUPPORT_PRODUCT_VERSION="7.5" +""" +) + +OS_RELEASE_ALMALINUX_8 = dedent( + """\ + 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 (Purple Manul)" + ANSI_COLOR="0;34" + CPE_NAME="cpe:/o:almalinux:almalinux:8.3:GA" + HOME_URL="https://almalinux.org/" + BUG_REPORT_URL="https://bugs.almalinux.org/" + + ALMALINUX_MANTISBT_PROJECT="AlmaLinux-8" + ALMALINUX_MANTISBT_PROJECT_VERSION="8.3" +""" +) + +OS_RELEASE_EUROLINUX_7 = dedent( + """\ + VERSION="7.9 (Minsk)" + ID="eurolinux" + ID_LIKE="rhel scientific centos fedora" + VERSION_ID="7.9" + PRETTY_NAME="EuroLinux 7.9 (Minsk)" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:eurolinux:eurolinux:7.9:GA" + HOME_URL="http://www.euro-linux.com/" + BUG_REPORT_URL="mailto:support@euro-linux.com" + REDHAT_BUGZILLA_PRODUCT="EuroLinux 7" + REDHAT_BUGZILLA_PRODUCT_VERSION=7.9 + REDHAT_SUPPORT_PRODUCT="EuroLinux" + REDHAT_SUPPORT_PRODUCT_VERSION="7.9" +""" +) + +OS_RELEASE_EUROLINUX_8 = dedent( + """\ + NAME="EuroLinux" + VERSION="8.4 (Vaduz)" + ID="eurolinux" + ID_LIKE="rhel fedora centos" + VERSION_ID="8.4" + PLATFORM_ID="platform:el8" + PRETTY_NAME="EuroLinux 8.4 (Vaduz)" + 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" +""" +) + +OS_RELEASE_ROCKY_8 = dedent( + """\ + NAME="Rocky Linux" + VERSION="8.3 (Green Obsidian)" + ID="rocky" + ID_LIKE="rhel fedora" + VERSION_ID="8.3" + PLATFORM_ID="platform:el8" + PRETTY_NAME="Rocky Linux 8.3 (Green Obsidian)" + 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" +""" +) + +OS_RELEASE_VIRTUOZZO_8 = dedent( + """\ + NAME="Virtuozzo Linux" + VERSION="8" + ID="virtuozzo" + ID_LIKE="rhel fedora" + VERSION_ID="8" + PLATFORM_ID="platform:el8" + PRETTY_NAME="Virtuozzo Linux" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:virtuozzoproject:vzlinux:8" + HOME_URL="https://www.vzlinux.org" + BUG_REPORT_URL="https://bugs.openvz.org" +""" +) + +OS_RELEASE_CLOUDLINUX_8 = dedent( + """\ + NAME="CloudLinux" + VERSION="8.4 (Valery Rozhdestvensky)" + ID="cloudlinux" + ID_LIKE="rhel fedora centos" + VERSION_ID="8.4" + PLATFORM_ID="platform:el8" + PRETTY_NAME="CloudLinux 8.4 (Valery Rozhdestvensky)" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:cloudlinux:cloudlinux:8.4:GA:server" + HOME_URL="https://www.cloudlinux.com/" + BUG_REPORT_URL="https://www.cloudlinux.com/support" +""" +) + +OS_RELEASE_OPENEULER_20 = dedent( + """\ + NAME="openEuler" + VERSION="20.03 (LTS-SP2)" + ID="openEuler" + VERSION_ID="20.03" + PRETTY_NAME="openEuler 20.03 (LTS-SP2)" + ANSI_COLOR="0;31" +""" +) + +REDHAT_RELEASE_CENTOS_6 = "CentOS release 6.10 (Final)" +REDHAT_RELEASE_CENTOS_7 = "CentOS Linux release 7.5.1804 (Core)" +REDHAT_RELEASE_REDHAT_6 = ( + "Red Hat Enterprise Linux Server release 6.10 (Santiago)" +) +REDHAT_RELEASE_REDHAT_7 = "Red Hat Enterprise Linux Server release 7.5 (Maipo)" +REDHAT_RELEASE_ALMALINUX_8 = "AlmaLinux release 8.3 (Purple Manul)" +REDHAT_RELEASE_EUROLINUX_7 = "EuroLinux release 7.9 (Minsk)" +REDHAT_RELEASE_EUROLINUX_8 = "EuroLinux release 8.4 (Vaduz)" +REDHAT_RELEASE_ROCKY_8 = "Rocky Linux release 8.3 (Green Obsidian)" +REDHAT_RELEASE_VIRTUOZZO_8 = "Virtuozzo Linux release 8" +REDHAT_RELEASE_CLOUDLINUX_8 = "CloudLinux release 8.4 (Valery Rozhdestvensky)" +OS_RELEASE_DEBIAN = dedent( + """\ + PRETTY_NAME="Debian GNU/Linux 9 (stretch)" + NAME="Debian GNU/Linux" + VERSION_ID="9" + VERSION="9 (stretch)" + ID=debian + HOME_URL="https://www.debian.org/" + SUPPORT_URL="https://www.debian.org/support" + BUG_REPORT_URL="https://bugs.debian.org/" +""" +) + +OS_RELEASE_UBUNTU = dedent( + """\ + NAME="Ubuntu"\n + # comment test + VERSION="16.04.3 LTS (Xenial Xerus)"\n + ID=ubuntu\n + ID_LIKE=debian\n + PRETTY_NAME="Ubuntu 16.04.3 LTS"\n + VERSION_ID="16.04"\n + HOME_URL="http://www.ubuntu.com/"\n + SUPPORT_URL="http://help.ubuntu.com/"\n + BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"\n + VERSION_CODENAME=xenial\n + UBUNTU_CODENAME=xenial\n +""" +) + +OS_RELEASE_PHOTON = """\ + NAME="VMware Photon OS" + VERSION="4.0" + ID=photon + VERSION_ID=4.0 + PRETTY_NAME="VMware Photon OS/Linux" + ANSI_COLOR="1;34" + HOME_URL="https://vmware.github.io/photon/" + BUG_REPORT_URL="https://github.com/vmware/photon/issues" +""" + + +class FakeCloud(object): + def __init__(self, hostname, fqdn): + self.hostname = hostname + self.fqdn = fqdn + self.calls = [] + + def get_hostname(self, fqdn=None, metadata_only=None): + myargs = {} + if fqdn is not None: + myargs['fqdn'] = fqdn + if metadata_only is not None: + myargs['metadata_only'] = metadata_only + self.calls.append(myargs) + if fqdn: + return self.fqdn + return self.hostname + + +class TestUtil(CiTestCase): + def test_parse_mount_info_no_opts_no_arg(self): + result = util.parse_mount_info('/home', MOUNT_INFO, LOG) + self.assertEqual(('/dev/sda2', 'xfs', '/home'), result) + + def test_parse_mount_info_no_opts_arg(self): + result = util.parse_mount_info('/home', MOUNT_INFO, LOG, False) + self.assertEqual(('/dev/sda2', 'xfs', '/home'), result) + + def test_parse_mount_info_with_opts(self): + result = util.parse_mount_info('/', MOUNT_INFO, LOG, True) + self.assertEqual(('/dev/sda1', 'btrfs', '/', 'ro,relatime'), result) + + @mock.patch('cloudinit.util.get_mount_info') + def test_mount_is_rw(self, m_mount_info): + m_mount_info.return_value = ('/dev/sda1', 'btrfs', '/', 'rw,relatime') + is_rw = util.mount_is_read_write('/') + self.assertEqual(is_rw, True) + + @mock.patch('cloudinit.util.get_mount_info') + def test_mount_is_ro(self, m_mount_info): + m_mount_info.return_value = ('/dev/sda1', 'btrfs', '/', 'ro,relatime') + is_rw = util.mount_is_read_write('/') + self.assertEqual(is_rw, False) + + +class TestUptime(CiTestCase): + @mock.patch('cloudinit.util.boottime') + @mock.patch('cloudinit.util.os.path.exists') + @mock.patch('cloudinit.util.time.time') + def test_uptime_non_linux_path(self, m_time, m_exists, m_boottime): + boottime = 1000.0 + uptime = 10.0 + m_boottime.return_value = boottime + m_time.return_value = boottime + uptime + m_exists.return_value = False + result = util.uptime() + self.assertEqual(str(uptime), result) + + +class TestShellify(CiTestCase): + def test_input_dict_raises_type_error(self): + self.assertRaisesRegex( + TypeError, + 'Input.*was.*dict.*xpected', + util.shellify, + {'mykey': 'myval'}, + ) + def test_input_str_raises_type_error(self): + self.assertRaisesRegex( + TypeError, 'Input.*was.*str.*xpected', util.shellify, "foobar" + ) -class FakeSelinux(object): + def test_value_with_int_raises_type_error(self): + self.assertRaisesRegex( + TypeError, 'shellify.*int', util.shellify, ["foo", 1] + ) + + def test_supports_strings_and_lists(self): + self.assertEqual( + '\n'.join( + [ + "#!/bin/sh", + "echo hi mom", + "'echo' 'hi dad'", + "'echo' 'hi' 'sis'", + "", + ] + ), + util.shellify( + ["echo hi mom", ["echo", "hi dad"], ('echo', 'hi', 'sis')] + ), + ) + + def test_supports_comments(self): + self.assertEqual( + '\n'.join(["#!/bin/sh", "echo start", "echo end", ""]), + util.shellify(["echo start", None, "echo end"]), + ) + + +class TestGetHostnameFqdn(CiTestCase): + def test_get_hostname_fqdn_from_only_cfg_fqdn(self): + """When cfg only has the fqdn key, derive hostname and fqdn from it.""" + hostname, fqdn = util.get_hostname_fqdn( + cfg={'fqdn': 'myhost.domain.com'}, cloud=None + ) + self.assertEqual('myhost', hostname) + self.assertEqual('myhost.domain.com', fqdn) + + def test_get_hostname_fqdn_from_cfg_fqdn_and_hostname(self): + """When cfg has both fqdn and hostname keys, return them.""" + hostname, fqdn = util.get_hostname_fqdn( + cfg={'fqdn': 'myhost.domain.com', 'hostname': 'other'}, cloud=None + ) + self.assertEqual('other', hostname) + self.assertEqual('myhost.domain.com', fqdn) + + def test_get_hostname_fqdn_from_cfg_hostname_with_domain(self): + """When cfg has only hostname key which represents a fqdn, use that.""" + hostname, fqdn = util.get_hostname_fqdn( + cfg={'hostname': 'myhost.domain.com'}, cloud=None + ) + self.assertEqual('myhost', hostname) + self.assertEqual('myhost.domain.com', fqdn) + + def test_get_hostname_fqdn_from_cfg_hostname_without_domain(self): + """When cfg has a hostname without a '.' query cloud.get_hostname.""" + mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') + hostname, fqdn = util.get_hostname_fqdn( + cfg={'hostname': 'myhost'}, cloud=mycloud + ) + self.assertEqual('myhost', hostname) + self.assertEqual('cloudhost.mycloud.com', fqdn) + self.assertEqual( + [{'fqdn': True, 'metadata_only': False}], mycloud.calls + ) + + def test_get_hostname_fqdn_from_without_fqdn_or_hostname(self): + """When cfg has neither hostname nor fqdn cloud.get_hostname.""" + mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') + hostname, fqdn = util.get_hostname_fqdn(cfg={}, cloud=mycloud) + self.assertEqual('cloudhost', hostname) + self.assertEqual('cloudhost.mycloud.com', fqdn) + self.assertEqual( + [{'fqdn': True, 'metadata_only': False}, {'metadata_only': False}], + mycloud.calls, + ) + + def test_get_hostname_fqdn_from_passes_metadata_only_to_cloud(self): + """Calls to cloud.get_hostname pass the metadata_only parameter.""" + mycloud = FakeCloud('cloudhost', 'cloudhost.mycloud.com') + _hn, _fqdn = util.get_hostname_fqdn( + cfg={}, cloud=mycloud, metadata_only=True + ) + self.assertEqual( + [{'fqdn': True, 'metadata_only': True}, {'metadata_only': True}], + mycloud.calls, + ) + + +class TestBlkid(CiTestCase): + ids = { + "id01": "1111-1111", + "id02": "22222222-2222", + "id03": "33333333-3333", + "id04": "44444444-4444", + "id05": "55555555-5555-5555-5555-555555555555", + "id06": "66666666-6666-6666-6666-666666666666", + "id07": "52894610484658920398", + "id08": "86753098675309867530", + "id09": "99999999-9999-9999-9999-999999999999", + } + + blkid_out = dedent( + """\ + /dev/loop0: TYPE="squashfs" + /dev/loop1: TYPE="squashfs" + /dev/loop2: TYPE="squashfs" + /dev/loop3: TYPE="squashfs" + /dev/sda1: UUID="{id01}" TYPE="vfat" PARTUUID="{id02}" + /dev/sda2: UUID="{id03}" TYPE="ext4" PARTUUID="{id04}" + /dev/sda3: UUID="{id05}" TYPE="ext4" PARTUUID="{id06}" + /dev/sda4: LABEL="default" UUID="{id07}" UUID_SUB="{id08}" """ + """TYPE="zfs_member" PARTUUID="{id09}" + /dev/loop4: TYPE="squashfs" + """ + ) + + maxDiff = None + + def _get_expected(self): + return { + "/dev/loop0": {"DEVNAME": "/dev/loop0", "TYPE": "squashfs"}, + "/dev/loop1": {"DEVNAME": "/dev/loop1", "TYPE": "squashfs"}, + "/dev/loop2": {"DEVNAME": "/dev/loop2", "TYPE": "squashfs"}, + "/dev/loop3": {"DEVNAME": "/dev/loop3", "TYPE": "squashfs"}, + "/dev/loop4": {"DEVNAME": "/dev/loop4", "TYPE": "squashfs"}, + "/dev/sda1": { + "DEVNAME": "/dev/sda1", + "TYPE": "vfat", + "UUID": self.ids["id01"], + "PARTUUID": self.ids["id02"], + }, + "/dev/sda2": { + "DEVNAME": "/dev/sda2", + "TYPE": "ext4", + "UUID": self.ids["id03"], + "PARTUUID": self.ids["id04"], + }, + "/dev/sda3": { + "DEVNAME": "/dev/sda3", + "TYPE": "ext4", + "UUID": self.ids["id05"], + "PARTUUID": self.ids["id06"], + }, + "/dev/sda4": { + "DEVNAME": "/dev/sda4", + "TYPE": "zfs_member", + "LABEL": "default", + "UUID": self.ids["id07"], + "UUID_SUB": self.ids["id08"], + "PARTUUID": self.ids["id09"], + }, + } + + @mock.patch("cloudinit.subp.subp") + def test_functional_blkid(self, m_subp): + m_subp.return_value = (self.blkid_out.format(**self.ids), "") + self.assertEqual(self._get_expected(), util.blkid()) + m_subp.assert_called_with( + ["blkid", "-o", "full"], capture=True, decode="replace" + ) + + @mock.patch("cloudinit.subp.subp") + def test_blkid_no_cache_uses_no_cache(self, m_subp): + """blkid should turn off cache if disable_cache is true.""" + m_subp.return_value = (self.blkid_out.format(**self.ids), "") + self.assertEqual(self._get_expected(), util.blkid(disable_cache=True)) + m_subp.assert_called_with( + ["blkid", "-o", "full", "-c", "/dev/null"], + capture=True, + decode="replace", + ) + + +@mock.patch('cloudinit.subp.subp') +class TestUdevadmSettle(CiTestCase): + def test_with_no_params(self, m_subp): + """called with no parameters.""" + util.udevadm_settle() + m_subp.called_once_with(mock.call(['udevadm', 'settle'])) + + def test_with_exists_and_not_exists(self, m_subp): + """with exists=file where file does not exist should invoke subp.""" + mydev = self.tmp_path("mydev") + util.udevadm_settle(exists=mydev) + m_subp.called_once_with( + ['udevadm', 'settle', '--exit-if-exists=%s' % mydev] + ) + + def test_with_exists_and_file_exists(self, m_subp): + """with exists=file where file does exist should not invoke subp.""" + mydev = self.tmp_path("mydev") + util.write_file(mydev, "foo\n") + util.udevadm_settle(exists=mydev) + self.assertIsNone(m_subp.call_args) + + def test_with_timeout_int(self, m_subp): + """timeout can be an integer.""" + timeout = 9 + util.udevadm_settle(timeout=timeout) + m_subp.called_once_with( + ['udevadm', 'settle', '--timeout=%s' % timeout] + ) + + def test_with_timeout_string(self, m_subp): + """timeout can be a string.""" + timeout = "555" + util.udevadm_settle(timeout=timeout) + m_subp.assert_called_once_with( + ['udevadm', 'settle', '--timeout=%s' % timeout] + ) + + def test_with_exists_and_timeout(self, m_subp): + """test call with both exists and timeout.""" + mydev = self.tmp_path("mydev") + timeout = "3" + util.udevadm_settle(exists=mydev) + m_subp.called_once_with( + [ + 'udevadm', + 'settle', + '--exit-if-exists=%s' % mydev, + '--timeout=%s' % timeout, + ] + ) + + def test_subp_exception_raises_to_caller(self, m_subp): + m_subp.side_effect = subp.ProcessExecutionError("BOOM") + self.assertRaises(subp.ProcessExecutionError, util.udevadm_settle) + + +@mock.patch('os.path.exists') +class TestGetLinuxDistro(CiTestCase): + def setUp(self): + # python2 has no lru_cache, and therefore, no cache_clear() + if hasattr(util.get_linux_distro, "cache_clear"): + util.get_linux_distro.cache_clear() + + @classmethod + def os_release_exists(self, path): + """Side effect function""" + if path == '/etc/os-release': + return 1 + + @classmethod + def redhat_release_exists(self, path): + """Side effect function""" + if path == '/etc/redhat-release': + return 1 + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_distro_quoted_name(self, m_os_release, m_path_exists): + """Verify we get the correct name if the os-release file has + the distro name in quotes""" + m_os_release.return_value = OS_RELEASE_SLES + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('sles', '12.3', platform.machine()), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_distro_bare_name(self, m_os_release, m_path_exists): + """Verify we get the correct name if the os-release file does not + have the distro name in quotes""" + m_os_release.return_value = OS_RELEASE_UBUNTU + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('ubuntu', '16.04', 'xenial'), dist) + + @mock.patch('platform.system') + @mock.patch('platform.release') + @mock.patch('cloudinit.util._parse_redhat_release') + def test_get_linux_freebsd( + self, + m_parse_redhat_release, + m_platform_release, + m_platform_system, + m_path_exists, + ): + """Verify we get the correct name and release name on FreeBSD.""" + m_path_exists.return_value = False + m_platform_release.return_value = '12.0-RELEASE-p10' + m_platform_system.return_value = 'FreeBSD' + m_parse_redhat_release.return_value = {} + util.is_BSD.cache_clear() + dist = util.get_linux_distro() + self.assertEqual(('freebsd', '12.0-RELEASE-p10', ''), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_centos6(self, m_os_release, m_path_exists): + """Verify we get the correct name and release name on CentOS 6.""" + m_os_release.return_value = REDHAT_RELEASE_CENTOS_6 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('centos', '6.10', 'Final'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_centos7_redhat_release(self, m_os_release, m_exists): + """Verify the correct release info on CentOS 7 without os-release.""" + m_os_release.return_value = REDHAT_RELEASE_CENTOS_7 + m_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('centos', '7.5.1804', 'Core'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_redhat7_osrelease(self, m_os_release, m_path_exists): + """Verify redhat 7 read from os-release.""" + m_os_release.return_value = OS_RELEASE_REDHAT_7 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('redhat', '7.5', 'Maipo'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_redhat7_rhrelease(self, m_os_release, m_path_exists): + """Verify redhat 7 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_REDHAT_7 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('redhat', '7.5', 'Maipo'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_redhat6_rhrelease(self, m_os_release, m_path_exists): + """Verify redhat 6 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_REDHAT_6 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('redhat', '6.10', 'Santiago'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_copr_centos(self, m_os_release, m_path_exists): + """Verify we get the correct name and release name on COPR CentOS.""" + m_os_release.return_value = OS_RELEASE_CENTOS + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('centos', '7', 'Core'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_almalinux8_rhrelease(self, m_os_release, m_path_exists): + """Verify almalinux 8 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_ALMALINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('almalinux', '8.3', 'Purple Manul'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_almalinux8_osrelease(self, m_os_release, m_path_exists): + """Verify almalinux 8 read from os-release.""" + m_os_release.return_value = OS_RELEASE_ALMALINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('almalinux', '8.3', 'Purple Manul'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_eurolinux7_rhrelease(self, m_os_release, m_path_exists): + """Verify eurolinux 7 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_EUROLINUX_7 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('eurolinux', '7.9', 'Minsk'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_eurolinux7_osrelease(self, m_os_release, m_path_exists): + """Verify eurolinux 7 read from os-release.""" + m_os_release.return_value = OS_RELEASE_EUROLINUX_7 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('eurolinux', '7.9', 'Minsk'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_eurolinux8_rhrelease(self, m_os_release, m_path_exists): + """Verify eurolinux 8 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_EUROLINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('eurolinux', '8.4', 'Vaduz'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_eurolinux8_osrelease(self, m_os_release, m_path_exists): + """Verify eurolinux 8 read from os-release.""" + m_os_release.return_value = OS_RELEASE_EUROLINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('eurolinux', '8.4', 'Vaduz'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_rocky8_rhrelease(self, m_os_release, m_path_exists): + """Verify rocky linux 8 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_ROCKY_8 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('rocky', '8.3', 'Green Obsidian'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_rocky8_osrelease(self, m_os_release, m_path_exists): + """Verify rocky linux 8 read from os-release.""" + m_os_release.return_value = OS_RELEASE_ROCKY_8 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('rocky', '8.3', 'Green Obsidian'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_virtuozzo8_rhrelease(self, m_os_release, m_path_exists): + """Verify virtuozzo linux 8 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_VIRTUOZZO_8 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('virtuozzo', '8', 'Virtuozzo Linux'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_virtuozzo8_osrelease(self, m_os_release, m_path_exists): + """Verify virtuozzo linux 8 read from os-release.""" + m_os_release.return_value = OS_RELEASE_VIRTUOZZO_8 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('virtuozzo', '8', 'Virtuozzo Linux'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_cloud8_rhrelease(self, m_os_release, m_path_exists): + """Verify cloudlinux 8 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_CLOUDLINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('cloudlinux', '8.4', 'Valery Rozhdestvensky'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_cloud8_osrelease(self, m_os_release, m_path_exists): + """Verify cloudlinux 8 read from os-release.""" + m_os_release.return_value = OS_RELEASE_CLOUDLINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('cloudlinux', '8.4', 'Valery Rozhdestvensky'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_debian(self, m_os_release, m_path_exists): + """Verify we get the correct name and release name on Debian.""" + m_os_release.return_value = OS_RELEASE_DEBIAN + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('debian', '9', 'stretch'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_openeuler(self, m_os_release, m_path_exists): + """Verify get the correct name and release name on Openeuler.""" + m_os_release.return_value = OS_RELEASE_OPENEULER_20 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('openEuler', '20.03', 'LTS-SP2'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_opensuse(self, m_os_release, m_path_exists): + """Verify we get the correct name and machine arch on openSUSE + prior to openSUSE Leap 15. + """ + m_os_release.return_value = OS_RELEASE_OPENSUSE + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('opensuse', '42.3', platform.machine()), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_opensuse_l15(self, m_os_release, m_path_exists): + """Verify we get the correct name and machine arch on openSUSE + for openSUSE Leap 15.0 and later. + """ + m_os_release.return_value = OS_RELEASE_OPENSUSE_L15 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('opensuse-leap', '15.0', platform.machine()), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_opensuse_tw(self, m_os_release, m_path_exists): + """Verify we get the correct name and machine arch on openSUSE + for openSUSE Tumbleweed + """ + m_os_release.return_value = OS_RELEASE_OPENSUSE_TW + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual( + ('opensuse-tumbleweed', '20180920', platform.machine()), dist + ) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_photon_os_release(self, m_os_release, m_path_exists): + """Verify we get the correct name and machine arch on PhotonOS""" + m_os_release.return_value = OS_RELEASE_PHOTON + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('photon', '4.0', 'VMware Photon OS/Linux'), dist) + + @mock.patch('platform.system') + @mock.patch('platform.dist', create=True) + def test_get_linux_distro_no_data( + self, m_platform_dist, m_platform_system, m_path_exists + ): + """Verify we get no information if os-release does not exist""" + m_platform_dist.return_value = ('', '', '') + m_platform_system.return_value = "Linux" + m_path_exists.return_value = 0 + dist = util.get_linux_distro() + self.assertEqual(('', '', ''), dist) + + @mock.patch('platform.system') + @mock.patch('platform.dist', create=True) + def test_get_linux_distro_no_impl( + self, m_platform_dist, m_platform_system, m_path_exists + ): + """Verify we get an empty tuple when no information exists and + Exceptions are not propagated""" + m_platform_dist.side_effect = Exception() + m_platform_system.return_value = "Linux" + m_path_exists.return_value = 0 + dist = util.get_linux_distro() + self.assertEqual(('', '', ''), dist) + + @mock.patch('platform.system') + @mock.patch('platform.dist', create=True) + def test_get_linux_distro_plat_data( + self, m_platform_dist, m_platform_system, m_path_exists + ): + """Verify we get the correct platform information""" + m_platform_dist.return_value = ('foo', '1.1', 'aarch64') + m_platform_system.return_value = "Linux" + m_path_exists.return_value = 0 + dist = util.get_linux_distro() + self.assertEqual(('foo', '1.1', 'aarch64'), dist) + + +class TestGetVariant: + @pytest.mark.parametrize( + 'info, expected_variant', + [ + ({'system': 'Linux', 'dist': ('almalinux',)}, 'almalinux'), + ({'system': 'linux', 'dist': ('alpine',)}, 'alpine'), + ({'system': 'linux', 'dist': ('arch',)}, 'arch'), + ({'system': 'linux', 'dist': ('centos',)}, 'centos'), + ({'system': 'linux', 'dist': ('cloudlinux',)}, 'cloudlinux'), + ({'system': 'linux', 'dist': ('debian',)}, 'debian'), + ({'system': 'linux', 'dist': ('eurolinux',)}, 'eurolinux'), + ({'system': 'linux', 'dist': ('fedora',)}, 'fedora'), + ({'system': 'linux', 'dist': ('openEuler',)}, 'openeuler'), + ({'system': 'linux', 'dist': ('photon',)}, 'photon'), + ({'system': 'linux', 'dist': ('rhel',)}, 'rhel'), + ({'system': 'linux', 'dist': ('rocky',)}, 'rocky'), + ({'system': 'linux', 'dist': ('suse',)}, 'suse'), + ({'system': 'linux', 'dist': ('virtuozzo',)}, 'virtuozzo'), + ({'system': 'linux', 'dist': ('ubuntu',)}, 'ubuntu'), + ({'system': 'linux', 'dist': ('linuxmint',)}, 'ubuntu'), + ({'system': 'linux', 'dist': ('mint',)}, 'ubuntu'), + ({'system': 'linux', 'dist': ('redhat',)}, 'rhel'), + ({'system': 'linux', 'dist': ('opensuse',)}, 'suse'), + ({'system': 'linux', 'dist': ('opensuse-tumbleweed',)}, 'suse'), + ({'system': 'linux', 'dist': ('opensuse-leap',)}, 'suse'), + ({'system': 'linux', 'dist': ('sles',)}, 'suse'), + ({'system': 'linux', 'dist': ('sle_hpc',)}, 'suse'), + ({'system': 'linux', 'dist': ('my_distro',)}, 'linux'), + ({'system': 'Windows', 'dist': ('dontcare',)}, 'windows'), + ({'system': 'Darwin', 'dist': ('dontcare',)}, 'darwin'), + ({'system': 'Freebsd', 'dist': ('dontcare',)}, 'freebsd'), + ({'system': 'Netbsd', 'dist': ('dontcare',)}, 'netbsd'), + ({'system': 'Openbsd', 'dist': ('dontcare',)}, 'openbsd'), + ({'system': 'Dragonfly', 'dist': ('dontcare',)}, 'dragonfly'), + ], + ) + def test_get_variant(self, info, expected_variant): + """Verify we get the correct variant name""" + assert util._get_variant(info) == expected_variant + + +class TestJsonDumps(CiTestCase): + def test_is_str(self): + """json_dumps should return a string.""" + self.assertTrue(isinstance(util.json_dumps({'abc': '123'}), str)) + + def test_utf8(self): + smiley = '\\ud83d\\ude03' + self.assertEqual( + {'smiley': smiley}, json.loads(util.json_dumps({'smiley': smiley})) + ) + + def test_non_utf8(self): + blob = b'\xba\x03Qx-#y\xea' + self.assertEqual( + {'blob': 'ci-b64:' + base64.b64encode(blob).decode('utf-8')}, + json.loads(util.json_dumps({'blob': blob})), + ) + + +@mock.patch('os.path.exists') +class TestIsLXD(CiTestCase): + def test_is_lxd_true_on_sock_device(self, m_exists): + """When lxd's /dev/lxd/sock exists, is_lxd returns true.""" + m_exists.return_value = True + self.assertTrue(util.is_lxd()) + m_exists.assert_called_once_with('/dev/lxd/sock') + def test_is_lxd_false_when_sock_device_absent(self, m_exists): + """When lxd's /dev/lxd/sock is absent, is_lxd returns false.""" + m_exists.return_value = False + self.assertFalse(util.is_lxd()) + m_exists.assert_called_once_with('/dev/lxd/sock') + + +class TestReadCcFromCmdline: + @pytest.mark.parametrize( + "cmdline,expected_cfg", + [ + # Return None if cmdline has no cc:<YAML>end_cc content. + (CiTestCase.random_string(), None), + # Return None if YAML content is empty string. + ('foo cc: end_cc bar', None), + # Return expected dictionary without trailing end_cc marker. + ('foo cc: ssh_pwauth: true', {'ssh_pwauth': True}), + # Return expected dictionary w escaped newline and no end_cc. + ('foo cc: ssh_pwauth: true\\n', {'ssh_pwauth': True}), + # Return expected dictionary of yaml between cc: and end_cc. + ('foo cc: ssh_pwauth: true end_cc bar', {'ssh_pwauth': True}), + # Return dict with list value w escaped newline, no end_cc. + ( + 'cc: ssh_import_id: [smoser, kirkland]\\n', + {'ssh_import_id': ['smoser', 'kirkland']}, + ), + # Parse urlencoded brackets in yaml content. + ( + 'cc: ssh_import_id: %5Bsmoser, kirkland%5D end_cc', + {'ssh_import_id': ['smoser', 'kirkland']}, + ), + # Parse complete urlencoded yaml content. + ( + 'cc: ssh_import_id%3A%20%5Buser1%2C%20user2%5D end_cc', + {'ssh_import_id': ['user1', 'user2']}, + ), + # Parse nested dictionary in yaml content. + ( + 'cc: ntp: {enabled: true, ntp_client: myclient} end_cc', + {'ntp': {'enabled': True, 'ntp_client': 'myclient'}}, + ), + # Parse single mapping value in yaml content. + ('cc: ssh_import_id: smoser end_cc', {'ssh_import_id': 'smoser'}), + # Parse multiline content with multiple mapping and nested lists. + ( + ( + 'cc: ssh_import_id: [smoser, bob]\\n' + 'runcmd: [ [ ls, -l ], echo hi ] end_cc' + ), + { + 'ssh_import_id': ['smoser', 'bob'], + 'runcmd': [['ls', '-l'], 'echo hi'], + }, + ), + # Parse multiline encoded content w/ mappings and nested lists. + ( + ( + 'cc: ssh_import_id: %5Bsmoser, bob%5D\\n' + 'runcmd: [ [ ls, -l ], echo hi ] end_cc' + ), + { + 'ssh_import_id': ['smoser', 'bob'], + 'runcmd': [['ls', '-l'], 'echo hi'], + }, + ), + # test encoded escaped newlines work. + # + # unquote(encoded_content) + # 'ssh_import_id: [smoser, bob]\\nruncmd: [ [ ls, -l ], echo hi ]' + ( + ( + 'cc: ' + + ( + 'ssh_import_id%3A%20%5Bsmoser%2C%20bob%5D%5Cn' + 'runcmd%3A%20%5B%20%5B%20ls%2C%20-l%20%5D%2C' + '%20echo%20hi%20%5D' + ) + + ' end_cc' + ), + { + 'ssh_import_id': ['smoser', 'bob'], + 'runcmd': [['ls', '-l'], 'echo hi'], + }, + ), + # test encoded newlines work. + # + # unquote(encoded_content) + # 'ssh_import_id: [smoser, bob]\nruncmd: [ [ ls, -l ], echo hi ]' + ( + ( + "cc: " + + ( + 'ssh_import_id%3A%20%5Bsmoser%2C%20bob%5D%0A' + 'runcmd%3A%20%5B%20%5B%20ls%2C%20-l%20%5D%2C' + '%20echo%20hi%20%5D' + ) + + ' end_cc' + ), + { + 'ssh_import_id': ['smoser', 'bob'], + 'runcmd': [['ls', '-l'], 'echo hi'], + }, + ), + # Parse and merge multiple yaml content sections. + ( + ( + 'cc:ssh_import_id: [smoser, bob] end_cc ' + 'cc: runcmd: [ [ ls, -l ] ] end_cc' + ), + {'ssh_import_id': ['smoser', 'bob'], 'runcmd': [['ls', '-l']]}, + ), + # Parse and merge multiple encoded yaml content sections. + ( + ( + 'cc:ssh_import_id%3A%20%5Bsmoser%5D end_cc ' + 'cc:runcmd%3A%20%5B%20%5B%20ls%2C%20-l%20%5D%20%5D end_cc' + ), + {'ssh_import_id': ['smoser'], 'runcmd': [['ls', '-l']]}, + ), + ], + ) + def test_read_conf_from_cmdline_config(self, expected_cfg, cmdline): + assert expected_cfg == util.read_conf_from_cmdline(cmdline=cmdline) + + +class TestMountCb: + """Tests for ``util.mount_cb``. + + These tests consider the "unit" under test to be ``util.mount_cb`` and + ``util.unmounter``, which is only used by ``mount_cb``. + + TODO: Test default mtype determination + TODO: Test the if/else branch that actually performs the mounting operation + """ + + @pytest.yield_fixture + def already_mounted_device_and_mountdict(self): + """Mock an already-mounted device, and yield (device, mount dict)""" + device = "/dev/fake0" + mountpoint = "/mnt/fake" + with mock.patch("cloudinit.util.subp.subp"): + with mock.patch("cloudinit.util.mounts") as m_mounts: + mounts = {device: {"mountpoint": mountpoint}} + m_mounts.return_value = mounts + yield device, mounts[device] + + @pytest.fixture + def already_mounted_device(self, already_mounted_device_and_mountdict): + """already_mounted_device_and_mountdict, but return only the device""" + return already_mounted_device_and_mountdict[0] + + @pytest.mark.parametrize( + "mtype,expected", + [ + # While the filesystem is called iso9660, the mount type is cd9660 + ("iso9660", "cd9660"), + # vfat is generally called "msdos" on BSD + ("vfat", "msdos"), + # judging from man pages, only FreeBSD has this alias + ("msdosfs", "msdos"), + # Test happy path + ("ufs", "ufs"), + ], + ) + @mock.patch("cloudinit.util.is_Linux", autospec=True) + @mock.patch("cloudinit.util.is_BSD", autospec=True) + @mock.patch("cloudinit.util.subp.subp") + @mock.patch("cloudinit.temp_utils.tempdir", autospec=True) + def test_normalize_mtype_on_bsd( + self, m_tmpdir, m_subp, m_is_BSD, m_is_Linux, mtype, expected + ): + m_is_BSD.return_value = True + m_is_Linux.return_value = False + m_tmpdir.return_value.__enter__ = mock.Mock( + autospec=True, return_value="/tmp/fake" + ) + m_tmpdir.return_value.__exit__ = mock.Mock( + autospec=True, return_value=True + ) + callback = mock.Mock(autospec=True) + + util.mount_cb('/dev/fake0', callback, mtype=mtype) + assert ( + mock.call( + [ + "mount", + "-o", + "ro", + "-t", + expected, + "/dev/fake0", + "/tmp/fake", + ], + update_env=None, + ) + in m_subp.call_args_list + ) + + @pytest.mark.parametrize("invalid_mtype", [int(0), float(0.0), dict()]) + def test_typeerror_raised_for_invalid_mtype(self, invalid_mtype): + with pytest.raises(TypeError): + util.mount_cb(mock.Mock(), mock.Mock(), mtype=invalid_mtype) + + @mock.patch("cloudinit.util.subp.subp") + def test_already_mounted_does_not_mount_or_umount_anything( + self, m_subp, already_mounted_device + ): + util.mount_cb(already_mounted_device, mock.Mock()) + + assert 0 == m_subp.call_count + + @pytest.mark.parametrize("trailing_slash_in_mounts", ["/", ""]) + def test_already_mounted_calls_callback( + self, trailing_slash_in_mounts, already_mounted_device_and_mountdict + ): + device, mount_dict = already_mounted_device_and_mountdict + mountpoint = mount_dict["mountpoint"] + mount_dict["mountpoint"] += trailing_slash_in_mounts + + callback = mock.Mock() + util.mount_cb(device, callback) + + # The mountpoint passed to callback should always have a trailing + # slash, regardless of the input + assert [mock.call(mountpoint + "/")] == callback.call_args_list + + def test_already_mounted_calls_callback_with_data( + self, already_mounted_device + ): + callback = mock.Mock() + util.mount_cb( + already_mounted_device, callback, data=mock.sentinel.data + ) + + assert [ + mock.call(mock.ANY, mock.sentinel.data) + ] == callback.call_args_list + + +@mock.patch("cloudinit.util.write_file") +class TestEnsureFile: + """Tests for ``cloudinit.util.ensure_file``.""" + + def test_parameters_passed_through(self, m_write_file): + """Test the parameters in the signature are passed to write_file.""" + util.ensure_file( + mock.sentinel.path, + mode=mock.sentinel.mode, + preserve_mode=mock.sentinel.preserve_mode, + ) + + assert 1 == m_write_file.call_count + args, kwargs = m_write_file.call_args + assert (mock.sentinel.path,) == args + assert mock.sentinel.mode == kwargs["mode"] + assert mock.sentinel.preserve_mode == kwargs["preserve_mode"] + + @pytest.mark.parametrize( + "kwarg,expected", + [ + # Files should be world-readable by default + ("mode", 0o644), + # The previous behaviour of not preserving mode should be retained + ("preserve_mode", False), + ], + ) + def test_defaults(self, m_write_file, kwarg, expected): + """Test that ensure_file defaults appropriately.""" + util.ensure_file(mock.sentinel.path) + + assert 1 == m_write_file.call_count + _args, kwargs = m_write_file.call_args + assert expected == kwargs[kwarg] + + def test_static_parameters_are_passed(self, m_write_file): + """Test that the static write_files parameters are passed correctly.""" + util.ensure_file(mock.sentinel.path) + + assert 1 == m_write_file.call_count + _args, kwargs = m_write_file.call_args + assert "" == kwargs["content"] + assert "ab" == kwargs["omode"] + + +@mock.patch("cloudinit.util.grp.getgrnam") +@mock.patch("cloudinit.util.os.setgid") +@mock.patch("cloudinit.util.os.umask") +class TestRedirectOutputPreexecFn: + """This tests specifically the preexec_fn used in redirect_output.""" + + @pytest.fixture(params=["outfmt", "errfmt"]) + def preexec_fn(self, request): + """A fixture to gather the preexec_fn used by redirect_output. + + This enables simpler direct testing of it, and parameterises any tests + using it to cover both the stdout and stderr code paths. + """ + test_string = "| piped output to invoke subprocess" + if request.param == "outfmt": + args = (test_string, None) + elif request.param == "errfmt": + args = (None, test_string) + with mock.patch("cloudinit.util.subprocess.Popen") as m_popen: + util.redirect_output(*args) + + assert 1 == m_popen.call_count + _args, kwargs = m_popen.call_args + assert "preexec_fn" in kwargs, "preexec_fn not passed to Popen" + return kwargs["preexec_fn"] + + def test_preexec_fn_sets_umask( + self, m_os_umask, _m_setgid, _m_getgrnam, preexec_fn + ): + """preexec_fn should set a mask that avoids world-readable files.""" + preexec_fn() + + assert [mock.call(0o037)] == m_os_umask.call_args_list + + def test_preexec_fn_sets_group_id_if_adm_group_present( + self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn + ): + """We should setgrp to adm if present, so files are owned by them.""" + fake_group = mock.Mock(gr_gid=mock.sentinel.gr_gid) + m_getgrnam.return_value = fake_group + + preexec_fn() + + assert [mock.call("adm")] == m_getgrnam.call_args_list + assert [mock.call(mock.sentinel.gr_gid)] == m_setgid.call_args_list + + def test_preexec_fn_handles_absent_adm_group_gracefully( + self, _m_os_umask, m_setgid, m_getgrnam, preexec_fn + ): + """We should handle an absent adm group gracefully.""" + m_getgrnam.side_effect = KeyError("getgrnam(): name not found: 'adm'") + + preexec_fn() + + assert 0 == m_setgid.call_count + + +class FakeSelinux(object): def __init__(self, match_what): self.match_what = match_what self.restored = [] @@ -175,8 +1463,9 @@ class TestWriteFile(helpers.TestCase): fake_se = FakeSelinux(my_file) - with mock.patch.object(importer, 'import_module', - return_value=fake_se) as mockobj: + with mock.patch.object( + importer, 'import_module', return_value=fake_se + ) as mockobj: with util.SeLinuxGuard(my_file) as is_on: self.assertTrue(is_on) @@ -261,8 +1550,9 @@ class TestKeyValStrings(helpers.TestCase): class TestGetCmdline(helpers.TestCase): def test_cmdline_reads_debug_env(self): - with mock.patch.dict("os.environ", - values={'DEBUG_PROC_CMDLINE': 'abcd 123'}): + with mock.patch.dict( + "os.environ", values={'DEBUG_PROC_CMDLINE': 'abcd 123'} + ): ret = util.get_cmdline() self.assertEqual("abcd 123", ret) @@ -279,52 +1569,68 @@ class TestLoadYaml(helpers.CiTestCase): '''Any unallowed types result in returning default; log the issue.''' # for now, anything not in the allowed list just returns the default. myyaml = yaml.dump({'1': "one"}) - self.assertEqual(util.load_yaml(blob=myyaml, - default=self.mydefault, - allowed=(str,)), - self.mydefault) + self.assertEqual( + util.load_yaml( + blob=myyaml, default=self.mydefault, allowed=(str,) + ), + self.mydefault, + ) regex = re.compile( r'Yaml load allows \(<(class|type) \'str\'>,\) root types, but' - r' got dict') - self.assertTrue(regex.search(self.logs.getvalue()), - msg='Missing expected yaml load error') + r' got dict' + ) + self.assertTrue( + regex.search(self.logs.getvalue()), + msg='Missing expected yaml load error', + ) def test_bogus_scan_error_returns_default(self): '''On Yaml scan error, load_yaml returns the default and logs issue.''' badyaml = "1\n 2:" - self.assertEqual(util.load_yaml(blob=badyaml, - default=self.mydefault), - self.mydefault) + self.assertEqual( + util.load_yaml(blob=badyaml, default=self.mydefault), + self.mydefault, + ) self.assertIn( 'Failed loading yaml blob. Invalid format at line 2 column 3:' ' "mapping values are not allowed here', - self.logs.getvalue()) + self.logs.getvalue(), + ) def test_bogus_parse_error_returns_default(self): '''On Yaml parse error, load_yaml returns default and logs issue.''' badyaml = "{}}" - self.assertEqual(util.load_yaml(blob=badyaml, - default=self.mydefault), - self.mydefault) + self.assertEqual( + util.load_yaml(blob=badyaml, default=self.mydefault), + self.mydefault, + ) self.assertIn( 'Failed loading yaml blob. Invalid format at line 1 column 3:' " \"expected \'<document start>\', but found \'}\'", - self.logs.getvalue()) + self.logs.getvalue(), + ) def test_unsafe_types(self): # should not load complex types - unsafe_yaml = yaml.dump((1, 2, 3,)) - self.assertEqual(util.load_yaml(blob=unsafe_yaml, - default=self.mydefault), - self.mydefault) + unsafe_yaml = yaml.dump( + ( + 1, + 2, + 3, + ) + ) + self.assertEqual( + util.load_yaml(blob=unsafe_yaml, default=self.mydefault), + self.mydefault, + ) def test_python_unicode(self): # complex type of python/unicode is explicitly allowed myobj = {'1': "FOOBAR"} safe_yaml = yaml.dump(myobj) - self.assertEqual(util.load_yaml(blob=safe_yaml, - default=self.mydefault), - myobj) + self.assertEqual( + util.load_yaml(blob=safe_yaml, default=self.mydefault), myobj + ) def test_none_returns_default(self): """If yaml.load returns None, then default should be returned.""" @@ -332,13 +1638,16 @@ class TestLoadYaml(helpers.CiTestCase): mdef = self.mydefault self.assertEqual( [(b, self.mydefault) for b in blobs], - [(b, util.load_yaml(blob=b, default=mdef)) for b in blobs]) + [(b, util.load_yaml(blob=b, default=mdef)) for b in blobs], + ) class TestMountinfoParsing(helpers.ResourceUsingTestCase): def test_invalid_mountinfo(self): - line = ("20 1 252:1 / / rw,relatime - ext4 /dev/mapper/vg0-root" - "rw,errors=remount-ro,data=ordered") + line = ( + "20 1 252:1 / / rw,relatime - ext4 /dev/mapper/vg0-root" + "rw,errors=remount-ro,data=ordered" + ) elements = line.split() for i in range(len(elements) + 1): lines = [' '.join(elements[0:i])] @@ -398,7 +1707,8 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase): m_os.path.exists.return_value = True # mock subp command from util.get_mount_info_fs_on_zpool zpool_output.return_value = ( - helpers.readResource('zpool_status_simple.txt'), '' + helpers.readResource('zpool_status_simple.txt'), + '', ) # save function return values and do asserts ret = util.get_device_info_from_zpool('vmzroot') @@ -431,7 +1741,8 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase): m_os.path.exists.return_value = True # mock subp command from util.get_mount_info_fs_on_zpool zpool_output.return_value = ( - helpers.readResource('zpool_status_simple.txt'), 'error' + helpers.readResource('zpool_status_simple.txt'), + 'error', ) # save function return values and do asserts ret = util.get_device_info_from_zpool('vmzroot') @@ -440,7 +1751,9 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase): @mock.patch('cloudinit.subp.subp') def test_parse_mount_with_ext(self, mount_out): mount_out.return_value = ( - helpers.readResource('mount_parse_ext.txt'), '') + helpers.readResource('mount_parse_ext.txt'), + '', + ) # this one is valid and exists in mount_parse_ext.txt ret = util.parse_mount('/var') self.assertEqual(('/dev/mapper/vg00-lv_var', 'ext4', '/var'), ret) @@ -457,7 +1770,9 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase): @mock.patch('cloudinit.subp.subp') def test_parse_mount_with_zfs(self, mount_out): mount_out.return_value = ( - helpers.readResource('mount_parse_zfs.txt'), '') + helpers.readResource('mount_parse_zfs.txt'), + '', + ) # this one is valid and exists in mount_parse_zfs.txt ret = util.parse_mount('/var') self.assertEqual(('vmzroot/ROOT/freebsd/var', 'zfs', '/var'), ret) @@ -470,20 +1785,21 @@ class TestMountinfoParsing(helpers.ResourceUsingTestCase): class TestIsX86(helpers.CiTestCase): - def test_is_x86_matches_x86_types(self): """is_x86 returns True if CPU architecture matches.""" matched_arches = ['x86_64', 'i386', 'i586', 'i686'] for arch in matched_arches: self.assertTrue( - util.is_x86(arch), 'Expected is_x86 for arch "%s"' % arch) + util.is_x86(arch), 'Expected is_x86 for arch "%s"' % arch + ) def test_is_x86_unmatched_types(self): """is_x86 returns Fale on non-intel x86 architectures.""" unmatched_arches = ['ia64', '9000/800', 'arm64v71'] for arch in unmatched_arches: self.assertFalse( - util.is_x86(arch), 'Expected not is_x86 for arch "%s"' % arch) + util.is_x86(arch), 'Expected not is_x86 for arch "%s"' % arch + ) @mock.patch('cloudinit.util.os.uname') def test_is_x86_calls_uname_for_architecture(self, m_uname): @@ -493,7 +1809,6 @@ class TestIsX86(helpers.CiTestCase): class TestGetConfigLogfiles(helpers.CiTestCase): - def test_empty_cfg_returns_empty_list(self): """An empty config passed to get_config_logfiles returns empty list.""" self.assertEqual([], util.get_config_logfiles(None)) @@ -502,36 +1817,53 @@ class TestGetConfigLogfiles(helpers.CiTestCase): def test_default_log_file_present(self): """When default_log_file is set get_config_logfiles finds it.""" self.assertEqual( - ['/my.log'], - util.get_config_logfiles({'def_log_file': '/my.log'})) + ['/my.log'], util.get_config_logfiles({'def_log_file': '/my.log'}) + ) def test_output_logs_parsed_when_teeing_files(self): """When output configuration is parsed when teeing files.""" self.assertEqual( ['/himom.log', '/my.log'], - sorted(util.get_config_logfiles({ - 'def_log_file': '/my.log', - 'output': {'all': '|tee -a /himom.log'}}))) + sorted( + util.get_config_logfiles( + { + 'def_log_file': '/my.log', + 'output': {'all': '|tee -a /himom.log'}, + } + ) + ), + ) def test_output_logs_parsed_when_redirecting(self): """When output configuration is parsed when redirecting to a file.""" self.assertEqual( ['/my.log', '/test.log'], - sorted(util.get_config_logfiles({ - 'def_log_file': '/my.log', - 'output': {'all': '>/test.log'}}))) + sorted( + util.get_config_logfiles( + { + 'def_log_file': '/my.log', + 'output': {'all': '>/test.log'}, + } + ) + ), + ) def test_output_logs_parsed_when_appending(self): """When output configuration is parsed when appending to a file.""" self.assertEqual( ['/my.log', '/test.log'], - sorted(util.get_config_logfiles({ - 'def_log_file': '/my.log', - 'output': {'all': '>> /test.log'}}))) + sorted( + util.get_config_logfiles( + { + 'def_log_file': '/my.log', + 'output': {'all': '>> /test.log'}, + } + ) + ), + ) class TestMultiLog(helpers.FilesystemMockingTestCase): - def _createConsole(self, root): os.mkdir(os.path.join(root, 'dev')) open(os.path.join(root, 'dev', 'console'), 'a').close() @@ -580,8 +1912,9 @@ class TestMultiLog(helpers.FilesystemMockingTestCase): log = mock.MagicMock() logged_string = 'something very important' util.multi_log(logged_string, log=log) - self.assertEqual([((mock.ANY, logged_string), {})], - log.log.call_args_list) + self.assertEqual( + [((mock.ANY, logged_string), {})], log.log.call_args_list + ) def test_newlines_stripped_from_log_call(self): log = mock.MagicMock() @@ -602,7 +1935,6 @@ class TestMultiLog(helpers.FilesystemMockingTestCase): class TestMessageFromString(helpers.TestCase): - def test_unicode_not_messed_up(self): roundtripped = util.message_from_string('\n').as_string() self.assertNotIn('\x00', roundtripped) @@ -618,8 +1950,9 @@ class TestReadSeeded(helpers.TestCase): ud = b"userdatablob" vd = b"vendordatablob" helpers.populate_dir( - self.tmp, {'meta-data': "key1: val1", 'user-data': ud, - 'vendor-data': vd}) + self.tmp, + {'meta-data': "key1: val1", 'user-data': ud, 'vendor-data': vd}, + ) sdir = self.tmp + os.path.sep (found_md, found_ud, found_vd) = util.read_seeded(sdir) @@ -638,7 +1971,8 @@ class TestReadSeededWithoutVendorData(helpers.TestCase): ud = b"userdatablob" vd = None helpers.populate_dir( - self.tmp, {'meta-data': "key1: val1", 'user-data': ud}) + self.tmp, {'meta-data': "key1: val1", 'user-data': ud} + ) sdir = self.tmp + os.path.sep (found_md, found_ud, found_vd) = util.read_seeded(sdir) @@ -649,6 +1983,7 @@ class TestReadSeededWithoutVendorData(helpers.TestCase): class TestEncode(helpers.TestCase): """Test the encoding functions""" + def test_decode_binary_plain_text_with_hex(self): blob = 'BOOTABLE_FLAG=\x80init=/bin/systemd' text = util.decode_binary(blob) @@ -657,12 +1992,14 @@ class TestEncode(helpers.TestCase): class TestProcessExecutionError(helpers.TestCase): - template = ('{description}\n' - 'Command: {cmd}\n' - 'Exit code: {exit_code}\n' - 'Reason: {reason}\n' - 'Stdout: {stdout}\n' - 'Stderr: {stderr}') + template = ( + '{description}\n' + 'Command: {cmd}\n' + 'Exit code: {exit_code}\n' + 'Reason: {reason}\n' + 'Stdout: {stdout}\n' + 'Stderr: {stderr}' + ) empty_attr = '-' empty_description = 'Unexpected error while running command.' @@ -671,23 +2008,37 @@ class TestProcessExecutionError(helpers.TestCase): msg = 'abc\ndef' formatted = 'abc\n{0}def'.format(' ' * 4) self.assertEqual(error._indent_text(msg, indent_level=4), formatted) - self.assertEqual(error._indent_text(msg.encode(), indent_level=4), - formatted.encode()) + self.assertEqual( + error._indent_text(msg.encode(), indent_level=4), + formatted.encode(), + ) self.assertIsInstance( - error._indent_text(msg.encode()), type(msg.encode())) + error._indent_text(msg.encode()), type(msg.encode()) + ) def test_pexec_error_type(self): self.assertIsInstance(subp.ProcessExecutionError(), IOError) def test_pexec_error_empty_msgs(self): error = subp.ProcessExecutionError() - self.assertTrue(all(attr == self.empty_attr for attr in - (error.stderr, error.stdout, error.reason))) + self.assertTrue( + all( + attr == self.empty_attr + for attr in (error.stderr, error.stdout, error.reason) + ) + ) self.assertEqual(error.description, self.empty_description) - self.assertEqual(str(error), self.template.format( - description=self.empty_description, exit_code=self.empty_attr, - reason=self.empty_attr, stdout=self.empty_attr, - stderr=self.empty_attr, cmd=self.empty_attr)) + self.assertEqual( + str(error), + self.template.format( + description=self.empty_description, + exit_code=self.empty_attr, + reason=self.empty_attr, + stdout=self.empty_attr, + stderr=self.empty_attr, + cmd=self.empty_attr, + ), + ) def test_pexec_error_single_line_msgs(self): stdout_msg = 'out out' @@ -695,33 +2046,46 @@ class TestProcessExecutionError(helpers.TestCase): cmd = 'test command' exit_code = 3 error = subp.ProcessExecutionError( - stdout=stdout_msg, stderr=stderr_msg, exit_code=3, cmd=cmd) - self.assertEqual(str(error), self.template.format( - description=self.empty_description, stdout=stdout_msg, - stderr=stderr_msg, exit_code=str(exit_code), - reason=self.empty_attr, cmd=cmd)) + stdout=stdout_msg, stderr=stderr_msg, exit_code=3, cmd=cmd + ) + self.assertEqual( + str(error), + self.template.format( + description=self.empty_description, + stdout=stdout_msg, + stderr=stderr_msg, + exit_code=str(exit_code), + reason=self.empty_attr, + cmd=cmd, + ), + ) def test_pexec_error_multi_line_msgs(self): # make sure bytes is converted handled properly when formatting stdout_msg = 'multi\nline\noutput message'.encode() stderr_msg = 'multi\nline\nerror message\n\n\n' error = subp.ProcessExecutionError( - stdout=stdout_msg, stderr=stderr_msg) + stdout=stdout_msg, stderr=stderr_msg + ) self.assertEqual( str(error), - '\n'.join(( - '{description}', - 'Command: {empty_attr}', - 'Exit code: {empty_attr}', - 'Reason: {empty_attr}', - 'Stdout: multi', - ' line', - ' output message', - 'Stderr: multi', - ' line', - ' error message', - )).format(description=self.empty_description, - empty_attr=self.empty_attr)) + '\n'.join( + ( + '{description}', + 'Command: {empty_attr}', + 'Exit code: {empty_attr}', + 'Reason: {empty_attr}', + 'Stdout: multi', + ' line', + ' output message', + 'Stderr: multi', + ' line', + ' error message', + ) + ).format( + description=self.empty_description, empty_attr=self.empty_attr + ), + ) class TestSystemIsSnappy(helpers.FilesystemMockingTestCase): @@ -758,7 +2122,8 @@ class TestSystemIsSnappy(helpers.FilesystemMockingTestCase): "BOOT_IMAGE=(loop)/kernel.img root=LABEL=writable " "snap_core=core_x1.snap snap_kernel=pc-kernel_x1.snap ro " "net.ifnames=0 init=/lib/systemd/systemd console=tty1 " - "console=ttyS0 panic=-1") + "console=ttyS0 panic=-1" + ) m_cmdline.return_value = cmdline self.assertTrue(util.system_is_snappy()) self.assertTrue(m_cmdline.call_count > 0) @@ -777,8 +2142,7 @@ class TestSystemIsSnappy(helpers.FilesystemMockingTestCase): m_cmdline.return_value = 'root=/dev/sda' root_d = self.tmp_dir() content = '\n'.join(["[Foo]", "source = 'ubuntu-core'", ""]) - helpers.populate_dir( - root_d, {'etc/system-image/channel.ini': content}) + helpers.populate_dir(root_d, {'etc/system-image/channel.ini': content}) self.reRoot(root_d) self.assertTrue(util.system_is_snappy()) @@ -788,7 +2152,8 @@ class TestSystemIsSnappy(helpers.FilesystemMockingTestCase): m_cmdline.return_value = 'root=/dev/sda' root_d = self.tmp_dir() helpers.populate_dir( - root_d, {'etc/system-image/config.d/my.file': "_unused"}) + root_d, {'etc/system-image/config.d/my.file': "_unused"} + ) self.reRoot(root_d) self.assertTrue(util.system_is_snappy()) @@ -798,18 +2163,24 @@ class TestLoadShellContent(helpers.TestCase): """Shell comments should be allowed in the content.""" self.assertEqual( {'key1': 'val1', 'key2': 'val2', 'key3': 'val3 #tricky'}, - util.load_shell_content('\n'.join([ - "#top of file comment", - "key1=val1 #this is a comment", - "# second comment", - 'key2="val2" # inlin comment' - '#badkey=wark', - 'key3="val3 #tricky"', - '']))) + util.load_shell_content( + '\n'.join( + [ + "#top of file comment", + "key1=val1 #this is a comment", + "# second comment", + 'key2="val2" # inlin comment#badkey=wark', + 'key3="val3 #tricky"', + '', + ] + ) + ), + ) class TestGetProcEnv(helpers.TestCase): """test get_proc_env.""" + null = b'\x00' simple1 = b'HOME=/' simple2 = b'PATH=/bin:/sbin' @@ -824,14 +2195,19 @@ class TestGetProcEnv(helpers.TestCase): def test_non_utf8_in_environment(self, m_load_file): """env may have non utf-8 decodable content.""" content = self.null.join( - (self.bootflag, self.simple1, self.simple2, self.mixed)) + (self.bootflag, self.simple1, self.simple2, self.mixed) + ) m_load_file.return_value = content self.assertEqual( - {'BOOTABLE_FLAG': self._val_decoded(self.bootflag), - 'HOME': '/', 'PATH': '/bin:/sbin', - 'MIXED': self._val_decoded(self.mixed)}, - util.get_proc_env(1)) + { + 'BOOTABLE_FLAG': self._val_decoded(self.bootflag), + 'HOME': '/', + 'PATH': '/bin:/sbin', + 'MIXED': self._val_decoded(self.mixed), + }, + util.get_proc_env(1), + ) self.assertEqual(1, m_load_file.call_count) @mock.patch("cloudinit.util.load_file") @@ -843,7 +2219,8 @@ class TestGetProcEnv(helpers.TestCase): self.assertEqual( dict([t.split(b'=') for t in lines]), - util.get_proc_env(1, encoding=None)) + util.get_proc_env(1, encoding=None), + ) self.assertEqual(1, m_load_file.call_count) @mock.patch("cloudinit.util.load_file") @@ -852,8 +2229,8 @@ class TestGetProcEnv(helpers.TestCase): content = self.null.join((self.simple1, self.simple2)) m_load_file.return_value = content self.assertEqual( - {'HOME': '/', 'PATH': '/bin:/sbin'}, - util.get_proc_env(1)) + {'HOME': '/', 'PATH': '/bin:/sbin'}, util.get_proc_env(1) + ) self.assertEqual(1, m_load_file.call_count) @mock.patch("cloudinit.util.load_file") @@ -871,14 +2248,15 @@ class TestGetProcEnv(helpers.TestCase): self.assertEqual(my_ppid, util.get_proc_ppid(my_pid)) -class TestKernelVersion(): +class TestKernelVersion: """test kernel version function""" params = [ ('5.6.19-300.fc32.x86_64', (5, 6)), ('4.15.0-101-generic', (4, 15)), ('3.10.0-1062.12.1.vz7.131.10', (3, 10)), - ('4.18.0-144.el8.x86_64', (4, 18))] + ('4.18.0-144.el8.x86_64', (4, 18)), + ] @mock.patch('os.uname') @pytest.mark.parametrize("uname_release,expected", params) @@ -892,29 +2270,27 @@ class TestFindDevs: def test_find_devs_with(self, m_subp): m_subp.return_value = ( '/dev/sda1: UUID="some-uuid" TYPE="ext4" PARTUUID="some-partid"', - '' + '', ) devlist = util.find_devs_with() assert devlist == [ - '/dev/sda1: UUID="some-uuid" TYPE="ext4" PARTUUID="some-partid"'] + '/dev/sda1: UUID="some-uuid" TYPE="ext4" PARTUUID="some-partid"' + ] devlist = util.find_devs_with("LABEL_FATBOOT=A_LABEL") assert devlist == [ - '/dev/sda1: UUID="some-uuid" TYPE="ext4" PARTUUID="some-partid"'] + '/dev/sda1: UUID="some-uuid" TYPE="ext4" PARTUUID="some-partid"' + ] @mock.patch('cloudinit.subp.subp') def test_find_devs_with_openbsd(self, m_subp): - m_subp.return_value = ( - 'cd0:,sd0:630d98d32b5d3759,sd1:,fd0:', '' - ) + m_subp.return_value = ('cd0:,sd0:630d98d32b5d3759,sd1:,fd0:', '') devlist = util.find_devs_with_openbsd() assert devlist == ['/dev/cd0a', '/dev/sd1i'] @mock.patch('cloudinit.subp.subp') def test_find_devs_with_openbsd_with_criteria(self, m_subp): - m_subp.return_value = ( - 'cd0:,sd0:630d98d32b5d3759,sd1:,fd0:', '' - ) + m_subp.return_value = ('cd0:,sd0:630d98d32b5d3759,sd1:,fd0:', '') devlist = util.find_devs_with_openbsd(criteria="TYPE=iso9660") assert devlist == ['/dev/cd0a'] @@ -923,7 +2299,8 @@ class TestFindDevs: assert devlist == ['/dev/cd0a', '/dev/sd1i'] @pytest.mark.parametrize( - 'criteria,expected_devlist', ( + 'criteria,expected_devlist', + ( (None, ['/dev/msdosfs/EFISYS', '/dev/iso9660/config-2']), ('TYPE=iso9660', ['/dev/iso9660/config-2']), ('TYPE=vfat', ['/dev/msdosfs/EFISYS']), @@ -940,19 +2317,23 @@ class TestFindDevs: elif pattern == "/dev/iso9660/*": return iso9660 raise Exception + m_glob.side_effect = fake_glob devlist = util.find_devs_with_freebsd(criteria=criteria) assert devlist == expected_devlist @pytest.mark.parametrize( - 'criteria,expected_devlist', ( + 'criteria,expected_devlist', + ( (None, ['/dev/ld0', '/dev/dk0', '/dev/dk1', '/dev/cd0']), ('TYPE=iso9660', ['/dev/cd0']), ('TYPE=vfat', ["/dev/ld0", "/dev/dk0", "/dev/dk1"]), - ('LABEL_FATBOOT=A_LABEL', # lp: #1841466 - ['/dev/ld0', '/dev/dk0', '/dev/dk1', '/dev/cd0']), - ) + ( + 'LABEL_FATBOOT=A_LABEL', # lp: #1841466 + ['/dev/ld0', '/dev/dk0', '/dev/dk1', '/dev/cd0'], + ), + ), ) @mock.patch("cloudinit.subp.subp") def test_find_devs_with_netbsd(self, m_subp, criteria, expected_devlist): @@ -1000,21 +2381,24 @@ class TestFindDevs: assert devlist == expected_devlist @pytest.mark.parametrize( - 'criteria,expected_devlist', ( + 'criteria,expected_devlist', + ( (None, ['/dev/vbd0', '/dev/cd0', '/dev/acd0']), ('TYPE=iso9660', ['/dev/cd0', '/dev/acd0']), ('TYPE=vfat', ['/dev/vbd0']), - ('LABEL_FATBOOT=A_LABEL', # lp: #1841466 - ['/dev/vbd0', '/dev/cd0', '/dev/acd0']), - ) + ( + 'LABEL_FATBOOT=A_LABEL', # lp: #1841466 + ['/dev/vbd0', '/dev/cd0', '/dev/acd0'], + ), + ), ) @mock.patch("cloudinit.subp.subp") - def test_find_devs_with_dragonflybsd(self, m_subp, criteria, - expected_devlist): - m_subp.return_value = ( - 'md2 md1 cd0 vbd0 acd0 vn3 vn2 vn1 vn0 md0', '' - ) + def test_find_devs_with_dragonflybsd( + self, m_subp, criteria, expected_devlist + ): + m_subp.return_value = ('md2 md1 cd0 vbd0 acd0 vn3 vn2 vn1 vn0 md0', '') devlist = util.find_devs_with_dragonflybsd(criteria=criteria) assert devlist == expected_devlist + # vi: ts=4 expandtab diff --git a/cloudinit/tests/test_version.py b/tests/unittests/test_version.py index 778a762c..ed66b09f 100644 --- a/cloudinit/tests/test_version.py +++ b/tests/unittests/test_version.py @@ -2,7 +2,7 @@ from unittest import mock -from cloudinit.tests.helpers import CiTestCase +from tests.unittests.helpers import CiTestCase from cloudinit import version diff --git a/tests/unittests/test_vmware/__init__.py b/tests/unittests/test_vmware/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/tests/unittests/test_vmware/__init__.py +++ /dev/null diff --git a/tests/unittests/util.py b/tests/unittests/util.py index 383f5f5c..2204c28f 100644 --- a/tests/unittests/util.py +++ b/tests/unittests/util.py @@ -15,7 +15,7 @@ def get_cloud(distro=None, paths=None, sys_cfg=None, metadata=None): """ paths = paths or helpers.Paths({}) sys_cfg = sys_cfg or {} - cls = distros.fetch(distro) if distro else TestingDistro + cls = distros.fetch(distro) if distro else MockDistro mydist = cls(distro, sys_cfg, paths) myds = DataSourceTesting(sys_cfg, mydist, paths) if metadata: @@ -49,14 +49,14 @@ class DataSourceTesting(DataSourceNone): return 'testing' -class TestingDistro(distros.Distro): - # TestingDistro is here to test base Distro class implementations +class MockDistro(distros.Distro): + # MockDistro is here to test base Distro class implementations def __init__(self, name="testingdistro", cfg=None, paths=None): if not cfg: cfg = {} if not paths: paths = {} - super(TestingDistro, self).__init__(name, cfg, paths) + super(MockDistro, self).__init__(name, cfg, paths) def install_packages(self, pkglist): pass diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers index fac3fcec..492ed15e 100644 --- a/tools/.github-cla-signers +++ b/tools/.github-cla-signers @@ -17,6 +17,7 @@ cawamata cclauss ciprianbadescu citrus-it +cjp256 dankenigsberg ddymko dermotbradley @@ -3,7 +3,7 @@ envlist = py3, xenial-dev, flake8, pylint recreate = True [testenv] -commands = {envpython} -m pytest {posargs:tests/unittests cloudinit} +commands = {envpython} -m pytest {posargs:tests/unittests} setenv = LC_ALL = en_US.utf-8 passenv= @@ -37,7 +37,7 @@ deps = commands = {envpython} -m pytest \ --durations 10 \ {posargs:--cov=cloudinit --cov-branch \ - tests/unittests cloudinit} + tests/unittests} [testenv:py27] basepython = python2.7 @@ -86,7 +86,7 @@ deps = # [testenv:xenial-dev]. See the comment there for details. commands = python ./tools/pipremove jsonschema - python -m pytest {posargs:tests/unittests cloudinit} + python -m pytest {posargs:tests/unittests} basepython = python3 deps = # Refer to the comment in [xenial-shared-deps] for details @@ -104,7 +104,7 @@ deps = # changes here are reflected in [testenv:xenial]. commands = python ./tools/pipremove jsonschema - python -m pytest {posargs:tests/unittests cloudinit} + python -m pytest {posargs:tests/unittests} basepython = {[testenv:xenial]basepython} deps = # Refer to the comment in [xenial-shared-deps] for details @@ -163,7 +163,7 @@ setenv = [pytest] # TODO: s/--strict/--strict-markers/ once xenial support is dropped -testpaths = cloudinit tests/unittests +testpaths = tests/unittests addopts = --strict log_format = %(asctime)s %(levelname)-9s %(name)s:%(filename)s:%(lineno)d %(message)s log_date_format = %Y-%m-%d %H:%M:%S |