summaryrefslogtreecommitdiff
path: root/tests/integration_tests/bugs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/integration_tests/bugs')
-rw-r--r--tests/integration_tests/bugs/test_gh570.py39
-rw-r--r--tests/integration_tests/bugs/test_gh626.py43
-rw-r--r--tests/integration_tests/bugs/test_gh632.py33
-rw-r--r--tests/integration_tests/bugs/test_gh668.py46
-rw-r--r--tests/integration_tests/bugs/test_gh671.py53
-rw-r--r--tests/integration_tests/bugs/test_gh868.py27
-rw-r--r--tests/integration_tests/bugs/test_lp1813396.py31
-rw-r--r--tests/integration_tests/bugs/test_lp1835584.py101
-rw-r--r--tests/integration_tests/bugs/test_lp1886531.py4
-rw-r--r--tests/integration_tests/bugs/test_lp1897099.py14
-rw-r--r--tests/integration_tests/bugs/test_lp1898997.py77
-rw-r--r--tests/integration_tests/bugs/test_lp1900837.py5
-rw-r--r--tests/integration_tests/bugs/test_lp1901011.py67
-rw-r--r--tests/integration_tests/bugs/test_lp1910835.py64
-rw-r--r--tests/integration_tests/bugs/test_lp1912844.py105
15 files changed, 696 insertions, 13 deletions
diff --git a/tests/integration_tests/bugs/test_gh570.py b/tests/integration_tests/bugs/test_gh570.py
new file mode 100644
index 00000000..e98ab5d0
--- /dev/null
+++ b/tests/integration_tests/bugs/test_gh570.py
@@ -0,0 +1,39 @@
+"""Integration test for #570.
+
+Test that we can add optional vendor-data to the seedfrom file in a
+NoCloud environment
+"""
+
+import pytest
+
+from tests.integration_tests.instances import IntegrationInstance
+
+VENDOR_DATA = """\
+#cloud-config
+runcmd:
+ - touch /var/tmp/seeded_vendordata_test_file
+"""
+
+
+# Only running on LXD because we need NoCloud for this test
+@pytest.mark.lxd_container
+@pytest.mark.lxd_vm
+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")
diff --git a/tests/integration_tests/bugs/test_gh626.py b/tests/integration_tests/bugs/test_gh626.py
new file mode 100644
index 00000000..b80b677a
--- /dev/null
+++ b/tests/integration_tests/bugs/test_gh626.py
@@ -0,0 +1,43 @@
+"""Integration test for gh-626.
+
+Ensure if wakeonlan is specified in the network config that it is rendered
+in the /etc/network/interfaces or netplan config.
+"""
+
+import pytest
+import yaml
+
+from tests.integration_tests import random_mac_address
+from tests.integration_tests.instances import IntegrationInstance
+
+MAC_ADDRESS = random_mac_address()
+NETWORK_CONFIG = """\
+version: 2
+ethernets:
+ eth0:
+ dhcp4: true
+ wakeonlan: true
+ match:
+ macaddress: {}
+""".format(
+ MAC_ADDRESS
+)
+
+EXPECTED_ENI_END = """\
+iface eth0 inet dhcp
+ ethernet-wol g"""
+
+
+@pytest.mark.lxd_container
+@pytest.mark.lxd_vm
+@pytest.mark.lxd_config_dict(
+ {
+ "user.network-config": NETWORK_CONFIG,
+ "volatile.eth0.hwaddr": MAC_ADDRESS,
+ }
+)
+def test_wakeonlan(client: IntegrationInstance):
+ netplan_cfg = client.execute("cat /etc/netplan/50-cloud-init.yaml")
+ netplan_yaml = yaml.safe_load(netplan_cfg)
+ assert "wakeonlan" in netplan_yaml["network"]["ethernets"]["eth0"]
+ assert netplan_yaml["network"]["ethernets"]["eth0"]["wakeonlan"] is True
diff --git a/tests/integration_tests/bugs/test_gh632.py b/tests/integration_tests/bugs/test_gh632.py
new file mode 100644
index 00000000..c7a897c6
--- /dev/null
+++ b/tests/integration_tests/bugs/test_gh632.py
@@ -0,0 +1,33 @@
+"""Integration test for gh-632.
+
+Verify that if cloud-init is using DataSourceRbxCloud, there is
+no traceback if the metadata disk cannot be found.
+"""
+import pytest
+
+from tests.integration_tests.instances import IntegrationInstance
+from tests.integration_tests.util import verify_clean_log
+
+
+# With some datasource hacking, we can run this on a NoCloud instance
+@pytest.mark.lxd_container
+@pytest.mark.lxd_vm
+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")
+ 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
+ )
diff --git a/tests/integration_tests/bugs/test_gh668.py b/tests/integration_tests/bugs/test_gh668.py
new file mode 100644
index 00000000..95edb48d
--- /dev/null
+++ b/tests/integration_tests/bugs/test_gh668.py
@@ -0,0 +1,46 @@
+"""Integration test for gh-668.
+
+Ensure that static route to host is working correctly.
+The original problem is specific to the ENI renderer but that test is suitable
+for all network configuration outputs.
+"""
+
+import pytest
+
+from tests.integration_tests import random_mac_address
+from tests.integration_tests.instances import IntegrationInstance
+
+DESTINATION_IP = "172.16.0.10"
+GATEWAY_IP = "10.0.0.100"
+MAC_ADDRESS = random_mac_address()
+
+NETWORK_CONFIG = """\
+version: 2
+ethernets:
+ eth0:
+ addresses: [10.0.0.10/8]
+ dhcp4: false
+ routes:
+ - to: {}/32
+ via: {}
+ match:
+ macaddress: {}
+""".format(
+ DESTINATION_IP, GATEWAY_IP, MAC_ADDRESS
+)
+
+EXPECTED_ROUTE = "{} via {}".format(DESTINATION_IP, GATEWAY_IP)
+
+
+@pytest.mark.lxd_container
+@pytest.mark.lxd_vm
+@pytest.mark.lxd_config_dict(
+ {
+ "user.network-config": NETWORK_CONFIG,
+ "volatile.eth0.hwaddr": MAC_ADDRESS,
+ }
+)
+@pytest.mark.lxd_use_exec
+def test_static_route_to_host(client: IntegrationInstance):
+ route = client.execute("ip route | grep {}".format(DESTINATION_IP))
+ assert route.startswith(EXPECTED_ROUTE)
diff --git a/tests/integration_tests/bugs/test_gh671.py b/tests/integration_tests/bugs/test_gh671.py
new file mode 100644
index 00000000..2d7c8118
--- /dev/null
+++ b/tests/integration_tests/bugs/test_gh671.py
@@ -0,0 +1,53 @@
+"""Integration test for gh-671.
+
+Verify that on Azure that if a default user and password are specified
+through the Azure API that a change in the default password overwrites
+the old password
+"""
+
+import crypt
+
+import pytest
+
+from tests.integration_tests.clouds import IntegrationCloud
+
+OLD_PASSWORD = "DoIM33tTheComplexityRequirements!??"
+NEW_PASSWORD = "DoIM33tTheComplexityRequirementsNow!??"
+
+
+def _check_password(instance, unhashed_password):
+ shadow_password = instance.execute("getent shadow ubuntu").split(":")[1]
+ salt = shadow_password.rsplit("$", 1)[0]
+ hashed_password = crypt.crypt(unhashed_password, salt)
+ assert shadow_password == hashed_password
+
+
+@pytest.mark.azure
+def test_update_default_password(setup_image, session_cloud: IntegrationCloud):
+ os_profile = {
+ "os_profile": {
+ "admin_password": "",
+ "linux_configuration": {"disable_password_authentication": False},
+ }
+ }
+ os_profile["os_profile"]["admin_password"] = OLD_PASSWORD
+ instance1 = session_cloud.launch(launch_kwargs={"vm_params": os_profile})
+
+ _check_password(instance1, OLD_PASSWORD)
+
+ snapshot_id = instance1.cloud.cloud_instance.snapshot(
+ instance1.instance, delete_provisioned_user=False
+ )
+
+ os_profile["os_profile"]["admin_password"] = NEW_PASSWORD
+ try:
+ with session_cloud.launch(
+ launch_kwargs={
+ "image_id": snapshot_id,
+ "vm_params": os_profile,
+ }
+ ) as instance2:
+ _check_password(instance2, NEW_PASSWORD)
+ finally:
+ session_cloud.cloud_instance.delete_image(snapshot_id)
+ instance1.destroy()
diff --git a/tests/integration_tests/bugs/test_gh868.py b/tests/integration_tests/bugs/test_gh868.py
new file mode 100644
index 00000000..a62e8b36
--- /dev/null
+++ b/tests/integration_tests/bugs/test_gh868.py
@@ -0,0 +1,27 @@
+"""Ensure no Traceback when 'chef_license' is set"""
+import pytest
+
+from tests.integration_tests.instances import IntegrationInstance
+from tests.integration_tests.util import verify_clean_log
+
+USERDATA = """\
+#cloud-config
+chef:
+ install_type: omnibus
+ chef_license: accept
+ server_url: https://chef.yourorg.invalid
+ validation_name: some-validator
+"""
+
+
+@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")
+ verify_clean_log(log)
diff --git a/tests/integration_tests/bugs/test_lp1813396.py b/tests/integration_tests/bugs/test_lp1813396.py
new file mode 100644
index 00000000..ddae02f5
--- /dev/null
+++ b/tests/integration_tests/bugs/test_lp1813396.py
@@ -0,0 +1,31 @@
+"""Integration test for lp-1813396
+
+Ensure gpg is called with no tty flag.
+"""
+
+import pytest
+
+from tests.integration_tests.instances import IntegrationInstance
+from tests.integration_tests.util import verify_ordered_items_in_text
+
+USER_DATA = """\
+#cloud-config
+apt:
+ sources:
+ cloudinit:
+ source: 'deb [arch=amd64] http://ppa.launchpad.net/cloud-init-dev/daily/ubuntu focal main'
+ keyserver: keyserver.ubuntu.com
+ keyid: E4D304DF
+""" # noqa: E501
+
+
+@pytest.mark.user_data(USER_DATA)
+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)
diff --git a/tests/integration_tests/bugs/test_lp1835584.py b/tests/integration_tests/bugs/test_lp1835584.py
new file mode 100644
index 00000000..765d73ef
--- /dev/null
+++ b/tests/integration_tests/bugs/test_lp1835584.py
@@ -0,0 +1,101 @@
+""" Integration test for LP #1835584
+
+Upstream linux kernels prior to 4.15 provide DMI product_uuid in uppercase.
+More recent kernels switched to lowercase for DMI product_uuid. Azure
+datasource uses this product_uuid as the instance-id for cloud-init.
+
+The linux-azure-fips kernel installed in PRO FIPs images, that product UUID is
+uppercase whereas the linux-azure cloud-optimized kernel reports the UUID as
+lowercase.
+
+In cases where product_uuid changes case, ensure cloud-init doesn't
+recreate ssh hostkeys across reboot (due to detecting an instance_id change).
+
+This currently only affects linux-azure-fips -> linux-azure on Bionic.
+This test won't run on Xenial because both linux-azure-fips and linux-azure
+report uppercase product_uuids.
+
+The test will launch a specific Bionic Ubuntu PRO FIPS image which has a
+linux-azure-fips kernel known to report product_uuid as uppercase. Then upgrade
+and reboot into linux-azure kernel which is known to report product_uuid as
+lowercase.
+
+Across the reboot, assert that we didn't re-run config_ssh by virtue of
+seeing only one semaphore creation log entry of type:
+
+ Writing to /var/lib/cloud/instances/<UUID>/sem/config_ssh -
+
+https://bugs.launchpad.net/cloud-init/+bug/1835584
+"""
+import re
+
+import pytest
+
+from tests.integration_tests.clouds import ImageSpecification, IntegrationCloud
+from tests.integration_tests.conftest import get_validated_source
+from tests.integration_tests.instances import IntegrationInstance
+
+IMG_AZURE_UBUNTU_PRO_FIPS_BIONIC = (
+ "Canonical:0001-com-ubuntu-pro-bionic-fips:pro-fips-18_04:18.04.202010201"
+)
+
+
+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)
+
+
+@pytest.mark.azure
+def test_azure_kernel_upgrade_case_insensitive_uuid(
+ session_cloud: IntegrationCloud,
+):
+ cfg_image_spec = ImageSpecification.from_os_image()
+ if (cfg_image_spec.os, cfg_image_spec.release) != ("ubuntu", "bionic"):
+ pytest.skip(
+ "Test only supports ubuntu:bionic not {0.os}:{0.release}".format(
+ cfg_image_spec
+ )
+ )
+ source = get_validated_source(session_cloud)
+ if not source.installs_new_version():
+ pytest.skip(
+ "Provide CLOUD_INIT_SOURCE to install expected working cloud-init"
+ )
+ image_id = IMG_AZURE_UBUNTU_PRO_FIPS_BIONIC
+ with session_cloud.launch(
+ launch_kwargs={"image_id": image_id}
+ ) as instance:
+ # We can't use setup_image fixture here because we want to avoid
+ # taking a snapshot or cleaning the booted machine after cloud-init
+ # upgrade.
+ instance.install_new_cloud_init(
+ source, take_snapshot=False, clean=False
+ )
+ _check_iid_insensitive_across_kernel_upgrade(instance)
diff --git a/tests/integration_tests/bugs/test_lp1886531.py b/tests/integration_tests/bugs/test_lp1886531.py
index 058ea8bb..d56ca320 100644
--- a/tests/integration_tests/bugs/test_lp1886531.py
+++ b/tests/integration_tests/bugs/test_lp1886531.py
@@ -11,6 +11,7 @@ https://bugs.launchpad.net/ubuntu/+source/cloud-init/+bug/1886531
"""
import pytest
+from tests.integration_tests.util import verify_clean_log
USER_DATA = """\
#cloud-config
@@ -20,8 +21,7 @@ bootcmd:
class TestLp1886531:
-
@pytest.mark.user_data(USER_DATA)
def test_lp1886531(self, client):
log_content = client.read_from_file("/var/log/cloud-init.log")
- assert "WARNING" not in log_content
+ verify_clean_log(log_content)
diff --git a/tests/integration_tests/bugs/test_lp1897099.py b/tests/integration_tests/bugs/test_lp1897099.py
index 27c8927f..1f5030ce 100644
--- a/tests/integration_tests/bugs/test_lp1897099.py
+++ b/tests/integration_tests/bugs/test_lp1897099.py
@@ -7,7 +7,6 @@ https://bugs.launchpad.net/cloud-init/+bug/1897099
import pytest
-
USER_DATA = """\
#cloud-config
bootcmd:
@@ -19,13 +18,12 @@ swap:
"""
-@pytest.mark.sru_2020_11
@pytest.mark.user_data(USER_DATA)
-@pytest.mark.no_container('Containers cannot configure swap')
+@pytest.mark.no_container("Containers cannot configure swap")
def test_fallocate_fallback(client):
- log = client.read_from_file('/var/log/cloud-init.log')
- assert '/swap.img' in client.execute('cat /proc/swaps')
- assert '/swap.img' in client.execute('cat /etc/fstab')
- assert 'fallocate swap creation failed, will attempt with dd' in log
+ log = client.read_from_file("/var/log/cloud-init.log")
+ assert "/swap.img" in client.execute("cat /proc/swaps")
+ assert "/swap.img" in client.execute("cat /etc/fstab")
+ assert "fallocate swap creation failed, will attempt with dd" in log
assert "Running command ['dd', 'if=/dev/zero', 'of=/swap.img'" in log
- assert 'SUCCESS: config-mounts ran successfully' in log
+ assert "SUCCESS: config-mounts ran successfully" in log
diff --git a/tests/integration_tests/bugs/test_lp1898997.py b/tests/integration_tests/bugs/test_lp1898997.py
new file mode 100644
index 00000000..d8ea54c3
--- /dev/null
+++ b/tests/integration_tests/bugs/test_lp1898997.py
@@ -0,0 +1,77 @@
+"""Integration test for LP: #1898997
+
+cloud-init was incorrectly excluding Open vSwitch bridge members from its list
+of interfaces. This meant that instances which had only one interface which
+was in an Open vSwitch bridge would not boot correctly: cloud-init would not
+find the expected physical interfaces, so would not apply network config.
+
+This test checks that cloud-init believes it has successfully applied the
+network configuration, and confirms that the bridge can be used to ping the
+default gateway.
+"""
+import pytest
+
+from tests.integration_tests import random_mac_address
+from tests.integration_tests.util import verify_clean_log
+
+MAC_ADDRESS = random_mac_address()
+
+
+NETWORK_CONFIG = """\
+bridges:
+ ovs-br:
+ dhcp4: true
+ interfaces:
+ - enp5s0
+ macaddress: 52:54:00:d9:08:1c
+ mtu: 1500
+ openvswitch: {{}}
+ethernets:
+ enp5s0:
+ mtu: 1500
+ set-name: enp5s0
+ match:
+ macaddress: {}
+version: 2
+""".format(
+ MAC_ADDRESS
+)
+
+
+@pytest.mark.lxd_config_dict(
+ {
+ "user.network-config": NETWORK_CONFIG,
+ "volatile.eth0.hwaddr": MAC_ADDRESS,
+ }
+)
+@pytest.mark.lxd_vm
+@pytest.mark.lxd_use_exec
+@pytest.mark.not_bionic
+@pytest.mark.ubuntu
+class TestInterfaceListingWithOpenvSwitch:
+ def test_ovs_member_interfaces_not_excluded(self, client):
+ # We need to install openvswitch for our provided network configuration
+ # to apply (on next boot), so DHCP on our default interface to fetch it
+ client.execute("dhclient enp5s0")
+ client.execute("apt update -qqy")
+ client.execute("apt-get install -qqy openvswitch-switch")
+
+ # Now our networking config should successfully apply on a clean reboot
+ client.execute("cloud-init clean --logs")
+ client.restart()
+
+ cloudinit_output = client.read_from_file("/var/log/cloud-init.log")
+
+ # Confirm that the network configuration was applied successfully
+ verify_clean_log(cloudinit_output)
+ # Confirm that the applied network config created the OVS bridge
+ assert "ovs-br" in client.execute("ip addr")
+
+ # Test that we can ping our gateway using our bridge
+ gateway = client.execute(
+ "ip -4 route show default | awk '{ print $3 }'"
+ )
+ ping_result = client.execute(
+ "ping -c 1 -W 1 -I ovs-br {}".format(gateway)
+ )
+ assert ping_result.ok
diff --git a/tests/integration_tests/bugs/test_lp1900837.py b/tests/integration_tests/bugs/test_lp1900837.py
index 3fe7d0d0..d9ef18aa 100644
--- a/tests/integration_tests/bugs/test_lp1900837.py
+++ b/tests/integration_tests/bugs/test_lp1900837.py
@@ -4,14 +4,12 @@ This test mirrors the reproducing steps from the reported bug: it changes the
permissions on cloud-init.log to 600 and confirms that they remain 600 after a
reboot.
"""
-import pytest
def _get_log_perms(client):
return client.execute("stat -c %a /var/log/cloud-init.log")
-@pytest.mark.sru_2020_11
class TestLogPermissionsNotResetOnReboot:
def test_permissions_unchanged(self, client):
# Confirm that the current permissions aren't 600
@@ -22,7 +20,8 @@ class TestLogPermissionsNotResetOnReboot:
assert "600" == _get_log_perms(client)
# Reboot
- client.instance.restart()
+ client.restart()
+ assert client.execute("cloud-init status").ok
# Check that permissions are not reset on reboot
assert "600" == _get_log_perms(client)
diff --git a/tests/integration_tests/bugs/test_lp1901011.py b/tests/integration_tests/bugs/test_lp1901011.py
new file mode 100644
index 00000000..7de8bd77
--- /dev/null
+++ b/tests/integration_tests/bugs/test_lp1901011.py
@@ -0,0 +1,67 @@
+"""Integration test for LP: #1901011
+
+Ensure an ephemeral disk exists after boot.
+
+See https://github.com/canonical/cloud-init/pull/800
+"""
+import pytest
+
+from tests.integration_tests.clouds import IntegrationCloud
+
+
+@pytest.mark.azure
+@pytest.mark.parametrize(
+ "instance_type,is_ephemeral",
+ [
+ ("Standard_DS1_v2", True),
+ ("Standard_D2s_v4", False),
+ ],
+)
+def test_ephemeral(
+ instance_type, is_ephemeral, session_cloud: IntegrationCloud, setup_image
+):
+ if is_ephemeral:
+ expected_log = (
+ "Ephemeral resource disk '/dev/disk/cloud/azure_resource' exists. "
+ "Merging default Azure cloud ephemeral disk configs."
+ )
+ else:
+ expected_log = (
+ "Ephemeral resource disk '/dev/disk/cloud/azure_resource' does "
+ "not exist. Not merging default Azure cloud ephemeral disk "
+ "configs."
+ )
+
+ with session_cloud.launch(
+ launch_kwargs={"instance_type": instance_type}
+ ) as client:
+ # Verify log file
+ log = client.read_from_file("/var/log/cloud-init.log")
+ assert expected_log in log
+
+ # Verify devices
+ dev_links = client.execute("ls /dev/disk/cloud")
+ assert "azure_root" in dev_links
+ assert "azure_root-part1" in dev_links
+ if is_ephemeral:
+ assert "azure_resource" in dev_links
+ assert "azure_resource-part1" in dev_links
+
+ # Verify mounts
+ blks = client.execute("lsblk -pPo NAME,TYPE,MOUNTPOINT")
+ root_device = client.execute(
+ "realpath /dev/disk/cloud/azure_root-part1"
+ )
+ assert (
+ 'NAME="{}" TYPE="part" MOUNTPOINT="/"'.format(root_device) in blks
+ )
+ if is_ephemeral:
+ ephemeral_device = client.execute(
+ "realpath /dev/disk/cloud/azure_resource-part1"
+ )
+ assert (
+ 'NAME="{}" TYPE="part" MOUNTPOINT="/mnt"'.format(
+ ephemeral_device
+ )
+ in blks
+ )
diff --git a/tests/integration_tests/bugs/test_lp1910835.py b/tests/integration_tests/bugs/test_lp1910835.py
new file mode 100644
index 00000000..1844594c
--- /dev/null
+++ b/tests/integration_tests/bugs/test_lp1910835.py
@@ -0,0 +1,64 @@
+"""Integration test for LP: #1910835.
+
+If users do not provide an SSH key and instead ask Azure to generate a key for
+them, the key material available in the IMDS may include CRLF sequences. Prior
+to e56b55452549cb037da0a4165154ffa494e9678a, the Azure datasource handled keys
+via a certificate, the tooling for which removed these sequences. This test
+ensures that cloud-init does not regress support for this Azure behaviour.
+
+This test provides the SSH key configured for tests to the instance in two
+ways: firstly, with CRLFs to mimic the generated keys, via the Azure API;
+secondly, as user-data in unmodified form. This means that even on systems
+which exhibit the bug fetching the platform's metadata, we can SSH into the SUT
+to confirm this (instead of having to assert SSH failure; there are lots of
+reasons SSH might fail).
+
+Once SSH'd in, we check that the two keys in .ssh/authorized_keys have the same
+material: if the Azure datasource has removed the CRLFs correctly, then they
+will match.
+"""
+import pytest
+
+USER_DATA_TMPL = """\
+#cloud-config
+ssh_authorized_keys:
+ - {}"""
+
+
+@pytest.mark.azure
+def test_crlf_in_azure_metadata_ssh_keys(session_cloud, setup_image):
+ authorized_keys_path = "/home/{}/.ssh/authorized_keys".format(
+ session_cloud.cloud_instance.username
+ )
+ # Pass in user-data to allow us to access the instance when the normal
+ # path fails
+ key_data = session_cloud.cloud_instance.key_pair.public_key_content
+ user_data = USER_DATA_TMPL.format(key_data)
+ # Throw a CRLF into the otherwise good key data, to emulate Azure's
+ # behaviour for generated keys
+ key_data = key_data[:20] + "\r\n" + key_data[20:]
+ vm_params = {
+ "os_profile": {
+ "linux_configuration": {
+ "ssh": {
+ "public_keys": [
+ {"path": authorized_keys_path, "key_data": key_data}
+ ]
+ }
+ }
+ }
+ }
+ with session_cloud.launch(
+ launch_kwargs={"vm_params": vm_params, "user_data": user_data}
+ ) as client:
+ authorized_keys = (
+ client.read_from_file(authorized_keys_path).strip().splitlines()
+ )
+ # We expect one key from the cloud, one from user-data
+ assert 2 == len(authorized_keys)
+ # And those two keys should be the same, except for a possible key
+ # comment, which Azure strips out
+ assert (
+ authorized_keys[0].rsplit(" ")[:2]
+ == authorized_keys[1].split(" ")[:2]
+ )
diff --git a/tests/integration_tests/bugs/test_lp1912844.py b/tests/integration_tests/bugs/test_lp1912844.py
new file mode 100644
index 00000000..55511ed2
--- /dev/null
+++ b/tests/integration_tests/bugs/test_lp1912844.py
@@ -0,0 +1,105 @@
+"""Integration test for LP: #1912844
+
+cloud-init should ignore OVS-internal interfaces when performing its own
+interface determination: these interfaces are handled fully by OVS, so
+cloud-init should never need to touch them.
+
+This test is a semi-synthetic reproducer for the bug. It uses a similar
+network configuration, tweaked slightly to DHCP in a way that will succeed even
+on "failed" boots. The exact bug doesn't reproduce with the NoCloud
+datasource, because it runs at init-local time (whereas the MAAS datasource,
+from the report, runs only at init (network) time): this means that the
+networking code runs before OVS creates its interfaces (which happens after
+init-local but, of course, before networking is up), and so doesn't generate
+the traceback that they cause. We work around this by calling
+``get_interfaces_by_mac` directly in the test code.
+"""
+import pytest
+
+from tests.integration_tests import random_mac_address
+
+MAC_ADDRESS = random_mac_address()
+
+NETWORK_CONFIG = """\
+bonds:
+ bond0:
+ interfaces:
+ - enp5s0
+ macaddress: {0}
+ mtu: 1500
+bridges:
+ ovs-br:
+ interfaces:
+ - bond0
+ macaddress: {0}
+ mtu: 1500
+ openvswitch: {{}}
+ dhcp4: true
+ethernets:
+ enp5s0:
+ mtu: 1500
+ set-name: enp5s0
+ match:
+ macaddress: {0}
+version: 2
+vlans:
+ ovs-br.100:
+ id: 100
+ link: ovs-br
+ mtu: 1500
+ ovs-br.200:
+ id: 200
+ link: ovs-br
+ mtu: 1500
+""".format(
+ MAC_ADDRESS
+)
+
+
+SETUP_USER_DATA = """\
+#cloud-config
+packages:
+- openvswitch-switch
+"""
+
+
+@pytest.fixture
+def ovs_enabled_session_cloud(session_cloud):
+ """A session_cloud wrapper, to use an OVS-enabled image for tests.
+
+ This implementation is complicated by wanting to use ``session_cloud``s
+ snapshot cleanup/retention logic, to avoid having to reimplement that here.
+ """
+ old_snapshot_id = session_cloud.snapshot_id
+ with session_cloud.launch(
+ user_data=SETUP_USER_DATA,
+ ) as instance:
+ instance.instance.clean()
+ session_cloud.snapshot_id = instance.snapshot()
+
+ yield session_cloud
+
+ try:
+ session_cloud.delete_snapshot()
+ finally:
+ session_cloud.snapshot_id = old_snapshot_id
+
+
+@pytest.mark.lxd_vm
+def test_get_interfaces_by_mac_doesnt_traceback(ovs_enabled_session_cloud):
+ """Launch our OVS-enabled image and confirm the bug doesn't reproduce."""
+ launch_kwargs = {
+ "config_dict": {
+ "user.network-config": NETWORK_CONFIG,
+ "volatile.eth0.hwaddr": MAC_ADDRESS,
+ },
+ }
+ with ovs_enabled_session_cloud.launch(
+ launch_kwargs=launch_kwargs,
+ ) as client:
+ result = client.execute(
+ "python3 -c"
+ "'from cloudinit.net import get_interfaces_by_mac;"
+ "get_interfaces_by_mac()'"
+ )
+ assert result.ok