summaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2021-02-20 08:55:17 +0200
committerLars Wirzenius <liw@liw.fi>2021-02-20 19:39:24 +0200
commita315fab485429c0e4dfd665ced86f51130e3ac3c (patch)
tree8e320e2a4595befaff447005868d47ad266c0463 /src/bin
parent0d10bc096bb4d791b6528d7ca6d450c83cfd1778 (diff)
downloadvmadm-a315fab485429c0e4dfd665ced86f51130e3ac3c.tar.gz
feat: vmadm command to create, list, and delete virtual machines
Includes test suite.
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/tool.rs27
-rw-r--r--src/bin/vmadm.rs156
2 files changed, 156 insertions, 27 deletions
diff --git a/src/bin/tool.rs b/src/bin/tool.rs
deleted file mode 100644
index 18a5c19..0000000
--- a/src/bin/tool.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-use std::path::PathBuf;
-
-use vmadm::cloudinit::CloudInitConfig;
-use vmadm::image::VirtualMachineImage;
-use vmadm::install::{virt_install, VirtInstallArgs};
-
-const BASE_PATH: &'static str = "/home/liw/tmp/debian-10-openstack-amd64.qcow2";
-const IMAGE_PATH: &'static str = "/home/liw/tmp/try-vm.qcow2";
-
-fn main() -> anyhow::Result<()> {
- let mut init = CloudInitConfig::default();
- init.set_hostname("toy-vm");
- init.set_authorized_keys("xxx liw-openpgp xxx");
- println!("init: {:#?}", init);
-
- let base = PathBuf::from(BASE_PATH);
- let image = PathBuf::from(IMAGE_PATH);
- let image = VirtualMachineImage::new_from_base(&base, &image)?;
- image.resize(1024 * 1024 * 1024 * 10)?;
-
- let args = VirtInstallArgs::new("toy-vm", &image, &init);
- println!("{:#?}", args);
- virt_install(&args)?;
- println!("OK");
-
- Ok(())
-}
diff --git a/src/bin/vmadm.rs b/src/bin/vmadm.rs
new file mode 100644
index 0000000..0da9af9
--- /dev/null
+++ b/src/bin/vmadm.rs
@@ -0,0 +1,156 @@
+use bytesize::GIB;
+use log::{debug, info};
+use std::fs;
+use std::net::TcpStream;
+use std::path::{Path, PathBuf};
+use std::thread;
+use std::time::Duration;
+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;
+
+const SSH_PORT: i32 = 22;
+
+#[derive(StructOpt, Debug)]
+enum Cli {
+ New {
+ #[structopt(help = "create a new virtual machine", parse(from_os_str))]
+ spec: PathBuf,
+ },
+ List,
+ Delete {
+ #[structopt(help = "create a new virtual machine", parse(from_os_str))]
+ spec: PathBuf,
+ },
+}
+
+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)?,
+ }
+ Ok(())
+}
+
+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)?;
+
+ debug!("reading specified SSH public keys");
+ let ssh_keys = spec.ssh_keys()?;
+
+ info!("creating cloud-init config");
+ let mut init = CloudInitConfig::default();
+ init.set_hostname(&spec.name);
+ init.set_authorized_keys(&ssh_keys);
+
+ 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 {
+ match TcpStream::connect(&addr) {
+ Ok(_) => return Ok(()),
+ Err(_) => (),
+ }
+ }
+}
+
+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 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 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)?;
+ }
+ }
+ Ok(())
+}