From 4eded18d1cffe2bcbc432e4c14e4712b0c1ab4cb Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Thu, 4 Mar 2021 10:14:15 +0200 Subject: feat! allow specification files to have any number of machines --- src/bin/vmadm.rs | 33 ++++++++-------------------- src/cmd/cloud_init.rs | 35 +++++++++++++----------------- src/cmd/delete.rs | 60 ++++++++++++++++++++++++++------------------------- src/cmd/mod.rs | 2 +- src/cmd/new.rs | 50 +++++++++++++++++++++--------------------- src/spec.rs | 58 ++++++++++++++++++++++++++++++++----------------- subplot/vmadm.py | 8 +++---- vmadm.md | 54 +++++++++++++++++++++++----------------------- 8 files changed, 151 insertions(+), 149 deletions(-) diff --git a/src/bin/vmadm.rs b/src/bin/vmadm.rs index 4cc2248..8fe5540 100644 --- a/src/bin/vmadm.rs +++ b/src/bin/vmadm.rs @@ -57,17 +57,6 @@ enum Command { #[structopt(parse(from_os_str))] dirname: PathBuf, }, - - CloudInitIso { - #[structopt(flatten)] - common: CommonOptions, - - #[structopt(parse(from_os_str))] - spec: PathBuf, - - #[structopt(parse(from_os_str))] - iso: PathBuf, - }, } #[derive(StructOpt, Debug)] @@ -83,8 +72,8 @@ fn main() -> anyhow::Result<()> { match cli.cmd { Command::New { common, spec } => { - let spec = get_spec(&common, &spec)?; - cmd::new(&spec)?; + let specs = get_specs(&common, &spec)?; + cmd::new(&specs)?; } Command::List { common } => { @@ -93,8 +82,8 @@ fn main() -> anyhow::Result<()> { } Command::Delete { common, spec } => { - let spec = get_spec(&common, &spec)?; - cmd::delete(&spec)?; + let specs = get_specs(&common, &spec)?; + cmd::delete(&specs)?; } Command::CloudInit { @@ -102,21 +91,17 @@ fn main() -> anyhow::Result<()> { spec, dirname, } => { - let spec = get_spec(&common, &spec)?; - cmd::cloud_init(&spec, &dirname)?; - } - Command::CloudInitIso { common, spec, iso } => { - let spec = get_spec(&common, &spec)?; - cmd::cloud_init_iso(&spec, &iso)?; + let specs = get_specs(&common, &spec)?; + cmd::cloud_init(&specs, &dirname)?; } } Ok(()) } -fn get_spec(common: &CommonOptions, spec: &Path) -> anyhow::Result { +fn get_specs(common: &CommonOptions, spec: &Path) -> anyhow::Result> { let config = config(&common)?; - let spec = Specification::from_file(&config, &spec)?; - Ok(spec) + let specs = Specification::from_file(&config, &spec)?; + Ok(specs) } fn config(common: &CommonOptions) -> anyhow::Result { diff --git a/src/cmd/cloud_init.rs b/src/cmd/cloud_init.rs index 615b2a8..a9b3588 100644 --- a/src/cmd/cloud_init.rs +++ b/src/cmd/cloud_init.rs @@ -12,26 +12,21 @@ pub enum CloudInitCommandError { IoError(#[from] std::io::Error), } -pub fn cloud_init(spec: &Specification, dirname: &Path) -> Result<(), CloudInitCommandError> { - info!( - "generating cloud-init configuration into {}", - dirname.display() - ); - - let init = CloudInitConfig::from(&spec)?; - - debug!("creating directory {}", dirname.display()); - std::fs::create_dir_all(dirname)?; - init.create_dir(dirname)?; - - Ok(()) -} - -pub fn cloud_init_iso(spec: &Specification, iso: &Path) -> Result<(), CloudInitCommandError> { - info!("generating cloud-init ISO into {}", iso.display()); - - let init = CloudInitConfig::from(&spec)?; - init.create_iso(iso)?; +pub fn cloud_init(specs: &[Specification], dirname: &Path) -> Result<(), CloudInitCommandError> { + for spec in specs { + let dirname = dirname.join(&spec.name); + info!( + "generating cloud-init configuration for {} into {}", + spec.name, + dirname.display() + ); + + let init = CloudInitConfig::from(&spec)?; + + debug!("creating directory {}", dirname.display()); + std::fs::create_dir_all(&dirname)?; + init.create_dir(&dirname)?; + } Ok(()) } diff --git a/src/cmd/delete.rs b/src/cmd/delete.rs index ac8c5b4..3f60e27 100644 --- a/src/cmd/delete.rs +++ b/src/cmd/delete.rs @@ -13,39 +13,41 @@ pub enum DeleteError { IoError(#[from] std::io::Error), } -pub fn delete(spec: &Specification) -> Result<(), DeleteError> { - info!("deleting virtual machine {}", spec.name); - - debug!("connecting to libvirtd"); - let conn = Connect::open("qemu:///system")?; - - debug!("listing all domains"); - let domains = conn.list_all_domains(0)?; - - for domain in domains { - debug!("considering {}", domain.get_name()?); - if domain.get_name()? == spec.name { - debug!("shutdown {}", spec.name); - domain.shutdown().ok(); - - let briefly = Duration::from_millis(1000); - loop { - thread::sleep(briefly); - match domain.is_active() { - Ok(true) => (), - Ok(false) => break, - Err(err) => { - debug!("is_active: {}", err); +pub fn delete(specs: &[Specification]) -> Result<(), DeleteError> { + for spec in specs { + info!("deleting virtual machine {}", spec.name); + + debug!("connecting to libvirtd"); + let conn = Connect::open("qemu:///system")?; + + debug!("listing all domains"); + let domains = conn.list_all_domains(0)?; + + for domain in domains { + debug!("considering {}", domain.get_name()?); + if domain.get_name()? == spec.name { + debug!("shutdown {}", spec.name); + domain.shutdown().ok(); + + let briefly = Duration::from_millis(1000); + loop { + thread::sleep(briefly); + match domain.is_active() { + Ok(true) => (), + Ok(false) => break, + Err(err) => { + debug!("is_active: {}", err); + } } + debug!("{} is still running", spec.name); } - debug!("{} is still running", spec.name); - } - debug!("undefine {}", spec.name); - domain.undefine()?; + debug!("undefine {}", spec.name); + domain.undefine()?; - debug!("removing image file {}", spec.image.display()); - std::fs::remove_file(&spec.image)?; + debug!("removing image file {}", spec.image.display()); + std::fs::remove_file(&spec.image)?; + } } } Ok(()) diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index ad32e49..f1e029c 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -8,4 +8,4 @@ pub mod delete; pub use delete::delete; pub mod cloud_init; -pub use cloud_init::{cloud_init, cloud_init_iso}; +pub use cloud_init::cloud_init; diff --git a/src/cmd/new.rs b/src/cmd/new.rs index 0a4b3ab..0f7bc94 100644 --- a/src/cmd/new.rs +++ b/src/cmd/new.rs @@ -21,30 +21,32 @@ pub enum NewError { VirtInstallError(#[from] VirtInstallError), } -pub fn new(spec: &Specification) -> Result<(), NewError> { - info!("creating new VM {}", spec.name); - - info!("creating cloud-init config"); - let init = CloudInitConfig::from(&spec)?; - - info!( - "creating VM image {} from {}", - spec.image.display(), - spec.base.display() - ); - let image = VirtualMachineImage::new_from_base(&spec.base, &spec.image)?; - - info!("resizing image to {} GiB", spec.image_size_gib); - image.resize(spec.image_size_gib * GIB)?; - - info!("creating VM"); - let mut args = VirtInstallArgs::new(&spec.name, &image, &init); - args.set_memory(spec.memory_mib); - args.set_vcpus(spec.cpus); - virt_install(&args)?; - - info!("waiting for {} to open its SSH port", spec.name); - wait_for_port(&spec.name, SSH_PORT)?; +pub fn new(specs: &[Specification]) -> Result<(), NewError> { + for spec in specs { + info!("creating new VM {}", spec.name); + + info!("creating cloud-init config"); + let init = CloudInitConfig::from(&spec)?; + + info!( + "creating VM image {} from {}", + spec.image.display(), + spec.base.display() + ); + let image = VirtualMachineImage::new_from_base(&spec.base, &spec.image)?; + + info!("resizing image to {} GiB", spec.image_size_gib); + image.resize(spec.image_size_gib * GIB)?; + + info!("creating VM"); + let mut args = VirtInstallArgs::new(&spec.name, &image, &init); + args.set_memory(spec.memory_mib); + args.set_vcpus(spec.cpus); + virt_install(&args)?; + + info!("waiting for {} to open its SSH port", spec.name); + wait_for_port(&spec.name, SSH_PORT)?; + } Ok(()) } diff --git a/src/spec.rs b/src/spec.rs index 8325d98..c99da6b 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -2,14 +2,13 @@ use crate::config::Configuration; 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 InputSpecification { - pub name: String, - +struct OneVmInputSpecification { #[serde(default)] pub ssh_key_files: Vec, @@ -51,8 +50,8 @@ pub struct Specification { #[derive(Debug, thiserror::Error)] pub enum SpecificationError { - #[error("No base image or default base image specified: {0}")] - NoBaseImage(PathBuf), + #[error("No base image or default base image specified in {0} for {1}")] + NoBaseImage(PathBuf, String), #[error("No image filename and no image directory specified in configuration")] NoImage, @@ -74,39 +73,58 @@ impl Specification { pub fn from_file( config: &Configuration, filename: &Path, - ) -> Result { + ) -> Result, SpecificationError> { debug!("reading specification from {}", filename.display()); let spec = fs::read(filename)?; - let input: InputSpecification = serde_yaml::from_slice(&spec)?; + let input: HashMap = serde_yaml::from_slice(&spec)?; debug!("specification as read from file: {:#?}", input); - let base = if let Some(base) = input.base { + let mut machines = vec![]; + for (name, machine) in input.iter() { + let spec = Specification::one_machine(config, &filename, &name, &machine)?; + debug!("machine with defaults applied: {:#?}", spec); + machines.push(spec); + } + + Ok(machines) + } + + 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())); + return Err(SpecificationError::NoBaseImage( + filename.to_path_buf(), + name.to_string(), + )); }; - let image = if let Some(image) = input.image { + let image = if let Some(image) = &input.image { image.to_path_buf() } else if let Some(dirname) = &config.image_directory { - dirname.join(format!("{}.qcow2", input.name)) + dirname.join(format!("{}.qcow2", name)) } else { return Err(SpecificationError::NoImage.into()); }; let spec = Specification { - name: input.name.clone(), + name: name.to_string(), ssh_keys: ssh_keys(&input.ssh_key_files)?, - rsa_host_key: input.rsa_host_key, - rsa_host_cert: input.rsa_host_cert, - dsa_host_key: input.dsa_host_key, - dsa_host_cert: input.dsa_host_cert, - ecdsa_host_key: input.ecdsa_host_key, - ecdsa_host_cert: input.ecdsa_host_cert, - ed25519_host_key: input.ed25519_host_key, - ed25519_host_cert: input.ed25519_host_cert, + 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, image, image_size_gib: input.image_size_gib, diff --git a/subplot/vmadm.py b/subplot/vmadm.py index 1ee2914..ae3a3a3 100644 --- a/subplot/vmadm.py +++ b/subplot/vmadm.py @@ -19,7 +19,7 @@ def install_vmadm(ctx): # This can be removed once the Subplot lib/files library creates # directories. os.mkdir(".ssh") - os.mkdir("expected") + os.makedirs("expected/init-test") os.mkdir("images") @@ -48,12 +48,12 @@ def directories_match(ctx, actual=None, expected=None): assert_eq = globals()["assert_eq"] efiles = list(sorted(os.listdir(expected))) - afiles = list(sorted(os.listdir(expected))) + afiles = list(sorted(os.listdir(actual))) assert_eq(efiles, afiles) for filename in efiles: - with open(os.path.join("expected", filename)) as f: + with open(os.path.join(expected, filename)) as f: edata = yaml.safe_load(f) - with open(os.path.join("actual", filename)) as f: + with open(os.path.join(actual, filename)) as f: adata = yaml.safe_load(f) if "runcmd" in adata: del adata["runcmd"] diff --git a/vmadm.md b/vmadm.md index 1a056ac..d14fc3d 100644 --- a/vmadm.md +++ b/vmadm.md @@ -3,14 +3,14 @@ This section has some data files used by scenarios. ~~~{#smoke.yaml .file .yaml} -name: 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_files: + - .ssh/id_rsa.pub + base: base.qcow2 + image: smoke.qcow2 + image_size_gib: 5 + memory_mib: 2048 + cpus: 1 ~~~ ~~~{#ssh_key .file} @@ -64,10 +64,10 @@ correctly. given an installed vmadm given file init.yaml given file .ssh/id_rsa.pub from init_ssh_key_pub -given file expected/meta-data from init-metadata -given file expected/user-data from init-userdata +given file expected/init-test/meta-data from init-metadata +given file expected/init-test/user-data from init-userdata when I invoke vmadm cloud-init --config config.yaml init.yaml actual -then directories actual and expected are identical +then directories actual/init-test and expected/init-test are identical ~~~ ~~~{#config.yaml .file. yaml} @@ -75,22 +75,22 @@ image_directory: images --- ~~~{#init.yaml .file .yaml} -name: init-test -ssh_key_files: - - .ssh/id_rsa.pub -rsa_host_key: rsa-private -rsa_host_cert: rsa-certificate -dsa_host_key: dsa-private -dsa_host_cert: dsa-certificate -ecdsa_host_key: ecdsa-private -ecdsa_host_cert: ecdsa-certificate -ed25519_host_key: ed25519-private -ed25519_host_cert: ed25519-certificate -base: /home/liw/tmp/debian-10-openstack-amd64.qcow2 -image: images/init.qcow2 -image_size_gib: 5 -memory_mib: 2048 -cpus: 1 +init-test: + ssh_key_files: + - .ssh/id_rsa.pub + rsa_host_key: rsa-private + rsa_host_cert: rsa-certificate + dsa_host_key: dsa-private + dsa_host_cert: dsa-certificate + ecdsa_host_key: ecdsa-private + ecdsa_host_cert: ecdsa-certificate + ed25519_host_key: ed25519-private + ed25519_host_cert: ed25519-certificate + base: /home/liw/tmp/debian-10-openstack-amd64.qcow2 + image: images/init.qcow2 + image_size_gib: 5 + memory_mib: 2048 + cpus: 1 ~~~ ~~~{#init_ssh_key_pub .file} -- cgit v1.2.1