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 test_oci_networking_iscsi_instance(client: IntegrationInstance, tmpdir): customize_environment(client, tmpdir, configure_secondary_nics=False) result_net_files = client.execute("ls /run/net-*.conf") assert result_net_files.ok, "No net files found under /run" log = client.read_from_file("/var/log/cloud-init.log") verify_clean_log(log) assert ("opc/v2/vnics/" not in log), "vnic data was fetched and it should not have been" netplan_yaml = client.read_from_file("/etc/netplan/50-cloud-init.yaml") netplan_cfg = yaml.safe_load(netplan_yaml) configured_interfaces = extract_interface_names(netplan_cfg["network"]) assert 1 <= len(configured_interfaces ), "Expected at least 1 primary network configuration." expected_interfaces = set( re.findall(r"/run/net-(.+)\.conf", result_net_files.stdout)) for expected_interface in expected_interfaces: assert (f"Reading from /run/net-{expected_interface}.conf" in log), "Expected {expected_interface} not found in: {log}" not_found_interfaces = expected_interfaces.difference( configured_interfaces) assert not not_found_interfaces, ( f"Interfaces, {not_found_interfaces}, expected to be configured in" f" {netplan_cfg['network']}") assert client.execute("ping -c 2 canonical.com").ok
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_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_hotplug_add_remove(client: IntegrationInstance): ips_before = _get_ip_addr(client) log = client.read_from_file('/var/log/cloud-init.log') assert 'Exiting hotplug handler' not in log # Add new NIC added_ip = client.instance.add_network_interface() _wait_till_hotplug_complete(client) ips_after_add = _get_ip_addr(client) new_addition = [ip for ip in ips_after_add if ip.ip4 == added_ip][0] assert len(ips_after_add) == len(ips_before) + 1 assert added_ip not in [ip.ip4 for ip in ips_before] assert added_ip in [ip.ip4 for ip in ips_after_add] assert new_addition.state == 'UP' netplan_cfg = client.read_from_file('/etc/netplan/50-cloud-init.yaml') config = yaml.safe_load(netplan_cfg) assert new_addition.interface in config['network']['ethernets'] # Remove new NIC client.instance.remove_network_interface(added_ip) _wait_till_hotplug_complete(client, expected_runs=2) ips_after_remove = _get_ip_addr(client) assert len(ips_after_remove) == len(ips_before) assert added_ip not in [ip.ip4 for ip in ips_after_remove] netplan_cfg = client.read_from_file('/etc/netplan/50-cloud-init.yaml') config = yaml.safe_load(netplan_cfg) assert new_addition.interface not in config['network']['ethernets']
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 test_hotplug_add_remove(client: IntegrationInstance): ips_before = _get_ip_addr(client) log = client.read_from_file("/var/log/cloud-init.log") assert "Exiting hotplug handler" not in log assert client.execute( "test -f /etc/udev/rules.d/10-cloud-init-hook-hotplug.rules").ok # Add new NIC added_ip = client.instance.add_network_interface() _wait_till_hotplug_complete(client, expected_runs=1) ips_after_add = _get_ip_addr(client) new_addition = [ip for ip in ips_after_add if ip.ip4 == added_ip][0] assert len(ips_after_add) == len(ips_before) + 1 assert added_ip not in [ip.ip4 for ip in ips_before] assert added_ip in [ip.ip4 for ip in ips_after_add] assert new_addition.state == "UP" netplan_cfg = client.read_from_file("/etc/netplan/50-cloud-init.yaml") config = yaml.safe_load(netplan_cfg) assert new_addition.interface in config["network"]["ethernets"] # Remove new NIC client.instance.remove_network_interface(added_ip) _wait_till_hotplug_complete(client, expected_runs=2) ips_after_remove = _get_ip_addr(client) assert len(ips_after_remove) == len(ips_before) assert added_ip not in [ip.ip4 for ip in ips_after_remove] netplan_cfg = client.read_from_file("/etc/netplan/50-cloud-init.yaml") config = yaml.safe_load(netplan_cfg) assert new_addition.interface not in config["network"]["ethernets"] assert "enabled" == client.execute( "cloud-init devel hotplug-hook -s net query")
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_chrony(client: IntegrationInstance): if client.execute('test -f /etc/chrony.conf').ok: chrony_conf = '/etc/chrony.conf' else: chrony_conf = '/etc/chrony/chrony.conf' contents = client.read_from_file(chrony_conf) assert '.pool.ntp.org' in contents
def test_chrony(client: IntegrationInstance): if client.execute('test -f /etc/chrony.conf').ok: chrony_conf = '/etc/chrony.conf' else: chrony_conf = '/etc/chrony/chrony.conf' contents = client.read_from_file(chrony_conf) assert 'server 172.16.15.14' in contents
def test_device_alias(self, create_disk, client: IntegrationInstance): log = client.read_from_file("/var/log/cloud-init.log") assert ("updated disk_setup device entry 'my_alias' to '/dev/sdb'" in log) assert "changed my_alias.1 => /dev/sdb1" in log assert "changed my_alias.2 => /dev/sdb2" in log 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"]) == 2 assert sdb["children"][0]["name"] == "sdb1" assert sdb["children"][1]["name"] == "sdb2" if "mountpoint" in sdb["children"][0]: assert sdb["children"][0]["mountpoint"] == "/mnt1" assert sdb["children"][1]["mountpoint"] == "/mnt2" else: assert sdb["children"][0]["mountpoints"] == ["/mnt1"] assert sdb["children"][1]["mountpoints"] == ["/mnt2"] result = client.execute("mount -a") assert result.return_code == 0 assert result.stdout.strip() == "" assert result.stderr.strip() == "" result = client.execute("findmnt -J /mnt1") assert result.return_code == 0 result = client.execute("findmnt -J /mnt2") assert result.return_code == 0
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
def test_chrony(client: IntegrationInstance): if client.execute("test -f /etc/chrony.conf").ok: chrony_conf = "/etc/chrony.conf" else: chrony_conf = "/etc/chrony/chrony.conf" contents = client.read_from_file(chrony_conf) assert "server 172.16.15.14" in contents
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_ntp_entries(self, class_client: IntegrationInstance): ntp_conf = class_client.read_from_file("/etc/ntp.conf") for expected_server in EXPECTED_SERVERS: assert re.search(r"^server {} iburst".format(expected_server), ntp_conf, re.MULTILINE) for expected_pool in EXPECTED_POOLS: assert re.search(r"^pool {} iburst".format(expected_pool), ntp_conf, re.MULTILINE)
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_primary_on_openstack(self, class_client: IntegrationInstance): """Test apt default primary source on openstack. When no uri is provided. """ zone = class_client.execute('cloud-init query v1.availability_zone') sources_list = class_client.read_from_file('/etc/apt/sources.list') assert '{}.clouds.archive.ubuntu.com'.format(zone) in sources_list
def test_primary(self, class_client: IntegrationInstance): """Test apt default primary sources. Ported from tests/cloud_tests/testcases/modules/apt_configure_primary.py """ sources_list = class_client.read_from_file('/etc/apt/sources.list') assert 'deb http://archive.ubuntu.com/ubuntu' in sources_list
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 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_gpg_no_tty(client: IntegrationInstance): log = client.read_from_file('/var/log/cloud-init.log') to_verify = [ "Running command ['gpg', '--no-tty', " "'--keyserver=keyserver.ubuntu.com', '--recv-keys', 'E4D304DF'] " "with allowed return codes [0] (shell=False, capture=True)", "Imported key 'E4D304DF' from keyserver 'keyserver.ubuntu.com'", ] verify_ordered_items_in_text(to_verify, log)
def test_disable_apt_pipelining(self, class_client: IntegrationInstance): """Test disabling of apt pipelining. Ported from tests/cloud_tests/testcases/modules/apt_pipelining_disable.py """ conf = class_client.read_from_file( '/etc/apt/apt.conf.d/90cloud-init-pipelining') assert 'Acquire::http::Pipeline-Depth "0";' in conf
def test_apt_conf(self, class_client: IntegrationInstance): """Test the apt conf functionality. Ported from tests/cloud_tests/testcases/modules/apt_configure_conf.py """ apt_config = class_client.read_from_file( '/etc/apt/apt.conf.d/94cloud-init-config') assert 'Assume-Yes "true";' in apt_config assert 'Fix-Broken "true";' in apt_config
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_no_hotplug_in_userdata(client: IntegrationInstance): ips_before = _get_ip_addr(client) log = client.read_from_file('/var/log/cloud-init.log') assert 'Exiting hotplug handler' not in log # Add new NIC client.instance.add_network_interface() _wait_till_hotplug_complete(client) log = client.read_from_file('/var/log/cloud-init.log') assert 'hotplug not enabled for event of type network' in log ips_after_add = _get_ip_addr(client) if len(ips_after_add) == len(ips_before) + 1: # We can see the device, but it should not have been brought up new_ip = [ip for ip in ips_after_add if ip not in ips_before][0] assert new_ip.state == 'DOWN' else: assert len(ips_after_add) == len(ips_before)
def test_network_disabled_via_etc_cloud(client: IntegrationInstance): """Test that network can be disabled via config file in /etc/cloud""" if client.settings.CLOUD_INIT_SOURCE == "IN_PLACE": pytest.skip( "IN_PLACE not supported as we mount /etc/cloud contents into the " "container") _customize_envionment(client) log = client.read_from_file("/var/log/cloud-init.log") assert "network config is disabled by system_cfg" in log
def test_default_primary_with_uri(client: IntegrationInstance): """Test apt default primary sources. Ported from tests/cloud_tests/testcases/modules/apt_configure_primary.py """ sources_list = client.read_from_file('/etc/apt/sources.list') assert 'archive.ubuntu.com' not in sources_list assert 'something.random.invalid' in sources_list
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_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'
def test_dist_config_file_is_empty(self, class_client: IntegrationInstance): """Test that the distributed config file is empty. (This test is skipped on all currently supported Ubuntu releases, so may not actually be needed any longer.) """ if class_client.execute("test -e /etc/ntp.conf.dist").failed: pytest.skip("/etc/ntp.conf.dist does not exist") dist_file = class_client.read_from_file("/etc/ntp.conf.dist") assert 0 == len(dist_file.strip().splitlines())