def test_reboot_without_version_change(client: IntegrationInstance): log = client.read_from_file("/var/log/cloud-init.log") assert "Python version change detected" not in log assert "Cache compatibility status is currently unknown." not in log _assert_no_pickle_problems(log) client.restart() log = client.read_from_file("/var/log/cloud-init.log") assert "Python version change detected" not in log assert "Could not determine Python version used to write cache" not in log _assert_no_pickle_problems(log) # Now ensure that loading a bad pickle gives us problems client.push_file(TEST_PICKLE, PICKLE_PATH) client.restart() log = client.read_from_file("/var/log/cloud-init.log") # no cache found is an "expected" upgrade error, and # "Failed" means we're unable to load the pickle assert any( [ "Failed loading pickled blob from {}".format(PICKLE_PATH) in log, "no cache found" in log, ] )
def test_dual_stack(client: IntegrationInstance): # Drop IPv4 responses assert client.execute("iptables -I INPUT -s 169.254.169.254 -j DROP").ok _test_crawl(client, "http://[fd00:ec2::254]") # Block IPv4 requests assert client.execute("iptables -I OUTPUT -d 169.254.169.254 -j REJECT").ok _test_crawl(client, "http://[fd00:ec2::254]") # Re-enable IPv4 assert client.execute("iptables -D OUTPUT -d 169.254.169.254 -j REJECT").ok assert client.execute("iptables -D INPUT -s 169.254.169.254 -j DROP").ok # Drop IPv6 responses assert client.execute("ip6tables -I INPUT -s fd00:ec2::254 -j DROP").ok _test_crawl(client, "http://169.254.169.254") # Block IPv6 requests assert client.execute("ip6tables -I OUTPUT -d fd00:ec2::254 -j REJECT").ok _test_crawl(client, "http://169.254.169.254") # Force NoDHCPLeaseError (by removing dhclient) and assert ipv6 still works # Destructive test goes last # dhclient is at /sbin/dhclient on bionic but /usr/sbin/dhclient elseware assert client.execute("rm $(which dhclient)").ok client.restart() log = client.read_from_file("/var/log/cloud-init.log") assert "Crawl of metadata service using link-local ipv6 took" in log
def test_sudoers_includedir(client: IntegrationInstance): """Ensure we don't add additional #includedir to sudoers. Newer versions of /etc/sudoers will use @includedir rather than #includedir. Ensure we handle that properly and don't include an additional #includedir when one isn't warranted. https://github.com/canonical/cloud-init/pull/783 """ if ImageSpecification.from_os_image().release in [ 'xenial', 'bionic', 'focal' ]: raise pytest.skip( 'Test requires version of sudo installed on groovy and later') client.execute("sed -i 's/#include/@include/g' /etc/sudoers") sudoers = client.read_from_file('/etc/sudoers') if '@includedir /etc/sudoers.d' not in sudoers: client.execute("echo '@includedir /etc/sudoers.d' >> /etc/sudoers") client.instance.clean() client.restart() sudoers = client.read_from_file('/etc/sudoers') assert '#includedir' not in sudoers assert sudoers.count('includedir /etc/sudoers.d') == 1
def test_boot_event_disabled_by_default(client: IntegrationInstance): log = client.read_from_file("/var/log/cloud-init.log") if "network config is disabled" in log: pytest.skip("network config disabled. Test doesn't apply") assert "Applying network configuration" in log assert "dummy0" not in client.execute("ls /sys/class/net") _add_dummy_bridge_to_netplan(client) client.execute("rm /var/log/cloud-init.log") client.restart() log2 = client.read_from_file("/var/log/cloud-init.log") if "cache invalid in datasource" in log2: # Invalid cache will get cleared, meaning we'll create a new # "instance" and apply networking config, so events aren't # really relevant here pytest.skip("Test only valid for existing instances") # We attempt to apply network config twice on every boot. # Ensure neither time works. assert 2 == len( re.findall(r"Event Denied: scopes=\['network'\] EventType=boot[^-]", log2)) assert 2 == log2.count( "Event Denied: scopes=['network'] EventType=boot-legacy") assert 2 == log2.count("No network config applied. Neither a new instance" " nor datasource network update allowed") assert "dummy0" in client.execute("ls /sys/class/net")
def _customize_envionment(client: IntegrationInstance): # Assert our platform can detect LXD during sytemd generator timeframe. ds_id_log = client.execute("cat /run/cloud-init/ds-identify.log").stdout assert "check for 'LXD' returned found" in ds_id_log # At some point Jammy will fail this test. We want to be informed # when Jammy images no longer ship NoCloud template files (LP: #1958460). assert "check for 'NoCloud' returned found" in ds_id_log if client.settings.PLATFORM == "lxd_vm": # ds-identify runs at systemd generator time before /dev/lxd/sock. # Assert we can expected artifact which indicates LXD is viable. result = client.execute("cat /sys/class/dmi/id/board_name") if not result.ok: raise AssertionError( "Missing expected /sys/class/dmi/id/board_name") if "LXD" != result.stdout: raise AssertionError(f"DMI board_name is not LXD: {result.stdout}") # Having multiple datasources prevents ds-identify from short-circuiting # detection logic with a log like: # single entry in datasource_list (LXD) use that. # Also, NoCloud is detected during init-local timeframe. # If there is a race on VMs where /dev/lxd/sock is not setup in init-local # cloud-init will fallback to NoCloud and fail this test. client.write_to_file( "/etc/cloud/cloud.cfg.d/99-detect-lxd-first.cfg", "datasource_list: [LXD, NoCloud]\n", ) client.execute("cloud-init clean --logs") client.restart()
def _check_iid_insensitive_across_kernel_upgrade( instance: IntegrationInstance, ): uuid = instance.read_from_file("/sys/class/dmi/id/product_uuid") assert (uuid.isupper() ), "Expected uppercase UUID on Ubuntu FIPS image {}".format(uuid) orig_kernel = instance.execute("uname -r").strip() assert "azure-fips" in orig_kernel result = instance.execute("apt-get update") # Install a 5.4+ kernel which provides lowercase product_uuid result = instance.execute("apt-get install linux-azure --assume-yes") if not result.ok: pytest.fail("Unable to install linux-azure kernel: {}".format(result)) # Remove ubuntu-azure-fips metapkg which mandates FIPS-flavour kernel result = instance.execute("ua disable fips --assume-yes") assert result.ok, "Unable to disable fips: {}".format(result) instance.restart() new_kernel = instance.execute("uname -r").strip() assert orig_kernel != new_kernel assert "azure-fips" not in new_kernel assert "azure" in new_kernel new_uuid = instance.read_from_file("/sys/class/dmi/id/product_uuid") assert ( uuid.lower() == new_uuid ), "Expected UUID on linux-azure to be lowercase of FIPS: {}".format(uuid) log = instance.read_from_file("/var/log/cloud-init.log") RE_CONFIG_SSH_SEMAPHORE = r"Writing.*sem/config_ssh " ssh_runs = len(re.findall(RE_CONFIG_SSH_SEMAPHORE, log)) assert 1 == ssh_runs, "config_ssh ran too many times {}".format(ssh_runs)
def _customize_envionment(client: IntegrationInstance): client.write_to_file( "/etc/cloud/cloud.cfg.d/99-detect-lxd.cfg", "datasource_list: [LXD]\n", ) client.execute("cloud-init clean --logs") client.restart()
def _customize_environment(client: IntegrationInstance): # Insert our "disable_network_activation" file here client.write_to_file( "/etc/cloud/cloud.cfg.d/99-disable-network-activation.cfg", "disable_network_activation: true\n", ) client.execute("cloud-init clean --logs") client.restart()
def custom_client(client: IntegrationInstance, tmpdir) -> Iterator[IntegrationInstance]: client.write_to_file(f"/etc/cloud/cloud.cfg.d/{CUSTOM_CLOUD_DIR_FN}", CUSTOM_CLOUD_DIR) client.execute(f"rm -rf {DEFAULT_CLOUD_DIR}") # Remove previous cloud_dir client.execute("cloud-init clean --logs") client.restart() yield client
def test_log_message_on_missing_version_file(client: IntegrationInstance): # Start by pushing a pickle so we can see the log message client.push_file(TEST_PICKLE, PICKLE_PATH) client.execute("rm /var/lib/cloud/data/python-version") client.restart() log = client.read_from_file('/var/log/cloud-init.log') assert ('Writing python-version file. ' 'Cache compatibility status is currently unknown.') in log
def _customize_envionment(client: IntegrationInstance): # Insert our "disable_network_config" file here client.write_to_file( "/etc/cloud/cloud.cfg.d/99-disable-network-config.cfg", "network: {config: disabled}\n", ) client.execute("cloud-init clean --logs") client.restart()
def test_cache_purged_on_version_change(client: IntegrationInstance): # Start by pushing the invalid pickle so we'll hit an error if the # cache didn't actually get purged client.push_file(TEST_PICKLE, PICKLE_PATH) client.execute("echo '1.0' > /var/lib/cloud/data/python-version") client.restart() log = client.read_from_file("/var/log/cloud-init.log") assert "Python version change detected. Purging cache" in log _assert_no_pickle_problems(log)
def test_log_message_on_missing_version_file(client: IntegrationInstance): client.push_file(TEST_PICKLE, PICKLE_PATH) client.restart() assert client.execute('cloud-init status --wait').ok log = client.read_from_file('/var/log/cloud-init.log') verify_ordered_items_in_text([ "Unable to unpickle datasource: 'MIMEMultipart' object has no " "attribute 'policy'. Ignoring current cache.", 'no cache found', 'Searching for local data source', 'SUCCESS: found local data from DataSourceNoCloud' ], log)
def test_disk_setup_no_partprobe(create_disk, client: IntegrationInstance): """Ensure disk setup still works as expected without partprobe.""" # We can't do this part in a bootcmd because the path has already # been found by the time we get to the bootcmd client.execute('rm $(which partprobe)') client.execute('cloud-init clean --logs') client.restart() log = client.read_from_file('/var/log/cloud-init.log') _verify_first_disk_setup(client, log) assert 'partprobe' not in log
def customize_environment( client: IntegrationInstance, tmpdir, configure_secondary_nics: bool = False, ): cfg = tmpdir.join("01_oracle_datasource.cfg") with open(cfg, "w") as f: f.write( DS_CFG.format(configure_secondary_nics=configure_secondary_nics)) client.push_file(cfg, "/etc/cloud/cloud.cfg.d/01_oracle_datasource.cfg") client.execute("cloud-init clean --logs") client.restart()
def _test_network_config_applied_on_reboot(client: IntegrationInstance): log = client.read_from_file('/var/log/cloud-init.log') assert 'Applying network configuration' in log assert 'dummy0' not in client.execute('ls /sys/class/net') _add_dummy_bridge_to_netplan(client) client.execute('rm /var/log/cloud-init.log') client.restart() log = client.read_from_file('/var/log/cloud-init.log') assert 'Event Allowed: scope=network EventType=boot' in log assert 'Applying network configuration' in log assert 'dummy0' not in client.execute('ls /sys/class/net')
def test_disk_setup_when_mounted( self, create_disk, client: IntegrationInstance ): """Test lp-1920939. We insert an extra disk into our VM, format it to have two partitions, modify our cloud config to mount devices before disk setup, and modify our userdata to setup a single partition on the disk. This allows cloud-init to attempt disk setup on a mounted partition. When blockdev is in use, it will fail with "blockdev: ioctl error on BLKRRPART: Device or resource busy" along with a warning and a traceback. When partprobe is in use, everything should work successfully. """ log = client.read_from_file("/var/log/cloud-init.log") self._verify_first_disk_setup(client, log) # Ensure NoCloud gets detected on reboot client.execute("mkdir -p /var/lib/cloud/seed/nocloud-net/") client.execute("touch /var/lib/cloud/seed/nocloud-net/meta-data") client.write_to_file( "/etc/cloud/cloud.cfg.d/99_nocloud.cfg", "datasource_list: [ NoCloud ]\n", ) # Update our userdata and cloud.cfg to mount then perform new disk # setup client.write_to_file( "/var/lib/cloud/seed/nocloud-net/user-data", UPDATED_PARTPROBE_USERDATA, ) client.execute( "sed -i 's/write-files/write-files\\n - mounts/' " "/etc/cloud/cloud.cfg" ) client.execute("cloud-init clean --logs") client.restart() # Assert new setup works as expected verify_clean_log(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" if "mountpoint" in sdb["children"][0]: assert sdb["children"][0]["mountpoint"] == "/mnt3" else: assert sdb["children"][0]["mountpoints"] == ["/mnt3"]
def test_log_message_on_missing_version_file(client: IntegrationInstance): # Start by pushing a pickle so we can see the log message client.push_file(TEST_PICKLE, PICKLE_PATH) client.execute("rm /var/lib/cloud/data/python-version") client.execute("rm /var/log/cloud-init.log") client.restart() log = client.read_from_file("/var/log/cloud-init.log") if "no cache found" not in log: # We don't expect the python version file to exist if we have no # pre-existing cache assert ( "Writing python-version file. " "Cache compatibility status is currently unknown." in log )
def test_datasource_rbx_no_stacktrace(client: IntegrationInstance): client.write_to_file( "/etc/cloud/cloud.cfg.d/90_dpkg.cfg", "datasource_list: [ RbxCloud, NoCloud, LXD ]\n", ) client.write_to_file( "/etc/cloud/ds-identify.cfg", "policy: enabled\n", ) client.execute("cloud-init clean --logs") client.restart() log = client.read_from_file("/var/log/cloud-init.log") verify_clean_log(log) assert "Failed to load metadata and userdata" not in log assert ("Getting data from <class 'cloudinit.sources.DataSourceRbxCloud." "DataSourceRbxCloud'> failed" not in log)
def test_reboot_without_version_change(client: IntegrationInstance): log = client.read_from_file('/var/log/cloud-init.log') assert 'Python version change detected' not in log assert 'Cache compatibility status is currently unknown.' not in log _assert_no_pickle_problems(log) client.restart() log = client.read_from_file('/var/log/cloud-init.log') assert 'Python version change detected' not in log assert 'Could not determine Python version used to write cache' not in log _assert_no_pickle_problems(log) # Now ensure that loading a bad pickle gives us problems client.push_file(TEST_PICKLE, PICKLE_PATH) client.restart() log = client.read_from_file('/var/log/cloud-init.log') assert 'Failed loading pickled blob from {}'.format(PICKLE_PATH) in log
def test_frequency_override(client: IntegrationInstance): # Some pre-checks assert ("running config-scripts-user with frequency once-per-instance" in client.read_from_file("/var/log/cloud-init.log")) assert client.read_from_file("/var/tmp/hi").strip().count("hi") == 1 # Change frequency of scripts-user to always config = client.read_from_file("/etc/cloud/cloud.cfg") new_config = config.replace("- scripts-user", "- [ scripts-user, always ]") client.write_to_file("/etc/cloud/cloud.cfg", new_config) client.restart() # Ensure the script was run again assert ("running config-scripts-user with frequency always" in client.read_from_file("/var/log/cloud-init.log")) assert client.read_from_file("/var/tmp/hi").strip().count("hi") == 2
def test_datasource_rbx_no_stacktrace(client: IntegrationInstance): client.write_to_file( '/etc/cloud/cloud.cfg.d/90_dpkg.cfg', 'datasource_list: [ RbxCloud, NoCloud ]\n', ) client.write_to_file( '/etc/cloud/ds-identify.cfg', 'policy: enabled\n', ) client.execute('cloud-init clean --logs') client.restart() log = client.read_from_file('/var/log/cloud-init.log') assert 'WARNING' not in log assert 'Traceback' not in log assert 'Failed to load metadata and userdata' not in log assert ("Getting data from <class 'cloudinit.sources.DataSourceRbxCloud." "DataSourceRbxCloud'> failed") not in log
def test_nocloud_seedfrom_vendordata(client: IntegrationInstance): seed_dir = "/var/tmp/test_seed_dir" result = client.execute( "mkdir {seed_dir} && " "touch {seed_dir}/user-data && " "touch {seed_dir}/meta-data && " "echo 'seedfrom: {seed_dir}/' > " "/var/lib/cloud/seed/nocloud-net/meta-data".format(seed_dir=seed_dir)) assert result.return_code == 0 client.write_to_file( "{}/vendor-data".format(seed_dir), VENDOR_DATA, ) client.execute("cloud-init clean --logs") client.restart() assert client.execute("cloud-init status").ok assert "seeded_vendordata_test_file" in client.execute("ls /var/tmp")
def test_nocloud_seedfrom_vendordata(client: IntegrationInstance): seed_dir = '/var/tmp/test_seed_dir' result = client.execute( "mkdir {seed_dir} && " "touch {seed_dir}/user-data && " "touch {seed_dir}/meta-data && " "echo 'seedfrom: {seed_dir}/' > " "/var/lib/cloud/seed/nocloud-net/meta-data".format(seed_dir=seed_dir) ) assert result.return_code == 0 client.write_to_file( '{}/vendor-data'.format(seed_dir), VENDOR_DATA, ) client.execute('cloud-init clean --logs') client.restart(raise_on_cloudinit_failure=True) assert 'seeded_vendordata_test_file' in client.execute('ls /var/tmp')
def test_disk_setup_when_mounted( self, create_disk, client: IntegrationInstance ): """Test lp-1920939. We insert an extra disk into our VM, format it to have two partitions, modify our cloud config to mount devices before disk setup, and modify our userdata to setup a single partition on the disk. This allows cloud-init to attempt disk setup on a mounted partition. When blockdev is in use, it will fail with "blockdev: ioctl error on BLKRRPART: Device or resource busy" along with a warning and a traceback. When partprobe is in use, everything should work successfully. """ log = client.read_from_file('/var/log/cloud-init.log') self._verify_first_disk_setup(client, log) # Update our userdata and cloud.cfg to mount then perform new disk # setup client.write_to_file( '/var/lib/cloud/seed/nocloud-net/user-data', UPDATED_PARTPROBE_USERDATA, ) client.execute( "sed -i 's/write-files/write-files\\n - mounts/' " "/etc/cloud/cloud.cfg" ) client.execute('cloud-init clean --logs') client.restart() # Assert new setup works as expected assert 'Traceback' not in log assert 'WARN' not 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['children'][0]['mountpoint'] == '/mnt3'
def _test_network_config_applied_on_reboot(client: IntegrationInstance): log = client.read_from_file("/var/log/cloud-init.log") if "network config is disabled" in log: pytest.skip("network config disabled. Test doesn't apply") assert "Applying network configuration" in log assert "dummy0" not in client.execute("ls /sys/class/net") _add_dummy_bridge_to_netplan(client) client.execute('echo "" > /var/log/cloud-init.log') client.restart() log = client.read_from_file("/var/log/cloud-init.log") if "cache invalid in datasource" in log: # Invalid cache will get cleared, meaning we'll create a new # "instance" and apply networking config, so events aren't # really relevant here pytest.skip("Test only valid for existing instances") assert "Event Allowed: scope=network EventType=boot" in log assert "Applying network configuration" in log assert "dummy0" not in client.execute("ls /sys/class/net")
def test_boot_event_disabled_by_default(client: IntegrationInstance): log = client.read_from_file('/var/log/cloud-init.log') assert 'Applying network configuration' in log assert 'dummy0' not in client.execute('ls /sys/class/net') _add_dummy_bridge_to_netplan(client) client.execute('rm /var/log/cloud-init.log') client.restart() log2 = client.read_from_file('/var/log/cloud-init.log') # We attempt to apply network config twice on every boot. # Ensure neither time works. assert 2 == len( re.findall(r"Event Denied: scopes=\['network'\] EventType=boot[^-]", log2)) assert 2 == log2.count( "Event Denied: scopes=['network'] EventType=boot-legacy") assert 2 == log2.count("No network config applied. Neither a new instance" " nor datasource network update allowed") assert 'dummy0' in client.execute('ls /sys/class/net')
def _customize_envionment(client: IntegrationInstance): # Assert our platform can detect LXD during systemd generator timeframe. ds_id_log = client.execute("cat /run/cloud-init/ds-identify.log").stdout assert "check for 'LXD' returned found" in ds_id_log if client.settings.PLATFORM == "lxd_vm": # ds-identify runs at systemd generator time before /dev/lxd/sock. # Assert we can expected artifact which indicates LXD is viable. result = client.execute("cat /sys/class/dmi/id/board_name") if not result.ok: raise AssertionError( "Missing expected /sys/class/dmi/id/board_name") if "LXD" != result.stdout: raise AssertionError(f"DMI board_name is not LXD: {result.stdout}") # Having multiple datasources prevents ds-identify from short-circuiting # detection logic with a log like: # single entry in datasource_list (LXD) use that. # Also, NoCloud is detected during init-local timeframe. # If there is a race on VMs where /dev/lxd/sock is not setup in init-local # cloud-init will fallback to NoCloud and fail this test. client.write_to_file( "/etc/cloud/cloud.cfg.d/99-detect-lxd-first.cfg", "datasource_list: [LXD, NoCloud]\n", ) # This is also to ensure that NoCloud can be detected if ImageSpecification.from_os_image().release == "jammy": # Add nocloud-net seed files because Jammy no longer delivers NoCloud # (LP: #1958460). client.execute("mkdir -p /var/lib/cloud/seed/nocloud-net") client.write_to_file("/var/lib/cloud/seed/nocloud-net/meta-data", "") client.write_to_file("/var/lib/cloud/seed/nocloud-net/user-data", "#cloud-config\n{}") client.execute("cloud-init clean --logs") client.restart()