feat(vms): VM de dev microvm.nix (just vm, nix run .#dev)

vms/dev.nix décrit une VM de dev (autologin marc, console série) construite par
l'input microvm ; ses runners sont exposés dans flake.nix pour être lancés
directement (`nix run .#dev`) et le Justfile ajoute `just vm` / `just vm-build`,
qui gardent l'état de la VM (overlay du store, socket qmp) dans
~/.local/state/microvms/<nom> (supprimer le dossier pour repartir de zéro).

Le runner passe par `path:` volontairement : ça marche même quand vms/<nom>.nix
n'est pas encore commité.
This commit is contained in:
Hermes Agent
2026-09-27 13:00:56 -04:00
parent 2622780459
commit ce6235d8f2
4 changed files with 286 additions and 0 deletions
+18
View File
@@ -13,6 +13,24 @@ home:
nixos:
sudo nixos-rebuild switch --impure --flake .#{{env('HOST')}}
# VM de dev microvm.nix : construit si besoin puis lance la VM dans le terminal
# (console série, autologin marc). L'état (overlay du /nix/store, socket qmp)
# vit dans ~/.local/state/microvms/<nom>/, supprimer ce dossier pour repartir
# d'une VM vierge. Nom = clé de nixosConfigurations (vms/<nom>.nix), défaut dev.
vm name="dev":
#!/usr/bin/env bash
set -euo pipefail
state="${XDG_STATE_HOME:-$HOME/.local/state}/microvms/{{name}}"
mkdir -p "$state"
cd "$state"
# path: volontaire : marche même si vms/<nom>.nix n'est pas encore commité
exec nix run "path:{{justfile_directory()}}#{{name}}"
# Construit le runner d'une VM sans la lancer (préchauffe, pratique pour voir
# les erreurs de build sans VM au milieu)
vm-build name="dev":
nix build --no-link "path:{{justfile_directory()}}#{{name}}"
iso:
nix build .#nixosConfigurations.{{env('HOST')}}-iso.config.system.build.isoImage > nixos.iso
+14
View File
@@ -239,8 +239,22 @@
./users.nix
];
};
# VM de dev microvm.nix (input `microvm`, voir vms/) : `just vm` ou
# `nix run .#dev`. Dupliquer vms/dev.nix pour une 2e VM.
dev = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = {inherit inputs;};
modules = [
inputs.microvm.nixosModules.microvm
./vms/dev.nix
];
};
};
# Runners des VM de dev, lançables directement : `nix run .#dev`.
packages.x86_64-linux.dev = self.nixosConfigurations.dev.config.microvm.declaredRunner;
nixOnDroidConfigurations.default = inputs.nix-on-droid.lib.nixOnDroidConfiguration {
# pkgs = import inputs.nixpkgs-droid {
pkgs = import inputs.nix-on-droid.inputs.nixpkgs {
+180
View File
@@ -0,0 +1,180 @@
# Base commune des VM de dev microvm.nix (input `microvm` du flake).
#
# Une VM = ce module + vms/<nom>.nix + deux entrées dans flake.nix
# (nixosConfigurations.<nom> et packages.x86_64-linux.<nom>).
# Exemple complet et mode d'emploi : vms/dev.nix.
#
# Pourquoi le build est rapide :
# - le /nix/store de tower est partagé en 9p (lecture seule) : aucune image
# squashfs/erofs de store n'est construite, et tout ce que tower a déjà
# construit ou téléchargé est directement disponible dans la VM
# (`nix shell`, `nix develop`, exécuter un binaire du store : 0 build) ;
# - même nixpkgs que l'hôte : le noyau invité est le dérivé de
# boot.kernelPackages de tower, donc déjà en store ;
# - /nix/store est monté par-dessus un overlay écrivable posé sur un petit
# disque (nix-store-overlay.img) : `nix build` fonctionne DANS la VM.
# Le reste (/ /var /home) est un tmpfs : chaque démarrage repart propre.
{
config,
lib,
pkgs,
...
}: {
microvm = {
hypervisor = "qemu";
# optimize = false : microvm.nix compile sinon une variante de qemu sans
# GUI (`minimal = true`) qui n'est dans aucun cache binaire (28 min de
# compilation locale) ET qui est compilée sans le réseau user de qemu
# (slirp) : `-netdev user` échoue donc au démarrage. Le qemu_kvm standard
# (même version, avec slirp) est en cache.nixos.org : rien à construire.
optimize.enable = false;
# Ressources ; surcharger dans vms/<nom>.nix. NE PAS mettre mem = 2048
# (valeur exacte qui bloque qemu, cf. microvm.nix#171).
vcpu = lib.mkDefault 4;
mem = lib.mkDefault 4096;
shares = [
{
tag = "ro-store";
source = "/nix/store";
mountPoint = "/nix/.ro-store";
# 9p est intégré à qemu (pas besoin de virtiofsd, contrairement à
# proto = "virtiofs" qui suppose le module hôte microvm + systemd).
proto = "9p";
}
];
# Overlay écrivable du store : lower = /nix/.ro-store, upper = le disque
# ci-dessous. Le disque est créé au premier lancement (chemin relatif au
# répertoire d'état depuis lequel tourne le runner, cf. recette `just vm`).
writableStoreOverlay = "/nix/.rw-store";
volumes = [
{
image = "nix-store-overlay.img";
mountPoint = config.microvm.writableStoreOverlay;
size = 8192; # Mo, fichier sparse (n'occupe que ce qui est écrit)
}
];
# Réseau slirp (user) : la VM sort sur Internet, et les ports de
# forwardPorts sont redirigés depuis tower (bind 127.0.0.1 uniquement).
interfaces = [
{
type = "user";
id = "user";
mac = "02:00:00:1f:1d:01"; # changer par VM (cosmétique, réseaux isolés)
}
];
forwardPorts = [
# ssh dans la VM depuis tower
{
from = "host";
host.address = "127.0.0.1";
host.port = 2222;
guest.port = 22;
}
];
# Pas de fenêtre graphique : la console s'affiche en série dans le terminal
# qui lance la VM (qemu -nographic), avec autologin de marc.
graphics.enable = false;
};
# --- Optimisations de démarrage ---
# `microvm.optimize.enable = false` plus haut empêche de récupérer celles du
# module microvm (il les couple à un qemu sans slirp) : on les reprend ici
# telles quelles, sans toucher au paquet qemu.
documentation.enable = lib.mkDefault false; # les docs pèsent lourd, inutiles en VM
boot = {
initrd.systemd.enable = lib.mkDefault true; # initrd systemd : démarrage bien plus rapide
swraid.enable = lib.mkDefault false;
kernelParams = ["8250.nr_uarts=1"]; # une seule console série
};
networking.useNetworkd = lib.mkDefault true; # networkd plutôt que dhcpcd (plus rapide)
systemd.network.wait-online.enable = lib.mkDefault false; # bug systemd#29388
systemd.tpm2.enable = lib.mkDefault false;
# --- Système invité minimal ---
system.stateVersion = "25.11";
networking.hostName = lib.mkDefault "vm-dev"; # vms/<nom>.nix met le vrai nom
networking.useDHCP = true; # l'interface slirp a besoin d'un bail DHCP
networking.firewall.enable = false; # NAT ; seuls les ports de forwardPorts sont joignables
time.timeZone = "America/New_York";
services.openssh = {
enable = true;
settings.PermitRootLogin = "no";
};
# Console série : shell direct en marc (bac à sable local, jetable).
services.getty.autologinUser = "marc";
users.users.marc = {
isNormalUser = true;
description = "Marc Partensky";
shell = pkgs.zsh;
extraGroups = ["wheel" "kvm"];
# Même hash que users.nix (celui de tower) : login console par mot de passe
# possible si l'autologin est retiré.
hashedPassword = "$6$dE28cUDZ9G81wzwo$JoO.6M91zdtR0ja/g1pw/LKE8aAMugmGkIyOiSuqdjEWVKId9F2jxqdvh5Bo69uOC3/g7uP6O3mZmzxqD2pKU1";
openssh.authorizedKeys.keys = [
# clé de marc (users.nix)
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDiUE73uIEgijfmSsDwBvZmecQnqjBUPRKlMDmevsThc1YJNWEHl57NNIUcx6XSCDPKu5azayImLqIBt8wT5xlqtNX20uCnikDfXZ8gFbGlMRTGZKutQZIRmUrUS5mz97S4dVVK+n5WU+OwOfEg/XKXPh4WbTVDpfTTg7RopRAXkma56HV2TJM0ndPRN8VLmBmtnwdQwEpJ0tRRY+KOHmTojsH65eaZ89+BHbto+Kg+lk6x8IH5VDCRQNHgTEccOpOGYBSHRpoZi1a5h3yajf/eGAQ9Cd38DOsfMtm84oFlii7oXPyxwXoM+uH1SDnvLXyheIrV/XLUurSbEb4aJni6Zu79Z9l8xHhUNmVNSZqWOWUvPbAHlDKUzsbxgk9Zs9OTvSDaRzGhViYl4e1Qc993yerGSW1HHIvYUKM7o5nSQqskSOvOI+ahL5fIbgdyVx4FeuURZIyZSxCz4jIJTK15/6pkT/miHKv+vmQhsoLCqgyXY4SG1p9ruzKkzBe03ZQVW5WeFDLYRjZ+Z4Q2IL2K3BmLgp8tInkPJizQ7v5UGSiajJmPxY0j+CqdH9ZlIBdf8GS+run/N4hpMC1/ayUZRbCY5jg4c8bev8dKEZYJKPs/Hq2zLRZe4YtxcKuiGhgIwQOzo/QrCvSM4pVDgo+d2DjEzIdapqE8hF6BHWDg/w== marc.partensky@gmail.com"
# clé de l'agent hermes (debug / aide depuis tower)
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKF3huPUXHP6P6SBXHHw9k7HGh6Cs8ntoRk2pnqrG2Hc hermes@tower"
];
};
security.sudo.wheelNeedsPassword = false; # configuration jetable
programs.zsh.enable = true;
users.defaultUserShell = pkgs.zsh;
nix.settings = {
experimental-features = ["nix-command" "flakes"];
trusted-users = ["root" "marc"];
# ne pas activer auto-optimise-store : incompatible avec writableStoreOverlay
};
# Volontairement léger : tout le reste se récupère depuis le store partagé de
# tower (`nix shell`/`nix develop`) ou s'ajoute dans vms/<nom>.nix.
environment.systemPackages = with pkgs; [
curl
fd
git
htop
jq
just
neovim
ripgrep
rsync
tmux
tree
unzip
vim
wget
];
# /home est un tmpfs recréé à chaque démarrage : sans ~/.zshrc, zsh lance son
# assistant "nouvel utilisateur" à chaque connexion. On dépose un rc minimal.
system.activationScripts.vm-zshrc = ''
install -D -o marc -g users -m 0644 ${pkgs.writeText "vm-zshrc" "# VM de dev jetable : ports redirigés listés dans le motd\nautoload -Uz compinit && compinit -C\nalias ll='ls -alF'\nexport EDITOR=nvim\n"} /home/marc/.zshrc
'';
# motd généré depuis microvm.forwardPorts : la correspondance des ports
# VM -> tower s'affiche à la connexion (et suit toute modif des forwards).
# Depuis 26.05 users.motd n'écrit plus /etc/motd : pam_motd l'affiche en
# direct, et les modules sshd/login l'activent déjà (showMotd = true).
users.motd = let
sshRule = lib.findFirst (p: p.guest.port == 22) null config.microvm.forwardPorts;
appRules = builtins.filter (p: p.guest.port != 22) config.microvm.forwardPorts;
addr = p: if p.host.address == "" then "127.0.0.1" else p.host.address;
in ''
VM de dev microvm.nix : /nix/store de tower partagé.
${lib.optionalString (sshRule != null) " ssh depuis tower : ssh -p ${toString sshRule.host.port} marc@127.0.0.1\n"}${lib.concatMapStringsSep "\n" (p: " port VM ${toString p.guest.port} -> http://${addr p}:${toString p.host.port} sur tower") appRules}
code de l'hôte : microvm.shares (voir vms/<nom>.nix)
arrêt : poweroff, ou <état>/current/bin/microvm-shutdown
'';
}
+74
View File
@@ -0,0 +1,74 @@
# VM de dev « dev » : `just vm` (ou `nix run .#dev`).
#
# Pour créer une autre VM :
# 1. cp vms/dev.nix vms/app2.nix, changer networking.hostName (et les ports)
# 2. flake.nix : dupliquer le bloc nixosConfigurations.dev en .app2, et
# packages.x86_64-linux.app2 = self.nixosConfigurations.app2.config.microvm.declaredRunner;
# 3. just vm app2
{
lib,
pkgs,
...
}: {
imports = [./default.nix];
networking.hostName = "dev";
microvm = {
# tower : 16 cœurs / 62 Go mais machine bien chargée, rester raisonnable.
vcpu = 6;
mem = 6144;
# Code de l'hôte visible dans la VM, en lecture/écriture (9p). Le montage
# est à l'uid de marc (1000 = le même que dans la VM), donc les fichiers
# restent à lui. Réduire à un projet précis si besoin.
shares = [
{
tag = "git";
source = "/home/marc/git";
mountPoint = "/mnt/host/git";
proto = "9p";
}
];
# Ports de dev : le serveur lancé DANS la VM est joignable depuis tower.
# Décalés de +10000 pour ne pas entrer en conflit avec un service de tower
# (3000 = gitea par ex.) : qemu refuse de démarrer si le port hôte est déjà
# pris. Ici : VM:3000 -> tower:13000, VM:5173 -> tower:15173, etc.
forwardPorts = [
{
from = "host";
host.address = "127.0.0.1";
host.port = 13000;
guest.port = 3000;
} # node/next
{
from = "host";
host.address = "127.0.0.1";
host.port = 14000;
guest.port = 4000;
} # phoenix
{
from = "host";
host.address = "127.0.0.1";
host.port = 15173;
guest.port = 5173;
} # vite
{
from = "host";
host.address = "127.0.0.1";
host.port = 18000;
guest.port = 8000;
} # django/fastapi
{
from = "host";
host.address = "127.0.0.1";
host.port = 18080;
guest.port = 8080;
} # divers
];
};
# Paquets propres à cette VM (la base est dans vms/default.nix) :
# environment.systemPackages = with pkgs; [python3 postgresql];
}