Use SCRIPT_DIR, add sshpass and auto-mount workspaces, update flakes

This commit is contained in:
Joshua Bell 2026-01-26 08:41:33 -06:00
parent 8534f7efb9
commit 9aa72fade7
8 changed files with 292 additions and 413 deletions

View file

@ -8,8 +8,11 @@
set -euo pipefail
# Determine script directory for locating sibling scripts
readonly SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Source common library (use QVM_LIB_DIR from wrapper or relative path for dev)
source "${QVM_LIB_DIR:-$(dirname "$0")/../lib}/common.sh"
source "${QVM_LIB_DIR:-${SCRIPT_DIR}/../lib}/common.sh"
readonly VERSION="0.1.0"
@ -72,7 +75,7 @@ main() {
start|stop|run|ssh|status|rebuild|reset)
# Route to the appropriate qvm-* script
# Use exec to replace this process with the subcommand
exec "qvm-${subcommand}" "$@"
exec "${SCRIPT_DIR}/qvm-${subcommand}" "$@"
;;
help|--help|-h)
show_help

View file

@ -28,9 +28,17 @@ ensure_user_flake() {
log_info "User flake not found, copying default template..."
# Determine default flake location
# In installed version: $QVM_LIB_DIR/../flake/default-vm/
# In installed version: $QVM_LIB_DIR/../share/qvm/default-vm/
# In development: $(dirname "$0")/../flake/default-vm/
local default_flake_dir="$QVM_LIB_DIR/../flake/default-vm"
local default_flake_dir
# Try installed location first
if [[ -d "$QVM_LIB_DIR/../share/qvm/default-vm" ]]; then
default_flake_dir="$QVM_LIB_DIR/../share/qvm/default-vm"
else
# Fall back to development location
default_flake_dir="$(dirname "$(readlink -f "$0")")/../flake/default-vm"
fi
if [[ ! -d "$default_flake_dir" ]]; then
die "Default flake template not found at: $default_flake_dir"

View file

@ -95,6 +95,34 @@ register_workspace() {
return 1 # Indicate new workspace added
}
#
# ensure_workspace_mounted - Mount workspace in VM if not already mounted
# Args: $1 - SSH port
# $2 - mount tag (e.g., ws_abc123)
# $3 - guest path (e.g., /workspace/abc123)
# Returns: 0 on success
#
ensure_workspace_mounted() {
local ssh_port="$1"
local mount_tag="$2"
local guest_path="$3"
# SSH into VM and mount the workspace
# - mkdir creates the mount point if missing
# - mount attempts to mount the 9p virtfs
# - || true ensures we don't fail if already mounted
sshpass -p root ssh -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \
-o PubkeyAuthentication=no \
-o PasswordAuthentication=yes \
-p "$ssh_port" \
root@localhost \
"mkdir -p '$guest_path' && mount -t 9p -o trans=virtio,version=9p2000.L,msize=104857600 '$mount_tag' '$guest_path' 2>/dev/null || true" >/dev/null 2>&1
return 0
}
#
# is_workspace_mounted - Check if workspace is actually mounted in VM
# Args: $1 - SSH port
@ -106,9 +134,11 @@ is_workspace_mounted() {
local guest_path="$2"
# SSH into VM and check if guest path exists and is a directory
if ssh -o StrictHostKeyChecking=no \
if sshpass -p root ssh -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \
-o PubkeyAuthentication=no \
-o PasswordAuthentication=yes \
-p "$ssh_port" \
root@localhost \
"test -d '$guest_path'" 2>/dev/null; then
@ -169,14 +199,22 @@ main() {
local ssh_port
ssh_port=$(get_ssh_port)
# Check if workspace is actually mounted in VM
# Get mount tag from workspaces.json
local mount_tag
mount_tag=$(jq -r --arg path "$workspace_path" '.[] | select(.host_path == $path) | .mount_tag' "$QVM_WORKSPACES_FILE")
# Ensure workspace is mounted (auto-mount if not)
log_info "Ensuring workspace is mounted..."
ensure_workspace_mounted "$ssh_port" "$mount_tag" "$guest_path"
# Verify workspace is actually mounted
if ! is_workspace_mounted "$ssh_port" "$guest_path"; then
log_error "Workspace not mounted in VM"
log_error "Failed to mount workspace in VM"
echo ""
echo "This workspace was just registered but is not mounted in the VM."
echo "Workspaces must be mounted at VM start time."
echo "The workspace could not be mounted automatically."
echo "This may indicate the VM was started before this workspace was registered."
echo ""
echo "Please restart the VM to mount this workspace:"
echo "Please restart the VM to properly configure the workspace:"
echo " qvm stop"
echo " qvm start"
echo ""
@ -185,14 +223,18 @@ main() {
fi
# Build SSH command
# - Use sshpass for automated password auth (password: root)
# - Use -t if stdin is a TTY (for interactive commands)
# - Suppress SSH warnings (ephemeral VM, host keys change)
# - cd to guest path and execute command
local ssh_cmd=(
sshpass -p root
ssh
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o LogLevel=ERROR
-o PubkeyAuthentication=no
-o PasswordAuthentication=yes
-p "$ssh_port"
)

View file

@ -85,12 +85,15 @@ main() {
local port
port=$(get_ssh_port)
# Build SSH command
# Build SSH command with sshpass for automated password auth
local ssh_cmd=(
sshpass -p root
ssh
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o LogLevel=ERROR # Suppress host key warnings
-o PubkeyAuthentication=no # Use password auth (password: root)
-o PasswordAuthentication=yes
-p "$port"
root@localhost
)

View file

@ -76,7 +76,7 @@ mount_workspaces() {
local i=0
while (( i < workspace_count )); do
local path mount_tag
path=$(jq -r ".[$i].path" "$QVM_WORKSPACES_FILE")
path=$(jq -r ".[$i].host_path" "$QVM_WORKSPACES_FILE")
mount_tag=$(jq -r ".[$i].mount_tag" "$QVM_WORKSPACES_FILE")
if [[ -z "$path" || -z "$mount_tag" || "$path" == "null" || "$mount_tag" == "null" ]]; then
@ -93,9 +93,9 @@ mount_workspaces() {
fi
log_info " - $path -> $mount_tag"
cmd_array+=(-virtfs "local,path=$path,mount_tag=$mount_tag,security_model=mapped-xattr,trans=virtio,version=9p2000.L,msize=104857600")
cmd_array+=(-virtfs "local,path=$path,mount_tag=$mount_tag,security_model=mapped-xattr")
(( i++ ))
(( i++ )) || true # Prevent set -e from exiting when i was 0
done
}
@ -173,10 +173,11 @@ main() {
-device "virtio-net-pci,netdev=net0"
# 9p mounts for shared caches (security_model=mapped-xattr for proper permissions)
-virtfs "local,path=$QVM_CARGO_HOME,mount_tag=cargo_home,security_model=mapped-xattr,trans=virtio,version=9p2000.L,msize=104857600"
-virtfs "local,path=$QVM_CARGO_TARGET,mount_tag=cargo_target,security_model=mapped-xattr,trans=virtio,version=9p2000.L,msize=104857600"
-virtfs "local,path=$QVM_PNPM_STORE,mount_tag=pnpm_store,security_model=mapped-xattr,trans=virtio,version=9p2000.L,msize=104857600"
-virtfs "local,path=$QVM_SCCACHE,mount_tag=sccache,security_model=mapped-xattr,trans=virtio,version=9p2000.L,msize=104857600"
# Note: trans, version, msize are kernel-side mount options (in NixOS flake), not QEMU options
-virtfs "local,path=$QVM_CARGO_HOME,mount_tag=cargo_home,security_model=mapped-xattr"
-virtfs "local,path=$QVM_CARGO_TARGET,mount_tag=cargo_target,security_model=mapped-xattr"
-virtfs "local,path=$QVM_PNPM_STORE,mount_tag=pnpm_store,security_model=mapped-xattr"
-virtfs "local,path=$QVM_SCCACHE,mount_tag=sccache,security_model=mapped-xattr"
)
# Add workspace mounts from registry
@ -187,8 +188,8 @@ main() {
# Serial console to log file
-serial "file:$QVM_SERIAL_LOG"
# No graphics
-nographic
# No graphics (use -display none for daemonized mode)
-display none
# Daemonize with PID file
-daemonize

View file

@ -32,10 +32,16 @@
# Create output directories
mkdir -p $out/bin
mkdir -p $out/lib/qvm
mkdir -p $out/share/qvm
# Install library files
install -Dm755 lib/common.sh $out/lib/qvm/common.sh
# Install default VM flake template
if [ -d "flake/default-vm" ]; then
cp -r flake/default-vm $out/share/qvm/default-vm
fi
# Install all scripts from bin/
for script in bin/*; do
if [ -f "$script" ]; then
@ -57,6 +63,7 @@
pkgs.netcat-gnu
pkgs.bc
pkgs.procps
pkgs.sshpass
]} \
--set QVM_LIB_DIR "$out/lib/qvm"
done
@ -104,6 +111,7 @@
netcat-gnu
bc
procps
sshpass
# Development tools
shellcheck

View file

@ -3,210 +3,234 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixos-generators = {
url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-generators = {
url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs";
};
opencode.url = "github:anomalyco/opencode";
common.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/common";
ros_neovim.url = "git+https://git.joshuabell.xyz/ringofstorms/nvim";
};
outputs = {
self,
nixpkgs,
nixos-generators,
home-manager,
opencode,
...
}: let
system = "x86_64-linux";
stateVersion = "24.11";
outputs =
{
self,
nixpkgs,
nixos-generators,
...
}@inputs:
let
system = "x86_64-linux";
stateVersion = "26.05";
vmModule = { config, pkgs, lib, ... }: {
imports = [
home-manager.nixosModules.home-manager
];
vmModule =
{
config,
pkgs,
lib,
...
}:
{
imports = [
inputs.home-manager.nixosModules.home-manager
nixpkgs.config = {
allowUnfree = true;
allowUnfreePredicate = (_: true);
};
inputs.ros_neovim.nixosModules.default
inputs.common.nixosModules.essentials
inputs.common.nixosModules.git
inputs.common.nixosModules.zsh
inputs.common.nixosModules.tmux
];
# Distinctive hostname for easy identification
networking.hostName = "qvm-dev";
nixpkgs.config = {
allowUnfree = true;
allowUnfreePredicate = (_: true);
};
# SSH enabled with password auth for root
services.openssh = {
enable = true;
settings.PasswordAuthentication = true;
settings.PermitRootLogin = "yes";
};
# Distinctive hostname for easy identification
networking.hostName = "qvm-dev";
# Root user with password and zsh
users.users.root = {
password = "root";
shell = pkgs.zsh;
};
programs.zsh.enable = true;
# Home manager configuration for nice shell
home-manager = {
useUserPackages = true;
useGlobalPkgs = true;
backupFileExtension = "bak";
users.root = {
home.stateVersion = stateVersion;
programs.home-manager.enable = true;
# Starship prompt that shows we're in qvm-dev
programs.starship = {
# SSH enabled with password auth for root
services.openssh = {
enable = true;
settings = {
add_newline = false;
format = lib.concatStrings [
"[](bold green)"
"[$hostname](bold red)"
"[$directory](bold blue)"
"$git_branch"
"$git_status"
"\n"
"[>](bold green) "
settings.PasswordAuthentication = true;
settings.PermitRootLogin = "yes";
};
# Root user with password and zsh
users.users.root = {
password = "root";
shell = pkgs.zsh;
};
programs.zsh.enable = true;
# Home manager configuration for nice shell
home-manager = {
useUserPackages = true;
useGlobalPkgs = true;
backupFileExtension = "bak";
users.root = {
home.stateVersion = stateVersion;
programs.home-manager.enable = true;
sharedModules = [
inputs.common.homeManagerModules.atuin
inputs.common.homeManagerModules.git
inputs.common.homeManagerModules.postgres_cli_options
inputs.common.homeManagerModules.starship
inputs.common.homeManagerModules.zoxide
inputs.common.homeManagerModules.zsh
inputs.common.homeManagerModules.tmux
inputs.common.homeManagerModules.direnv
];
hostname = {
ssh_only = false;
format = "[@$hostname](bold red) ";
disabled = false;
};
};
};
programs.zsh = {
enable = true;
enableCompletion = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
# Avoid slow boots due to wait-online
systemd.network.wait-online.enable = false;
systemd.services.NetworkManager-wait-online.enable = lib.mkForce false;
systemd.services.systemd-networkd-wait-online.enable = lib.mkForce false;
networking.firewall.allowedTCPPorts = [ 22 ];
# Enable flakes
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
# Josh's timezone
time.timeZone = "America/Chicago";
# Git safe.directory for 9p ownership issues
environment.etc."gitconfig".text = ''
[safe]
directory = *
'';
# 9p mount points for caches (must match qvm-start mount tags)
fileSystems."/cache/cargo" = {
device = "cargo_home";
fsType = "9p";
options = [
"trans=virtio"
"version=9p2000.L"
"msize=104857600"
"_netdev"
"nofail"
];
};
fileSystems."/cache/target" = {
device = "cargo_target";
fsType = "9p";
options = [
"trans=virtio"
"version=9p2000.L"
"msize=104857600"
"_netdev"
"nofail"
];
};
fileSystems."/cache/pnpm" = {
device = "pnpm_store";
fsType = "9p";
options = [
"trans=virtio"
"version=9p2000.L"
"msize=104857600"
"_netdev"
"nofail"
];
};
fileSystems."/cache/sccache" = {
device = "sccache";
fsType = "9p";
options = [
"trans=virtio"
"version=9p2000.L"
"msize=104857600"
"_netdev"
"nofail"
];
};
# Environment variables for cache directories
environment.variables = {
CARGO_HOME = "/cache/cargo";
CARGO_TARGET_DIR = "/cache/target";
PNPM_HOME = "/cache/pnpm";
SCCACHE_DIR = "/cache/sccache";
};
# Ensure workspace directory exists
systemd.tmpfiles.rules = [
"d /workspace 0755 root root -"
];
# Essential packages for development
environment.systemPackages = with pkgs; [
git
vim
tmux
htop
curl
jq
ripgrep
fd
inputs.opencode.packages.${system}.default
];
# Opencode aliases without proxy interference
environment.shellAliases = {
"oc" = "all_proxy='' http_proxy='' https_proxy='' opencode";
"occ" = "oc -c";
};
# MOTD to clearly show this is qvm-dev
users.motd = ''
QVM Development VM
Hostname: qvm-dev
Caches: /cache/{cargo,target,...}
Workspace: /workspace
'';
# 20GB disk size
virtualisation.diskSize = 20 * 1024;
system.stateVersion = stateVersion;
};
in
let
qcow2Image = nixos-generators.nixosGenerate {
inherit system;
format = "qcow";
modules = [ vmModule ];
};
in
{
# Export the qcow2 image
packages.${system} = {
qcow2 = qcow2Image;
default = qcow2Image;
};
# Avoid slow boots due to wait-online
systemd.network.wait-online.enable = false;
systemd.services.NetworkManager-wait-online.enable = lib.mkForce false;
systemd.services.systemd-networkd-wait-online.enable = lib.mkForce false;
networking.firewall.allowedTCPPorts = [ 22 ];
# Enable flakes
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
# Josh's timezone
time.timeZone = "America/Chicago";
# Git safe.directory for 9p ownership issues
environment.etc."gitconfig".text = ''
[safe]
directory = *
'';
# 9p mount points for caches (must match qvm-start mount tags)
fileSystems."/cache/cargo" = {
device = "cargo_home";
fsType = "9p";
options = [ "trans=virtio" "version=9p2000.L" "msize=104857600" "_netdev" "nofail" ];
};
fileSystems."/cache/target" = {
device = "cargo_target";
fsType = "9p";
options = [ "trans=virtio" "version=9p2000.L" "msize=104857600" "_netdev" "nofail" ];
};
fileSystems."/cache/pnpm" = {
device = "pnpm_store";
fsType = "9p";
options = [ "trans=virtio" "version=9p2000.L" "msize=104857600" "_netdev" "nofail" ];
};
fileSystems."/cache/sccache" = {
device = "sccache";
fsType = "9p";
options = [ "trans=virtio" "version=9p2000.L" "msize=104857600" "_netdev" "nofail" ];
};
# Environment variables for cache directories
environment.variables = {
CARGO_HOME = "/cache/cargo";
CARGO_TARGET_DIR = "/cache/target";
PNPM_HOME = "/cache/pnpm";
SCCACHE_DIR = "/cache/sccache";
};
# Ensure workspace directory exists
systemd.tmpfiles.rules = [
"d /workspace 0755 root root -"
];
# Essential packages for development
environment.systemPackages = with pkgs; [
git
vim
tmux
htop
curl
jq
ripgrep
fd
opencode.packages.${system}.default
];
# Opencode aliases without proxy interference
environment.shellAliases = {
"oc" = "all_proxy='' http_proxy='' https_proxy='' opencode";
"occ" = "oc -c";
};
# MOTD to clearly show this is qvm-dev
users.motd = ''
QVM Development VM
Hostname: qvm-dev
Caches: /cache/{cargo,target,...}
Workspace: /workspace
'';
# 20GB disk size
virtualisation.diskSize = 20 * 1024;
system.stateVersion = stateVersion;
# Export the module for reuse
nixosModules.default = vmModule;
};
in {
# Export the qcow2 image
packages.${system}.qcow2 = nixos-generators.nixosGenerate {
inherit system;
format = "qcow";
modules = [ vmModule ];
};
# Also export a default package
packages.${system}.default = self.packages.${system}.qcow2;
# Export the module for reuse
nixosModules.default = vmModule;
};
}

View file

@ -1,210 +0,0 @@
{
description = "Qai base NixOS VM image";
inputs = {
home-manager = {
url = "github:rycee/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils = {
url = "github:numtide/flake-utils";
};
nixos-generators = {
url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs";
};
common.url = "git+https://git.joshuabell.xyz/ringofstorms/dotfiles?dir=flakes/common";
opencode.url = "github:anomalyco/opencode?ref=ad4bdd9f0fb7670949b5c47917bb656247ac60ac";
ros_neovim.url = "git+https://git.joshuabell.xyz/ringofstorms/nvim";
};
outputs =
inputs@{
self,
nixpkgs,
flake-utils,
nixos-generators,
...
}:
let
baseModule =
{
config,
pkgs,
lib,
...
}:
let
stateVersion = "26.05";
in
{
imports = [
inputs."home-manager".nixosModules.default
inputs.ros_neovim.nixosModules.default
inputs.common.nixosModules.essentials
inputs.common.nixosModules.git
inputs.common.nixosModules.zsh
inputs.common.nixosModules.tmux
(
{
...
}:
{
home-manager = {
useUserPackages = true;
useGlobalPkgs = true;
backupFileExtension = "bak";
users.root = {
home.stateVersion = stateVersion;
programs.home-manager.enable = true;
};
sharedModules = [
inputs.common.homeManagerModules.atuin
inputs.common.homeManagerModules.git
inputs.common.homeManagerModules.postgres_cli_options
inputs.common.homeManagerModules.starship
inputs.common.homeManagerModules.zoxide
inputs.common.homeManagerModules.zsh
inputs.common.homeManagerModules.tmux
inputs.common.homeManagerModules.direnv
];
};
}
)
];
nixpkgs.config = {
allowUnfree = true;
allowUnfreePredicate = (_: true);
};
networking.hostName = "qai-base";
# SSH enabled for terminal access via WebSocket proxy.
services.openssh = {
enable = true;
settings.PasswordAuthentication = true;
settings.PermitRootLogin = "yes";
};
users.users.root.password = "root";
# Avoid slow boots due to wait-online.
systemd.network.wait-online.enable = false;
systemd.services.NetworkManager-wait-online.enable = false;
systemd.services.systemd-networkd-wait-online.enable = false;
networking.firewall.allowedTCPPorts = [
22
];
# Needed so `nix develop` works inside the VM.
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
# Host binary cache (QEMU user-net host is reachable at 10.0.2.2).
# Only effective at runtime, not during image build.
networking.hosts."10.0.2.2" = [ "lio" ];
# Note: These substituters are for runtime use. The build VM can't reach them.
nix.settings.substituters = lib.mkAfter [ "http://lio:5000" ];
nix.settings.trusted-public-keys = lib.mkAfter [
"lio:9jKQ2xJyZjD0AWFzMcLe5dg3s8vOJ3uffujbUkBg4ms="
];
# Fallback timeout so nix doesn't hang if lio is unreachable
nix.settings.connect-timeout = 5;
time.timeZone = "America/Chicago";
# Git 2.35+ blocks repos owned by different uid; 9p shares can trip this.
# Use wildcard to allow all subdirectories under /workspace (task-1, task-2, etc.)
environment.etc."gitconfig".text = ''
[safe]
directory = *
'';
programs.zsh.enable = true;
users.users.root.shell = pkgs.zsh;
environment.systemPackages = with pkgs; [
zsh
git
htop
vim
inputs.opencode.packages.${pkgs.system}.default
];
environment.shellAliases = {
"oc" = "all_proxy='' http_proxy='' https_proxy='' opencode";
"occ" = "oc -c";
};
# Default disk is too small for `nix develop` / direnv.
virtualisation.diskSize = 20 * 1024;
virtualisation.vmVariant = {
virtualisation = {
memorySize = 4096;
cores = 2;
graphics = false;
};
virtualisation.forwardPorts = [
{
from = "host";
host.port = 2221;
guest.port = 22;
}
];
};
system.stateVersion = stateVersion;
};
in
{
nixosModules.default = baseModule;
}
// flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
baseVm = nixpkgs.lib.nixosSystem {
inherit system;
modules = [ baseModule ];
};
in
{
nixosConfigurations.base = baseVm;
# Runnable VM (./result/bin/run-nixos-vm)
packages.vm = baseVm.config.system.build.vm;
# Bootable qcow2 disk image (./result/nixos.qcow2)
packages.qcow2 = nixos-generators.nixosGenerate {
inherit system;
format = "qcow";
modules = [ baseModule ];
};
apps.default = {
type = "app";
program = "${baseVm.config.system.build.vm}/bin/run-nixos-vm";
};
devShells.default = pkgs.mkShellNoCC {
QEMU_NET_OPTS = "hostfwd=tcp::2221-:22";
};
}
);
}