Skip to content

Commit

Permalink
providers/hetzner: private ipv4 addresses in attributes
Browse files Browse the repository at this point in the history
Adds new attributes `AFTERBURN_HETZNER_PRIVATE_IPV4_*` set to the
servers IPv4 addresses in private networks.

This is useful for e.g. discovery of etcd members that should
communicate over the private network.

Signed-off-by: Julian Tölle <[email protected]>
  • Loading branch information
apricote committed Jul 21, 2024
1 parent f82b8e6 commit 3806655
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 22 deletions.
1 change: 1 addition & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Major changes:
Minor changes:

- Hetzner: fix duplicate attribute prefix
- Hetzner: Return private IPv4 addresses in attributes
- Fix Azure SSH key fetching when no key provisioned

Packaging changes:
Expand Down
1 change: 1 addition & 0 deletions docs/usage/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ Cloud providers with supported metadata endpoints and their respective attribute
- AFTERBURN_HETZNER_HOSTNAME
- AFTERBURN_HETZNER_INSTANCE_ID
- AFTERBURN_HETZNER_PUBLIC_IPV4
- AFTERBURN_HETZNER_PRIVATE_IPV4_0
- AFTERBURN_HETZNER_REGION
* ibmcloud
- AFTERBURN_IBMCLOUD_INSTANCE_ID
Expand Down
43 changes: 34 additions & 9 deletions src/providers/hetzner/mock_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ fn setup() -> (mockito::ServerGuard, HetznerProvider) {

#[test]
fn test_attributes() {
let endpoint = "/hetzner/v1/metadata";
let endpoint_metadata = "/hetzner/v1/metadata";
let endpoint_networks = "/hetzner/v1/metadata/private-networks";
let (mut server, provider) = setup();

let availability_zone = "fsn1-dc14";
Expand All @@ -23,7 +24,7 @@ fn test_attributes() {
let public_ipv4 = "192.0.2.10";
let region = "eu-central";

let body = format!(
let body_metadata = format!(
r#"availability-zone: {availability_zone}
hostname: {hostname}
instance-id: {instance_id}
Expand All @@ -34,30 +35,54 @@ public-keys: []
vendor_data: "blah blah blah""#
);

let ip_0 = "10.0.0.2";
let ip_1 = "10.128.0.2";

let body_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(),
};

// Fail on not found
provider.attributes().unwrap_err();

// Fail on internal server errors
let mock = server.mock("GET", endpoint).with_status(503).create();
// Fail on internal server errors (metadata endpoint)
let mock_metadata = server.mock("GET", endpoint_metadata).with_status(503).create();
provider.attributes().unwrap_err();
mock.assert();
mock_metadata.assert();

let mock_metadata = server
.mock("GET", endpoint_metadata)
.with_status(200)
.with_body(body_metadata)
.expect(2) // Once for the private-networks error test and once to compare the result
.create();

// Fail on internal server errors (networks endpoint)
let mock_networks = server.mock("GET", endpoint_networks).with_status(503).create();
provider.attributes().unwrap_err();
mock_networks.assert();

// Fetch metadata
let mock = server
.mock("GET", endpoint)
let mock_networks = server
.mock("GET", endpoint_networks)
.with_status(200)
.with_body(body)
.with_body(body_networks)
.create();

let actual = provider.attributes().unwrap();
mock.assert();
mock_metadata.assert();
mock_networks.assert();
assert_eq!(actual, expected);
}

Expand Down
76 changes: 63 additions & 13 deletions src/providers/hetzner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,20 @@ impl HetznerProvider {
}

impl MetadataProvider for HetznerProvider {
fn attributes(&self) -> Result<std::collections::HashMap<String, String>> {
let meta: HetznerMetadata = self
fn attributes(&self) -> Result<HashMap<String, String>> {
let metadata: Metadata = self
.client
.get(retry::Yaml, HETZNER_METADATA_BASE_URL.to_string())
.send()?
.unwrap();

Ok(meta.into())
let private_networks: Vec<PrivateNetwork> = self
.client
.get(retry::Yaml, Self::endpoint_for("private-networks"))
.send()?
.unwrap();

Ok(Attributes{ metadata, private_networks }.into())
}

fn hostname(&self) -> Result<Option<String>> {
Expand Down Expand Up @@ -90,18 +96,28 @@ impl MetadataProvider for HetznerProvider {
}
}

#[derive(Debug, Deserialize)]
struct PrivateNetwork {
ip: Option<String>
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
struct HetznerMetadata {
struct Metadata {
hostname: Option<String>,
instance_id: Option<i64>,
public_ipv4: Option<String>,
availability_zone: Option<String>,
region: Option<String>,
}

impl From<HetznerMetadata> for HashMap<String, String> {
fn from(meta: HetznerMetadata) -> Self {
struct Attributes {
metadata: Metadata,
private_networks: Vec<PrivateNetwork>
}

impl From<Attributes> for HashMap<String, String> {
fn from(attributes: Attributes) -> Self {
let mut out = HashMap::with_capacity(5);

let add_value = |map: &mut HashMap<_, _>, key: &str, value: Option<String>| {
Expand All @@ -113,24 +129,29 @@ impl From<HetznerMetadata> for HashMap<String, String> {
add_value(
&mut out,
"HETZNER_AVAILABILITY_ZONE",
meta.availability_zone,
attributes.metadata.availability_zone,
);
add_value(&mut out, "HETZNER_HOSTNAME", meta.hostname);
add_value(&mut out, "HETZNER_HOSTNAME", attributes.metadata.hostname);
add_value(
&mut out,
"HETZNER_INSTANCE_ID",
meta.instance_id.map(|i| i.to_string()),
attributes.metadata.instance_id.map(|i| i.to_string()),
);
add_value(&mut out, "HETZNER_PUBLIC_IPV4", meta.public_ipv4);
add_value(&mut out, "HETZNER_REGION", meta.region);
add_value(&mut out, "HETZNER_PUBLIC_IPV4", attributes.metadata.public_ipv4);
add_value(&mut out, "HETZNER_REGION", attributes.metadata.region);

for (i, a) in attributes.private_networks.iter().enumerate() {
add_value(&mut out, format!("HETZNER_PRIVATE_IPV4_{i}").as_str(), a.ip.clone());
}

out
}
}

#[cfg(test)]
mod tests {
use super::HetznerMetadata;
use super::{Metadata, PrivateNetwork};


#[test]
fn test_metadata_deserialize() {
Expand All @@ -141,11 +162,40 @@ public-ipv4: 1.2.3.4
region: eu-central
public-keys: []"#;

let meta: HetznerMetadata = serde_yaml::from_str(body).unwrap();
let meta: Metadata = serde_yaml::from_str(body).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");
}

#[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<PrivateNetwork> = serde_yaml::from_str(body).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");
}
}

0 comments on commit 3806655

Please sign in to comment.