Skip to content

Commit

Permalink
providers/vmware: Process guestinfo.metadata netplan configuration
Browse files Browse the repository at this point in the history
The network environment can be dynamic and thus needs to be provided as
VM metadata. Since the format should not depend on whether the VM runs
uses Ignition and Afterburn or Cloud-Init, the idea is to also support
the guestinfo.metadata variable as used by Cloud-Init which contains
Netplan YAML/JSON network configuration.
Add a new command to write out netplan configs to a given directory,
similar as we do with networkd units. While this is currently just used
for VMware, other providers could also construct the netplan data type
to provide netplan configurations if the OS rather wants to use
NetworkManager than systemd-networkd. For backwards compatibility and
to not need netplan it would be nice to keep the systemd-networkd
support as long as its used.

References:
https://cloudinit.readthedocs.io/en/latest/reference/datasources/vmware.html#walkthrough-of-guestinfo-keys-transport
https://cloudinit.readthedocs.io/en/latest/reference/network-config-format-v2.html
https://netplan.io/reference/
https://linux-on-z.blogspot.com/p/using-netplan-on-ibm-z.html
  • Loading branch information
pothos committed Sep 28, 2023
1 parent ebd3186 commit 2a33e02
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 2 deletions.
53 changes: 53 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,15 @@ debug = true

[dependencies]
anyhow = "1.0"
base64 = "0.21"
cfg-if = "1.0"
clap = { version = "4", "default_features" = false, "features" = ["std", "cargo", "derive", "error-context", "help", "suggestions", "usage", "wrap_help"] }
ipnetwork = ">= 0.17, < 0.21"
libflate = "1.3"
libsystemd = ">= 0.2.1, < 0.7.0"
mailparse = ">= 0.13, < 0.15"
maplit = "1.0"
netplan-types = "0.3"
nix = { version = ">= 0.19, < 0.28", "default_features" = false, "features" = [ "mount", "user"] }
openssh-keys = ">= 0.5, < 0.7"
openssl = ">= 0.10.46, < 0.11"
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ nav_order: 8
Major changes:

- Add support for Scaleway
- VMware: `afterburn multi --netplan-configs FOLDER --provider vmware` can now process `guestinfo.metadata` netplan configuration

Minor changes:

