diff --git a/docs/platforms.md b/docs/platforms.md index d86a4219..72d6700e 100644 --- a/docs/platforms.md +++ b/docs/platforms.md @@ -64,6 +64,11 @@ The following platforms are supported, with a different set of features availabl * powervs - Attributes - SSH keys +* proxmoxve + - Attributes + - Hostname + - SSH keys + - Network configuration * scaleway - Attributes - Boot check-in diff --git a/docs/release-notes.md b/docs/release-notes.md index e5f4e366..43e720d5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,8 @@ nav_order: 8 Major changes: +- Add support for Proxmox VE + Minor changes: Packaging changes: diff --git a/docs/usage/attributes.md b/docs/usage/attributes.md index 8baaab4f..42e5a41a 100644 --- a/docs/usage/attributes.md +++ b/docs/usage/attributes.md @@ -135,6 +135,11 @@ Cloud providers with supported metadata endpoints and their respective attribute * powervs - AFTERBURN_POWERVS_INSTANCE_ID - AFTERBURN_POWERVS_LOCAL_HOSTNAME +* proxmoxve + - AFTERBURN_PROXMOXVE_HOSTNAME + - AFTERBURN_PROXMOXVE_INSTANCE_ID + - AFTERBURN_PROXMOXVE_IPV4 + - AFTERBURN_PROXMOXVE_IPV6 * scaleway - AFTERBURN_SCALEWAY_HOSTNAME - AFTERBURN_SCALEWAY_INSTANCE_ID diff --git a/dracut/30afterburn/afterburn-hostname.service b/dracut/30afterburn/afterburn-hostname.service index 485cd82a..268522bd 100644 --- a/dracut/30afterburn/afterburn-hostname.service +++ b/dracut/30afterburn/afterburn-hostname.service @@ -12,6 +12,7 @@ ConditionKernelCommandLine=|ignition.platform.id=exoscale ConditionKernelCommandLine=|ignition.platform.id=hetzner ConditionKernelCommandLine=|ignition.platform.id=ibmcloud ConditionKernelCommandLine=|ignition.platform.id=kubevirt +ConditionKernelCommandLine=|ignition.platform.id=proxmoxve ConditionKernelCommandLine=|ignition.platform.id=scaleway ConditionKernelCommandLine=|ignition.platform.id=vultr diff --git a/src/metadata.rs b/src/metadata.rs index b89bf5b3..94f9238b 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -33,6 +33,7 @@ use crate::providers::openstack; use crate::providers::openstack::network::OpenstackProviderNetwork; use crate::providers::packet::PacketProvider; use crate::providers::powervs::PowerVSProvider; +use crate::providers::proxmoxve::ProxmoxVEConfigDrive; use crate::providers::scaleway::ScalewayProvider; use crate::providers::vmware::VmwareProvider; use crate::providers::vultr::VultrProvider; @@ -70,6 +71,7 @@ pub fn fetch_metadata(provider: &str) -> Result box_result!(OpenstackProviderNetwork::try_new()?), "packet" => box_result!(PacketProvider::try_new()?), "powervs" => box_result!(PowerVSProvider::try_new()?), + "proxmoxve" => box_result!(ProxmoxVEConfigDrive::try_new()?), "scaleway" => box_result!(ScalewayProvider::try_new()?), "vmware" => box_result!(VmwareProvider::try_new()?), "vultr" => box_result!(VultrProvider::try_new()?), diff --git a/src/network.rs b/src/network.rs index 77957352..26c0a6fb 100644 --- a/src/network.rs +++ b/src/network.rs @@ -79,6 +79,8 @@ pub struct Interface { pub priority: u8, pub nameservers: Vec, pub ip_addresses: Vec, + // Optionally enable DHCP + pub dhcp: Option, pub routes: Vec, pub bond: Option, pub unmanaged: bool, @@ -128,6 +130,31 @@ impl NetDevKind { } } +/// Optional use of DHCP. +#[allow(dead_code)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DhcpSetting { + Both, + V4, + V6, +} + +impl DhcpSetting { + /// Return DHCP setting according to `systemd.network` + /// + /// See [systemd documentation](dhcp) for the full list. + /// + /// dhcp: https://www.freedesktop.org/software/systemd/man/latest/systemd.network.html#DHCP= + fn sd_dhcp_setting(&self) -> String { + let setting = match *self { + DhcpSetting::Both => "yes", + DhcpSetting::V4 => "ipv4", + DhcpSetting::V6 => "ipv6", + }; + setting.to_string() + } +} + impl Interface { /// Return a deterministic `systemd.network` unit name for this device. pub fn sd_network_unit_name(&self) -> Result { @@ -158,6 +185,9 @@ impl Interface { // [Network] section writeln!(config, "\n[Network]").unwrap(); + if let Some(dhcp) = &self.dhcp { + writeln!(config, "DHCP={}", dhcp.sd_dhcp_setting()).unwrap(); + } for ns in &self.nameservers { writeln!(config, "DNS={ns}").unwrap() } @@ -246,6 +276,7 @@ mod tests { priority: 20, nameservers: vec![], ip_addresses: vec![], + dhcp: None, routes: vec![], bond: None, unmanaged: false, @@ -261,6 +292,7 @@ mod tests { priority: 10, nameservers: vec![], ip_addresses: vec![], + dhcp: None, routes: vec![], bond: None, unmanaged: false, @@ -276,6 +308,7 @@ mod tests { priority: 20, nameservers: vec![], ip_addresses: vec![], + dhcp: None, routes: vec![], bond: None, unmanaged: false, @@ -291,6 +324,7 @@ mod tests { priority: 20, nameservers: vec![], ip_addresses: vec![], + dhcp: None, routes: vec![], bond: None, unmanaged: false, @@ -306,6 +340,7 @@ mod tests { priority: 20, nameservers: vec![], ip_addresses: vec![], + dhcp: None, routes: vec![], bond: None, unmanaged: false, @@ -330,6 +365,7 @@ mod tests { priority: 20, nameservers: vec![], ip_addresses: vec![], + dhcp: None, routes: vec![], bond: None, unmanaged: false, @@ -387,6 +423,7 @@ mod tests { Ipv6Network::new(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1), 128).unwrap(), ), ], + dhcp: None, routes: vec![NetworkRoute { destination: IpNetwork::V4( Ipv4Network::new(Ipv4Addr::new(127, 0, 0, 1), 8).unwrap(), @@ -428,6 +465,7 @@ Gateway=127.0.0.1 priority: 10, nameservers: vec![], ip_addresses: vec![], + dhcp: None, routes: vec![], bond: None, unmanaged: false, @@ -447,6 +485,7 @@ Gateway=127.0.0.1 priority: 10, nameservers: vec![], ip_addresses: vec![], + dhcp: None, routes: vec![], bond: None, unmanaged: false, @@ -470,6 +509,7 @@ RequiredForOnline=no priority: 10, nameservers: vec![], ip_addresses: vec![], + dhcp: None, routes: vec![], bond: None, unmanaged: true, @@ -482,6 +522,28 @@ Name=* [Link] Unmanaged=yes +", + ), + // test the DHCP setting + ( + Interface { + name: Some("*".to_owned()), + mac_address: None, + path: None, + priority: 10, + nameservers: vec![], + ip_addresses: vec![], + dhcp: Some(DhcpSetting::V4), + routes: vec![], + bond: None, + unmanaged: false, + required_for_online: None, + }, + "[Match] +Name=* + +[Network] +DHCP=ipv4 ", ), ]; diff --git a/src/providers/digitalocean/mod.rs b/src/providers/digitalocean/mod.rs index 25836967..dd817dc4 100644 --- a/src/providers/digitalocean/mod.rs +++ b/src/providers/digitalocean/mod.rs @@ -156,6 +156,7 @@ impl DigitalOceanProvider { mac_address: Some(mac), nameservers: self.dns.nameservers.clone(), ip_addresses: addrs, + dhcp: None, routes, bond: None, name: None, diff --git a/src/providers/ibmcloud_classic/mod.rs b/src/providers/ibmcloud_classic/mod.rs index 7c31707d..88c5483e 100644 --- a/src/providers/ibmcloud_classic/mod.rs +++ b/src/providers/ibmcloud_classic/mod.rs @@ -251,6 +251,7 @@ impl IBMClassicProvider { priority: 10, nameservers: nameservers.clone(), ip_addresses: vec![ip_net], + dhcp: None, routes, bond: None, unmanaged: false, diff --git a/src/providers/mod.rs b/src/providers/mod.rs index ab9698a7..e17d5519 100644 --- a/src/providers/mod.rs +++ b/src/providers/mod.rs @@ -38,6 +38,7 @@ pub mod microsoft; pub mod openstack; pub mod packet; pub mod powervs; +pub mod proxmoxve; pub mod scaleway; pub mod vmware; pub mod vultr; diff --git a/src/providers/packet/mod.rs b/src/providers/packet/mod.rs index 314c6420..4a8ec351 100644 --- a/src/providers/packet/mod.rs +++ b/src/providers/packet/mod.rs @@ -216,6 +216,7 @@ impl PacketProvider { priority: 10, nameservers: Vec::new(), ip_addresses: Vec::new(), + dhcp: None, routes: Vec::new(), // the interface should be unmanaged if it doesn't have a bond // section @@ -241,6 +242,7 @@ impl PacketProvider { path: None, bond: None, ip_addresses: Vec::new(), + dhcp: None, routes: Vec::new(), unmanaged: false, required_for_online: Some("degraded-carrier".to_owned()), @@ -334,6 +336,7 @@ impl PacketProvider { bond: None, nameservers: Vec::new(), ip_addresses: Vec::new(), + dhcp: None, routes: Vec::new(), required_for_online: None, }; diff --git a/src/providers/proxmoxve/cloudconfig.rs b/src/providers/proxmoxve/cloudconfig.rs new file mode 100644 index 00000000..6be26763 --- /dev/null +++ b/src/providers/proxmoxve/cloudconfig.rs @@ -0,0 +1,275 @@ +use crate::{ + network::{self, DhcpSetting, NetworkRoute}, + providers::MetadataProvider, +}; +use anyhow::Result; +use ipnetwork::IpNetwork; +use openssh_keys::PublicKey; +use pnet_base::MacAddr; +use serde::Deserialize; +use slog_scope::warn; +use std::{ + collections::HashMap, + fs::File, + net::{AddrParseError, IpAddr}, + path::Path, + str::FromStr, +}; + +#[derive(Debug)] +pub struct ProxmoxVECloudConfig { + pub meta_data: ProxmoxVECloudMetaData, + pub user_data: Option, + pub vendor_data: ProxmoxVECloudVendorData, + pub network_config: ProxmoxVECloudNetworkConfig, +} + +#[derive(Debug, Deserialize)] +pub struct ProxmoxVECloudMetaData { + #[serde(rename = "instance-id")] + pub instance_id: String, +} + +#[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, +} + +#[derive(Debug, Deserialize)] +pub struct ProxmoxVECloudNetworkConfigEntry { + #[serde(rename = "type")] + pub network_type: String, + pub name: Option, + pub mac_address: Option, + #[serde(default)] + pub address: Vec, + #[serde(default)] + pub search: Vec, + #[serde(default)] + pub subnets: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct ProxmoxVECloudNetworkConfigSubnet { + #[serde(rename = "type")] + pub subnet_type: String, + pub address: Option, + pub netmask: Option, + pub gateway: Option, +} + +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"))?; + + 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)?; + } + } + + if user_data.is_none() { + warn!( + "user-data does not have the expected header `#cloud-config`, ignoring this file" + ); + } + + 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"))?)?, + }) + } +} + +impl MetadataProvider for ProxmoxVECloudConfig { + fn attributes(&self) -> Result> { + let mut out = HashMap::new(); + + out.insert( + "PROXMOXVE_INSTANCE_ID".to_owned(), + self.meta_data.instance_id.clone(), + ); + + if let Some(hostname) = self.hostname()? { + out.insert("PROXMOXVE_HOSTNAME".to_owned(), hostname); + } + + if let Some(first_interface) = self.networks()?.first() { + first_interface.ip_addresses.iter().for_each(|ip| match ip { + IpNetwork::V4(network) => { + out.insert("PROXMOXVE_IPV4".to_owned(), network.ip().to_string()); + } + IpNetwork::V6(network) => { + out.insert("PROXMOXVE_IPV6".to_owned(), network.ip().to_string()); + } + }); + } + + Ok(out) + } + + fn hostname(&self) -> Result> { + Ok(self + .user_data + .as_ref() + .map(|user_data| user_data.hostname.clone())) + } + + fn ssh_keys(&self) -> Result> { + if let Some(user_data) = &self.user_data { + return Ok(user_data + .ssh_authorized_keys + .iter() + .map(|key| PublicKey::from_str(key)) + .collect::, _>>()?); + } + + Ok(vec![]) + } + + fn networks(&self) -> Result> { + let nameservers = self + .network_config + .config + .iter() + .filter(|config| config.network_type == "nameserver") + .collect::>(); + + if nameservers.len() > 1 { + return Err(anyhow::anyhow!("too many nameservers, only one supported")); + } + + let mut interfaces = self + .network_config + .config + .iter() + .filter(|config| config.network_type == "physical") + .map(|entry| entry.to_interface()) + .collect::, _>>()?; + + if let Some(iface) = interfaces.first_mut() { + if let Some(nameserver) = nameservers.first() { + iface.nameservers = nameserver + .address + .iter() + .map(|ip| IpAddr::from_str(ip)) + .collect::, AddrParseError>>()?; + } + } + + Ok(interfaces) + } +} + +impl ProxmoxVECloudNetworkConfigEntry { + pub fn to_interface(&self) -> Result { + if self.network_type != "physical" { + return Err(anyhow::anyhow!( + "cannot convert config to interface: unsupported config type \"{}\"", + self.network_type + )); + } + + let mut iface = network::Interface { + name: self.name.clone(), + + // filled later + nameservers: vec![], + // filled below + ip_addresses: vec![], + // filled below + routes: vec![], + // filled below + dhcp: None, + // filled below because Option::try_map doesn't exist yet + mac_address: None, + + // unsupported by proxmox ve + bond: None, + + // default values + path: None, + priority: 20, + unmanaged: false, + required_for_online: None, + }; + + for subnet in &self.subnets { + if subnet.subnet_type.contains("static") { + if subnet.address.is_none() { + return Err(anyhow::anyhow!( + "cannot convert static subnet to interface: missing address" + )); + } + + if let Some(netmask) = &subnet.netmask { + iface.ip_addresses.push(IpNetwork::with_netmask( + IpAddr::from_str(subnet.address.as_ref().unwrap())?, + IpAddr::from_str(netmask)?, + )?); + } else { + iface + .ip_addresses + .push(IpNetwork::from_str(subnet.address.as_ref().unwrap())?); + } + + if let Some(gateway) = &subnet.gateway { + let gateway = IpAddr::from_str(gateway)?; + + let destination = if gateway.is_ipv6() { + IpNetwork::from_str("::/0")? + } else { + IpNetwork::from_str("0.0.0.0/0")? + }; + + iface.routes.push(NetworkRoute { + destination, + gateway, + }); + } else { + warn!("found subnet type \"static\" without gateway"); + } + } + + if subnet.subnet_type == "dhcp" || subnet.subnet_type == "dhcp4" { + iface.dhcp = Some(DhcpSetting::V4) + } + if subnet.subnet_type == "dhcp6" { + iface.dhcp = Some(DhcpSetting::V6) + } + if subnet.subnet_type == "ipv6_slaac" { + warn!("subnet type \"ipv6_slaac\" not supported, ignoring"); + } + } + + if let Some(mac) = &self.mac_address { + iface.mac_address = Some(MacAddr::from_str(mac)?); + } + + Ok(iface) + } +} diff --git a/src/providers/proxmoxve/configdrive.rs b/src/providers/proxmoxve/configdrive.rs new file mode 100644 index 00000000..4b31a76d --- /dev/null +++ b/src/providers/proxmoxve/configdrive.rs @@ -0,0 +1,63 @@ +use super::ProxmoxVECloudConfig; +use crate::{network, providers::MetadataProvider}; +use anyhow::{Context, Result}; +use openssh_keys::PublicKey; +use slog_scope::error; +use std::{collections::HashMap, path::Path}; +use tempfile::TempDir; + +const CONFIG_DRIVE_LABEL: &str = "cidata"; +const TARGET_FS: &str = "iso9660"; + +#[derive(Debug)] +pub struct ProxmoxVEConfigDrive { + mount_dir: TempDir, + config: ProxmoxVECloudConfig, +} + +impl ProxmoxVEConfigDrive { + 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, + )?; + + Ok(Self { + config: ProxmoxVECloudConfig::try_new(mount_dir.path())?, + mount_dir, + }) + } +} + +impl MetadataProvider for ProxmoxVEConfigDrive { + fn attributes(&self) -> Result> { + self.config.attributes() + } + + fn hostname(&self) -> Result> { + self.config.hostname() + } + + fn ssh_keys(&self) -> Result> { + self.config.ssh_keys() + } + + fn networks(&self) -> Result> { + self.config.networks() + } +} + +impl Drop for ProxmoxVEConfigDrive { + fn drop(&mut self) { + if let Err(e) = crate::util::unmount(self.mount_dir.path(), 3) { + error!("failed to cleanup Proxmox VE config-drive: {:?}", e); + }; + } +} diff --git a/src/providers/proxmoxve/mod.rs b/src/providers/proxmoxve/mod.rs new file mode 100644 index 00000000..14146b0e --- /dev/null +++ b/src/providers/proxmoxve/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2017 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod configdrive; +pub use configdrive::*; + +mod cloudconfig; +pub use cloudconfig::*; + +#[cfg(test)] +mod tests; diff --git a/src/providers/proxmoxve/tests.rs b/src/providers/proxmoxve/tests.rs new file mode 100644 index 00000000..b4b764e3 --- /dev/null +++ b/src/providers/proxmoxve/tests.rs @@ -0,0 +1,156 @@ +use super::ProxmoxVECloudConfig; +use crate::{ + network::{self, DhcpSetting, NetworkRoute}, + providers::MetadataProvider, +}; +use ipnetwork::IpNetwork; +use openssh_keys::PublicKey; +use pnet_base::MacAddr; +use std::{net::IpAddr, path::Path, str::FromStr}; + +#[test] +fn test_attributes() { + let config = ProxmoxVECloudConfig::try_new(Path::new("tests/fixtures/proxmoxve/static")) + .expect("cannot parse config"); + let attributes = config.attributes().expect("cannot get hostname"); + + assert_eq!(attributes["PROXMOXVE_HOSTNAME"], "dummy".to_string()); + + assert_eq!( + attributes["PROXMOXVE_INSTANCE_ID"], + "15a9919cb91024fbd1d70fa07f0efa749cbba03b".to_string() + ); + + assert_eq!(attributes["PROXMOXVE_IPV4"], "192.168.1.1".to_string()); + + assert_eq!( + attributes["PROXMOXVE_IPV6"], + "2001:db8:85a3::8a2e:370:0".to_string() + ); +} + +#[test] +fn test_hostname() { + let config = ProxmoxVECloudConfig::try_new(Path::new("tests/fixtures/proxmoxve/dhcp")) + .expect("cannot parse config"); + + assert_eq!( + config.hostname().expect("cannot get hostname"), + Some("dummy".to_string()) + ); +} + +#[test] +fn test_ssh_keys() { + let test_ssh_key = PublicKey::from_str("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDd1hElre4j44sbmULXyO5j6dRnkRFCMjEGtRSy2SuvFD8WyB5uectcEMvz7ORhQIVbPlz94wFjpSX5wl/gmSKL/7GOyerJo0Y2cvyjJJahuDn+JnIL0tT0HS1pJ5iJqQpxXeOAzMK5Heum+uGw9BzbiUHnRzjJr8Ltx4CAGMfubevD4SX32Q8BTQiaU4ZnGtdHo16pWwRsq1f6/UtL4gDCni9vm8QmmGDRloi/pBn1csjKw+volFyu/kSEmGLWow6NuT6TrhGAbMKas5HfYq0Mn3LGPZL7XjqJQ6CO0TzkG/BNplZT2tiwHtsvXsbePTp4ZUi4dkCMz2xR4eikaI1V dummy@dummy.local").unwrap(); + let config = ProxmoxVECloudConfig::try_new(Path::new("tests/fixtures/proxmoxve/dhcp")) + .expect("cannot parse config"); + + assert_eq!( + config.ssh_keys().expect("cannot get ssh keys"), + vec![test_ssh_key] + ); +} + +#[test] +fn test_network_dhcp() { + let config = ProxmoxVECloudConfig::try_new(Path::new("tests/fixtures/proxmoxve/dhcp")) + .expect("cannot parse config"); + + assert_eq!( + config.networks().expect("cannot get networks"), + vec![network::Interface { + name: Some("eth0".to_owned()), + mac_address: Some(MacAddr::from_str("01:23:45:67:89:00").unwrap()), + path: None, + priority: 20, + dhcp: Some(DhcpSetting::V4), + nameservers: vec![ + IpAddr::from_str("1.1.1.1").unwrap(), + IpAddr::from_str("8.8.8.8").unwrap() + ], + ip_addresses: vec![], + routes: vec![], + bond: None, + unmanaged: false, + required_for_online: None + }] + ); +} + +#[test] +fn test_network_static() { + let config = ProxmoxVECloudConfig::try_new(Path::new("tests/fixtures/proxmoxve/static")) + .expect("cannot parse config"); + + assert_eq!( + config.networks().expect("cannot get networks"), + vec![ + network::Interface { + name: Some("eth0".to_owned()), + mac_address: Some(MacAddr::from_str("01:23:45:67:89:00").unwrap()), + path: None, + priority: 20, + nameservers: vec![ + IpAddr::from_str("1.1.1.1").unwrap(), + IpAddr::from_str("8.8.8.8").unwrap() + ], + ip_addresses: vec![ + IpNetwork::from_str("192.168.1.1/24").unwrap(), + IpNetwork::from_str("2001:0db8:85a3:0000:0000:8a2e:0370:0/24").unwrap(), + ], + dhcp: None, + routes: vec![ + NetworkRoute { + destination: IpNetwork::from_str("0.0.0.0/0").unwrap(), + gateway: IpAddr::from_str("192.168.1.254").unwrap(), + }, + NetworkRoute { + destination: IpNetwork::from_str("::/0").unwrap(), + gateway: IpAddr::from_str("2001:0db8:85a3:0000:0000:8a2e:0370:9999") + .unwrap(), + }, + ], + bond: None, + unmanaged: false, + required_for_online: None + }, + network::Interface { + name: Some("eth1".to_owned()), + mac_address: Some(MacAddr::from_str("01:23:45:67:89:99").unwrap()), + path: None, + priority: 20, + nameservers: vec![], + ip_addresses: vec![ + IpNetwork::from_str("192.168.42.1/24").unwrap(), + IpNetwork::from_str("2001:0db8:85a3:0000:0000:8a2e:4242:0/24").unwrap(), + ], + dhcp: None, + routes: vec![ + NetworkRoute { + destination: IpNetwork::from_str("0.0.0.0/0").unwrap(), + gateway: IpAddr::from_str("192.168.42.254").unwrap(), + }, + NetworkRoute { + destination: IpNetwork::from_str("::/0").unwrap(), + gateway: IpAddr::from_str("2001:0db8:85a3:0000:0000:8a2e:4242:9999") + .unwrap(), + }, + ], + bond: None, + unmanaged: false, + required_for_online: None + }, + ] + ); +} + +#[test] +fn test_invalid_user_data() { + let config = + ProxmoxVECloudConfig::try_new(Path::new("tests/fixtures/proxmoxve/invalid-user-data")) + .expect("cannot parse config"); + + assert!(config.hostname().unwrap().is_none()); + assert_eq!(config.ssh_keys().unwrap(), vec![]); +} diff --git a/systemd/afterburn-sshkeys@.service.in b/systemd/afterburn-sshkeys@.service.in index 5709131e..911a57e6 100644 --- a/systemd/afterburn-sshkeys@.service.in +++ b/systemd/afterburn-sshkeys@.service.in @@ -16,6 +16,7 @@ ConditionKernelCommandLine=|ignition.platform.id=gcp ConditionKernelCommandLine=|ignition.platform.id=hetzner ConditionKernelCommandLine=|ignition.platform.id=ibmcloud ConditionKernelCommandLine=|ignition.platform.id=openstack +ConditionKernelCommandLine=|ignition.platform.id=proxmoxve ConditionKernelCommandLine=|ignition.platform.id=scaleway ConditionKernelCommandLine=|ignition.platform.id=packet ConditionKernelCommandLine=|ignition.platform.id=powervs diff --git a/tests/fixtures/proxmoxve/dhcp/meta-data b/tests/fixtures/proxmoxve/dhcp/meta-data new file mode 100644 index 00000000..bd5926b3 --- /dev/null +++ b/tests/fixtures/proxmoxve/dhcp/meta-data @@ -0,0 +1 @@ +instance-id: 15a9919cb91024fbd1d70fa07f0efa749cbba03b diff --git a/tests/fixtures/proxmoxve/dhcp/network-config b/tests/fixtures/proxmoxve/dhcp/network-config new file mode 100644 index 00000000..4031d025 --- /dev/null +++ b/tests/fixtures/proxmoxve/dhcp/network-config @@ -0,0 +1,13 @@ +version: 1 +config: + - type: physical + name: eth0 + mac_address: '01:23:45:67:89:00' + subnets: + - type: dhcp4 + - type: nameserver + address: + - '1.1.1.1' + - '8.8.8.8' + search: + - 'local.com' diff --git a/tests/fixtures/proxmoxve/dhcp/user-data b/tests/fixtures/proxmoxve/dhcp/user-data new file mode 100644 index 00000000..e62a3e6a --- /dev/null +++ b/tests/fixtures/proxmoxve/dhcp/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/dhcp/vendor-data b/tests/fixtures/proxmoxve/dhcp/vendor-data new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/proxmoxve/invalid-user-data/meta-data b/tests/fixtures/proxmoxve/invalid-user-data/meta-data new file mode 100644 index 00000000..bd5926b3 --- /dev/null +++ b/tests/fixtures/proxmoxve/invalid-user-data/meta-data @@ -0,0 +1 @@ +instance-id: 15a9919cb91024fbd1d70fa07f0efa749cbba03b diff --git a/tests/fixtures/proxmoxve/invalid-user-data/network-config b/tests/fixtures/proxmoxve/invalid-user-data/network-config new file mode 100644 index 00000000..e708ca1d --- /dev/null +++ b/tests/fixtures/proxmoxve/invalid-user-data/network-config @@ -0,0 +1,30 @@ +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' + gateway: '192.168.1.254' + - type: static6 + address: '2001:0db8:85a3:0000:0000:8a2e:0370:0/24' + gateway: '2001:0db8:85a3:0000:0000:8a2e:0370:9999' + - type: physical + name: eth1 + mac_address: '01:23:45:67:89:99' + subnets: + - type: static + address: '192.168.42.1' + netmask: '255.255.255.0' + gateway: '192.168.42.254' + - type: static6 + address: '2001:0db8:85a3:0000:0000:8a2e:4242:0/24' + gateway: '2001:0db8:85a3:0000:0000:8a2e:4242:9999' + - type: nameserver + address: + - '1.1.1.1' + - '8.8.8.8' + search: + - 'local.com' diff --git a/tests/fixtures/proxmoxve/invalid-user-data/user-data b/tests/fixtures/proxmoxve/invalid-user-data/user-data new file mode 100644 index 00000000..c1baeda3 --- /dev/null +++ b/tests/fixtures/proxmoxve/invalid-user-data/user-data @@ -0,0 +1,5 @@ +{ + "some": { + "ignition": "config" + } +} diff --git a/tests/fixtures/proxmoxve/invalid-user-data/vendor-data b/tests/fixtures/proxmoxve/invalid-user-data/vendor-data new file mode 100644 index 00000000..e69de29b diff --git a/tests/fixtures/proxmoxve/static/meta-data b/tests/fixtures/proxmoxve/static/meta-data new file mode 100644 index 00000000..bd5926b3 --- /dev/null +++ b/tests/fixtures/proxmoxve/static/meta-data @@ -0,0 +1 @@ +instance-id: 15a9919cb91024fbd1d70fa07f0efa749cbba03b diff --git a/tests/fixtures/proxmoxve/static/network-config b/tests/fixtures/proxmoxve/static/network-config new file mode 100644 index 00000000..e708ca1d --- /dev/null +++ b/tests/fixtures/proxmoxve/static/network-config @@ -0,0 +1,30 @@ +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' + gateway: '192.168.1.254' + - type: static6 + address: '2001:0db8:85a3:0000:0000:8a2e:0370:0/24' + gateway: '2001:0db8:85a3:0000:0000:8a2e:0370:9999' + - type: physical + name: eth1 + mac_address: '01:23:45:67:89:99' + subnets: + - type: static + address: '192.168.42.1' + netmask: '255.255.255.0' + gateway: '192.168.42.254' + - type: static6 + address: '2001:0db8:85a3:0000:0000:8a2e:4242:0/24' + gateway: '2001:0db8:85a3:0000:0000:8a2e:4242:9999' + - type: nameserver + address: + - '1.1.1.1' + - '8.8.8.8' + search: + - 'local.com' diff --git a/tests/fixtures/proxmoxve/static/user-data b/tests/fixtures/proxmoxve/static/user-data new file mode 100644 index 00000000..e62a3e6a --- /dev/null +++ b/tests/fixtures/proxmoxve/static/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/vendor-data b/tests/fixtures/proxmoxve/static/vendor-data new file mode 100644 index 00000000..e69de29b