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 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 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" readonly VERSION="0.1.0"
@ -72,7 +75,7 @@ main() {
start|stop|run|ssh|status|rebuild|reset) start|stop|run|ssh|status|rebuild|reset)
# Route to the appropriate qvm-* script # Route to the appropriate qvm-* script
# Use exec to replace this process with the subcommand # Use exec to replace this process with the subcommand
exec "qvm-${subcommand}" "$@" exec "${SCRIPT_DIR}/qvm-${subcommand}" "$@"
;; ;;
help|--help|-h) help|--help|-h)
show_help show_help

View file

@ -28,9 +28,17 @@ ensure_user_flake() {
log_info "User flake not found, copying default template..." log_info "User flake not found, copying default template..."
# Determine default flake location # 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/ # 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 if [[ ! -d "$default_flake_dir" ]]; then
die "Default flake template not found at: $default_flake_dir" die "Default flake template not found at: $default_flake_dir"

View file

@ -95,6 +95,34 @@ register_workspace() {
return 1 # Indicate new workspace added 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 # is_workspace_mounted - Check if workspace is actually mounted in VM
# Args: $1 - SSH port # Args: $1 - SSH port
@ -106,9 +134,11 @@ is_workspace_mounted() {
local guest_path="$2" local guest_path="$2"
# SSH into VM and check if guest path exists and is a directory # 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 UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \ -o LogLevel=ERROR \
-o PubkeyAuthentication=no \
-o PasswordAuthentication=yes \
-p "$ssh_port" \ -p "$ssh_port" \
root@localhost \ root@localhost \
"test -d '$guest_path'" 2>/dev/null; then "test -d '$guest_path'" 2>/dev/null; then
@ -169,14 +199,22 @@ main() {
local ssh_port local ssh_port
ssh_port=$(get_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 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 ""
echo "This workspace was just registered but is not mounted in the VM." echo "The workspace could not be mounted automatically."
echo "Workspaces must be mounted at VM start time." echo "This may indicate the VM was started before this workspace was registered."
echo "" 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 stop"
echo " qvm start" echo " qvm start"
echo "" echo ""
@ -185,14 +223,18 @@ main() {
fi fi
# Build SSH command # Build SSH command
# - Use sshpass for automated password auth (password: root)
# - Use -t if stdin is a TTY (for interactive commands) # - Use -t if stdin is a TTY (for interactive commands)
# - Suppress SSH warnings (ephemeral VM, host keys change) # - Suppress SSH warnings (ephemeral VM, host keys change)
# - cd to guest path and execute command # - cd to guest path and execute command
local ssh_cmd=( local ssh_cmd=(
sshpass -p root
ssh ssh
-o StrictHostKeyChecking=no -o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null -o UserKnownHostsFile=/dev/null
-o LogLevel=ERROR -o LogLevel=ERROR
-o PubkeyAuthentication=no
-o PasswordAuthentication=yes
-p "$ssh_port" -p "$ssh_port"
) )

View file

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

View file

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

View file

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

View file

@ -3,34 +3,48 @@
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-generators = { nixos-generators = {
url = "github:nix-community/nixos-generators"; url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
opencode.url = "github:anomalyco/opencode"; 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 = { outputs =
{
self, self,
nixpkgs, nixpkgs,
nixos-generators, nixos-generators,
home-manager,
opencode,
... ...
}: let }@inputs:
let
system = "x86_64-linux"; system = "x86_64-linux";
stateVersion = "24.11"; stateVersion = "26.05";
vmModule = { config, pkgs, lib, ... }: { vmModule =
{
config,
pkgs,
lib,
...
}:
{
imports = [ imports = [
home-manager.nixosModules.home-manager inputs.home-manager.nixosModules.home-manager
inputs.ros_neovim.nixosModules.default
inputs.common.nixosModules.essentials
inputs.common.nixosModules.git
inputs.common.nixosModules.zsh
inputs.common.nixosModules.tmux
]; ];
nixpkgs.config = { nixpkgs.config = {
@ -66,34 +80,16 @@
home.stateVersion = stateVersion; home.stateVersion = stateVersion;
programs.home-manager.enable = true; programs.home-manager.enable = true;
# Starship prompt that shows we're in qvm-dev sharedModules = [
programs.starship = { inputs.common.homeManagerModules.atuin
enable = true; inputs.common.homeManagerModules.git
settings = { inputs.common.homeManagerModules.postgres_cli_options
add_newline = false; inputs.common.homeManagerModules.starship
format = lib.concatStrings [ inputs.common.homeManagerModules.zoxide
"[](bold green)" inputs.common.homeManagerModules.zsh
"[$hostname](bold red)" inputs.common.homeManagerModules.tmux
"[$directory](bold blue)" inputs.common.homeManagerModules.direnv
"$git_branch"
"$git_status"
"\n"
"[>](bold green) "
]; ];
hostname = {
ssh_only = false;
format = "[@$hostname](bold red) ";
disabled = false;
};
};
};
programs.zsh = {
enable = true;
enableCompletion = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
};
}; };
}; };
@ -123,25 +119,49 @@
fileSystems."/cache/cargo" = { fileSystems."/cache/cargo" = {
device = "cargo_home"; device = "cargo_home";
fsType = "9p"; fsType = "9p";
options = [ "trans=virtio" "version=9p2000.L" "msize=104857600" "_netdev" "nofail" ]; options = [
"trans=virtio"
"version=9p2000.L"
"msize=104857600"
"_netdev"
"nofail"
];
}; };
fileSystems."/cache/target" = { fileSystems."/cache/target" = {
device = "cargo_target"; device = "cargo_target";
fsType = "9p"; fsType = "9p";
options = [ "trans=virtio" "version=9p2000.L" "msize=104857600" "_netdev" "nofail" ]; options = [
"trans=virtio"
"version=9p2000.L"
"msize=104857600"
"_netdev"
"nofail"
];
}; };
fileSystems."/cache/pnpm" = { fileSystems."/cache/pnpm" = {
device = "pnpm_store"; device = "pnpm_store";
fsType = "9p"; fsType = "9p";
options = [ "trans=virtio" "version=9p2000.L" "msize=104857600" "_netdev" "nofail" ]; options = [
"trans=virtio"
"version=9p2000.L"
"msize=104857600"
"_netdev"
"nofail"
];
}; };
fileSystems."/cache/sccache" = { fileSystems."/cache/sccache" = {
device = "sccache"; device = "sccache";
fsType = "9p"; fsType = "9p";
options = [ "trans=virtio" "version=9p2000.L" "msize=104857600" "_netdev" "nofail" ]; options = [
"trans=virtio"
"version=9p2000.L"
"msize=104857600"
"_netdev"
"nofail"
];
}; };
# Environment variables for cache directories # Environment variables for cache directories
@ -167,7 +187,7 @@
jq jq
ripgrep ripgrep
fd fd
opencode.packages.${system}.default inputs.opencode.packages.${system}.default
]; ];
# Opencode aliases without proxy interference # Opencode aliases without proxy interference
@ -195,16 +215,20 @@
system.stateVersion = stateVersion; system.stateVersion = stateVersion;
}; };
in { in
# Export the qcow2 image let
packages.${system}.qcow2 = nixos-generators.nixosGenerate { qcow2Image = nixos-generators.nixosGenerate {
inherit system; inherit system;
format = "qcow"; format = "qcow";
modules = [ vmModule ]; modules = [ vmModule ];
}; };
in
# Also export a default package {
packages.${system}.default = self.packages.${system}.qcow2; # Export the qcow2 image
packages.${system} = {
qcow2 = qcow2Image;
default = qcow2Image;
};
# Export the module for reuse # Export the module for reuse
nixosModules.default = vmModule; 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";
};
}
);
}