Skip to content

Commit

Permalink
ibmcloud: add support for SSH keys
Browse files Browse the repository at this point in the history
This allows IBM Cloud VPC Gen 2 SSH keys to be parsed
by Afterburn.

Fixes #472
  • Loading branch information
Eric Larese committed Aug 17, 2020
1 parent af45270 commit 6acfec1
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 0 deletions.
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ error-chain = { version = "^0.12", default-features = false }
hostname = "^0.3"
ipnetwork = "^0.17"
libsystemd = "^0.2.1"
mailparse = "^0.13"
maplit = "^1.0"
mime = "^0.3"
nix = "^0.18"
Expand All @@ -45,6 +46,7 @@ serde = { version = "^1.0", features = [ "derive" ] }
serde-xml-rs = "^0.4"
serde_derive = "^1.0"
serde_json = "^1.0"
serde_yaml = "^0.8"
slog-async = "^2.5"
slog-scope = "^4.3"
slog-term = "^2.6"
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The following platforms are supported, with a different set of features availabl
- SSH Keys
* ibmcloud
- Attributes
- SSH Keys
* ibmcloud-classic
- Attributes
* openstack
Expand Down
81 changes: 81 additions & 0 deletions src/providers/ibmcloud/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@
//!
//! nocloud: https://cloudinit.readthedocs.io/en/latest/topics/datasources/nocloud.html
use openssh_keys::PublicKey;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader, Read};
use std::path::{Path, PathBuf};
use std::str;

use tempfile::TempDir;

use crate::errors::*;
use crate::providers::MetadataProvider;

use mailparse::*;
use serde_derive::Deserialize;

const CONFIG_DRIVE_LABEL: &str = "cidata";

/// IBMCloud provider (VPC Gen2).
Expand Down Expand Up @@ -90,6 +95,16 @@ impl IBMGen2Provider {
Ok(output)
}

/// Read vendordata file into a string
fn read_vendordata(&self) -> Result<Vec<u8>> {
let filename = self.metadata_dir().join("vendor-data");
let mut file = File::open(&filename)
.chain_err(|| format!("Failed to open vendordata '{:?}'", filename))?;
let mut contents = String::new();
let _ = file.read_to_string(&mut contents);
Ok(contents.into_bytes())
}

/// Extract supported metadata values and convert to Afterburn attributes.
///
/// The `AFTERBURN_` prefix is added later on, so it is not part of the
Expand All @@ -109,6 +124,37 @@ impl IBMGen2Provider {
}
output
}

/// Find the SSH keys in the vendordata file
fn fetch_ssh_keys(vendordata_vec: Vec<u8>) -> Result<Vec<String>> {
// Parse MIME format from vendor-data file
let vendor_data_mail =
parse_mail(&vendordata_vec).chain_err(|| "failed to parse MIME vendor-data")?;
let mut cloud_config = String::new();
for section in vendor_data_mail.subparts {
for header in &section.headers {
if let "text/cloud-config" = header.get_value().as_str() {
if section
.get_body()
.unwrap_or_default()
.contains("ssh_authorized_keys")
{
cloud_config = section
.get_body()
.chain_err(|| "failed to get cloud-config content")?;
break;
}
}
}
}
// Parse YAML to find SSH keys
if cloud_config.is_empty() {
return Err("no cloud-config section found in vendor-data".into());
}
let deserialized_cloud_config: VendorDataCloudConfig = serde_yaml::from_str(&cloud_config)
.chain_err(|| "failed to deserialize cloud-config content")?;
Ok(deserialized_cloud_config.ssh_authorized_keys)
}
}

impl MetadataProvider for IBMGen2Provider {
Expand All @@ -123,6 +169,18 @@ impl MetadataProvider for IBMGen2Provider {
let hostname = metadata.get("local-hostname").map(String::from);
Ok(hostname)
}

fn ssh_keys(&self) -> Result<Vec<PublicKey>> {
let mut out = Vec::new();

let vendordata = self.read_vendordata()?;
for key in IBMGen2Provider::fetch_ssh_keys(vendordata)? {
let key = PublicKey::parse(&key)?;
out.push(key);
}

Ok(out)
}
}

impl Drop for IBMGen2Provider {
Expand All @@ -136,9 +194,19 @@ impl Drop for IBMGen2Provider {
}
}

/// This struct represents the portion of the vendor-data file we will be deserializing.
/// This data is in the "cloud-config" portion of the vendor-data file.
/// The cloud-config can have fields not defined here, they will be ignored.
/// The vendor-data file is in MIME format, the cloud-config data is in YAML format.
#[derive(Debug, Deserialize, Clone)]
struct VendorDataCloudConfig {
ssh_authorized_keys: Vec<String>,
}

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

#[test]
Expand Down Expand Up @@ -173,4 +241,17 @@ foo: ba:r
Some(&"test_instance-vpc-gen2".to_string())
);
}

