Skip to content
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 docs/platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ The following platforms are supported, with a different set of features availabl
- Attributes
- First-boot check-in
- SSH Keys
* powervs
- Attributes
- SSH keys
* vmware
- Custom network command-line arguments
* vultr
Expand Down
3 changes: 3 additions & 0 deletions docs/usage/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ Cloud providers with supported metadata endpoints and their respective attribute
- AFTERBURN_PACKET_IPV4_PRIVATE_GATEWAY_0
- AFTERBURN_PACKET_IPV6_PUBLIC_0
- AFTERBURN_PACKET_IPV6_PUBLIC_GATEWAY_0
* powervs
- AFTERBURN_POWERVS_INSTANCE_ID
- AFTERBURN_POWERVS_LOCAL_HOSTNAME
* vultr
- AFTERBURN_VULTR_HOSTNAME
- AFTERBURN_VULTR_INSTANCE_ID
Expand Down
2 changes: 2 additions & 0 deletions src/metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use crate::providers::microsoft::azurestack::AzureStack;
use crate::providers::openstack;
use crate::providers::openstack::network::OpenstackProviderNetwork;
use crate::providers::packet::PacketProvider;
use crate::providers::powervs::PowerVSProvider;
use crate::providers::vmware::VmwareProvider;
use crate::providers::vultr::VultrProvider;

Expand Down Expand Up @@ -61,6 +62,7 @@ pub fn fetch_metadata(provider: &str) -> Result<Box<dyn providers::MetadataProvi
"openstack" => openstack::try_config_drive_else_network(),
"openstack-metadata" => box_result!(OpenstackProviderNetwork::try_new()?),
"packet" => box_result!(PacketProvider::try_new()?),
"powervs" => box_result!(PowerVSProvider::try_new()?),
"vmware" => box_result!(VmwareProvider::try_new()?),
"vultr" => box_result!(VultrProvider::try_new()?),
_ => bail!("unknown provider '{}'", provider),
Expand Down
1 change: 1 addition & 0 deletions src/providers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub mod ibmcloud_classic;
pub mod microsoft;
pub mod openstack;
pub mod packet;
pub mod powervs;
pub mod vmware;
pub mod vultr;

Expand Down
239 changes: 239 additions & 0 deletions src/providers/powervs/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
//! Metadata fetcher for PowerVS instances.
//!
//! This provider supports the Power Virtual Server infrastructure type on IBMCloud.
//! It provides a config-drive as the only metadata source, whose layout
//! follows the `cloud-init ConfigDrive v2` [datasource][configdrive], with
//! the following details:
//! - disk filesystem label is `config-2` (lowercase)
//! - filesystem is `iso9660`
//! - drive contains a single directory at `/openstack/latest/`
//! - content is exposed as JSON files called `meta_data.json`.
//!
//! configdrive: https://cloudinit.readthedocs.io/en/latest/topics/datasources/configdrive.html

use anyhow::{bail, Context, Result};
use openssh_keys::PublicKey;
use serde::Deserialize;
use slog_scope::warn;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use tempfile::TempDir;

use crate::network;
use crate::providers::MetadataProvider;

// Filesystem label for the Config Drive.
static CONFIG_DRIVE_FS_LABEL: &str = "config-2";

// Filesystem type for the Config Drive.
static CONFIG_DRIVE_FS_TYPE: &str = "iso9660";

///PowerVS provider.
#[derive(Debug)]
pub struct PowerVSProvider {
/// Path to the top directory of the mounted config-drive.
drive_path: PathBuf,
/// Temporary directory for own mountpoint.
temp_dir: TempDir,
}

/// Partial object for `meta_data.json`
#[derive(Debug, Deserialize)]
pub struct MetaDataJSON {
/// Fully-Qualified Domain Name (FQDN).
#[serde(rename = "hostname")]
pub fqdn: String,
/// Local hostname.
#[serde(rename = "name")]
pub local_hostname: String,
/// Instance ID (UUID).
#[serde(rename = "uuid")]
pub instance_id: String,
/// SSH public keys.
pub public_keys: Option<HashMap<String, String>>,
}

impl PowerVSProvider {
/// Try to build a new provider client.
///
/// This internally tries to mount (and own) the config-drive.
pub fn try_new() -> Result<Self> {
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_FS_LABEL),
target.path(),
CONFIG_DRIVE_FS_TYPE,
3, // maximum retries
)?;

let provider = Self {
drive_path: target.path().to_owned(),
temp_dir: target,
};
Ok(provider)
}

/// Return the path to the metadata directory.
fn metadata_dir(&self) -> PathBuf {
let drive = self.drive_path.clone();
drive.join("openstack").join("latest")
}

