more progress

main
eater 3 years ago
parent b538ff1657
commit 921a791590
Signed by: eater
GPG Key ID: AD2560A0F84F0759

@ -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 <feature>.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/<name>/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 <addr>`
- [ ] 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

@ -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/

@ -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

@ -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");

@ -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<String, Value>) -> Result<PulseConfig, anyhow::Error> {
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)
}
}

@ -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();
}
}

@ -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<Mutex<VoreLuaStorageInner>>);
#[derive(Debug)]
pub struct VoreLuaStorageInner {
build_command: Option<RegistryKey>,
disk_presets: HashMap<String, RegistryKey>,
disk_presets: HashMap<String, VoreLuaDiskPreset>,
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::<Function>(key)?
lua.registry_value::<Function>(&preset.callback)?
};
function.call((vm, instance, index, disk))
@ -262,6 +276,28 @@ impl QemuCommandBuilder {
Ok(())
}
pub fn list_presets(self) -> anyhow::Result<Vec<(String, String)>> {
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::<Vec<_>>()
};
self.clean_up()?;
Ok(result)
}
pub fn build(self, config: &InstanceConfig) -> Result<Vec<String>, anyhow::Error> {
self.lua
.load(&self.script)
@ -287,6 +323,8 @@ impl QemuCommandBuilder {
let mut vm_instance = build_command.call::<MultiValue, VirtualMachine>(multi)?;
mem::drop(build_command);
// Weird building way is for clarity sake
let mut cmd: Vec<String> = 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(())
}
}

@ -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,
}, {})
}
DiskPresets({}, {
pub presets: Vec<DiskPreset>
})
}

@ -0,0 +1,32 @@
use anyhow::Context;
use std::ffi::{CStr, CString};
pub fn get_username_by_uid(uid: u32) -> anyhow::Result<Option<String>> {
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<u32> {
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)
}
}

@ -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(())

@ -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"] }

@ -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

@ -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<String>) -> anyhow::Result<VirtualMachineInfo> {
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<String>,
) -> anyhow::Result<VirtualMachineInfo> {
Ok(self
.send(LoadRequest {
cdroms,
save,
toml: toml.to_string(),
working_directory: None,
})?
.info)
}
pub fn list_vms(&mut self) -> anyhow::Result<Vec<VirtualMachineInfo>> {
Ok(self.send(ListRequest {})?.items)
}
pub fn list_disk_presets(&mut self) -> anyhow::Result<Vec<DiskPreset>> {
Ok(self.send(DiskPresetsRequest {})?.presets)
}
pub fn host_version(&mut self) -> anyhow::Result<InfoResponse> {
self.send(InfoRequest {})
}
@ -64,4 +75,4 @@ impl Client {
self.send(StopRequest { name: vm })?;
Ok(())
}
}
}

@ -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(

@ -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<String> = None;
let ucred = unsafe {
let mut ucred: libc::ucred = mem::zeroed();
let mut length = size_of::<libc::ucred>() 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,

Loading…
Cancel
Save