Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ibmcloud: add support for SSH keys #475

Merged
merged 1 commit into from
Aug 21, 2020
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
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)
lucab marked this conversation as resolved.
Show resolved Hide resolved
.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 {
lucab marked this conversation as resolved.
Show resolved Hide resolved
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