/*! * `House`s are where [crate::waifu::Waifu]s live (the physical hypervisors that * libvirtd connects to */ use crate::waifu::*; use crate::errors::Error; use crate::van::Van; use serde::{Serialize, Deserialize}; use virt::{connect::Connect, domain::Domain}; use std::process::{ExitStatus}; use std::os::unix::process::ExitStatusExt; use rand::Rng; use tokio::{process::Command}; #[derive(Serialize, Deserialize, Debug)] pub enum Address { IP(String), Domain(String) } impl ToString for Address { fn to_string(&self) -> String { match self { Address::IP(s) => s.clone(), Address::Domain(s) => s.clone(), } } } /// Defines a "house" where waifus live #[derive(Debug, Serialize, Deserialize)] pub struct House { /// Hostname pub name: String, /// FQDN or IP address, a way to talk to the house pub address: String, /// Whether or not the House is local (same machine) or remote (networked machine) pub remote: bool, /// Connection to the House, if available #[serde(skip)] con: Option, } impl House { /// Creates a new House based on a libvirtd connect URL /// /// Example: /// ``` /// use waifulib::house::House; /// let mut h: House = House::new("test:///default".to_string()).unwrap(); /// ``` pub fn new(url: String) -> Result { let c = Connect::open(&url.clone())?; let remote = if url.contains("qemu:///") || url.contains("localhost") || url.contains("127.0.0.1") { true } else { false }; // If the connection succeeds, we've got one! Ok(Self { name: c.get_hostname()?, address: c.get_uri()?, remote, con: Some(c), }) } /// Lists the "inhabitants" of the House (the [Waifu]s on the machine /// /// ``` /// use waifulib::house::House; /// let mut h: House = House::new("test:///default".to_string()).unwrap(); /// /// assert_eq!(h.inhabitants().unwrap().len(), 1); /// ``` pub fn inhabitants(&mut self) -> Result, Error> { match &self.con { Some(c) => { let domains = c.list_all_domains(0)?; let mut waifus: Vec = Vec::new(); for d in domains.iter() { waifus.push(d.clone().try_into()?); } Ok(waifus) }, None => { return Err(Error::Connection("Domain connection was None".to_string())); } } } /// Introduces a new Waifu into the House, taking care of everything needed to make the VM run /// fine /// /// If the installation image doesn't exist in the default libvirtd pool, this will fail with /// [`Error::MissingImage`][crate::errors::Error::MissingImage]. /// /// ``` /// use waifulib::house::House /// /// let mut h: House = House::new("test:///default".to_string()).unwrap(); /// /// h.introduce("test-2", 1024, 1, 20000, "test.iso").unwrap(); /// ``` pub async fn introduce(&mut self, name: String, max_mem: Memory, max_cpus: CpuCount, disk_size_mb: u64, van: Van) -> Result { // Check for image on host machine if self.remote { let mut output = Command::new("ssh") .args([ "-oStrictHostKeyChecking=accept-new", &self.address.clone(), "stat", &format!("/var/lib/libvirt/images/{}", van.make_pretty_name().clone()) ]) .output() .await?; if output.status != ExitStatus::from_raw(0) { return Err(Error::MissingImage(van.name.clone())); } // Allocate VM disk output = Command::new("ssh") .args([ "-oStrictHostKeyChecking=accept-new", &self.address.clone(), "qemu-img", "create", "-f", "qcow2", &format!("/var/lib/libvirt/images/{}.qcow2", name.clone()), &format!("{}M", disk_size_mb) ]) .output() .await?; if output.status != ExitStatus::from_raw(0) { return Err(Error::Allocation(String::from_utf8(output.stdout).unwrap())); } } else { // It's local let mut output = Command::new("stat") .args([ &format!("/var/lib/libvirt/images/{}", van.make_pretty_name().clone()), ]) .output() .await?; if output.status != ExitStatus::from_raw(0) { return Err(Error::MissingImage(van.name.clone())); } output = Command::new("qemu-img") .args([ "create", "-f", "qcow2", &format!("/var/lib/libvirt/images/{}.qcow2", name.clone()), &format!("{}M", disk_size_mb) ]) .output() .await?; if output.status != ExitStatus::from_raw(0) { return Err(Error::Allocation(String::from_utf8(output.stdout).unwrap())); } } let uuid = uuid::Uuid::new_v4(); // Let's get that XML ready let mut buf: Vec = vec![]; crate::templates::vm_xml( &mut buf, name.clone(), uuid.to_string(), random_mac().clone(), true, max_mem.0, max_cpus.0, "blank".to_string() )?; let buf = String::from_utf8(buf).unwrap(); println!("{}", buf); let dom: Domain = match &self.con { Some(c) => { let dom = Domain::define_xml(&c, &buf)?; dom.create()?; dom }, None => { return Err(Error::Connection("Connection was None".to_string())); } }; dom.try_into() } } fn random_mac() -> String { let mut addr = rand::thread_rng().gen::<[u8; 6]>(); addr[0] = (addr[0] | 2) & 0xfe; mac_address::MacAddress::new(addr).to_string() } #[cfg(test)] mod test { use super::*; /// Kind of a stupid test, but just a sanity check to make sure that [ToString] impl up above /// is still working #[test] fn addr_to_string() { let a: Address = Address::IP("127.0.0.1".to_string()); let d: Address = Address::Domain("example.com".to_string()); assert_eq!(String::from("127.0.0.1"), a.to_string()); assert_eq!("example.com".to_string(), d.to_string()); } #[test] fn connect_to_house() { let _: House = House::new("test:///default".to_string()).unwrap(); } #[test] fn list_inhabitants() { let mut h: House = House::new("test:///default".to_string()).unwrap(); assert_eq!(h.inhabitants().unwrap().len(), 1); } }