From bb31a6135c107d2c09eae1a24c6fb22299b6156d 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. Previously supported platform id 'openstack-metadata' will continue to be supported. Closes: https://github.com/coreos/fedora-coreos-tracker/issues/422 Signed-off-by: Allen Bai --- README.md | 3 + docs/usage/attributes.md | 6 + src/metadata.rs | 6 +- src/providers/openstack/configdrive.rs | 247 ++++++++++++++++++ src/providers/openstack/mod.rs | 19 ++ src/providers/openstack/network.rs | 26 +- .../openstack-config-drive/ec2/meta-data.json | 28 ++ .../openstack/meta_data.json | 14 + 8 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 src/providers/openstack/configdrive.rs create mode 100644 tests/fixtures/openstack-config-drive/ec2/meta-data.json create mode 100644 tests/fixtures/openstack-config-drive/openstack/meta_data.json diff --git a/README.md b/README.md index b6844ca9..fb61f2ce 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/usage/attributes.md b/docs/usage/attributes.md index 4ad4a8c7..c8fbb895 100644 --- a/docs/usage/attributes.md +++ b/docs/usage/attributes.md @@ -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 diff --git a/src/metadata.rs b/src/metadata.rs index b07782cb..d3ac4499 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; +use crate::providers::openstack::network::OpenstackProviderNetwork; use crate::providers::packet::PacketProvider; #[cfg(feature = "cl-legacy")] use crate::providers::vagrant_virtualbox::VagrantVirtualboxProvider; @@ -62,7 +63,8 @@ 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" => 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()), diff --git a/src/providers/openstack/configdrive.rs b/src/providers/openstack/configdrive.rs new file mode 100644 index 00000000..7d7a42a7 --- /dev/null +++ b/src/providers/openstack/configdrive.rs @@ -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, + /// Instance type. + #[serde(rename = "instance-type")] + pub instance_type: Option, + /// Local IPV4. + #[serde(rename = "local-ipv4")] + pub local_ipv4: Option, + /// Public IPV4. + #[serde(rename = "public-ipv4")] + pub public_ipv4: Option, +} + +/// Partial object for openstack `meta_data.json` +#[derive(Debug, Deserialize)] +pub struct MetadataOpenstackJSON { + /// Availability zone. + pub availability_zone: Option, + /// Local hostname. + pub hostname: Option, + /// SSH public keys. + pub public_keys: Option>, +} + +/// 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 { + 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(input: BufReader) -> Result { + 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(input: BufReader) -> Result { + 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 { + 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 { + 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> { + 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> { + 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> { + let metadata: MetadataOpenstackJSON = self.read_metadata_openstack()?; + Ok(metadata.hostname) + } + + 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); + }; + } + } +} + +#[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); + } +} diff --git a/src/providers/openstack/mod.rs b/src/providers/openstack/mod.rs index 3af9e984..068da2fb 100644 --- a/src/providers/openstack/mod.rs +++ b/src/providers/openstack/mod.rs @@ -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> { + 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()?)) + } +} diff --git a/src/providers/openstack/network.rs b/src/providers/openstack/network.rs index 9d07b5af..6f4ec280 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 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); @@ -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() } diff --git a/tests/fixtures/openstack-config-drive/ec2/meta-data.json b/tests/fixtures/openstack-config-drive/ec2/meta-data.json new file mode 100644 index 00000000..1c1c4437 --- /dev/null +++ b/tests/fixtures/openstack-config-drive/ec2/meta-data.json @@ -0,0 +1,28 @@ +{ + "reservation-id": "r-bxkh1822", + "security-groups": [ + "wide-open", + "default", + "wide-open", + "default" + ], + "public-ipv4": "", + "ami-manifest-path": "FIXME", + "instance-type": "m1.small", + "instance-id": "i-022da7a2", + "local-ipv4": "10.0.151.35", + "local-hostname": "abai-fcos-afterburn-test", + "placement": { + "availability-zone": "nova" + }, + "ami-launch-index": 0, + "public-hostname": "abai-fcos-afterburn-test", + "hostname": "abai-fcos-afterburn-test", + "ami-id": null, + "instance-action": "none", + "block-device-mapping": { + "ami": "vda", + "ebs0": "/dev/vda", + "root": "/dev/vda" + } +} diff --git a/tests/fixtures/openstack-config-drive/openstack/meta_data.json b/tests/fixtures/openstack-config-drive/openstack/meta_data.json new file mode 100644 index 00000000..96d4dfe9 --- /dev/null +++ b/tests/fixtures/openstack-config-drive/openstack/meta_data.json @@ -0,0 +1,14 @@ +{ + "admin_pass": "Qs634jUssypC", + "random_seed": "stYL6s4C4tTyttfG9s7QzngiR/2X7X1fGkWl7Ok/bfl5dCevFBDYh5BvGnzd4/vNYWAjtDpe4/5mTogmF0uew4eMD4p96mLtQYjj9JaQRd18xvyULk1jJOBr+G3BVtyOSqDuBwJ6e6fsH2bmbhW6RSSq80t1tqo/qrhJP8i/jrH2DS7iQ8RvmZjbYJ1zSxyV2axNSWwie //WQXbB3sffb3+sDYhoAc+wUjGLK7MuPhNlaJC193Hvuqd7CVTYWuh4lPEjTe7QB4oGBNOHkfF4xixiKXns6UBtFwOJh4xiHKOL2Y4hz9gGDO1N0XprxZManAXc/BzW8AoTMWvsMzYgG6ZxF2kc60OP5x6y8naBV1GxAmJvURBjxZ245pHAmJ4ruduy0XKnvZQO7EIaE7xKxm8Va4f7rKSvvhUFXNURAlP3RrF1Nu4XRirXST0SnoYrwwljCJNG2ywa1VP46GgN/Zj6brUzVjXN8YiMYObgkcnkgNWTBmYkn2zniENVaWSpCpr+RDC8iEkZIAVZA9/uBHHTbsfmzjzvdWtE6woRkRCC9lwTsuwAmmbrJa3bdACQ+kACVy7VSx7RdUyoYQw0k5bWsZchZzxCp2KnDn60Qi1OL5oej0Wn9ZUczHy+P2vDl1GNhqvPFK513Oa+tqmw8NnJNJftgg9fCmgMbZjWSlA=", + "uuid": "b3c7f4de-da0b-44ae-a42c-fa3806b61d7f", + "availability_zone": "nova", + "hostname": "abai-fcos-afterburn-test", + "launch_index": 0, + "devices": [], + "project_id": "8aa32be0972144a99ce5001d0f07905f", + "name": "abai-fcos-afterburn-test", + "public_keys": { + "mykey": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDYVEprvtYJXVOBN0XNKVVRNCRX6BlnNbI+USLGais1sUWPwtSg7z9K9vhbYAPUZcq8c/s5S9dg5vTHbsiyPCIDOKyeHba4MUJq8Oh5b2i71/3BISpyxTBH/uZDHdslW2a+SrPDCeuMMoss9NFhBdKtDkdG9zyi0ibmCP6yMdEX8Q== Generated by Nova\n" + } +}