use indexmap::IndexMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::Error;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "GpusInput")]
pub struct Gpus {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub count: Option<GpuCount>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub ids: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub driver: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<String>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub options: IndexMap<String, String>,
}
pub const DEFAULT_GPU_DRIVER: &str = "nvidia";
impl Gpus {
#[must_use]
pub fn driver(&self) -> &str {
self.driver.as_deref().unwrap_or(DEFAULT_GPU_DRIVER)
}
#[must_use]
pub fn fixed_count(&self) -> Option<u32> {
match self.count {
Some(GpuCount::All) => None,
Some(GpuCount::Count(n)) => Some(n),
None if !self.ids.is_empty() => u32::try_from(self.ids.len()).ok(),
None => Some(1),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GpuCount {
Count(u32),
All,
}
impl GpuCount {
const ALL: &'static str = "all";
}
impl Serialize for GpuCount {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
match self {
GpuCount::Count(n) => s.serialize_u32(*n),
GpuCount::All => s.serialize_str(GpuCount::ALL),
}
}
}
#[derive(Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum GpusInput {
Count(u32),
Word(String),
Full(GpusFull),
}
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct GpusFull {
#[serde(default)]
#[schemars(schema_with = "count_schema")]
count: Option<serde_json::Value>,
#[serde(default)]
ids: Vec<String>,
#[serde(default)]
driver: Option<String>,
#[serde(default)]
capabilities: Vec<String>,
#[serde(default)]
options: IndexMap<String, String>,
}
fn count_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"anyOf": [
{ "type": "integer", "format": "uint32", "minimum": 0, "description": "That many." },
{ "type": "string", "const": GpuCount::ALL, "description": "Every GPU of the host." }
]
})
}
fn count(value: &serde_json::Value) -> Result<GpuCount, Error> {
match value {
serde_json::Value::String(s) if s == GpuCount::ALL => Ok(GpuCount::All),
serde_json::Value::Number(n) => n
.as_u64()
.and_then(|n| u32::try_from(n).ok())
.map(GpuCount::Count)
.ok_or_else(|| Error::GpuCount(n.to_string())),
other => Err(Error::GpuCount(other.to_string())),
}
}
impl TryFrom<GpusInput> for Gpus {
type Error = Error;
fn try_from(input: GpusInput) -> Result<Gpus, Error> {
match input {
GpusInput::Count(n) => Ok(Gpus {
count: Some(GpuCount::Count(n)),
..Gpus::default()
}),
GpusInput::Word(w) if w == GpuCount::ALL => Ok(Gpus {
count: Some(GpuCount::All),
..Gpus::default()
}),
GpusInput::Word(w) => Err(Error::GpusWord(w)),
GpusInput::Full(f) => Ok(Gpus {
count: f.count.as_ref().map(count).transpose()?,
ids: f.ids,
driver: f.driver,
capabilities: f.capabilities,
options: f.options,
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "DeviceInput")]
pub struct Device {
pub host: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub guest: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permissions: Option<String>,
}
impl Device {
#[must_use]
pub fn guest(&self) -> &str {
self.guest.as_deref().unwrap_or(&self.host)
}
}
#[derive(Deserialize, JsonSchema)]
#[serde(untagged)]
pub enum DeviceInput {
Short(String),
Full(DeviceFull),
}
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct DeviceFull {
host: String,
#[serde(default)]
guest: Option<String>,
#[serde(default)]
permissions: Option<String>,
}
impl TryFrom<DeviceInput> for Device {
type Error = Error;
fn try_from(input: DeviceInput) -> Result<Device, Error> {
match input {
DeviceInput::Full(DeviceFull { host, guest, permissions }) => Ok(Device { host, guest, permissions }),
DeviceInput::Short(s) => {
let mut parts = s.split(':').map(String::from);
let host = parts.next().unwrap_or_default();
let device = Device {
host,
guest: parts.next(),
permissions: parts.next(),
};
match parts.next() {
Some(_) => Err(Error::Device(s)),
None => Ok(device),
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gpus(yaml: &str) -> Result<Gpus, String> {
serde_yaml_ng::from_str(yaml).map_err(|e| e.to_string())
}
#[test]
fn gpus_read_as_a_count_all_or_in_full() {
assert_eq!(gpus("2").unwrap().fixed_count(), Some(2));
assert_eq!(gpus("all").unwrap().count, Some(GpuCount::All));
let full = gpus("{ ids: ['0', '1'], driver: amd, capabilities: [compute] }").unwrap();
assert_eq!((full.fixed_count(), full.driver()), (Some(2), "amd"));
assert_eq!(gpus("{ count: all }").unwrap().fixed_count(), None);
assert_eq!(gpus("{}").unwrap().fixed_count(), Some(1));
assert_eq!(gpus("{}").unwrap().driver(), DEFAULT_GPU_DRIVER);
assert!(gpus("some").unwrap_err().contains("a number, `all` or a mapping"));
assert!(gpus("{ count: many }").is_err());
}
#[test]
fn devices_read_short_or_in_full() {
for (yaml, guest, permissions) in [
("/dev/fuse", "/dev/fuse", None),
("/dev/ttyUSB0:/dev/ttyS1:r", "/dev/ttyS1", Some("r")),
("{ host: /dev/kvm, permissions: rw }", "/dev/kvm", Some("rw")),
] {
let d: Device = serde_yaml_ng::from_str(yaml).unwrap();
assert_eq!((d.guest(), d.permissions.as_deref()), (guest, permissions), "{yaml}");
}
assert!(serde_yaml_ng::from_str::<Device>("/a:/b:r:x").is_err());
}
#[test]
fn they_serialize_in_full() {
let d: Device = serde_yaml_ng::from_str("/dev/fuse").unwrap();
assert_eq!(serde_yaml_ng::to_string(&d).unwrap(), "host: /dev/fuse\n");
assert_eq!(serde_yaml_ng::to_string(&gpus("all").unwrap()).unwrap(), "count: all\n");
}
}