summaryrefslogtreecommitdiff
path: root/src/cloudinit.rs
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-02-27 07:54:40 +0200
committerLars Wirzenius <liw@liw.fi>2021-02-28 12:24:08 +0200
commit1c5c97532c91faf8a59bbe7ebcfc645f653db697 (patch)
tree5d690b3e826472c8515710dce3d9bd256106b249 /src/cloudinit.rs
parent98c482d7903d059a6598a6e7280f0e7b431eac29 (diff)
downloadvmadm-1c5c97532c91faf8a59bbe7ebcfc645f653db697.tar.gz
feat: export cloud-init config, set SSH host keys
Diffstat (limited to 'src/cloudinit.rs')
-rw-r--r--src/cloudinit.rs307
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");
- }
-}