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

openstack: use config-drive by default and metadata API as fallback #462

Merged
merged 1 commit into from
Aug 3, 2020
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 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
6 changes: 4 additions & 2 deletions src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ 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::openstack::network::OpenstackProviderNetwork;
use crate::providers::packet::PacketProvider;
#[cfg(feature = "cl-legacy")]
use crate::providers::vagrant_virtualbox::VagrantVirtualboxProvider;
Expand Down Expand Up @@ -62,7 +63,8 @@ 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::try_config_drive_else_network(),
"openstack-metadata" => box_result!(OpenstackProviderNetwork::try_new()?),
"packet" => box_result!(PacketProvider::try_new()?),
#[cfg(feature = "cl-legacy")]
"vagrant-virtualbox" => box_result!(VagrantVirtualboxProvider::new()),
Expand Down
247 changes: 247 additions & 0 deletions src/providers/openstack/configdrive.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
//! 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 {
/// 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)
lucab marked this conversation as resolved.
Show resolved Hide resolved
.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");
zonggen marked this conversation as resolved.
Show resolved Hide resolved
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>> {
zonggen marked this conversation as resolved.
Show resolved Hide resolved
let mut out = HashMap::with_capacity(5);
let metadata_ec2: MetadataEc2JSON = self.read_metadata_ec2()?;
let metadata_openstack: MetadataOpenstackJSON = self.read_metadata_openstack()?;
if let Some(hostname) = metadata_openstack.hostname {
out.insert("OPENSTACK_HOSTNAME".to_string(), hostname);
}
if let Some(instance_id) = metadata_ec2.instance_id {
out.insert("OPENSTACK_INSTANCE_ID".to_string(), instance_id);
}
if let Some(instance_type) = metadata_ec2.instance_type {
out.insert("OPENSTACK_INSTANCE_TYPE".to_string(), instance_type);
}
if let Some(local_ipv4) = metadata_ec2.local_ipv4 {
out.insert("OPENSTACK_IPV4_LOCAL".to_string(), local_ipv4);
}
if let Some(public_ipv4) = metadata_ec2.public_ipv4 {
out.insert("OPENSTACK_IPV4_PUBLIC".to_string(), public_ipv4);
}
Ok(out)
}

fn hostname(&self) -> Result<Option<String>> {
let metadata: MetadataOpenstackJSON = self.read_metadata_openstack()?;
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.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);
}
}
19 changes: 19 additions & 0 deletions src/providers/openstack/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,23 @@

//! 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;

/// Read metadata from the config-drive first then fallback to fetch from metadata server.
///
/// Reference: https://github.com/coreos/fedora-coreos-tracker/issues/422
pub fn try_config_drive_else_network() -> errors::Result<Box<dyn providers::MetadataProvider>> {
lucab marked this conversation as resolved.
Show resolved Hide resolved
if let Ok(config_drive) = OpenstackConfigDrive::try_new() {
Ok(Box::new(config_drive))
} else {
warn!("failed to locate config-drive, using the metadata service API instead");
Ok(Box::new(OpenstackProviderNetwork::try_new()?))
}
}
26 changes: 16 additions & 10 deletions src/providers/openstack/network.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ use crate::retry;
const URL: &str = "http://169.254.169.254/latest/meta-data";

#[derive(Clone, Debug)]
pub struct OpenstackProvider {
pub struct OpenstackProviderNetwork {
client: retry::Client,
}

impl OpenstackProvider {
pub fn try_new() -> Result<OpenstackProvider> {
impl OpenstackProviderNetwork {
pub fn try_new() -> Result<OpenstackProviderNetwork> {
let client = retry::Client::try_new()?;
Ok(OpenstackProvider { client })
Ok(OpenstackProviderNetwork { client })
}

fn endpoint_for(key: &str) -> String {
Expand All @@ -30,7 +30,10 @@ impl OpenstackProvider {
fn fetch_keys(&self) -> Result<Vec<String>> {
let keys_list: Option<String> = self
.client
.get(retry::Raw, OpenstackProvider::endpoint_for("public-keys"))
.get(
retry::Raw,
OpenstackProviderNetwork::endpoint_for("public-keys"),
)
.send()?;
let mut keys = Vec::new();
if let Some(keys_list) = keys_list {
Expand All @@ -43,7 +46,7 @@ impl OpenstackProvider {
.client
.get(
retry::Raw,
OpenstackProvider::endpoint_for(&format!(
OpenstackProviderNetwork::endpoint_for(&format!(
"public-keys/{}/openssh-key",
tokens[0]
)),
Expand All @@ -57,14 +60,14 @@ impl OpenstackProvider {
}
}

impl MetadataProvider for OpenstackProvider {
impl MetadataProvider for OpenstackProviderNetwork {
fn attributes(&self) -> Result<HashMap<String, String>> {
let mut out = HashMap::with_capacity(4);
let mut out = HashMap::with_capacity(5);

let add_value = |map: &mut HashMap<_, _>, key: &str, name| -> Result<()> {
let value = self
.client
.get(retry::Raw, OpenstackProvider::endpoint_for(name))
.get(retry::Raw, OpenstackProviderNetwork::endpoint_for(name))
.send()?;
if let Some(value) = value {
map.insert(key.to_string(), value);
Expand All @@ -83,7 +86,10 @@ impl MetadataProvider for OpenstackProvider {

fn hostname(&self) -> Result<Option<String>> {
self.client
.get(retry::Raw, OpenstackProvider::endpoint_for("hostname"))
.get(
retry::Raw,
OpenstackProviderNetwork::endpoint_for("hostname"),
)
.send()
}

Expand Down
Loading