Add initial QVM CLI, Nix flake, scripts and README

This commit is contained in:
Joshua Bell 2026-01-26 00:16:18 -06:00
parent 25b1cca0e6
commit 8534f7efb9
14 changed files with 2359 additions and 0 deletions

224
bin/qvm-start Executable file
View file

@ -0,0 +1,224 @@
#!/usr/bin/env bash
#
# qvm-start - Launch the QEMU VM with all required configuration
#
# This script starts the QVM virtual machine with:
# - KVM acceleration and host CPU passthrough
# - Configurable memory and CPU count
# - Overlay disk backed by base.qcow2 (copy-on-write)
# - SSH port forwarding on auto-selected port
# - 9p mounts for shared caches (cargo, pnpm, sccache)
# - Serial console logging
# - Daemonized execution with PID file
#
set -euo pipefail
# Source common library
QVM_LIB_DIR="${QVM_LIB_DIR:-$(dirname "$(readlink -f "$0")")/../lib}"
# shellcheck source=lib/common.sh
source "$QVM_LIB_DIR/common.sh"
#
# find_available_port - Find an available TCP port starting from base
# Args: $1 - starting port number (default: 2222)
# Returns: available port number on stdout
#
find_available_port() {
local port="${1:-2222}"
local max_attempts=100
local attempt=0
while (( attempt < max_attempts )); do
if ! nc -z localhost "$port" 2>/dev/null; then
echo "$port"
return 0
fi
(( port++ ))
(( attempt++ ))
done
die "Could not find available port after $max_attempts attempts"
}
#
# mount_workspaces - Add virtfs entries for registered workspaces
# Args: $1 - name of array variable to append to
# Usage: mount_workspaces qemu_cmd
#
mount_workspaces() {
local -n cmd_array=$1
# Check if workspaces registry exists
if [[ ! -f "$QVM_WORKSPACES_FILE" ]]; then
log_info "No workspaces registry found, skipping workspace mounts"
return 0
fi
# Check if file is empty or invalid JSON
if [[ ! -s "$QVM_WORKSPACES_FILE" ]]; then
log_info "Workspaces registry is empty, skipping workspace mounts"
return 0
fi
# Parse workspaces and add virtfs entries
local workspace_count
workspace_count=$(jq -r 'length' "$QVM_WORKSPACES_FILE" 2>/dev/null || echo "0")
if [[ "$workspace_count" -eq 0 ]]; then
log_info "No workspaces registered, skipping workspace mounts"
return 0
fi
log_info "Mounting $workspace_count workspace(s)..."
# Iterate through workspaces and add virtfs entries
local i=0
while (( i < workspace_count )); do
local path mount_tag
path=$(jq -r ".[$i].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
log_warn "Skipping invalid workspace entry at index $i"
(( i++ ))
continue
fi
# Verify path exists
if [[ ! -d "$path" ]]; then
log_warn "Workspace path does not exist: $path (skipping)"
(( i++ ))
continue
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")
(( i++ ))
done
}
#
# cleanup_on_failure - Clean up state files if VM start fails
#
cleanup_on_failure() {
log_warn "Cleaning up after failed start..."
rm -f "$QVM_PID_FILE" "$QVM_SSH_PORT_FILE"
}
#
# main - Main execution flow
#
main() {
log_info "Starting QVM..."
# Check if VM is already running
if is_vm_running; then
log_info "VM is already running"
local port
port=$(get_ssh_port)
echo "SSH available on port: $port"
echo "Use 'qvm ssh' to connect or 'qvm status' for details"
exit 0
fi
# First-run initialization
ensure_dirs
if [[ ! -f "$QVM_BASE_IMAGE" ]]; then
log_info "First run detected - building base image..."
log_info "This may take several minutes."
# Call qvm-rebuild to build the image
SCRIPT_DIR="$(dirname "$0")"
if ! "$SCRIPT_DIR/qvm-rebuild"; then
die "Failed to build base image. Run 'qvm rebuild' manually to debug."
fi
fi
# Create overlay image if it doesn't exist
if [[ ! -f "$QVM_OVERLAY" ]]; then
log_info "Creating overlay disk..."
if ! qemu-img create -f qcow2 -b "$QVM_BASE_IMAGE" -F qcow2 "$QVM_OVERLAY"; then
die "Failed to create overlay disk"
fi
else
log_info "Using existing overlay disk"
fi
# Find available SSH port
local ssh_port
ssh_port=$(find_available_port 2222)
log_info "Using SSH port: $ssh_port"
# Get memory and CPU settings from environment or use defaults
local memory="${QVM_MEMORY:-8G}"
local cpus="${QVM_CPUS:-4}"
log_info "VM resources: ${memory} memory, ${cpus} CPUs"
# Build QEMU command
local qemu_cmd=(
qemu-system-x86_64
-enable-kvm
-cpu host
-m "$memory"
-smp "$cpus"
# Overlay disk (virtio for performance)
-drive "file=$QVM_OVERLAY,if=virtio,format=qcow2"
# User-mode networking with SSH port forward
-netdev "user,id=net0,hostfwd=tcp::${ssh_port}-:22"
-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"
)
# Add workspace mounts from registry
mount_workspaces qemu_cmd
# Continue building QEMU command
qemu_cmd+=(
# Serial console to log file
-serial "file:$QVM_SERIAL_LOG"
# No graphics
-nographic
# Daemonize with PID file
-daemonize
-pidfile "$QVM_PID_FILE"
)
# Launch QEMU
log_info "Launching QEMU..."
if ! "${qemu_cmd[@]}"; then
cleanup_on_failure
die "Failed to start QEMU"
fi
# Save SSH port to file
echo "$ssh_port" > "$QVM_SSH_PORT_FILE"
# Wait for SSH to become available
if ! wait_for_ssh "$ssh_port" 60; then
cleanup_on_failure
die "VM started but SSH did not become available"
fi
# Success!
log_info "VM started successfully"
echo ""
echo "SSH available on port: $ssh_port"
echo "Connect with: qvm ssh"
echo "Check status: qvm status"
echo "Serial log: $QVM_SERIAL_LOG"
}
# Run main function
main "$@"