#[test]
fn test_fetch_ssh_keys() {
let vendordata = fs::read("./tests/fixtures/ibmcloud/vendor-data")
.expect("Unable to read vendor-data fixture");
let ssh_keys = IBMGen2Provider::fetch_ssh_keys(vendordata).unwrap();
assert!(ssh_keys
.iter()
.any(|i| i == "ssh-rsa AAAAB3NzaC1yc2 <<snip>> 3TIX+eesnqasq9w== testuser@test.com"));
assert!(ssh_keys
.iter()
.any(|i| i == "ssh-rsa AAAAB4NzaC2yc3 <<snip>> 3TIX+eesnqasq9w== testuser2@test.com"));
}
}
1 change: 1 addition & 0 deletions systemd/afterburn-sshkeys@.service.in
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ ConditionKernelCommandLine=|ignition.platform.id=exoscale
ConditionKernelCommandLine=|ignition.platform.id=gcp
ConditionKernelCommandLine=|ignition.platform.id=packet
ConditionKernelCommandLine=|ignition.platform.id=vultr
ConditionKernelCommandLine=|ignition.platform.id=ibmcloud

[Service]
Type=oneshot
Expand Down
99 changes: 99 additions & 0 deletions tests/fixtures/ibmcloud/vendor-data
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
Content-Type: multipart/form-data; boundary=3efa30189c9e0e8ebc24a4decbbf4c2be7b26120c1cdd7cb7bc2ecb0c07c
MIME-Version: 1.0

--3efa30189c9e0e8ebc24a4decbbf4c2be7b26120c1cdd7cb7bc2ecb0c07c
Content-Type: text/cloud-config

#cloud-config
disable_root: false
ssh_authorized_keys:
- ssh-rsa AAAAB3NzaC1yc2 <<snip>> 3TIX+eesnqasq9w==
testuser@test.com
- ssh-rsa AAAAB4NzaC2yc3 <<snip>> 3TIX+eesnqasq9w==
testuser2@test.com
users:
- default
- name: root
lock-passwd: false
ssh_pwauth: true

--3efa30189c9e0e8ebc24a4decbbf4c2be7b26120c1cdd7cb7bc2ecb0c07c
Content-Type: text/x-shellscript

#!/bin/bash
PER_BOOT_SCRIPTS_DIR=/var/lib/cloud/scripts/per-boot
IFACE_CONFIG_PATH=$PER_BOOT_SCRIPTS_DIR/iface-config
mkdir -p $PER_BOOT_SCRIPTS_DIR
cat << EOT > $IFACE_CONFIG_PATH
#!/bin/bash
for iface in \$(ip -br link show | cut -d ' ' -f1 | sed '/^lo\$/d')
do
if [ -x "\$(command -v ethtool)" ]; then
CPU_COUNT=`getconf _NPROCESSORS_ONLN`
if [ "\$CPU_COUNT" -gt 2 ]; then
QUEUE_COUNT=3
else
QUEUE_COUNT=1
fi
ethtool -L \$iface combined \$QUEUE_COUNT
fi
ip link set \$iface up
done
dhclient > /dev/null 2>&1 || true # prevents exit code 1 when dhclient is already running
EOT
chmod +x $IFACE_CONFIG_PATH
$IFACE_CONFIG_PATH

--3efa30189c9e0e8ebc24a4decbbf4c2be7b26120c1cdd7cb7bc2ecb0c07c
Content-Type: text/cloud-config

#cloud-config

write_files:
-
content: |
[base]
name=CentOS-$releasever - Base
baseurl=http://mirrors.adn.networklayer.com/centos/$releasever/os/$basearch/
#mirrorlist=http://#mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=os
gpgcheck=1
gpgkey=http://mirrors.adn.networklayer.com/centos/RPM-GPG-KEY-CentOS-7

#released updates
[updates]
name=CentOS-$releasever - Updates
baseurl=http://mirrors.adn.networklayer.com/centos/$releasever/updates/$basearch/
#mirrorlist=http://#mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=updates
gpgcheck=1
gpgkey=http://mirrors.adn.networklayer.com/centos/RPM-GPG-KEY-CentOS-7

#additional packages that may be useful
[extras]\n",
name=CentOS-$releasever - Extras
baseurl=http://mirrors.adn.networklayer.com/centos/$releasever/extras/$basearch/
#mirrorlist=http://#mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=extras
gpgcheck=1
gpgkey=http://mirrors.adn.networklayer.com/centos/RPM-GPG-KEY-CentOS-7

#additional packages that extend functionality of existing packages
[centosplus]
name=CentOS-$releasever - Plus
baseurl=http://mirrors.adn.networklayer.com/centos/$releasever/centosplus/$basearch/
#mirrorlist=http://#mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=centosplus
gpgcheck=1
enabled=0
gpgkey=http://mirrors.adn.networklayer.com/centos/RPM-GPG-KEY-CentOS-7

#contrib - packages by Centos Users
[contrib]
name=CentOS-$releasever - Contrib
baseurl=http://mirrors.adn.networklayer.com/centos/$releasever/contrib/$basearch/
#mirrorlist=http://#mirrorlist.centos.org/?release=$releasever&arch=$basearch&repo=contrib
gpgcheck=1
enabled=0
gpgkey=http://mirrors.adn.networklayer.com/centos/RPM-GPG-KEY-CentOS-7
owner: "root:root"
path: /etc/yum.repos.d/CentOS-Base.repo
permissions: "0644"

--3efa30189c9e0e8ebc24a4decbbf4c2be7b26120c1cdd7cb7bc2ecb0c07c

0 comments on commit 6acfec1

Please sign in to comment.