diff options
author | Lars Wirzenius <liw@liw.fi> | 2021-02-20 08:55:17 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2021-02-20 19:39:24 +0200 |
commit | a315fab485429c0e4dfd665ced86f51130e3ac3c (patch) | |
tree | 8e320e2a4595befaff447005868d47ad266c0463 /src/bin | |
parent | 0d10bc096bb4d791b6528d7ca6d450c83cfd1778 (diff) | |
download | vmadm-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.rs | 27 | ||||
-rw-r--r-- | src/bin/vmadm.rs | 156 |
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(()) +} |