Expand Down
9 changes: 9 additions & 0 deletions src/cli/multi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ pub struct CliMulti {
/// The directory into which network units are written
#[arg(long = "network-units", value_name = "path")]
network_units_dir: Option<String>,
/// The directory into which netplan configs are written
#[arg(long = "netplan-configs", value_name = "path")]
netplan_config_dir: Option<String>,
/// Update SSH keys for the given user
#[arg(long = "ssh-keys", value_name = "username")]
ssh_keys_user: Option<String>,
Expand All @@ -41,6 +44,7 @@ impl CliMulti {

if self.attributes_file.is_none()
&& self.network_units_dir.is_none()
&& self.netplan_config_dir.is_none()
&& !self.check_in
&& self.ssh_keys_user.is_none()
&& self.hostname_file.is_none()
Expand Down Expand Up @@ -72,6 +76,11 @@ impl CliMulti {
.map_or(Ok(()), |x| metadata.write_network_units(x))
.context("writing network units")?;

// write netplan configs if configured to do so
self.netplan_config_dir
.map_or(Ok(()), |x| metadata.write_netplan_configs(x))
.context("writing network units")?;

// perform boot check-in.
if self.check_in {
metadata
Expand Down
21 changes: 21 additions & 0 deletions src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ pub trait MetadataProvider {
Ok(vec![])
}

fn netplan_config(&self) -> Result<Option<netplan_types::NetplanConfig>> {
Ok(None)
}

fn boot_checkin(&self) -> Result<()> {
warn!("boot check-in requested, but not supported on this platform");
Ok(())
Expand Down Expand Up @@ -295,6 +299,23 @@ pub trait MetadataProvider {
}
Ok(())
}

fn write_netplan_configs(&self, netplan_configs_dir: String) -> Result<()> {
let dir_path = Path::new(&netplan_configs_dir);
fs::create_dir_all(dir_path)
.with_context(|| format!("failed to create directory {dir_path:?}"))?;

// Write a single afterburn `.yaml` netplan config.
if let Some(netplan_config) = &self.netplan_config()? {
let netplan_yaml = serde_yaml::to_string(&netplan_config)?;
let file_path = dir_path.join("afterburn.yaml");
let mut config_file = File::create(&file_path)
.with_context(|| format!("failed to create file {file_path:?}"))?;
write!(&mut config_file, "{netplan_yaml}")
.with_context(|| format!("failed to write netplan config file {config_file:?}"))?;
}
Ok(())
}
}

#[cfg(test)]
Expand Down
174 changes: 172 additions & 2 deletions src/providers/vmware/amd64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@

use super::VmwareProvider;
use anyhow::{bail, Context, Result};
use base64::{engine::general_purpose, Engine as _};
use libflate::gzip::Decoder;
use std::io::Read;

/// Guestinfo key for network kargs.
static INITRD_NET_KARGS: &str = "guestinfo.afterburn.initrd.network-kargs";
static METADATA: &str = "guestinfo.metadata";
static METADATA_ENCODING: &str = "guestinfo.metadata.encoding";

impl VmwareProvider {
/// Build the VMware provider, fetching and caching guestinfo entries.
Expand All @@ -24,11 +29,24 @@ impl VmwareProvider {
vmw_backdoor::probe_backdoor()
})?;

let mut erpc = backdoor.open_enhanced_chan()?;
let mut erpc = vmw_backdoor::EnhancedChan::open(&mut backdoor)?;
let guestinfo_net_kargs = Self::fetch_guestinfo(&mut erpc, INITRD_NET_KARGS)?;

drop(erpc);
let mut erpc = vmw_backdoor::EnhancedChan::open(&mut backdoor)?;

let guestinfo_metadata_raw = Self::fetch_guestinfo(&mut erpc, METADATA)?;

drop(erpc);
let mut erpc = vmw_backdoor::EnhancedChan::open(&mut backdoor)?;

let guestinfo_metadata_encoding = Self::fetch_guestinfo(&mut erpc, METADATA_ENCODING)?;
let guestinfo_metadata =
parse_metadata(guestinfo_metadata_encoding, guestinfo_metadata_raw)?;

let provider = Self {
guestinfo_net_kargs,
guestinfo_metadata,
};

slog_scope::trace!("cached vmware provider: {:?}", provider);
Expand All @@ -39,8 +57,160 @@ impl VmwareProvider {
fn fetch_guestinfo(erpc: &mut vmw_backdoor::EnhancedChan, key: &str) -> Result<Option<String>> {
let guestinfo = erpc
.get_guestinfo(key.as_bytes())
.context("failed to retrieve network kargs")?
.context(format!("failed to retrieve guestinfo for {}", key))?
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned());
Ok(guestinfo)
}

pub fn parse_netplan_config(&self) -> Result<Option<netplan_types::NetplanConfig>> {
if let Some(metadata) = &self.guestinfo_metadata {
// To provide at least some sanity checking, parse the netplan config.
// First check if we have JSON userdata.
let netplan_config: netplan_types::NetplanConfig =
if let Ok(netplan_json_config) = serde_json::from_str(metadata) {
netplan_json_config
} else {
// The JSON parsing error is discarded,
// and if YAML parsing fails as well, we only return this error.
serde_yaml::from_str(metadata)?
};
// Also check the version to make it easier for the OS to say that
// this mechanism is supported or not.
if netplan_config.network.version != 2 {
bail!("only netplan version 2 supported");
}
Ok(Some(netplan_config))
} else {
Ok(None)
}
}

#[cfg(test)]
pub fn new_from_metadata(metadata: String) -> Result<Self> {
Ok(Self {
guestinfo_net_kargs: None,
guestinfo_metadata: Some(metadata),
})
}
}

fn parse_metadata(
guestinfo_metadata_encoding: Option<String>,
guestinfo_metadata_raw: Option<String>,
) -> Result<Option<String>> {
let guestinfo_metadata =
if let Some(guestinfo_metadata_encoding_val) = guestinfo_metadata_encoding {
match (
guestinfo_metadata_encoding_val.as_str(),
guestinfo_metadata_raw,
) {
("base64" | "b64", Some(guestinfo_metadata_raw_val)) => {
let decoded =
general_purpose::STANDARD.decode(guestinfo_metadata_raw_val.as_bytes())?;
Some(String::from_utf8(decoded)?)
}
("gzip+base64" | "gz+b64", Some(guestinfo_metadata_raw_val)) => {
let decoded =
general_purpose::STANDARD.decode(guestinfo_metadata_raw_val.as_bytes())?;
let mut decompressor = Decoder::new(decoded.as_slice())?;
let mut uncompressed = Vec::new();
decompressor.read_to_end(&mut uncompressed)?;
Some(String::from_utf8(uncompressed)?)
}
("", guestinfo_metadata_raw) => guestinfo_metadata_raw,
(&_, _) => bail!("unknown guestinfo.metadata.encoding"),
}
} else {
guestinfo_metadata_raw
};
Ok(guestinfo_metadata)
}

#[test]
fn test_netplan_json() {
let metadata = r#"{
"network": {
"version": 2,
"ethernets": {
"nics": {
"match": {
"name": "ens*"
},
"dhcp4": true
}
}
}
}"#;
let provider = VmwareProvider::new_from_metadata(metadata.to_owned()).unwrap();
let netplan_config = provider.parse_netplan_config().unwrap();
let netplan_yaml = serde_yaml::to_string(&netplan_config).unwrap();
let expected = r#"network:
version: 2
ethernets:
nics:
match:
name: ens*
dhcp4: true
"#;
assert_eq!(netplan_yaml, expected);
}

#[test]
fn test_netplan_dhcp() {
let metadata = r#"network:
version: 2
ethernets:
nics:
match:
name: ens*
dhcp4: true
"#;
let provider = VmwareProvider::new_from_metadata(metadata.to_owned()).unwrap();
let netplan_config = provider.parse_netplan_config().unwrap();
let netplan_yaml = serde_yaml::to_string(&netplan_config).unwrap();
assert_eq!(metadata, netplan_yaml);
}

#[test]
fn test_metadata_plain_1() {
let guestinfo_metadata_raw = Some("hello".to_owned());
let parsed = parse_metadata(None, guestinfo_metadata_raw)
.unwrap()
.unwrap();
assert_eq!(parsed, "hello");
}

#[test]
fn test_metadata_plain_2() {
let guestinfo_metadata_raw = Some("hello".to_owned());
let parsed = parse_metadata(Some("".into()), guestinfo_metadata_raw)
.unwrap()
.unwrap();
assert_eq!(parsed, "hello");
}

#[test]
fn test_metadata_base64() {
let guestinfo_metadata_raw = Some("aGVsbG8=".to_owned());
let parsed = parse_metadata(Some("base64".into()), guestinfo_metadata_raw.clone())
.unwrap()
.unwrap();
assert_eq!(parsed, "hello");
let parsed_b64 = parse_metadata(Some("b64".into()), guestinfo_metadata_raw)
.unwrap()
.unwrap();
assert_eq!(parsed_b64, "hello");
}

#[test]
fn test_metadata_gzip_base64() {
let guestinfo_metadata_raw = Some("H4sIAAAAAAACA8tIzcnJBwCGphA2BQAAAA==".to_owned());
let parsed = parse_metadata(Some("gzip+base64".into()), guestinfo_metadata_raw.clone())
.unwrap()
.unwrap();
assert_eq!(parsed, "hello");
let parsed_b64 = parse_metadata(Some("gz+b64".into()), guestinfo_metadata_raw)
.unwrap()
.unwrap();
assert_eq!(parsed_b64, "hello");
}
Loading

0 comments on commit 2a33e02

Please sign in to comment.