summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-03-01 10:29:14 +0000
committerLars Wirzenius <liw@liw.fi>2021-03-01 10:29:14 +0000
commitf3722501a719806932d80f70844b16c12e1abea1 (patch)
tree46659f0fa296fe2128d1a7bcb929008d034f51c1
parent05dbbc46d7b3fad9e7a1b85e10469022e07e0b40 (diff)
parenta4e59f7f6363c2bc62b91298f41b5b645842385e (diff)
downloadvmadm-f3722501a719806932d80f70844b16c12e1abea1.tar.gz
Merge branch 'config' into 'main'
feat: change how command line interface works See merge request larswirzenius/vmadm!4
-rw-r--r--Cargo.lock32
-rw-r--r--Cargo.toml1
-rw-r--r--src/bin/vmadm.rs262
-rw-r--r--src/cloudinit.rs2
-rw-r--r--src/cmd/cloud_init.rs28
-rw-r--r--src/cmd/delete.rs43
-rw-r--r--src/cmd/list.rs31
-rw-r--r--src/cmd/mod.rs11
-rw-r--r--src/cmd/new.rs47
-rw-r--r--src/config.rs33
-rw-r--r--src/lib.rs2
-rw-r--r--src/spec.rs94
12 files changed, 431 insertions, 155 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2a03b2b..1b1bcd6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -69,6 +69,27 @@ dependencies = [
]
[[package]]
+name = "directories-next"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc"
+dependencies = [
+ "cfg-if",
+ "dirs-sys-next",
+]
+
+[[package]]
+name = "dirs-sys-next"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
name = "dtoa"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -278,6 +299,16 @@ dependencies = [
]
[[package]]
+name = "redox_users"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+]
+
+[[package]]
name = "regex"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -489,6 +520,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bytesize",
+ "directories-next",
"log",
"pretty_env_logger",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
index 54db867..740246b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -18,3 +18,4 @@ bytesize = "1"
log = "0.4"
pretty_env_logger = "0.4"
shell-words = "1"
+directories-next = "2" \ No newline at end of file
diff --git a/src/bin/vmadm.rs b/src/bin/vmadm.rs
index a2717ee..4cc2248 100644
--- a/src/bin/vmadm.rs
+++ b/src/bin/vmadm.rs
@@ -1,38 +1,67 @@
-use bytesize::GIB;
-use log::{debug, info};
-use std::fs;
-use std::net::TcpStream;
+use anyhow::Context;
+use directories_next::ProjectDirs;
+use vmadm::cmd;
+use vmadm::config::Configuration;
+use vmadm::spec::Specification;
+//use bytesize::GIB;
+use log::debug;
+//use std::fs;
use std::path::{Path, PathBuf};
-use std::thread;
-use std::time::Duration;
+//use std::thread;
use structopt::StructOpt;
-use virt::connect::Connect;
-use vmadm::cloudinit::CloudInitConfig;
-use vmadm::image::VirtualMachineImage;
-use vmadm::install::{virt_install, VirtInstallArgs};
-use vmadm::spec::Specification;
+//use vmadm::cloudinit::CloudInitConfig;
+//use vmadm::image::VirtualMachineImage;
+//use vmadm::install::{virt_install, VirtInstallArgs};
+//use vmadm::spec::Specification;
+
+const QUALIFIER: &str = "";
+const ORG: &str = "";
+const APP: &str = "vmadm";
-const SSH_PORT: i32 = 22;
+#[derive(StructOpt, Debug)]
+struct Cli {
+ #[structopt(subcommand)]
+ cmd: Command,
+}
#[derive(StructOpt, Debug)]
-enum Cli {
+enum Command {
New {
+ #[structopt(flatten)]
+ common: CommonOptions,
+
#[structopt(parse(from_os_str))]
spec: PathBuf,
},
- List,
+
+ List {
+ #[structopt(flatten)]
+ common: CommonOptions,
+ },
+
Delete {
+ #[structopt(flatten)]
+ common: CommonOptions,
+
#[structopt(parse(from_os_str))]
spec: PathBuf,
},
+
CloudInit {
+ #[structopt(flatten)]
+ common: CommonOptions,
+
#[structopt(parse(from_os_str))]
spec: PathBuf,
#[structopt(parse(from_os_str))]
dirname: PathBuf,
},
+
CloudInitIso {
+ #[structopt(flatten)]
+ common: CommonOptions,
+
#[structopt(parse(from_os_str))]
spec: PathBuf,
@@ -41,158 +70,103 @@ enum Cli {
},
}
-fn main() -> anyhow::Result<()> {
- pretty_env_logger::init();
- match Cli::from_args() {
- Cli::New { spec } => new(&spec)?,
- Cli::List => list()?,
- Cli::Delete { spec } => delete(&spec)?,
- Cli::CloudInit { spec, dirname } => cloud_init(&spec, &dirname)?,
- Cli::CloudInitIso { spec, iso } => cloud_init_iso(&spec, &iso)?,
- }
- Ok(())
+#[derive(StructOpt, Debug)]
+struct CommonOptions {
+ #[structopt(short, long, parse(from_os_str))]
+ config: Option<PathBuf>,
}
-fn new(spec: &Path) -> anyhow::Result<()> {
- info!("creating new VM");
-
- debug!("reading specification from {}", spec.display());
- let spec = fs::read(spec)?;
- let spec: Specification = serde_yaml::from_slice(&spec)?;
-
- 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)?;
+fn main() -> anyhow::Result<()> {
+ pretty_env_logger::init_custom_env("VMADM_LOG");
+ let cli = Cli::from_args();
+ debug!("{:#?}", cli);
+
+ match cli.cmd {
+ Command::New { common, spec } => {
+ let spec = get_spec(&common, &spec)?;
+ cmd::new(&spec)?;
+ }
- info!("waiting for {} to open its SSH port", spec.name);
- wait_for_port(&spec.name, SSH_PORT)?;
+ Command::List { common } => {
+ let config = config(&common)?;
+ cmd::list(&config)?;
+ }
- Ok(())
-}
+ Command::Delete { common, spec } => {
+ let spec = get_spec(&common, &spec)?;
+ cmd::delete(&spec)?;
+ }
-fn wait_for_port(name: &str, port: i32) -> anyhow::Result<()> {
- let addr = format!("{}:{}", name, port);
- loop {
- if TcpStream::connect(&addr).is_ok() {
- return Ok(());
+ Command::CloudInit {
+ common,
+ 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)?;
}
}
+ Ok(())
}
-fn list() -> anyhow::Result<()> {
- let conn = Connect::open("qemu:///system")?;
- let domains = conn.list_all_domains(0)?;
- for domain in domains {
- let name = domain.get_name()?;
- let (state, _) = domain.get_state()?;
- let state = state_name(state);
- println!("{} {}", name, state);
- }
-
- Ok(())
+fn get_spec(common: &CommonOptions, spec: &Path) -> anyhow::Result<Specification> {
+ let config = config(&common)?;
+ let spec = Specification::from_file(&config, &spec)?;
+ Ok(spec)
}
-fn state_name(state: virt::domain::DomainState) -> String {
- let name = match state {
- virt::domain::VIR_DOMAIN_NOSTATE => "none",
- virt::domain::VIR_DOMAIN_RUNNING => "running",
- virt::domain::VIR_DOMAIN_BLOCKED => "blocked",
- virt::domain::VIR_DOMAIN_PAUSED => "paused",
- virt::domain::VIR_DOMAIN_SHUTDOWN => "shutdown",
- virt::domain::VIR_DOMAIN_SHUTOFF => "shutoff",
- virt::domain::VIR_DOMAIN_CRASHED => "crashed",
- virt::domain::VIR_DOMAIN_PMSUSPENDED => "power management suspended",
- _ => "unknown",
- };
- name.to_string()
+fn config(common: &CommonOptions) -> anyhow::Result<Configuration> {
+ let filename = config_filename(common);
+ let config = Configuration::from_file(&filename)
+ .with_context(|| format!("reading configuration file {}", filename.display()))?;
+ Ok(config)
}
-fn delete(spec: &Path) -> anyhow::Result<()> {
- info!("deleting virtual machine specified in {}", spec.display());
-
- debug!("reading specification from {}", spec.display());
- let spec = fs::read(spec)?;
- let spec: Specification = serde_yaml::from_slice(&spec)?;
-
- 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!("undefine {}", spec.name);
- domain.undefine()?;
-
- debug!("removing image file {}", spec.image.display());
- std::fs::remove_file(&spec.image)?;
+fn config_filename(common: &CommonOptions) -> PathBuf {
+ if let Some(ref filename) = common.config {
+ filename.to_path_buf()
+ } else {
+ if let Some(dirs) = ProjectDirs::from(QUALIFIER, ORG, APP) {
+ dirs.config_dir().join("config.yaml")
+ } else {
+ PathBuf::from("xxx")
}
}
- Ok(())
}
-fn cloud_init(spec: &Path, dirname: &Path) -> anyhow::Result<()> {
- info!("generating cloud-init configuration");
+// fn cloud_init(spec: &Path, dirname: &Path) -> anyhow::Result<()> {
+// info!("generating cloud-init configuration");
- debug!("reading specification from {}", spec.display());
- let spec = fs::read(spec)?;
- let spec: Specification = serde_yaml::from_slice(&spec)?;
- debug!("spec:\n{:#?}", spec);
+// debug!("reading specification from {}", spec.display());
+// let spec = fs::read(spec)?;
+// let spec: Specification = serde_yaml::from_slice(&spec)?;
+// debug!("spec:\n{:#?}", spec);
- info!("creating cloud-init config");
- let init = CloudInitConfig::from(&spec)?;
+// info!("creating cloud-init config");
+// let init = CloudInitConfig::from(&spec)?;
- debug!("creating directory {}", dirname.display());
- std::fs::create_dir_all(dirname)?;
- init.create_dir(dirname)?;
+// debug!("creating directory {}", dirname.display());
+// std::fs::create_dir_all(dirname)?;
+// init.create_dir(dirname)?;
- Ok(())
-}
+// Ok(())
+// }
-fn cloud_init_iso(spec: &Path, iso: &Path) -> anyhow::Result<()> {
- info!("generating cloud-init ISO");
+// fn cloud_init_iso(spec: &Path, iso: &Path) -> anyhow::Result<()> {
+// info!("generating cloud-init ISO");
- debug!("reading specification from {}", spec.display());
- let spec = fs::read(spec)?;
- let spec: Specification = serde_yaml::from_slice(&spec)?;
- debug!("spec:\n{:#?}", spec);
+// debug!("reading specification from {}", spec.display());
+// let spec = fs::read(spec)?;
+// let spec: Specification = serde_yaml::from_slice(&spec)?;
+// debug!("spec:\n{:#?}", spec);
- info!("creating cloud-init config");
- let init = CloudInitConfig::from(&spec)?;
- init.create_iso(iso)?;
+// info!("creating cloud-init config");
+// let init = CloudInitConfig::from(&spec)?;
+// init.create_iso(iso)?;
- Ok(())
-}
+// Ok(())
+// }
diff --git a/src/cloudinit.rs b/src/cloudinit.rs
index 2c521fd..6e62cc5 100644
--- a/src/cloudinit.rs
+++ b/src/cloudinit.rs
@@ -147,7 +147,7 @@ struct Userdata {
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_authorized_keys: spec.ssh_keys.clone(),
ssh_keys: Hostkeys::from(spec),
runcmd: vec![
format!("python3 -c {}", quote(SCRIPT)),
diff --git a/src/cmd/cloud_init.rs b/src/cmd/cloud_init.rs
new file mode 100644
index 0000000..3ab4139
--- /dev/null
+++ b/src/cmd/cloud_init.rs
@@ -0,0 +1,28 @@
+use crate::cloudinit::CloudInitConfig;
+use crate::spec::Specification;
+use log::{debug, info};
+use std::path::Path;
+
+pub fn cloud_init(spec: &Specification, dirname: &Path) -> anyhow::Result<()> {
+ 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) -> anyhow::Result<()> {
+ info!("generating cloud-init ISO into {}", iso.display());
+
+ let init = CloudInitConfig::from(&spec)?;
+ init.create_iso(iso)?;
+
+ Ok(())
+}
diff --git a/src/cmd/delete.rs b/src/cmd/delete.rs
new file mode 100644
index 0000000..ee8f0e9
--- /dev/null
+++ b/src/cmd/delete.rs
@@ -0,0 +1,43 @@
+use crate::spec::Specification;
+use log::{debug, info};
+use std::thread;
+use std::time::Duration;
+use virt::connect::Connect;
+
+pub fn delete(spec: &Specification) -> anyhow::Result<()> {
+ 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!("undefine {}", spec.name);
+ domain.undefine()?;
+
+ debug!("removing image file {}", spec.image.display());
+ std::fs::remove_file(&spec.image)?;
+ }
+ }
+ Ok(())
+}
diff --git a/src/cmd/list.rs b/src/cmd/list.rs
new file mode 100644
index 0000000..e58db3d
--- /dev/null
+++ b/src/cmd/list.rs
@@ -0,0 +1,31 @@
+use crate::config::Configuration;
+
+use virt::connect::Connect;
+
+pub fn list(_config: &Configuration) -> anyhow::Result<()> {
+ let conn = Connect::open("qemu:///system")?;
+ let domains = conn.list_all_domains(0)?;
+ for domain in domains {
+ let name = domain.get_name()?;
+ let (state, _) = domain.get_state()?;
+ let state = state_name(state);
+ println!("{} {}", name, state);
+ }
+
+ Ok(())
+}
+
+fn state_name(state: virt::domain::DomainState) -> String {
+ let name = match state {
+ virt::domain::VIR_DOMAIN_NOSTATE => "none",
+ virt::domain::VIR_DOMAIN_RUNNING => "running",
+ virt::domain::VIR_DOMAIN_BLOCKED => "blocked",
+ virt::domain::VIR_DOMAIN_PAUSED => "paused",
+ virt::domain::VIR_DOMAIN_SHUTDOWN => "shutdown",
+ virt::domain::VIR_DOMAIN_SHUTOFF => "shutoff",
+ virt::domain::VIR_DOMAIN_CRASHED => "crashed",
+ virt::domain::VIR_DOMAIN_PMSUSPENDED => "power management suspended",
+ _ => "unknown",
+ };
+ name.to_string()
+}
diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs
new file mode 100644
index 0000000..ad32e49
--- /dev/null
+++ b/src/cmd/mod.rs
@@ -0,0 +1,11 @@
+pub mod new;
+pub use new::new;
+
+pub mod list;
+pub use list::list;
+
+pub mod delete;
+pub use delete::delete;
+
+pub mod cloud_init;
+pub use cloud_init::{cloud_init, cloud_init_iso};
diff --git a/src/cmd/new.rs b/src/cmd/new.rs
new file mode 100644
index 0000000..9930379
--- /dev/null
+++ b/src/cmd/new.rs
@@ -0,0 +1,47 @@
+use crate::cloudinit::CloudInitConfig;
+use crate::image::VirtualMachineImage;
+use crate::install::{virt_install, VirtInstallArgs};
+use crate::spec::Specification;
+
+use bytesize::GIB;
+use log::info;
+use std::net::TcpStream;
+
+const SSH_PORT: i32 = 22;
+
+pub fn new(spec: &Specification) -> anyhow::Result<()> {
+ 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(())
+}
+
+fn wait_for_port(name: &str, port: i32) -> anyhow::Result<()> {
+ let addr = format!("{}:{}", name, port);
+ loop {
+ if TcpStream::connect(&addr).is_ok() {
+ return Ok(());
+ }
+ }
+}
diff --git a/src/config.rs b/src/config.rs
new file mode 100644
index 0000000..9ba4a7f
--- /dev/null
+++ b/src/config.rs
@@ -0,0 +1,33 @@
+use log::debug;
+use serde::Deserialize;
+use std::default::Default;
+use std::fs;
+use std::path::{Path, PathBuf};
+
+#[derive(Default, Debug, Deserialize)]
+pub struct Configuration {
+ pub default_base_image: Option<PathBuf>,
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum ConfigurationError {
+ #[error(transparent)]
+ IoError(#[from] std::io::Error),
+
+ #[error(transparent)]
+ YamlError(#[from] serde_yaml::Error),
+}
+
+impl Configuration {
+ pub fn from_file(filename: &Path) -> Result<Self, ConfigurationError> {
+ if filename.exists() {
+ debug!("reading configuration file {}", filename.display());
+ let config = fs::read(filename)?;
+ let config: Configuration = serde_yaml::from_slice(&config)?;
+ debug!("config: {:#?}", config);
+ Ok(config)
+ } else {
+ Ok(Self::default())
+ }
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 4cc1d15..b4c07e9 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,4 +1,6 @@
pub mod cloudinit;
+pub mod cmd;
+pub mod config;
pub mod image;
pub mod install;
pub mod spec;
diff --git a/src/spec.rs b/src/spec.rs
index 2cb61f6..0a3d097 100644
--- a/src/spec.rs
+++ b/src/spec.rs
@@ -1,9 +1,13 @@
+use crate::config::Configuration;
+
+use log::debug;
use serde::{Deserialize, Serialize};
-use std::path::PathBuf;
+use std::fs;
+use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
-pub struct Specification {
+struct InputSpecification {
pub name: String,
#[serde(default)]
@@ -18,6 +22,26 @@ pub struct Specification {
pub ed25519_host_key: Option<String>,
pub ed25519_host_cert: Option<String>,
+ pub base: Option<PathBuf>,
+ pub image: PathBuf,
+ pub image_size_gib: u64,
+ pub memory_mib: u64,
+ pub cpus: u64,
+}
+
+#[derive(Debug)]
+pub struct Specification {
+ pub name: String,
+ pub ssh_keys: Vec<String>,
+ pub rsa_host_key: Option<String>,
+ pub rsa_host_cert: Option<String>,
+ pub dsa_host_key: Option<String>,
+ pub dsa_host_cert: Option<String>,
+ pub ecdsa_host_key: Option<String>,
+ pub ecdsa_host_cert: Option<String>,
+ pub ed25519_host_key: Option<String>,
+ pub ed25519_host_cert: Option<String>,
+
pub base: PathBuf,
pub image: PathBuf,
pub image_size_gib: u64,
@@ -27,21 +51,71 @@ pub struct Specification {
#[derive(Debug, thiserror::Error)]
pub enum SpecificationError {
+ #[error("No base image or default base image specified: {0}")]
+ NoBaseImage(PathBuf),
+
+ #[error("Failed to read SSH public key file {0}")]
+ SshKeyRead(PathBuf, #[source] std::io::Error),
+
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
FromUtf8Error(#[from] std::string::FromUtf8Error),
+
+ #[error(transparent)]
+ YamlError(#[from] serde_yaml::Error),
}
impl Specification {
- pub fn ssh_keys(&self) -> Result<String, SpecificationError> {
- let mut keys = String::new();
- for filename in self.ssh_key_files.iter() {
- let key = std::fs::read(filename)?;
- let key = String::from_utf8(key)?;
- keys.push_str(&key);
- }
- Ok(keys)
+ pub fn from_file(
+ config: &Configuration,
+ filename: &Path,
+ ) -> Result<Specification, SpecificationError> {
+ debug!("reading specification from {}", filename.display());
+ let spec = fs::read(filename)?;
+ let input: InputSpecification = serde_yaml::from_slice(&spec)?;
+ debug!("specification as read from file: {:#?}", input);
+
+ 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()));
+ };
+
+ let spec = Specification {
+ name: input.name.clone(),
+ 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,
+ base,
+ image: input.image.clone(),
+ image_size_gib: input.image_size_gib,
+ memory_mib: input.memory_mib,
+ cpus: input.cpus,
+ };
+
+ debug!("specification as with defaults applied: {:#?}", spec);
+ Ok(spec)
+ }
+}
+
+fn ssh_keys(filenames: &[PathBuf]) -> Result<Vec<String>, SpecificationError> {
+ let mut keys = vec![];
+ for filename in filenames {
+ let key = std::fs::read(filename)
+ .map_err(|e| SpecificationError::SshKeyRead(filename.to_path_buf(), e))?;
+ let key = String::from_utf8(key)?;
+ let key = key.strip_suffix("\n").or(Some(&key)).unwrap();
+ keys.push(key.to_string());
}
+ Ok(keys)
}