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-stop <name>` — stop VM via systemd (polkit, no sudo)
|
||||||
- `vm-start-debug <name>` — start VM directly for debugging (requires 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`
|
- `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.
|
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) |
|
| `sound.capture` | bool | `false` | Enable sound capture (implies playback) |
|
||||||
| `sharedDirectories` | attrsOf submodule | `{}` | Shared directories via virtiofsd (keys are fs tags, see below) |
|
| `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) |
|
| `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 |
|
| `guestPrograms` | list of packages | `[]` | VM-specific packages |
|
||||||
| `guestConfig` | NixOS module(s) | `[]` | VM-specific NixOS configuration (module, list of modules, or path) |
|
| `guestConfig` | NixOS module(s) | `[]` | VM-specific NixOS configuration (module, list of modules, or path) |
|
||||||
| `vhostUser` | list of attrsets | `[]` | Manual vhost-user devices |
|
| `vhostUser` | list of attrsets | `[]` | Manual vhost-user devices |
|
||||||
|
|
@ -403,6 +404,37 @@ programs.vmsilo = {
|
||||||
boot.blacklistedKernelModules = [ "xhci_hcd" ]; # for USB controllers
|
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
|
### vhost-user Devices
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
|
|
@ -527,7 +559,7 @@ The host provides:
|
||||||
- Console PTY for serial access (`/run/vmsilo/<name>-console`)
|
- Console PTY for serial access (`/run/vmsilo/<name>-console`)
|
||||||
- VM services run as root for PCI passthrough and sandboxing (crosvm drops privileges)
|
- 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
|
- 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
|
- Desktop integration with .desktop files for guest applications
|
||||||
|
|
||||||
### Tray Integration
|
### Tray Integration
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,20 @@ in
|
||||||
) effectiveSharedDirs
|
) effectiveSharedDirs
|
||||||
)
|
)
|
||||||
) (lib.attrValues cfg.nixosVms)
|
) (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")
|
# netvm: network.netvm must reference a VM with isNetvm = true (skip for "host")
|
||||||
++ lib.concatMap (
|
++ lib.concatMap (
|
||||||
vm:
|
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 {
|
guestPrograms = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.package;
|
type = lib.types.listOf lib.types.package;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
|
|
@ -935,6 +972,12 @@ in
|
||||||
internal = true;
|
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 }
|
# Generated by netvm.nix: maps VM name -> { interfaces, guestConfig }
|
||||||
# Used to inject netvm-derived interfaces and guest config into VMs
|
# Used to inject netvm-derived interfaces and guest config into VMs
|
||||||
# without creating a self-referential cycle on nixosVms.
|
# without creating a self-referential cycle on nixosVms.
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ let
|
||||||
|
|
||||||
# Bash completions
|
# Bash completions
|
||||||
${lib.optionalString cfg.enableBashIntegration ''
|
${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
|
ln -s ${cfg._internal.bashCompletionScript} $out/share/bash-completion/completions/$cmd
|
||||||
done
|
done
|
||||||
''}
|
''}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,352 @@ let
|
||||||
|
|
||||||
vms = assignVmIds cfg.nixosVms;
|
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.
|
# 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
|
# 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.
|
# don't share a common let-binding scope. Keep the copies in sync.
|
||||||
|
|
@ -593,7 +939,10 @@ in
|
||||||
vm-start-debug = vmStartDebugScript;
|
vm-start-debug = vmStartDebugScript;
|
||||||
vm-stop = vmStopScript;
|
vm-stop = vmStopScript;
|
||||||
vm-shell = vmShellScript;
|
vm-shell = vmShellScript;
|
||||||
|
vmsilo-usb = vmsiloUsbScript;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
usbHelperLib = usbHelperLib;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -131,8 +131,13 @@ in
|
||||||
cleanupSocket = pkgs.writeShellScript "cleanup-socket-${vm.name}" ''
|
cleanupSocket = pkgs.writeShellScript "cleanup-socket-${vm.name}" ''
|
||||||
rm -f /run/vmsilo/${vm.name}-crosvm-control.socket
|
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 = [
|
stopPostScripts = [
|
||||||
"${cleanupSocket}"
|
"${cleanupSocket}"
|
||||||
|
"${usbCleanup}"
|
||||||
]
|
]
|
||||||
++ lib.optionals (vm.rootOverlay.type == "raw") [ "${deleteEphemeral}" ];
|
++ lib.optionals (vm.rootOverlay.type == "raw") [ "${deleteEphemeral}" ];
|
||||||
in
|
in
|
||||||
|
|
@ -295,6 +300,81 @@ in
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
) vms
|
) 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
|
# Session-bind user services: tie GPU VM lifecycle to the desktop session
|
||||||
|
|
|
||||||
|
|
@ -48,11 +48,11 @@
|
||||||
services.dbus.enable = lib.mkDefault true;
|
services.dbus.enable = lib.mkDefault true;
|
||||||
|
|
||||||
# KDE desktop portal (file dialogs, screen sharing, etc.)
|
# KDE desktop portal (file dialogs, screen sharing, etc.)
|
||||||
xdg.portal = {
|
#xdg.portal = {
|
||||||
enable = true;
|
# enable = true;
|
||||||
extraPortals = [ pkgs.kdePackages.xdg-desktop-portal-kde ];
|
# extraPortals = [ pkgs.kdePackages.xdg-desktop-portal-kde ];
|
||||||
config.common.default = [ "kde" ];
|
# config.common.default = [ "kde" ];
|
||||||
};
|
#};
|
||||||
|
|
||||||
# Enable SSH over vsock (used by `vm-shell --ssh`)
|
# Enable SSH over vsock (used by `vm-shell --ssh`)
|
||||||
systemd.sockets."sshd-vsock" = {
|
systemd.sockets."sshd-vsock" = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue