wip
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,3 +5,6 @@ result-*
|
||||
# Ignore automatically generated direnv output
|
||||
.direnv
|
||||
|
||||
# Ignore generated seed ISOs
|
||||
*.iso
|
||||
|
||||
|
||||
69
.gitlab-ci.yml
Normal file
69
.gitlab-ci.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
stages:
|
||||
- build
|
||||
- upload
|
||||
- release
|
||||
|
||||
build-x86_64:
|
||||
stage: build
|
||||
image: ubuntu:24.04
|
||||
script:
|
||||
- apt-get update && apt-get install -y curl xz-utils sudo
|
||||
- curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux --no-confirm
|
||||
- . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||
- nix build .#nixosConfigurations.sandbox.config.system.build.image --out-link result-x86_64
|
||||
- cp $(find -L result-x86_64 -name '*.qcow2') sandbox-x86_64.qcow2
|
||||
artifacts:
|
||||
paths:
|
||||
- sandbox-x86_64.qcow2
|
||||
expire_in: 1 week
|
||||
|
||||
build-aarch64:
|
||||
stage: build
|
||||
image: ubuntu:24.04
|
||||
tags:
|
||||
- aarch64
|
||||
allow_failure: true
|
||||
script:
|
||||
- apt-get update && apt-get install -y curl xz-utils sudo
|
||||
- curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install linux --no-confirm
|
||||
- . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
|
||||
- nix build .#nixosConfigurations.sandbox-aarch64.config.system.build.image --out-link result-aarch64
|
||||
- cp $(find -L result-aarch64 -name '*.qcow2') sandbox-aarch64.qcow2
|
||||
artifacts:
|
||||
paths:
|
||||
- sandbox-aarch64.qcow2
|
||||
expire_in: 1 week
|
||||
|
||||
upload:
|
||||
stage: upload
|
||||
image: curlimages/curl:latest
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- |
|
||||
curl --header "JOB-TOKEN: $CI_JOB_TOKEN" \
|
||||
--upload-file sandbox-x86_64.qcow2 \
|
||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/sandbox/${CI_COMMIT_TAG}/sandbox-x86_64.qcow2"
|
||||
- |
|
||||
if [ -f sandbox-aarch64.qcow2 ]; then
|
||||
curl --header "JOB-TOKEN: $CI_JOB_TOKEN" \
|
||||
--upload-file sandbox-aarch64.qcow2 \
|
||||
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/sandbox/${CI_COMMIT_TAG}/sandbox-aarch64.qcow2"
|
||||
fi
|
||||
|
||||
release:
|
||||
stage: release
|
||||
image: registry.gitlab.com/gitlab-org/release-cli:latest
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
script:
|
||||
- echo "Creating release $CI_COMMIT_TAG"
|
||||
release:
|
||||
tag_name: $CI_COMMIT_TAG
|
||||
description: "Sandbox VM $CI_COMMIT_TAG"
|
||||
assets:
|
||||
links:
|
||||
- name: sandbox-x86_64.qcow2
|
||||
url: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/sandbox/${CI_COMMIT_TAG}/sandbox-x86_64.qcow2"
|
||||
- name: sandbox-aarch64.qcow2
|
||||
url: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/sandbox/${CI_COMMIT_TAG}/sandbox-aarch64.qcow2"
|
||||
42
dist/make-seed.sh
vendored
Executable file
42
dist/make-seed.sh
vendored
Executable file
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: make-seed.sh <output.iso> <pubkey-file>..."
|
||||
echo "Creates a seed ISO with the given SSH public keys."
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ "${1:-}" ] || usage
|
||||
[ "${2:-}" ] || usage
|
||||
|
||||
OUTPUT="$1"
|
||||
shift
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
: > "$TMPDIR/authorized_keys"
|
||||
for PUBKEY_FILE in "$@"; do
|
||||
if [ ! -f "$PUBKEY_FILE" ]; then
|
||||
echo "error: public key file not found: $PUBKEY_FILE"
|
||||
exit 1
|
||||
fi
|
||||
cat "$PUBKEY_FILE" >> "$TMPDIR/authorized_keys"
|
||||
done
|
||||
|
||||
if command -v mkisofs >/dev/null 2>&1; then
|
||||
ISO_CMD="mkisofs"
|
||||
elif command -v genisoimage >/dev/null 2>&1; then
|
||||
ISO_CMD="genisoimage"
|
||||
else
|
||||
echo "error: mkisofs or genisoimage required"
|
||||
echo " linux: sudo apt install genisoimage"
|
||||
echo " macos: brew install cdrtools"
|
||||
echo " nix: nix shell nixpkgs#cdrtools"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$ISO_CMD" -quiet -V SEEDCONFIG -J -R -o "$OUTPUT" "$TMPDIR"
|
||||
|
||||
echo "seed ISO created: $OUTPUT"
|
||||
42
dist/nix-cheatsheet.md
vendored
Normal file
42
dist/nix-cheatsheet.md
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# Nix package manager cheatsheet
|
||||
|
||||
## Search for packages
|
||||
|
||||
```sh
|
||||
nix search nixpkgs python
|
||||
```
|
||||
|
||||
## Add packages to current shell
|
||||
|
||||
```sh
|
||||
# single package
|
||||
nix shell nixpkgs#python3
|
||||
|
||||
# multiple packages
|
||||
nix shell nixpkgs#python3 nixpkgs#nodejs
|
||||
```
|
||||
|
||||
## Run a command directly
|
||||
|
||||
```sh
|
||||
nix run nixpkgs#python3 -- --version
|
||||
```
|
||||
|
||||
## Install packages persistently (available in new SSH sessions)
|
||||
|
||||
```sh
|
||||
# nix shell packages are lost when the session ends
|
||||
# use profile install to persist across sessions
|
||||
# packages use the nixpkgs# prefix
|
||||
nix profile install nixpkgs#nodejs nixpkgs#pnpm nixpkgs#yarn
|
||||
```
|
||||
|
||||
## Language stacks
|
||||
|
||||
| Stack | Packages |
|
||||
|-------|----------|
|
||||
| Node.js | `nodejs`, `pnpm`, `yarn` |
|
||||
| Python | `python3`, `python3Packages.pip`, `python3Packages.virtualenv`, `uv` |
|
||||
| Go | `go` |
|
||||
| Rust | `cargo`, `rustc`, `rustfmt`, `clippy` |
|
||||
| Foundry | `foundry` |
|
||||
183
dist/run.sh
vendored
Executable file
183
dist/run.sh
vendored
Executable file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# defaults
|
||||
SSH_PORT=2222
|
||||
MEMORY=8G
|
||||
CPUS=4
|
||||
PROJECTS=""
|
||||
CLAUDE_DIR=""
|
||||
SSH_KEYS=()
|
||||
SEED_ISO=""
|
||||
IMAGE=""
|
||||
GUEST_ARCH=""
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: run.sh <image.qcow2> [options]
|
||||
|
||||
Options:
|
||||
--arch <arch> Guest architecture (auto-detected from image name)
|
||||
--ssh-key <key.pub> SSH public key (repeatable, auto-generates seed ISO)
|
||||
--seed-iso <iso> Pre-built seed ISO (alternative to --ssh-key)
|
||||
--projects <path> Mount host directory as ~/projects in VM
|
||||
--claude-dir <path> Mount host .claude directory for auth
|
||||
--memory <size> VM memory (default: 8G)
|
||||
--cpus <n> VM CPUs (default: 4)
|
||||
--ssh-port <port> SSH port forward (default: 2222)
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
[ "${1:-}" ] || usage
|
||||
|
||||
IMAGE="$1"
|
||||
shift
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--arch) GUEST_ARCH="$2"; shift 2 ;;
|
||||
--ssh-key) SSH_KEYS+=("$2"); shift 2 ;;
|
||||
--seed-iso) SEED_ISO="$2"; shift 2 ;;
|
||||
--projects) PROJECTS="$2"; shift 2 ;;
|
||||
--claude-dir) CLAUDE_DIR="$2"; shift 2 ;;
|
||||
--memory) MEMORY="$2"; shift 2 ;;
|
||||
--cpus) CPUS="$2"; shift 2 ;;
|
||||
--ssh-port) SSH_PORT="$2"; shift 2 ;;
|
||||
*) echo "unknown option: $1"; usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ ! -f "$IMAGE" ]; then
|
||||
echo "error: image not found: $IMAGE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# detect guest architecture from image filename if not specified
|
||||
if [ -z "$GUEST_ARCH" ]; then
|
||||
case "$IMAGE" in
|
||||
*aarch64*|*arm64*) GUEST_ARCH="aarch64" ;;
|
||||
*x86_64*|*amd64*) GUEST_ARCH="x86_64" ;;
|
||||
*)
|
||||
# fallback to host architecture
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) GUEST_ARCH="x86_64" ;;
|
||||
aarch64|arm64) GUEST_ARCH="aarch64" ;;
|
||||
*) echo "error: cannot detect guest arch, use --arch"; exit 1 ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# normalize
|
||||
case "$GUEST_ARCH" in
|
||||
x86_64|amd64) GUEST_ARCH="x86_64" ;;
|
||||
aarch64|arm64) GUEST_ARCH="aarch64" ;;
|
||||
*) echo "error: unsupported architecture: $GUEST_ARCH"; exit 1 ;;
|
||||
esac
|
||||
|
||||
# platform detection
|
||||
HOST_ARCH=$(uname -m)
|
||||
OS=$(uname -s)
|
||||
ACCEL="tcg"
|
||||
|
||||
case "$OS" in
|
||||
Linux)
|
||||
[ -r /dev/kvm ] && ACCEL="kvm"
|
||||
;;
|
||||
Darwin)
|
||||
# hvf only works when guest matches host
|
||||
case "$HOST_ARCH" in
|
||||
aarch64|arm64) [ "$GUEST_ARCH" = "aarch64" ] && ACCEL="hvf" ;;
|
||||
x86_64|amd64) [ "$GUEST_ARCH" = "x86_64" ] && ACCEL="hvf" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$GUEST_ARCH" in
|
||||
x86_64) QEMU_BIN="qemu-system-x86_64" ;;
|
||||
aarch64) QEMU_BIN="qemu-system-aarch64" ;;
|
||||
esac
|
||||
|
||||
# auto-generate seed ISO from SSH key
|
||||
CLEANUP_SEED=""
|
||||
if [ "${#SSH_KEYS[@]}" -gt 0 ] && [ -z "$SEED_ISO" ]; then
|
||||
SEED_ISO="$(mktemp -d)/seed.iso"
|
||||
CLEANUP_SEED="$SEED_ISO"
|
||||
bash "$SCRIPT_DIR/make-seed.sh" "$SEED_ISO" "${SSH_KEYS[@]}"
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
[ -n "$CLEANUP_SEED" ] && rm -rf "$(dirname "$CLEANUP_SEED")"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# build qemu command
|
||||
QEMU_ARGS=(
|
||||
"$QEMU_BIN"
|
||||
-accel "$ACCEL"
|
||||
-m "$MEMORY"
|
||||
-smp "$CPUS"
|
||||
-drive "file=$IMAGE,format=qcow2,snapshot=on"
|
||||
-nic "user,hostfwd=tcp::${SSH_PORT}-:22"
|
||||
-nographic
|
||||
)
|
||||
|
||||
# aarch64 guest needs machine type and uefi firmware
|
||||
if [ "$GUEST_ARCH" = "aarch64" ]; then
|
||||
if [ "$ACCEL" = "hvf" ]; then
|
||||
QEMU_ARGS+=(-machine virt -cpu host)
|
||||
else
|
||||
QEMU_ARGS+=(-machine virt -cpu max)
|
||||
fi
|
||||
|
||||
EFI_CODE=""
|
||||
for p in \
|
||||
/opt/homebrew/share/qemu/edk2-aarch64-code.fd \
|
||||
/usr/local/share/qemu/edk2-aarch64-code.fd \
|
||||
/usr/share/qemu-efi-aarch64/QEMU_EFI.fd \
|
||||
/usr/share/AAVMF/AAVMF_CODE.fd; do
|
||||
[ -f "$p" ] && EFI_CODE="$p" && break
|
||||
done
|
||||
|
||||
if [ -z "$EFI_CODE" ]; then
|
||||
echo "error: aarch64 EFI firmware not found"
|
||||
echo " macos: brew install qemu"
|
||||
echo " linux: apt install qemu-efi-aarch64"
|
||||
exit 1
|
||||
fi
|
||||
QEMU_ARGS+=(-bios "$EFI_CODE")
|
||||
fi
|
||||
|
||||
# seed ISO
|
||||
if [ -n "$SEED_ISO" ]; then
|
||||
QEMU_ARGS+=(-drive "file=$SEED_ISO,format=raw,media=cdrom,readonly=on")
|
||||
fi
|
||||
|
||||
# 9p mounts — none lets the guest control ownership via dfltuid/dfltgid
|
||||
SECURITY_MODEL="none"
|
||||
|
||||
if [ -n "$PROJECTS" ]; then
|
||||
QEMU_ARGS+=(
|
||||
-virtfs "local,path=$PROJECTS,mount_tag=projects,security_model=$SECURITY_MODEL,id=fs0"
|
||||
)
|
||||
fi
|
||||
|
||||
if [ -n "$CLAUDE_DIR" ]; then
|
||||
QEMU_ARGS+=(
|
||||
-virtfs "local,path=$CLAUDE_DIR,mount_tag=hostclaude,security_model=$SECURITY_MODEL,id=fs1"
|
||||
)
|
||||
# also mount parent home for .claude.json
|
||||
QEMU_ARGS+=(
|
||||
-virtfs "local,path=$(dirname "$CLAUDE_DIR"),mount_tag=hosthome,security_model=$SECURITY_MODEL,id=fs2,readonly=on"
|
||||
)
|
||||
fi
|
||||
|
||||
echo "---"
|
||||
echo "Guest: $GUEST_ARCH | Accel: $ACCEL"
|
||||
echo "SSH: ssh -p $SSH_PORT sandbox@localhost"
|
||||
echo "---"
|
||||
|
||||
exec "${QEMU_ARGS[@]}"
|
||||
@@ -93,6 +93,15 @@
|
||||
system = "x86_64-linux";
|
||||
users = [ ];
|
||||
};
|
||||
|
||||
sandbox-x86_64 = mkHost "sandbox" {
|
||||
system = "x86_64-linux";
|
||||
users = [ "sandbox" ];
|
||||
};
|
||||
sandbox-aarch64 = mkHost "sandbox" {
|
||||
system = "aarch64-linux";
|
||||
users = [ "sandbox" ];
|
||||
};
|
||||
};
|
||||
|
||||
nixosModules = import ./modules/nixos {
|
||||
|
||||
@@ -26,6 +26,7 @@ in
|
||||
inputs.self.nixosModules.nvidia
|
||||
inputs.self.nixosModules.initrd-ssh
|
||||
inputs.self.nixosModules.localisation
|
||||
inputs.self.nixosModules.aarch64-vm
|
||||
];
|
||||
|
||||
yubikey.enable = true;
|
||||
@@ -65,6 +66,8 @@ in
|
||||
base16Scheme = "${pkgs.base16-schemes}/share/themes/gruvbox-material-dark-medium.yaml";
|
||||
};
|
||||
|
||||
aarch64-vm.enable = true;
|
||||
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
inputs.self.nixosModules.workstation
|
||||
inputs.self.nixosModules.initrd-ssh
|
||||
inputs.self.nixosModules.localisation
|
||||
inputs.self.nixosModules.aarch64-vm
|
||||
];
|
||||
|
||||
yubikey.enable = true;
|
||||
@@ -38,6 +39,7 @@
|
||||
command = "sway";
|
||||
};
|
||||
sway.enable = true;
|
||||
aarch64-vm.enable = true;
|
||||
|
||||
initrd-ssh = {
|
||||
enable = true;
|
||||
|
||||
177
hosts/sandbox/configuration.nix
Normal file
177
hosts/sandbox/configuration.nix
Normal file
@@ -0,0 +1,177 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
inputs,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
./hardware-configuration.nix
|
||||
inputs.self.nixosModules.vm-guest
|
||||
inputs.self.nixosModules.seed-ssh
|
||||
inputs.self.nixosModules.localisation
|
||||
];
|
||||
|
||||
networking.hostName = "sandbox";
|
||||
|
||||
vm-guest = {
|
||||
enable = true;
|
||||
headless = true;
|
||||
};
|
||||
|
||||
seed-ssh = {
|
||||
enable = true;
|
||||
user = "sandbox";
|
||||
};
|
||||
|
||||
localisation = {
|
||||
enable = true;
|
||||
timeZone = "UTC";
|
||||
defaultLocale = "en_US.UTF-8";
|
||||
};
|
||||
|
||||
users.users.sandbox = {
|
||||
isNormalUser = true;
|
||||
extraGroups = [
|
||||
"wheel"
|
||||
"docker"
|
||||
];
|
||||
};
|
||||
|
||||
# 9p mounts — silently fail if shares not provided at runtime
|
||||
fileSystems."/mnt/9p-projects" = {
|
||||
device = "projects";
|
||||
fsType = "9p";
|
||||
options = [
|
||||
"trans=virtio"
|
||||
"version=9p2000.L"
|
||||
"msize=65536"
|
||||
"nofail"
|
||||
"x-systemd.automount"
|
||||
"x-systemd.device-timeout=2s"
|
||||
];
|
||||
};
|
||||
|
||||
# remap ownership so sandbox user can read/write regardless of host UID
|
||||
fileSystems."/home/sandbox/projects" = {
|
||||
device = "/mnt/9p-projects";
|
||||
fsType = "fuse.bindfs";
|
||||
options = [
|
||||
"force-user=sandbox"
|
||||
"force-group=users"
|
||||
"perms=0755"
|
||||
"create-for-user=sandbox"
|
||||
"create-for-group=users"
|
||||
"nofail"
|
||||
"x-systemd.automount"
|
||||
];
|
||||
};
|
||||
|
||||
fileSystems."/mnt/host-claude" = {
|
||||
device = "hostclaude";
|
||||
fsType = "9p";
|
||||
options = [
|
||||
"trans=virtio"
|
||||
"version=9p2000.L"
|
||||
"msize=65536"
|
||||
"nofail"
|
||||
"x-systemd.device-timeout=2s"
|
||||
];
|
||||
};
|
||||
|
||||
fileSystems."/mnt/host-home" = {
|
||||
device = "hosthome";
|
||||
fsType = "9p";
|
||||
options = [
|
||||
"trans=virtio"
|
||||
"version=9p2000.L"
|
||||
"msize=65536"
|
||||
"nofail"
|
||||
"x-systemd.device-timeout=2s"
|
||||
"ro"
|
||||
];
|
||||
};
|
||||
|
||||
# pre-auth claude-code from host config
|
||||
systemd.services.claude-auth = {
|
||||
description = "Copy claude-code credentials from host mount";
|
||||
after = [
|
||||
"mnt-host\\x2dclaude.mount"
|
||||
"mnt-host\\x2dhome.mount"
|
||||
];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart =
|
||||
let
|
||||
mountpoint = "${pkgs.util-linux}/bin/mountpoint";
|
||||
in
|
||||
pkgs.writeShellScript "claude-auth" ''
|
||||
# wait for mounts to appear
|
||||
for i in $(seq 1 10); do
|
||||
${mountpoint} -q /mnt/host-claude && break
|
||||
${mountpoint} -q /mnt/host-home && break
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if ! ${mountpoint} -q /mnt/host-claude && ! ${mountpoint} -q /mnt/host-home; then
|
||||
echo "no host mounts found, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p /home/sandbox/.claude
|
||||
if ${mountpoint} -q /mnt/host-claude; then
|
||||
cp -a /mnt/host-claude/. /home/sandbox/.claude/
|
||||
fi
|
||||
if ${mountpoint} -q /mnt/host-home; then
|
||||
cp /mnt/host-home/.claude.json /home/sandbox/.claude.json || true
|
||||
fi
|
||||
chown -R sandbox:users /home/sandbox/.claude /home/sandbox/.claude.json 2>/dev/null || true
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
virtualisation.docker = {
|
||||
enable = true;
|
||||
logDriver = "json-file";
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
bindfs
|
||||
claude-code
|
||||
|
||||
# tools
|
||||
tmux
|
||||
fd
|
||||
ripgrep
|
||||
jq
|
||||
fzf
|
||||
just
|
||||
];
|
||||
|
||||
# image builder VM needs more than the default 1G to copy closure
|
||||
image.modules =
|
||||
let
|
||||
imageMemOverride =
|
||||
{ config, modulesPath, ... }:
|
||||
{
|
||||
system.build.image = lib.mkForce (
|
||||
import (modulesPath + "/../lib/make-disk-image.nix") {
|
||||
inherit lib config pkgs;
|
||||
inherit (config.virtualisation) diskSize;
|
||||
inherit (config.image) baseName format;
|
||||
partitionTableType = if config.image.efiSupport then "efi" else "legacy";
|
||||
memSize = 16384;
|
||||
}
|
||||
);
|
||||
};
|
||||
in
|
||||
{
|
||||
qemu = imageMemOverride;
|
||||
qemu-efi = imageMemOverride;
|
||||
};
|
||||
|
||||
system.stateVersion = "25.11";
|
||||
}
|
||||
21
hosts/sandbox/hardware-configuration.nix
Normal file
21
hosts/sandbox/hardware-configuration.nix
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
modulesPath,
|
||||
...
|
||||
}:
|
||||
{
|
||||
imports = [
|
||||
(modulesPath + "/profiles/qemu-guest.nix")
|
||||
];
|
||||
|
||||
fileSystems."/" = {
|
||||
device = "/dev/disk/by-label/nixos";
|
||||
autoResize = true;
|
||||
fsType = "ext4";
|
||||
};
|
||||
|
||||
# image.modules (disk-image.nix) overrides boot loader per variant
|
||||
# x86_64: qemu (grub), aarch64: qemu-efi (systemd-boot)
|
||||
boot.loader.grub.device = lib.mkDefault "/dev/vda";
|
||||
}
|
||||
21
justfile
21
justfile
@@ -36,3 +36,24 @@ iso:
|
||||
# garbage collect old generations
|
||||
clean:
|
||||
sudo nix-collect-garbage $(nix eval --raw -f ./nix.nix nix.gc.options)
|
||||
|
||||
# build sandbox VM image
|
||||
sandbox-build arch:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [ "{{arch}}" = "x86_64" ]; then
|
||||
nixos-rebuild build-image --image-variant qemu --flake .#sandbox-x86_64
|
||||
elif [ "{{arch}}" = "aarch64" ]; then
|
||||
nixos-rebuild build-image --image-variant qemu-efi --flake .#sandbox-aarch64
|
||||
else
|
||||
echo "error: arch must be x86_64 or aarch64"; exit 1
|
||||
fi
|
||||
ln -sfn "$(readlink result)" "result-sandbox-{{arch}}"
|
||||
|
||||
# run sandbox VM
|
||||
sandbox-run arch *ARGS:
|
||||
bash dist/run.sh $(find -L result-sandbox-{{arch}} -name '*.qcow2' | head -1) {{ARGS}}
|
||||
|
||||
# ssh into running sandbox
|
||||
sandbox-ssh:
|
||||
ssh -p 2222 -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null sandbox@localhost
|
||||
|
||||
14
modules/nixos/aarch64-vm.nix
Normal file
14
modules/nixos/aarch64-vm.nix
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
options = {
|
||||
aarch64-vm.enable = lib.mkEnableOption "aarch64 virtualisation support";
|
||||
};
|
||||
|
||||
config = lib.mkIf config.aarch64-vm.enable {
|
||||
boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
|
||||
};
|
||||
}
|
||||
69
modules/nixos/seed-ssh.nix
Normal file
69
modules/nixos/seed-ssh.nix
Normal file
@@ -0,0 +1,69 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
options = {
|
||||
seed-ssh = {
|
||||
enable = lib.mkEnableOption "SSH key injection from seed ISO";
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "user to install authorized_keys for";
|
||||
};
|
||||
|
||||
label = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "SEEDCONFIG";
|
||||
description = "volume label of the seed ISO";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf config.seed-ssh.enable {
|
||||
systemd.services.seed-ssh = {
|
||||
description = "Install SSH authorized_keys from seed ISO";
|
||||
after = [
|
||||
"local-fs.target"
|
||||
"nss-user-lookup.target"
|
||||
];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
ExecStart =
|
||||
let
|
||||
cfg = config.seed-ssh;
|
||||
inherit (cfg) user;
|
||||
inherit (config.users.users.${user}) home group;
|
||||
in
|
||||
pkgs.writeShellScript "seed-ssh" ''
|
||||
# try by-label first, then scan block devices for the volume label
|
||||
DEVICE="/dev/disk/by-label/${cfg.label}"
|
||||
if [ ! -e "$DEVICE" ]; then
|
||||
DEVICE=$(${pkgs.util-linux}/bin/blkid -t LABEL="${cfg.label}" -o device | head -1)
|
||||
fi
|
||||
|
||||
if [ -z "$DEVICE" ] || [ ! -e "$DEVICE" ]; then
|
||||
echo "seed ISO not found, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MOUNT=$(mktemp -d)
|
||||
${pkgs.util-linux}/bin/mount -o ro "$DEVICE" "$MOUNT"
|
||||
|
||||
mkdir -p "${home}/.ssh"
|
||||
cp "$MOUNT/authorized_keys" "${home}/.ssh/authorized_keys"
|
||||
chmod 700 "${home}/.ssh"
|
||||
chmod 600 "${home}/.ssh/authorized_keys"
|
||||
chown -R ${user}:${group} "${home}/.ssh"
|
||||
|
||||
${pkgs.util-linux}/bin/umount "$MOUNT"
|
||||
rmdir "$MOUNT"
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
62
modules/nixos/vm-guest.nix
Normal file
62
modules/nixos/vm-guest.nix
Normal file
@@ -0,0 +1,62 @@
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
options = {
|
||||
vm-guest = {
|
||||
enable = lib.mkEnableOption "VM guest configuration";
|
||||
headless = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = "run without display, serial console only";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf config.vm-guest.enable {
|
||||
services.qemuGuest.enable = true;
|
||||
services.spice-vdagentd.enable = lib.mkIf (!config.vm-guest.headless) true;
|
||||
|
||||
boot.kernelParams = lib.mkIf config.vm-guest.headless [ "console=ttyS0,115200" ];
|
||||
|
||||
# 9p for host file mounting
|
||||
boot.initrd.availableKernelModules = [
|
||||
"9p"
|
||||
"9pnet_virtio"
|
||||
];
|
||||
boot.kernelModules = [
|
||||
"9p"
|
||||
"9pnet_virtio"
|
||||
];
|
||||
|
||||
# ssh with agent forwarding for git and hot-mount
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
ports = [ 22 ];
|
||||
settings = {
|
||||
PasswordAuthentication = false;
|
||||
PermitRootLogin = "no";
|
||||
AllowAgentForwarding = true;
|
||||
StreamLocalBindUnlink = "yes";
|
||||
};
|
||||
};
|
||||
|
||||
networking = {
|
||||
useDHCP = true;
|
||||
firewall.allowedTCPPorts = [ 22 ];
|
||||
};
|
||||
|
||||
security.sudo.wheelNeedsPassword = false;
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
curl
|
||||
wget
|
||||
htop
|
||||
sshfs
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
inputs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
packages = inputs.self.outputs.packages.${pkgs.stdenv.hostPlatform.system};
|
||||
in
|
||||
|
||||
{
|
||||
home.stateVersion = "24.11";
|
||||
|
||||
home.packages = [
|
||||
pkgs.git
|
||||
];
|
||||
|
||||
programs.neovim = {
|
||||
enable = true;
|
||||
vimAlias = true;
|
||||
defaultEditor = true;
|
||||
|
||||
package = inputs.neovim-nightly-overlay.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||
|
||||
extraPackages = with pkgs; [
|
||||
# runtime deps
|
||||
fzf
|
||||
ripgrep
|
||||
gnumake
|
||||
gcc
|
||||
luajit
|
||||
|
||||
lua-language-server
|
||||
nil
|
||||
nixd
|
||||
|
||||
nixpkgs-fmt
|
||||
stylua
|
||||
|
||||
];
|
||||
|
||||
extraWrapperArgs = [
|
||||
"--suffix"
|
||||
"LD_LIBRARY_PATH"
|
||||
":"
|
||||
"${lib.makeLibraryPath [ pkgs.stdenv.cc.cc.lib ]}"
|
||||
];
|
||||
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,9 @@ in
|
||||
services.dunst.enable = true;
|
||||
|
||||
home.packages = [
|
||||
pkgs.qemu
|
||||
pkgs.cdrtools
|
||||
|
||||
pkgs.bibata-cursors
|
||||
|
||||
pkgs.starship
|
||||
|
||||
21
users/sandbox/home-manager.nix
Normal file
21
users/sandbox/home-manager.nix
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
home.stateVersion = "25.11";
|
||||
|
||||
home.packages = with pkgs; [
|
||||
git
|
||||
tmux
|
||||
ripgrep
|
||||
fd
|
||||
jq
|
||||
];
|
||||
|
||||
programs.neovim = {
|
||||
enable = true;
|
||||
vimAlias = true;
|
||||
defaultEditor = true;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user