diff --git a/config/example.toml b/config/example.toml index 90cd5f6..68c937b 100644 --- a/config/example.toml +++ b/config/example.toml @@ -5,7 +5,6 @@ features = [ "uefi", "spice", "pulse", - # "scream", "looking-glass" ] @@ -43,4 +42,4 @@ addr = "0b:00.3" [looking-glass] width = 2560 -height = 1080 +height = 1080 \ No newline at end of file diff --git a/config/global.toml b/config/global.toml index 874bb9c..7a6e2b3 100644 --- a/config/global.toml +++ b/config/global.toml @@ -1,3 +1,6 @@ +[vore] +group = "vore" + [qemu] script = "qemu.lua" diff --git a/config/qemu.lua b/config/qemu.lua index 0c21779..3d3e031 100644 --- a/config/qemu.lua +++ b/config/qemu.lua @@ -126,7 +126,7 @@ vore:set_build_command(function(instance, vm) end if instance.pulse.enabled then - vm:arg("-device", "intel-hda", "-device", "hda-duplex") + vm:arg("-device", "intel-hda", "-device", "hda-duplex,audiodev=pa0") vm:arg("-audiodev", "pa,server=/run/user/1000/pulse/native,id=pa0") end diff --git a/vore-core/Cargo.toml b/vore-core/Cargo.toml index 82d9b03..7805da7 100644 --- a/vore-core/Cargo.toml +++ b/vore-core/Cargo.toml @@ -4,7 +4,10 @@ version = "0.1.0" authors = ["eater <=@eater.me>"] edition = "2018" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["client"] +host = ["qapi", "qapi-qmp", "mlua"] +client = [] [dependencies] config = { version = "0.11.0", default-features = false, features = ["toml"] } @@ -13,10 +16,10 @@ serde_json = "1.0.64" toml = "*" anyhow = "1.0.40" kiam = "0.1" -mlua = { version = "0.5.3", features = ["lua54", "serialize", "send"] } +mlua = { optional = true, version = "0.5.3", features = ["lua54", "serialize", "send"] } beau_collector = "0.2.1" -qapi-qmp = "0.7.0" -qapi = { version = "0.7.0", features = ["qapi-qmp"] } +qapi-qmp = { optional = true, version = "0.7.0" } +qapi = { optional = true, version = "0.7.0", features = ["qapi-qmp"] } libc = "0.2.94" lazy_static = "1.4.0" paste = "1.0" diff --git a/vore-core/src/consts.rs b/vore-core/src/consts.rs new file mode 100644 index 0000000..4938104 --- /dev/null +++ b/vore-core/src/consts.rs @@ -0,0 +1,21 @@ +// File with all static constants like e.g. paths +#![allow(clippy::manual_unwrap_or)] + +macro_rules! default_env { + ($val:expr, $def:expr) => { + match option_env!($val) { + None => $def, + Some(x) => x, + }; + }; +} + +pub const VORE_DIRECTORY: &str = default_env!("VORE_DIRECTORY", "/var/lib/vore"); +pub const VORE_SOCKET: &str = default_env!("VORE_SOCKET", "/run/vore.sock"); +#[cfg(debug_assertions)] +pub const VORE_CONFIG: &str = default_env!( + "VORE_CONFIG", + concat!(file!(), "/../../../../config/vored.toml") +); +#[cfg(not(debug_assertions))] +pub const VORE_CONFIG: &str = default_env!("VORE_CONFIG", "/etc/vore/vored.toml"); diff --git a/vore-core/src/cpu_list.rs b/vore-core/src/cpu_list.rs index 9e871f7..371ba6c 100644 --- a/vore-core/src/cpu_list.rs +++ b/vore-core/src/cpu_list.rs @@ -19,7 +19,7 @@ lazy_static! { pub fn get_cpus() -> Vec { if cfg!(target_os = "linux") { - return crate::cpu_list::linux::get_cpus(); + crate::cpu_list::linux::get_cpus() } else { unimplemented!(); } @@ -32,7 +32,7 @@ pub struct CpuList { impl CpuList { pub fn _get() -> CpuList { - return *CPU_LIST; + *CPU_LIST } pub fn _amount() -> usize { diff --git a/vore-core/src/global_config.rs b/vore-core/src/global_config.rs index 0e71f4f..d5c07d2 100644 --- a/vore-core/src/global_config.rs +++ b/vore-core/src/global_config.rs @@ -1,15 +1,69 @@ use anyhow::Context; use serde::{Deserialize, Serialize}; use std::collections::HashMap; - -pub const GLOBAL_CONFIG_LOCATION: &str = "/home/eater/projects/vored/config/global.toml"; +use std::ffi::CString; +use std::fs; +use std::fs::Permissions; +use std::os::unix::fs::MetadataExt; +use std::os::unix::fs::PermissionsExt; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct GlobalConfig { + pub vore: GlobalVoreConfig, pub qemu: GlobalQemuConfig, pub uefi: HashMap, } +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all(deserialize = "kebab-case"))] +pub struct GlobalVoreConfig { + #[serde(default)] + pub group: Option, + #[serde(default)] + pub unix_group_id: Option, +} + +impl GlobalVoreConfig { + pub fn get_gid(&mut self) -> Result, anyhow::Error> { + if let Some(id) = self.unix_group_id { + return Ok(Some(id)); + } + + let name = self.group.as_ref().cloned(); + + name.map(|group_name| { + let group_name_c = CString::new(group_name.as_str())?; + Ok(unsafe { + let group = libc::getgrnam(group_name_c.as_ptr()); + if group.is_null() { + anyhow::bail!("No group found with the name '{}'", group_name); + } + + let gid = (*group).gr_gid; + + self.unix_group_id = Some(gid); + + gid + }) + }) + .transpose() + } + + pub fn chown(&mut self, path: &str) -> Result<(), anyhow::Error> { + if let Some(gid) = self.get_gid()? { + let meta = fs::metadata(path)?; + let path_c = CString::new(path)?; + unsafe { + libc::chown(path_c.as_ptr(), meta.uid(), gid); + } + + fs::set_permissions(path, Permissions::from_mode(0o774))?; + } + + Ok(()) + } +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub struct GlobalQemuConfig { pub script: String, diff --git a/vore-core/src/instance_config.rs b/vore-core/src/instance_config.rs index 3dc37ea..b9dd67f 100644 --- a/vore-core/src/instance_config.rs +++ b/vore-core/src/instance_config.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Error}; use config::{Config, File, FileFormat, Value}; -use serde::de::{Visitor}; +use serde::de::Visitor; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; @@ -12,6 +12,7 @@ pub struct InstanceConfig { pub arch: String, pub chipset: String, pub kvm: bool, + pub auto_start: bool, pub memory: u64, pub cpu: CpuConfig, pub disks: Vec, @@ -46,6 +47,10 @@ impl InstanceConfig { instance_config.memory = parse_size(&mem)?; } + if let Ok(auto_start) = config.get_bool("machine.auto-start") { + instance_config.auto_start = auto_start; + } + if let Ok(cpu) = config.get_table("cpu") { instance_config.cpu.apply_table(cpu)? } @@ -74,16 +79,15 @@ impl InstanceConfig { } } - instance_config.looking_glass = LookingGlassConfig::from_table( - config.get_table("looking-glass").unwrap_or_default(), - )?; - instance_config.scream = ScreamConfig::from_table( - config.get_table("scream").unwrap_or_default(), - )?; + instance_config.looking_glass = + LookingGlassConfig::from_table(config.get_table("looking-glass").unwrap_or_default())?; + instance_config.scream = + ScreamConfig::from_table(config.get_table("scream").unwrap_or_default())?; instance_config.spice = SpiceConfig::from_table(config.get_table("spice").unwrap_or_default())?; - instance_config.pulse = PulseConfig::from_table(config.get_table("pulse").unwrap_or_default())?; + instance_config.pulse = + PulseConfig::from_table(config.get_table("pulse").unwrap_or_default())?; if let Ok(features) = config.get::>("machine.features") { for feature in features { @@ -109,6 +113,7 @@ impl Default for InstanceConfig { arch: std::env::consts::ARCH.to_string(), chipset: "q35".to_string(), kvm: true, + auto_start: false, // 2 GB memory: 2 * 1024 * 1024 * 1024, cpu: Default::default(), @@ -191,23 +196,19 @@ impl CpuConfig { if !table.contains_key("amount") { self.amount = self.sockets * self.dies * self.cores * self.threads; - } else { - if table - .keys() - .any(|x| ["cores", "sockets", "dies", "threads"].contains(&x.as_str())) - { - let calc_amount = self.sockets * self.dies * self.cores * self.threads; - if self.amount != calc_amount { - Err(anyhow::Error::msg(format!("Amount of cpu's ({}) from sockets ({}), dies ({}), cores ({}) and threads ({}) differs from specified ({}) cpu's", calc_amount, self.sockets, self.dies, self.cores, self.threads, self.amount)))?; - } - } else { - if (self.amount % 2) == 0 { - self.cores = self.amount / 2; - } else { - self.threads = 1; - self.cores = self.amount; - } + } else if table + .keys() + .any(|x| ["cores", "sockets", "dies", "threads"].contains(&x.as_str())) + { + let calc_amount = self.sockets * self.dies * self.cores * self.threads; + if self.amount != calc_amount { + return Err(anyhow::Error::msg(format!("Amount of cpu's ({}) from sockets ({}), dies ({}), cores ({}) and threads ({}) differs from specified ({}) cpu's", calc_amount, self.sockets, self.dies, self.cores, self.threads, self.amount))); } + } else if (self.amount % 2) == 0 { + self.cores = self.amount / 2; + } else { + self.threads = 1; + self.cores = self.amount; } Ok(()) @@ -240,7 +241,7 @@ fn parse_size(orig_input: &str) -> Result { input = &input[..input.len() - 1]; } - if input.len() == 0 { + if input.is_empty() { return Err(anyhow::Error::msg(format!( "'{}' is not a valid size", orig_input @@ -286,9 +287,7 @@ pub struct ScreamConfig { } impl ScreamConfig { - pub fn from_table( - table: HashMap, - ) -> Result { + pub fn from_table(table: HashMap) -> Result { let mut cfg = ScreamConfig::default(); if let Some(enabled) = table.get("enabled").cloned() { cfg.enabled = enabled.into_bool()?; @@ -368,9 +367,7 @@ impl LookingGlassConfig { self.buffer_size = buffer_size; } - pub fn from_table( - table: HashMap, - ) -> Result { + pub fn from_table(table: HashMap) -> Result { let mut cfg = LookingGlassConfig::default(); if let Some(enabled) = table.get("enabled").cloned() { @@ -434,12 +431,14 @@ impl DiskConfig { }).to_string() }; - let preset = table.get("preset") + let preset = table + .get("preset") .cloned() .context("Every disk should have a preset set")? .into_str()?; - let read_only = table.get("read-only") + let read_only = table + .get("read-only") .cloned() .map(|x| x.into_bool()) .transpose() @@ -459,15 +458,16 @@ impl DiskConfig { #[derive(Deserialize, Serialize, Clone, Debug)] pub struct VfioConfig { - pub address: PCIAddress, + pub address: PciAddress, pub vendor: Option, pub device: Option, pub index: u32, pub graphics: bool, pub multifunction: bool, + pub reserve: bool, } -pub fn read_pci_ids(addr: &PCIAddress) -> Result<(u32, u32), anyhow::Error> { +pub fn read_pci_ids(addr: &PciAddress) -> Result<(u32, u32), anyhow::Error> { let device = std::fs::read_to_string(format!("/sys/bus/pci/devices/{:#}/device", addr)) .with_context(|| { format!( @@ -497,7 +497,7 @@ impl VfioConfig { .get("addr") .or_else(|| table.get("address")) .cloned() - .map(|x| PCIAddress::from_str(&x.into_str()?)) + .map(|x| PciAddress::from_str(&x.into_str()?)) .transpose()?; let vendor = table @@ -548,7 +548,7 @@ impl VfioConfig { (None, Some(vendor), Some(device)) => { let mut counter = index; - let mut items: Vec<(PCIAddress, u32, u32)> = vec![]; + let mut items: Vec<(PciAddress, u32, u32)> = vec![]; for entry in std::fs::read_dir("/sys/bus/pci/devices")? { let entry = entry?; @@ -556,7 +556,7 @@ impl VfioConfig { let addr_name = file_name .to_str() .ok_or_else(|| anyhow::anyhow!("Failed to parse PCI device name"))?; - let addr = PCIAddress::from_str(addr_name)?; + let addr = PciAddress::from_str(addr_name)?; let (found_vendor, found_device) = read_pci_ids(&addr)?; items.push((addr, found_vendor, found_device)); } @@ -596,6 +596,7 @@ impl VfioConfig { index: 0, graphics: false, multifunction: false, + reserve: false, }; if let Some(graphics) = table.get("graphics").cloned() { @@ -606,6 +607,10 @@ impl VfioConfig { cfg.multifunction = multifunction.into_bool()?; } + if let Some(reserve) = table.get("reserve").cloned() { + cfg.reserve = reserve.into_bool()?; + } + Ok(cfg) } } @@ -617,9 +622,7 @@ pub struct PulseConfig { impl PulseConfig { pub fn from_table(table: HashMap) -> Result { - let mut cfg = PulseConfig { - enabled: false, - }; + let mut cfg = PulseConfig { enabled: false }; if let Some(enabled) = table.get("enabled").cloned() { cfg.enabled = enabled.into_bool()?; @@ -629,7 +632,6 @@ impl PulseConfig { } } - #[derive(Deserialize, Serialize, Clone, Debug, Default)] pub struct SpiceConfig { pub enabled: bool, @@ -656,17 +658,17 @@ impl SpiceConfig { } #[derive(Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] -pub struct PCIAddress { +pub struct PciAddress { domain: u32, bus: u8, slot: u8, func: u8, } -impl<'de> Deserialize<'de> for PCIAddress { +impl<'de> Deserialize<'de> for PciAddress { fn deserialize(deserializer: D) -> Result>::Error> - where - D: Deserializer<'de>, + where + D: Deserializer<'de>, { struct X; impl Visitor<'_> for X { @@ -676,8 +678,10 @@ impl<'de> Deserialize<'de> for PCIAddress { formatter.write_str("Expecting a string") } - fn visit_str(self, v: &str) -> Result where - E: de::Error, { + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { Ok(v.to_string()) } @@ -687,21 +691,22 @@ impl<'de> Deserialize<'de> for PCIAddress { } let x = deserializer.deserialize_string(X)?; - Ok(PCIAddress::from_str(&x).map_err(|x| de::Error::custom(x))?) + + PciAddress::from_str(&x).map_err(de::Error::custom) } } -impl Serialize for PCIAddress { +impl Serialize for PciAddress { fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> - where - S: Serializer, + where + S: Serializer, { - serializer.serialize_str(&self.to_string()) + serializer.serialize_str(&self.to_pci_string()) } } -impl PCIAddress { - fn to_string(&self) -> String { +impl PciAddress { + fn to_pci_string(&self) -> String { format!( "{:04x}:{:02x}:{:02x}.{:x}", self.domain, self.bus, self.slot, self.func @@ -709,7 +714,7 @@ impl PCIAddress { } } -impl Debug for PCIAddress { +impl Debug for PciAddress { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str("PCIAddress(")?; if f.alternate() && self.domain == 0 { @@ -724,7 +729,7 @@ impl Debug for PCIAddress { } } -impl Display for PCIAddress { +impl Display for PciAddress { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if f.alternate() && self.domain == 0 { f.write_str(&format!("{:04x}:", self.domain))?; @@ -737,15 +742,15 @@ impl Display for PCIAddress { } } -impl FromStr for PCIAddress { +impl FromStr for PciAddress { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let mut rev = s.rsplit(":"); - let mut addr = PCIAddress::default(); + let mut rev = s.rsplit(':'); + let mut addr = PciAddress::default(); if let Some(slot_and_func) = rev.next() { - let mut splitter = slot_and_func.split("."); + let mut splitter = slot_and_func.split('.'); if let Some(slot) = splitter.next() { addr.slot = u8::from_str_radix(slot, 16)?; @@ -770,20 +775,20 @@ impl FromStr for PCIAddress { #[cfg(test)] mod tests { - use crate::PCIAddress; + use crate::PciAddress; use std::str::FromStr; #[test] fn test_input_and_output_are_same() { assert_eq!( - PCIAddress::from_str("0000:00:00.1") + PciAddress::from_str("0000:00:00.1") .expect("Failed to parse correct string") .to_string(), "0000:00:00.1" ); assert_eq!( - PCIAddress::from_str("0000:00:01.0") + PciAddress::from_str("0000:00:01.0") .expect("Failed to parse correct string") .to_string(), "0000:00:01.0" diff --git a/vore-core/src/lib.rs b/vore-core/src/lib.rs index c53d1a1..e748049 100644 --- a/vore-core/src/lib.rs +++ b/vore-core/src/lib.rs @@ -4,18 +4,22 @@ mod qemu; mod virtual_machine; mod cpu_list; pub mod rpc; +pub mod consts; +mod virtual_machine_info; pub use global_config::*; pub use instance_config::*; pub use qemu::QemuCommandBuilder; +#[cfg(feature = "host")] pub use virtual_machine::*; -use log::LevelFilter; +pub use virtual_machine_info::*; pub fn init_logging() { let mut builder = pretty_env_logger::formatted_timed_builder(); #[cfg(debug_assertions)] { + use log::LevelFilter; builder.filter_level(LevelFilter::Debug); } - builder.parse_filters(&std::env::var("RUST_LOG").unwrap_or("".to_string())); + builder.parse_filters(&std::env::var("RUST_LOG").unwrap_or_else(|_| "".to_string())); builder.init(); } \ No newline at end of file diff --git a/vore-core/src/qemu.rs b/vore-core/src/qemu.rs index 7f6f9cb..3a040e1 100644 --- a/vore-core/src/qemu.rs +++ b/vore-core/src/qemu.rs @@ -1,4 +1,7 @@ -use crate::{GlobalConfig, InstanceConfig, GLOBAL_CONFIG_LOCATION}; +#![cfg(feature = "host")] + +use crate::consts::VORE_CONFIG; +use crate::{GlobalConfig, InstanceConfig}; use anyhow::Context; use mlua::prelude::LuaError; use mlua::{ @@ -8,31 +11,30 @@ use mlua::{ use serde::ser::Error; use serde::Deserialize; use std::collections::HashMap; -use std::path::{PathBuf, Path}; -use std::sync::{Arc, Mutex, Weak}; use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, Mutex, Weak}; #[derive(Debug, Default, Deserialize, Clone)] -struct VM { +struct VirtualMachine { args: Vec, bus_ids: HashMap, devices: HashMap, device: bool, } -impl UserData for VM { +impl UserData for VirtualMachine { fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_method_mut("arg", |_, this, args: MultiValue| { for item in args.iter() { if let Value::String(item) = item { let item = item.to_str()?.to_string(); if this.device { - let mut items = item.split(","); + let mut items = item.split(','); if let Some(_type) = items.next() { for item in items { - if item.starts_with("id=") { - this.devices - .insert(_type.to_string(), item[3..].to_string()); + if let Some(id) = item.strip_prefix("id=") { + this.devices.insert(_type.to_string(), id.to_string()); break; } } @@ -59,15 +61,13 @@ impl UserData for VM { }); methods.add_method_mut("get_next_bus", |lua, this, name: String| { - format!( - "{}.{}", - name.clone(), - this.bus_ids - .entry(name) - .and_modify(|x| *x += 1) - .or_insert(0) - ) - .to_lua(lua) + let id = this + .bus_ids + .entry(name.clone()) + .and_modify(|x| *x += 1) + .or_insert(0); + + format!("{}.{}", name, id).to_lua(lua) }); methods.add_method_mut("get_counter", |lua, this, args: (String, usize)| { @@ -107,7 +107,7 @@ impl UserData for VoreLuaWeakStorage { let strong = weak .0 .upgrade() - .ok_or(LuaError::custom("vore storage has expired"))?; + .ok_or_else(|| LuaError::custom("vore storage has expired"))?; let mut this = strong .try_lock() .map_err(|_| LuaError::custom("Failed to lock vore storage"))?; @@ -126,7 +126,7 @@ impl UserData for VoreLuaWeakStorage { let strong = weak .0 .upgrade() - .ok_or(LuaError::custom("vore storage has expired"))?; + .ok_or_else(|| LuaError::custom("vore storage has expired"))?; let mut this = strong .try_lock() .map_err(|_| LuaError::custom("Failed to lock vore storage"))?; @@ -145,7 +145,7 @@ impl UserData for VoreLuaWeakStorage { let strong = weak .0 .upgrade() - .ok_or(LuaError::custom("vore storage has expired"))?; + .ok_or_else(|| LuaError::custom("vore storage has expired"))?; let this = strong .try_lock() .map_err(|_| LuaError::custom("Failed to lock vore storage"))?; @@ -168,13 +168,16 @@ impl UserData for VoreLuaWeakStorage { methods.add_method( "add_disk", - |lua, weak, args: (VM, mlua::Table, u64, mlua::Table)| -> Result { - let (vm, instance, index, disk): (VM, mlua::Table, u64, Table) = args; + |lua, + weak, + args: (VirtualMachine, mlua::Table, u64, mlua::Table)| + -> Result { + let (vm, instance, index, disk): (VirtualMachine, mlua::Table, u64, Table) = args; let function = { let strong = weak .0 .upgrade() - .ok_or(LuaError::custom("vore storage has expired"))?; + .ok_or_else(|| LuaError::custom("vore storage has expired"))?; let this = strong .try_lock() .map_err(|_| LuaError::custom("Failed to lock vore storage"))?; @@ -223,11 +226,16 @@ impl QemuCommandBuilder { global: &GlobalConfig, working_dir: PathBuf, ) -> Result { - let lua = Path::new(GLOBAL_CONFIG_LOCATION).parent().unwrap().join(&global.qemu.script); + let lua = Path::new(VORE_CONFIG) + .parent() + .unwrap() + .join(&global.qemu.script); let builder = QemuCommandBuilder { lua: Lua::new(), - script: fs::read_to_string(&lua).with_context(|| format!("Failed to load lua qemu command build script ({:?})", lua))?, + script: fs::read_to_string(&lua).with_context(|| { + format!("Failed to load lua qemu command build script ({:?})", lua) + })?, storage: VoreLuaStorage::new(working_dir), }; @@ -260,7 +268,7 @@ impl QemuCommandBuilder { .eval::<()>() .context("Failed to run the configured qemu lua script")?; - let item = VM::default(); + let item = VirtualMachine::default(); let multi = MultiValue::from_vec(vec![self.lua.to_value(config)?, item.to_lua(&self.lua)?]); let working_dir = { self.storage.0.lock().unwrap().working_dir.clone() }; @@ -277,22 +285,21 @@ impl QemuCommandBuilder { anyhow::bail!("No qemu build command registered in lua script"); }; - let mut vm_instance = build_command.call::(multi)?; - - let mut cmd: Vec = vec![]; - cmd.push("-name".to_string()); - cmd.push(format!("guest={},debug-threads=on", config.name)); - - // Don't start the machine - cmd.push("-S".to_string()); - - // Set timestamps on log - cmd.push("-msg".to_string()); - cmd.push("timestamp=on".to_string()); - - // Drop privileges as soon as possible - cmd.push("-runas".to_string()); - cmd.push("nobody".to_string()); + let mut vm_instance = build_command.call::(multi)?; + + // Weird building way is for clarity sake + let mut cmd: Vec = vec![ + "-name".into(), + format!("guest={},debug-threads=on", config.name), + // Don't start the machine + "-S".into(), + // Set timestamps on log + "-msg".into(), + "timestamp=on".into(), + // Drop privileges as soon as possible + "-runas".into(), + "nobody".into(), + ]; let working_dir = working_dir .to_str() diff --git a/vore-core/src/rpc/calls.rs b/vore-core/src/rpc/calls.rs index 8b26d2b..47f185b 100644 --- a/vore-core/src/rpc/calls.rs +++ b/vore-core/src/rpc/calls.rs @@ -9,13 +9,13 @@ macro_rules! define_requests { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "query", rename_all = "snake_case")] pub enum AllRequests { - $($name(paste! { [<$name Request >] })),+ + $($name(Box] }>)),+ } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "answer", rename_all = "snake_case")] pub enum AllResponses { - $($name(paste! { [<$name Response >] })),+ + $($name(Box] }>)),+ } $( @@ -29,13 +29,13 @@ macro_rules! define_requests { type Response = [<$name Response>]; fn into_enum(self) -> AllRequests { - AllRequests::$name(self) + AllRequests::$name(Box::new(self)) } } impl Response for [<$name Response>] { fn into_enum(self) -> AllResponses { - AllResponses::$name(self) + AllResponses::$name(Box::new(self)) } } } diff --git a/vore-core/src/rpc/serde.rs b/vore-core/src/rpc/serde.rs index 2708838..631691b 100644 --- a/vore-core/src/rpc/serde.rs +++ b/vore-core/src/rpc/serde.rs @@ -35,7 +35,7 @@ impl CommandCenter { let mut str = serde_json::to_string(&answer)?; str.push('\n'); - return Ok(str); + Ok(str) } pub fn read_command(request: &str) -> Result { diff --git a/vore-core/src/virtual_machine.rs b/vore-core/src/virtual_machine.rs index 1e2fadc..3eaad0c 100644 --- a/vore-core/src/virtual_machine.rs +++ b/vore-core/src/virtual_machine.rs @@ -1,56 +1,31 @@ -use crate::{GlobalConfig, InstanceConfig, QemuCommandBuilder}; +#![cfg(feature = "host")] + +use crate::cpu_list::CpuList; +use crate::{ + GlobalConfig, InstanceConfig, QemuCommandBuilder, VfioConfig, VirtualMachineInfo, + VirtualMachineState, +}; use anyhow::{Context, Error}; use beau_collector::BeauCollector; -use qapi::qmp::{QMP, Event}; -use qapi::{Qmp}; -use std::{fmt, mem}; -use std::fmt::{Debug, Formatter, Display}; -use std::fs::{read_link, OpenOptions, read_dir}; +use libc::{cpu_set_t, sched_setaffinity, CPU_SET}; +use qapi::qmp::{Event, QMP}; +use qapi::Qmp; +use qapi_qmp::QmpCommand; +use std::fmt::{Debug, Formatter}; +use std::fs::{read_dir, read_link, OpenOptions}; use std::io; use std::io::{BufReader, ErrorKind, Read, Write}; use std::option::Option::Some; use std::os::unix::net::UnixStream; -use std::path::{PathBuf, Path}; +use std::os::unix::prelude::AsRawFd; +use std::path::{Path, PathBuf}; use std::process::{Child, Command}; use std::result::Result::Ok; +use std::slice::Iter; +use std::str::FromStr; use std::sync::{Arc, Mutex, MutexGuard}; use std::time::{Duration, Instant}; -use qapi_qmp::QmpCommand; -use std::str::FromStr; -use libc::{cpu_set_t, CPU_SET, sched_setaffinity}; -use crate::cpu_list::CpuList; -use std::os::unix::prelude::AsRawFd; -use serde::{Deserialize, Serialize}; - -#[derive(Eq, PartialEq, Copy, Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum VirtualMachineState { - Loaded, - Prepared, - Stopped, - Paused, - Running, -} - -impl Display for VirtualMachineState { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - VirtualMachineState::Loaded => write!(f, "loaded"), - VirtualMachineState::Prepared => write!(f, "prepared"), - VirtualMachineState::Stopped => write!(f, "stopped"), - VirtualMachineState::Paused => write!(f, "paused"), - VirtualMachineState::Running => write!(f, "running") - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct VirtualMachineInfo { - pub name: String, - pub working_dir: PathBuf, - pub config: InstanceConfig, - pub state: VirtualMachineState, -} +use std::{fmt, mem}; #[derive(Debug)] pub struct VirtualMachine { @@ -60,6 +35,7 @@ pub struct VirtualMachine { global_config: GlobalConfig, process: Option, control_socket: Option, + quit_after_shutdown: bool, } struct ControlSocket { @@ -91,9 +67,14 @@ impl VirtualMachine { global_config: global_config.clone(), process: None, control_socket: None, + quit_after_shutdown: true, } } + pub fn vfio_devices(&self) -> Iter<'_, VfioConfig> { + self.config.vfio.iter() + } + pub fn name(&self) -> &str { &self.config.name } @@ -104,6 +85,7 @@ impl VirtualMachine { working_dir: self.working_dir.clone(), config: self.config.clone(), state: self.state, + quit_after_shutdown: self.quit_after_shutdown, } } @@ -128,7 +110,8 @@ impl VirtualMachine { let mut shm = vec![]; if self.config.looking_glass.enabled { if self.config.looking_glass.mem_path.is_empty() { - self.config.looking_glass.mem_path = format!("/dev/shm/vore/{}/looking-glass", self.config.name); + self.config.looking_glass.mem_path = + format!("/dev/shm/vore/{}/looking-glass", self.config.name); } shm.push(&self.config.looking_glass.mem_path); @@ -142,12 +125,15 @@ impl VirtualMachine { shm.push(&self.config.scream.mem_path); } - shm - .into_iter() + shm.into_iter() .map(|x| Path::new(x)) .filter_map(|x| x.parent()) .filter(|x| !x.is_dir()) - .map(|x| std::fs::create_dir_all(&x).with_context(|| format!("Failed creating directories for shared memory ({:?})", x))) + .map(|x| { + std::fs::create_dir_all(&x).with_context(|| { + format!("Failed creating directories for shared memory ({:?})", x) + }) + }) .collect() } @@ -155,7 +141,12 @@ impl VirtualMachine { let mut sockets = vec![]; if self.config.spice.enabled { if self.config.spice.socket_path.is_empty() { - self.config.spice.socket_path = self.working_dir.join("spice.sock").to_str().unwrap().to_string(); + self.config.spice.socket_path = self + .working_dir + .join("spice.sock") + .to_str() + .unwrap() + .to_string(); } sockets.push(&self.config.spice.socket_path); @@ -166,7 +157,11 @@ impl VirtualMachine { .map(|x| Path::new(x)) .filter_map(|x| x.parent()) .filter(|x| !x.is_dir()) - .map(|x| std::fs::create_dir_all(&x).with_context(|| format!("Failed creating directories for shared memory ({:?})", x))) + .map(|x| { + std::fs::create_dir_all(&x).with_context(|| { + format!("Failed creating directories for shared memory ({:?})", x) + }) + }) .collect() } @@ -214,84 +209,97 @@ impl VirtualMachine { Ok(_) => {} } - self.config.vfio.iter().map(|vfio| { - let pci_driver_path = format!("/sys/bus/pci/devices/{:#}/driver", vfio.address); - - let driver = match read_link(&pci_driver_path) { - Ok(driver_link) => { - let driver_path = driver_link.to_str().ok_or_else(|| { - anyhow::anyhow!( - "Path to device driver for PCI device at {} is not valid utf-8", - vfio.address - ) - })?; - let driver = driver_path.split("/").last().ok_or_else(|| { - anyhow::anyhow!( + self.config + .vfio + .iter() + .map(|vfio| VirtualMachine::prepare_vfio_device(execute_fixes, force, vfio)) + .collect::>() + } + + pub fn should_auto_start(&self) -> bool { + self.config.auto_start + } + + pub fn prepare_vfio_device( + execute_fixes: bool, + force: bool, + vfio: &VfioConfig, + ) -> Result<(), Error> { + let pci_driver_path = format!("/sys/bus/pci/devices/{:#}/driver", vfio.address); + + let driver = match read_link(&pci_driver_path) { + Ok(driver_link) => { + let driver_path = driver_link.to_str().ok_or_else(|| { + anyhow::anyhow!( + "Path to device driver for PCI device at {} is not valid utf-8", + vfio.address + ) + })?; + let driver = driver_path.split('/').last().ok_or_else(|| { + anyhow::anyhow!( "Path to device driver for PCI device at {} doesn't have a path to a driver", vfio.address ) - })?; + })?; - driver.to_string() - } + driver.to_string() + } - Err(err) if err.kind() == ErrorKind::NotFound => "".to_string(), + Err(err) if err.kind() == ErrorKind::NotFound => "".to_string(), - Err(err) => return Err(err.into()), - }; + Err(err) => return Err(err.into()), + }; - let is_blacklisted = AUTO_UNBIND_BLACKLIST.contains(&driver.as_str()) && !force; + let is_blacklisted = AUTO_UNBIND_BLACKLIST.contains(&driver.as_str()) && !force; - if driver != "vfio-pci" && (!execute_fixes || is_blacklisted) { - if !driver.is_empty() && is_blacklisted { - anyhow::bail!("PCI device {} it's current driver is {}, but to be used with VFIO needs to be set to vfio-pci, this driver ({1}) has been blacklisted from automatic rebinding because it can't be cleanly unbound, please make sure this device is unbound before running vore", vfio.address, driver) - } else if !driver.is_empty() { - anyhow::bail!("PCI device {} it's current driver is {}, but to be used with VFIO needs to be set to vfio-pci", vfio.address, driver) - } else { - anyhow::bail!("PCI device at {} currently has no driver, but to be used with VFIO needs to be set to vfio-pci", vfio.address) - } + if driver != "vfio-pci" && (!execute_fixes || is_blacklisted) { + if !driver.is_empty() && is_blacklisted { + anyhow::bail!("PCI device {} it's current driver is {}, but to be used with VFIO needs to be set to vfio-pci, this driver ({1}) has been blacklisted from automatic rebinding because it can't be cleanly unbound, please make sure this device is unbound before running vore", vfio.address, driver) + } else if !driver.is_empty() { + anyhow::bail!("PCI device {} it's current driver is {}, but to be used with VFIO needs to be set to vfio-pci", vfio.address, driver) + } else { + anyhow::bail!("PCI device at {} currently has no driver, but to be used with VFIO needs to be set to vfio-pci", vfio.address) } + } - if driver != "vfio-pci" && execute_fixes && !is_blacklisted { - let address = format!("{:#}\n", vfio.address).into_bytes(); + if driver != "vfio-pci" && execute_fixes && !is_blacklisted { + let address = format!("{:#}\n", vfio.address).into_bytes(); - if !driver.is_empty() { - // Unbind the PCI device from the current driver - let mut unbind = std::fs::OpenOptions::new().append(true).open(format!( - "/sys/bus/pci/devices/{:#}/driver/unbind", - vfio.address - ))?; + if !driver.is_empty() { + // Unbind the PCI device from the current driver + let mut unbind = std::fs::OpenOptions::new().append(true).open(format!( + "/sys/bus/pci/devices/{:#}/driver/unbind", + vfio.address + ))?; - unbind.write_all(&address)?; - } + unbind.write_all(&address)?; + } - { - // Set a driver override - let mut driver_override = OpenOptions::new().append(true).open(format!( - "/sys/bus/pci/devices/{:#}/driver_override", - vfio.address - ))?; + { + // Set a driver override + let mut driver_override = OpenOptions::new().append(true).open(format!( + "/sys/bus/pci/devices/{:#}/driver_override", + vfio.address + ))?; - driver_override.write_all(b"vfio-pci\n")?; - } + driver_override.write_all(b"vfio-pci\n")?; + } - { - // Probe the PCI device so the driver override is picked up - let mut probe = OpenOptions::new() - .append(true) - .open("/sys/bus/pci/drivers_probe")?; - probe.write_all(&address)?; - } + { + // Probe the PCI device so the driver override is picked up + let mut probe = OpenOptions::new() + .append(true) + .open("/sys/bus/pci/drivers_probe")?; + probe.write_all(&address)?; + } - let new_link = read_link(&pci_driver_path)?; - if !new_link.ends_with("vfio-pci") { - anyhow::bail!("Tried to bind {} to vfio-pci but failed to do so (see /sys/bus/pci/devices/{:#} for more info)", vfio.address, vfio.address) - } + let new_link = read_link(&pci_driver_path)?; + if !new_link.ends_with("vfio-pci") { + anyhow::bail!("Tried to bind {} to vfio-pci but failed to do so (see /sys/bus/pci/devices/{:#} for more info)", vfio.address, vfio.address) } + } - Ok(()) - }) - .collect::>() + Ok(()) } pub fn get_cmd_line(&self) -> Result, anyhow::Error> { @@ -321,7 +329,11 @@ impl VirtualMachine { continue; } - let res = entry.file_name().to_str().ok_or_else(|| anyhow::anyhow!("")).and_then(|x| usize::from_str(x).map_err(From::from)); + let res = entry + .file_name() + .to_str() + .ok_or_else(|| anyhow::anyhow!("")) + .and_then(|x| usize::from_str(x).map_err(From::from)); if res.is_err() { continue; } @@ -330,7 +342,11 @@ impl VirtualMachine { let name = entry.path().join("comm"); let comm = std::fs::read_to_string(name)?; if comm.starts_with("CPU ") { - let nr = comm.chars().skip(4).take_while(|x| x.is_ascii_digit()).collect::(); + let nr = comm + .chars() + .skip(4) + .take_while(|x| x.is_ascii_digit()) + .collect::(); let cpu_id = usize::from_str(&nr).unwrap(); kvm_threads.push((tid, cpu_id)); } @@ -358,10 +374,12 @@ impl VirtualMachine { qmp.qmp.nop()?; } - self.process_qmp_events() + self.process_qmp_events()?; + + Ok(()) } - fn process_qmp_events(&mut self) -> Result<(), anyhow::Error> { + fn process_qmp_events(&mut self) -> anyhow::Result<()> { let events = if let Some(qmp) = self.control_socket.as_mut() { // While we could iter, we keep hold of the mutable reference, so it's easier to just collect the events qmp.qmp.events().collect::>() @@ -370,11 +388,11 @@ impl VirtualMachine { }; for event in events { - println!("Event: {:?}", event); + log::info!("vm {} got event: {:?}", self.name(), event); match event { Event::STOP { .. } => { - if self.state != VirtualMachineState::Stopped { + if self.state == VirtualMachineState::Running { self.state = VirtualMachineState::Paused; } } @@ -383,6 +401,10 @@ impl VirtualMachine { } Event::SHUTDOWN { .. } => { self.state = VirtualMachineState::Stopped; + + if self.quit_after_shutdown { + self.quit()?; + } } _ => {} @@ -414,7 +436,10 @@ impl VirtualMachine { } pub fn stop(&mut self) -> Result<(), anyhow::Error> { - if self.process.is_none() || self.control_socket.is_none() || self.state == VirtualMachineState::Stopped { + if self.process.is_none() + || self.control_socket.is_none() + || self.state == VirtualMachineState::Stopped + { return Ok(()); } @@ -433,17 +458,32 @@ impl VirtualMachine { } match self.send_qmp_command(&qapi_qmp::quit {}) { - Err(err) if err.downcast_ref::().map_or(false, |x| x.kind() == io::ErrorKind::UnexpectedEof) => {} - err => { err?; } + Err(err) + if err.downcast_ref::().map_or(false, |x| { + x.kind() == io::ErrorKind::UnexpectedEof + || x.kind() == io::ErrorKind::ConnectionReset + }) => {} + err => { + err?; + } } + self.control_socket = None; + + self.state = VirtualMachineState::Prepared; Ok(()) } - fn wait(&mut self, duration: Option, target_state: VirtualMachineState) -> Result { + fn wait( + &mut self, + duration: Option, + target_state: VirtualMachineState, + ) -> Result { let start = Instant::now(); while duration.map_or(true, |dur| (Instant::now() - start) < dur) { - let has_socket = self.control_socket.as_mut() + let has_socket = self + .control_socket + .as_mut() .map(|x| x.qmp.nop()) .transpose()? .is_some(); @@ -480,7 +520,10 @@ impl VirtualMachine { } let mut command = Command::new("qemu-system-x86_64"); - command.args(self.get_cmd_line().context("Failed to generate qemu command line")?); + command.args( + self.get_cmd_line() + .context("Failed to generate qemu command line")?, + ); self.process = Some(command.spawn()?); let mut res = || { @@ -499,7 +542,7 @@ impl VirtualMachine { unix_stream = UnixStream::connect(&qemu_control_socket); if let Some(proc) = self.process.as_mut() { - if let Some(_) = proc.try_wait()? { + if proc.try_wait()?.is_some() { anyhow::bail!("QEMU quit early") } } @@ -520,6 +563,18 @@ impl VirtualMachine { self.pin_qemu_threads()?; + if self.config.looking_glass.enabled { + self.global_config + .vore + .chown(&self.config.looking_glass.mem_path)?; + } + + if self.config.spice.enabled { + self.global_config + .vore + .chown(&self.config.spice.socket_path)?; + } + control_socket .qmp .execute(&qapi_qmp::cont {}) @@ -588,4 +643,4 @@ impl Write for CloneableUnixStream { fn flush(&mut self) -> io::Result<()> { self.lock()?.flush() } -} \ No newline at end of file +} diff --git a/vore-core/src/virtual_machine_info.rs b/vore-core/src/virtual_machine_info.rs new file mode 100644 index 0000000..90e07b9 --- /dev/null +++ b/vore-core/src/virtual_machine_info.rs @@ -0,0 +1,36 @@ +use std::fmt::{Display, Formatter}; +use std::fmt; +use crate::InstanceConfig; +use std::path::PathBuf; +use serde::{Deserialize, Serialize}; + +#[derive(Eq, PartialEq, Copy, Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum VirtualMachineState { + Loaded, + Prepared, + Stopped, + Paused, + Running, +} + +impl Display for VirtualMachineState { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + VirtualMachineState::Loaded => write!(f, "loaded"), + VirtualMachineState::Prepared => write!(f, "prepared"), + VirtualMachineState::Stopped => write!(f, "stopped"), + VirtualMachineState::Paused => write!(f, "paused"), + VirtualMachineState::Running => write!(f, "running") + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct VirtualMachineInfo { + pub name: String, + pub working_dir: PathBuf, + pub config: InstanceConfig, + pub state: VirtualMachineState, + pub quit_after_shutdown: bool, +} \ No newline at end of file diff --git a/vore/clap.yml b/vore/clap.yml index 2a0e4b5..21b5802 100644 --- a/vore/clap.yml +++ b/vore/clap.yml @@ -8,7 +8,6 @@ args: help: "Connect to the specified socket" required: false takes_value: true - default_value: "/run/vore.sock" long: conn short: c @@ -102,6 +101,7 @@ subcommands: visible_alias: lg args: - vm-name: + long: vm help: "VM to start looking glass instance for, if not given the ONLY running instance will be used" required: false takes_value: true diff --git a/vore/src/main.rs b/vore/src/main.rs index 560aae3..d9a9212 100644 --- a/vore/src/main.rs +++ b/vore/src/main.rs @@ -1,13 +1,14 @@ mod client; -use vore_core::{init_logging, VirtualMachineInfo}; use crate::client::Client; -use clap::{App, ArgMatches}; -use std::{fs, mem}; use anyhow::Context; +use clap::{App, ArgMatches}; use std::option::Option::Some; -use std::process::Command; use std::os::unix::process::CommandExt; +use std::process::Command; +use std::{fs, mem}; +use vore_core::consts::VORE_SOCKET; +use vore_core::{init_logging, VirtualMachineInfo}; fn main() { init_logging(); @@ -21,11 +22,9 @@ fn main_res() -> anyhow::Result<()> { let yaml = clap::load_yaml!("../clap.yml"); let app: App = App::from(yaml); let matches = app.get_matches(); - let client = Client::connect(matches.value_of("vored-socket").unwrap())?; + let client = Client::connect(matches.value_of("vored-socket").unwrap_or(VORE_SOCKET))?; - let mut vore = VoreApp { - client - }; + let mut vore = VoreApp { client }; match matches.subcommand() { ("load", Some(args)) => { @@ -52,17 +51,15 @@ fn main_res() -> anyhow::Result<()> { vore.looking_glass(args)?; } - ("daemon", Some(args)) => { - match args.subcommand() { - ("version", _) => { - vore.daemon_version()?; - } + ("daemon", Some(args)) => match args.subcommand() { + ("version", _) => { + vore.daemon_version()?; + } - (s, _) => { - log::error!("Subcommand daemon.{} not implemented", s); - } + (s, _) => { + log::error!("Subcommand daemon.{} not implemented", s); } - } + }, (s, _) => { log::error!("Subcommand {} not implemented", s); @@ -72,20 +69,22 @@ fn main_res() -> anyhow::Result<()> { Ok(()) } -struct LoadVMOptions { +struct LoadVirtualMachineOptions { config: String, cd_roms: Vec, save: bool, } -fn get_load_vm_options(args: &ArgMatches) -> anyhow::Result { +fn get_load_vm_options(args: &ArgMatches) -> anyhow::Result { let vm_config_path = args.value_of("vm-config").unwrap(); let config = fs::read_to_string(vm_config_path) .with_context(|| format!("Failed to read vm config at {}", vm_config_path))?; - Ok(LoadVMOptions { + Ok(LoadVirtualMachineOptions { config, - cd_roms: args.values_of("cdrom").map_or(vec![], |x| x.map(|x| x.to_string()).collect::>()), + cd_roms: args + .values_of("cdrom") + .map_or(vec![], |x| x.map(|x| x.to_string()).collect::>()), save: args.is_present("save"), }) } @@ -102,11 +101,13 @@ impl VoreApp { pub fn get_vm(&mut self, args: &ArgMatches) -> anyhow::Result { let mut items = self.client.list_vms()?; if let Some(vm_name) = args.value_of("vm-name") { - items.into_iter().find(|x| x.name == vm_name) + items + .into_iter() + .find(|x| x.name == vm_name) .with_context(|| format!("Couldn't find VM with the name '{}'", vm_name)) } else { match (items.len(), items.pop()) { - (amount, Some(x)) if amount == 1 => return Ok(x), + (amount, Some(x)) if amount == 1 => Ok(x), (0, None) => anyhow::bail!("There are no VM's loaded"), _ => anyhow::bail!("Multiple VM's are loaded, please specify one"), } @@ -122,7 +123,9 @@ impl VoreApp { fn load(&mut self, args: &ArgMatches) -> anyhow::Result<()> { let vm_options = get_load_vm_options(args)?; - let vm_info = self.client.load_vm(&vm_options.config, vm_options.save, vm_options.cd_roms)?; + let vm_info = + self.client + .load_vm(&vm_options.config, vm_options.save, vm_options.cd_roms)?; log::info!("Loaded VM {}", vm_info.name); Ok(()) } @@ -139,13 +142,21 @@ impl VoreApp { fn prepare(&mut self, args: &ArgMatches) -> anyhow::Result<()> { let name = self.get_vm_name(args)?; - self.client.prepare(name, args.values_of("cdrom").map_or(vec![], |x| x.map(|x| x.to_string()).collect::>()))?; + self.client.prepare( + name, + args.values_of("cdrom") + .map_or(vec![], |x| x.map(|x| x.to_string()).collect::>()), + )?; Ok(()) } fn start(&mut self, args: &ArgMatches) -> anyhow::Result<()> { let name = self.get_vm_name(args)?; - self.client.start(name, args.values_of("cdrom").map_or(vec![], |x| x.map(|x| x.to_string()).collect::>()))?; + self.client.start( + name, + args.values_of("cdrom") + .map_or(vec![], |x| x.map(|x| x.to_string()).collect::>()), + )?; Ok(()) } @@ -155,7 +166,9 @@ impl VoreApp { anyhow::bail!("VM '{}' has no looking glass", vm.name); } - let mut command = Command::new(std::env::var("LOOKING_GLASS").unwrap_or("looking-glass-client".to_string())); + let mut command = Command::new( + std::env::var("LOOKING_GLASS").unwrap_or_else(|_| "looking-glass-client".to_string()), + ); if vm.config.spice.enabled { command.args(&["-c", &vm.config.spice.socket_path, "-p", "0"]); } else { @@ -163,7 +176,10 @@ impl VoreApp { } command.args(&["-f", &vm.config.looking_glass.mem_path]); - command.args(args.values_of("looking-glass-args").map_or(vec![], |x| x.into_iter().collect::>())); + command.args( + args.values_of("looking-glass-args") + .map_or(vec![], |x| x.into_iter().collect::>()), + ); mem::drop(self); command.exec(); diff --git a/vored/Cargo.toml b/vored/Cargo.toml index 8a6c80d..5c749b2 100644 --- a/vored/Cargo.toml +++ b/vored/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" [dependencies] anyhow = "1.0.40" -vore-core = { path = "../vore-core" } +vore-core = { path = "../vore-core", features = ["host"] } polling = "2.0.3" log = "0.4.14" pretty_env_logger = "0.3" diff --git a/vored/src/daemon.rs b/vored/src/daemon.rs index 951f1bd..de8ee8f 100644 --- a/vored/src/daemon.rs +++ b/vored/src/daemon.rs @@ -1,24 +1,27 @@ -use std::collections::{HashMap}; -use vore_core::{VirtualMachine, InstanceConfig, GlobalConfig, GLOBAL_CONFIG_LOCATION}; -use std::os::unix::net::{UnixListener, SocketAddr, UnixStream}; -use polling::{Poller, Event}; -use std::time::Duration; use anyhow::Context; -use std::{mem, io}; -use std::io::{Read, Write}; -use vore_core::rpc::{CommandCenter, Response, Command, AllRequests, AllResponses}; -use vore_core::rpc; -use signal_hook::low_level::{signal_name}; -use signal_hook::consts::{SIGINT, SIGTERM, SIGHUP}; -use std::path::PathBuf; -use std::str::FromStr; -use signal_hook::iterator::{SignalsInfo, Signals, Handle}; -use std::os::unix::io::AsRawFd; +use polling::{Event, Poller}; +use signal_hook::consts::{SIGHUP, SIGINT, SIGTERM}; +use signal_hook::iterator::{Handle, Signals, SignalsInfo}; +use signal_hook::low_level::signal_name; +use std::collections::HashMap; use std::ffi::CStr; +use std::fs; +use std::fs::{read_dir, read_to_string, DirEntry}; +use std::io::{Read, Write}; use std::mem::size_of; +use std::os::unix::io::AsRawFd; +use std::os::unix::net::{SocketAddr, UnixListener, UnixStream}; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::time::Duration; +use std::{io, mem}; +use vore_core::consts::{VORE_CONFIG, VORE_DIRECTORY, VORE_SOCKET}; +use vore_core::rpc::{AllRequests, AllResponses, Command, CommandCenter, Response}; +use vore_core::{rpc, VirtualMachineInfo}; +use vore_core::{GlobalConfig, InstanceConfig, VirtualMachine}; #[derive(Debug)] -struct RPCConnection { +struct RpcConnection { stream: UnixStream, address: SocketAddr, buffer: Vec, @@ -27,7 +30,7 @@ struct RPCConnection { pid: i32, } -impl Write for RPCConnection { +impl Write for RpcConnection { fn write(&mut self, buf: &[u8]) -> io::Result { self.stream.write(buf) } @@ -37,16 +40,20 @@ impl Write for RPCConnection { } } -impl Read for RPCConnection { +impl Read for RpcConnection { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.stream.read(buf) } } +#[allow(clippy::char_lit_as_u8)] const NEWLINE: u8 = '\n' as u8; -impl RPCConnection { - pub fn handle_input(&mut self, own_id: usize) -> Result<(bool, Vec<(usize, Command)>), anyhow::Error> { +impl RpcConnection { + pub fn handle_input( + &mut self, + own_id: usize, + ) -> Result<(bool, Vec<(usize, Command)>), anyhow::Error> { let mut still_open = true; loop { let mut buffer = vec![0u8; 4096]; @@ -57,13 +64,21 @@ impl RPCConnection { } Ok(amount) => self.buffer.extend_from_slice(&buffer[..amount]), Err(err) if err.kind() == io::ErrorKind::WouldBlock => break, - Err(err) => return Err(err.into()) + Err(err) => return Err(err.into()), }; } let mut buffer = mem::take(&mut self.buffer); if still_open { - self.buffer = buffer.split_off(buffer.iter().enumerate().rev().find(|(_, x)| **x == NEWLINE).map(|(idx, _)| idx + 1).unwrap_or(buffer.len())); + self.buffer = buffer.split_off( + buffer + .iter() + .enumerate() + .rev() + .find(|(_, x)| **x == NEWLINE) + .map(|(idx, _)| idx + 1) + .unwrap_or(buffer.len()), + ); } let mut commands = vec![]; @@ -73,7 +88,6 @@ impl RPCConnection { continue; } - let lossy = String::from_utf8_lossy(part); match CommandCenter::read_command(&lossy) { @@ -94,9 +108,9 @@ impl RPCConnection { #[derive(Clone, Eq, PartialEq, Debug)] enum EventTarget { - RPCListener, + RpcListener, Machine(String), - RPCConnection(usize), + RpcConnection(usize), None, } @@ -105,7 +119,7 @@ pub struct Daemon { event_key_storage: Vec, global_config: GlobalConfig, machines: HashMap, - connections: Vec>, + connections: Vec>, rpc_listener: UnixListener, socket_path: PathBuf, poller: Poller, @@ -117,18 +131,22 @@ pub struct Daemon { impl Daemon { pub fn new() -> Result { - log::debug!("Loading global config ({})", GLOBAL_CONFIG_LOCATION); - let toml = std::fs::read_to_string(GLOBAL_CONFIG_LOCATION)?; - let global_config = GlobalConfig::load(&toml)?; + log::debug!("Loading global config ({})", VORE_CONFIG); + let toml = std::fs::read_to_string(VORE_CONFIG)?; + let mut global_config = GlobalConfig::load(&toml)?; log::debug!("Creating vore daemon"); let signals = Signals::new(&[SIGINT, SIGHUP])?; let handle = signals.handle(); log::debug!("Bound signal handlers"); let poller = Poller::new().context("Failed to make poller")?; - let socket_path = PathBuf::from_str("/run/vore.sock")?; - let rpc_listener = UnixListener::bind(&socket_path).context("Failed to bind vore socket")?; + let socket_path = PathBuf::from_str(VORE_SOCKET)?; + let rpc_listener = + UnixListener::bind(&socket_path).context("Failed to bind vore socket")?; + + global_config.vore.chown(socket_path.to_str().unwrap())?; + rpc_listener.set_nonblocking(true)?; - log::debug!("Bound to /run/vore.sock"); + log::debug!("Bound to {}", VORE_SOCKET); let mut daemon = Daemon { event_key_storage: vec![], @@ -149,23 +167,106 @@ impl Daemon { } pub fn init(&mut self) -> Result<(), anyhow::Error> { - let new_key = self.add_target(EventTarget::RPCListener); - self.poller.add(&self.rpc_listener, Event::readable(new_key))?; + let new_key = self.add_target(EventTarget::RpcListener); + self.poller + .add(&self.rpc_listener, Event::readable(new_key))?; Ok(()) } + pub fn load_definitions(&mut self) -> Result<(), anyhow::Error> { + let vm_dir = PathBuf::from(format!("{}/definitions", VORE_DIRECTORY)); + if !vm_dir.is_dir() { + return Ok(()); + } + + let dir_iter = + read_dir(&vm_dir).with_context(|| format!("Failed to list {:?} for vm's", &vm_dir))?; + + let mut process = |entry: Result| -> anyhow::Result<()> { + let entry = entry?; + let file_name = entry.path(); + let path = file_name.to_str().context("Entry has invalid UTF-8 path")?; + if !path.ends_with(".toml") { + return Ok(()); + } + + let toml = read_to_string(path) + .with_context(|| format!("Failed to read VM definition {}", path))?; + self.load_virtual_machine(&toml, None, false)?; + Ok(()) + }; + + for entry in dir_iter { + if let Err(err) = process(entry) { + log::error!("Failed parsing entry in {:?}: {:?}", vm_dir, err); + } + } + + Ok(()) + } + + pub fn reserve_vfio_devices(&mut self) { + for machine in self.machines.values() { + for vfio_device in machine.vfio_devices() { + if !vfio_device.reserve { + continue; + } + + if let Err(err) = VirtualMachine::prepare_vfio_device(true, true, &vfio_device) { + log::error!( + "Failed to reserve PCI device {} for {}: {:?}", + vfio_device.address, + machine.name(), + err + ); + } else { + log::info!( + "Reserved PCI device {} for {}", + vfio_device.address, + machine.name() + ); + } + } + } + } + + pub fn auto_start_machines(&mut self) { + for machine in self.machines.values_mut() { + if !machine.should_auto_start() { + continue; + } + + if let Err(err) = machine.start() { + log::error!("Failed to auto-start {}: {:?}", machine.name(), err); + } else { + log::info!("Autostarted {}", machine.name()); + } + } + } + pub fn run(&mut self) -> Result<(), anyhow::Error> { + self.load_definitions()?; + self.reserve_vfio_devices(); + self.auto_start_machines(); + loop { - let res = self.wait().context("Got error while waiting for new notifications"); + let res = self + .wait() + .context("Got error while waiting for new notifications"); match res { // Interrupted is uh "always" when we get a signal - Err(err) if err.downcast_ref::().map(|x| x.kind() == io::ErrorKind::Interrupted).unwrap_or(false) => { + Err(err) + if err + .downcast_ref::() + .map(|x| x.kind() == io::ErrorKind::Interrupted) + .unwrap_or(false) => + { if !self.handle_exit_code()? { break; } } - err => err? + err => err?, } if !self.handle_event_queue()? { @@ -196,37 +297,63 @@ impl Daemon { Ok(()) } + pub fn load_virtual_machine( + &mut self, + toml: &str, + working_directory: Option, + save: bool, + ) -> anyhow::Result { + let config = InstanceConfig::from_toml(&toml)?; + if save { + let save_file = format!("{}/definitions/{}.toml", VORE_DIRECTORY, config.name); + let file_path = Path::new(&save_file); + if let Some(parent_dir) = file_path.parent() { + if !parent_dir.is_dir() { + fs::create_dir_all(parent_dir)?; + } + } + + fs::write(&save_file, toml).with_context(|| { + format!( + "Failed to save vm definition for {} to {}", + config.name, save_file + ) + })?; + } + + let working_dir = working_directory + .unwrap_or_else(|| format!("{}/instance/{}", VORE_DIRECTORY, config.name)); + let vm = VirtualMachine::new(config, &self.global_config, working_dir); + let info = vm.info(); + self.mount_machine(vm); + Ok(info) + } + pub fn handle_command(&mut self, command: &Command) -> Result { let resp = match &command.data { - AllRequests::Info(_) => { - rpc::InfoResponse { - name: "vore".to_string(), - version: format!("{}.{}.{}{}", - env!("CARGO_PKG_VERSION_MAJOR"), - env!("CARGO_PKG_VERSION_MINOR"), - env!("CARGO_PKG_VERSION_PATCH"), - option_env!("CARGO_PKG_VERSION_PRE").unwrap_or("")), - } - .into_enum() + AllRequests::Info(_) => rpc::InfoResponse { + name: "vore".to_string(), + version: format!( + "{}.{}.{}{}", + env!("CARGO_PKG_VERSION_MAJOR"), + env!("CARGO_PKG_VERSION_MINOR"), + env!("CARGO_PKG_VERSION_PATCH"), + option_env!("CARGO_PKG_VERSION_PRE").unwrap_or("") + ), } - AllRequests::List(_) => { - rpc::ListResponse { - items: self.machines.values().map(|x| x.info()).collect() - } - .into_enum() + .into_enum(), + AllRequests::List(_) => rpc::ListResponse { + items: self.machines.values().map(|x| x.info()).collect(), } - AllRequests::Load(val) => { - let config = InstanceConfig::from_toml(&val.toml)?; - let working_dir = val.working_directory.as_ref().cloned().unwrap_or_else(|| format!("/var/lib/vore/{}", config.name)); - let vm = VirtualMachine::new(config, &self.global_config, working_dir); - let info = vm.info(); - self.mount_machine(vm); - - rpc::LoadResponse { - info, - } - .into_enum() + .into_enum(), + AllRequests::Load(val) => rpc::LoadResponse { + info: self.load_virtual_machine( + &val.toml, + val.working_directory.as_ref().cloned(), + val.save, + )?, } + .into_enum(), AllRequests::Prepare(val) => { if let Some(machine) = self.machines.get_mut(&val.name) { machine.prepare(true, false)?; @@ -280,7 +407,11 @@ impl Daemon { pub fn handle_exit_code(&mut self) -> Result { for signal in self.signals.pending() { - log::info!("Received signal {} ({})", signal_name(signal).unwrap_or(""), signal); + log::info!( + "Received signal {} ({})", + signal_name(signal).unwrap_or(""), + signal + ); match signal { SIGINT | SIGTERM => return Ok(false), _ => {} @@ -297,8 +428,9 @@ impl Daemon { log::debug!("Handling {:?} from target {:?}", event, item); match item { - EventTarget::RPCListener => { - self.poller.modify(&self.rpc_listener, Event::readable(event.key))?; + EventTarget::RpcListener => { + self.poller + .modify(&self.rpc_listener, Event::readable(event.key))?; self.accept_rpc_connections()?; } EventTarget::Machine(name) if self.machines.contains_key(&name) => { @@ -306,15 +438,27 @@ impl Daemon { machine.boop()?; } - if let Some(control_socket) = self.machines.get(&name).and_then(|x| x.control_stream()) { - self.poller.modify(control_socket, Event::readable(event.key))?; + if let Some(control_socket) = + self.machines.get(&name).and_then(|x| x.control_stream()) + { + self.poller + .modify(control_socket, Event::readable(event.key))?; } } - EventTarget::RPCConnection(rpc_connection_id) if self.connections.get(rpc_connection_id).map(Option::is_some).unwrap_or(false) => { - let (still_open, mut commands) = if let Some(rpc_connection) = &mut self.connections[rpc_connection_id] { + EventTarget::RpcConnection(rpc_connection_id) + if self + .connections + .get(rpc_connection_id) + .map(Option::is_some) + .unwrap_or(false) => + { + let (still_open, mut commands) = if let Some(rpc_connection) = + &mut self.connections[rpc_connection_id] + { let input_res = rpc_connection.handle_input(rpc_connection_id)?; if input_res.0 { - self.poller.modify(&rpc_connection.stream, Event::readable(event.key))?; + self.poller + .modify(&rpc_connection.stream, Event::readable(event.key))?; } input_res @@ -342,7 +486,7 @@ impl Daemon { let (stream, address) = match self.rpc_listener.accept() { Ok(value) => value, Err(err) if err.kind() == io::ErrorKind::WouldBlock => return Ok(()), - Err(err) => return Err(err)? + Err(err) => return Err(err.into()), }; stream.set_nonblocking(true)?; @@ -351,16 +495,25 @@ impl Daemon { let ucred = unsafe { let mut ucred: libc::ucred = mem::zeroed(); let mut length = size_of::() as u32; - libc::getsockopt(stream.as_raw_fd(), libc::SOL_SOCKET, libc::SO_PEERCRED, (&mut ucred) as *mut _ as _, &mut length); + libc::getsockopt( + stream.as_raw_fd(), + libc::SOL_SOCKET, + libc::SO_PEERCRED, + (&mut ucred) as *mut _ as _, + &mut length, + ); let passwd = libc::getpwuid(ucred.uid); if !passwd.is_null() { - user = CStr::from_ptr((*passwd).pw_name).to_str().ok().map(|x| x.to_string()) + user = CStr::from_ptr((*passwd).pw_name) + .to_str() + .ok() + .map(|x| x.to_string()) } ucred }; - let conn = RPCConnection { + let conn = RpcConnection { stream, address, buffer: vec![], @@ -371,24 +524,35 @@ impl Daemon { log::info!( "Got new RPC connection from {} (pid: {}, socket: {:?})", - conn.user.as_ref().map_or_else(|| format!("uid:{}", conn.uid), |x| format!("{} ({})", x, conn.uid)), + conn.user.as_ref().map_or_else( + || format!("uid:{}", conn.uid), + |x| format!("{} ({})", x, conn.uid), + ), conn.pid, conn.address, ); let id = self.add_rpc_connection(conn); - let event_target = self.add_target(EventTarget::RPCConnection(id)); - self.poller.add(&self.connections[id].as_ref().unwrap().stream, Event::readable(event_target))?; + let event_target = self.add_target(EventTarget::RpcConnection(id)); + self.poller.add( + &self.connections[id].as_ref().unwrap().stream, + Event::readable(event_target), + )?; } } pub fn wait(&mut self) -> Result<(), anyhow::Error> { - self.poller.wait(&mut self.queue, Some(Duration::from_secs(5)))?; + self.poller + .wait(&mut self.queue, Some(Duration::from_secs(5)))?; Ok(()) } fn add_target(&mut self, event_target: EventTarget) -> usize { - let id = self.event_key_storage.iter().enumerate().find(|(_, target)| target.eq(&&EventTarget::None)); + let id = self + .event_key_storage + .iter() + .enumerate() + .find(|(_, target)| target.eq(&&EventTarget::None)); if let Some((id, _)) = id { self.event_key_storage[id] = event_target; return id; @@ -396,11 +560,15 @@ impl Daemon { let new_id = self.event_key_storage.len(); self.event_key_storage.push(event_target); - return new_id; + new_id } - fn add_rpc_connection(&mut self, rpc_connection: RPCConnection) -> usize { - let id = self.connections.iter().enumerate().find(|(_, target)| target.is_none()); + fn add_rpc_connection(&mut self, rpc_connection: RpcConnection) -> usize { + let id = self + .connections + .iter() + .enumerate() + .find(|(_, target)| target.is_none()); if let Some((id, _)) = id { self.connections[id] = Some(rpc_connection); return id; @@ -408,11 +576,12 @@ impl Daemon { let new_id = self.connections.len(); self.connections.push(Some(rpc_connection)); - return new_id; + new_id } fn mount_machine(&mut self, vm: VirtualMachine) { + log::info!("Loaded {}", vm.name()); let name = vm.name().to_string(); - self.machines.insert(name.clone(), vm); + self.machines.insert(name, vm); } -} \ No newline at end of file +}