diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6f4db3 --- /dev/null +++ b/README.md @@ -0,0 +1,150 @@ +# vore + +> VFIO Orientated Emulation (_definitely_) + +## What is vore? + +`vore` is a virtual machine management tool focused on VFIO set ups. with a minimal TOML file you should be able to get +you should be able to create a VFIO-focused VM. + +It features close integration for this use cases, for example automatic configuration of Looking Glass. + +## How it works + +`vore` loads a TOML file, sends it to the `vored` daemon, which processes it and auto completes required information, +and then passes it to a Lua script. this Lua script builds up the qemu command, which then gets started and managed +by `vored`. + +`vored` also allows you to save definitions, and `reserve` vfio devices, so that they are claimed at system start up. + +## Requirements + +Building: + +- Rust +- Lua 5.4 (including headers) + +Runtime: + +- Lua 5.4 + +## VM Definition + +This is a annotated VM definition with about every option displayed + +```toml +[machine] +# Name of the VM, this will be the name used internally and externally for the vm +name = "win10" +# Amount of memory for the virtual machine +memory = "12G" +# Shorthand for .enabled = true +features = [ + "uefi", + "spice", + "pulse", + "looking-glass" +] +# If vore should automatically start this VM when the daemon starts +#auto-start = false + +[cpu] +# Amount of vCPU's should be given to the +amount = 12 +# If any of the following are given, vore will automatically calculate +# the amount of vCPU's, however if both are given, vore will verify it's correctness +# Amount of threads ever core has +# If amount is even or not set, this is set to 2, if odd, it's set to 1 +#threads = 2 +# Amount of cores on this die +# If amount is even, this is set to amount/2, if odd it's set to amount +# If amount is not set this is 2 +#cores = 6 +# Amount of dies per socket, defaults to 1 +#dies = 1 +# Amount of sockets, defaults to 1 +#sockets = 1 + +# You can add multiple disks by adding more `[[disk]]` entries +[[disk]] +# Preset used for this disk, defined in qemu.lua, +# run `vore disk preset` to list all available presets +preset = "nvme" +# Path to disk file +path = "/dev/disk/by-id/nvme-eui.6479a74530201073" +# Type of disk file, will be automatically set, +# but vore will tell you if it can't figure it out +#disk_type = "raw" + +[[vfio]] +# If when this VM is saved, vored should try to automatically +# bind it to the vfio-pci driver +reserve = true +# vendor, device and index (0-indexed!) can be used to select a certain card +# this will grab the second GTX 1080 in the system +vendor = 0x10de +device = 0x1b80 +index = 1 +# you can also instead set addr directly +# -however- if you set both vore will check if both match and error out if not +# this can be helpful when passing through system devices, +# which may move after insertion of e.g. nvme drive +addr = "0b:00.3" + +# if this device is a graphics card +# it'll both set x-vga, and disable QEMU's virtual GPU +graphics = true + +# if this device is multifunctional +#multifunction = false + +[pulse] +# If a pulseaudio backed audio device should be created +# using the features shorthand is preferred +#enabled = true +# Path to PulseAudio native socket +# if not specified vore will automatically resolve it +#socket-path = "" +# To which user's PulseAudio session it should connect +# Can be prefixed with # to set an id +# Default is #1000, which is the common default user id +#user = "#1000" + +[spice] +# if spice support should be enabled +# using the features shorthand is preferred +#enabled = true +# on which path the SPICE socket should listen +# If not set vore will use /var/lib/vore/instance//spice.sock +#socket-path = "/run/spicy.sock" + +[looking-glass] +# if looking-glass support should be enabled +# using the features shorthand is preferred +#enabled = true +# width, height, and bit depth of the screen LG will transfer +# this info is used to calculate the required shared memory file size +width = 2560 +height = 1080 +#bit-depth = 8 +# Alternatively you can set the buffer size directly +# vore will automatically pick the lowest higher or equal to buffer-size +# that is a power of 2 +#buffer-size = 999999 +# Path to the shared memory file looking-glass should use +# if not specified vore will create a path. +# this is mostly for in the case you use the kvmfr kernel module +#mem-path = "/dev/kvmfr0" +``` + + +# TODO + +- [ ] hugepages support +- [ ] USB passthrough +- [ ] Hot-plug USB via `vore attach ` +- [ ] jack audiodev support +- [ ] qemu cmdline on request (`vore x qemucmd`) +- [ ] Better CPU support and feature assignment +- [ ] more control over CPU pinning (now just pickes the fist amount of CPU's) +- [ ] Network device configuration \ No newline at end of file diff --git a/config/qemu.lua b/config/qemu.lua index 3d3e031..ec7cebe 100644 --- a/config/qemu.lua +++ b/config/qemu.lua @@ -125,6 +125,10 @@ vore:set_build_command(function(instance, vm) vm:arg("-spice", "unix,addr=" .. instance.spice.socket_path .. ",disable-ticketing=on,seamless-migration=on") end + if instance.jack.enabled then + + end + if instance.pulse.enabled then vm:arg("-device", "intel-hda", "-device", "hda-duplex,audiodev=pa0") vm:arg("-audiodev", "pa,server=/run/user/1000/pulse/native,id=pa0") @@ -201,13 +205,13 @@ function ide_disk_gen(name, device_type) end end -vore:register_disk_preset("ssd", virtio_scsi_disk_gen("ssd")) -vore:register_disk_preset("hdd", virtio_scsi_disk_gen("hdd")) +vore:register_disk_preset("ssd", "virtio based SSD (requires virtio drivers)", virtio_scsi_disk_gen("ssd")) +vore:register_disk_preset("hdd", "virtio based HDD (requires virtio drivers)", virtio_scsi_disk_gen("hdd")) -vore:register_disk_preset("iso", ide_disk_gen("iso", "ide-cd")) -vore:register_disk_preset("ide", ide_disk_gen("ide", "ide-hd")) +vore:register_disk_preset("iso", "IDE based CD", ide_disk_gen("iso", "ide-cd")) +vore:register_disk_preset("ide", "IDE based HDD (not recommended, useful when missing virtio drivers)", ide_disk_gen("ide", "ide-hd")) -vore:register_disk_preset("nvme", function(vm, _, _, disk) +vore:register_disk_preset("nvme", "PCIe based NVMe drive", function(vm, _, _, disk) local nvme_id = vm:get_counter("nvme", 1) -- see https://blog.christophersmart.com/2019/12/18/kvm-guests-with-emulated-ssd-and-nvme-drives/ diff --git a/config/global.toml b/config/vored.toml similarity index 100% rename from config/global.toml rename to config/vored.toml diff --git a/resources/vore.def.lua b/resources/vore.def.lua index 858e859..794927a 100644 --- a/resources/vore.def.lua +++ b/resources/vore.def.lua @@ -114,8 +114,9 @@ end ---- ---Register a disk preset ---@param name string +---@param description string ---@param cb fun(vm: VM, instance: Instance, idx: number, disk: Disk): VM -function vore:register_disk_preset(name, cb) +function vore:register_disk_preset(name, description, cb) end ---set_build_command diff --git a/vore-core/src/consts.rs b/vore-core/src/consts.rs index 4938104..3ffb669 100644 --- a/vore-core/src/consts.rs +++ b/vore-core/src/consts.rs @@ -13,9 +13,7 @@ macro_rules! default_env { 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") -); +pub const VORE_CONFIG: &str = + default_env!("VORE_CONFIG", concat!(env!("PWD"), "/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/instance_config.rs b/vore-core/src/instance_config.rs index b9dd67f..78e9fb2 100644 --- a/vore-core/src/instance_config.rs +++ b/vore-core/src/instance_config.rs @@ -1,3 +1,4 @@ +use crate::utils::get_uid_by_username; use anyhow::{Context, Error}; use config::{Config, File, FileFormat, Value}; use serde::de::Visitor; @@ -140,8 +141,8 @@ pub struct CpuConfig { impl Default for CpuConfig { fn default() -> Self { CpuConfig { - amount: 2, - cores: 1, + amount: 4, + cores: 2, threads: 2, dies: 1, sockets: 1, @@ -357,9 +358,13 @@ impl LookingGlassConfig { // Add additional 2mb minimum_needed += 2 * 1024 * 1024; + self.set_buffer_size(minimum_needed); + } + + pub fn set_buffer_size(&mut self, wanted: u64) { let mut i = 1; let mut buffer_size = 1; - while buffer_size < minimum_needed { + while buffer_size <= wanted { i += 1; buffer_size = 2u64.pow(i); } @@ -380,7 +385,7 @@ impl LookingGlassConfig { match (table.get("buffer-size").cloned(), table.get("width").cloned(), table.get("height").cloned()) { (Some(buffer_size), None, None) => { - cfg.buffer_size = buffer_size.into_int()? as u64; + cfg.set_buffer_size(buffer_size.into_int()? as u64); } (None, Some(width), Some(height)) => { @@ -618,16 +623,42 @@ impl VfioConfig { #[derive(Deserialize, Serialize, Clone, Debug, Default)] pub struct PulseConfig { pub enabled: bool, + pub socket_path: String, + pub user: String, + pub user_uid: u32, } impl PulseConfig { pub fn from_table(table: HashMap) -> Result { - let mut cfg = PulseConfig { enabled: false }; + let mut cfg = PulseConfig { + enabled: false, + socket_path: "".to_string(), + user: "#1000".to_string(), + user_uid: 1000, + }; if let Some(enabled) = table.get("enabled").cloned() { cfg.enabled = enabled.into_bool()?; } + if let Some(socket_path) = table.get("socket-path").cloned() { + cfg.socket_path = socket_path.into_str()?; + } + + if let Some(user) = table.get("user").cloned() { + cfg.user = user.into_str()?; + } + + if cfg.socket_path.is_empty() { + if let Some(number) = cfg.user.strip_prefix('#') { + cfg.user_uid = u32::from_str(number).with_context(|| { + format!("Couldn't parse {} as number (for pulse.user)", number) + })?; + } else { + cfg.user_uid = get_uid_by_username(&cfg.user)?; + } + } + Ok(cfg) } } diff --git a/vore-core/src/lib.rs b/vore-core/src/lib.rs index e748049..c95e0c8 100644 --- a/vore-core/src/lib.rs +++ b/vore-core/src/lib.rs @@ -1,10 +1,11 @@ +pub mod consts; +mod cpu_list; mod global_config; mod instance_config; mod qemu; -mod virtual_machine; -mod cpu_list; pub mod rpc; -pub mod consts; +pub mod utils; +mod virtual_machine; mod virtual_machine_info; pub use global_config::*; @@ -16,10 +17,11 @@ pub use virtual_machine_info::*; pub fn init_logging() { let mut builder = pretty_env_logger::formatted_timed_builder(); - #[cfg(debug_assertions)] { + #[cfg(debug_assertions)] + { use log::LevelFilter; builder.filter_level(LevelFilter::Debug); } 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 3a040e1..bb337ed 100644 --- a/vore-core/src/qemu.rs +++ b/vore-core/src/qemu.rs @@ -11,9 +11,9 @@ use mlua::{ use serde::ser::Error; use serde::Deserialize; use std::collections::HashMap; -use std::fs; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, Weak}; +use std::{fs, mem}; #[derive(Debug, Default, Deserialize, Clone)] struct VirtualMachine { @@ -97,10 +97,16 @@ pub struct VoreLuaWeakStorage(Weak>); #[derive(Debug)] pub struct VoreLuaStorageInner { build_command: Option, - disk_presets: HashMap, + disk_presets: HashMap, working_dir: PathBuf, } +#[derive(Debug)] +pub struct VoreLuaDiskPreset { + description: String, + callback: RegistryKey, +} + impl UserData for VoreLuaWeakStorage { fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) { methods.add_method("set_build_command", |l, weak, func: Function| { @@ -122,7 +128,7 @@ impl UserData for VoreLuaWeakStorage { methods.add_method( "register_disk_preset", - |lua, weak, args: (mlua::String, Function)| { + |lua, weak, args: (mlua::String, mlua::String, Function)| { let strong = weak .0 .upgrade() @@ -130,10 +136,18 @@ impl UserData for VoreLuaWeakStorage { let mut this = strong .try_lock() .map_err(|_| LuaError::custom("Failed to lock vore storage"))?; - let key = lua.create_registry_value(args.1)?; + let key = lua.create_registry_value(args.2)?; - if let Some(old) = this.disk_presets.insert(args.0.to_str()?.to_string(), key) { - lua.remove_registry_value(old)?; + let new_preset = VoreLuaDiskPreset { + description: args.1.to_str()?.to_string(), + callback: key, + }; + + if let Some(old) = this + .disk_presets + .insert(args.0.to_str()?.to_string(), new_preset) + { + lua.remove_registry_value(old.callback)?; } Ok(Value::Nil) @@ -187,7 +201,7 @@ impl UserData for VoreLuaWeakStorage { .with_context(|| format!("Disk {} has no preset", index)) .map_err(LuaError::external)?; - let key = this + let preset = this .disk_presets .get(&preset_name) .clone() @@ -196,7 +210,7 @@ impl UserData for VoreLuaWeakStorage { }) .map_err(LuaError::external)?; - lua.registry_value::(key)? + lua.registry_value::(&preset.callback)? }; function.call((vm, instance, index, disk)) @@ -262,6 +276,28 @@ impl QemuCommandBuilder { Ok(()) } + pub fn list_presets(self) -> anyhow::Result> { + self.lua + .load(&self.script) + .eval::<()>() + .context("Failed to run the configured qemu lua script")?; + + let result = { + self.storage + .0 + .lock() + .unwrap() + .disk_presets + .iter() + .map(|(name, preset)| (name.clone(), preset.description.clone())) + .collect::>() + }; + + self.clean_up()?; + + Ok(result) + } + pub fn build(self, config: &InstanceConfig) -> Result, anyhow::Error> { self.lua .load(&self.script) @@ -287,6 +323,8 @@ impl QemuCommandBuilder { let mut vm_instance = build_command.call::(multi)?; + mem::drop(build_command); + // Weird building way is for clarity sake let mut cmd: Vec = vec![ "-name".into(), @@ -318,6 +356,12 @@ impl QemuCommandBuilder { cmd.append(&mut vm_instance.args); + self.clean_up()?; + + Ok(cmd) + } + + pub fn clean_up(self) -> anyhow::Result<()> { self.lua.globals().raw_remove("vore")?; self.lua.gc_collect()?; @@ -335,11 +379,11 @@ impl QemuCommandBuilder { self.lua .remove_registry_value(storage.build_command.unwrap())?; for (_, item) in storage.disk_presets.into_iter() { - self.lua.remove_registry_value(item)?; + self.lua.remove_registry_value(item.callback)?; } self.lua.gc_collect()?; - Ok(cmd) + Ok(()) } } diff --git a/vore-core/src/rpc/calls.rs b/vore-core/src/rpc/calls.rs index 47f185b..26493b8 100644 --- a/vore-core/src/rpc/calls.rs +++ b/vore-core/src/rpc/calls.rs @@ -1,8 +1,8 @@ -use serde::{Deserialize, Serialize}; -use std::fmt::Debug; -use paste::paste; use crate::rpc::{Request, Response}; use crate::VirtualMachineInfo; +use paste::paste; +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; macro_rules! define_requests { ($($name:ident($req:tt, $resp:tt))+) => { @@ -57,6 +57,12 @@ impl Response for AllResponses { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DiskPreset { + pub name: String, + pub description: String, +} + define_requests! { Info({}, { pub name: String, @@ -102,4 +108,8 @@ define_requests! { Kill({ pub name: String, }, {}) -} \ No newline at end of file + + DiskPresets({}, { + pub presets: Vec + }) +} diff --git a/vore-core/src/utils.rs b/vore-core/src/utils.rs new file mode 100644 index 0000000..b12e738 --- /dev/null +++ b/vore-core/src/utils.rs @@ -0,0 +1,32 @@ +use anyhow::Context; +use std::ffi::{CStr, CString}; + +pub fn get_username_by_uid(uid: u32) -> anyhow::Result> { + unsafe { + let passwd = libc::getpwuid(uid); + if !passwd.is_null() { + return Ok(Some( + CStr::from_ptr((*passwd).pw_name) + .to_str() + .with_context(|| { + format!("Username of user with uid {} is not valid UTF-8", uid) + }) + .map(|x| x.to_string())?, + )); + } else { + Ok(None) + } + } +} + +pub fn get_uid_by_username(username: &str) -> anyhow::Result { + unsafe { + let c_str = CString::new(username)?; + let passwd = libc::getpwnam(c_str.as_ptr()); + if passwd.is_null() { + anyhow::bail!("No user found with the name {}", username); + } + + Ok((*passwd).pw_uid) + } +} diff --git a/vore-core/src/virtual_machine.rs b/vore-core/src/virtual_machine.rs index 3eaad0c..dbf0663 100644 --- a/vore-core/src/virtual_machine.rs +++ b/vore-core/src/virtual_machine.rs @@ -467,8 +467,12 @@ impl VirtualMachine { err?; } } - self.control_socket = None; + if let Some(mut proc) = self.process.take() { + let _ = proc.wait(); + } + + self.control_socket = None; self.state = VirtualMachineState::Prepared; Ok(()) diff --git a/vore/Cargo.toml b/vore/Cargo.toml index 6436f70..2adc80a 100644 --- a/vore/Cargo.toml +++ b/vore/Cargo.toml @@ -8,7 +8,7 @@ edition = "2018" [dependencies] anyhow = "1.0.40" -vore-core = { path = "../vore-core" } +vore-core = { features = ["client"], path = "../vore-core" } log = "0.4.14" pretty_env_logger = "0.3" clap = { version = "2.33.3", features = ["yaml"] } \ No newline at end of file diff --git a/vore/clap.yml b/vore/clap.yml index 21b5802..6316d24 100644 --- a/vore/clap.yml +++ b/vore/clap.yml @@ -75,6 +75,12 @@ subcommands: takes_value: true - list: about: "List loaded VMs" + - disk: + setting: SubcommandRequiredElseHelp + about: "Disk related actions" + subcommands: + - presets: + about: "List the defined presets as currently known to the daemon" - scream: setting: SubcommandRequiredElseHelp diff --git a/vore/src/client.rs b/vore/src/client.rs index 04ec78b..5bc9af2 100644 --- a/vore/src/client.rs +++ b/vore/src/client.rs @@ -1,9 +1,9 @@ +use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::UnixStream; use std::path::Path; +use vore_core::rpc::*; use vore_core::rpc::{CommandCenter, Request}; -use std::io::{BufReader, Write, BufRead}; use vore_core::{CloneableUnixStream, VirtualMachineInfo}; -use vore_core::rpc::*; pub struct Client { stream: CloneableUnixStream, @@ -33,19 +33,30 @@ impl Client { Ok(info) } - pub fn load_vm(&mut self, toml: &str, save: bool, cdroms: Vec) -> anyhow::Result { - Ok(self.send(LoadRequest { - cdroms, - save, - toml: toml.to_string(), - working_directory: None, - })?.info) + pub fn load_vm( + &mut self, + toml: &str, + save: bool, + cdroms: Vec, + ) -> anyhow::Result { + Ok(self + .send(LoadRequest { + cdroms, + save, + toml: toml.to_string(), + working_directory: None, + })? + .info) } pub fn list_vms(&mut self) -> anyhow::Result> { Ok(self.send(ListRequest {})?.items) } + pub fn list_disk_presets(&mut self) -> anyhow::Result> { + Ok(self.send(DiskPresetsRequest {})?.presets) + } + pub fn host_version(&mut self) -> anyhow::Result { self.send(InfoRequest {}) } @@ -64,4 +75,4 @@ impl Client { self.send(StopRequest { name: vm })?; Ok(()) } -} \ No newline at end of file +} diff --git a/vore/src/main.rs b/vore/src/main.rs index d9a9212..26c4598 100644 --- a/vore/src/main.rs +++ b/vore/src/main.rs @@ -8,6 +8,7 @@ use std::os::unix::process::CommandExt; use std::process::Command; use std::{fs, mem}; use vore_core::consts::VORE_SOCKET; +use vore_core::rpc::DiskPreset; use vore_core::{init_logging, VirtualMachineInfo}; fn main() { @@ -61,6 +62,16 @@ fn main_res() -> anyhow::Result<()> { } }, + ("disk", Some(args)) => match args.subcommand() { + ("presets", _) => { + vore.list_presets()?; + } + + (s, _) => { + log::error!("Subcommand disk.{} not implemented", s); + } + }, + (s, _) => { log::error!("Subcommand {} not implemented", s); } @@ -140,6 +151,16 @@ impl VoreApp { Ok(()) } + fn list_presets(&mut self) -> anyhow::Result<()> { + let items = self.client.list_disk_presets()?; + + for DiskPreset { name, description } in items { + println!("{}\t{}", name, description) + } + + Ok(()) + } + fn prepare(&mut self, args: &ArgMatches) -> anyhow::Result<()> { let name = self.get_vm_name(args)?; self.client.prepare( diff --git a/vored/src/daemon.rs b/vored/src/daemon.rs index de8ee8f..99a5967 100644 --- a/vored/src/daemon.rs +++ b/vored/src/daemon.rs @@ -4,7 +4,6 @@ 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}; @@ -16,8 +15,9 @@ 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::rpc::{AllRequests, AllResponses, Command, CommandCenter, DiskPreset, Response}; +use vore_core::utils::get_username_by_uid; +use vore_core::{rpc, QemuCommandBuilder, VirtualMachineInfo}; use vore_core::{GlobalConfig, InstanceConfig, VirtualMachine}; #[derive(Debug)] @@ -400,6 +400,19 @@ impl Daemon { rpc::StartResponse {}.into_enum() } + AllRequests::DiskPresets(_) => { + let builder = + QemuCommandBuilder::new(&self.global_config, PathBuf::from("/dev/empty"))?; + + rpc::DiskPresetsResponse { + presets: builder + .list_presets()? + .into_iter() + .map(|(name, description)| DiskPreset { name, description }) + .collect(), + } + .into_enum() + } }; Ok(resp) @@ -491,7 +504,6 @@ impl Daemon { stream.set_nonblocking(true)?; - let mut user: Option = None; let ucred = unsafe { let mut ucred: libc::ucred = mem::zeroed(); let mut length = size_of::() as u32; @@ -502,17 +514,11 @@ impl Daemon { (&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()) - } - ucred }; + let user = get_username_by_uid(ucred.uid)?; + let conn = RpcConnection { stream, address,