summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChad Smith <chad.smith@canonical.com>2021-12-03 15:44:06 -0700
committergit-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com>2021-12-06 17:15:09 +0000
commit2fe7d2a5227c55b79182cd1eac831a63e6d7006b (patch)
tree5dea8a911dca291b74d94b271beaece84775fb1c
parent5552b6be6680af032bcf1fc02d4af96736c741b9 (diff)
downloadcloud-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.md2
-rw-r--r--CONTRIBUTING.rst (renamed from HACKING.rst)58
-rw-r--r--cloudinit/cmd/query.py134
-rwxr-xr-xcloudinit/config/cc_ssh_authkey_fingerprints.py2
-rw-r--r--cloudinit/config/tests/test_mounts.py61
-rw-r--r--cloudinit/config/tests/test_resolv_conf.py92
-rwxr-xr-xcloudinit/distros/__init__.py7
-rw-r--r--cloudinit/distros/alpine.py3
-rw-r--r--cloudinit/distros/debian.py90
-rw-r--r--cloudinit/handlers/jinja_template.py48
-rw-r--r--cloudinit/net/activators.py6
-rw-r--r--cloudinit/net/networkd.py17
-rwxr-xr-xcloudinit/sources/DataSourceAzure.py6
-rw-r--r--cloudinit/sources/DataSourceGCE.py25
-rw-r--r--cloudinit/sources/DataSourceLXD.py101
-rw-r--r--cloudinit/sources/DataSourceVultr.py4
-rw-r--r--cloudinit/sources/helpers/vmware/imc/config_nic.py2
-rw-r--r--cloudinit/sources/helpers/vultr.py52
-rw-r--r--cloudinit/sources/tests/test_lxd.py185
-rw-r--r--cloudinit/tests/test_gpg.py55
-rw-r--r--cloudinit/tests/test_util.py1149
-rw-r--r--cloudinit/util.py41
-rw-r--r--conftest.py18
-rw-r--r--debian/changelog39
-rw-r--r--doc/examples/cloud-config-datasources.txt1
-rw-r--r--doc/rtd/index.rst2
-rw-r--r--doc/rtd/topics/contributing.rst2
-rw-r--r--doc/rtd/topics/hacking.rst2
-rw-r--r--doc/rtd/topics/instancedata.rst16
-rw-r--r--doc/rtd/topics/testing.rst13
-rwxr-xr-xsetup.py2
-rw-r--r--tests/integration_tests/bugs/test_gh868.py6
-rw-r--r--tests/integration_tests/conftest.py10
-rw-r--r--tests/integration_tests/datasources/test_lxd_discovery.py39
-rw-r--r--tests/integration_tests/modules/test_apt.py52
-rw-r--r--tests/integration_tests/modules/test_combined.py148
-rw-r--r--tests/integration_tests/modules/test_command_output.py1
-rw-r--r--tests/integration_tests/modules/test_growpart.py62
-rw-r--r--tests/integration_tests/modules/test_jinja_templating.py12
-rw-r--r--tests/integration_tests/modules/test_keys_to_console.py43
-rw-r--r--tests/integration_tests/modules/test_ntp_servers.py3
-rw-r--r--tests/integration_tests/modules/test_puppet.py39
-rw-r--r--tests/integration_tests/modules/test_seed_random_data.py30
-rw-r--r--tests/integration_tests/modules/test_snap.py30
-rw-r--r--tests/integration_tests/modules/test_ssh_auth_key_fingerprints.py5
-rw-r--r--tests/integration_tests/modules/test_ssh_import_id.py40
-rw-r--r--tests/integration_tests/modules/test_timezone.py25
-rw-r--r--tests/integration_tests/util.py9
-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.py64
-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.py376
-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__.py2
-rw-r--r--tests/unittests/test_atomic_helper.py2
-rw-r--r--tests/unittests/test_builtin_handlers.py70
-rw-r--r--tests/unittests/test_cli.py2
-rw-r--r--tests/unittests/test_conftest.py (renamed from cloudinit/tests/test_conftest.py)2
-rw-r--r--tests/unittests/test_cs_util.py2
-rw-r--r--tests/unittests/test_data.py2
-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.py2
-rw-r--r--tests/unittests/test_ec2_util.py2
-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.py49
-rw-r--r--tests/unittests/test_helpers.py2
-rw-r--r--tests/unittests/test_log.py2
-rw-r--r--tests/unittests/test_merging.py2
-rw-r--r--tests/unittests/test_net.py27
-rw-r--r--tests/unittests/test_net_activators.py5
-rw-r--r--tests/unittests/test_net_freebsd.py2
-rw-r--r--tests/unittests/test_netinfo.py (renamed from cloudinit/tests/test_netinfo.py)2
-rw-r--r--tests/unittests/test_pathprefix2dict.py2
-rw-r--r--tests/unittests/test_persistence.py (renamed from cloudinit/tests/test_persistence.py)0
-rw-r--r--tests/unittests/test_registry.py2
-rw-r--r--tests/unittests/test_reporting.py2
-rw-r--r--tests/unittests/test_reporting_hyperv.py2
-rw-r--r--tests/unittests/test_runs/__init__.py0
-rw-r--r--tests/unittests/test_simpletable.py (renamed from cloudinit/tests/test_simpletable.py)2
-rw-r--r--tests/unittests/test_sshutil.py2
-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.py2
-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.py1660
-rw-r--r--tests/unittests/test_version.py (renamed from cloudinit/tests/test_version.py)2
-rw-r--r--tests/unittests/test_vmware/__init__.py0
-rw-r--r--tests/unittests/util.py8
-rw-r--r--tools/.github-cla-signers1
-rw-r--r--tox.ini10
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
----------------
diff --git a/setup.py b/setup.py
index 58fddf0f..100b07fe 100755
--- a/setup.py
+++ b/setup.py
@@ -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
diff --git a/tox.ini b/tox.ini
index 874d3f20..ff888266 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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