diff --git a/Justfile b/Justfile index 8b52ccc..ef7e8e6 100644 --- a/Justfile +++ b/Justfile @@ -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//, supprimer ce dossier pour repartir +# d'une VM vierge. Nom = clé de nixosConfigurations (vms/.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/.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 diff --git a/flake.nix b/flake.nix index 49ff5cd..6a98ef0 100644 --- a/flake.nix +++ b/flake.nix @@ -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 { diff --git a/vms/default.nix b/vms/default.nix new file mode 100644 index 0000000..df20b17 --- /dev/null +++ b/vms/default.nix @@ -0,0 +1,180 @@ +# Base commune des VM de dev microvm.nix (input `microvm` du flake). +# +# Une VM = ce module + vms/.nix + deux entrées dans flake.nix +# (nixosConfigurations. et packages.x86_64-linux.). +# 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/.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/.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/.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/.nix) + arrêt : poweroff, ou <état>/current/bin/microvm-shutdown + ''; +} diff --git a/vms/dev.nix b/vms/dev.nix new file mode 100644 index 0000000..bfd8f0d --- /dev/null +++ b/vms/dev.nix @@ -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]; +}