diff --git a/docs/release-notes.md b/docs/release-notes.md index 251df6a6..7723e365 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,8 @@ nav_order: 8 Major changes: +- ProxmoxVE: Add support for static IP configuration from cloud-init + Minor changes: - ProxmoxVE: Fixed instance boot without config drive @@ -160,8 +162,8 @@ Changes: - providers/gcp: access GCP metadata service by IP address - providers/packet: access metadata service over HTTPS - cli: don't report an error when --help or --version is specified -- cli: correctly print version when --version specified -- providers: Add PowerVS +- cli: correctly print version when --version specified +- providers: Add PowerVS - workflows: bump toolchains; restrict repository access @@ -173,7 +175,7 @@ Changes: - cargo: update all dependencies - *: remove cl-legacy feature - ibmcloud: don't ignore I/O error when parsing metadata -- providers: fix clippy::unnecessary_wraps lint on 1.50 +- providers: fix clippy::unnecessary_wraps lint on 1.50 - workflows: update pinned lint toolchain to 1.50.0 - *: switch from `error-chain` to `anyhow` - cli: stop wrapping command-line parse errors @@ -266,7 +268,7 @@ Changes: - sshkeys: send structured info to journald - ci: test a secondary arch on Travis -- ci: rust version from 1.39.0 to 1.40.0 +- ci: rust version from 1.39.0 to 1.40.0 - makefile: tweak install step - providers: add vmware - util/cmdline: add helpers for detecting network kargs @@ -278,7 +280,7 @@ Changes: Changes: -- cargo: relax dependencies micro versions +- cargo: relax dependencies micro versions - cargo: switch from deprecated tempdir crate to tempfile - providers: add exoscale - providers: add ibmcloud-classic as a separate platform @@ -361,7 +363,7 @@ Changes: Changes: -- providers/azure: fetch hostname from metadata +- providers/azure: fetch hostname from metadata - add checkin service files for Azure and Packet - metadata: accept "ec2" provider name only in legacy mode - bump minimum toolchain to 1.31 @@ -384,7 +386,7 @@ Bugfixes: - providers/gce: fix panic fetching metadata -Misc: +Misc: - providers/gce: add basic hostname mock-test - rustfmt whole project @@ -491,4 +493,4 @@ and behavior for all providers should be identical to the previous golang version. If it's not, please file a bug in our bug tracker, https://github.com/coreos/bugs (or submit a pr!). -Additionally, `coreos-metadata` now supports ssh keys for azure. +Additionally, `coreos-metadata` now supports ssh keys for azure. diff --git a/src/initrd/mod.rs b/src/initrd/mod.rs index 9cd4466a..baf92606 100644 --- a/src/initrd/mod.rs +++ b/src/initrd/mod.rs @@ -4,6 +4,7 @@ //! services are configured, so it may not be able to use all usual metadata //! fetcher. +use crate::providers::proxmoxve::ProxmoxVEConfigDrive; use crate::providers::vmware::VmwareProvider; use crate::providers::MetadataProvider; use anyhow::{Context, Result}; @@ -17,6 +18,7 @@ static KARGS_PATH: &str = "/etc/cmdline.d/50-afterburn-network-kargs.conf"; pub(crate) fn fetch_network_kargs(provider: &str) -> Result> { match provider { "vmware" => VmwareProvider::try_new()?.rd_network_kargs(), + "proxmoxve" => ProxmoxVEConfigDrive::try_new()?.rd_network_kargs(), _ => Ok(None), } } diff --git a/src/providers/proxmoxve/cloudconfig.rs b/src/providers/proxmoxve/cloudconfig.rs index 6be26763..4d420931 100644 --- a/src/providers/proxmoxve/cloudconfig.rs +++ b/src/providers/proxmoxve/cloudconfig.rs @@ -2,7 +2,7 @@ use crate::{ network::{self, DhcpSetting, NetworkRoute}, providers::MetadataProvider, }; -use anyhow::Result; +use anyhow::{Context, Result}; use ipnetwork::IpNetwork; use openssh_keys::PublicKey; use pnet_base::MacAddr; @@ -20,6 +20,7 @@ use std::{ pub struct ProxmoxVECloudConfig { pub meta_data: ProxmoxVECloudMetaData, pub user_data: Option, + #[allow(dead_code)] pub vendor_data: ProxmoxVECloudVendorData, pub network_config: ProxmoxVECloudNetworkConfig, } @@ -33,26 +34,15 @@ pub struct ProxmoxVECloudMetaData { #[derive(Debug, Deserialize)] pub struct ProxmoxVECloudUserData { pub hostname: String, - pub manage_etc_hosts: bool, - pub fqdn: String, - pub chpasswd: ProxmoxVECloudChpasswdConfig, - pub users: Vec, - pub package_upgrade: bool, #[serde(default)] pub ssh_authorized_keys: Vec, } -#[derive(Debug, Deserialize)] -pub struct ProxmoxVECloudChpasswdConfig { - pub expire: bool, -} - #[derive(Debug, Deserialize)] pub struct ProxmoxVECloudVendorData {} #[derive(Debug, Deserialize)] pub struct ProxmoxVECloudNetworkConfig { - pub version: u32, pub config: Vec, } @@ -65,8 +55,6 @@ pub struct ProxmoxVECloudNetworkConfigEntry { #[serde(default)] pub address: Vec, #[serde(default)] - pub search: Vec, - #[serde(default)] pub subnets: Vec, } @@ -82,11 +70,13 @@ pub struct ProxmoxVECloudNetworkConfigSubnet { impl ProxmoxVECloudConfig { pub fn try_new(path: &Path) -> Result { let mut user_data = None; - let raw_user_data = std::fs::read_to_string(path.join("user-data"))?; + let raw_user_data = std::fs::read_to_string(path.join("user-data")) + .context("failed to read user-data file")?; if let Some(first_line) = raw_user_data.split('\n').next() { if first_line.starts_with("#cloud-config") { - user_data = serde_yaml::from_str(&raw_user_data)?; + user_data = serde_yaml::from_str(&raw_user_data) + .context("failed to parse user-data as YAML")?; } } @@ -98,9 +88,19 @@ impl ProxmoxVECloudConfig { Ok(Self { user_data, - meta_data: serde_yaml::from_reader(File::open(path.join("meta-data"))?)?, - vendor_data: serde_yaml::from_reader(File::open(path.join("vendor-data"))?)?, - network_config: serde_yaml::from_reader(File::open(path.join("network-config"))?)?, + meta_data: serde_yaml::from_reader( + File::open(path.join("meta-data")).context("failed to open meta-data file")?, + ) + .context("failed to parse meta-data as YAML")?, + vendor_data: serde_yaml::from_reader( + File::open(path.join("vendor-data")).context("failed to open vendor-data file")?, + ) + .context("failed to parse vendor-data as YAML")?, + network_config: serde_yaml::from_reader( + File::open(path.join("network-config")) + .context("failed to open network-config file")?, + ) + .context("failed to parse network-config as YAML")?, }) } } @@ -183,6 +183,141 @@ impl MetadataProvider for ProxmoxVECloudConfig { Ok(interfaces) } + + fn rd_network_kargs(&self) -> Result> { + let mut kargs = Vec::new(); + + if let Ok(networks) = self.networks() { + for iface in networks { + // Add IP configuration if static + for addr in iface.ip_addresses { + match addr { + IpNetwork::V4(network) => { + if let Some(gateway) = iface + .routes + .iter() + .find(|r| r.destination.is_ipv4() && r.destination.prefix() == 0) + { + kargs.push(format!( + "ip={}::{}:{}", + network.ip(), + gateway.gateway, + network.mask() + )); + } else { + kargs.push(format!("ip={}:::{}", network.ip(), network.mask())); + } + } + IpNetwork::V6(network) => { + if let Some(gateway) = iface + .routes + .iter() + .find(|r| r.destination.is_ipv6() && r.destination.prefix() == 0) + { + kargs.push(format!( + "ip={}::{}:{}", + network.ip(), + gateway.gateway, + network.prefix() + )); + } else { + kargs.push(format!("ip={}:::{}", network.ip(), network.prefix())); + } + } + } + } + + // Add DHCP configuration + if let Some(dhcp) = iface.dhcp { + match dhcp { + DhcpSetting::V4 => kargs.push("ip=dhcp".to_string()), + DhcpSetting::V6 => kargs.push("ip=dhcp6".to_string()), + DhcpSetting::Both => kargs.push("ip=dhcp,dhcp6".to_string()), + } + } + + // Add nameservers + if !iface.nameservers.is_empty() { + let nameservers = iface + .nameservers + .iter() + .map(|ns| ns.to_string()) + .collect::>() + .join(","); + kargs.push(format!("nameserver={}", nameservers)); + } + } + } + + if kargs.is_empty() { + Ok(None) + } else { + Ok(Some(kargs.join(" "))) + } + } + + fn netplan_config(&self) -> Result> { + // Convert network config to netplan format + if let Ok(networks) = self.networks() { + let mut netplan = serde_yaml::Mapping::new(); + let mut network = serde_yaml::Mapping::new(); + let mut ethernets = serde_yaml::Mapping::new(); + + for iface in networks { + let mut eth_config = serde_yaml::Mapping::new(); + + // Add DHCP settings + if let Some(dhcp) = iface.dhcp { + match dhcp { + DhcpSetting::V4 => { + eth_config.insert("dhcp4".into(), true.into()); + } + DhcpSetting::V6 => { + eth_config.insert("dhcp6".into(), true.into()); + } + DhcpSetting::Both => { + eth_config.insert("dhcp4".into(), true.into()); + eth_config.insert("dhcp6".into(), true.into()); + } + } + } + + // Add static addresses if any + if !iface.ip_addresses.is_empty() { + let addresses: Vec = iface + .ip_addresses + .iter() + .map(|addr| addr.to_string()) + .collect(); + eth_config.insert("addresses".into(), addresses.into()); + } + + // Add nameservers if any + if !iface.nameservers.is_empty() { + let nameservers: Vec = + iface.nameservers.iter().map(|ns| ns.to_string()).collect(); + eth_config.insert( + "nameservers".into(), + serde_yaml::Value::Mapping(serde_yaml::Mapping::from_iter(vec![( + "addresses".into(), + nameservers.into(), + )])), + ); + } + + if let Some(name) = iface.name { + ethernets.insert(name.into(), eth_config.into()); + } + } + + network.insert("ethernets".into(), ethernets.into()); + netplan.insert("network".into(), network.into()); + + Ok(Some(serde_yaml::to_string(&netplan)?)) + } else { + Ok(None) + } + } } impl ProxmoxVECloudNetworkConfigEntry { diff --git a/src/providers/proxmoxve/configdrive.rs b/src/providers/proxmoxve/configdrive.rs index 4b31a76d..d45537f4 100644 --- a/src/providers/proxmoxve/configdrive.rs +++ b/src/providers/proxmoxve/configdrive.rs @@ -3,7 +3,7 @@ use crate::{network, providers::MetadataProvider}; use anyhow::{Context, Result}; use openssh_keys::PublicKey; use slog_scope::error; -use std::{collections::HashMap, path::Path}; +use std::{collections::HashMap, path::Path, process::Command}; use tempfile::TempDir; const CONFIG_DRIVE_LABEL: &str = "cidata"; @@ -16,23 +16,34 @@ pub struct ProxmoxVEConfigDrive { } impl ProxmoxVEConfigDrive { + fn find_cidata_device() -> Option { + let output = Command::new("blkid") + .args(["--cache-file", "/dev/null", "-L", CONFIG_DRIVE_LABEL]) + .output() + .ok()?; + + if !output.status.success() { + return None; + } + + Some(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } + pub fn try_new() -> Result { let mount_dir = tempfile::Builder::new() .prefix("afterburn-") .tempdir() .context("failed to create temporary directory")?; - crate::util::mount_ro( - &Path::new("/dev/disk/by-label/").join(CONFIG_DRIVE_LABEL), - mount_dir.path(), - TARGET_FS, - 3, - )?; + let device_path = Self::find_cidata_device() + .ok_or_else(|| anyhow::anyhow!("could not find cidata device"))?; + + crate::util::mount_ro(Path::new(&device_path), mount_dir.path(), TARGET_FS, 3)?; - Ok(Self { - config: ProxmoxVECloudConfig::try_new(mount_dir.path())?, - mount_dir, - }) + let config = ProxmoxVECloudConfig::try_new(mount_dir.path()) + .context("failed to read ProxmoxVE cloud config")?; + + Ok(Self { config, mount_dir }) } } @@ -52,6 +63,14 @@ impl MetadataProvider for ProxmoxVEConfigDrive { fn networks(&self) -> Result> { self.config.networks() } + + fn rd_network_kargs(&self) -> Result> { + self.config.rd_network_kargs() + } + + fn netplan_config(&self) -> Result> { + self.config.netplan_config() + } } impl Drop for ProxmoxVEConfigDrive { diff --git a/src/providers/proxmoxve/mod.rs b/src/providers/proxmoxve/mod.rs index 1ce543fa..1c9043ea 100644 --- a/src/providers/proxmoxve/mod.rs +++ b/src/providers/proxmoxve/mod.rs @@ -29,8 +29,9 @@ mod tests; pub fn try_config_drive_else_leave() -> Result> { match ProxmoxVEConfigDrive::try_new() { Ok(config_drive) => Ok(Box::new(config_drive)), - Err(_) => { - warn!("failed to locate config-drive - aborting ProxmoxVE provider"); + Err(e) => { + warn!("failed to locate config-drive: {}", e); + warn!("aborting ProxmoxVE provider"); Ok(Box::new(NoopProvider::try_new()?)) } } diff --git a/src/providers/proxmoxve/tests.rs b/src/providers/proxmoxve/tests.rs index b4b764e3..d07946a0 100644 --- a/src/providers/proxmoxve/tests.rs +++ b/src/providers/proxmoxve/tests.rs @@ -154,3 +154,128 @@ fn test_invalid_user_data() { assert!(config.hostname().unwrap().is_none()); assert_eq!(config.ssh_keys().unwrap(), vec![]); } + +#[test] +fn test_network_kargs() { + let config = ProxmoxVECloudConfig::try_new(Path::new("tests/fixtures/proxmoxve/static")) + .expect("cannot parse config"); + + let kargs = config.rd_network_kargs().expect("cannot get network kargs"); + assert!(kargs.is_some()); + let kargs = kargs.unwrap(); + + // Check static IP configuration with gateway + assert!(kargs.contains("ip=192.168.1.1::192.168.1.254:255.255.255.0")); + assert!(kargs.contains("ip=2001:db8:85a3::8a2e:370:0::2001:db8:85a3::8a2e:370:9999:24")); + + // Check nameservers + assert!(kargs.contains("nameserver=1.1.1.1,8.8.8.8")); +} + +#[test] +fn test_network_kargs_dhcp() { + let config = ProxmoxVECloudConfig::try_new(Path::new("tests/fixtures/proxmoxve/dhcp")) + .expect("cannot parse config"); + + let kargs = config.rd_network_kargs().expect("cannot get network kargs"); + assert!(kargs.is_some()); + let kargs = kargs.unwrap(); + + // Check DHCP configuration + assert!(kargs.contains("ip=dhcp")); + + // Check nameservers + assert!(kargs.contains("nameserver=1.1.1.1,8.8.8.8")); +} + +#[test] +fn test_network_kargs_no_gateway() { + let config = + ProxmoxVECloudConfig::try_new(Path::new("tests/fixtures/proxmoxve/static-no-gateway")) + .expect("cannot parse config"); + + let kargs = config.rd_network_kargs().expect("cannot get network kargs"); + assert!(kargs.is_some()); + let kargs = kargs.unwrap(); + + // Check static IP configuration without gateway + assert!(kargs.contains("ip=192.168.1.1:::255.255.255.0")); + + // Check nameservers + assert!(kargs.contains("nameserver=1.1.1.1,8.8.8.8")); +} + +#[test] +fn test_netplan_config_static() { + let config = ProxmoxVECloudConfig::try_new(Path::new("tests/fixtures/proxmoxve/static")) + .expect("cannot parse config"); + + let netplan = config.netplan_config().expect("cannot get netplan config"); + assert!(netplan.is_some()); + let netplan = netplan.unwrap(); + + // Parse the YAML to verify its structure + let parsed: serde_yaml::Value = serde_yaml::from_str(&netplan).expect("invalid YAML"); + + // Check network configuration + let network = &parsed["network"]; + assert!(network.is_mapping()); + + // Check ethernet interfaces + let ethernets = &network["ethernets"]; + assert!(ethernets.is_mapping()); + + // Check eth0 configuration + let eth0 = ðernets["eth0"]; + assert!(eth0.is_mapping()); + + // Verify static addresses + let addresses = eth0["addresses"].as_sequence().unwrap(); + assert!(addresses.contains(&serde_yaml::Value::String("192.168.1.1/24".into()))); + assert!(addresses.contains(&serde_yaml::Value::String( + "2001:db8:85a3::8a2e:370:0/24".into() + ))); + + // Verify nameservers + let nameservers = ð0["nameservers"]["addresses"]; + assert!(nameservers + .as_sequence() + .unwrap() + .contains(&serde_yaml::Value::String("1.1.1.1".into()))); + assert!(nameservers + .as_sequence() + .unwrap() + .contains(&serde_yaml::Value::String("8.8.8.8".into()))); +} + +#[test] +fn test_netplan_config_dhcp() { + let config = ProxmoxVECloudConfig::try_new(Path::new("tests/fixtures/proxmoxve/dhcp")) + .expect("cannot parse config"); + + let netplan = config.netplan_config().expect("cannot get netplan config"); + assert!(netplan.is_some()); + let netplan = netplan.unwrap(); + + // Parse the YAML to verify its structure + let parsed: serde_yaml::Value = serde_yaml::from_str(&netplan).expect("invalid YAML"); + + // Check network configuration + let network = &parsed["network"]; + let ethernets = &network["ethernets"]; + let eth0 = ðernets["eth0"]; + + // Verify DHCP configuration + assert_eq!(eth0["dhcp4"], serde_yaml::Value::Bool(true)); + + // Verify nameservers + let nameservers = ð0["nameservers"]["addresses"]; + assert!(nameservers + .as_sequence() + .unwrap() + .contains(&serde_yaml::Value::String("1.1.1.1".into()))); + assert!(nameservers + .as_sequence() + .unwrap() + .contains(&serde_yaml::Value::String("8.8.8.8".into()))); +} diff --git a/tests/fixtures/proxmoxve/static-no-gateway/meta-data b/tests/fixtures/proxmoxve/static-no-gateway/meta-data new file mode 100644 index 00000000..bd5926b3 --- /dev/null +++ b/tests/fixtures/proxmoxve/static-no-gateway/meta-data @@ -0,0 +1 @@ +instance-id: 15a9919cb91024fbd1d70fa07f0efa749cbba03b diff --git a/tests/fixtures/proxmoxve/static-no-gateway/network-config b/tests/fixtures/proxmoxve/static-no-gateway/network-config new file mode 100644 index 00000000..dfef8824 --- /dev/null +++ b/tests/fixtures/proxmoxve/static-no-gateway/network-config @@ -0,0 +1,13 @@ +version: 1 +config: + - type: physical + name: eth0 + mac_address: "01:23:45:67:89:00" + subnets: + - type: static + address: "192.168.1.1" + netmask: "255.255.255.0" + - type: nameserver + address: + - "1.1.1.1" + - "8.8.8.8" diff --git a/tests/fixtures/proxmoxve/static-no-gateway/user-data b/tests/fixtures/proxmoxve/static-no-gateway/user-data new file mode 100644 index 00000000..e62a3e6a --- /dev/null +++ b/tests/fixtures/proxmoxve/static-no-gateway/user-data @@ -0,0 +1,13 @@ +#cloud-config +hostname: dummy +manage_etc_hosts: true +fqdn: dummy.local.com +user: dummy-user +password: $5$6LDowW6p$.RyFu8lVH7Cw3AB.pPS/K2lmB8IczVs99A7gbcUCLV2 +ssh_authorized_keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDd1hElre4j44sbmULXyO5j6dRnkRFCMjEGtRSy2SuvFD8WyB5uectcEMvz7ORhQIVbPlz94wFjpSX5wl/gmSKL/7GOyerJo0Y2cvyjJJahuDn+JnIL0tT0HS1pJ5iJqQpxXeOAzMK5Heum+uGw9BzbiUHnRzjJr8Ltx4CAGMfubevD4SX32Q8BTQiaU4ZnGtdHo16pWwRsq1f6/UtL4gDCni9vm8QmmGDRloi/pBn1csjKw+volFyu/kSEmGLWow6NuT6TrhGAbMKas5HfYq0Mn3LGPZL7XjqJQ6CO0TzkG/BNplZT2tiwHtsvXsbePTp4ZUi4dkCMz2xR4eikaI1V dummy@dummy.local +chpasswd: + expire: False +users: + - default +package_upgrade: true diff --git a/tests/fixtures/proxmoxve/static-no-gateway/vendor-data b/tests/fixtures/proxmoxve/static-no-gateway/vendor-data new file mode 100644 index 00000000..e69de29b