//! Virtual machine specification. use crate::config::Configuration; use crate::util::{check_network_names, expand_tilde}; use log::debug; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; #[derive(Debug, Serialize, Deserialize)] #[serde(deny_unknown_fields)] struct OneVmInputSpecification { #[serde(default)] pub ssh_key_files: Option>, pub rsa_host_key: Option, pub rsa_host_cert: Option, pub dsa_host_key: Option, pub dsa_host_cert: Option, pub ecdsa_host_key: Option, pub ecdsa_host_cert: Option, pub ed25519_host_key: Option, pub ed25519_host_cert: Option, pub base: Option, pub image: Option, pub image_size_gib: Option, pub memory_mib: Option, pub cpus: Option, pub generate_host_certificate: Option, pub autostart: Option, pub networks: Option>, pub ca_key: Option, pub user_ca_pubkey: Option, pub allow_authorized_keys: Option, } impl OneVmInputSpecification { fn ssh_key_files(&self, config: &Configuration, name: &str) -> Option> { if let Ok(x) = get( &self.ssh_key_files, &config.authorized_keys, SpecificationError::NoAuthorizedKeys(name.to_string()), ) { Some(x) } else { None } } fn user_ca_pubkey(&self, config: &Configuration, name: &str) -> Option { if let Ok(x) = get( &self.user_ca_pubkey, &config.user_ca_pubkey, SpecificationError::NoAuthorizedKeys(name.to_string()), ) { Some(x) } else { None } } fn allow_authorized_keys(&self, config: &Configuration) -> bool { if let Ok(x) = get( &self.allow_authorized_keys, &config.default_allow_authorized_keys, SpecificationError::NoAuthorizedKeys("".to_string()), ) { x } else { true } } fn base_image( &self, config: &Configuration, name: &str, ) -> Result { get( &self.base, &config.default_base_image, SpecificationError::NoBaseImage(name.to_string()), ) } fn image(&self, config: &Configuration, name: &str) -> Result { let default_image = &config .image_directory .as_ref() .map(|dirname| dirname.join(format!("{}.qcow2", name))); get( &self.image, default_image, SpecificationError::NoImage(name.to_string()), ) } fn image_size_gib( &self, config: &Configuration, name: &str, ) -> Result { get( &self.image_size_gib, &config.default_image_gib, SpecificationError::NoBaseImage(name.to_string()), ) } fn memory_mib(&self, config: &Configuration, name: &str) -> Result { get( &self.memory_mib, &config.default_memory_mib, SpecificationError::NoBaseImage(name.to_string()), ) } fn cpus(&self, config: &Configuration, name: &str) -> Result { get( &self.cpus, &config.default_cpus, SpecificationError::NoBaseImage(name.to_string()), ) } fn autostart(&self, config: &Configuration) -> bool { if let Some(x) = self.autostart { x } else if let Some(x) = config.default_autostart { x } else { false } } fn networks(&self, config: &Configuration) -> Vec { if let Some(ref x) = self.networks { x.clone() } else if let Some(ref x) = config.default_networks { x.clone() } else { vec!["default".to_string()] } } } fn get<'a, T>( input: &'a Option, default: &'a Option, error: SpecificationError, ) -> Result where T: Clone, { if let Some(input) = input { Ok((*input).clone()) } else if let Some(default) = default { Ok((*default).clone()) } else { Err(error) } } /// Effective virtual machine specification. /// /// This is the specification as read from the input file, with the /// defaults from the configuration file already applied. #[derive(Debug, Serialize)] pub struct Specification { /// Name of new virtual machine to create. pub name: String, /// SSH public keys to install in the default user's `authorized_keys` file. pub ssh_keys: Vec, /// RSA host key to install in new VM. pub rsa_host_key: Option, /// RSA host certificate. pub rsa_host_cert: Option, /// DSA host key to install in new VM. pub dsa_host_key: Option, /// DSA host certificate. pub dsa_host_cert: Option, /// ECDSA host key to install in new VM. pub ecdsa_host_key: Option, /// ECDSA host certificate. pub ecdsa_host_cert: Option, /// Ed25519 host key to install in new VM. pub ed25519_host_key: Option, /// Ed25519 host certificate. pub ed25519_host_cert: Option, /// Path to base image. pub base: PathBuf, /// Path to new VM image, to be created. pub image: PathBuf, /// Size of new image, in GiB. pub image_size_gib: u64, /// Size of memory for new VM, in MiB. pub memory_mib: u64, /// CPUs new VM should have. pub cpus: u64, /// Should a new host key and certificate be created for new VM? pub generate_host_certificate: bool, /// Should the VM be started automatically when host starts? pub autostart: bool, /// Path to CA key for creating host certificate. pub ca_key: Option, /// Path to CA publicv key for verifying user certificates. pub user_ca_pubkey: Option, /// Allow SSH server to use per-user authorized keys files? pub allow_authorized_keys: bool, /// List of networks to which host should be added. pub networks: Vec, } /// Errors from this module. #[derive(Debug, thiserror::Error)] pub enum SpecificationError { /// No base image file specified. #[error("No base image or default base image specified for {0}")] NoBaseImage(String), /// No image file specified. #[error("No image filename specified for {0} and no image_directory in configuration")] NoImage(String), /// No image size specified. #[error("No image size specified for {0} and no default configured")] NoImageSize(String), /// No memory size specified. #[error("No memory size specified for {0} and no default configured")] NoMemorySize(String), /// No CPU count specified. #[error("No CPU count specified for {0} and no default configured")] NoCpuCount(String), /// No SSH authorized keys or user CA specified. #[error("No SSH authorized keys nor user CA specified for {0} and no default configured")] NoAuthorizedKeys(String), /// Error reading specification file. #[error("Couldn't read specification file {0}")] Read(PathBuf, #[source] std::io::Error), /// Error reading SSH public key. #[error("Failed to read SSH public key file {0}")] SshKeyRead(PathBuf, #[source] std::io::Error), /// Network name error. #[error(transparent)] NetworkNameError(#[from] crate::util::NetworkNameError), /// Error parsing string as UTF8. #[error(transparent)] FromUtf8Error(#[from] std::string::FromUtf8Error), /// Error parsing YAML. #[error(transparent)] YamlError(#[from] serde_yaml::Error), /// Error expanding a ~user in a path name. #[error(transparent)] HomeDirError(#[from] home_dir::Error), } impl Specification { /// Read all specifications from a file. /// /// Apply values from the provided configuration so that the /// returned specifications are *effective* and the caller doesn't /// need to worry about the configuration anymore. /// /// Also, SSH public keys are read from the files named in the /// input specification. pub fn from_file( config: &Configuration, filename: &Path, ) -> Result, SpecificationError> { debug!("reading specification from {}", filename.display()); let spec = fs::read(filename) .map_err(|err| SpecificationError::Read(filename.to_path_buf(), err))?; let input: HashMap = serde_yaml::from_slice(&spec)?; debug!("specification as read from file: {:#?}", input); let mut machines = vec![]; for (name, machine) in input.iter() { let spec = Specification::one_machine(config, name, machine)?; debug!("machine with defaults applied: {:#?}", spec); machines.push(spec); } Ok(machines) } fn one_machine( config: &Configuration, name: &str, input: &OneVmInputSpecification, ) -> Result { let ssh_keys = if let Some(key_filenames) = input.ssh_key_files(config, name) { ssh_keys(&key_filenames)? } else { vec![] }; let user_ca_pubkey = input.user_ca_pubkey(config, name); if ssh_keys.is_empty() && user_ca_pubkey.is_none() { return Err(SpecificationError::NoAuthorizedKeys(name.to_string())); } let user_ca_pubkey = if let Some(filename) = user_ca_pubkey { Some(expand_tilde(&filename)?) } else { None }; let ca_key = if let Some(filename) = &input.ca_key { Some(expand_tilde(filename)?) } else { config.ca_key.clone() }; let gen_cert = if let Some(v) = &input.generate_host_certificate { *v } else if let Some(v) = &config.default_generate_host_certificate { *v } else { false }; let networks = input.networks(config); check_network_names(&networks)?; let spec = Specification { name: name.to_string(), ssh_keys, rsa_host_key: input.rsa_host_key.clone(), rsa_host_cert: input.rsa_host_cert.clone(), dsa_host_key: input.dsa_host_key.clone(), dsa_host_cert: input.dsa_host_cert.clone(), ecdsa_host_key: input.ecdsa_host_key.clone(), ecdsa_host_cert: input.ecdsa_host_cert.clone(), ed25519_host_key: input.ed25519_host_key.clone(), ed25519_host_cert: input.ed25519_host_cert.clone(), base: expand_tilde(&input.base_image(config, name)?)?, image: expand_tilde(&input.image(config, name)?)?, image_size_gib: input.image_size_gib(config, name)?, memory_mib: input.memory_mib(config, name)?, cpus: input.cpus(config, name)?, generate_host_certificate: gen_cert, autostart: input.autostart(config), ca_key, user_ca_pubkey, allow_authorized_keys: input.allow_authorized_keys(config), networks, }; debug!("specification as with defaults applied: {:#?}", spec); Ok(spec) } } fn ssh_keys(filenames: &[PathBuf]) -> Result, SpecificationError> { let mut keys = vec![]; for filename in filenames { let filename = expand_tilde(filename)?; let key = std::fs::read(&filename).map_err(|e| SpecificationError::SshKeyRead(filename, e))?; let key = String::from_utf8(key)?; let key = key.strip_suffix('\n').unwrap_or(&key); keys.push(key.to_string()); } Ok(keys) }