diff options
author | Lars Wirzenius <liw@liw.fi> | 2021-02-27 07:54:40 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2021-02-28 12:24:08 +0200 |
commit | 1c5c97532c91faf8a59bbe7ebcfc645f653db697 (patch) | |
tree | 5d690b3e826472c8515710dce3d9bd256106b249 /src/cloudinit.rs | |
parent | 98c482d7903d059a6598a6e7280f0e7b431eac29 (diff) | |
download | vmadm-1c5c97532c91faf8a59bbe7ebcfc645f653db697.tar.gz |
feat: export cloud-init config, set SSH host keys
Diffstat (limited to 'src/cloudinit.rs')
-rw-r--r-- | src/cloudinit.rs | 307 |
1 files changed, 250 insertions, 57 deletions
diff --git a/src/cloudinit.rs b/src/cloudinit.rs index 6c52eff..f1b56ce 100644 --- a/src/cloudinit.rs +++ b/src/cloudinit.rs @@ -1,69 +1,288 @@ -use std::default::Default; +use crate::spec::{Specification, SpecificationError}; +use log::debug; +use serde::{Deserialize, Serialize}; +use shell_words::quote; use std::fs::write; +// use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::process::Command; use tempfile::tempdir; +const SCRIPT: &str = r#" +import os +import yaml + + +def log(msg): + logfile.write(msg) + logfile.write("\n") + logfile.flush() + + +logfile = open("/tmp/vmadm.script", "w") +log("vmadm cloud-init script starting") + +if os.environ.get("VMADM_TESTING"): + filename = "smoke/user-data" + etc = "x" +else: + filename = "/var/lib/cloud/instance/user-data.txt" + etc = "/etc/ssh" + +key_types = ("rsa", "dsa", "ecdsa", "ed25519") + +log(f"loading user-data from {filename}") +obj = yaml.safe_load(open(filename)) + +ssh_keys = obj.get("ssh_keys", {}) +# log(f"ssh_keys: {json.dumps(ssh_keys)}") + +keys = [] +certs = [] + +for key_type in key_types: + filename = os.path.join(etc, f"ssh_host_{key_type}_key.pub") + if os.path.exists(filename): + log(f"removing {filename}") + os.remove(filename) + else: + log(f"file {filename} does not exist") + +for key_type in key_types: + key = ssh_keys.get(f"{key_type}_private") + cert = ssh_keys.get(f"{key_type}_certificate") + log(f"key {key_type} {key}") + log(f"cert {key_type} {cert }") + + if key: + filename = os.path.join(etc, f"ssh_host_{key_type}_key") + log(f"writing key {filename}") + keys.append(filename) + with open(filename, "w") as f: + f.write(key) + + if cert: + filename = os.path.join(etc, f"ssh_host_{key_type}_key-cert.pub") + log(f"writing cert {filename}") + certs.append(filename) + with open(filename, "w") as f: + f.write(cert) + +config = os.path.join(etc, "sshd_config") +data = "" +if os.path.exists(config): + data = open(config).read() + +log(f"configuring sshd {config}") +log(f"keys {keys}") +log(f"certs {certs}") + +with open(config, "w") as f: + for filename in keys: + log(f"hostkey {filename}") + f.write(f"hostkey {filename}\n") + for filename in certs: + log(f"hostcert {filename}") + f.write(f"hostcertificate {filename}\n") + f.write(data) + +log("vmadm cloud-init script ending") +logfile.close() +"#; + #[derive(Debug, thiserror::Error)] pub enum CloudInitError { #[error("failed to create ISO image with genisoimage: {0}")] IsoFailed(String), #[error(transparent)] + SpecificationError(#[from] SpecificationError), + + #[error(transparent)] IoError(#[from] std::io::Error), #[error(transparent)] StringError(#[from] std::string::FromUtf8Error), + + #[error(transparent)] + SerdeError(#[from] serde_yaml::Error), +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "kebab-case")] +struct Metadata { + local_hostname: String, +} + +impl Metadata { + fn from(spec: &Specification) -> Self { + Self { + local_hostname: spec.name.to_string(), + } + } + + fn as_yaml(&self) -> Result<String, serde_yaml::Error> { + let yaml = serde_yaml::to_string(self)?; + let yaml = cleanup_document(&yaml); + Ok(format!("# Amazon EC2 style metadata\n{}", &yaml)) + } +} + +fn cleanup_document(doc: &str) -> String { + let marker = "---\n"; + let doc = if doc.starts_with(marker) { + &doc[marker.len()..] + } else { + doc + }; + if doc.ends_with("\n") { + &doc[..doc.len() - 1] + } else { + doc + }; + doc.to_string() +} + +#[derive(Clone, Debug, Serialize)] +struct Userdata { + ssh_authorized_keys: Vec<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + ssh_keys: Option<Hostkeys>, + + runcmd: Vec<String>, +} + +impl Userdata { + fn from(spec: &Specification) -> Result<Self, CloudInitError> { + Ok(Self { + ssh_authorized_keys: spec.ssh_keys()?.lines().map(|s| s.to_string()).collect(), + ssh_keys: Hostkeys::from(spec), + runcmd: vec![ + format!("python3 -c {}", quote(SCRIPT)), + format!("systemctl reload ssh"), + ], + }) + } + + fn as_yaml(&self) -> Result<String, serde_yaml::Error> { + let yaml = serde_yaml::to_string(self)?; + let yaml = cleanup_document(&yaml); + debug!("userdata self:\n{:#?}", self); + debug!("userdata yaml:\n{}", yaml); + Ok(format!("#cloud-config\n{}", &yaml)) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct Hostkeys { + #[serde(skip_serializing_if = "Option::is_none")] + rsa_private: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + rsa_certificate: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + dsa_private: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + dsa_certificate: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + ecdsa_private: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + ecdsa_certificate: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + ed25519_private: Option<String>, + + #[serde(skip_serializing_if = "Option::is_none")] + ed25519_certificate: Option<String>, +} + +impl Hostkeys { + fn from(spec: &Specification) -> Option<Self> { + let rsa = spec.rsa_host_key.clone(); + let rsa_cert = spec.rsa_host_cert.clone(); + + let dsa = spec.dsa_host_key.clone(); + let dsa_cert = spec.dsa_host_cert.clone(); + + let ecdsa = spec.ecdsa_host_key.clone(); + let ecdsa_cert = spec.ecdsa_host_cert.clone(); + + let ed25519 = spec.ed25519_host_key.clone(); + let ed25519_cert = spec.ed25519_host_cert.clone(); + + if rsa.is_some() || dsa.is_some() || ecdsa.is_some() || ed25519.is_some() { + Some(Self { + rsa_private: rsa, + rsa_certificate: rsa_cert, + dsa_private: dsa, + dsa_certificate: dsa_cert, + ecdsa_private: ecdsa, + ecdsa_certificate: ecdsa_cert, + ed25519_private: ed25519, + ed25519_certificate: ed25519_cert, + }) + } else { + None + } + } } -#[derive(Default, Debug, Clone)] +#[derive(Clone, Debug)] pub struct CloudInitConfig { - hostname: String, - authorized_keys: String, + metadata: Metadata, + userdata: Userdata, } impl CloudInitConfig { - pub fn hostname(&self) -> &str { - &self.hostname + pub fn from(spec: &Specification) -> Result<Self, CloudInitError> { + let metadata = Metadata::from(spec); + let userdata = Userdata::from(spec)?; + Ok(CloudInitConfig { metadata, userdata }) } - pub fn set_hostname(&mut self, hostname: &str) { - self.hostname = hostname.to_string(); + pub fn dump(&self) { + println!("==== meta-data:\n{}", self.metadata().unwrap()); + println!("==== user-data:\n{}", self.userdata().unwrap()); } - pub fn authorized_keys(&self) -> &str { - &self.authorized_keys + fn metadata(&self) -> Result<String, serde_yaml::Error> { + Ok(self.metadata.as_yaml()?) } - pub fn set_authorized_keys(&mut self, keys: &str) { - self.authorized_keys = keys.to_string(); + fn userdata(&self) -> Result<String, serde_yaml::Error> { + Ok(self.userdata.as_yaml()?) } - fn write(&self, dir: &Path) -> std::io::Result<()> { - let metadata = dir.join("meta-data"); - let userdata = dir.join("user-data"); - - write( - &metadata, - format!( - "# Amazon EC2 style metadata\nlocal-hostname: {}\n", - self.hostname() - ), - )?; - write( - &userdata, - format!( - "#cloud-config\nssh_authorized_keys:\n- {}", - self.authorized_keys() - ), - )?; + pub fn create_dir(&self, path: &Path) -> Result<(), CloudInitError> { + let metadata = path.join("meta-data"); + debug!("writing metadata to {}", metadata.display()); + write(&metadata, self.metadata()?)?; + + let userdata = path.join("user-data"); + debug!("writing userdata to {}", userdata.display()); + write(&userdata, self.userdata()?)?; + + // let scriptsdir = path.join("scripts/per-once"); + // let script = scriptsdir.join("vmadm-configure-sshd"); + // std::fs::create_dir_all(&scriptsdir)?; + // debug!("writing script to {}", script.display()); + // write(&script, SCRIPT)?; + // let mut permissions = std::fs::metadata(&script)?.permissions(); + // permissions.set_mode(0o755); + // debug!("setting permissions on {:?} to {:?}", script, permissions); + // std::fs::set_permissions(&script, permissions)?; Ok(()) } pub fn create_iso(&self, filename: &Path) -> Result<(), CloudInitError> { let dir = tempdir()?; - self.write(dir.path())?; + self.create_dir(dir.path())?; let r = Command::new("genisoimage") .arg("-quiet") @@ -82,29 +301,3 @@ impl CloudInitConfig { Ok(()) } } - -#[cfg(test)] -mod test { - use super::CloudInitConfig; - - #[test] - fn is_empty_by_default() { - let init = CloudInitConfig::default(); - assert_eq!(init.hostname(), ""); - assert_eq!(init.authorized_keys(), ""); - } - - #[test] - fn sets_hostname() { - let mut init = CloudInitConfig::default(); - init.set_hostname("foo"); - assert_eq!(init.hostname(), "foo"); - } - - #[test] - fn sets_authorized_keys() { - let mut init = CloudInitConfig::default(); - init.set_authorized_keys("auth"); - assert_eq!(init.authorized_keys(), "auth"); - } -} |