Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add KubeVirt platform support #725

Merged
merged 1 commit into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ The following platforms are supported, with a different set of features availabl
- SSH Keys
* ibmcloud-classic
- Attributes
* kubevirt
- Attributes
- SSH Keys
* openstack
- Attributes
- SSH Keys
Expand Down
4 changes: 4 additions & 0 deletions docs/usage/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ Cloud providers with supported metadata endpoints and their respective attribute
* ibmcloud-classic
- AFTERBURN_IBMCLOUD_CLASSIC_INSTANCE_ID
- AFTERBURN_IBMCLOUD_CLASSIC_LOCAL_HOSTNAME
* kubevirt
- AFTERBURN_KUBEVIRT_HOSTNAME
- AFTERBURN_KUBEVIRT_INSTANCE_ID
bgilbert marked this conversation as resolved.
Show resolved Hide resolved
- AFTERBURN_KUBEVIRT_INSTANCE_TYPE
* openstack
- AFTERBURN_OPENSTACK_HOSTNAME
- AFTERBURN_OPENSTACK_IPV4_LOCAL
Expand Down
2 changes: 2 additions & 0 deletions src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::providers::exoscale::ExoscaleProvider;
use crate::providers::gcp::GcpProvider;
use crate::providers::ibmcloud::IBMGen2Provider;
use crate::providers::ibmcloud_classic::IBMClassicProvider;
use crate::providers::kubevirt::KubeVirtProvider;
use crate::providers::microsoft::azure::Azure;
use crate::providers::microsoft::azurestack::AzureStack;
use crate::providers::openstack;
Expand Down Expand Up @@ -59,6 +60,7 @@ pub fn fetch_metadata(provider: &str) -> Result<Box<dyn providers::MetadataProvi
"ibmcloud" => box_result!(IBMGen2Provider::try_new()?),
// IBM Cloud - Classic infrastructure.
"ibmcloud-classic" => box_result!(IBMClassicProvider::try_new()?),
"kubevirt" => box_result!(KubeVirtProvider::try_new()?),
"openstack" => openstack::try_config_drive_else_network(),
"openstack-metadata" => box_result!(OpenstackProviderNetwork::try_new()?),
"packet" => box_result!(PacketProvider::try_new()?),
Expand Down
271 changes: 271 additions & 0 deletions src/providers/kubevirt/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
//! Metadata fetcher for KubeVirt instances.
//!
//! This provider supports platforms based on KubeVirt.
//! It provides a config-drive as the only metadata source, whose layout
//! follows the `cloud-init ConfigDrive v2` [datasource][configdrive], with
//! the following details:
//! - disk filesystem label is `config-2` (lowercase)
//! - filesystem is `iso9660`
//! - drive contains a single directory at `/openstack/latest/`
//! - content is exposed as JSON files called `meta_data.json`.
//!
//! configdrive: https://cloudinit.readthedocs.io/en/latest/topics/datasources/configdrive.html

use anyhow::{bail, Context, Result};
use openssh_keys::PublicKey;
use serde::Deserialize;
use slog_scope::warn;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use tempfile::TempDir;

use crate::network;
use crate::providers::MetadataProvider;

// Filesystem label for the Config Drive.
static CONFIG_DRIVE_FS_LABEL: &str = "config-2";

// Filesystem type for the Config Drive.
static CONFIG_DRIVE_FS_TYPE: &str = "iso9660";

///KubeVirt provider.
#[derive(Debug)]
pub struct KubeVirtProvider {
/// Path to the top directory of the mounted config-drive.
drive_path: PathBuf,
/// Temporary directory for own mountpoint.
temp_dir: TempDir,
}

/// Partial object for `meta_data.json`
#[derive(Debug, Deserialize)]
pub struct MetaDataJSON {
/// Local hostname
pub hostname: String,
/// Instance ID (UUID).
#[serde(rename = "uuid")]
pub instance_id: String,
/// Instance type.
pub instance_type: Option<String>,
/// SSH public keys.
pub public_keys: Option<HashMap<String, String>>,
}

impl KubeVirtProvider {
/// Try to build a new provider client.
///
/// This internally tries to mount (and own) the config-drive.
pub fn try_new() -> Result<Self> {
let target = 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_FS_LABEL),
target.path(),
CONFIG_DRIVE_FS_TYPE,
3, // maximum retries
)?;

