aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCara Salter <cara@devcara.com>2022-04-20 13:08:28 -0400
committerCara Salter <cara@devcara.com>2022-04-20 13:08:28 -0400
commit6b995785c780dd47cb0e02821001f446cf4ec211 (patch)
tree2bbe9147151056527468c1dcf70b6b98140ccb40
parent5978befd317189f1f18dddbab3db7ddd0061c236 (diff)
downloadsolarlib-6b995785c780dd47cb0e02821001f446cf4ec211.tar.gz
solarlib-6b995785c780dd47cb0e02821001f446cf4ec211.zip
house: Allow for the introduction of new VMs
Templates the XML and creates the disk images
-rw-r--r--Cargo.toml19
-rw-r--r--src/build.rs5
-rw-r--r--src/errors.rs7
-rw-r--r--src/house.rs115
-rw-r--r--src/lib.rs2
-rw-r--r--src/waifu.rs52
-rw-r--r--templates/vm.rs.xml83
7 files changed, 279 insertions, 4 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 3b329be..76d3b8a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -5,14 +5,33 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+build = "src/build.rs"
+
[dependencies]
virt = { git = "https://gitlab.com/libvirt/libvirt-rust.git", rev = "10456b6e59ec73e8ef418cf0a29a9bf33be8ded6" }
thiserror = "1"
+rand = "0.8"
+mac_address = "1"
+
[dependencies.serde]
version = "1"
features = [
"derive"
]
+
+[dependencies.tokio]
+version = "1"
+features = [ "full" ]
+
+[dependencies.uuid]
+version = "1"
+features = [
+ "serde",
+ "v4"
+ ]
+
+[build-dependencies]
+ructe = "0.14"
diff --git a/src/build.rs b/src/build.rs
new file mode 100644
index 0000000..9a6d3c3
--- /dev/null
+++ b/src/build.rs
@@ -0,0 +1,5 @@
+use ructe::{Result, Ructe};
+
+fn main() -> Result<()> {
+ Ructe::from_env()?.compile_templates("templates")
+}
diff --git a/src/errors.rs b/src/errors.rs
index 3a0eedd..29f7f4f 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -8,4 +8,11 @@ pub enum Error {
Other(String),
#[error("Missing connection: {0}")]
Connection(String),
+ #[error("Missing image: {0}")]
+ MissingImage(String),
+ #[error("Could not allocate VM storage: {0}")]
+ Allocation(String),
+ #[error("I/O: {0}")]
+ Io(#[from] std::io::Error),
}
+
diff --git a/src/house.rs b/src/house.rs
index 06dd62f..ad6e055 100644
--- a/src/house.rs
+++ b/src/house.rs
@@ -5,7 +5,12 @@
use crate::waifu::*;
use crate::errors::Error;
use serde::{Serialize, Deserialize};
-use virt::connect::Connect;
+use virt::{connect::Connect, domain::Domain};
+use std::process::{ExitStatus};
+use std::os::unix::process::ExitStatusExt;
+use rand::Rng;
+
+use tokio::{process::Command, task::spawn_blocking};
#[derive(Serialize, Deserialize, Debug)]
pub enum Address {
@@ -36,6 +41,13 @@ pub struct House {
}
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<Self, Error> {
let mut c = Connect::open(&url.clone())?;
@@ -47,6 +59,14 @@ impl House {
})
}
+ /// 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<Vec<Waifu>, Error> {
match &self.con {
Some(c) => {
@@ -65,10 +85,97 @@ impl House {
}
}
- /// TODO: Implement and figure out what the hell I need to define one of these
- pub fn introduce(&mut self, name: String, max_mem: Memory, max_cpus: CpuCount) -> Result<&Waifu, Error> {
- unimplemented!();
+ /// 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, image_name: String) -> Result<Waifu, Error> {
+ // Check for image on host machine
+
+ let mut output = Command::new("ssh")
+ .args([
+ "-oStrictHostKeyChecking=accept-new",
+ &self.address.clone(),
+ "stat",
+ &format!("/var/lib/libvirt/images/{}", image_name.clone())
+ ])
+ .output()
+ .await?;
+
+ if output.status != ExitStatus::from_raw(0) {
+ return Err(Error::MissingImage(image_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()));
+ }
+
+ let uuid = uuid::Uuid::new_v4();
+
+ // Let's get that XML ready
+ let mut buf: Vec<u8> = vec![];
+
+ crate::templates::vm_xml(
+ &mut buf,
+ name.clone(),
+ uuid.to_string(),
+ random_mac().clone(),
+ true,
+ max_mem.count(),
+ max_cpus.count(),
+ "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)]
diff --git a/src/lib.rs b/src/lib.rs
index 6ba4038..2ee2274 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,3 +3,5 @@ pub mod errors;
pub mod waifu;
pub mod house;
+
+include!(concat!(env!("OUT_DIR"), "/templates.rs"));
diff --git a/src/waifu.rs b/src/waifu.rs
index 55ff4ed..da9653f 100644
--- a/src/waifu.rs
+++ b/src/waifu.rs
@@ -15,6 +15,12 @@ impl From<u64> for Memory {
Self(u)
}
}
+
+impl Memory {
+ pub fn count(&self) -> u64 {
+ self.0
+ }
+}
/**
* Defines the number of vCPUs a waifu has
*/
@@ -27,6 +33,11 @@ impl From<u64> for CpuCount {
}
}
+impl CpuCount {
+ pub fn count(&self) -> u64 {
+ self.0
+ }
+}
/**
* Represents a virtual machine, that's active on some server
*
@@ -59,6 +70,47 @@ impl PartialEq for Waifu {
}
}
+impl TryFrom<Domain> for Waifu {
+ type Error = Error;
+
+ fn try_from(d: Domain) -> Result<Self, Self::Error> {
+ let c = d.get_connect()?;
+
+ // This... feels wrong
+ //
+ // I know it probably works
+ //
+ // Based on code by Cadey in waifud
+ let addr: Option<String> = if d.is_active()? {
+ let mut addr: Vec<String> = d
+ .interface_addresses(virt::domain::VIR_DOMAIN_INTERFACE_ADDRESSES_SRC_LEASE, 0)?
+ .into_iter()
+ .map(|iface| iface.addrs.clone())
+ .filter(|addrs| addrs.get(0).is_some())
+ .map(|addrs| addrs.get(0).unwrap().clone().addr)
+ .collect();
+
+ if addr.get(0).is_none() {
+ Some(String::from("localhost"))
+ } else {
+ Some(addr.swap_remove(0))
+ }
+ } else {
+ None
+ };
+
+ Ok(Self {
+ name: d.get_name()?,
+ host: c.get_hostname()?,
+ addr,
+ uuid: d.get_uuid_string()?,
+ mem: d.get_max_memory()?.into(),
+ cpu_count: d.get_max_vcpus()?.into(),
+ })
+ }
+}
+
+
impl TryFrom<&Domain> for Waifu {
type Error = Error;
diff --git a/templates/vm.rs.xml b/templates/vm.rs.xml
new file mode 100644
index 0000000..2004b35
--- /dev/null
+++ b/templates/vm.rs.xml
@@ -0,0 +1,83 @@
+@(name: String, uuid: String, mac_address: String, sata: bool, memory: u64, cpus: u64, seed: String)
+<domain type="kvm" xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
+ <name>@name</name>
+ <uuid>@uuid</uuid>
+ <metadata>
+ <libosinfo:libosinfo xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
+ <libosinfo:os id="http://nixos.org/nixos/unstable"/>
+ </libosinfo:libosinfo>
+ </metadata>
+ <memory>@memory</memory>
+ <currentMemory>@memory</currentMemory>
+ <vcpu>@cpus</vcpu>
+ <os>
+ <type arch="x86_64" machine="q35">hvm</type>
+ <boot dev="hd"/>
+ </os>
+ <features>
+ <acpi/>
+ <apic/>
+ <vmport state="off"/>
+ </features>
+ <cpu mode="host-model"/>
+ <clock offset="utc">
+ <timer name="rtc" tickpolicy="catchup"/>
+ <timer name="pit" tickpolicy="delay"/>
+ <timer name="hpet" present="no"/>
+ </clock>
+ <on_poweroff>destroy</on_poweroff>
+ <on_reboot>restart</on_reboot>
+ <on_crash>destroy</on_crash>
+ <pm>
+ <suspend-to-mem enabled="no"/>
+ <suspend-to-disk enabled="no"/>
+ </pm>
+ <devices>
+ <emulator>/run/libvirt/nix-emulators/qemu-system-x86_64</emulator>
+ <disk type="block" device="disk">
+ <driver name="qemu" type="raw" cache="none" io="native"/>
+ <source file="/var/lib/libvirt/images/@name\.qcow2"/>
+ @if sata {
+ <target dev="sda" bus="sata"/>
+ } else {
+ <target dev="vda" bus="virtio"/>
+ }
+ </disk>
+ <controller type="usb" model="qemu-xhci" ports="15"/>
+ <interface type="network">
+ <source network="default"/>
+ <mac address="@mac_address"/>
+ @if sata {
+ <model type="e1000e"/>
+ <address type="pci" domain="0x0000" bus="0x01" slot="0x00" function="0x0"/>
+ } else {
+ <model type="virtio"/>
+ }
+ </interface>
+ <console type="pty"/>
+ <channel type="unix">
+ <source mode="bind"/>
+ <target type="virtio" name="org.qemu.guest_agent.0"/>
+ </channel>
+ <channel type="spicevmc">
+ <target type="virtio" name="com.redhat.spice.0"/>
+ </channel>
+ <input type="tablet" bus="usb"/>
+ <graphics type="spice" port="-1" tlsPort="-1" autoport="yes"/>
+ <sound model="ich9"/>
+ <video>
+ <model type="qxl"/>
+ </video>
+ <redirdev bus="usb" type="spicevmc"/>
+ <redirdev bus="usb" type="spicevmc"/>
+ <memballoon model="virtio"/>
+ <rng model="virtio">
+ <backend model="random">/dev/urandom</backend>
+ </rng>
+ </devices>
+ <qemu:commandline>
+ <qemu:arg value="-smbios" />
+ <qemu:arg value="type=1,serial=ds=nocloud-net;s=@seed" />
+ </qemu:commandline>
+</domain>
+