Skip to content

Commit

Permalink
openstack: use config-drive by default
Browse files Browse the repository at this point in the history
This changes afterburn to use config-drive as default for scraping
metadata and then try metadata API if config-drive is not available.
Previously supported platform id 'openstack-metadata' will continue
to be supported.

Closes: coreos/fedora-coreos-tracker#422
Signed-off-by: Allen Bai <abai@redhat.com>
  • Loading branch information
Allen Bai committed Jul 30, 2020
1 parent ff48266 commit d0ea1c4
Show file tree
Hide file tree
Showing 8 changed files with 358 additions and 12 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ The following platforms are supported, with a different set of features availabl
- Attributes
* ibmcloud-classic
- Attributes
* openstack
- Attributes
- SSH Keys
* openstack-metadata
- Attributes
- SSH Keys
Expand Down
6 changes: 6 additions & 0 deletions docs/usage/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ Cloud providers with supported metadata endpoints and their respective attribute
* ibmcloud-classic
- AFTERBURN_IBMCLOUD_CLASSIC_INSTANCE_ID
- AFTERBURN_IBMCLOUD_CLASSIC_LOCAL_HOSTNAME
* openstack
- AFTERBURN_OPENSTACK_HOSTNAME
- AFTERBURN_OPENSTACK_IPV4_LOCAL
- AFTERBURN_OPENSTACK_IPV4_PUBLIC
- AFTERBURN_OPENSTACK_INSTANCE_ID
- AFTERBURN_OPENSTACK_INSTANCE_TYPE
* openstack-metadata
- AFTERBURN_OPENSTACK_HOSTNAME
- AFTERBURN_OPENSTACK_IPV4_LOCAL
Expand Down
4 changes: 2 additions & 2 deletions src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +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::openstack::network::OpenstackProvider;
use crate::providers::openstack;
use crate::providers::packet::PacketProvider;
#[cfg(feature = "cl-legacy")]
use crate::providers::vagrant_virtualbox::VagrantVirtualboxProvider;
Expand Down Expand Up @@ -62,7 +62,7 @@ pub fn fetch_metadata(provider: &str) -> errors::Result<Box<dyn providers::Metad
"ibmcloud" => box_result!(IBMGen2Provider::try_new()?),
// IBM Cloud - Classic infrastructure.
"ibmcloud-classic" => box_result!(IBMClassicProvider::try_new()?),
"openstack-metadata" => box_result!(OpenstackProvider::try_new()?),
"openstack" | "openstack-metadata" => openstack::try_config_drive_else_network(),
"packet" => box_result!(PacketProvider::try_new()?),
#[cfg(feature = "cl-legacy")]
"vagrant-virtualbox" => box_result!(VagrantVirtualboxProvider::new()),
Expand Down
273 changes: 273 additions & 0 deletions src/providers/openstack/configdrive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
//! configdrive metadata fetcher for OpenStack
//! reference: https://docs.openstack.org/nova/latest/user/metadata.html

use serde::Deserialize;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};

use openssh_keys::PublicKey;
use slog_scope::{error, warn};
use tempfile::TempDir;

use crate::errors::*;
use crate::network;
use crate::providers::MetadataProvider;

const CONFIG_DRIVE_LABEL: &str = "config-2";

/// Partial object for ec2 `meta_data.json`
#[derive(Debug, Deserialize)]
pub struct MetadataEc2JSON {
/// Local hostname.
pub hostname: Option<String>,
/// Instance ID.
#[serde(rename = "instance-id")]
pub instance_id: Option<String>,
/// Instance type.
#[serde(rename = "instance-type")]
pub instance_type: Option<String>,
/// Local IPV4.
#[serde(rename = "local-ipv4")]
pub local_ipv4: Option<String>,
/// Public IPV4.
#[serde(rename = "public-ipv4")]
pub public_ipv4: Option<String>,
}

/// Partial object for openstack `meta_data.json`
#[derive(Debug, Deserialize)]
pub struct MetadataOpenstackJSON {
/// Availability zone.
pub availability_zone: Option<String>,
/// Local hostname.
pub hostname: Option<String>,
/// SSH public keys.
pub public_keys: Option<HashMap<String, String>>,
}