let provider = Self {
drive_path: target.path().to_owned(),
temp_dir: target,
};
Ok(provider)
}

/// Return the path to the metadata directory.
fn metadata_dir(&self) -> PathBuf {
let drive = self.drive_path.clone();
drive.join("openstack").join("latest")
}

/// Read and parse metadata file.
fn read_metadata(&self) -> Result<MetaDataJSON> {
let filename = self.metadata_dir().join("meta_data.json");
let file = File::open(&filename)
.with_context(|| format!("failed to open file '{:?}'", filename))?;
let bufrd = BufReader::new(file);
Self::parse_metadata(bufrd)
}

/// Parse metadata attributes.
///
/// Metadata file contains a JSON object, corresponding to `MetaDataJSON`.
fn parse_metadata<T: Read>(input: BufReader<T>) -> Result<MetaDataJSON> {
serde_json::from_reader(input).context("failed to parse JSON metadata")
}

/// Extract supported metadata values and convert to Afterburn attributes.
///
/// The `AFTERBURN_` prefix is added later on, so it is not part of the
/// key-labels here.
fn known_attributes(metadata: MetaDataJSON) -> Result<HashMap<String, String>> {
if metadata.instance_id.is_empty() {
bail!("empty instance ID");
}

if metadata.hostname.is_empty() {
bail!("empty local hostname");
}

let mut attrs = maplit::hashmap! {
"KUBEVIRT_INSTANCE_ID".to_string() => metadata.instance_id,
"KUBEVIRT_HOSTNAME".to_string() => metadata.hostname,
};
if let Some(instance_type) = metadata.instance_type {
attrs.insert("KUBEVIRT_INSTANCE_TYPE".to_string(), instance_type);
}
Ok(attrs)
}

/// The public key is stored as key:value pair in openstack/latest/meta_data.json file
fn public_keys(metadata: MetaDataJSON) -> Result<Vec<PublicKey>> {
let public_keys_map = metadata.public_keys.unwrap_or_default();
let public_keys_vec: Vec<&std::string::String> = public_keys_map.values().collect();
let mut out = vec![];
for key in public_keys_vec {
let key = PublicKey::parse(key)?;
out.push(key);
}
Ok(out)
}
}

impl MetadataProvider for KubeVirtProvider {
fn attributes(&self) -> Result<HashMap<String, String>> {
let metadata = self.read_metadata()?;
Self::known_attributes(metadata)
}

fn hostname(&self) -> Result<Option<String>> {
let metadata = self.read_metadata()?;
let hostname = if metadata.hostname.is_empty() {
None
} else {
Some(metadata.hostname)
};
Ok(hostname)
}

fn ssh_keys(&self) -> Result<Vec<PublicKey>> {
let metadata = self.read_metadata()?;
Self::public_keys(metadata)
}

fn networks(&self) -> Result<Vec<network::Interface>> {
warn!("network interfaces metadata requested, but not supported on this platform");
Ok(vec![])
}

fn virtual_network_devices(&self) -> Result<Vec<network::VirtualNetDev>> {
warn!("virtual network devices metadata requested, but not supported on this platform");
Ok(vec![])
}

fn boot_checkin(&self) -> Result<()> {
warn!("boot check-in requested, but not supported on this platform");
Ok(())
}
}

