diff --git a/CLAUDE.md b/CLAUDE.md index 9edd133..6b05dad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -91,6 +91,7 @@ The configured user can manage VM services via polkit (no sudo required for `vm- - `vm-stop ` — stop VM via systemd (polkit, no sudo) - `vm-start-debug ` — start VM directly for debugging (requires sudo) - `vm-shell ` — connect to VM serial console (default) or SSH with `--ssh` +- `vmsilo-usb [attach|detach] [ ]` — list USB devices, attach/detach USB devices to VMs See README.md for full usage details and options. diff --git a/README.md b/README.md index 75b8b17..1ffe3c0 100644 --- a/README.md +++ b/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 # Attach a device (detaches from current VM if needed) +vmsilo-usb detach # 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/-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 diff --git a/modules/assertions.nix b/modules/assertions.nix index 9b2a5ef..8c5af8a 100644 --- a/modules/assertions.nix +++ b/modules/assertions.nix @@ -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: diff --git a/modules/options.nix b/modules/options.nix index c559bba..8e8a8b9 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -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. diff --git a/modules/package.nix b/modules/package.nix index eac5c25..418f439 100644 --- a/modules/package.nix +++ b/modules/package.nix @@ -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 ''} diff --git a/modules/scripts.nix b/modules/scripts.nix index aa16611..2d6aaac 100644 --- a/modules/scripts.nix +++ b/modules/scripts.nix @@ -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=$(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 " >&2 + exit 1 + fi + cmd_attach "$2" "$3" + ;; + detach) + if [ $# -ne 3 ]; then + echo "Usage: vmsilo-usb detach " >&2 + exit 1 + fi + cmd_detach "$2" "$3" + ;; + *) + echo "Usage: vmsilo-usb [attach | detach ]" >&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; }; }; } diff --git a/modules/services.nix b/modules/services.nix index 9b86985..43472d2 100644 --- a/modules/services.nix +++ b/modules/services.nix @@ -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 diff --git a/rootfs-nixos/guest/system.nix b/rootfs-nixos/guest/system.nix index faa3f9b..fa6934e 100644 --- a/rootfs-nixos/guest/system.nix +++ b/rootfs-nixos/guest/system.nix @@ -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" = {