diff --git a/_machines/beacon.nix b/_machines/beacon.nix index bc7bfe9..2c4b854 100644 --- a/_machines/beacon.nix +++ b/_machines/beacon.nix @@ -11,4 +11,7 @@ }; modules = [../nixos/configuration-beacon.nix]; }; + + config.flake.packages.x86_64-linux.beacon-image = + config.flake.nixosConfigurations.beacon.config.system.build.image; } diff --git a/azos-core b/azos-core index e62f366..99e97c9 160000 --- a/azos-core +++ b/azos-core @@ -1 +1 @@ -Subproject commit e62f366f560fd8e051ac5827a236052af5ea9bfc +Subproject commit 99e97c9489b06514d963e32e2e035debf679d110 diff --git a/features/lauretta/emacs/config.org b/features/lauretta/emacs/config.org index 7a39f27..58302a1 100644 --- a/features/lauretta/emacs/config.org +++ b/features/lauretta/emacs/config.org @@ -157,6 +157,166 @@ (kbd "C") 'azos/caldav-sync) #+end_src +** Beacon Remote Jobs + +#+begin_src emacs-lisp +(defvar azos/beacon/host "aner@192.168.1.200" + "SSH connection for beacon machine.") + +(defvar azos/beacon/max-logs 5 + "Number of most-recent logs to keep per project on beacon.") + +(defvar azos/beacon/keymap (make-sparse-keymap) + "Keymap for beacon remote job commands (M-o b prefix).") + +(define-key azos/global-minor-mode/open-keymap (kbd "b") azos/beacon/keymap) + +(defun azos/beacon--project-root () + (or (and (fboundp 'projectile-project-root) (projectile-project-root)) + (error "Not in a projectile project"))) + +(defun azos/beacon--project-name (root) + (file-name-nondirectory (directory-file-name root))) + +(defun azos/beacon--list-python-files (root) + (split-string + (shell-command-to-string + (format "cd %s && git ls-files | grep '\\.py$'" (shell-quote-argument root))) + "\n" t)) + +(defun azos/beacon--list-executables (root) + (split-string + (shell-command-to-string + (format "cd %s && git ls-files | while IFS= read -r f; do [ -x \"$f\" ] && echo \"$f\"; done" + (shell-quote-argument root))) + "\n" t)) + +(defun azos/beacon--ssh-run (script) + "Pipe SCRIPT as bash to beacon over SSH stdin. Error on non-zero exit." + (with-temp-buffer + (insert script) + (let ((ret (call-process-region (point-min) (point-max) "ssh" nil t nil + azos/beacon/host "bash"))) + (unless (zerop ret) + (error "Beacon SSH error (exit %d): %s" ret (string-trim (buffer-string))))))) + +(defun azos/beacon--ssh-query (cmd &optional allow-exit-one) + "Run CMD on beacon, return output string. Error on failure. +With ALLOW-EXIT-ONE, exit code 1 is also treated as success (for tmux ls)." + (with-temp-buffer + (let ((ret (call-process "ssh" nil t nil azos/beacon/host cmd))) + (unless (or (zerop ret) (and allow-exit-one (= ret 1))) + (error "Beacon SSH error (exit %d): %s" ret (string-trim (buffer-string))))) + (buffer-string))) + +(defun azos/beacon--dispatch (root cmd label) + "Sync ROOT to beacon and run CMD in project dir under direnv. +LABEL names the tmux session and log file." + (let* ((project-name (azos/beacon--project-name root)) + (timestamp (format-time-string "%Y%m%d-%H%M%S")) + (session (format "%s-%s-%s" project-name label timestamp)) + (log-name (format "%s-%s.log" label timestamp))) + (message "Syncing %s to beacon..." project-name) + (azos/beacon--ssh-run (format "mkdir -p ~/beacon-projects/%s\n" project-name)) + (with-temp-buffer + (let ((ret (call-process-shell-command + (format "bash -c %s" + (shell-quote-argument + (format "cd %s && git ls-files | rsync -avz --files-from=- . %s:~/beacon-projects/%s/" + root azos/beacon/host project-name))) + nil t nil))) + (unless (zerop ret) + (error "Beacon rsync failed (exit %d): %s" ret (string-trim (buffer-string)))))) + (azos/beacon--ssh-run + (format "RDIR=~/beacon-projects/%s +LDIR=~/beacon-logs/%s +mkdir -p \"$LDIR\" +direnv allow \"$RDIR\" +tmux new-session -d -s %s -- bash -c \"cd $RDIR && direnv exec . %s 2>&1 | tee $LDIR/%s\" +" + project-name project-name + (shell-quote-argument session) + cmd log-name)) + (azos/beacon--ssh-run + (format "find ~/beacon-logs/%s -name '*.log' -printf '%%T@ %%p\\n' 2>/dev/null | sort -rn | tail -n +%d | cut -d' ' -f2- | xargs -r rm -f\n" + project-name (1+ azos/beacon/max-logs))) + (message "Beacon job started: %s" session))) + +(defun azos/beacon/run-python () + "Sync current project to beacon and run a selected Python file." + (interactive) + (let* ((root (azos/beacon--project-root)) + (files (azos/beacon--list-python-files root)) + (file (completing-read "Python file: " files nil t)) + (label (file-name-sans-extension (file-name-nondirectory file)))) + (azos/beacon--dispatch root (format "python3 %s" file) label))) + +(defun azos/beacon/run-exec () + "Sync current project to beacon and run a selected executable." + (interactive) + (let* ((root (azos/beacon--project-root)) + (files (azos/beacon--list-executables root)) + (file (completing-read "Executable: " files nil t)) + (label (file-name-sans-extension (file-name-nondirectory file)))) + (azos/beacon--dispatch root (format "./%s" file) label))) + +(defun azos/beacon/run-command () + "Sync current project to beacon and run an arbitrary command." + (interactive) + (let* ((root (azos/beacon--project-root)) + (cmd (read-string "Command: ")) + (label (replace-regexp-in-string + "[^a-zA-Z0-9-]" "-" + (substring cmd 0 (min 20 (length cmd)))))) + (azos/beacon--dispatch root cmd label))) + +(defun azos/beacon/list-jobs () + "Show all running beacon tmux sessions." + (interactive) + (async-shell-command + (format "ssh %s 'tmux ls 2>/dev/null || echo \"No jobs running\"'" azos/beacon/host) + "*beacon-jobs*")) + +(defun azos/beacon/kill-job () + "Kill a beacon tmux session selected interactively." + (interactive) + (let* ((output (azos/beacon--ssh-query "tmux ls -F '#S' 2>/dev/null" t)) + (sessions (split-string (string-trim output) "\n" t)) + (session (completing-read "Kill job: " sessions nil t))) + (azos/beacon--ssh-run (format "tmux kill-session -t %s\n" (shell-quote-argument session))) + (message "Killed beacon job: %s" session))) + +(defun azos/beacon--tail-path (path) + (async-shell-command + (format "ssh %s %s" azos/beacon/host + (shell-quote-argument (format "tail -f %s" path))) + (format "*beacon-tail-%s*" (file-name-nondirectory path)))) + +(defun azos/beacon/tail-log (&optional arg) + "Tail the most recent beacon log. +With prefix ARG, prompt for a pattern then select from matches." + (interactive "P") + (let* ((sorted-output (azos/beacon--ssh-query + "find ~/beacon-logs -name '*.log' -printf '%T@ %P\n' 2>/dev/null | sort -rn | cut -d' ' -f2-")) + (logs (split-string (string-trim sorted-output) "\n" t))) + (when (null logs) (error "No logs found on beacon")) + (if (not arg) + (azos/beacon--tail-path (format "~/beacon-logs/%s" (car logs))) + (let* ((pattern (read-string "Log pattern: ")) + (matches (seq-filter (lambda (l) (string-match-p pattern l)) logs)) + (log (if (= (length matches) 1) + (car matches) + (completing-read "Tail log: " matches nil t)))) + (azos/beacon--tail-path (format "~/beacon-logs/%s" log)))))) + +(define-key azos/beacon/keymap (kbd "p") #'azos/beacon/run-python) +(define-key azos/beacon/keymap (kbd "e") #'azos/beacon/run-exec) +(define-key azos/beacon/keymap (kbd "c") #'azos/beacon/run-command) +(define-key azos/beacon/keymap (kbd "l") #'azos/beacon/list-jobs) +(define-key azos/beacon/keymap (kbd "k") #'azos/beacon/kill-job) +(define-key azos/beacon/keymap (kbd "t") #'azos/beacon/tail-log) +#+end_src + * Provide #+begin_src emacs-lisp diff --git a/nixos/configuration-beacon.nix b/nixos/configuration-beacon.nix index 95ed3ba..8ae2947 100644 --- a/nixos/configuration-beacon.nix +++ b/nixos/configuration-beacon.nix @@ -7,10 +7,22 @@ ... }: { imports = [ - "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix" + "${modulesPath}/virtualisation/disk-image.nix" suiteModules.nixos.attic ]; + image.format = "raw"; + + boot.initrd.availableKernelModules = [ + "xhci_pci" + "usb_storage" + "uas" + "ahci" + "nvme" + "usbhid" + "sd_mod" + ]; + nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.config.allowUnfree = true; nixpkgs.config.cudaSupport = true; @@ -28,11 +40,13 @@ ]; }; + security.sudo.wheelNeedsPassword = false; + networking.hostName = "beacon"; time.timeZone = "Asia/Jerusalem"; - # NetworkManager is enabled by installation-cd-minimal; configure WiFi + static IP - # via a keyfile so it activates automatically on boot. + networking.networkmanager.enable = true; + hardware.enableRedistributableFirmware = true; networking.useDHCP = false; environment.etc."NetworkManager/system-connections/Zakobar.nmconnection" = { mode = "0600"; @@ -62,47 +76,27 @@ ''; }; - # Storage drive (ext4, label "storage") provides persistent nix store and data dir. - # Prerequisites — run once on the storage drive before first boot: - # mkfs.ext4 -L storage /dev/sdX - # mount /dev/sdX /mnt/storage - # mkdir -p /mnt/storage/nix-rw/store /mnt/storage/nix-rw/work /mnt/storage/data - # umount /mnt/storage - # The drive is required to boot; boot halts if it is not plugged in. - fileSystems."/mnt/storage" = { - device = "/dev/disk/by-label/storage"; - fsType = "ext4"; - neededForBoot = true; - options = ["noatime"]; + # Swap is created on first boot after boot.growPartition expands the root + # partition to the full drive size. Non-blocking: a failure just means no + # swap on this boot (retried next boot once the partition has grown). + systemd.services.beacon-swap = { + description = "Create and activate swapfile"; + after = ["systemd-growfs@-.service" "local-fs.target"]; + wantedBy = ["multi-user.target"]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + if [ ! -f /swapfile ]; then + dd if=/dev/zero of=/swapfile bs=1M count=16384 status=progress + chmod 600 /swapfile + ${pkgs.util-linux}/bin/mkswap /swapfile + fi + ${pkgs.util-linux}/bin/swapon /swapfile + ''; }; - # Redirect the live CD's tmpfs rw-store to the storage drive so nix store - # writes survive across boots and don't consume RAM. - fileSystems."/nix/.rw-store" = lib.mkForce { - device = "/mnt/storage/nix-rw"; - fsType = "none"; - options = ["bind"]; - depends = ["/mnt/storage"]; - neededForBoot = true; - }; - - fileSystems."/data" = { - device = "/mnt/storage/data"; - fsType = "none"; - options = ["bind"]; - depends = ["/mnt/storage"]; - }; - - swapDevices = [ - { - device = "/mnt/storage/swapfile"; - size = 16384; - } - ]; - - # Ensure ext4 is available in initrd for the storage drive - boot.initrd.kernelModules = ["ext4"]; - # NVIDIA RTX 4050 — Ada Lovelace supports open kernel modules services.xserver.videoDrivers = ["nvidia"]; hardware.nvidia = { @@ -112,7 +106,7 @@ }; hardware.graphics.enable = true; - services.getty.autologinUser = lib.mkForce "aner"; + services.getty.autologinUser = "aner"; services.openssh = { enable = true; @@ -135,8 +129,15 @@ rsync tmux vim + wget + rclone + pciutils + nvtopPackages.nvidia cudaPackages.cudatoolkit + cudaPackages.cudnn + cudaPackages.nccl python3 + direnv ]; azos.attic.enable = true; diff --git a/nixos/configuration.nix b/nixos/configuration.nix index 345a5ff..875f368 100644 --- a/nixos/configuration.nix +++ b/nixos/configuration.nix @@ -22,7 +22,11 @@ suiteModules.nixos.attic ]; - boot.loader.systemd-boot.enable = true; + boot.loader.grub = { + enable = true; + efiSupport = true; + device = "nodev"; + }; boot.loader.efi.canTouchEfiVariables = true; nixpkgs = { @@ -71,7 +75,7 @@ users.users.aner = { isNormalUser = true; description = "Aner Zakobar"; - extraGroups = ["networkmanager" "wheel" "audio" "video"]; + extraGroups = ["networkmanager" "wheel" "audio" "video" "seat" "input"]; packages = with pkgs; []; }; @@ -91,11 +95,29 @@ }; specialisation = { - hyprland = { + steam-big-picture = { configuration = { - home-manager.users.aner = {pkgs, ...}: { + environment.systemPackages = [pkgs.sway]; + environment.etc."sway-steam.conf".text = '' + output * bg #000000 solid_color + default_border none + default_floating_border none + exec ${pkgs.dbus}/bin/dbus-run-session -- ${pkgs.bash}/bin/bash -c "steam -tenfoot; ${pkgs.sway}/bin/swaymsg exit" + ''; + services.seatd.enable = true; + services.greetd.settings = lib.mkForce { + terminal.vt = 1; + default_session = { + command = "${pkgs.tuigreet}/bin/tuigreet --time --user-menu --cmd '/home/aner/.login.sh'"; + user = "greeter"; + }; + initial_session = { + command = "${pkgs.sway}/bin/sway --config /etc/sway-steam.conf"; + user = "aner"; + }; + }; + home-manager.users.aner = {lib, ...}: { azos.suites.exwm.enable = lib.mkForce false; - azos.suites.hyprland.enable = true; }; }; }; @@ -111,6 +133,7 @@ killall brightnessctl exfatprogs + gamescope ]; fonts.enableDefaultPackages = true; diff --git a/shells/defaultShell.nix b/shells/defaultShell.nix index d4cfc5a..0529f3a 100644 --- a/shells/defaultShell.nix +++ b/shells/defaultShell.nix @@ -13,6 +13,9 @@ pkgs.mkShell { "nix flake update --flake '.?submodules=1'") (pkgs.writeShellScriptBin "azos-beacon-build-image" - "nix build '.?submodules=1#nixosConfigurations.beacon.config.system.build.isoImage'") + "nix build '.?submodules=1#packages.x86_64-linux.beacon-image'") + (pkgs.writeShellScriptBin + "azos-beacon-remote-update" + "nixos-rebuild switch --flake '.?submodules=1#beacon' --target-host aner@192.168.1.200 --build-host aner@192.168.1.200 --sudo") ]; }