/
githubmirror
/
afterburn
Обзор
Документация
Войти
/
githubmirror
/
afterburn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/providers/openstack/configdrive.rs
285 строк
10 KB
Rolv Apneseth
*: drop maplit dependency
05 авг 2026, 14:39
05 авг 2026, 14:39
84d673e
Код
Авторство
О чём код?
//! 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 anyhow::{Context, Result}; use openssh_keys::PublicKey; use slog_scope::{error, warn}; use tempfile::TempDir; 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 { /// Instance ID. pub uuid: Option<String>, /// Availability zone. #[allow(dead_code)] 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() .context("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).context("failed to 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).context("failed to parse JSON metadata") } /// The metadata is stored as key:value pair in ec2/latest/meta-data.json file fn read_metadata_ec2(&self) -> Result<Option<MetadataEc2JSON>> { use std::io::ErrorKind::NotFound; let filename = self.metadata_dir("ec2").join("meta-data.json"); let file = match File::open(&filename) { Ok(file) => file, Err(e) if e.kind() == NotFound => return Ok(None), Err(e) => return Err(e).with_context(|| format!("failed to open file '{filename:?}'")), }; let bufrd = BufReader::new(file); Self::parse_metadata_ec2(bufrd) .with_context(|| format!("failed to parse file '{filename:?}'")) .map(Some) } /// 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).with_context(|| format!("failed to open file '{filename:?}'"))?; let bufrd = BufReader::new(file); Self::parse_metadata_openstack(bufrd) .with_context(|| 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).with_context(|| format!("failed to open file '{filename:?}'"))?; let bufrd = BufReader::new(file); let metadata: MetadataOpenstackJSON = Self::parse_metadata_openstack(bufrd) .with_context(|| 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(6); 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(uuid) = metadata_openstack.uuid { out.insert("OPENSTACK_INSTANCE_UUID".to_string(), uuid); } let Some(metadata_ec2) = self.read_metadata_ec2()? else { return Ok(out); }; 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 = HashMap::from([ ("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.uuid.unwrap_or_default(), "b3c7f4de-da0b-44ae-a42c-fa3806b61d7f" ); 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 = HashMap::from([ ("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); } #[test] fn test_attributes_openstack_without_ec2() { let provider = OpenstackConfigDrive { drive_path: PathBuf::from( "./tests/fixtures/openstack-config-drive/openstack-config-drive2", ), temp_dir: None, }; let result = provider.attributes(); assert!(result.is_ok(), "Expected Ok, got Err: {:?}", result.err()); let attributes = result.unwrap(); assert_eq!( attributes .get("OPENSTACK_INSTANCE_UUID") .unwrap_or(&String::new()), "b3f43d9c-9198-4cd4-ac9f-bed14960794b" ); } }