Skip to content

Commit

Permalink
Add PowerVS provider
Browse files Browse the repository at this point in the history
As part of supporting Power Virtual server as a platform (coreos/fedora-coreos-tracker#817), Afterburn will be used to set the hostname attributes and possibly the ssh-keys.

This provider is very similar to the IBMCloud classic provider with the exception of the filesystem for the config drive which is `iso9660` similar to Openstack. The contents of the config drive are usually the meta_data.json and the network_data.json. This provider includes support for:
- hostname
- ssh keys
  • Loading branch information
Prashanth684 committed Jul 16, 2021
1 parent a5d98ae commit 542ee1b
Show file tree
Hide file tree
Showing 6 changed files with 280 additions and 0 deletions.
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());
}

#[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"
}

0 comments on commit 542ee1b

Please sign in to comment.