impl Drop for KubeVirtProvider {
fn drop(&mut self) {
if let Err(e) = crate::util::unmount(
self.temp_dir.path(),
3, // maximum retries
) {
slog_scope::error!("failed to unmount kubevirt config-drive: {}", e);
};
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;

#[test]
fn test_kubevirt_basic_attributes() {
let metadata = r#"
{
"hostname": "test_instance-kubevirt.foo.cloud",
"uuid": "41b4fb82-ca29-11eb-b8bc-0242ac130003"
}
"#;

let bufrd = BufReader::new(Cursor::new(metadata));
let parsed = KubeVirtProvider::parse_metadata(bufrd).unwrap();
assert_eq!(parsed.instance_id, "41b4fb82-ca29-11eb-b8bc-0242ac130003");
assert_eq!(parsed.hostname, "test_instance-kubevirt.foo.cloud");

let attrs = KubeVirtProvider::known_attributes(parsed).unwrap();
assert_eq!(attrs.len(), 2);
assert_eq!(
attrs.get("KUBEVIRT_INSTANCE_ID"),
Some(&"41b4fb82-ca29-11eb-b8bc-0242ac130003".to_string())
);
assert_eq!(
attrs.get("KUBEVIRT_HOSTNAME"),
Some(&"test_instance-kubevirt.foo.cloud".to_string())
);
}

#[test]
fn test_kubevirt_extended_attributes() {
let metadata = r#"
{
"hostname": "test_instance-kubevirt.foo.cloud",
"uuid": "41b4fb82-ca29-11eb-b8bc-0242ac130003",
"instance_type": "some_type"
}
"#;

let bufrd = BufReader::new(Cursor::new(metadata));
let parsed = KubeVirtProvider::parse_metadata(bufrd).unwrap();
assert_eq!(parsed.instance_id, "41b4fb82-ca29-11eb-b8bc-0242ac130003");
assert_eq!(parsed.hostname, "test_instance-kubevirt.foo.cloud");
assert_eq!(parsed.instance_type.as_deref().unwrap(), "some_type");

let attrs = KubeVirtProvider::known_attributes(parsed).unwrap();
assert_eq!(attrs.len(), 3);
assert_eq!(
attrs.get("KUBEVIRT_INSTANCE_ID"),
Some(&"41b4fb82-ca29-11eb-b8bc-0242ac130003".to_string())
);
assert_eq!(
attrs.get("KUBEVIRT_HOSTNAME"),
Some(&"test_instance-kubevirt.foo.cloud".to_string())
);
assert_eq!(
attrs.get("KUBEVIRT_INSTANCE_TYPE"),
Some(&"some_type".to_string())
);
}

#[test]
fn test_kubevirt_parse_metadata_json() {
let fixture = File::open("./tests/fixtures/kubevirt/meta_data.json").unwrap();
let bufrd = BufReader::new(fixture);
let parsed = KubeVirtProvider::parse_metadata(bufrd).unwrap();

assert!(!parsed.instance_id.is_empty());
assert!(!parsed.hostname.is_empty());
assert!(!parsed.public_keys.is_none());
}

#[test]
fn test_kubevirt_ssh_keys() {
let fixture = File::open("./tests/fixtures/kubevirt/meta_data.json").unwrap();

let bufrd = BufReader::new(fixture);
let parsed = KubeVirtProvider::parse_metadata(bufrd).unwrap();
let keys = KubeVirtProvider::public_keys(parsed).unwrap();
let expect = PublicKey::parse("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDYVEprvtYJXVOBN0XNKVVRNCRX6BlnNbI+USLGais1sUWPwtSg7z9K9vhbYAPUZcq8c/s5S9dg5vTHbsiyPCIDOKyeHba4MUJq8Oh5b2i71/3BISpyxTBH/uZDHdslW2a+SrPDCeuMMoss9NFhBdKtDkdG9zyi0ibmCP6yMdEX8Q== Generated by Nova").unwrap();

assert_eq!(keys.len(), 1);
assert_eq!(keys[0], expect);
}
}
1 change: 1 addition & 0 deletions src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub mod exoscale;
pub mod gcp;
pub mod ibmcloud;
pub mod ibmcloud_classic;
pub mod kubevirt;
pub mod microsoft;
pub mod openstack;
pub mod packet;
Expand Down
8 changes: 8 additions & 0 deletions tests/fixtures/kubevirt/meta_data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"hostname": "fake",
"uuid": "5d307ca9-b3ef-428c-8861-06e72d69f223",
"devices": [],
"public_keys": {
"mykey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDYVEprvtYJXVOBN0XNKVVRNCRX6BlnNbI+USLGais1sUWPwtSg7z9K9vhbYAPUZcq8c/s5S9dg5vTHbsiyPCIDOKyeHba4MUJq8Oh5b2i71/3BISpyxTBH/uZDHdslW2a+SrPDCeuMMoss9NFhBdKtDkdG9zyi0ibmCP6yMdEX8Q== Generated by Nova"
}
}