From 4f52a9e15c794be5af3f3ae9ab03501d06853d48 Mon Sep 17 00:00:00 2001 From: Allen Bai Date: Wed, 22 Jul 2020 16:28:39 -0400 Subject: [PATCH] openstack: use config-drive by default This changes afterburn to use config-drive as default for scraping metadata and then try metadata API if config-drive is not available. Also changes the name of the platform from `openstack-metadata` to `openstack`. Closes: https://github.com/coreos/fedora-coreos-tracker/issues/422 Signed-off-by: Allen Bai --- src/metadata.rs | 8 +- src/providers/openstack/configdrive.rs | 167 +++++++++++++++++++++++++ src/providers/openstack/mod.rs | 1 + src/providers/openstack/network.rs | 24 ++-- 4 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 src/providers/openstack/configdrive.rs diff --git a/src/metadata.rs b/src/metadata.rs index b07782cb..c1683047 100644 --- a/src/metadata.rs +++ b/src/metadata.rs @@ -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::configdrive::OpenstackConfigDrive; +use crate::providers::openstack::network::OpenstackProviderNetwork; use crate::providers::packet::PacketProvider; #[cfg(feature = "cl-legacy")] use crate::providers::vagrant_virtualbox::VagrantVirtualboxProvider; @@ -62,7 +63,10 @@ pub fn fetch_metadata(provider: &str) -> errors::Result box_result!(IBMGen2Provider::try_new()?), // IBM Cloud - Classic infrastructure. "ibmcloud-classic" => box_result!(IBMClassicProvider::try_new()?), - "openstack-metadata" => box_result!(OpenstackProvider::try_new()?), + "openstack" => match OpenstackConfigDrive::try_new() { + Ok(result) => box_result!(result), + Err(_) => box_result!(OpenstackProviderNetwork::try_new()?), + }, "packet" => box_result!(PacketProvider::try_new()?), #[cfg(feature = "cl-legacy")] "vagrant-virtualbox" => box_result!(VagrantVirtualboxProvider::new()), diff --git a/src/providers/openstack/configdrive.rs b/src/providers/openstack/configdrive.rs new file mode 100644 index 00000000..9c352a7c --- /dev/null +++ b/src/providers/openstack/configdrive.rs @@ -0,0 +1,167 @@ +//! configdrive metadata fetcher for OpenStack +//! reference: https://docs.openstack.org/nova/latest/user/metadata.html + +use std::collections::HashMap; +use std::fs::File; +use std::io::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"; + +/// 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, +} + +impl OpenstackConfigDrive { + /// Try to build a new provider client. + /// + /// This internally tries to mount (and own) the config-drive. + pub fn try_new() -> Result { + // Short-circuit if the config-drive is already mounted. + let path = Path::new("/media/ConfigDrive/openstack/latest/"); + if path.exists() { + return Ok(OpenstackConfigDrive { + temp_dir: None, + drive_path: PathBuf::from("/media/ConfigDrive/"), + }); + } + + // Otherwise, try and mount with the label. + 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(), + "iso9660", + 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") + } + + // The metadata is stored as key:value pair in ec2/meta-data.json file + fn fetch_value(&self, key: &str) -> Result> { + let filename = self.metadata_dir("ec2").join("meta-data.json"); + + if !filename.exists() { + return Ok(None); + } + + let mut file = + File::open(&filename).chain_err(|| format!("failed to open file '{:?}'", filename))?; + + let mut contents = String::new(); + file.read_to_string(&mut contents) + .chain_err(|| format!("failed to read from file '{:?}'", filename))?; + let contents_json: serde_json::Value = serde_json::from_str(&contents)?; + + if let Some(value) = contents_json[key].as_str() { + Ok(Some(value.to_owned())) + } else { + Ok(None) + } + } + + fn fetch_publickeys(&self) -> Result> { + let filename = self.metadata_dir("openstack").join("meta_data.json"); + let mut file = + File::open(&filename).chain_err(|| format!("failed to open file '{:?}'", filename))?; + + let mut contents = String::new(); + file.read_to_string(&mut contents) + .chain_err(|| format!("failed to read from file '{:?}'", filename))?; + let contents_json: serde_json::Value = serde_json::from_str(&contents)?; + + if let Some(public_keys_map) = contents_json["public_keys"].as_object() { + let public_keys_vec: Vec<&std::string::String> = + public_keys_map.keys().collect(); + let mut out = vec![]; + for key in public_keys_vec { + let key = PublicKey::parse(&key)?; + out.push(key); + } + Ok(out) + } else { + Ok(vec![]) + } + } +} + +impl MetadataProvider for OpenstackConfigDrive { + fn attributes(&self) -> Result> { + let mut out = HashMap::with_capacity(5); + let add_value = |map: &mut HashMap<_, _>, key: &str, name| -> Result<()> { + let value = self.fetch_value(name)?; + + if let Some(value) = value { + map.insert(key.to_string(), value); + } + + Ok(()) + }; + + add_value(&mut out, "OPENSTACK_HOSTNAME", "hostname")?; + add_value(&mut out, "OPENSTACK_INSTANCE_ID", "instance-id")?; + add_value(&mut out, "OPENSTACK_INSTANCE_TYPE", "instance-type")?; + add_value(&mut out, "OPENSTACK_IPV4_LOCAL", "local-ipv4")?; + add_value(&mut out, "OPENSTACK_IPV4_PUBLIC", "public-ipv4")?; + + Ok(out) + } + + fn hostname(&self) -> Result> { + Ok(None) + } + + fn ssh_keys(&self) -> Result> { + self.fetch_publickeys() + } + + fn networks(&self) -> Result> { + Ok(vec![]) + } + + fn virtual_network_devices(&self) -> Result> { + 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); + }; + } + } +} diff --git a/src/providers/openstack/mod.rs b/src/providers/openstack/mod.rs index 3af9e984..76523e9d 100644 --- a/src/providers/openstack/mod.rs +++ b/src/providers/openstack/mod.rs @@ -14,4 +14,5 @@ //! openstack metadata fetcher +pub mod configdrive; pub mod network; diff --git a/src/providers/openstack/network.rs b/src/providers/openstack/network.rs index 9d07b5af..a557bebb 100644 --- a/src/providers/openstack/network.rs +++ b/src/providers/openstack/network.rs @@ -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 { +impl OpenstackProviderNetwork { + pub fn try_new() -> Result { let client = retry::Client::try_new()?; - Ok(OpenstackProvider { client }) + Ok(OpenstackProviderNetwork { client }) } fn endpoint_for(key: &str) -> String { @@ -30,7 +30,10 @@ impl OpenstackProvider { fn fetch_keys(&self) -> Result> { let keys_list: Option = 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 { @@ -43,7 +46,7 @@ impl OpenstackProvider { .client .get( retry::Raw, - OpenstackProvider::endpoint_for(&format!( + OpenstackProviderNetwork::endpoint_for(&format!( "public-keys/{}/openssh-key", tokens[0] )), @@ -57,14 +60,14 @@ impl OpenstackProvider { } } -impl MetadataProvider for OpenstackProvider { +impl MetadataProvider for OpenstackProviderNetwork { fn attributes(&self) -> Result> { let mut out = HashMap::with_capacity(4); 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); @@ -83,7 +86,10 @@ impl MetadataProvider for OpenstackProvider { fn hostname(&self) -> Result> { self.client - .get(retry::Raw, OpenstackProvider::endpoint_for("hostname")) + .get( + retry::Raw, + OpenstackProviderNetwork::endpoint_for("hostname"), + ) .send() }