feat: improve ephvm ux

This commit is contained in:
2026-04-13 11:48:18 +02:00
parent 2e5eb92e32
commit f7d86e7718
3 changed files with 99 additions and 12 deletions

View File

@@ -23,6 +23,7 @@
packages.ahab packages.ahab
pkgs.just pkgs.just
pkgs.presenterm pkgs.presenterm
pkgs.qemu
]; ];
}; };
}; };

View File

@@ -29,6 +29,39 @@
); );
}; };
# auto-login on serial console
services.getty.autologinUser = "matej";
# enable zsh in home-manager so starship init gets wired up
home-manager.users.matej.programs.zsh = {
enable = true;
dotDir = "/home/matej/.config/zsh";
shellAliases.dsp = "claude --dangerously-skip-permissions";
};
home-manager.users.matej.programs.starship = {
enable = true;
settings = {
add_newline = false;
format = "$username$hostname$directory$character";
hostname = {
ssh_only = false;
style = "bold blue";
format = "[@$hostname]($style)";
};
username = {
show_always = true;
style_user = "bold blue";
format = "[$user]($style)";
};
directory.format = " [$path]($style) ";
character = {
success_symbol = "[>](bold green)";
error_symbol = "[>](bold red)";
};
};
};
features.vm-guest.headless = true; features.vm-guest.headless = true;
features.vm-guest.automount = { features.vm-guest.automount = {
enable = true; enable = true;
@@ -39,6 +72,14 @@
# ensure .config exists with correct ownership before automount # ensure .config exists with correct ownership before automount
systemd.tmpfiles.rules = [ "d /home/matej/.config 0755 matej users -" ]; systemd.tmpfiles.rules = [ "d /home/matej/.config 0755 matej users -" ];
# TODO:(@janezicmatej) replace ssh with virtio-console (hvc0) when qemu 11.0 lands
# https://www.mail-archive.com/qemu-devel@nongnu.org/msg1162844.html
# accept any ssh key (ephemeral localhost-only vm)
services.openssh.settings.AuthorizedKeysCommand = let
acceptKey = pkgs.writeShellScript "ephvm-accept-key" ''echo "$1 $2"'';
in "${acceptKey} %t %k";
services.openssh.settings.AuthorizedKeysCommandUser = "nobody";
# writable claude config via 9p # writable claude config via 9p
fileSystems."/home/matej/.config/claude" = { fileSystems."/home/matej/.config/claude" = {
device = "claude"; device = "claude";

View File

@@ -27,7 +27,9 @@ info() {
# globals for cleanup trap # globals for cleanup trap
CLEANUP_OVERLAY="" CLEANUP_OVERLAY=""
QEMU_PID=""
cleanup() { cleanup() {
[ -n "$QEMU_PID" ] && kill "$QEMU_PID" 2>/dev/null && wait "$QEMU_PID" 2>/dev/null
[ -n "$CLEANUP_OVERLAY" ] && rm -rf "$CLEANUP_OVERLAY" [ -n "$CLEANUP_OVERLAY" ] && rm -rf "$CLEANUP_OVERLAY"
return 0 return 0
} }
@@ -39,11 +41,12 @@ Usage: ephvm-run.sh [options]
Options: Options:
--mount <path> Mount host directory into VM (repeatable) --mount <path> Mount host directory into VM (repeatable)
--claude Mount claude config dir (requires CLAUDE_CONFIG_DIR) --no-claude Skip mounting claude config dir
--disk-size <size> Resize guest disk (e.g. 50G) --disk-size <size> Resize guest disk (e.g. 50G)
--memory <size> VM memory (default: 8G) --memory <size> VM memory (default: 4G)
--cpus <n> VM CPUs (default: 4) --cpus <n> VM CPUs (default: 2)
--ssh-port <port> SSH port forward (default: 2222) --ssh-port <port> Use specific SSH port (default: auto)
--serial Attach to serial console instead of SSH
-h, --help Show usage -h, --help Show usage
EOF EOF
exit "${1:-0}" exit "${1:-0}"
@@ -52,7 +55,7 @@ EOF
main() { main() {
setup_colors setup_colors
local ssh_port=2222 memory=8G cpus=4 claude=false disk_size="" local ssh_port="" memory=4G cpus=2 claude=true disk_size="" serial=false
local -a mounts=() local -a mounts=()
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
@@ -61,8 +64,8 @@ main() {
mounts+=("$2") mounts+=("$2")
shift 2 shift 2
;; ;;
--claude) --no-claude)
claude=true claude=false
shift shift
;; ;;
--disk-size) --disk-size)
@@ -81,6 +84,10 @@ main() {
ssh_port="$2" ssh_port="$2"
shift 2 shift 2
;; ;;
--serial)
serial=true
shift
;;
-h | --help) usage ;; -h | --help) usage ;;
*) *)
echo "${red}error:${reset} unknown option: $1" >&2 echo "${red}error:${reset} unknown option: $1" >&2
@@ -91,7 +98,9 @@ main() {
info "building ephvm image..." info "building ephvm image..."
local image_dir image local image_dir image
image_dir=$(nix build --no-link --print-out-paths .#nixosConfigurations.ephvm.config.system.build.images.qemu) [ -n "${EPHVM_FLAKE:-}" ] || die "EPHVM_FLAKE must be set to the flake directory"
local flake="$EPHVM_FLAKE"
image_dir=$(nix build --no-link --print-out-paths "${flake}#nixosConfigurations.ephvm.config.system.build.images.qemu")
image=$(find "$image_dir" -name '*.qcow2' -print -quit) image=$(find "$image_dir" -name '*.qcow2' -print -quit)
[ -n "$image" ] || die "no qcow2 image found in $image_dir" [ -n "$image" ] || die "no qcow2 image found in $image_dir"
@@ -106,16 +115,31 @@ main() {
drive_arg="file=$image,format=qcow2,snapshot=on" drive_arg="file=$image,format=qcow2,snapshot=on"
fi fi
command -v qemu-system-x86_64 &>/dev/null || die "qemu-system-x86_64 not found"
local accel="tcg" local accel="tcg"
[ -r /dev/kvm ] && accel="kvm" [ -r /dev/kvm ] && accel="kvm"
# auto-allocate ssh port unless serial mode
if [ "$serial" = false ] && [ -z "$ssh_port" ]; then
ssh_port=10022
while ss -tln | grep -q ":${ssh_port}\b"; do
ssh_port=$((ssh_port + 1))
done
fi
local nic_arg="user"
if [ -n "$ssh_port" ]; then
nic_arg="user,hostfwd=tcp::${ssh_port}-:22"
fi
local -a qemu_args=( local -a qemu_args=(
qemu-system-x86_64 qemu-system-x86_64
-accel "$accel" -accel "$accel"
-m "$memory" -m "$memory"
-smp "$cpus" -smp "$cpus"
-drive "$drive_arg" -drive "$drive_arg"
-nic "user,hostfwd=tcp::${ssh_port}-:22" -nic "$nic_arg"
-nographic -nographic
) )
@@ -135,7 +159,7 @@ main() {
done done
if [ "$claude" = true ]; then if [ "$claude" = true ]; then
[ -n "${CLAUDE_CONFIG_DIR:-}" ] || die "--claude requires CLAUDE_CONFIG_DIR to be set" [ -n "${CLAUDE_CONFIG_DIR:-}" ] || die "CLAUDE_CONFIG_DIR must be set (use --no-claude to skip)"
mkdir -p "$CLAUDE_CONFIG_DIR" mkdir -p "$CLAUDE_CONFIG_DIR"
local claude_dir local claude_dir
claude_dir=$(realpath "$CLAUDE_CONFIG_DIR") claude_dir=$(realpath "$CLAUDE_CONFIG_DIR")
@@ -147,10 +171,31 @@ main() {
fi fi
info "---" info "---"
info "Accel: $accel | SSH: ssh -p $ssh_port matej@localhost" info "Accel: $accel"
info "---" info "---"
exec "${qemu_args[@]}" if [ "$serial" = true ]; then
exec "${qemu_args[@]}"
fi
# start qemu in background and auto-ssh
"${qemu_args[@]}" &>/dev/null &
QEMU_PID=$!
info "waiting for vm (port $ssh_port)..."
local attempts=0
while ! (echo > /dev/tcp/localhost/"$ssh_port") 2>/dev/null; do
attempts=$((attempts + 1))
[ $attempts -gt 60 ] && die "vm did not become ready in 60s"
kill -0 "$QEMU_PID" 2>/dev/null || die "qemu exited unexpectedly"
sleep 1
done
ssh -p "$ssh_port" -t \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \
matej@localhost
} }
main "$@" main "$@"