diff --git a/config/example.toml b/config/example.toml index 5a02a4c..90cd5f6 100644 --- a/config/example.toml +++ b/config/example.toml @@ -1,7 +1,13 @@ [machine] name = "win10" memory = "12G" -features = ["uefi", "spice", "scream"] +features = [ + "uefi", + "spice", + "pulse", + # "scream", + "looking-glass" +] [cpu] amount = 12 @@ -18,23 +24,23 @@ path = "/dev/disk/by-id/wwn-0x500a0751f008e09d" preset = "ssd" path = "/dev/disk/by-id/wwn-0x5002538e4038852d" -#[[vfio]] -#vendor = 0x10de -#device = 0x1b80 -#index = 1 -# -#graphics = true -# -#[[vfio]] -#vendor = 0x10de -#device = 0x10f0 -#index = 1 -# -#[[vfio]] -#vendor = 0x1022 -#device = 0x149c -#addr = "0b:00.3" -# -#[looking-glass] -#width = 2560 -#height = 1080 +[[vfio]] +vendor = 0x10de +device = 0x1b80 +index = 1 + +graphics = true + +[[vfio]] +vendor = 0x10de +device = 0x10f0 +index = 1 + +[[vfio]] +vendor = 0x1022 +device = 0x149c +addr = "0b:00.3" + +[looking-glass] +width = 2560 +height = 1080 diff --git a/config/qemu.lua b/config/qemu.lua index 9ff54ae..0c21779 100644 --- a/config/qemu.lua +++ b/config/qemu.lua @@ -125,14 +125,20 @@ 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.pulse.enabled then + vm:arg("-device", "intel-hda", "-device", "hda-duplex") + vm:arg("-audiodev", "pa,server=/run/user/1000/pulse/native,id=pa0") + end + vm:arg( "-machine", "q35,accel=kvm,usb=off,vmport=off,dump-guest-core=off,kernel_irqchip=on" ) + -- Pls update vm:arg( "-cpu", - "host,migratable=on,hv-time,hv-relaxed,hv-vapic,hv-spinlocks=0x1fff,hv-vendor-id=whatever,kvm=off" + "host,hv-time,hv-relaxed,hv-vapic,hv-spinlocks=0x1fff,hv-vendor-id=whatever,kvm=off,+topoext" ) return vm diff --git a/resources/vore.def.lua b/resources/vore.def.lua index dfdf524..858e859 100644 --- a/resources/vore.def.lua +++ b/resources/vore.def.lua @@ -83,6 +83,9 @@ end ---@field enabled boolean ---@field socket_path string +---@class Pulse +---@field enabled boolean + ---@class Instance ---@field name string ---@field kvm boolean @@ -96,6 +99,7 @@ end ---@field looking_glass LookingGlass ---@field scream Scream ---@field spice Spice +---@field pulse Pulse ---- ---Add a disk definition to the argument list diff --git a/vore-core/src/instance_config.rs b/vore-core/src/instance_config.rs index ecc52c3..3dc37ea 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}; @@ -19,6 +19,7 @@ pub struct InstanceConfig { pub vfio: Vec, pub looking_glass: LookingGlassConfig, pub scream: ScreamConfig, + pub pulse: PulseConfig, pub spice: SpiceConfig, } @@ -75,15 +76,15 @@ impl InstanceConfig { instance_config.looking_glass = LookingGlassConfig::from_table( config.get_table("looking-glass").unwrap_or_default(), - &instance_config.name, )?; instance_config.scream = ScreamConfig::from_table( config.get_table("scream").unwrap_or_default(), - &instance_config.name, )?; 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())?; + if let Ok(features) = config.get::>("machine.features") { for feature in features { match feature.as_str() { @@ -91,6 +92,7 @@ impl InstanceConfig { "spice" => instance_config.spice.enabled = true, "scream" => instance_config.scream.enabled = true, "uefi" => instance_config.uefi.enabled = true, + "pulse" => instance_config.pulse.enabled = true, _ => {} } } @@ -115,6 +117,7 @@ impl Default for InstanceConfig { vfio: vec![], looking_glass: Default::default(), scream: Default::default(), + pulse: Default::default(), spice: Default::default(), } } @@ -285,7 +288,6 @@ pub struct ScreamConfig { impl ScreamConfig { pub fn from_table( table: HashMap, - name: &str, ) -> Result { let mut cfg = ScreamConfig::default(); if let Some(enabled) = table.get("enabled").cloned() { @@ -294,8 +296,6 @@ impl ScreamConfig { if let Some(mem_path) = table.get("mem-path").cloned() { cfg.mem_path = mem_path.into_str()?; - } else { - cfg.mem_path = format!("/dev/shm/{}-scream", name); } if let Some(buffer_size) = table.get("buffer-size").cloned() { @@ -370,7 +370,6 @@ impl LookingGlassConfig { pub fn from_table( table: HashMap, - name: &str, ) -> Result { let mut cfg = LookingGlassConfig::default(); @@ -380,8 +379,6 @@ impl LookingGlassConfig { if let Some(mem_path) = table.get("mem-path").cloned() { cfg.mem_path = mem_path.into_str()?; - } else { - cfg.mem_path = format!("/dev/shm/{}/looking-glass", name); } match (table.get("buffer-size").cloned(), table.get("width").cloned(), table.get("height").cloned()) { @@ -613,6 +610,26 @@ impl VfioConfig { } } +#[derive(Deserialize, Serialize, Clone, Debug, Default)] +pub struct PulseConfig { + pub enabled: bool, +} + +impl PulseConfig { + pub fn from_table(table: HashMap) -> Result { + let mut cfg = PulseConfig { + enabled: false, + }; + + if let Some(enabled) = table.get("enabled").cloned() { + cfg.enabled = enabled.into_bool()?; + } + + Ok(cfg) + } +} + + #[derive(Deserialize, Serialize, Clone, Debug, Default)] pub struct SpiceConfig { pub enabled: bool, @@ -623,13 +640,17 @@ impl SpiceConfig { pub fn from_table(table: HashMap) -> Result { let mut cfg = SpiceConfig { enabled: false, - socket_path: "/tmp/win10.sock".to_string(), + socket_path: "".to_string(), }; 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()?; + } + Ok(cfg) } } @@ -655,6 +676,11 @@ impl<'de> Deserialize<'de> for PCIAddress { formatter.write_str("Expecting a string") } + fn visit_str(self, v: &str) -> Result where + E: de::Error, { + Ok(v.to_string()) + } + fn visit_string(self, v: String) -> Result { Ok(v) } diff --git a/vore-core/src/qemu.rs b/vore-core/src/qemu.rs index 6a1d823..7f6f9cb 100644 --- a/vore-core/src/qemu.rs +++ b/vore-core/src/qemu.rs @@ -223,11 +223,11 @@ impl QemuCommandBuilder { global: &GlobalConfig, working_dir: PathBuf, ) -> Result { - let lua = Path::new(GLOBAL_CONFIG_LOCATION).join(&global.qemu.script); + let lua = Path::new(GLOBAL_CONFIG_LOCATION).parent().unwrap().join(&global.qemu.script); let builder = QemuCommandBuilder { lua: Lua::new(), - script: fs::read_to_string(lua)?, + script: fs::read_to_string(&lua).with_context(|| format!("Failed to load lua qemu command build script ({:?})", lua))?, storage: VoreLuaStorage::new(working_dir), }; diff --git a/vore-core/src/rpc/calls.rs b/vore-core/src/rpc/calls.rs index 6cedfe3..8b26d2b 100644 --- a/vore-core/src/rpc/calls.rs +++ b/vore-core/src/rpc/calls.rs @@ -13,7 +13,7 @@ macro_rules! define_requests { } #[derive(Clone, Debug, Serialize, Deserialize)] - #[serde(untagged, rename_all = "snake_case")] + #[serde(tag = "answer", rename_all = "snake_case")] pub enum AllResponses { $($name(paste! { [<$name Response >] })),+ } @@ -98,4 +98,8 @@ define_requests! { Unload({ pub name: String, }, {}) + + Kill({ + pub name: String, + }, {}) } \ No newline at end of file diff --git a/vore-core/src/rpc/serde.rs b/vore-core/src/rpc/serde.rs index 300ce71..2708838 100644 --- a/vore-core/src/rpc/serde.rs +++ b/vore-core/src/rpc/serde.rs @@ -43,6 +43,7 @@ impl CommandCenter { } pub fn read_answer(answer: &str) -> Result<(u64, R::Response), CommandError> { + log::debug!("Reading answer: {}", answer); let answer_obj: Answer = serde_json::from_str(answer).map_err(|err| CommandError::InternalError(err.into()))?; match answer_obj.data { diff --git a/vore-core/src/rpc/traits.rs b/vore-core/src/rpc/traits.rs index cd88dfe..1979213 100644 --- a/vore-core/src/rpc/traits.rs +++ b/vore-core/src/rpc/traits.rs @@ -18,7 +18,7 @@ pub struct Answer { } #[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(untagged)] +#[serde(tag = "status", rename_all = "snake_case")] pub enum AnswerResult { Error(AnswerError), #[serde(bound = "R: Response")] diff --git a/vore-core/src/virtual_machine.rs b/vore-core/src/virtual_machine.rs index 662a2b3..1e2fadc 100644 --- a/vore-core/src/virtual_machine.rs +++ b/vore-core/src/virtual_machine.rs @@ -2,7 +2,7 @@ use crate::{GlobalConfig, InstanceConfig, QemuCommandBuilder}; use anyhow::{Context, Error}; use beau_collector::BeauCollector; use qapi::qmp::{QMP, Event}; -use qapi::{Qmp, ExecuteError}; +use qapi::{Qmp}; use std::{fmt, mem}; use std::fmt::{Debug, Formatter, Display}; use std::fs::{read_link, OpenOptions, read_dir}; @@ -76,7 +76,7 @@ impl Debug for ControlSocket { } } -const AUTO_UNBIND_BLACKLIST: &[&str] = &["nvidia"]; +const AUTO_UNBIND_BLACKLIST: &[&str] = &["nvidia", "amdgpu"]; impl VirtualMachine { pub fn new>( @@ -111,6 +111,8 @@ impl VirtualMachine { let mut results = vec![]; results.extend(self.prepare_disks()); results.extend(self.prepare_vfio(execute_fixes, force)); + results.extend(self.prepare_shm()); + results.extend(self.prepare_sockets()); results .into_iter() .bcollect::<()>() @@ -122,6 +124,52 @@ impl VirtualMachine { Ok(()) } + pub fn prepare_shm(&mut self) -> Vec> { + 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); + } + + shm.push(&self.config.looking_glass.mem_path); + } + + if self.config.scream.enabled { + if self.config.scream.mem_path.is_empty() { + self.config.scream.mem_path = format!("/dev/shm/vore/{}/scream", self.config.name); + } + + shm.push(&self.config.scream.mem_path); + } + + 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))) + .collect() + } + + pub fn prepare_sockets(&mut self) -> Vec> { + 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(); + } + + sockets.push(&self.config.spice.socket_path); + } + + sockets + .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))) + .collect() + } + /// /// Doesn't really prepare them, but mostly checks if the user has permissions to read them /// @@ -305,6 +353,14 @@ impl VirtualMachine { Ok(()) } + pub fn boop(&mut self) -> Result<(), anyhow::Error> { + if let Some(qmp) = self.control_socket.as_mut() { + qmp.qmp.nop()?; + } + + self.process_qmp_events() + } + fn process_qmp_events(&mut self) -> Result<(), anyhow::Error> { 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 @@ -366,18 +422,6 @@ impl VirtualMachine { Ok(()) } - pub fn stop_now(&mut self) -> Result<(), anyhow::Error> { - self.stop()?; - - if let Some(mut process) = self.process.take() { - self.wait(Some(Duration::from_secs(30)), VirtualMachineState::Stopped)?; - self.quit()?; - process.wait()?; - } - - Ok(()) - } - pub fn wait_till_stopped(&mut self) -> Result<(), anyhow::Error> { self.wait(None, VirtualMachineState::Stopped)?; Ok(()) @@ -388,19 +432,12 @@ impl VirtualMachine { return Ok(()); } - self.send_qmp_command(&qapi_qmp::quit {}) - .map(|_| ()) - .or_else(|x| - if let Some(ExecuteError::Io(err)) = x.downcast_ref::() { - if err.kind() == ErrorKind::UnexpectedEof { - Ok(()) - } else { - Err(x) - } - } else { - Err(x) - }) - .map_err(From::from) + 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?; } + } + + Ok(()) } fn wait(&mut self, duration: Option, target_state: VirtualMachineState) -> Result { @@ -438,8 +475,12 @@ impl VirtualMachine { } } + if self.state == VirtualMachineState::Loaded { + self.prepare(true, false)? + } + let mut command = Command::new("qemu-system-x86_64"); - command.args(self.get_cmd_line()?); + command.args(self.get_cmd_line().context("Failed to generate qemu command line")?); self.process = Some(command.spawn()?); let mut res = || { @@ -477,7 +518,7 @@ impl VirtualMachine { _info: handshake, }; - // self.pin_qemu_threads()?; + self.pin_qemu_threads()?; control_socket .qmp @@ -495,7 +536,7 @@ impl VirtualMachine { let result_ = res(); if result_.is_err() { if let Some(mut qemu) = self.process.take() { - qemu.kill()?; + let _ = qemu.kill(); qemu.wait()?; } } @@ -547,4 +588,4 @@ impl Write for CloneableUnixStream { fn flush(&mut self) -> io::Result<()> { self.lock()?.flush() } -} +} \ No newline at end of file diff --git a/vore/src/client.rs b/vore/src/client.rs index b98c8dd..04ec78b 100644 --- a/vore/src/client.rs +++ b/vore/src/client.rs @@ -54,4 +54,14 @@ impl Client { self.send(PrepareRequest { name: vm, cdroms })?; Ok(()) } + + pub fn start(&mut self, vm: String, cdroms: Vec) -> anyhow::Result<()> { + self.send(StartRequest { name: vm, cdroms })?; + Ok(()) + } + + pub fn stop(&mut self, vm: String) -> anyhow::Result<()> { + 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 689a851..560aae3 100644 --- a/vore/src/main.rs +++ b/vore/src/main.rs @@ -1,11 +1,13 @@ mod client; -use vore_core::{init_logging}; +use vore_core::{init_logging, VirtualMachineInfo}; use crate::client::Client; use clap::{App, ArgMatches}; -use std::fs; +use std::{fs, mem}; use anyhow::Context; use std::option::Option::Some; +use std::process::Command; +use std::os::unix::process::CommandExt; fn main() { init_logging(); @@ -38,6 +40,18 @@ fn main_res() -> anyhow::Result<()> { vore.prepare(args)?; } + ("start", Some(args)) => { + vore.start(args)?; + } + + ("stop", Some(args)) => { + vore.stop(args)?; + } + + ("looking-glass", Some(args)) => { + vore.looking_glass(args)?; + } + ("daemon", Some(args)) => { match args.subcommand() { ("version", _) => { @@ -60,7 +74,7 @@ fn main_res() -> anyhow::Result<()> { struct LoadVMOptions { config: String, - cdroms: Vec, + cd_roms: Vec, save: bool, } @@ -71,7 +85,7 @@ fn get_load_vm_options(args: &ArgMatches) -> anyhow::Result { Ok(LoadVMOptions { config, - cdroms: 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"), }) } @@ -82,12 +96,17 @@ struct VoreApp { impl VoreApp { fn get_vm_name(&mut self, args: &ArgMatches) -> anyhow::Result { + self.get_vm(args).map(|x| x.name) + } + + 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") { - Ok(vm_name.to_string()) + items.into_iter().find(|x| x.name == vm_name) + .with_context(|| format!("Couldn't find VM with the name '{}'", vm_name)) } else { - let mut items = self.client.list_vms()?; match (items.len(), items.pop()) { - (amount, Some(x)) if amount == 1 => return Ok(x.name), + (amount, Some(x)) if amount == 1 => return Ok(x), (0, None) => anyhow::bail!("There are no VM's loaded"), _ => anyhow::bail!("Multiple VM's are loaded, please specify one"), } @@ -103,7 +122,7 @@ 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.cdroms)?; + 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(()) } @@ -123,4 +142,38 @@ impl VoreApp { 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::>()))?; + Ok(()) + } + + fn looking_glass(mut self, args: &ArgMatches) -> anyhow::Result<()> { + let vm = self.get_vm(args)?; + if !vm.config.looking_glass.enabled { + 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())); + if vm.config.spice.enabled { + command.args(&["-c", &vm.config.spice.socket_path, "-p", "0"]); + } else { + command.args(&["-s", "no"]); + } + + 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::>())); + + mem::drop(self); + command.exec(); + + Ok(()) + } + + fn stop(&mut self, args: &ArgMatches) -> anyhow::Result<()> { + let name = self.get_vm_name(args)?; + self.client.stop(name)?; + Ok(()) + } } diff --git a/vored/src/daemon.rs b/vored/src/daemon.rs index 518c991..951f1bd 100644 --- a/vored/src/daemon.rs +++ b/vored/src/daemon.rs @@ -95,7 +95,7 @@ impl RPCConnection { #[derive(Clone, Eq, PartialEq, Debug)] enum EventTarget { RPCListener, - _Machine(String), + Machine(String), RPCConnection(usize), None, } @@ -236,15 +236,43 @@ impl Daemon { rpc::PrepareResponse {}.into_enum() } - AllRequests::Start(_) => { - anyhow::bail!("Unimplemented"); + AllRequests::Start(val) => { + let cloned = if let Some(machine) = self.machines.get_mut(&val.name) { + machine.start()?; + + machine.control_stream().cloned() + } else { + anyhow::bail!("No machine with the name {} exists", val.name); + }; + + if let Some(cloned) = cloned { + let new_id = self.add_target(EventTarget::Machine(val.name.clone())); + self.poller.add(&cloned, Event::readable(new_id))?; + } + + rpc::StartResponse {}.into_enum() } - AllRequests::Stop(_) => { - anyhow::bail!("Unimplemented"); + AllRequests::Stop(val) => { + if let Some(machine) = self.machines.get_mut(&val.name) { + machine.stop()?; + } else { + anyhow::bail!("No machine with the name {} exists", val.name); + } + + rpc::StartResponse {}.into_enum() } AllRequests::Unload(_) => { anyhow::bail!("Unimplemented"); } + AllRequests::Kill(val) => { + if let Some(machine) = self.machines.get_mut(&val.name) { + machine.quit()?; + } else { + anyhow::bail!("No machine with the name {} exists", val.name); + } + + rpc::StartResponse {}.into_enum() + } }; Ok(resp) @@ -273,8 +301,12 @@ impl Daemon { self.poller.modify(&self.rpc_listener, Event::readable(event.key))?; self.accept_rpc_connections()?; } - EventTarget::_Machine(name) if self.machines.contains_key(&name) => { - if let Some(control_socket) = self.machines[&name].control_stream() { + EventTarget::Machine(name) if self.machines.contains_key(&name) => { + if let Some(machine) = self.machines.get_mut(&name) { + 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))?; } }