/// Read and parse metadata file.
fn read_metadata(&self) -> Result<MetaDataJSON> {
let filename = self.metadata_dir().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(bufrd)
}

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

/// Extract supported metadata values and convert to Afterburn attributes.
///
/// The `AFTERBURN_` prefix is added later on, so it is not part of the
/// key-labels here.
fn known_attributes(metadata: MetaDataJSON) -> Result<HashMap<String, String>> {
if metadata.instance_id.is_empty() {
bail!("empty instance ID");
}

if metadata.local_hostname.is_empty() {
bail!("empty local hostname");
}

let attrs = maplit::hashmap! {
"POWERVS_INSTANCE_ID".to_string() => metadata.instance_id,
"POWERVS_LOCAL_HOSTNAME".to_string() => metadata.local_hostname,

};
Ok(attrs)
}

/// The public key is stored as key:value pair in openstack/latest/meta_data.json file
fn public_keys(metadata: MetaDataJSON) -> Result<Vec<PublicKey>> {
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 PowerVSProvider {
fn attributes(&self) -> Result<HashMap<String, String>> {
let metadata = self.read_metadata()?;
Self::known_attributes(metadata)
}

fn hostname(&self) -> Result<Option<String>> {
let metadata = self.read_metadata()?;
let hostname = if metadata.local_hostname.is_empty() {
None
} else {
Some(metadata.local_hostname)
};
Ok(hostname)
}

fn ssh_keys(&self) -> Result<Vec<PublicKey>> {
let metadata = self.read_metadata()?;
Self::public_keys(metadata)
}

fn networks(&self) -> Result<Vec<network::Interface>> {
warn!("network interfaces metadata requested, but not supported on this platform");
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 PowerVSProvider {
fn drop(&mut self) {
if let Err(e) = crate::util::unmount(
self.temp_dir.path(),
3, // maximum retries
) {
slog_scope::error!("failed to unmount powervs config-drive: {}", e);
};
}
}

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

#[test]
fn test_powervs_basic_attributes() {
let metadata = r#"
{
"hostname": "test_instance-powervs.foo.cloud",
"name": "test_instance-powervs",
"uuid": "41b4fb82-ca29-11eb-b8bc-0242ac130003"
}
"#;

let bufrd = BufReader::new(Cursor::new(metadata));
let parsed = PowerVSProvider::parse_metadata(bufrd).unwrap();
assert_eq!(parsed.instance_id, "41b4fb82-ca29-11eb-b8bc-0242ac130003",);
assert_eq!(parsed.local_hostname, "test_instance-powervs",);

let attrs = PowerVSProvider::known_attributes(parsed).unwrap();
assert_eq!(attrs.len(), 2);
assert_eq!(
attrs.get("POWERVS_INSTANCE_ID"),
Some(&"41b4fb82-ca29-11eb-b8bc-0242ac130003".to_string())
);
assert_eq!(
attrs.get("POWERVS_LOCAL_HOSTNAME"),
Some(&"test_instance-powervs".to_string())
);
}

#[test]
fn test_powervs_parse_metadata_json() {
let fixture = File::open("./tests/fixtures/powervs/meta_data.json").unwrap();
let bufrd = BufReader::new(fixture);
let parsed = PowerVSProvider::parse_metadata(bufrd).unwrap();

assert!(!parsed.instance_id.is_empty());
assert!(!parsed.local_hostname.is_empty());
assert!(!parsed.public_keys.is_none());
Comment thread
Prashanth684 marked this conversation as resolved.
}

#[test]
fn test_powervs_ssh_keys() {
let fixture = File::open("./tests/fixtures/powervs/meta_data.json").unwrap();
let bufrd = BufReader::new(fixture);
let parsed = PowerVSProvider::parse_metadata(bufrd).unwrap();
let keys = PowerVSProvider::public_keys(parsed).unwrap();
let expect = PublicKey::parse("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmMuiypdqqftqhrQeBTjOhcgyARvylZMLiH+6nCvi5Lv5M7evAnvvG3Hz4rbjbbqoVgSCIdAEb4PuttiCdwE6UyAl0TYAydOVPx7l87BlaucTEqDFbXkQB+yyUmzodllCpWAMUmxwvJB/ntFrC6rP0K0kKxx4SESvozutwM2X5oH3LNHcYI1xgKIMF9VMJLkkM0rLo8Fmj6mWF5KtbU7vS7JJPvLTCRhW5TYrqvhHKuIS6KBtj3GJqvRt+it8AsIb6/RUaji68Mt7W41UrmFSPt8bxJMdE/xKGMcFQjamURPSCHx7z8/pr2/pv2QbQF76FO7lRdPH3f542uAkOOpO1 user1@user1-MacBook-Pro-2.local").unwrap();

assert_eq!(keys.len(), 1);
assert_eq!(keys[0], expect);
}
}
32 changes: 32 additions & 0 deletions tests/fixtures/powervs/meta_data.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"admin_pass": "oSfw8JEKHvBo",
"random_seed": "YUQzy5FVwllOEHrRfvB9rims2ZtotnhJz/f7EVXIknrhh2htbSMxYt1DKxnTzLzWm+tfRjcvWyjie4aU4lIVFrwFgsFmDoAcBIjBID20QVObzKaN0rfrGtdwojyiu+7uGWuQlojoBR/m5pzO1HFcdexEdOSIE+EpU7WIHnS6K948rpH8kERslIz7W6kJixjTpsNanidsvV4DRiSoYdI3wtxyVxBt7ZSzSyuzUIPkbRQ5D/XE/DLs+B8ChRGe9apGCcPvpDRPvot3UUvmwBvtZtaNfH99poKp+j7GOjINT5tc+Wypvgls2EoI/klekzrIOqLmRx/q6UpvvmpoP2PhKT687WMoWNg4uS4d0VbxS2UkydSZoUVCSQmO9O95RdD/jYqXY6q5o7qdr3TZZFf4s1gdoXltb70oQzi7wo4q5Z4QsOsdZdNz5BZ5vtQ06+7neS8ppP5cD6OkQXm+d9bBBWElG5yTeN/zyURpgGdsQlocZzkVlhdVRW9wyeGeN6TGiIn3EkzOJdb6ypZ50iNAnnTN5+zQpt0Y3f1up2/Ppy+nBNzjGdGAHDl4cc+2ri1IiNAIiG2Ta/kcNghNx3+XqUuYsK97e9grW+qXDa3gkeHqclOu9753GJKXeBR5gcH6O9PpPWRQ6T6slltsOPUnW3K3E54Il87fbxd73WSPN/8=",
"uuid": "1c0c0bb6-8dcd-4966-ad80-946b871814f1",
"availability_zone": "s922",
"keys": [
{
"data": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmMuiypdqqftqhrQeBTjOhcgyARvylZMLiH+6nCvi5Lv5M7evAnvvG3Hz4rbjbbqoVgSCIdAEb4PuttiCdwE6UyAl0TYAydOVPx7l87BlaucTEqDFbXkQB+yyUmzodllCpWAMUmxwvJB/ntFrC6rP0K0kKxx4SESvozutwM2X5oH3LNHcYI1xgKIMF9VMJLkkM0rLo8Fmj6mWF5KtbU7vS7JJPvLTCRhW5TYrqvhHKuIS6KBtj3GJqvRt+it8AsIb6/RUaji68Mt7W41UrmFSPt8bxJMdE/xKGMcFQjamURPSCHx7z8/pr2/pv2QbQF76FO7lRdPH3f542uAkOOpO1 user1@user1-MacBook-Pro-2.local",
"type": "ssh",
"name": "65b64c1f1c29460e8c2e4bbfbd893c2c_7c6cd5d4-a7aa-4d07-bb89-94f274bfeed0_user1-pub-key"
}
],
"hostname": "test-tf-pvm.power-iaas.cloud.ibm.com",
"launch_index": 0,
"devices": [],
"meta": {
"ibmiDBQ": "false",
"ibmiCSS": "false",
"storage_pool": "Tier1-Flash-2",
"ibmiRDS": "false",
"ibmiPHA": "false"
},
"public_keys": {
"65b64c1f1c29460e8c2e4bbfbd893c2c_7c6cd5d4-a7aa-4d07-bb89-94f274bfeed0_user1-pub-key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmMuiypdqqftqhrQeBTjOhcgyARvylZMLiH+6nCvi5Lv5M7evAnvvG3Hz4rbjbbqoVgSCIdAEb4PuttiCdwE6UyAl0TYAydOVPx7l87BlaucTEqDFbXkQB+yyUmzodllCpWAMUmxwvJB/ntFrC6rP0K0kKxx4SESvozutwM2X5oH3LNHcYI1xgKIMF9VMJLkkM0rLo8Fmj6mWF5KtbU7vS7JJPvLTCRhW5TYrqvhHKuIS6KBtj3GJqvRt+it8AsIb6/RUaji68Mt7W41UrmFSPt8bxJMdE/xKGMcFQjamURPSCHx7z8/pr2/pv2QbQF76FO7lRdPH3f542uAkOOpO1 user1@user1-MacBook-Pro-2.local"
},
"project_id": "15ea5428420d4d47be42e19332645a18",
"network_config": {
"content_path": "/content/0000",
"name": "network_config"
},
"name": "test-tf-pVM"
}