feat: improve ephvm ux
This commit is contained in:
@@ -23,6 +23,7 @@
|
|||||||
packages.ahab
|
packages.ahab
|
||||||
pkgs.just
|
pkgs.just
|
||||||
pkgs.presenterm
|
pkgs.presenterm
|
||||||
|
pkgs.qemu
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 "---"
|
||||||
|
|
||||||
|
if [ "$serial" = true ]; then
|
||||||
exec "${qemu_args[@]}"
|
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 "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user