/// OpenStack config-drive.
#[derive(Debug)]
pub struct OpenstackConfigDrive {
/// Path to the top directory of the mounted config-drive.
drive_path: PathBuf,
/// Temporary directory for own mountpoint (if any).
temp_dir: Option<TempDir>,
}

impl OpenstackConfigDrive {
/// Try to build a new provider client.
///
/// This internally tries to mount (and own) the config-drive.
pub fn try_new() -> Result<Self> {
const TARGET_FS: &str = "iso9660";
let target = tempfile::Builder::new()
.prefix("afterburn-")
.tempdir()
.chain_err(|| "failed to create temporary directory")?;
crate::util::mount_ro(
&Path::new("/dev/disk/by-label/").join(CONFIG_DRIVE_LABEL),
target.path(),
TARGET_FS,
3,
)?;

let cd = OpenstackConfigDrive {
drive_path: target.path().to_owned(),
temp_dir: Some(target),
};
Ok(cd)
}

/// Return the path to the metadata directory.
fn metadata_dir(&self, platform: &str) -> PathBuf {
self.drive_path.clone().join(platform).join("latest")
}

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

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

/// The metadata is stored as key:value pair in ec2/latest/meta-data.json file
fn read_metadata_ec2(&self) -> Result<MetadataEc2JSON> {
let filename = self.metadata_dir("ec2").join("meta-data.json");
let file =
File::open(&filename).chain_err(|| format!("failed to open file '{:?}'", filename))?;
let bufrd = BufReader::new(file);
Self::parse_metadata_ec2(bufrd)
.chain_err(|| format!("failed to parse file '{:?}'", filename))
}

/// The metadata is stored as key:value pair in openstack/latest/meta_data.json file
fn read_metadata_openstack(&self) -> Result<MetadataOpenstackJSON> {
let filename = self.metadata_dir("openstack").join("meta_data.json");
let file =
File::open(&filename).chain_err(|| format!("failed to open file '{:?}'", filename))?;
let bufrd = BufReader::new(file);
Self::parse_metadata_openstack(bufrd)
.chain_err(|| format!("failed to parse file '{:?}'", filename))
}

/// The public key is stored as key:value pair in openstack/latest/meta_data.json file
fn fetch_publickeys(&self) -> Result<Vec<PublicKey>> {
let filename = self.metadata_dir("openstack").join("meta_data.json");
let file =
File::open(&filename).chain_err(|| format!("failed to open file '{:?}'", filename))?;

let bufrd = BufReader::new(file);
let metadata: MetadataOpenstackJSON = Self::parse_metadata_openstack(bufrd)
.chain_err(|| format!("failed to parse file '{:?}'", filename))?;

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 OpenstackConfigDrive {
fn attributes(&self) -> Result<HashMap<String, String>> {
let mut out = HashMap::with_capacity(5);
let metadata_ec2: MetadataEc2JSON = self.read_metadata_ec2()?;
let metadata_openstack: MetadataOpenstackJSON = self.read_metadata_openstack()?;
if metadata_openstack.hostname.is_some() {
out.insert(
"OPENSTACK_HOSTNAME".to_string(),
metadata_openstack.hostname.unwrap(),
);
} else if metadata_ec2.hostname.is_some() {
out.insert(
"OPENSTACK_HOSTNAME".to_string(),
metadata_ec2.hostname.unwrap(),
);
}
if metadata_ec2.instance_id.is_some() {
out.insert(
"OPENSTACK_INSTANCE_ID".to_string(),
metadata_ec2.instance_id.unwrap(),
);
}
if metadata_ec2.instance_type.is_some() {
out.insert(
"OPENSTACK_INSTANCE_TYPE".to_string(),
metadata_ec2.instance_type.unwrap(),
);
}
if metadata_ec2.local_ipv4.is_some() {
out.insert(
"OPENSTACK_IPV4_LOCAL".to_string(),
metadata_ec2.local_ipv4.unwrap(),
);
}
if metadata_ec2.public_ipv4.is_some() {
out.insert(
"OPENSTACK_IPV4_PUBLIC".to_string(),
metadata_ec2.public_ipv4.unwrap(),
);
}
Ok(out)
}

fn hostname(&self) -> Result<Option<String>> {
let metadata: MetadataEc2JSON = self.read_metadata_ec2()?;
Ok(metadata.hostname)
}

fn ssh_keys(&self) -> Result<Vec<PublicKey>> {
self.fetch_publickeys()
}

fn networks(&self) -> Result<Vec<network::Interface>> {
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 OpenstackConfigDrive {
fn drop(&mut self) {
if self.temp_dir.is_some() {
if let Err(e) = crate::util::unmount(&self.drive_path, 3) {
error!("failed to cleanup OpenStack config-drive: {:?}", e);
};
}
}
}

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

#[test]
fn test_attributes_ec2() {
let fixture =
File::open("./tests/fixtures/openstack-config-drive/ec2/meta-data.json").unwrap();
let bufrd = BufReader::new(fixture);
let parsed = OpenstackConfigDrive::parse_metadata_ec2(bufrd).unwrap();

assert_eq!(
parsed.hostname.unwrap_or_default(),
"abai-fcos-afterburn-test"
);
assert_eq!(parsed.instance_id.unwrap_or_default(), "i-022da7a2");
assert_eq!(parsed.instance_type.unwrap_or_default(), "m1.small");
assert_eq!(parsed.local_ipv4.unwrap_or_default(), "10.0.151.35");
assert_eq!(parsed.public_ipv4.unwrap_or_default(), "");
}

#[test]
fn test_attributes_openstack() {
let fixture =
File::open("./tests/fixtures/openstack-config-drive/openstack/meta_data.json").unwrap();
let bufrd = BufReader::new(fixture);
let parsed = OpenstackConfigDrive::parse_metadata_openstack(bufrd).unwrap();

let expect = maplit::hashmap! {
"mykey".to_string() => "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDYVEprvtYJXVOBN0XNKVVRNCRX6BlnNbI+USLGais1sUWPwtSg7z9K9vhbYAPUZcq8c/s5S9dg5vTHbsiyPCIDOKyeHba4MUJq8Oh5b2i71/3BISpyxTBH/uZDHdslW2a+SrPDCeuMMoss9NFhBdKtDkdG9zyi0ibmCP6yMdEX8Q== Generated by Nova\n".to_string(),
};

assert_eq!(
parsed.hostname.unwrap_or_default(),
"abai-fcos-afterburn-test"
);
assert_eq!(parsed.availability_zone.unwrap_or_default(), "nova");
assert_eq!(parsed.public_keys.unwrap_or_default(), expect);
}

#[test]
fn test_ssh_keys() {
let fixture =
File::open("./tests/fixtures/openstack-config-drive/openstack/meta_data.json").unwrap();
let bufrd = BufReader::new(fixture);
let parsed = OpenstackConfigDrive::parse_metadata_openstack(bufrd).unwrap();

let expect = maplit::hashmap! {
"mykey".to_string() => "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDYVEprvtYJXVOBN0XNKVVRNCRX6BlnNbI+USLGais1sUWPwtSg7z9K9vhbYAPUZcq8c/s5S9dg5vTHbsiyPCIDOKyeHba4MUJq8Oh5b2i71/3BISpyxTBH/uZDHdslW2a+SrPDCeuMMoss9NFhBdKtDkdG9zyi0ibmCP6yMdEX8Q== Generated by Nova\n".to_string(),
};

assert_eq!(parsed.public_keys.unwrap_or_default(), expect);
}
}
16 changes: 16 additions & 0 deletions src/providers/openstack/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,20 @@

//! openstack metadata fetcher

use crate::errors;
use crate::providers;
use configdrive::OpenstackConfigDrive;
use network::OpenstackProviderNetwork;
use slog_scope::warn;

pub mod configdrive;
pub mod network;

pub fn try_config_drive_else_network() -> errors::Result<Box<dyn providers::MetadataProvider>> {
if let Ok(config_drive) = OpenstackConfigDrive::try_new() {
Ok(Box::new(config_drive))
} else {
warn!("failed to utilize config-drive, using the metadata service API instead");
Ok(Box::new(OpenstackProviderNetwork::try_new()?))
}
}
Loading

0 comments on commit d0ea1c4

Please sign in to comment.