//! An abstraction on top of the libvirt bindings. use crate::util::wait_for_ssh; use log::debug; use std::path::Path; use std::thread; use std::time::Duration; use virt::connect::Connect; use virt::domain::{ Domain, VIR_DOMAIN_AFFECT_CONFIG, VIR_DOMAIN_AFFECT_CURRENT, VIR_DOMAIN_AFFECT_LIVE, }; /// Errors from this module. #[derive(Debug, thiserror::Error)] pub enum VirtError { /// Error creating virtual machine. #[error(transparent)] VirtError(#[from] virt::error::Error), /// Error doing I/O. #[error(transparent)] IoError(#[from] std::io::Error), } /// Access libvirt for all the things this program needs. pub struct Libvirt { conn: Connect, } impl Libvirt { pub fn connect(url: &str) -> Result { debug!("connecting to libvirtd {}", url); let conn = Connect::open(url)?; Ok(Self { conn }) } fn get_domains(&self) -> Result, VirtError> { debug!("listing all domains"); Ok(self.conn.list_all_domains(0)?) } fn get_domain(&self, name: &str) -> Result, VirtError> { for domain in self.get_domains()? { if domain.get_name()? == name { return Ok(Some(domain)); } } Ok(None) } pub fn names(&self) -> Result, VirtError> { let mut ret = vec![]; for domain in self.get_domains()? { ret.push(domain.get_name()?); } Ok(ret) } pub fn is_active(&self, name: &str) -> Result { if let Some(domain) = self.get_domain(name)? { Ok(domain.is_active()?) } else { Ok(false) } } pub fn wait_for_inactive(&self, name: &str) -> Result<(), VirtError> { loop { if !self.is_active(name)? { break; } } Ok(()) } pub fn detach_cloud_init_iso(&self, name: &str) -> Result<(), VirtError> { if let Some(domain) = self.get_domain(name)? { debug!("detaching cloud-init ISO from {}", name); let xml = domain.get_xml_desc(0)?; let disk = find_iso_xml(&xml); let flags = VIR_DOMAIN_AFFECT_CONFIG | VIR_DOMAIN_AFFECT_CURRENT | VIR_DOMAIN_AFFECT_LIVE; if disk.len() > 0 { domain.detach_device_flags(&disk, flags)?; } } Ok(()) } pub fn start(&self, name: &str) -> Result<(), VirtError> { if let Some(domain) = self.get_domain(name)? { domain.create()?; wait_for_ssh(name); } Ok(()) } pub fn shutdown(&self, name: &str) -> Result<(), VirtError> { if let Some(domain) = self.get_domain(name)? { domain.shutdown()?; wait_until_inactive(&domain, name); } Ok(()) } pub fn delete(&self, name: &str, image: &Path) -> Result<(), VirtError> { if let Some(domain) = self.get_domain(name)? { debug!("shutting down {}", name); domain.shutdown().ok(); wait_until_inactive(&domain, name); debug!("undefine {}", name); domain.undefine()?; debug!("removing image file {}", image.display()); std::fs::remove_file(image)?; } Ok(()) } } fn wait_until_inactive(domain: &Domain, name: &str) { debug!("waiting for domain {} to become inactive", name); 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!("domain {} is still running", name); } } // This is a HACK. The XML description of a domain contains // descriptions of attached virtual disks. We find one that contains // ".iso", and return that. // // // // // // // // //
// fn find_iso_xml(xml: &str) -> String { let mut xml = xml; loop { let start = xml.find(""); if end.is_none() { break; } let end = end.unwrap(); let disk = &xml[..end + 7]; if let Some(_) = disk.find(".iso") { return disk.to_string(); } xml = &xml[end..]; } "".to_string() }