//! An abstraction on top of the libvirt bindings. use crate::util::wait_for_ssh; use log::debug; use std::path::{Path, PathBuf}; use std::thread; use std::time::Duration; use virt::connect::Connect; use virt::domain::Domain; use virt::sys::{VIR_DOMAIN_AFFECT_CONFIG, VIR_DOMAIN_AFFECT_CURRENT, VIR_DOMAIN_AFFECT_LIVE}; /// Errors from this module. #[derive(Debug, thiserror::Error)] pub enum VirtError { /// Error connecting to libvirtd. #[error("couldn't connect to the libvirt daemon")] Connect(#[source] virt::error::Error), /// Error listing domains. #[error("couldn't list all domains")] Domains(#[source] virt::error::Error), /// Error listing domains. #[error("couldn't get name of domain")] GetName(#[source] virt::error::Error), /// Error checking if domain is active. #[error("couldn't check is domain {0} is active")] IsActive(String, #[source] virt::error::Error), /// Error getting domain's XML description. #[error("couldn't get domain's XML description: {0}")] GetXml(String, #[source] virt::error::Error), /// Error detaching cloud-init ISO from domain #[error("couldn't detach cloud-init ISO file from domain {0}")] DetachIso(String, #[source] virt::error::Error), /// Error detaching drive from domain #[error("couldn't create domain {0}")] Create(String, #[source] virt::error::Error), /// Error shutting down domain #[error("couldn't shut down domain {0}")] Shutdown(String, #[source] virt::error::Error), /// Error undefining domain #[error("couldn't undefine domain {0}")] Undefine(String, #[source] virt::error::Error), /// Error undefining domain #[error("couldn't set domain {0} to be autostarted")] Autostart(String, #[source] virt::error::Error), /// Failed to delete image file. #[error("failed to delete image file {0}")] DeleteImage(PathBuf, #[source] std::io::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).map_err(VirtError::Connect)?; Ok(Self { conn }) } fn get_domains(&self) -> Result, VirtError> { debug!("listing all domains"); self.conn.list_all_domains(0).map_err(VirtError::Domains) } fn get_domain(&self, name: &str) -> Result, VirtError> { for domain in self.get_domains()? { if get_name(&domain)? == 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(get_name(&domain)?); } Ok(ret) } pub fn is_active(&self, name: &str) -> Result { if let Some(domain) = self.get_domain(name)? { Ok(is_active(&domain, name)?) } 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 = get_xml(&domain, name)?; let disk = find_iso_xml(&xml); let flags = VIR_DOMAIN_AFFECT_CONFIG | VIR_DOMAIN_AFFECT_CURRENT | VIR_DOMAIN_AFFECT_LIVE; if !disk.is_empty() { domain .detach_device_flags(&disk, flags) .map_err(|err| VirtError::DetachIso(name.to_string(), err))?; } } Ok(()) } pub fn trigger_start(&self, name: &str) -> Result<(), VirtError> { if let Some(domain) = self.get_domain(name)? { if !is_active(&domain, name)? { debug!("starting {}", name); domain .create() .map_err(|err| VirtError::Create(name.to_string(), err))?; } } Ok(()) } pub fn start(&self, name: &str) -> Result<(), VirtError> { if self.get_domain(name)?.is_some() { self.trigger_start(name)?; wait_for_ssh(name); } Ok(()) } pub fn trigger_shutdown(&self, name: &str) -> Result<(), VirtError> { if let Some(domain) = self.get_domain(name)? { debug!("asking {} to shut down", name); if is_active(&domain, name)? { domain .shutdown() .map_err(|err| VirtError::Shutdown(name.to_string(), err))?; } } Ok(()) } pub fn shutdown(&self, name: &str) -> Result<(), VirtError> { if let Some(domain) = self.get_domain(name)? { debug!("shutting down {}", name); self.trigger_shutdown(name)?; wait_until_inactive(&domain, name); } Ok(()) } pub fn delete(&self, name: &str, image: &Path) -> Result<(), VirtError> { if let Some(domain) = self.get_domain(name)? { self.shutdown(name)?; debug!("undefine {}", name); domain .undefine() .map_err(|err| VirtError::Undefine(name.to_string(), err))?; debug!("removing image file {}", image.display()); std::fs::remove_file(image) .map_err(|e| VirtError::DeleteImage(image.to_path_buf(), e))?; } Ok(()) } pub fn set_autostart(&self, name: &str, autostart: bool) -> Result<(), VirtError> { if let Some(domain) = self.get_domain(name)? { domain .set_autostart(autostart) .map_err(|err| VirtError::Autostart(name.to_string(), err))?; } Ok(()) } } fn get_name(domain: &Domain) -> Result { let name = domain.get_name().map_err(VirtError::GetName)?; Ok(name) } fn is_active(domain: &Domain, name: &str) -> Result { let is = domain .is_active() .map_err(|err| VirtError::IsActive(name.to_string(), err))?; Ok(is) } fn get_xml(domain: &Domain, name: &str) -> Result { let is = domain .get_xml_desc(0) .map_err(|err| VirtError::GetXml(name.to_string(), err))?; Ok(is) } 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 disk.contains(".iso") { return disk.to_string(); } xml = &xml[end..]; } "".to_string() }