|
| 1 | +//! Integration tests for cloud-init ConfigDrive functionality |
| 2 | +//! |
| 3 | +//! These tests verify: |
| 4 | +//! - ConfigDrive generation from user-provided cloud-config files |
| 5 | +//! - ConfigDrive device creation and accessibility |
| 6 | +//! - ConfigDrive content structure (OpenStack format) |
| 7 | +//! - Kernel cmdline does NOT contain `ds=iid-datasource-none` when using ConfigDrive |
| 8 | +//! - Cloud-init processing of the ConfigDrive (using localhost/bootc-cloud-init image) |
| 9 | +
|
| 10 | +use color_eyre::eyre::Context as _; |
| 11 | +use color_eyre::Result; |
| 12 | +use integration_tests::integration_test; |
| 13 | +use linkme::distributed_slice; |
| 14 | + |
| 15 | +use crate::{run_bcvk, INTEGRATION_TEST_LABEL}; |
| 16 | + |
| 17 | +/// Get the cloud-init test image (built from tests/fixtures/cloud-init/) |
| 18 | +fn get_cloud_init_test_image() -> String { |
| 19 | + std::env::var("BCVK_CLOUD_INIT_TEST_IMAGE") |
| 20 | + .unwrap_or_else(|_| "localhost/bootc-cloud-init".to_string()) |
| 21 | +} |
| 22 | + |
| 23 | +/// Test basic cloud-init ConfigDrive functionality |
| 24 | +/// |
| 25 | +/// Creates a cloud-config file, runs an ephemeral VM with --cloud-init, |
| 26 | +/// and verifies that: |
| 27 | +/// - The ConfigDrive device exists at /dev/disk/by-id/virtio-config-2 |
| 28 | +/// - The ConfigDrive can be mounted and contains expected OpenStack structure |
| 29 | +/// - The user_data file contains the cloud-config content |
| 30 | +/// - The meta_data.json contains the instance-id |
| 31 | +fn test_cloud_init_configdrive_basic() -> Result<()> { |
| 32 | + let test_image = get_cloud_init_test_image(); |
| 33 | + |
| 34 | + println!("Testing basic cloud-init ConfigDrive functionality"); |
| 35 | + |
| 36 | + // Create a temporary cloud-config file |
| 37 | + let cloud_config_dir = tempfile::tempdir().context("Failed to create temp directory")?; |
| 38 | + let cloud_config_path = cloud_config_dir |
| 39 | + .path() |
| 40 | + .join("cloud-config.yaml") |
| 41 | + .to_str() |
| 42 | + .ok_or_else(|| color_eyre::eyre::eyre!("Invalid UTF-8 in temp path"))? |
| 43 | + .to_string(); |
| 44 | + |
| 45 | + // Create a simple cloud-config with identifiable content |
| 46 | + let cloud_config_content = r#"#cloud-config |
| 47 | +write_files: |
| 48 | + - path: /tmp/test-marker |
| 49 | + content: | |
| 50 | + ConfigDrive test content |
| 51 | + permissions: '0644' |
| 52 | +
|
| 53 | +runcmd: |
| 54 | + - echo "Test command from cloud-config" |
| 55 | +"#; |
| 56 | + |
| 57 | + std::fs::write(&cloud_config_path, cloud_config_content) |
| 58 | + .context("Failed to write cloud-config file")?; |
| 59 | + |
| 60 | + println!("Created cloud-config file at: {}", cloud_config_path); |
| 61 | + |
| 62 | + // Run ephemeral VM and verify ConfigDrive structure |
| 63 | + println!("Running ephemeral VM with --cloud-init..."); |
| 64 | + let output = run_bcvk(&[ |
| 65 | + "ephemeral", |
| 66 | + "run", |
| 67 | + "--rm", |
| 68 | + "--label", |
| 69 | + INTEGRATION_TEST_LABEL, |
| 70 | + "--cloud-init", |
| 71 | + &cloud_config_path, |
| 72 | + "--execute", |
| 73 | + "/bin/sh -c 'ls -la /dev/disk/by-id/virtio-config-2 && mkdir -p /mnt/configdrive && mount /dev/disk/by-id/virtio-config-2 /mnt/configdrive && ls -la /mnt/configdrive/ && cat /mnt/configdrive/openstack/latest/user_data && cat /mnt/configdrive/openstack/latest/meta_data.json'", |
| 74 | + &test_image, |
| 75 | + ])?; |
| 76 | + |
| 77 | + println!("VM execution completed"); |
| 78 | + |
| 79 | + // Check the output |
| 80 | + println!("=== STDOUT ==="); |
| 81 | + println!("{}", output.stdout); |
| 82 | + println!("=== STDERR ==="); |
| 83 | + println!("{}", output.stderr); |
| 84 | + |
| 85 | + let combined_output = format!("{}\n{}", output.stdout, output.stderr); |
| 86 | + |
| 87 | + // Verify ConfigDrive device symlink exists |
| 88 | + assert!( |
| 89 | + combined_output.contains("virtio-config-2"), |
| 90 | + "ConfigDrive device symlink 'virtio-config-2' not found in output. Output: {}", |
| 91 | + combined_output |
| 92 | + ); |
| 93 | + |
| 94 | + // Verify user_data contains the cloud-config header |
| 95 | + assert!( |
| 96 | + combined_output.contains("#cloud-config"), |
| 97 | + "user_data does not contain #cloud-config header. Output: {}", |
| 98 | + combined_output |
| 99 | + ); |
| 100 | + |
| 101 | + // Verify user_data contains our test content |
| 102 | + assert!( |
| 103 | + combined_output.contains("ConfigDrive test content"), |
| 104 | + "user_data does not contain expected test content. Output: {}", |
| 105 | + combined_output |
| 106 | + ); |
| 107 | + |
| 108 | + // Verify meta_data.json contains uuid (which cloud-init maps to instance-id) |
| 109 | + assert!( |
| 110 | + combined_output.contains("uuid"), |
| 111 | + "meta_data.json does not contain uuid. Output: {}", |
| 112 | + combined_output |
| 113 | + ); |
| 114 | + |
| 115 | + // Also verify it contains the expected uuid value |
| 116 | + assert!( |
| 117 | + combined_output.contains("iid-local01"), |
| 118 | + "meta_data.json does not contain expected uuid value 'iid-local01'. Output: {}", |
| 119 | + combined_output |
| 120 | + ); |
| 121 | + |
| 122 | + println!("✓ Basic cloud-init ConfigDrive test passed"); |
| 123 | + output.assert_success("ephemeral run with cloud-init"); |
| 124 | + Ok(()) |
| 125 | +} |
| 126 | +integration_test!(test_cloud_init_configdrive_basic); |
| 127 | + |
| 128 | +/// Test that kernel cmdline does NOT contain `ds=iid-datasource-none` when using ConfigDrive |
| 129 | +/// |
| 130 | +/// When a ConfigDrive is provided, the kernel cmdline should NOT contain the |
| 131 | +/// `ds=iid-datasource-none` parameter which would disable cloud-init. |
| 132 | +/// This test verifies the cmdline directly without depending on cloud-init. |
| 133 | +fn test_cloud_init_no_datasource_cmdline() -> Result<()> { |
| 134 | + let test_image = get_cloud_init_test_image(); |
| 135 | + |
| 136 | + println!("Testing kernel cmdline does NOT contain ds=iid-datasource-none with ConfigDrive"); |
| 137 | + |
| 138 | + // Create a temporary cloud-config file |
| 139 | + let cloud_config_dir = tempfile::tempdir().context("Failed to create temp directory")?; |
| 140 | + let cloud_config_path = cloud_config_dir |
| 141 | + .path() |
| 142 | + .join("cloud-config.yaml") |
| 143 | + .to_str() |
| 144 | + .ok_or_else(|| color_eyre::eyre::eyre!("Invalid UTF-8 in temp path"))? |
| 145 | + .to_string(); |
| 146 | + |
| 147 | + // Create a minimal cloud-config |
| 148 | + let cloud_config_content = r#"#cloud-config |
| 149 | +runcmd: |
| 150 | + - echo "test" |
| 151 | +"#; |
| 152 | + |
| 153 | + std::fs::write(&cloud_config_path, cloud_config_content) |
| 154 | + .context("Failed to write cloud-config file")?; |
| 155 | + |
| 156 | + println!("Created cloud-config file"); |
| 157 | + |
| 158 | + // Run ephemeral VM and check /proc/cmdline directly |
| 159 | + println!("Running ephemeral VM to check kernel cmdline..."); |
| 160 | + let output = run_bcvk(&[ |
| 161 | + "ephemeral", |
| 162 | + "run", |
| 163 | + "--rm", |
| 164 | + "--label", |
| 165 | + INTEGRATION_TEST_LABEL, |
| 166 | + "--cloud-init", |
| 167 | + &cloud_config_path, |
| 168 | + "--execute", |
| 169 | + "cat /proc/cmdline", |
| 170 | + &test_image, |
| 171 | + ])?; |
| 172 | + |
| 173 | + println!("VM execution completed"); |
| 174 | + println!("=== Output ==="); |
| 175 | + println!("{}", output.stdout); |
| 176 | + |
| 177 | + // Get the kernel cmdline from the output |
| 178 | + let combined_output = format!("{}\n{}", output.stdout, output.stderr); |
| 179 | + |
| 180 | + // Verify that ds=iid-datasource-none is NOT present in the cmdline |
| 181 | + assert!( |
| 182 | + !combined_output.contains("ds=iid-datasource-none"), |
| 183 | + "Kernel cmdline should NOT contain 'ds=iid-datasource-none' when using ConfigDrive.\nOutput: {}", |
| 184 | + combined_output |
| 185 | + ); |
| 186 | + |
| 187 | + println!("✓ Kernel cmdline does NOT contain ds=iid-datasource-none"); |
| 188 | + output.assert_success("ephemeral run with cloud-init"); |
| 189 | + Ok(()) |
| 190 | +} |
| 191 | +integration_test!(test_cloud_init_no_datasource_cmdline); |
| 192 | + |
| 193 | +/// Test that ConfigDrive contains expected user_data content |
| 194 | +/// |
| 195 | +/// Creates a cloud-config with multiple runcmd directives, |
| 196 | +/// then verifies the ConfigDrive user_data contains all expected content. |
| 197 | +/// This test does NOT depend on cloud-init being installed - it directly |
| 198 | +/// inspects the ConfigDrive contents. |
| 199 | +fn test_cloud_init_configdrive_content() -> Result<()> { |
| 200 | + let test_image = get_cloud_init_test_image(); |
| 201 | + |
| 202 | + println!("Testing ConfigDrive content verification"); |
| 203 | + |
| 204 | + // Create a temporary cloud-config file |
| 205 | + let cloud_config_dir = tempfile::tempdir().context("Failed to create temp directory")?; |
| 206 | + let cloud_config_path = cloud_config_dir |
| 207 | + .path() |
| 208 | + .join("cloud-config.yaml") |
| 209 | + .to_str() |
| 210 | + .ok_or_else(|| color_eyre::eyre::eyre!("Invalid UTF-8 in temp path"))? |
| 211 | + .to_string(); |
| 212 | + |
| 213 | + // Create a cloud-config with multiple runcmd directives |
| 214 | + let cloud_config_content = r#"#cloud-config |
| 215 | +runcmd: |
| 216 | + - echo "RUNCMD_TEST_1_SUCCESS" |
| 217 | + - echo "RUNCMD_TEST_2_SUCCESS" |
| 218 | + - echo "RUNCMD_TEST_3_SUCCESS" |
| 219 | +"#; |
| 220 | + |
| 221 | + std::fs::write(&cloud_config_path, cloud_config_content) |
| 222 | + .context("Failed to write cloud-config file")?; |
| 223 | + |
| 224 | + println!("Created cloud-config with runcmd directives"); |
| 225 | + |
| 226 | + // Run ephemeral VM and verify ConfigDrive user_data content |
| 227 | + println!("Running ephemeral VM to verify ConfigDrive content..."); |
| 228 | + let output = run_bcvk(&[ |
| 229 | + "ephemeral", |
| 230 | + "run", |
| 231 | + "--rm", |
| 232 | + "--label", |
| 233 | + INTEGRATION_TEST_LABEL, |
| 234 | + "--cloud-init", |
| 235 | + &cloud_config_path, |
| 236 | + "--execute", |
| 237 | + "/bin/sh -c 'mkdir -p /mnt && mount /dev/disk/by-id/virtio-config-2 /mnt && cat /mnt/openstack/latest/user_data'", |
| 238 | + &test_image, |
| 239 | + ])?; |
| 240 | + |
| 241 | + println!("VM execution completed"); |
| 242 | + println!("=== Output ==="); |
| 243 | + println!("{}", output.stdout); |
| 244 | + |
| 245 | + // Verify user_data contains all runcmd directives |
| 246 | + let combined_output = format!("{}\n{}", output.stdout, output.stderr); |
| 247 | + |
| 248 | + assert!( |
| 249 | + combined_output.contains("RUNCMD_TEST_1_SUCCESS"), |
| 250 | + "First runcmd directive not found in user_data. Output: {}", |
| 251 | + combined_output |
| 252 | + ); |
| 253 | + |
| 254 | + assert!( |
| 255 | + combined_output.contains("RUNCMD_TEST_2_SUCCESS"), |
| 256 | + "Second runcmd directive not found in user_data. Output: {}", |
| 257 | + combined_output |
| 258 | + ); |
| 259 | + |
| 260 | + assert!( |
| 261 | + combined_output.contains("RUNCMD_TEST_3_SUCCESS"), |
| 262 | + "Third runcmd directive not found in user_data. Output: {}", |
| 263 | + combined_output |
| 264 | + ); |
| 265 | + |
| 266 | + println!("✓ All expected content found in ConfigDrive user_data"); |
| 267 | + output.assert_success("ephemeral run with cloud-init configdrive content"); |
| 268 | + Ok(()) |
| 269 | +} |
| 270 | +integration_test!(test_cloud_init_configdrive_content); |
0 commit comments