Compare commits
9 commits
66abe2d2d3
...
29aeee4f49
| Author | SHA1 | Date | |
|---|---|---|---|
| 29aeee4f49 | |||
| 7dabe5a0d2 | |||
| 43c99ec162 | |||
| 40f726ffcc | |||
| 5126a16ffd | |||
| 3b4d90bcca | |||
| 609eccae4a | |||
| 4ef549b981 | |||
| 22259e187f |
8 changed files with 526 additions and 7 deletions
|
|
@ -91,6 +91,7 @@ The configured user can manage VM services via polkit (no sudo required for `vm-
|
|||
- `vm-stop <name>` — stop VM via systemd (polkit, no sudo)
|
||||
- `vm-start-debug <name>` — start VM directly for debugging (requires sudo)
|
||||
- `vm-shell <name>` — connect to VM serial console (default) or SSH with `--ssh`
|
||||
- `vmsilo-usb [attach|detach] [<name> <vid:pid|devpath>]` — list USB devices, attach/detach USB devices to VMs
|
||||
|
||||
See README.md for full usage details and options.
|
||||
|
||||
|
|
|
|||
34
README.md
34
README.md
|
|
@ -186,6 +186,7 @@ There are a lot of configuration options but you don't really need to touch most
|
|||
| `sound.capture` | bool | `false` | Enable sound capture (implies playback) |
|
||||
| `sharedDirectories` | attrsOf submodule | `{}` | Shared directories via virtiofsd (keys are fs tags, see below) |
|
||||
| `pciDevices` | list of attrsets | `[]` | PCI devices to passthrough (path + optional kv pairs) |
|
||||
| `usbDevices` | list of attrsets | `[]` | USB devices to passthrough (vendorId, productId, optional serial) |
|
||||
| `guestPrograms` | list of packages | `[]` | VM-specific packages |
|
||||
| `guestConfig` | NixOS module(s) | `[]` | VM-specific NixOS configuration (module, list of modules, or path) |
|
||||
| `vhostUser` | list of attrsets | `[]` | Manual vhost-user devices |
|
||||
|
|
@ -403,6 +404,37 @@ programs.vmsilo = {
|
|||
boot.blacklistedKernelModules = [ "xhci_hcd" ]; # for USB controllers
|
||||
```
|
||||
|
||||
### USB Passthrough
|
||||
|
||||
USB devices can be hot-attached to running VMs individually, without passing through an entire USB controller via PCI passthrough.
|
||||
|
||||
#### Configuration
|
||||
|
||||
Use the `usbDevices` per-VM option to declare persistent device assignments. Devices are matched by vendor/product ID, optionally narrowed by serial number. All matching physical devices are attached when the VM starts and detached when it stops.
|
||||
|
||||
```nix
|
||||
banking = {
|
||||
usbDevices = [
|
||||
{ vendorId = "17ef"; productId = "60e0"; }
|
||||
{ vendorId = "046d"; productId = "c52b"; serial = "A02019100900"; }
|
||||
];
|
||||
};
|
||||
```
|
||||
|
||||
#### Runtime CLI
|
||||
|
||||
The `vmsilo-usb` command manages USB device assignments at runtime:
|
||||
|
||||
```bash
|
||||
vmsilo-usb # List all USB devices and which VM they're attached to
|
||||
vmsilo-usb attach <vm> <vid:pid|devpath> # Attach a device (detaches from current VM if needed)
|
||||
vmsilo-usb detach <vm> <vid:pid|devpath> # Detach a device from a VM
|
||||
```
|
||||
|
||||
Devices can be identified by `vid:pid` (e.g., `17ef:60e0`) or by sysfs devpath (e.g., `1-2.3`).
|
||||
|
||||
Persistent devices configured via `usbDevices` are auto-attached at VM start. All USB state is cleaned up when the VM stops.
|
||||
|
||||
### vhost-user Devices
|
||||
|
||||
```nix
|
||||
|
|
@ -527,7 +559,7 @@ The host provides:
|
|||
- Console PTY for serial access (`/run/vmsilo/<name>-console`)
|
||||
- VM services run as root for PCI passthrough and sandboxing (crosvm drops privileges)
|
||||
- Polkit rules for the configured user to manage VM services without sudo
|
||||
- CLI tools: `vm-run`, `vm-start`, `vm-stop`, `vm-start-debug`, `vm-shell`
|
||||
- CLI tools: `vm-run`, `vm-start`, `vm-stop`, `vm-start-debug`, `vm-shell`, `vmsilo-usb`
|
||||
- Desktop integration with .desktop files for guest applications
|
||||
|
||||
### Tray Integration
|
||||
|
|
|
|||
|
|
@ -182,6 +182,20 @@ in
|
|||
) effectiveSharedDirs
|
||||
)
|
||||
) (lib.attrValues cfg.nixosVms)
|
||||
# USB device assertions: no duplicate VID:PID+serial across VMs
|
||||
++ (
|
||||
let
|
||||
allUsbDeviceKeys = lib.concatMap (
|
||||
vm: map (dev: "${dev.vendorId}:${dev.productId}:${toString (dev.serial or "")}") vm.usbDevices
|
||||
) (lib.attrValues cfg.nixosVms);
|
||||
in
|
||||
[
|
||||
{
|
||||
assertion = lib.length allUsbDeviceKeys == lib.length (lib.unique allUsbDeviceKeys);
|
||||
message = "USB devices (VID:PID+serial) cannot be assigned to multiple VMs";
|
||||
}
|
||||
]
|
||||
)
|
||||
# netvm: network.netvm must reference a VM with isNetvm = true (skip for "host")
|
||||
++ lib.concatMap (
|
||||
vm:
|
||||
|
|
|
|||
|
|
@ -602,6 +602,43 @@ let
|
|||
'';
|
||||
};
|
||||
|
||||
usbDevices = lib.mkOption {
|
||||
type = lib.types.listOf (
|
||||
lib.types.submodule {
|
||||
options = {
|
||||
vendorId = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "USB vendor ID (hex, e.g., '17ef').";
|
||||
example = "17ef";
|
||||
};
|
||||
productId = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "USB product ID (hex, e.g., '60e0').";
|
||||
example = "60e0";
|
||||
};
|
||||
serial = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
default = null;
|
||||
description = "USB serial number to match a specific device. If null, matches all devices with this VID:PID.";
|
||||
example = "A02019100900";
|
||||
};
|
||||
};
|
||||
}
|
||||
);
|
||||
default = [ ];
|
||||
description = ''
|
||||
USB devices to pass through to this VM. Devices are attached when the VM starts
|
||||
and detached when it stops. Matched by VID:PID, optionally narrowed by serial number.
|
||||
If multiple physical devices match a VID:PID (and no serial filter), all are attached.
|
||||
'';
|
||||
example = lib.literalExpression ''
|
||||
[
|
||||
{ vendorId = "17ef"; productId = "60e0"; }
|
||||
{ vendorId = "046d"; productId = "c52b"; serial = "A02019100900"; }
|
||||
]
|
||||
'';
|
||||
};
|
||||
|
||||
guestPrograms = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.package;
|
||||
default = [ ];
|
||||
|
|
@ -935,6 +972,12 @@ in
|
|||
internal = true;
|
||||
};
|
||||
|
||||
usbHelperLib = lib.mkOption {
|
||||
type = lib.types.path;
|
||||
description = "USB helper library script.";
|
||||
internal = true;
|
||||
};
|
||||
|
||||
# Generated by netvm.nix: maps VM name -> { interfaces, guestConfig }
|
||||
# Used to inject netvm-derived interfaces and guest config into VMs
|
||||
# without creating a self-referential cycle on nixosVms.
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ let
|
|||
|
||||
# Bash completions
|
||||
${lib.optionalString cfg.enableBashIntegration ''
|
||||
for cmd in vm-run vm-start vm-start-debug vm-stop vm-shell; do
|
||||
for cmd in vm-run vm-start vm-start-debug vm-stop vm-shell vmsilo-usb; do
|
||||
ln -s ${cfg._internal.bashCompletionScript} $out/share/bash-completion/completions/$cmd
|
||||
done
|
||||
''}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,352 @@ let
|
|||
|
||||
vms = assignVmIds cfg.nixosVms;
|
||||
|
||||
# USB helper library — sourced by vmsilo-usb CLI, systemd oneshot services, and persistent attach service
|
||||
usbHelperLib = pkgs.writeShellScript "vmsilo-usb-lib" ''
|
||||
# --- constants ---
|
||||
USB_STATE_FILE=/run/vmsilo/usb-state.json
|
||||
USB_STATE_LOCK=/run/vmsilo/usb-state.lock
|
||||
CROSVM=${cfg._internal.crosvm}/bin/crosvm
|
||||
|
||||
# --- locking (fd 9) ---
|
||||
usb_lock() {
|
||||
exec 9>"''${USB_STATE_LOCK}"
|
||||
${pkgs.util-linux}/bin/flock 9
|
||||
}
|
||||
|
||||
usb_unlock() {
|
||||
exec 9>&-
|
||||
}
|
||||
|
||||
# --- state file ---
|
||||
usb_read_state() {
|
||||
if [ -f "''${USB_STATE_FILE}" ]; then
|
||||
${pkgs.coreutils}/bin/cat "''${USB_STATE_FILE}"
|
||||
else
|
||||
echo '[]'
|
||||
fi
|
||||
}
|
||||
|
||||
usb_write_state() {
|
||||
local json="$1"
|
||||
local tmp="''${USB_STATE_FILE}.tmp.$$"
|
||||
printf '%s\n' "''${json}" > "''${tmp}"
|
||||
${pkgs.coreutils}/bin/mv "''${tmp}" "''${USB_STATE_FILE}"
|
||||
}
|
||||
|
||||
# --- enumerate USB devices from sysfs ---
|
||||
usb_enumerate() {
|
||||
local result='[]'
|
||||
for dev in /sys/bus/usb/devices/[0-9]*-[0-9]*; do
|
||||
[ -d "''${dev}" ] || continue
|
||||
# Skip interface entries (e.g. 1-3:1.0)
|
||||
local base
|
||||
base=$(${pkgs.coreutils}/bin/basename "''${dev}")
|
||||
case "''${base}" in
|
||||
*:*) continue ;;
|
||||
esac
|
||||
|
||||
# Read sysfs attributes
|
||||
local vid pid serial busnum devnum manufacturer product devpath
|
||||
vid=$(${pkgs.coreutils}/bin/cat "''${dev}/idVendor" 2>/dev/null || echo "")
|
||||
pid=$(${pkgs.coreutils}/bin/cat "''${dev}/idProduct" 2>/dev/null || echo "")
|
||||
serial=$(${pkgs.coreutils}/bin/cat "''${dev}/serial" 2>/dev/null || echo "")
|
||||
busnum=$(${pkgs.coreutils}/bin/cat "''${dev}/busnum" 2>/dev/null || echo "")
|
||||
devnum=$(${pkgs.coreutils}/bin/cat "''${dev}/devnum" 2>/dev/null || echo "")
|
||||
manufacturer=$(${pkgs.coreutils}/bin/cat "''${dev}/manufacturer" 2>/dev/null || echo "")
|
||||
product=$(${pkgs.coreutils}/bin/cat "''${dev}/product" 2>/dev/null || echo "")
|
||||
devpath="''${base}"
|
||||
local dev_file="/dev/bus/usb/$(printf '%03d' "''${busnum}")/$(printf '%03d' "''${devnum}")"
|
||||
|
||||
[ -z "''${vid}" ] && continue
|
||||
|
||||
result=$(printf '%s\n' "''${result}" | ${pkgs.jq}/bin/jq -c \
|
||||
--arg devpath "''${devpath}" \
|
||||
--arg vid "''${vid}" \
|
||||
--arg pid "''${pid}" \
|
||||
--arg serial "''${serial}" \
|
||||
--arg busnum "''${busnum}" \
|
||||
--arg devnum "''${devnum}" \
|
||||
--arg manufacturer "''${manufacturer}" \
|
||||
--arg product "''${product}" \
|
||||
--arg dev_file "''${dev_file}" \
|
||||
'. + [{devpath: $devpath, vid: $vid, pid: $pid, serial: $serial, busnum: $busnum, devnum: $devnum, manufacturer: $manufacturer, product: $product, dev_file: $dev_file}]')
|
||||
done
|
||||
printf '%s\n' "''${result}"
|
||||
}
|
||||
|
||||
# --- find by VID:PID (optional serial) ---
|
||||
usb_find_by_vidpid() {
|
||||
local vid="$1" pid="$2" serial="''${3:-}"
|
||||
local devices
|
||||
devices=$(usb_enumerate)
|
||||
if [ -n "''${serial}" ]; then
|
||||
printf '%s\n' "''${devices}" | ${pkgs.jq}/bin/jq -c \
|
||||
--arg vid "''${vid}" --arg pid "''${pid}" --arg serial "''${serial}" \
|
||||
'[.[] | select(.vid == $vid and .pid == $pid and .serial == $serial)]'
|
||||
else
|
||||
printf '%s\n' "''${devices}" | ${pkgs.jq}/bin/jq -c \
|
||||
--arg vid "''${vid}" --arg pid "''${pid}" \
|
||||
'[.[] | select(.vid == $vid and .pid == $pid)]'
|
||||
fi
|
||||
}
|
||||
|
||||
# --- find by devpath ---
|
||||
usb_find_by_devpath() {
|
||||
local devpath="$1"
|
||||
local devices
|
||||
devices=$(usb_enumerate)
|
||||
printf '%s\n' "''${devices}" | ${pkgs.jq}/bin/jq -c \
|
||||
--arg devpath "''${devpath}" \
|
||||
'[.[] | select(.devpath == $devpath)]'
|
||||
}
|
||||
|
||||
# --- attach (expects lock already held) ---
|
||||
usb_do_attach() {
|
||||
local vm_name="$1" devpath="$2" dev_file="$3" vid="$4" pid="$5" serial="$6" busnum="$7" devnum="$8"
|
||||
local state socket response port
|
||||
|
||||
socket="/run/vmsilo/''${vm_name}-crosvm-control.socket"
|
||||
state=$(usb_read_state)
|
||||
|
||||
# Check if already attached somewhere — detach first
|
||||
local existing
|
||||
existing=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
|
||||
--arg devpath "''${devpath}" \
|
||||
'.[] | select(.devpath == $devpath)')
|
||||
if [ -n "''${existing}" ]; then
|
||||
local old_vm old_port old_socket
|
||||
old_vm=$(printf '%s\n' "''${existing}" | ${pkgs.jq}/bin/jq -r '.vm')
|
||||
old_port=$(printf '%s\n' "''${existing}" | ${pkgs.jq}/bin/jq -r '.port')
|
||||
old_socket="/run/vmsilo/''${old_vm}-crosvm-control.socket"
|
||||
if [ -S "''${old_socket}" ]; then
|
||||
''${CROSVM} usb detach "''${old_port}" "''${old_socket}" >/dev/null 2>&1 || true
|
||||
fi
|
||||
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
|
||||
--arg devpath "''${devpath}" \
|
||||
'[.[] | select(.devpath != $devpath)]')
|
||||
fi
|
||||
|
||||
# Attach
|
||||
response=$(''${CROSVM} usb attach "1:1:0000:0000" "''${dev_file}" "''${socket}" 2>&1) || {
|
||||
echo "Error: crosvm usb attach failed: ''${response}" >&2
|
||||
usb_write_state "''${state}"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Parse "ok <port>"
|
||||
port=$(printf '%s\n' "''${response}" | ${pkgs.gnused}/bin/sed -n 's/^ok \([0-9]*\)/\1/p')
|
||||
if [ -z "''${port}" ]; then
|
||||
echo "Error: unexpected crosvm response: ''${response}" >&2
|
||||
usb_write_state "''${state}"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Update state
|
||||
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
|
||||
--arg vm "''${vm_name}" \
|
||||
--arg devpath "''${devpath}" \
|
||||
--arg vid "''${vid}" \
|
||||
--arg pid "''${pid}" \
|
||||
--arg serial "''${serial}" \
|
||||
--arg busnum "''${busnum}" \
|
||||
--arg devnum "''${devnum}" \
|
||||
--arg port "''${port}" \
|
||||
'. + [{vm: $vm, devpath: $devpath, vid: $vid, pid: $pid, serial: $serial, busnum: $busnum, devnum: $devnum, port: ($port | tonumber)}]')
|
||||
usb_write_state "''${state}"
|
||||
echo "Attached ''${vid}:''${pid} (''${devpath}) to ''${vm_name} on port ''${port}"
|
||||
}
|
||||
|
||||
# --- detach (expects lock already held) ---
|
||||
usb_do_detach() {
|
||||
local vm_name="$1" devpath="$2"
|
||||
local state socket
|
||||
|
||||
socket="/run/vmsilo/''${vm_name}-crosvm-control.socket"
|
||||
state=$(usb_read_state)
|
||||
|
||||
local entry
|
||||
entry=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
|
||||
--arg vm "''${vm_name}" --arg devpath "''${devpath}" \
|
||||
'.[] | select(.vm == $vm and .devpath == $devpath)')
|
||||
if [ -z "''${entry}" ]; then
|
||||
echo "Error: device ''${devpath} not attached to ''${vm_name}" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local port
|
||||
port=$(printf '%s\n' "''${entry}" | ${pkgs.jq}/bin/jq -r '.port')
|
||||
|
||||
if [ -S "''${socket}" ]; then
|
||||
local response
|
||||
response=$(''${CROSVM} usb detach "''${port}" "''${socket}" 2>&1) || {
|
||||
echo "Warning: crosvm usb detach failed: ''${response}" >&2
|
||||
}
|
||||
fi
|
||||
|
||||
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
|
||||
--arg vm "''${vm_name}" --arg devpath "''${devpath}" \
|
||||
'[.[] | select(.vm != $vm or .devpath != $devpath)]')
|
||||
usb_write_state "''${state}"
|
||||
echo "Detached ''${devpath} from ''${vm_name} (port ''${port})"
|
||||
}
|
||||
|
||||
# --- cleanup all entries for a VM (acquires lock) ---
|
||||
usb_cleanup_vm() {
|
||||
local vm_name="$1"
|
||||
usb_lock
|
||||
local state
|
||||
state=$(usb_read_state)
|
||||
state=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -c \
|
||||
--arg vm "''${vm_name}" \
|
||||
'[.[] | select(.vm != $vm)]')
|
||||
usb_write_state "''${state}"
|
||||
usb_unlock
|
||||
}
|
||||
'';
|
||||
|
||||
# vmsilo-usb CLI — list/attach/detach USB devices to/from VMs
|
||||
vmsiloUsbScript = pkgs.writeShellScript "vmsilo-usb" ''
|
||||
set -euo pipefail
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source ${usbHelperLib}
|
||||
|
||||
is_devpath() {
|
||||
case "$1" in
|
||||
[0-9]*-[0-9]*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
parse_vidpid() {
|
||||
local input="$1"
|
||||
USB_VID="''${input%%:*}"
|
||||
USB_PID="''${input#*:}"
|
||||
if [ -z "''${USB_VID}" ] || [ -z "''${USB_PID}" ] || [ "''${USB_VID}" = "''${input}" ]; then
|
||||
echo "Error: invalid VID:PID format: ''${input}" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
local devices state
|
||||
devices=$(usb_enumerate)
|
||||
if [ -f "''${USB_STATE_FILE}" ]; then
|
||||
state=$(usb_read_state)
|
||||
else
|
||||
state='[]'
|
||||
fi
|
||||
|
||||
# Merge state into device list and format table
|
||||
printf '%-10s%-6s%-14s%-14s%-24s%s\n' "VID:PID" "Port" "Serial" "Manufacturer" "Product" "VM"
|
||||
printf '%s\n' "''${devices}" | ${pkgs.jq}/bin/jq -r --argjson state "''${state}" '
|
||||
.[] |
|
||||
. as $dev |
|
||||
($state | map(select(.devpath == $dev.devpath)) | if length > 0 then .[0].vm else "-" end) as $vm |
|
||||
[$dev.vid + ":" + $dev.pid,
|
||||
$dev.devpath,
|
||||
(if $dev.serial == "" then "-" else $dev.serial end),
|
||||
(if $dev.manufacturer == "" then "-" else $dev.manufacturer end),
|
||||
(if $dev.product == "" then "-" else $dev.product end),
|
||||
$vm] |
|
||||
@tsv
|
||||
' | while IFS=$'\t' read -r vidpid port serial manufacturer product vm; do
|
||||
printf '%-10s%-6s%-14s%-14s%-24s%s\n' "''${vidpid}" "''${port}" "''${serial}" "''${manufacturer}" "''${product}" "''${vm}"
|
||||
done
|
||||
}
|
||||
|
||||
cmd_attach() {
|
||||
local vm="$1" identifier="$2"
|
||||
|
||||
if is_devpath "''${identifier}"; then
|
||||
local matches
|
||||
matches=$(usb_find_by_devpath "''${identifier}")
|
||||
local count
|
||||
count=$(printf '%s\n' "''${matches}" | ${pkgs.jq}/bin/jq 'length')
|
||||
if [ "''${count}" -eq 0 ]; then
|
||||
echo "Error: no USB device found at ''${identifier}" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "''${matches}" | ${pkgs.jq}/bin/jq -r '.[] | [.devpath, .dev_file, .vid, .pid, .serial, .busnum, .devnum] | @tsv' | \
|
||||
while IFS=$'\t' read -r devpath dev_file vid pid serial busnum devnum; do
|
||||
local instance
|
||||
instance=$(${pkgs.systemd}/bin/systemd-escape "''${vm}:''${devpath}:''${dev_file}:''${vid}:''${pid}:''${serial}:''${busnum}:''${devnum}")
|
||||
systemctl start "vmsilo-usb-attach@''${instance}.service"
|
||||
done
|
||||
else
|
||||
parse_vidpid "''${identifier}"
|
||||
local matches
|
||||
matches=$(usb_find_by_vidpid "''${USB_VID}" "''${USB_PID}")
|
||||
local count
|
||||
count=$(printf '%s\n' "''${matches}" | ${pkgs.jq}/bin/jq 'length')
|
||||
if [ "''${count}" -eq 0 ]; then
|
||||
echo "Error: no USB device found matching ''${identifier}" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "''${matches}" | ${pkgs.jq}/bin/jq -r '.[] | [.devpath, .dev_file, .vid, .pid, .serial, .busnum, .devnum] | @tsv' | \
|
||||
while IFS=$'\t' read -r devpath dev_file vid pid serial busnum devnum; do
|
||||
local instance
|
||||
instance=$(${pkgs.systemd}/bin/systemd-escape "''${vm}:''${devpath}:''${dev_file}:''${vid}:''${pid}:''${serial}:''${busnum}:''${devnum}")
|
||||
systemctl start "vmsilo-usb-attach@''${instance}.service"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_detach() {
|
||||
local vm="$1" identifier="$2"
|
||||
|
||||
if is_devpath "''${identifier}"; then
|
||||
local instance
|
||||
instance=$(${pkgs.systemd}/bin/systemd-escape "''${vm}:''${identifier}")
|
||||
systemctl start "vmsilo-usb-detach@''${instance}.service"
|
||||
else
|
||||
parse_vidpid "''${identifier}"
|
||||
local state
|
||||
state=$(usb_read_state)
|
||||
local devpaths
|
||||
devpaths=$(printf '%s\n' "''${state}" | ${pkgs.jq}/bin/jq -r \
|
||||
--arg vm "''${vm}" --arg vid "''${USB_VID}" --arg pid "''${USB_PID}" \
|
||||
'[.[] | select(.vm == $vm and .vid == $vid and .pid == $pid) | .devpath] | .[]')
|
||||
if [ -z "''${devpaths}" ]; then
|
||||
echo "Error: no ''${identifier} devices attached to ''${vm}" >&2
|
||||
exit 1
|
||||
fi
|
||||
while IFS= read -r devpath; do
|
||||
local instance
|
||||
instance=$(${pkgs.systemd}/bin/systemd-escape "''${vm}:''${devpath}")
|
||||
systemctl start "vmsilo-usb-detach@''${instance}.service"
|
||||
done <<< "''${devpaths}"
|
||||
fi
|
||||
}
|
||||
|
||||
case "''${1:-}" in
|
||||
"")
|
||||
cmd_list
|
||||
;;
|
||||
attach)
|
||||
if [ $# -ne 3 ]; then
|
||||
echo "Usage: vmsilo-usb attach <vm> <vid:pid|devpath>" >&2
|
||||
exit 1
|
||||
fi
|
||||
cmd_attach "$2" "$3"
|
||||
;;
|
||||
detach)
|
||||
if [ $# -ne 3 ]; then
|
||||
echo "Usage: vmsilo-usb detach <vm> <vid:pid|devpath>" >&2
|
||||
exit 1
|
||||
fi
|
||||
cmd_detach "$2" "$3"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: vmsilo-usb [attach <vm> <vid:pid|devpath> | detach <vm> <vid:pid|devpath>]" >&2
|
||||
echo "" >&2
|
||||
echo "With no arguments, lists all USB devices and their VM assignments." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
'';
|
||||
|
||||
# NOTE: getEffectiveInterfaces is intentionally duplicated in networking.nix and services.nix.
|
||||
# It cannot live in helpers.nix (which has no config access) and the three modules
|
||||
# don't share a common let-binding scope. Keep the copies in sync.
|
||||
|
|
@ -593,7 +939,10 @@ in
|
|||
vm-start-debug = vmStartDebugScript;
|
||||
vm-stop = vmStopScript;
|
||||
vm-shell = vmShellScript;
|
||||
vmsilo-usb = vmsiloUsbScript;
|
||||
};
|
||||
|
||||
usbHelperLib = usbHelperLib;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,8 +131,13 @@ in
|
|||
cleanupSocket = pkgs.writeShellScript "cleanup-socket-${vm.name}" ''
|
||||
rm -f /run/vmsilo/${vm.name}-crosvm-control.socket
|
||||
'';
|
||||
usbCleanup = pkgs.writeShellScript "usb-cleanup-${vm.name}" ''
|
||||
source ${cfg._internal.usbHelperLib}
|
||||
usb_cleanup_vm "${vm.name}"
|
||||
'';
|
||||
stopPostScripts = [
|
||||
"${cleanupSocket}"
|
||||
"${usbCleanup}"
|
||||
]
|
||||
++ lib.optionals (vm.rootOverlay.type == "raw") [ "${deleteEphemeral}" ];
|
||||
in
|
||||
|
|
@ -295,6 +300,81 @@ in
|
|||
};
|
||||
}
|
||||
) vms
|
||||
++ [
|
||||
# USB attach/detach oneshot template services (invoked by vmsilo-usb CLI)
|
||||
(lib.nameValuePair "vmsilo-usb-attach@" {
|
||||
description = "vmsilo USB attach: %I";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.writeShellScript "vmsilo-usb-do-attach" ''
|
||||
source ${cfg._internal.usbHelperLib}
|
||||
IFS=':' read -r vm_name devpath dev_file vid pid serial busnum devnum <<< "$1"
|
||||
usb_lock
|
||||
usb_do_attach "$vm_name" "$devpath" "$dev_file" "$vid" "$pid" "$serial" "$busnum" "$devnum"
|
||||
usb_unlock
|
||||
''} %I";
|
||||
};
|
||||
})
|
||||
(lib.nameValuePair "vmsilo-usb-detach@" {
|
||||
description = "vmsilo USB detach: %I";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.writeShellScript "vmsilo-usb-do-detach" ''
|
||||
source ${cfg._internal.usbHelperLib}
|
||||
IFS=':' read -r vm_name devpath <<< "$1"
|
||||
usb_lock
|
||||
usb_do_detach "$vm_name" "$devpath"
|
||||
usb_unlock
|
||||
''} %I";
|
||||
};
|
||||
})
|
||||
]
|
||||
++
|
||||
# Persistent USB attach services (one per VM with usbDevices)
|
||||
lib.concatMap (
|
||||
vm:
|
||||
lib.optional (vm.usbDevices != [ ]) (
|
||||
lib.nameValuePair "vmsilo-${vm.name}-usb-attach" {
|
||||
description = "USB device attach for VM ${vm.name}";
|
||||
requires = [ "vmsilo-${vm.name}-vm.service" ];
|
||||
after = [ "vmsilo-${vm.name}-vm.service" ];
|
||||
wantedBy = [ "vmsilo-${vm.name}-vm.service" ];
|
||||
bindsTo = [ "vmsilo-${vm.name}-vm.service" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart = pkgs.writeShellScript "vmsilo-usb-attach-${vm.name}" ''
|
||||
source ${cfg._internal.usbHelperLib}
|
||||
SOCKET="/run/vmsilo/${vm.name}-crosvm-control.socket"
|
||||
# Wait for control socket (up to 30s)
|
||||
ELAPSED=0
|
||||
while [ ! -S "''${SOCKET}" ] && [ "''${ELAPSED}" -lt 30 ]; do
|
||||
sleep 0.5
|
||||
ELAPSED=$((ELAPSED + 1))
|
||||
done
|
||||
if [ ! -S "''${SOCKET}" ]; then
|
||||
echo "Timeout waiting for control socket: ''${SOCKET}" >&2
|
||||
exit 1
|
||||
fi
|
||||
usb_lock
|
||||
${lib.concatMapStringsSep "\n" (dev: ''
|
||||
devices=$(usb_find_by_vidpid "${dev.vendorId}" "${dev.productId}" "${toString (dev.serial or "")}")
|
||||
count=$(echo "''${devices}" | ${pkgs.jq}/bin/jq 'length')
|
||||
if [ "''${count}" -eq 0 ]; then
|
||||
echo "Warning: USB device ${dev.vendorId}:${dev.productId} not found" >&2
|
||||
else
|
||||
echo "''${devices}" | ${pkgs.jq}/bin/jq -r '.[] | [.devpath, .dev_file, .vid, .pid, .serial, .busnum, .devnum] | @tsv' | \
|
||||
while IFS=$'\t' read -r devpath dev_file vid pid serial busnum devnum; do
|
||||
usb_do_attach "${vm.name}" "''${devpath}" "''${dev_file}" "''${vid}" "''${pid}" "''${serial}" "''${busnum}" "''${devnum}" || true
|
||||
done
|
||||
fi
|
||||
'') vm.usbDevices}
|
||||
usb_unlock
|
||||
'';
|
||||
};
|
||||
}
|
||||
)
|
||||
) vms
|
||||
);
|
||||
|
||||
# Session-bind user services: tie GPU VM lifecycle to the desktop session
|
||||
|
|
|
|||
|
|
@ -48,11 +48,11 @@
|
|||
services.dbus.enable = lib.mkDefault true;
|
||||
|
||||
# KDE desktop portal (file dialogs, screen sharing, etc.)
|
||||
xdg.portal = {
|
||||
enable = true;
|
||||
extraPortals = [ pkgs.kdePackages.xdg-desktop-portal-kde ];
|
||||
config.common.default = [ "kde" ];
|
||||
};
|
||||
#xdg.portal = {
|
||||
# enable = true;
|
||||
# extraPortals = [ pkgs.kdePackages.xdg-desktop-portal-kde ];
|
||||
# config.common.default = [ "kde" ];
|
||||
#};
|
||||
|
||||
# Enable SSH over vsock (used by `vm-shell --ssh`)
|
||||
systemd.sockets."sshd-vsock" = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue