summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-03-04 10:14:15 +0200
committerLars Wirzenius <liw@liw.fi>2021-03-04 10:14:24 +0200
commit4eded18d1cffe2bcbc432e4c14e4712b0c1ab4cb (patch)
tree903be1d7643439007ef4e39ef3d566171913f0ee
parent5141d3a654e3abc8064a6ba252220426701a5f89 (diff)
downloadvmadm-4eded18d1cffe2bcbc432e4c14e4712b0c1ab4cb.tar.gz
feat! allow specification files to have any number of machines
-rw-r--r--src/bin/vmadm.rs33
-rw-r--r--src/cmd/cloud_init.rs35
-rw-r--r--src/cmd/delete.rs60
-rw-r--r--src/cmd/mod.rs2
-rw-r--r--src/cmd/new.rs50
-rw-r--r--src/spec.rs58
-rw-r--r--subplot/vmadm.py8
-rw-r--r--vmadm.md54
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<Specification> {
+fn get_specs(common: &CommonOptions, spec: &Path) -> anyhow::Result<Vec<Specification>> {
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<Configuration> {
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<PathBuf>,
@@ -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<Specification, SpecificationError> {
+ ) -> Result<Vec<Specification>, SpecificationError> {
debug!("reading specification from {}", filename.display());
let spec = fs::read(filename)?;
- let input: InputSpecification = serde_yaml::from_slice(&spec)?;
+ let input: HashMap<String, OneVmInputSpecification> = 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<Specification, SpecificationError> {
+ 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}