From 458305fa48338de178739af2e3e86b85c0036a54 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Thu, 4 Mar 2021 10:40:21 +0200 Subject: feat: use configured defaults to unspecified machine attributes --- src/config.rs | 4 ++ src/spec.rs | 148 +++++++++++++++++++++++++++++++++++++++++------------ subplot/vmadm.py | 9 ++-- subplot/vmadm.yaml | 2 +- vmadm.md | 27 +++++----- 5 files changed, 138 insertions(+), 52 deletions(-) diff --git a/src/config.rs b/src/config.rs index dd6c2d7..a13c989 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,11 @@ use std::path::{Path, PathBuf}; #[derive(Default, Debug, Deserialize)] pub struct Configuration { pub default_base_image: Option, + pub default_image_gib: Option, + pub default_memory_mib: Option, + pub default_cpus: Option, pub image_directory: Option, + pub authorized_keys: Option>, } #[derive(Debug, thiserror::Error)] diff --git a/src/spec.rs b/src/spec.rs index c99da6b..d01abcf 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -10,7 +10,7 @@ use std::path::{Path, PathBuf}; #[serde(deny_unknown_fields)] struct OneVmInputSpecification { #[serde(default)] - pub ssh_key_files: Vec, + pub ssh_key_files: Option>, pub rsa_host_key: Option, pub rsa_host_cert: Option, @@ -23,9 +23,94 @@ struct OneVmInputSpecification { pub base: Option, pub image: Option, - pub image_size_gib: u64, - pub memory_mib: u64, - pub cpus: u64, + pub image_size_gib: Option, + pub memory_mib: Option, + pub cpus: Option, +} + +impl OneVmInputSpecification { + fn ssh_key_files( + &self, + config: &Configuration, + name: &str, + ) -> Result, SpecificationError> { + get( + &self.ssh_key_files, + &config.authorized_keys, + SpecificationError::NoAuthorizedKeys(name.to_string()), + ) + } + + 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 = if let Some(dirname) = &config.image_directory { + Some(dirname.join(format!("{}.qcow2", name))) + } else { + None + }; + + get( + &self.image, + &default_image, + SpecificationError::NoBaseImage(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 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) + } } #[derive(Debug)] @@ -50,11 +135,23 @@ pub struct Specification { #[derive(Debug, thiserror::Error)] pub enum SpecificationError { - #[error("No base image or default base image specified in {0} for {1}")] - NoBaseImage(PathBuf, String), + #[error("No base image or default base image specified for {0}")] + NoBaseImage(String), + + #[error("No image filename specified for {0} and no image_directory in configuration")] + NoImage(String), - #[error("No image filename and no image directory specified in configuration")] - NoImage, + #[error("No image size specified for {0} and no default configured")] + NoImageSize(String), + + #[error("No memory size specified for {0} and no default configured")] + NoMemorySize(String), + + #[error("No CPU count specified for {0} and no default configured")] + NoCpuCount(String), + + #[error("No SSH authorized keys specified for {0} and no default configured")] + NoAuthorizedKeys(String), #[error("Failed to read SSH public key file {0}")] SshKeyRead(PathBuf, #[source] std::io::Error), @@ -81,7 +178,7 @@ impl Specification { let mut machines = vec![]; for (name, machine) in input.iter() { - let spec = Specification::one_machine(config, &filename, &name, &machine)?; + let spec = Specification::one_machine(config, &name, &machine)?; debug!("machine with defaults applied: {:#?}", spec); machines.push(spec); } @@ -91,32 +188,15 @@ impl Specification { fn one_machine( config: &Configuration, - filename: &Path, name: &str, input: &OneVmInputSpecification, ) -> Result { - let base = if let Some(base) = &input.base { - base.to_path_buf() - } else if let Some(ref base) = config.default_base_image { - base.to_path_buf() - } else { - return Err(SpecificationError::NoBaseImage( - filename.to_path_buf(), - name.to_string(), - )); - }; - - let image = if let Some(image) = &input.image { - image.to_path_buf() - } else if let Some(dirname) = &config.image_directory { - dirname.join(format!("{}.qcow2", name)) - } else { - return Err(SpecificationError::NoImage.into()); - }; + let key_filenames = input.ssh_key_files(config, name)?; + let ssh_keys = ssh_keys(&key_filenames)?; let spec = Specification { name: name.to_string(), - ssh_keys: ssh_keys(&input.ssh_key_files)?, + ssh_keys: 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(), @@ -125,11 +205,11 @@ impl Specification { 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, - image, - image_size_gib: input.image_size_gib, - memory_mib: input.memory_mib, - cpus: input.cpus, + base: input.base_image(config, name)?, + image: 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)?, }; debug!("specification as with defaults applied: {:#?}", spec); diff --git a/subplot/vmadm.py b/subplot/vmadm.py index ae3a3a3..6997c5b 100644 --- a/subplot/vmadm.py +++ b/subplot/vmadm.py @@ -63,20 +63,21 @@ def directories_match(ctx, actual=None, expected=None): assert_eq(edata, adata) -def create_vm(ctx, filename=None): +def create_vm(ctx, config=None, filename=None): runcmd_run = globals()["runcmd_run"] runcmd_exit_code_is_zero = globals()["runcmd_exit_code_is_zero"] + ctx["config"] = config ctx["spec"] = filename - runcmd_run(ctx, ["vmadm", "new", filename]) + runcmd_run(ctx, ["vmadm", "new", "--config", config, filename]) runcmd_exit_code_is_zero(ctx) -def delete_vm(ctx, filename=None): +def delete_vm(ctx, config=None, filename=None): runcmd_run = globals()["runcmd_run"] runcmd_exit_code_is_zero = globals()["runcmd_exit_code_is_zero"] - runcmd_run(ctx, ["vmadm", "delete", ctx["spec"]]) + runcmd_run(ctx, ["vmadm", "delete", "--config", ctx["config"], ctx["spec"]]) runcmd_exit_code_is_zero(ctx) diff --git a/subplot/vmadm.yaml b/subplot/vmadm.yaml index ec7d424..21662b6 100644 --- a/subplot/vmadm.yaml +++ b/subplot/vmadm.yaml @@ -7,7 +7,7 @@ - when: "I invoke vmadm cloud-init --config {config} {filename} {dirname}" function: invoke_cloud_init -- when: "I invoke vmadm new {filename}" +- when: "I invoke vmadm new --config {config} {filename}" function: create_vm cleanup: delete_vm diff --git a/vmadm.md b/vmadm.md index d14fc3d..73b4a52 100644 --- a/vmadm.md +++ b/vmadm.md @@ -3,14 +3,7 @@ This section has some data files used by scenarios. ~~~{#smoke.yaml .file .yaml} -smoke: - ssh_key_files: - - .ssh/id_rsa.pub - base: base.qcow2 - image: smoke.qcow2 - image_size_gib: 5 - memory_mib: 2048 - cpus: 1 +smoke: {} ~~~ ~~~{#ssh_key .file} @@ -43,6 +36,16 @@ V4cecTlFJGBtUOUAAAAMbGl3QGV4b2xvYmUxAQIDBAUGBw== -----END OPENSSH PRIVATE KEY----- ~~~ +~~~{#config.yaml .file .yaml} +image_directory: images +default_base_image: base.qcow2 +default_image_gib: 5 +default_memory_mib: 2048 +default_cpus: 1 +authorized_keys: + - .ssh/id_rsa.pub +~~~ + ~~~{#ssh_key_pub .file} ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQChZ6mVuGLBpW7SarFU/Tu6TemquNxatbMUZuTk8RqVtbkvTKeWFZ5h5tntWPHgST8ykYFaIrr8eYuKQkKdBxHW7H8kejTNwRu/rDbRYX5wxTn4jw4RVopGTpxMlGrWeu5CkWPoLAhQtIzzUAnrDGp9sqG6P1G4ohI61wZMFQta9R2uNxXnnes+e2r4Y78GxmlQH/o0ouI8fBnsxRK0IoSfFs2LutO6wjyzR59FdC9TT7wufd5kXMRzxsmPGeXzNcaqvHGxBvRucGFclCkqSRwk3GNEpXZQhlCIoTIoRu0IPAp/430tlx9zJMhhwDlZsOOXRrFYpdWVMSTAAKECLSYx liw@exolobe1 ~~~ @@ -63,6 +66,7 @@ correctly. ~~~scenario given an installed vmadm given file init.yaml +given file config.yaml given file .ssh/id_rsa.pub from init_ssh_key_pub given file expected/init-test/meta-data from init-metadata given file expected/init-test/user-data from init-userdata @@ -70,10 +74,6 @@ when I invoke vmadm cloud-init --config config.yaml init.yaml actual then directories actual/init-test and expected/init-test are identical ~~~ -~~~{#config.yaml .file. yaml} -image_directory: images ---- - ~~~{#init.yaml .file .yaml} init-test: ssh_key_files: @@ -128,10 +128,11 @@ can be addressed by name. given an installed vmadm given a Debian 10 OpenStack cloud image given file smoke.yaml +given file config.yaml given file .ssh/id_rsa from ssh_key given file .ssh/id_rsa.pub from ssh_key_pub given file .ssh/config from ssh_config -when I invoke vmadm new smoke.yaml +when I invoke vmadm new --config config.yaml smoke.yaml when I invoke ssh -F .ssh/config debian@smoke hostname then stdout contains "smoke" when I invoke ssh -F .ssh/config debian@smoke df -h / -- cgit v1.2.1