You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

493 lines
14 KiB
Rust

use anyhow::{Context, Error};
use config::{Config, File, FileFormat, Value};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str::FromStr;
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct InstanceConfig {
pub name: String,
pub arch: String,
pub chipset: String,
pub kvm: bool,
pub memory: u64,
pub cpu: CpuConfig,
pub disks: Vec<DiskConfig>,
pub uefi: UefiConfig,
pub vfio: Vec<VfioConfig>,
pub looking_glass: LookingGlassConfig,
pub scream: ScreamConfig,
pub spice: SpiceConfig,
}
impl InstanceConfig {
pub fn from_toml(toml: &str) -> Result<Self, anyhow::Error> {
let toml = Config::new().with_merged(File::from_str(toml, FileFormat::Toml))?;
Self::from_config(toml)
}
pub fn from_config(config: Config) -> Result<InstanceConfig, anyhow::Error> {
let mut instance_config = InstanceConfig::default();
if let Ok(name) = config.get_str("machine.name") {
instance_config.name = name
}
if let Ok(kvm) = config.get::<Value>("machine.kvm") {
instance_config.kvm = kvm.into_bool().context("machine.kvm should be a boolean")?;
}
if let Ok(mem) = config.get::<Value>("machine.memory") {
let mem = mem
.into_str()
.context("machine.memory should be a string or number")?;
instance_config.memory = parse_size(&mem)?;
}
if let Ok(cpu) = config.get_table("cpu") {
instance_config.cpu.apply_table(cpu)?
}
if let Ok(disks) = config.get::<Value>("disk") {
let arr = disks.into_array().context("disk should be an array")?;
for (i, disk) in arr.into_iter().enumerate() {
let table = disk
.into_table()
.with_context(|| format!("disk[{}] should be a table", i))?;
instance_config.disks.push(DiskConfig::from_table(table)?);
}
}
if let Ok(uefi) = config.get_table("uefi") {
instance_config.uefi.apply_table(uefi)?;
}
if let Ok(vfio) = config.get::<Value>("vfio") {
let arr = vfio.into_array().context("vfio should be an array")?;
for (i, disk) in arr.into_iter().enumerate() {
let table = disk
.into_table()
.with_context(|| format!("vfio[{}] should be a table", i))?;
instance_config.vfio.push(VfioConfig::from_table(table)?);
}
}
if let Ok(looking_glass) = config.get_table("looking-glass") {
instance_config.looking_glass =
LookingGlassConfig::from_table(looking_glass, &instance_config.name)?;
}
if let Ok(scream) = config.get_table("scream") {
instance_config.scream = ScreamConfig::from_table(scream, &instance_config.name)?;
}
if let Ok(scream) = config.get_table("spice") {
instance_config.spice = SpiceConfig::from_table(scream)?;
}
Ok(instance_config)
}
}
impl Default for InstanceConfig {
fn default() -> Self {
InstanceConfig {
name: "vore".to_string(),
arch: std::env::consts::ARCH.to_string(),
chipset: "q35".to_string(),
kvm: true,
// 2 GB
memory: 2 * 1024 * 1024 * 1024,
cpu: Default::default(),
disks: vec![],
uefi: Default::default(),
vfio: vec![],
looking_glass: Default::default(),
scream: Default::default(),
spice: Default::default(),
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct CpuConfig {
pub amount: u64,
pub cores: u64,
pub threads: u64,
pub dies: u64,
pub sockets: u64,
}
impl Default for CpuConfig {
fn default() -> Self {
CpuConfig {
amount: 2,
cores: 1,
threads: 2,
dies: 1,
sockets: 1,
}
}
}
fn get_positive_number_from_table(
table: &HashMap<String, Value>,
key: &str,
prefix: &str,
) -> Result<Option<u64>, Error> {
table
.get(key)
.cloned()
.map(|x| {
x.into_int()
.with_context(|| format!("Failed to parse {}.{} as number", prefix, key))
.and_then(|x| {
Some(x)
.filter(|x| !x.is_negative())
.map(|x| x as u64)
.ok_or_else(|| {
anyhow::Error::msg(format!("{}.{} can't be negative", prefix, key))
})
})
})
.transpose()
}
impl CpuConfig {
fn apply_table(&mut self, table: HashMap<String, Value>) -> Result<(), anyhow::Error> {
if let Some(amount) = get_positive_number_from_table(&table, "amount", "cpu")? {
self.amount = amount;
}
if let Some(cores) = get_positive_number_from_table(&table, "cores", "cpu")? {
self.cores = cores;
}
if let Some(threads) = get_positive_number_from_table(&table, "threads", "cpu")? {
self.threads = threads;
}
if let Some(dies) = get_positive_number_from_table(&table, "dies", "cpu")? {
self.dies = dies;
}
if let Some(sockets) = get_positive_number_from_table(&table, "sockets", "cpu")? {
self.sockets = sockets;
}
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;
}
}
}
Ok(())
}
}
fn parse_size(orig_input: &str) -> Result<u64, anyhow::Error> {
let input = orig_input.to_string().to_lowercase().replace(" ", "");
let mut input = input.strip_suffix("b").unwrap_or(&input);
let mut modifier: u64 = 1;
if input.chars().last().unwrap_or('_').is_alphabetic() {
modifier = match input.chars().last().unwrap() {
'k' => {
return Err(anyhow::Error::msg(
"size can only be specified in megabytes or larger",
));
}
'm' => 1,
'g' => 1024,
't' => 1024 * 1024,
_ => {
return Err(anyhow::Error::msg(format!(
"'{}' is not a valid size",
orig_input
)));
}
};
input = &input[..input.len() - 1];
}
if input.len() == 0 {
return Err(anyhow::Error::msg(format!(
"'{}' is not a valid size",
orig_input
)));
}
u64::from_str(input)
.context(format!("'{}' is not a valid size", orig_input))
.map(|x| x * modifier)
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct UefiConfig {
pub enabled: bool,
}
impl Default for UefiConfig {
fn default() -> Self {
UefiConfig { enabled: false }
}
}
impl UefiConfig {
fn apply_table(&mut self, table: HashMap<String, Value>) -> Result<(), anyhow::Error> {
if let Some(enabled) = table
.get("enabled")
.cloned()
.map(|x| x.into_bool().context("eufi.enabled should be a boolean"))
.transpose()?
{
self.enabled = enabled
}
Ok(())
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ScreamConfig {
pub enabled: bool,
pub mem_path: String,
pub buffer_size: u64,
}
impl ScreamConfig {
pub fn from_table(
table: HashMap<String, Value>,
name: &str,
) -> Result<ScreamConfig, anyhow::Error> {
let mut cfg = ScreamConfig::default();
if let Some(enabled) = table.get("enabled").cloned() {
cfg.enabled = enabled.into_bool()?;
}
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() {
cfg.buffer_size = buffer_size.into_int()? as u64;
}
Ok(cfg)
}
}
impl Default for ScreamConfig {
fn default() -> Self {
ScreamConfig {
enabled: false,
mem_path: "".to_string(),
buffer_size: 2097152,
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct LookingGlassConfig {
pub enabled: bool,
pub mem_path: String,
pub buffer_size: u64,
pub width: u64,
pub height: u64,
pub bit_depth: u64,
}
impl Default for LookingGlassConfig {
fn default() -> Self {
LookingGlassConfig {
enabled: false,
mem_path: "".to_string(),
buffer_size: 0,
width: 1920,
height: 1080,
bit_depth: 8,
}
}
}
impl LookingGlassConfig {
pub fn calc_buffer_size_from_screen(&mut self) {
// https://forum.level1techs.com/t/solved-what-is-max-frame-size-determined-by/170312/4
//
// required memory size is
//
// height * width * 4 * 2 + 2mb
//
// And shared memory size needs to be a power off 2
//
let mut minimum_needed =
self.width * self.height * (((self.bit_depth * 4) as f64 / 8f64).ceil() as u64);
// 2 frames
minimum_needed *= 2;
// Add additional 2mb
minimum_needed += 2 * 1024 * 1024;
let mut i = 1;
let mut buffer_size = 1;
while buffer_size < minimum_needed {
i += 1;
buffer_size = 2u64.pow(i);
}
self.buffer_size = buffer_size;
}
pub fn from_table(
table: HashMap<String, Value>,
name: &str,
) -> Result<LookingGlassConfig, anyhow::Error> {
let mut cfg = LookingGlassConfig::default();
if let Some(enabled) = table.get("enabled").cloned() {
cfg.enabled = enabled.into_bool()?;
}
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()) {
(Some(buffer_size), None, None) => {
cfg.buffer_size = buffer_size.into_int()? as u64;
}
(None, Some(width), Some(height)) => {
let width = width.into_int()? as u64;
let height = height.into_int()? as u64;
let bit_depth = table.get("bit-depth").cloned().map_or(Ok(cfg.bit_depth), |x| x.into_int().map(|x| x as u64))?;
cfg.bit_depth = bit_depth;
cfg.width = width;
cfg.height = height;
cfg.calc_buffer_size_from_screen();
}
(None, None, None) => {
cfg.calc_buffer_size_from_screen()
}
_ => anyhow::bail!("for looking-glass either width and height need to be set or buffer-size should be set")
}
Ok(cfg)
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct DiskConfig {
pub disk_type: String,
pub preset: String,
pub path: String,
}
impl DiskConfig {
pub fn from_table(table: HashMap<String, Value>) -> Result<DiskConfig, anyhow::Error> {
let path = table
.get("path")
.cloned()
.ok_or_else(|| anyhow::Error::msg("Disk needs a path"))?
.into_str()
.context("Disk path must be a string")?;
let disk_type = if let Some(disk_type) = table.get("type").cloned() {
disk_type.into_str()?
} else {
(kiam::when! {
path.starts_with("/dev") => "raw",
path.ends_with(".qcow2") => "qcow2",
_ => return Err(anyhow::Error::msg("Can't figure out from path what type of disk driver should be used"))
}).to_string()
};
let preset = table.get("preset").cloned().context("gamer")?.into_str()?;
let disk = DiskConfig {
disk_type,
preset,
path,
};
// TODO: Add blockdev details
Ok(disk)
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct VfioConfig {
pub slot: String,
pub graphics: bool,
pub multifunction: bool,
}
impl VfioConfig {
pub fn from_table(table: HashMap<String, Value>) -> Result<VfioConfig, anyhow::Error> {
let slot = table
.get("slot")
.cloned()
.ok_or_else(|| anyhow::anyhow!("vfio table needs a slot"))?
.into_str()?;
let mut cfg = VfioConfig {
slot,
graphics: false,
multifunction: false,
};
if let Some(graphics) = table.get("graphics").cloned() {
cfg.graphics = graphics.into_bool()?;
}
if let Some(multifunction) = table.get("multifunction").cloned() {
cfg.multifunction = multifunction.into_bool()?;
}
Ok(cfg)
}
}
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
pub struct SpiceConfig {
pub enabled: bool,
pub socket_path: String,
}
impl SpiceConfig {
pub fn from_table(table: HashMap<String, Value>) -> Result<SpiceConfig, anyhow::Error> {
let mut cfg = SpiceConfig {
enabled: false,
socket_path: "/tmp/win10.sock".to_string(),
};
if let Some(enabled) = table.get("enabled").cloned() {
cfg.enabled = enabled.into_bool()?;
}
Ok(cfg)
}
}