diff --git a/sdk_container/src/third_party/coreos-overlay/coreos/user-patches/coreos-base/afterburn/1266.patch b/sdk_container/src/third_party/coreos-overlay/coreos/user-patches/coreos-base/afterburn/1266.patch new file mode 100644 index 00000000000..ef6310f3d4a --- /dev/null +++ b/sdk_container/src/third_party/coreos-overlay/coreos/user-patches/coreos-base/afterburn/1266.patch @@ -0,0 +1,835 @@ +From f50ac8cc7d836920131054080e88d8a7c640c5ab Mon Sep 17 00:00:00 2001 +From: Rolv-Apneseth +Date: Fri, 6 Mar 2026 15:17:48 +0000 +Subject: [PATCH 1/2] providers/hetzner: Add support for network configuration + +This should enable the generation of systemd-networkd unit and netplan +configuration files for Hetzner systems. Note that a network connection +is required to request network metadata (no alternative on Hetzner as +far as I can tell). +--- + src/network.rs | 11 + + src/providers/hetzner/mock_tests.rs | 110 ++++++- + src/providers/hetzner/mod.rs | 301 +++++++++++++++++-- + tests/fixtures/hetzner/metadata.yaml | 24 ++ + tests/fixtures/hetzner/netplan-config.yaml | 14 + + tests/fixtures/hetzner/network-config.yaml | 15 + + tests/fixtures/hetzner/private-networks.yaml | 18 ++ + 9 files changed, 461 insertions(+), 34 deletions(-) + create mode 100644 tests/fixtures/hetzner/metadata.yaml + create mode 100644 tests/fixtures/hetzner/netplan-config.yaml + create mode 100644 tests/fixtures/hetzner/network-config.yaml + create mode 100644 tests/fixtures/hetzner/private-networks.yaml + +diff --git a/src/network.rs b/src/network.rs +index 4e125104..2a80e401 100644 +--- a/src/network.rs ++++ b/src/network.rs +@@ -171,6 +171,17 @@ impl DhcpSetting { + }; + setting.to_string() + } ++ ++ /// Combine DHCP settings, to easily merge [`Self::V4`] and [`Self::V6`] variants into ++ /// [`Self::Both`] where applicable. ++ pub fn merge(self, other: Self) -> Self { ++ use DhcpSetting::*; ++ match (self, other) { ++ (V4, V4) => V4, ++ (V6, V6) => V6, ++ _ => Both, ++ } ++ } + } + + impl Interface { +diff --git a/src/providers/hetzner/mock_tests.rs b/src/providers/hetzner/mock_tests.rs +index 443c7027..cf088373 100644 +--- a/src/providers/hetzner/mock_tests.rs ++++ b/src/providers/hetzner/mock_tests.rs +@@ -1,7 +1,14 @@ ++use std::{collections::HashMap, fs, net::IpAddr, str::FromStr}; ++ ++use ipnetwork::IpNetwork; + use mockito; + use openssh_keys::Data; ++use pnet_base::MacAddr; + +-use crate::providers::MetadataProvider; ++use crate::{ ++ network::{Interface, NetworkRoute}, ++ providers::MetadataProvider, ++}; + + use super::HetznerProvider; + +@@ -178,3 +185,104 @@ fn test_pubkeys() { + let keys = provider.ssh_keys().unwrap(); + assert_eq!(keys.len(), 2); + } ++ ++#[test] ++fn test_networks() { ++ let endpoint_network_config = "/hetzner/v1/metadata/network-config"; ++ let (mut server, provider) = setup(); ++ ++ let name = "interface_name"; ++ let mac_addr = "11:11:11:11:11:11"; ++ let ipv6_network = "2a01:4f9:c013:f7c7::1/64"; ++ let gateway = "fa11::1"; ++ let dns_nameserver_1 = "1a01:0ff:ff00::add:1"; ++ let dns_nameserver_2 = "1a01:0ff:ff00::add:2"; ++ ++ let body_network_config = format!( ++ r#"version: 1 ++config: ++ - type: physical ++ name: {name} ++ mac_address: {mac_addr} ++ subnets: ++ - type: dhcp ++ ipv4: true ++ - type: static ++ address: {ipv6_network} ++ ipv6: true ++ gateway: {gateway} ++ dns_nameservers: ++ - {dns_nameserver_1} ++ - {dns_nameserver_2}"# ++ ); ++ ++ let expected = vec![Interface { ++ name: Some(name.into()), ++ mac_address: Some(MacAddr::from_str(mac_addr).unwrap()), ++ nameservers: vec![ ++ IpAddr::from_str(dns_nameserver_1).unwrap(), ++ IpAddr::from_str(dns_nameserver_2).unwrap(), ++ ], ++ ip_addresses: vec![IpNetwork::from_str(ipv6_network).unwrap()], ++ dhcp: Some(crate::network::DhcpSetting::V4), ++ routes: vec![NetworkRoute { ++ destination: IpNetwork::from_str("::/0").unwrap(), ++ gateway: IpAddr::from_str(gateway).unwrap(), ++ }], ++ unmanaged: false, ++ priority: 20, ++ bond: None, ++ required_for_online: None, ++ path: None, ++ }]; ++ ++ assert!(provider.networks().is_err(), "Should fail on not found"); ++ ++ let mock_metadata = server ++ .mock("GET", endpoint_network_config) ++ .with_status(503) ++ .create(); ++ assert!( ++ provider.networks().is_err(), ++ "Should fail on internal server error" ++ ); ++ mock_metadata.assert(); ++ ++ let mock_metadata = server ++ .mock("GET", endpoint_network_config) ++ .with_status(200) ++ .with_body(body_network_config) ++ .expect(1) ++ .create(); ++ ++ let actual = provider.networks().unwrap(); ++ mock_metadata.assert(); ++ ++ assert_eq!(actual, expected); ++} ++ ++#[test] ++fn test_netplan_config() { ++ let endpoint_network_config = "/hetzner/v1/metadata/network-config"; ++ let (mut server, provider) = setup(); ++ ++ let body = fs::read("./tests/fixtures/hetzner/network-config.yaml") ++ .expect("Unable to read network-config fixture"); ++ let expected = String::from_utf8( ++ fs::read("./tests/fixtures/hetzner/netplan-config.yaml") ++ .expect("Unable to read network-config fixture"), ++ ) ++ .unwrap(); ++ ++ let mock_metadata = server ++ .mock("GET", endpoint_network_config) ++ .with_status(200) ++ .with_body(body) ++ .expect(1) ++ .create(); ++ ++ let actual = provider.netplan_config().unwrap().unwrap(); ++ mock_metadata.assert(); ++ ++ assert_eq!(actual, expected); ++} +diff --git a/src/providers/hetzner/mod.rs b/src/providers/hetzner/mod.rs +index 6bb1d6ea..4630b6f7 100644 +--- a/src/providers/hetzner/mod.rs ++++ b/src/providers/hetzner/mod.rs +@@ -15,25 +15,41 @@ + //! Metadata fetcher for the hetzner provider + //! https://docs.hetzner.cloud/#server-metadata + +-use std::collections::HashMap; ++use std::{ ++ collections::HashMap, ++ net::{AddrParseError, IpAddr}, ++ str::FromStr, ++}; + + use anyhow::Result; + use openssh_keys::PublicKey; ++use pnet_base::MacAddr; + use serde::Deserialize; + +-use crate::retry; ++use ipnetwork::IpNetwork; ++use slog_scope::{error, warn}; ++ ++use crate::{ ++ network::{self, DhcpSetting, NetworkRoute}, ++ retry, ++}; + + use super::MetadataProvider; + + #[cfg(test)] + mod mock_tests; + ++// IPv4 address only. ++// cloud-init tries http://[fe80::a9fe:a9fe%]/hetzner/v1/metadata ++// first but Hetzner has no IPv6 address. However, even VMs without ++// public IPv4 addresses still have link-local access to fetch this ++// metadata. + const HETZNER_METADATA_BASE_URL: &str = "http://169.254.169.254/hetzner/v1/metadata"; + + /// Metadata provider for Hetzner Cloud + /// + /// See: https://docs.hetzner.cloud/#server-metadata +-#[derive(Clone, Debug)] ++#[derive(Debug)] + pub struct HetznerProvider { + client: retry::Client, + } +@@ -98,6 +114,107 @@ impl MetadataProvider for HetznerProvider { + + Ok(keys) + } ++ ++ fn networks(&self) -> Result> { ++ let network_config: NetworkConfig = self ++ .client ++ .get(retry::Yaml, Self::endpoint_for("network-config")) ++ .send()? ++ .unwrap_or_default(); ++ ++ network_config ++ .config ++ .iter() ++ .filter(|config| config.network_type == "physical") ++ .map(NetworkConfigEntry::to_interface) ++ .collect() ++ } ++ ++ fn netplan_config(&self) -> Result> { ++ let networks = self.networks()?; ++ ++ let mut ethernets = serde_yaml::Mapping::new(); ++ ++ for iface in networks { ++ let mut eth_config = serde_yaml::Mapping::new(); ++ let Some(name) = iface.name else { ++ warn!("Skipping interface, no name specified: {iface:?}"); ++ continue; ++ }; ++ ++ // 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()); ++ } ++ } ++ } ++ ++ if !iface.ip_addresses.is_empty() { ++ let addresses: Vec = iface ++ .ip_addresses ++ .iter() ++ .map(std::string::ToString::to_string) ++ .collect(); ++ eth_config.insert("addresses".into(), addresses.into()); ++ } ++ ++ if !iface.routes.is_empty() { ++ let routes: Vec = iface ++ .routes ++ .iter() ++ .map(|route| { ++ let mut route_map = serde_yaml::Mapping::new(); ++ route_map.insert("to".into(), route.destination.to_string().into()); ++ route_map.insert("via".into(), route.gateway.to_string().into()); ++ serde_yaml::Value::Mapping(route_map) ++ }) ++ .collect(); ++ eth_config.insert("routes".into(), routes.into()); ++ } ++ ++ if !iface.nameservers.is_empty() { ++ let nameservers: Vec<_> = iface ++ .nameservers ++ .iter() ++ .map(std::string::ToString::to_string) ++ .collect(); ++ eth_config.insert( ++ "nameservers".into(), ++ serde_yaml::Value::Mapping(serde_yaml::Mapping::from_iter([( ++ "addresses".into(), ++ nameservers.into(), ++ )])), ++ ); ++ } ++ ++ ethernets.insert(name.into(), eth_config.into()); ++ } ++ ++ let network = serde_yaml::Mapping::from_iter([ ++ ("version".into(), 2.into()), ++ ("ethernets".into(), ethernets.into()), ++ ]); ++ let netplan = serde_yaml::Mapping::from_iter([("network".into(), network.into())]); ++ ++ Ok(Some(serde_yaml::to_string(&netplan)?)) ++ } ++ ++ fn rd_network_kargs(&self) -> Result> { ++ warn!( ++ "initrd network kargs requested, but not supported on this platform due to network \ ++ requirements for fetching metadata" ++ ); ++ Ok(None) ++ } + } + + #[derive(Debug, Deserialize)] +@@ -113,6 +230,127 @@ struct Metadata { + public_ipv4: Option, + availability_zone: Option, + region: Option, ++ network_config: Option, ++} ++ ++// NOTE: Hetzner's network config seems to mostly follow the cloud-init v1 network config, but with ++// some minor deviations. See https://docs.cloud-init.io/en/latest/reference/network-config-format-v1.html ++#[derive(Debug, Deserialize)] ++struct NetworkConfig { ++ #[serde(default)] ++ config: Vec, ++} ++ ++#[derive(Debug, Deserialize)] ++struct NetworkConfigEntry { ++ #[serde(rename = "type")] ++ network_type: String, ++ mac_address: Option, ++ name: Option, ++ #[serde(default)] ++ subnets: Vec, ++} ++ ++impl NetworkConfigEntry { ++ fn to_interface(&self) -> Result { ++ if self.network_type != "physical" { ++ return Err(anyhow::anyhow!( ++ "cannot convert config to interface: unsupported network type \"{}\"", ++ self.network_type ++ )); ++ } ++ ++ let mut iface = network::Interface { ++ name: self.name.clone(), ++ ip_addresses: vec![], ++ routes: vec![], ++ dhcp: None, ++ mac_address: None, ++ nameservers: vec![], ++ bond: None, ++ path: None, ++ priority: 20, ++ unmanaged: false, ++ required_for_online: None, ++ }; ++ ++ if let Some(mac) = &self.mac_address { ++ iface.mac_address = Some(MacAddr::from_str(mac)?); ++ } ++ ++ for subnet in &self.subnets { ++ match subnet.subnet_type.as_ref() { ++ "static" | "static6" => { ++ let Some(ref address_str) = subnet.address else { ++ return Err(anyhow::anyhow!( ++ "cannot convert static subnet to interface: missing address" ++ )); ++ }; ++ ++ iface.nameservers.extend( ++ subnet ++ .dns_nameservers ++ .iter() ++ .map(|ip| IpAddr::from_str(ip)) ++ .collect::, AddrParseError>>()?, ++ ); ++ ++ if let Some(netmask) = &subnet.netmask { ++ iface.ip_addresses.push(IpNetwork::with_netmask( ++ IpAddr::from_str(address_str)?, ++ IpAddr::from_str(netmask)?, ++ )?); ++ } else { ++ iface.ip_addresses.push(IpNetwork::from_str(address_str)?); ++ } ++ ++ let Some(gateway_str) = &subnet.gateway else { ++ warn!("found subnet type \"static\" without gateway - address added but will not be routable to the internet"); ++ continue; ++ }; ++ ++ let gateway = IpAddr::from_str(gateway_str)?; ++ 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, ++ }); ++ } ++ ++ "dhcp" | "dhcp4" | "dhcp6" => { ++ let dhcp = if subnet.ipv6.is_some_and(|b| b) { ++ DhcpSetting::V6 ++ } else { ++ DhcpSetting::V4 ++ }; ++ iface.dhcp = iface.dhcp.map(|d| d.merge(dhcp.clone())).or(Some(dhcp)) ++ } ++ ++ subnet_type => warn!("Ignoring unsupported subnet type: \"{subnet_type}\""), ++ } ++ } ++ ++ Ok(iface) ++ } ++} ++ ++#[derive(Debug, Deserialize)] ++struct SubnetConfig { ++ #[serde(rename = "type")] ++ subnet_type: String, ++ #[allow(unused)] ++ ipv4: Option, ++ ipv6: Option, ++ netmask: Option, ++ address: Option, ++ gateway: Option, ++ #[serde(default)] ++ dns_nameservers: Vec, + } + + struct Attributes { +@@ -126,7 +364,7 @@ impl From for HashMap { + + let add_value = |map: &mut HashMap<_, _>, key: &str, value: Option| { + if let Some(value) = value { +- map.insert(key.to_string(), value); ++ map.insert(key.to_owned(), value); + } + }; + +@@ -162,50 +400,47 @@ impl From for HashMap { + + #[cfg(test)] + mod tests { +- use super::{Metadata, PrivateNetwork}; ++ use std::fs::{self}; ++ ++ use super::*; + + #[test] + fn test_metadata_deserialize() { +- let body = r#"availability-zone: hel1-dc2 +-hostname: my-server +-instance-id: 42 +-public-ipv4: 1.2.3.4 +-region: eu-central +-public-keys: []"#; +- +- let meta: Metadata = serde_yaml::from_str(body).unwrap(); ++ let body = fs::read("./tests/fixtures/hetzner/metadata.yaml") ++ .expect("Unable to read metadata fixture"); ++ let meta: Metadata = serde_yaml::from_slice(body.as_slice()).unwrap(); + + assert_eq!(meta.availability_zone.unwrap(), "hel1-dc2"); + assert_eq!(meta.hostname.unwrap(), "my-server"); + assert_eq!(meta.instance_id.unwrap(), 42); + assert_eq!(meta.public_ipv4.unwrap(), "1.2.3.4"); ++ assert_eq!(meta.network_config.unwrap().config.len(), 1); + } + + #[test] + fn test_private_networks_deserialize() { +- let body = r"- ip: 10.0.0.2 +- alias_ips: [] +- interface_num: 2 +- mac_address: 86:00:00:98:40:6e +- network_id: 4124728 +- network_name: foo +- network: 10.0.0.0/16 +- subnet: 10.0.0.0/24 +- gateway: 10.0.0.1 +-- ip: 10.128.0.2 +- alias_ips: [] +- interface_num: 1 +- mac_address: 86:00:00:98:40:6d +- network_id: 4451335 +- network_name: bar +- network: 10.128.0.0/16 +- subnet: 10.128.0.0/16 +- gateway: 10.128.0.1"; +- +- let private_networks: Vec = serde_yaml::from_str(body).unwrap(); ++ let body = fs::read("./tests/fixtures/hetzner/private-networks.yaml") ++ .expect("Unable to read metadata fixture"); ++ let private_networks: Vec = ++ serde_yaml::from_slice(body.as_slice()).unwrap(); + + assert_eq!(private_networks.len(), 2); + assert_eq!(private_networks[0].ip.clone().unwrap(), "10.0.0.2"); + assert_eq!(private_networks[1].ip.clone().unwrap(), "10.128.0.2"); + } ++ ++ #[test] ++ fn test_network_config_deserialize() { ++ let body = fs::read("./tests/fixtures/hetzner/network-config.yaml") ++ .expect("Unable to read network-config fixture"); ++ let network_config: NetworkConfig = serde_yaml::from_slice(body.as_slice()).unwrap(); ++ ++ assert_eq!(network_config.config.len(), 1); ++ ++ let entry = network_config.config.first().unwrap(); ++ assert_eq!(entry.name.as_ref().unwrap(), "eth0"); ++ assert_eq!(entry.mac_address.as_ref().unwrap(), "00:00:00:00:00:00"); ++ assert_eq!(entry.network_type, "physical"); ++ assert_eq!(entry.subnets.len(), 2); ++ } + } +diff --git a/tests/fixtures/hetzner/metadata.yaml b/tests/fixtures/hetzner/metadata.yaml +new file mode 100644 +index 00000000..1a59c927 +--- /dev/null ++++ b/tests/fixtures/hetzner/metadata.yaml +@@ -0,0 +1,24 @@ ++availability-zone: hel1-dc2 ++hostname: my-server ++instance-id: 42 ++local-ipv4: '' ++network-config: ++ version: 1 ++ config: ++ - type: physical ++ name: eth0 ++ mac_address: 00:00:00:00:00:00 ++ subnets: ++ - type: dhcp ++ ipv4: true ++ - type: static ++ address: 0a00:0a0:a000:a000::1/64 ++ ipv6: true ++ gateway: aa11::1 ++ dns_nameservers: ++ - 0a01:0ff:ff00::add:2 ++ - 0a01:0ff:ff00::add:1 ++public-ipv4: 1.2.3.4 ++public-keys: ++ - ssh-ed25519 AAAAA3AAAA1AAAA1AAA5AAAAAA6AAAAAA3AAAAAAA/A4AAAAAA+AAAAA8A3AAA3AA8AA ++region: eu-central +diff --git a/tests/fixtures/hetzner/netplan-config.yaml b/tests/fixtures/hetzner/netplan-config.yaml +new file mode 100644 +index 00000000..f656f0ae +--- /dev/null ++++ b/tests/fixtures/hetzner/netplan-config.yaml +@@ -0,0 +1,14 @@ ++network: ++ version: 2 ++ ethernets: ++ eth0: ++ dhcp4: true ++ addresses: ++ - 1a00:20a0:a000:a000::1/64 ++ routes: ++ - to: ::/0 ++ via: aa11::1 ++ nameservers: ++ addresses: ++ - 1a01:30ff:ff00::add:2 ++ - 1a01:30ff:ff00::add:1 +diff --git a/tests/fixtures/hetzner/network-config.yaml b/tests/fixtures/hetzner/network-config.yaml +new file mode 100644 +index 00000000..960833ae +--- /dev/null ++++ b/tests/fixtures/hetzner/network-config.yaml +@@ -0,0 +1,15 @@ ++version: 1 ++config: ++ - type: physical ++ name: eth0 ++ mac_address: 00:00:00:00:00:00 ++ subnets: ++ - type: dhcp ++ ipv4: true ++ - type: static ++ address: 1a00:20a0:a000:a000::1/64 ++ ipv6: true ++ gateway: aa11::1 ++ dns_nameservers: ++ - 1a01:30ff:ff00::add:2 ++ - 1a01:30ff:ff00::add:1 +diff --git a/tests/fixtures/hetzner/private-networks.yaml b/tests/fixtures/hetzner/private-networks.yaml +new file mode 100644 +index 00000000..1cbccbee +--- /dev/null ++++ b/tests/fixtures/hetzner/private-networks.yaml +@@ -0,0 +1,18 @@ ++- ip: 10.0.0.2 ++ alias_ips: [] ++ interface_num: 2 ++ mac_address: 86:00:00:98:40:6e ++ network_id: 4124728 ++ network_name: foo ++ network: 10.0.0.0/16 ++ subnet: 10.0.0.0/24 ++ gateway: 10.0.0.1 ++- ip: 10.128.0.2 ++ alias_ips: [] ++ interface_num: 1 ++ mac_address: 86:00:00:98:40:6d ++ network_id: 4451335 ++ network_name: bar ++ network: 10.128.0.0/16 ++ subnet: 10.128.0.0/16 ++ gateway: 10.128.0.1 + +From 6a91e5b408d3c95d20431b817d61e007685b8c3f Mon Sep 17 00:00:00 2001 +From: Rolv-Apneseth +Date: Fri, 6 Mar 2026 16:05:30 +0000 +Subject: [PATCH 2/2] providers/hetzner: Add the HETZNER_PUBLIC_IPV6 attribute + +--- + docs/release-notes.md | 1 + + docs/usage/attributes.md | 1 + + src/providers/hetzner/mock_tests.rs | 60 +++++++++++++++++++---------- + src/providers/hetzner/mod.rs | 39 +++++++++++++++++-- + 4 files changed, 77 insertions(+), 24 deletions(-) + +diff --git a/src/providers/hetzner/mock_tests.rs b/src/providers/hetzner/mock_tests.rs +index cf088373..772e0ab2 100644 +--- a/src/providers/hetzner/mock_tests.rs ++++ b/src/providers/hetzner/mock_tests.rs +@@ -22,13 +22,14 @@ fn setup() -> (mockito::ServerGuard, HetznerProvider) { + #[test] + fn test_attributes() { + let endpoint_metadata = "/hetzner/v1/metadata"; +- let endpoint_networks = "/hetzner/v1/metadata/private-networks"; ++ let endpoint_private_networks = "/hetzner/v1/metadata/private-networks"; + let (mut server, provider) = setup(); + + let availability_zone = "fsn1-dc14"; + let hostname = "some-hostname"; + let instance_id = "12345678"; + let public_ipv4 = "192.0.2.10"; ++ let public_ipv6 = "1a00:1a0:a000:a000::1"; + let region = "eu-central"; + + let body_metadata = format!( +@@ -39,26 +40,45 @@ public-ipv4: {public_ipv4} + region: {region} + local-ipv4: '' + public-keys: [] +-vendor_data: "blah blah blah""# ++vendor_data: "blah blah blah" ++network-config: ++ version: 1 ++ config: ++ - type: physical ++ name: eth0 ++ mac_address: "11:11:11:11:11:11" ++ subnets: ++ - type: dhcp ++ ipv4: true ++ - type: static ++ address: {public_ipv6}/64 ++ ipv6: true ++ gateway: aa11::1 ++ dns_nameservers: ++ - 0a01:0ff:ff00::add:2 ++ - 0a01:0ff:ff00::add:1"# + ); + + let ip_0 = "10.0.0.2"; + let ip_1 = "10.128.0.2"; +- +- let body_networks = format!( ++ let body_private_networks = format!( + r#"- ip: {ip_0} + - ip: {ip_1}"# + ); + +- let expected = maplit::hashmap! { +- "HETZNER_AVAILABILITY_ZONE".to_string() => availability_zone.to_string(), +- "HETZNER_HOSTNAME".to_string() => hostname.to_string(), +- "HETZNER_INSTANCE_ID".to_string() => instance_id.to_string(), +- "HETZNER_PUBLIC_IPV4".to_string() => public_ipv4.to_string(), +- "HETZNER_REGION".to_string() => region.to_string(), +- "HETZNER_PRIVATE_IPV4_0".to_string() => ip_0.to_string(), +- "HETZNER_PRIVATE_IPV4_1".to_string() => ip_1.to_string(), +- }; ++ let expected = HashMap::from([ ++ ( ++ "HETZNER_AVAILABILITY_ZONE".to_owned(), ++ availability_zone.to_owned(), ++ ), ++ ("HETZNER_HOSTNAME".to_owned(), hostname.to_owned()), ++ ("HETZNER_INSTANCE_ID".to_owned(), instance_id.to_owned()), ++ ("HETZNER_PUBLIC_IPV4".to_owned(), public_ipv4.to_owned()), ++ ("HETZNER_PUBLIC_IPV6".to_owned(), public_ipv6.to_owned()), ++ ("HETZNER_REGION".to_owned(), region.to_owned()), ++ ("HETZNER_PRIVATE_IPV4_0".to_owned(), ip_0.to_owned()), ++ ("HETZNER_PRIVATE_IPV4_1".to_owned(), ip_1.to_owned()), ++ ]); + + // Fail on not found + provider.attributes().unwrap_err(); +@@ -79,23 +99,23 @@ vendor_data: "blah blah blah""# + .create(); + + // Fail on internal server errors (networks endpoint) +- let mock_networks = server +- .mock("GET", endpoint_networks) ++ let mock_private_networks = server ++ .mock("GET", endpoint_private_networks) + .with_status(503) + .create(); + provider.attributes().unwrap_err(); +- mock_networks.assert(); ++ mock_private_networks.assert(); + + // Fetch metadata +- let mock_networks = server +- .mock("GET", endpoint_networks) ++ let mock_private_networks = server ++ .mock("GET", endpoint_private_networks) + .with_status(200) +- .with_body(body_networks) ++ .with_body(body_private_networks) + .create(); + + let actual = provider.attributes().unwrap(); + mock_metadata.assert(); +- mock_networks.assert(); ++ mock_private_networks.assert(); + assert_eq!(actual, expected); + } + +diff --git a/src/providers/hetzner/mod.rs b/src/providers/hetzner/mod.rs +index 4630b6f7..19434c90 100644 +--- a/src/providers/hetzner/mod.rs ++++ b/src/providers/hetzner/mod.rs +@@ -69,15 +69,15 @@ impl MetadataProvider for HetznerProvider { + fn attributes(&self) -> Result> { + let metadata: Metadata = self + .client +- .get(retry::Yaml, HETZNER_METADATA_BASE_URL.to_string()) ++ .get(retry::Yaml, HETZNER_METADATA_BASE_URL.to_owned()) + .send()? +- .unwrap(); ++ .expect("return_on_404 should be disabled"); + + let private_networks: Vec = self + .client + .get(retry::Yaml, Self::endpoint_for("private-networks")) + .send()? +- .unwrap(); ++ .expect("return_on_404 should be disabled"); + + Ok(Attributes { + metadata, +@@ -120,7 +120,7 @@ impl MetadataProvider for HetznerProvider { + .client + .get(retry::Yaml, Self::endpoint_for("network-config")) + .send()? +- .unwrap_or_default(); ++ .expect("return_on_404 should be disabled"); + + network_config + .config +@@ -241,6 +241,29 @@ struct NetworkConfig { + config: Vec, + } + ++impl NetworkConfig { ++ /// Helper method to extract the first available public IPv6 address. ++ fn get_public_ipv6(&self) -> Option { ++ let addr_str = self ++ .config ++ .iter() ++ .find(|n| n.network_type == "physical")? ++ .subnets ++ .iter() ++ .find(|sub| sub.ipv6.is_some_and(|b| b))? ++ .address ++ .as_ref()?; ++ ++ match IpNetwork::from_str(addr_str).ok()? { ++ IpNetwork::V4(_) => { ++ error!("Expected IPv6 but found IPv4 {addr_str}"); ++ None ++ } ++ IpNetwork::V6(network) => Some(network.ip().to_string()), ++ } ++ } ++} ++ + #[derive(Debug, Deserialize)] + struct NetworkConfigEntry { + #[serde(rename = "type")] +@@ -384,6 +407,14 @@ impl From for HashMap { + "HETZNER_PUBLIC_IPV4", + attributes.metadata.public_ipv4, + ); ++ add_value( ++ &mut out, ++ "HETZNER_PUBLIC_IPV6", ++ attributes ++ .metadata ++ .network_config ++ .and_then(|n| n.get_public_ipv6()), ++ ); + add_value(&mut out, "HETZNER_REGION", attributes.metadata.region); + + for (i, a) in attributes.private_networks.iter().enumerate() { diff --git a/sdk_container/src/third_party/portage-stable/sys-kernel/dracut/Manifest b/sdk_container/src/third_party/portage-stable/sys-kernel/dracut/Manifest index a676a2498af..c8f66631317 100644 --- a/sdk_container/src/third_party/portage-stable/sys-kernel/dracut/Manifest +++ b/sdk_container/src/third_party/portage-stable/sys-kernel/dracut/Manifest @@ -1,3 +1,3 @@ DIST dracut-108.tar.gz 552724 BLAKE2B fa5efd4039be8d2d30564a0768d2dec8c3d72b3721391d3a02bde8445cd7a5766d4b119594ebbca86dbd59bd71b64353febaaa7e2257f02d0075333dc29e9047 SHA512 ed06844b65003c1f75373309dbdc10a3036958f413b05b608510142fa78bb34dd9fb1e622242b02b2e9e4063bfa916fd755f5131a77fb3249f77d3f08eca2283 -DIST dracut-109.tar.gz 556054 BLAKE2B a5cc0a954fdacaca036d926eb37ecc1c5ed244f59078afb3d59f464f84cf109d52b61ef3ef58eb182164b491d87f8b81a8946c591f0207a44ecd6a6411f018e5 SHA512 c1fb0191911b3b3eac4f568db1cbc8184e2b518a70a0567777b72ef8a5af70a8bdb1b3f8acce2d92eb2dc807e056d5b75aed93292cd05bc8c333efe5d4b98adc +DIST dracut-109.tar.gz 556060 BLAKE2B b1f456182bd79e213d30751822c6d284a29e7b2f8c43bd59dad84e22520dbd461c872e44e4bc35c27d76faf35ed8b6a525f72ef8289b419f75ed68988e9937f0 SHA512 4bd846fb67a0af698a34f02b6b5594e547257d9904b2173e23298623a47295e589fe6b2dcd83f32b8721b50b94e8f7a0cf41d19d17b847238d7bfd9195c3061d DIST dracut-110.tar.gz 573501 BLAKE2B e82e9db9767bb3e02f13536b26b97a0db94a567980c17e356054015ab3108946b35f7d993864f2ffded3dd0915ee5853df21b7e40ce20a4df5a81a5014a73ee5 SHA512 be5affbe1c76889c0ffe3ae6c52704b559e364cfa5fa149e07a0bbe5e373c7e8c4b54e4a20e7564c91750c8e1593f7cd108806cf63053c8e30d143246e549597 diff --git a/sdk_container/src/third_party/portage-stable/sys-kernel/dracut/dracut-109-r1.ebuild b/sdk_container/src/third_party/portage-stable/sys-kernel/dracut/dracut-109-r2.ebuild similarity index 99% rename from sdk_container/src/third_party/portage-stable/sys-kernel/dracut/dracut-109-r1.ebuild rename to sdk_container/src/third_party/portage-stable/sys-kernel/dracut/dracut-109-r2.ebuild index 59515121d45..7a2bcaf0369 100644 --- a/sdk_container/src/third_party/portage-stable/sys-kernel/dracut/dracut-109-r1.ebuild +++ b/sdk_container/src/third_party/portage-stable/sys-kernel/dracut/dracut-109-r2.ebuild @@ -14,7 +14,6 @@ else KEYWORDS="~alpha ~amd64 ~arm ~arm64 ~hppa ~loong ~m68k ~mips ~ppc ~ppc64 ~riscv ~sparc ~x86" fi SRC_URI="https://github.com/dracut-ng/dracut-ng/archive/refs/tags/${PV}.tar.gz -> ${P}.tar.gz" - S="${WORKDIR}/${PN}-ng-${PV}" fi DESCRIPTION="Generic initramfs generation tool"