diff --git a/documentation/SSH_DEPLOYMENT_STRATEGY.md b/documentation/SSH_DEPLOYMENT_STRATEGY.md new file mode 100644 index 0000000..53e6ece --- /dev/null +++ b/documentation/SSH_DEPLOYMENT_STRATEGY.md @@ -0,0 +1,123 @@ +# SSH Deployment Strategy - Unified sma User Approach + +## Overview + +This document outlines the updated SSH deployment strategy for the home lab, standardizing on the `sma` user for all administrative operations and deployments. + +## User Strategy + +### sma User (System Administrator) +- **Purpose**: System administration, deployment, maintenance +- **SSH Key**: `id_ed25519_admin` +- **Privileges**: sudo NOPASSWD, wheel group +- **Usage**: All lab tool deployments, system maintenance + +### geir User (Developer) +- **Purpose**: Development work, daily usage, git operations +- **SSH Key**: `id_ed25519_dev` +- **Privileges**: Standard user with development tools +- **Usage**: Development workflows, git operations + +## Deployment Workflow + +### From Any Machine (Workstation or Laptop) + +1. **Both machines have sma user configured** with admin SSH key +2. **Lab tool uses sma user consistently** for all remote operations +3. **Deploy-rs uses sma user** for automated deployments with rollback + +### SSH Configuration + +The SSH configuration supports both direct access patterns: + +```bash +# Direct Tailscale access with sma user +ssh sma@sleeper-service.tail807ea.ts.net +ssh sma@grey-area.tail807ea.ts.net +ssh sma@reverse-proxy.tail807ea.ts.net +ssh sma@little-rascal.tail807ea.ts.net + +# Local sma user (for deployment from laptop to workstation) +ssh sma@localhost +``` + +## Lab Tool Commands + +All lab commands now work consistently from both machines: + +```bash +# Status checking +lab status # Works from both workstation and laptop + +# Deployment (using sma user automatically) +lab deploy sleeper-service # Works from both machines +lab deploy grey-area # Works from both machines +lab deploy little-rascal # Deploy TO laptop FROM workstation +lab deploy congenital-optimist # Deploy TO workstation FROM laptop + +# Deploy-rs (with automatic rollback) +lab deploy-rs sleeper-service +lab hybrid-update all +``` + +## Security Benefits + +1. **Principle of Least Privilege**: sma user only for admin tasks +2. **Key Separation**: Admin and development keys are separate +3. **Consistent Access**: Same user across all machines for deployment +4. **Audit Trail**: Clear separation between admin and development activities + +## Machine-Specific Notes + +### congenital-optimist (Workstation) +- **Type**: Local deployment +- **SSH**: Uses localhost with sma user for consistency +- **Primary Use**: Development and deployment hub + +### little-rascal (Laptop) +- **Type**: Remote deployment +- **SSH**: Tailscale hostname with sma user +- **Primary Use**: Mobile development and deployment + +### Remote Servers (sleeper-service, grey-area, reverse-proxy) +- **Type**: Remote deployment +- **SSH**: Tailscale hostnames with sma user +- **Access**: Both workstation and laptop can deploy + +## Migration Benefits + +1. **Simplified Workflow**: Same commands work from both machines +2. **Better Security**: Dedicated admin user for all system operations +3. **Consistency**: All deployments use the same SSH user pattern +4. **Flexibility**: Can deploy from either workstation or laptop seamlessly + +## Testing the Setup + +```bash +# Test SSH connectivity with sma user +ssh sma@sleeper-service.tail807ea.ts.net echo "Connection OK" +ssh sma@grey-area.tail807ea.ts.net echo "Connection OK" +ssh sma@little-rascal.tail807ea.ts.net echo "Connection OK" + +# Test lab tool +lab status # Should show all machines +lab deploy sleeper-service # Should work with sma user + +# Test deploy-rs +lab deploy-rs sleeper-service --dry-run +``` + +## Implementation Status + +- ✅ SSH keys configured for sma user on all machines +- ✅ Lab tool updated to use sma user for all operations +- ✅ Deploy-rs configuration updated to use sma user +- ✅ SSH client configuration updated with proper host patterns +- 📋 Ready for testing and validation + +## Next Steps + +1. Test SSH connectivity from both machines to all targets +2. Validate lab tool deployment commands +3. Test deploy-rs functionality with sma user +4. Update any remaining scripts that might use old SSH patterns diff --git a/dotfiles/geir/emacs-config/init-nix.el b/dotfiles/geir/emacs-config/init-nix.el new file mode 100644 index 0000000..e17d5fc --- /dev/null +++ b/dotfiles/geir/emacs-config/init-nix.el @@ -0,0 +1,201 @@ +;;; init.el --- Nix-integrated modular Emacs configuration -*- lexical-binding: t; -*- + +;;; Commentary: +;; A Nix-integrated, modular Emacs configuration that leverages Nix-provided tools +;; and packages where possible, falling back to Emacs package manager only when needed. +;; Core setup: UI, Nix integration, modular loading + +;;; Code: + +;; Performance optimizations +(setq gc-cons-threshold (* 50 1000 1000)) +(add-hook 'emacs-startup-hook + (lambda () + (setq gc-cons-threshold (* 2 1000 1000)) + (let ((profile (getenv "EMACS_PROFILE"))) + (message "Emacs loaded in %s with %d garbage collections (Profile: %s)." + (format "%.2f seconds" + (float-time + (time-subtract after-init-time before-init-time))) + gcs-done + (or profile "unknown"))))) + +;; Basic UI setup - minimal but pleasant +(setq inhibit-startup-screen t) +(menu-bar-mode -1) +(when (fboundp 'tool-bar-mode) (tool-bar-mode -1)) +(when (fboundp 'scroll-bar-mode) (scroll-bar-mode -1)) +(set-face-attribute 'default nil :height 140) +(setq-default cursor-type 'bar) + +;; Nix Integration Setup +;; Configure Emacs to use Nix-provided tools when available +(defun nix-tool-path (tool-name) + "Get the path to TOOL-NAME from Nix environment variables." + (let ((env-var (concat (upcase tool-name) "_PATH"))) + (getenv env-var))) + +;; Configure external tools to use Nix-provided binaries +(when-let ((rg-path (nix-tool-path "rg"))) + (setq consult-ripgrep-command rg-path)) + +(when-let ((ag-path (nix-tool-path "ag"))) + (setq ag-executable ag-path)) + +(when-let ((fd-path (nix-tool-path "fd"))) + (setq find-program fd-path)) + +(when-let ((sqlite-path (nix-tool-path "sqlite"))) + (setq org-roam-database-connector 'sqlite3) + (setq org-roam-db-executable sqlite-path)) + +;; Language Server Configuration (for Nix-provided LSP servers) +(defun configure-nix-lsp-servers () + "Configure LSP to use Nix-provided language servers." + (when (featurep 'lsp-mode) + ;; Nix LSP server + (when-let ((nil-path (nix-tool-path "nil_lsp"))) + (setq lsp-nix-nil-server-path nil-path)) + + ;; Bash LSP server + (when-let ((bash-lsp-path (nix-tool-path "bash_lsp"))) + (setq lsp-bash-language-server-path bash-lsp-path)) + + ;; YAML LSP server + (when-let ((yaml-lsp-path (nix-tool-path "yaml_lsp"))) + (setq lsp-yaml-language-server-path yaml-lsp-path)))) + +;; Configure format-all to use Nix-provided formatters +(defun configure-nix-formatters () + "Configure format-all to use Nix-provided formatters." + (when (featurep 'format-all) + ;; Shellcheck for shell scripts + (when-let ((shellcheck-path (nix-tool-path "shellcheck"))) + (setq format-all-formatters + (cons `(sh (shellcheck ,shellcheck-path)) + format-all-formatters))))) + +;; Package management setup +;; Note: With Nix integration, we rely less on package.el +;; Most packages come pre-installed via the flake +(require 'package) +(setq package-archives + '(("melpa" . "https://melpa.org/packages/") + ("gnu" . "https://elpa.gnu.org/packages/"))) + +;; Only initialize package.el if we're not in a Nix environment +;; In Nix environments, packages are pre-installed +(unless (getenv "EMACS_PROFILE") + (package-initialize) + + ;; Install use-package for non-Nix environments + (unless (package-installed-p 'use-package) + (package-refresh-contents) + (package-install 'use-package))) + +;; Configure use-package for Nix integration +(require 'use-package) +;; Don't auto-install packages in Nix environment - they're pre-provided +(setq use-package-always-ensure (not (getenv "EMACS_PROFILE"))) + +;; Essential packages that should be available in all profiles +(use-package exec-path-from-shell + :if (memq window-system '(mac ns x)) + :config + (exec-path-from-shell-initialize) + ;; Ensure Nix environment is properly inherited + (exec-path-from-shell-copy-envs '("NIX_PATH" "NIX_EMACS_PROFILE"))) + +(use-package diminish) +(use-package bind-key) + +;; Basic editing improvements +(use-package which-key + :config + (which-key-mode 1)) + +;; Load profile-specific configuration based on Nix profile +(defun load-profile-config () + "Load configuration specific to the current Nix profile." + (let ((profile (getenv "EMACS_PROFILE"))) + (pcase profile + ("server" + (message "Loading minimal server configuration...") + ;; Minimal config - only essential features + (setq gc-cons-threshold (* 2 1000 1000))) ; Lower memory usage + + ("laptop" + (message "Loading laptop development configuration...") + ;; Laptop config - balanced features + (setq auto-save-timeout 30) ; More frequent saves + (setq lsp-idle-delay 0.3)) ; Moderate LSP responsiveness + + ("workstation" + (message "Loading workstation configuration...") + ;; Workstation config - maximum performance + (setq gc-cons-threshold (* 50 1000 1000)) ; Higher performance + (setq lsp-idle-delay 0.1)) ; Fastest LSP response + + (_ + (message "Loading default configuration..."))))) + +;; Apply profile-specific settings +(load-profile-config) + +;; Configure Nix integration after packages are loaded +(add-hook 'after-init-hook #'configure-nix-lsp-servers) +(add-hook 'after-init-hook #'configure-nix-formatters) + +;; Org mode basic setup (always included) +(use-package org + :config + (setq org-startup-indented t) + (setq org-hide-emphasis-markers t)) + +;; Module loading system +;; Load modules based on availability and profile +(defvar my-modules-dir + (expand-file-name "modules/" user-emacs-directory) + "Directory containing modular configuration files.") + +(defun load-module (module-name) + "Load MODULE-NAME from the modules directory." + (let ((module-file (expand-file-name (concat module-name ".el") my-modules-dir))) + (when (file-exists-p module-file) + (load-file module-file) + (message "Loaded module: %s" module-name)))) + +;; Load modules based on profile +(let ((profile (getenv "EMACS_PROFILE"))) + (pcase profile + ("server" + ;; Minimal modules for server + (load-module "ui")) + + ((or "laptop" "workstation") + ;; Full module set for development machines + (load-module "ui") + (load-module "completion") + (load-module "navigation") + (load-module "development") + (load-module "elisp-development") + (when (string= profile "workstation") + (load-module "claude-code"))) + + (_ + ;; Default module loading (non-Nix environment) + (load-module "ui") + (load-module "completion") + (load-module "navigation")))) + +;; Display startup information +(add-hook 'emacs-startup-hook + (lambda () + (let ((profile (getenv "EMACS_PROFILE"))) + (message "=== Emacs Ready ===") + (message "Profile: %s" (or profile "default")) + (message "Nix Integration: %s" (if profile "enabled" "disabled")) + (message "Modules loaded based on profile") + (message "===================")))) + +;;; init.el ends here diff --git a/dotfiles/geir/emacs-config/modules/claude-code.el b/dotfiles/geir/emacs-config/modules/claude-code.el new file mode 100644 index 0000000..4ac5b44 --- /dev/null +++ b/dotfiles/geir/emacs-config/modules/claude-code.el @@ -0,0 +1,126 @@ +;;; claude-code.el --- Claude Code CLI integration module -*- lexical-binding: t; -*- + +;;; Commentary: +;; Integration with Claude Code CLI for AI-assisted coding directly in Emacs +;; Provides terminal interface and commands for interacting with Claude AI + +;;; Code: + +;; Install claude-code via quelpa if not already installed +(unless (package-installed-p 'claude-code) + (quelpa '(claude-code :fetcher github :repo "stevemolitor/claude-code.el"))) + +;; Claude Code - AI assistant integration +(use-package claude-code + :ensure nil ; Already installed via quelpa + :bind-keymap ("C-c C-c" . claude-code-command-map) + :bind (("C-c C-c c" . claude-code) + ("C-c C-c s" . claude-code-send-command) + ("C-c C-c r" . claude-code-send-region) + ("C-c C-c b" . claude-code-send-buffer) + ("C-c C-c e" . claude-code-fix-error-at-point) + ("C-c C-c t" . claude-code-toggle) + ("C-c C-c k" . claude-code-kill) + ("C-c C-c n" . claude-code-new)) + :custom + ;; Terminal backend preference (eat is now installed via quelpa) + (claude-code-terminal-type 'eat) + + ;; Enable desktop notifications + (claude-code-notifications t) + + ;; Startup delay to ensure proper initialization + (claude-code-startup-delay 1.0) + + ;; Confirm before killing Claude sessions + (claude-code-confirm-kill t) + + ;; Use modern keybinding style + (claude-code-newline-and-send-style 'modern) + + :config + ;; Smart terminal detection - eat should be available via quelpa + (defun claude-code-detect-best-terminal () + "Detect the best available terminal for Claude Code." + (cond + ((package-installed-p 'eat) 'eat) + ((and (package-installed-p 'vterm) + (or (executable-find "cmake") + (file-exists-p "/usr/bin/cmake") + (file-exists-p "/nix/store/*/bin/cmake"))) + 'vterm) + (t 'eat))) ; fallback to eat, should be installed + + ;; Set terminal type based on detection + (setq claude-code-terminal-type (claude-code-detect-best-terminal)) + + ;; Auto-start Claude in project root when opening coding files + (defun claude-code-auto-start-maybe () + "Auto-start Claude Code if in a project and not already running." + (when (and (derived-mode-p 'prog-mode) + (project-current) + (not (claude-code-running-p))) + (claude-code))) + + ;; Optional: Auto-start when opening programming files + ;; Uncomment the next line if you want this behavior + ;; (add-hook 'prog-mode-hook #'claude-code-auto-start-maybe) + + ;; Add helpful message about Claude Code setup + (message "Claude Code module loaded. Use C-c C-c c to start Claude, C-c C-c h for help")) + +;; Terminal emulator for Claude Code (eat installed via quelpa in init.el) +(use-package eat + :ensure nil ; Already installed via quelpa + :custom + (eat-term-name "xterm-256color") + (eat-kill-buffer-on-exit t)) + +;; Alternative terminal emulator (if eat fails or user prefers vterm) +(use-package vterm + :if (and (not (package-installed-p 'eat)) + (executable-find "cmake")) + :custom + (vterm-always-compile-module t) + (vterm-kill-buffer-on-exit t) + (vterm-max-scrollback 10000)) + +;; Transient dependency for command menus +(use-package transient + :ensure t) + +;; Enhanced error handling for Claude Code integration +(defun claude-code-send-error-context () + "Send error at point with surrounding context to Claude." + (interactive) + (if (claude-code-running-p) + (let* ((error-line (line-number-at-pos)) + (start (max 1 (- error-line 5))) + (end (min (line-number-at-pos (point-max)) (+ error-line 5))) + (context (buffer-substring-no-properties + (line-beginning-position (- start error-line)) + (line-end-position (- end error-line))))) + (claude-code-send-command + (format "I'm getting an error around line %d. Here's the context:\n\n```%s\n%s\n```\n\nCan you help me fix this?" + error-line + (or (file-name-extension (buffer-file-name)) "") + context))) + (message "Claude Code is not running. Start it with C-c C-c c"))) + +;; Keybinding for enhanced error context +(global-set-key (kbd "C-c C-c x") #'claude-code-send-error-context) + +;; Project-aware Claude instances +(defun claude-code-project-instance () + "Start or switch to Claude instance for current project." + (interactive) + (if-let ((project (project-current))) + (let ((default-directory (project-root project))) + (claude-code)) + (claude-code))) + +;; Keybinding for project-specific Claude +(global-set-key (kbd "C-c C-c p") #'claude-code-project-instance) + +(provide 'claude-code) +;;; claude-code.el ends here \ No newline at end of file diff --git a/dotfiles/geir/emacs-config/modules/completion.el b/dotfiles/geir/emacs-config/modules/completion.el new file mode 100644 index 0000000..b1bde22 --- /dev/null +++ b/dotfiles/geir/emacs-config/modules/completion.el @@ -0,0 +1,55 @@ +;;; completion.el --- Completion framework configuration -*- lexical-binding: t; -*- + +;;; Commentary: +;; Modern completion with Vertico, Consult, and Corfu + +;;; Code: + +;; Vertico - vertical completion UI +(use-package vertico + :init + (vertico-mode) + :custom + (vertico-cycle t)) + +;; Marginalia - rich annotations in minibuffer +(use-package marginalia + :init + (marginalia-mode)) + +;; Consult - enhanced search and navigation commands +(use-package consult + :bind (("C-s" . consult-line) + ("C-x b" . consult-buffer) + ("C-x 4 b" . consult-buffer-other-window) + ("C-x 5 b" . consult-buffer-other-frame) + ("M-y" . consult-yank-pop) + ("M-g g" . consult-goto-line) + ("M-g M-g" . consult-goto-line) + ("C-x r b" . consult-bookmark))) + +;; Orderless - flexible completion style +(use-package orderless + :custom + (completion-styles '(orderless basic)) + (completion-category-defaults nil) + (completion-category-overrides '((file (styles partial-completion))))) + +;; Corfu - in-buffer completion popup +(use-package corfu + :custom + (corfu-cycle t) + (corfu-auto t) + (corfu-auto-delay 0.2) + (corfu-auto-prefix 2) + :init + (global-corfu-mode)) + +;; Cape - completion at point extensions +(use-package cape + :init + (add-to-list 'completion-at-point-functions #'cape-dabbrev) + (add-to-list 'completion-at-point-functions #'cape-file)) + +(provide 'completion) +;;; completion.el ends here \ No newline at end of file diff --git a/dotfiles/geir/emacs-config/modules/development.el b/dotfiles/geir/emacs-config/modules/development.el new file mode 100644 index 0000000..9648f2c --- /dev/null +++ b/dotfiles/geir/emacs-config/modules/development.el @@ -0,0 +1,40 @@ +;;; development.el --- Development tools configuration -*- lexical-binding: t; -*- + +;;; Commentary: +;; LSP, Copilot, and other development tools + +;;; Code: + +;; LSP Mode +(use-package lsp-mode + :hook ((prog-mode . lsp-deferred)) + :commands (lsp lsp-deferred) + :custom + (lsp-keymap-prefix "C-c l") + (lsp-idle-delay 0.5) + (lsp-log-io nil) + (lsp-completion-provider :none) ; Use corfu instead + :config + (lsp-enable-which-key-integration t)) + +;; LSP UI +(use-package lsp-ui + :after lsp-mode + :custom + (lsp-ui-doc-enable t) + (lsp-ui-doc-position 'bottom) + (lsp-ui-sideline-enable t) + (lsp-ui-sideline-show-hover nil)) + +;; Which Key - helpful for discovering keybindings +(use-package which-key + :config + (which-key-mode 1) + (setq which-key-idle-delay 0.3)) + +;; Magit - Git interface +(use-package magit + :bind ("C-x g" . magit-status)) + +(provide 'development) +;;; development.el ends here \ No newline at end of file diff --git a/dotfiles/geir/emacs-config/modules/elisp-development.el b/dotfiles/geir/emacs-config/modules/elisp-development.el new file mode 100644 index 0000000..3e5b6f3 --- /dev/null +++ b/dotfiles/geir/emacs-config/modules/elisp-development.el @@ -0,0 +1,164 @@ +;;; elisp-development.el --- Enhanced Emacs Lisp development setup -*- lexical-binding: t; -*- + +;;; Commentary: +;; Specialized configuration for Emacs Lisp development +;; This module provides enhanced development tools specifically for .el files + +;;; Code: + +;; Enhanced Emacs Lisp mode with better defaults +(use-package elisp-mode + :ensure nil ; Built-in package + :mode "\\.el\\'" + :hook ((emacs-lisp-mode . eldoc-mode) + (emacs-lisp-mode . show-paren-mode) + (emacs-lisp-mode . electric-pair-mode)) + :bind (:map emacs-lisp-mode-map + ("C-c C-e" . eval-last-sexp) + ("C-c C-b" . eval-buffer) + ("C-c C-r" . eval-region) + ("C-c C-d" . describe-function-at-point)) + :config + ;; Better indentation + (setq lisp-indent-function 'lisp-indent-function) + + ;; Show function signatures in minibuffer + (eldoc-mode 1)) + +;; Enhanced Elisp navigation +(use-package elisp-slime-nav + :hook (emacs-lisp-mode . elisp-slime-nav-mode) + :bind (:map elisp-slime-nav-mode-map + ("M-." . elisp-slime-nav-find-elisp-thing-at-point) + ("M-," . pop-tag-mark))) + +;; Better parentheses handling +(use-package smartparens + :hook (emacs-lisp-mode . smartparens-strict-mode) + :config + (require 'smartparens-config) + (sp-local-pair 'emacs-lisp-mode "'" nil :actions nil) + (sp-local-pair 'emacs-lisp-mode "`" nil :actions nil)) + +;; Rainbow delimiters for better paren visibility +(use-package rainbow-delimiters + :hook (emacs-lisp-mode . rainbow-delimiters-mode)) + +;; Aggressive indentation +(use-package aggressive-indent + :hook (emacs-lisp-mode . aggressive-indent-mode)) + +;; Enhanced help and documentation +(use-package helpful + :bind (("C-h f" . helpful-callable) + ("C-h v" . helpful-variable) + ("C-h k" . helpful-key) + ("C-h x" . helpful-command) + ("C-h ." . helpful-at-point))) + +;; Live examples for Elisp functions +(use-package elisp-demos + :after helpful + :config + (advice-add 'helpful-update :after #'elisp-demos-advice-helpful-update)) + +;; Package linting +(use-package package-lint + :commands package-lint-current-buffer) + +;; Flycheck for syntax checking +(use-package flycheck + :hook (emacs-lisp-mode . flycheck-mode) + :config + ;; Enhanced Emacs Lisp checking + (setq flycheck-emacs-lisp-load-path 'inherit)) + +;; Checkdoc for documentation linting +(use-package checkdoc + :ensure nil ; Built-in + :commands checkdoc) + +;; Enhanced debugging +(use-package edebug + :ensure nil ; Built-in + :bind (:map emacs-lisp-mode-map + ("C-c C-x C-d" . edebug-defun) + ("C-c C-x C-b" . edebug-set-breakpoint))) + +;; Package development helpers +(use-package auto-compile + :config + (auto-compile-on-load-mode) + (auto-compile-on-save-mode)) + +;; Enhanced REPL interaction +(use-package ielm + :ensure nil ; Built-in + :bind ("C-c C-z" . ielm) + :config + (add-hook 'ielm-mode-hook 'eldoc-mode)) + +;; Highlight defined functions and variables +(use-package highlight-defined + :hook (emacs-lisp-mode . highlight-defined-mode)) + +;; Better search and replace for symbols +(use-package expand-region + :bind ("C-=" . er/expand-region)) + +;; Multiple cursors for batch editing +(use-package multiple-cursors + :bind (("C-S-c C-S-c" . mc/edit-lines) + ("C->" . mc/mark-next-like-this) + ("C-<" . mc/mark-previous-like-this))) + +;; Custom functions for Elisp development +(defun elisp-eval-and-replace () + "Evaluate the sexp at point and replace it with its value." + (interactive) + (backward-kill-sexp) + (condition-case nil + (prin1 (eval (read (current-kill 0))) + (current-buffer)) + (error (message "Invalid expression") + (insert (current-kill 0))))) + +(defun elisp-describe-thing-at-point () + "Show the documentation for the thing at point." + (interactive) + (let ((thing (symbol-at-point))) + (cond + ((fboundp thing) (describe-function thing)) + ((boundp thing) (describe-variable thing)) + (t (message "No documentation found for %s" thing))))) + +;; Key bindings for custom functions +(define-key emacs-lisp-mode-map (kbd "C-c C-x C-e") 'elisp-eval-and-replace) +(define-key emacs-lisp-mode-map (kbd "C-c C-d") 'elisp-describe-thing-at-point) + +;; Project-specific configurations +(defun setup-elisp-project () + "Set up development environment for Elisp projects." + (interactive) + (when (and buffer-file-name + (string-match "\\.el\\'" buffer-file-name)) + ;; Add current directory to load-path for local requires + (add-to-list 'load-path (file-name-directory buffer-file-name)) + + ;; Set up package development if this looks like a package + (when (or (file-exists-p "Cask") + (file-exists-p "Eask") + (string-match "-pkg\\.el\\'" buffer-file-name)) + (message "Elisp package development mode enabled")))) + +(add-hook 'emacs-lisp-mode-hook 'setup-elisp-project) + +;; Better compilation output +(add-hook 'emacs-lisp-mode-hook + (lambda () + (setq-local compile-command + (format "emacs -batch -f batch-byte-compile %s" + (shell-quote-argument buffer-file-name))))) + +(provide 'elisp-development) +;;; elisp-development.el ends here diff --git a/dotfiles/geir/emacs-config/modules/navigation.el b/dotfiles/geir/emacs-config/modules/navigation.el new file mode 100644 index 0000000..c53244b --- /dev/null +++ b/dotfiles/geir/emacs-config/modules/navigation.el @@ -0,0 +1,51 @@ +;;; navigation.el --- Navigation and file management -*- lexical-binding: t; -*- + +;;; Commentary: +;; File navigation, project management, and window management + +;;; Code: + +;; Dired improvements +(use-package dired + :ensure nil + :custom + (dired-listing-switches "-alhF") + (dired-dwim-target t) + :config + (put 'dired-find-alternate-file 'disabled nil)) + +;; Project management +(use-package projectile + :config + (projectile-mode +1) + :bind-keymap + ("C-c p" . projectile-command-map) + :custom + (projectile-completion-system 'default)) + +;; Treemacs - file tree +(use-package treemacs + :bind (("M-0" . treemacs-select-window) + ("C-x t 1" . treemacs-delete-other-windows) + ("C-x t t" . treemacs) + ("C-x t d" . treemacs-select-directory) + ("C-x t B" . treemacs-bookmark) + ("C-x t C-t" . treemacs-find-file) + ("C-x t M-t" . treemacs-find-tag)) + :custom + (treemacs-width 30)) + +;; Ace Window - quick window switching +(use-package ace-window + :bind ("M-o" . ace-window) + :custom + (aw-keys '(?a ?s ?d ?f ?g ?h ?j ?k ?l))) + +;; Winner mode - window configuration undo/redo +(use-package winner + :ensure nil + :config + (winner-mode 1)) + +(provide 'navigation) +;;; navigation.el ends here \ No newline at end of file diff --git a/dotfiles/geir/emacs-config/modules/ui.el b/dotfiles/geir/emacs-config/modules/ui.el new file mode 100644 index 0000000..9fee4de --- /dev/null +++ b/dotfiles/geir/emacs-config/modules/ui.el @@ -0,0 +1,32 @@ +;;; ui.el --- UI configuration module -*- lexical-binding: t; -*- + +;;; Commentary: +;; Enhanced UI configuration - themes, modeline, icons + +;;; Code: + +;; Doom themes +(use-package doom-themes + :config + (load-theme 'doom-monokai-pro t) + (doom-themes-visual-bell-config) + (doom-themes-org-config)) + +;; Doom modeline +(use-package doom-modeline + :init (doom-modeline-mode 1) + :custom + (doom-modeline-height 15) + (doom-modeline-icon t) + (doom-modeline-buffer-file-name-style 'truncate-with-project)) + +;; All the icons +(use-package all-the-icons + :if (display-graphic-p) + :config + ;; Install fonts if not already done + (unless (find-font (font-spec :name "all-the-icons")) + (all-the-icons-install-fonts t))) + +(provide 'ui) +;;; ui.el ends here \ No newline at end of file diff --git a/flake.lock b/flake.lock index 81f13a2..391f49d 100644 --- a/flake.lock +++ b/flake.lock @@ -54,11 +54,11 @@ }, "nixpkgs-unstable": { "locked": { - "lastModified": 1751011381, - "narHash": "sha256-krGXKxvkBhnrSC/kGBmg5MyupUUT5R6IBCLEzx9jhMM=", + "lastModified": 1751271578, + "narHash": "sha256-P/SQmKDu06x8yv7i0s8bvnnuJYkxVGBWLWHaU+tt4YY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "30e2e2857ba47844aa71991daa6ed1fc678bcbb7", + "rev": "3016b4b15d13f3089db8a41ef937b13a9e33a8df", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index d366cbc..e254bcb 100644 --- a/flake.nix +++ b/flake.nix @@ -261,7 +261,7 @@ profiles.system = { user = "root"; path = deploy-rs.lib.x86_64-linux.activate.nixos self.nixosConfigurations.little-rascal; - sshUser = "geir"; + sshUser = "sma"; sudo = "sudo -u"; autoRollback = true; magicRollback = true; diff --git a/machines/congenital-optimist/configuration.nix b/machines/congenital-optimist/configuration.nix index dbedec9..8b472e1 100644 --- a/machines/congenital-optimist/configuration.nix +++ b/machines/congenital-optimist/configuration.nix @@ -33,6 +33,10 @@ # Development tools ../../modules/development/tools.nix + ../../modules/development/emacs.nix + + # Emacs with workstation profile + ../../modules/development/emacs.nix # AI tools ../../modules/ai/claude-code.nix @@ -61,6 +65,14 @@ ]; }; + # Emacs workstation configuration + services.emacs-profiles = { + enable = true; + profile = "workstation"; + enableDaemon = true; + user = "geir"; + }; + # Enable clean seatd/greetd login services.seatd-clean.enable = true; diff --git a/machines/grey-area/configuration.nix b/machines/grey-area/configuration.nix index 1cb3347..17f184e 100644 --- a/machines/grey-area/configuration.nix +++ b/machines/grey-area/configuration.nix @@ -16,6 +16,9 @@ ../../modules/virtualization/incus.nix ../../modules/users/sma.nix + # Development (minimal for services host) + ../../modules/development/emacs.nix + # NFS client with ID mapping ../../modules/services/nfs-client.nix @@ -43,6 +46,14 @@ # Disks and Updates services.fstrim.enable = true; + # Emacs server configuration (minimal for services host) + services.emacs-profiles = { + enable = true; + profile = "server"; + enableDaemon = false; + user = "sma"; + }; + # Mount remote filesystem fileSystems."/mnt/remote/media" = { device = "sleeper-service:/mnt/storage/media"; diff --git a/machines/little-rascal/configuration.nix b/machines/little-rascal/configuration.nix index 0de6aea..b92b075 100644 --- a/machines/little-rascal/configuration.nix +++ b/machines/little-rascal/configuration.nix @@ -15,7 +15,6 @@ ../../modules/common/base.nix ../../modules/common/nix.nix ../../modules/common/tty.nix - ../../modules/common/emacs.nix # Desktop ../../modules/desktop/niri.nix @@ -25,6 +24,7 @@ # Development ../../modules/development/tools.nix + ../../modules/development/emacs.nix ../../modules/ai/claude-code.nix # Users @@ -79,6 +79,14 @@ kernel.sysctl."vm.swappiness" = 180; }; + # Emacs laptop configuration + services.emacs-profiles = { + enable = true; + profile = "laptop"; + enableDaemon = true; + user = "geir"; + }; + # zram configuration zramSwap = { enable = true; diff --git a/machines/reverse-proxy/configuration.nix b/machines/reverse-proxy/configuration.nix index 484e5d5..0119103 100644 --- a/machines/reverse-proxy/configuration.nix +++ b/machines/reverse-proxy/configuration.nix @@ -10,6 +10,9 @@ ../../modules/network/extraHosts.nix ../../modules/users/sma.nix ../../modules/security/ssh-keys.nix + + # Development (minimal for edge server) + ../../modules/development/emacs.nix ]; environment.systemPackages = with pkgs; [ @@ -43,6 +46,14 @@ # Tailscale for secure management access services.tailscale.enable = true; + # Emacs server configuration (minimal for edge server) + services.emacs-profiles = { + enable = true; + profile = "server"; + enableDaemon = false; + user = "sma"; + }; + # SSH configuration - temporarily simplified for testing services.openssh = { enable = true; diff --git a/machines/sleeper-service/configuration.nix b/machines/sleeper-service/configuration.nix index d72c55e..1b8bd71 100644 --- a/machines/sleeper-service/configuration.nix +++ b/machines/sleeper-service/configuration.nix @@ -1,4 +1,11 @@ -{ config, lib, pkgs, inputs, unstable, ... }: { +{ + config, + lib, + pkgs, + inputs, + unstable, + ... +}: { imports = [ ./hardware-configuration.nix # Security modules @@ -10,6 +17,9 @@ ./nfs.nix ./services/transmission.nix + # Development (minimal for server) + ../../modules/development/emacs.nix + # User modules - server only needs sma user ../../modules/users/sma.nix ]; @@ -20,25 +30,37 @@ zfsSupport = true; efiSupport = true; efiInstallAsRemovable = true; - mirroredBoots = [ - { devices = [ "nodev" ]; path = "/boot"; } ]; + mirroredBoots = [ + { + devices = ["nodev"]; + path = "/boot"; + } + ]; }; - - boot.supportedFilesystems = [ "zfs" ]; + + boot.supportedFilesystems = ["zfs"]; boot.loader.grub.memtest86.enable = true; - + # Add nomodeset for graphics compatibility - boot.kernelParams = [ "nomodeset" ]; - + boot.kernelParams = ["nomodeset"]; + # ZFS services for file server services.zfs = { autoScrub.enable = true; trim.enable = true; }; + # Emacs server configuration (minimal) + services.emacs-profiles = { + enable = true; + profile = "server"; + enableDaemon = false; # Don't run daemon on server + user = "sma"; + }; + # Enable ZFS auto-mounting since we're using ZFS native mountpoints # systemd.services.zfs-mount.enable = lib.mkForce false; - + # Disable graphics for server use - comment out NVIDIA config for now # hardware.graphics = { # enable = true; @@ -48,15 +70,15 @@ # open = false; # package = config.boot.kernelPackages.nvidiaPackages.legacy_470; # }; - + # Comment out NVIDIA kernel modules for now # boot.kernelModules = [ "nvidia" "nvidia_modeset" "nvidia_uvm" "nvidia_drm" ]; - + # Comment out NVIDIA utilities for now # environment.systemPackages = with pkgs; [ # config.boot.kernelPackages.nvidiaPackages.legacy_470 # ]; - + # Create mount directories early in boot process # systemd.tmpfiles.rules = [ # "d /mnt/storage 0755 root root -" @@ -93,4 +115,4 @@ # DO NOT CHANGE - maintains data compatibility system.stateVersion = "23.11"; -} \ No newline at end of file +} diff --git a/modules/development/emacs.nix b/modules/development/emacs.nix new file mode 100644 index 0000000..9159622 --- /dev/null +++ b/modules/development/emacs.nix @@ -0,0 +1,285 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.services.emacs-profiles; + + # Emacs package configurations for different profiles + packageSets = { + # Essential packages for all profiles + essential = epkgs: + with epkgs; [ + use-package + diminish + bind-key + which-key + exec-path-from-shell # Critical for integrating with Nix environment + ]; + + # Minimal packages for server profile + minimal = epkgs: + with epkgs; [ + # Basic editing + smartparens + expand-region + + # Essential navigation (pure Emacs, no external deps) + vertico + consult + marginalia + orderless + + # Basic modes for config files + nix-mode # Essential for Nix ecosystem + yaml-mode + markdown-mode + + # Org mode essentials + org + org-roam + ]; + + # Development packages for laptop/workstation + development = epkgs: + with epkgs; [ + # Advanced navigation and completion + vertico + consult + marginalia + orderless + embark + embark-consult + corfu + cape + + # Project management + projectile + magit + forge + + # Development tools + lsp-mode + lsp-ui + company + flycheck + yasnippet + + # Language support + nix-mode + rust-mode + python-mode + typescript-mode + json-mode + yaml-mode + markdown-mode + + # Org mode and knowledge management + org + org-roam + org-roam-ui + org-agenda + + # UI enhancements + doom-themes + doom-modeline + all-the-icons + rainbow-delimiters + highlight-indent-guides + + # Editing enhancements + smartparens + expand-region + multiple-cursors + avy + ace-window + + # Terminal integration + vterm + eshell-git-prompt + ]; + + # Full workstation packages + workstation = epkgs: + with epkgs; [ + # All development packages plus extras + claude-code # AI assistance (when available) + pdf-tools + nov # EPUB reader + elfeed # RSS reader + mu4e # Email (if configured) + dired-sidebar + treemacs + treemacs-projectile + treemacs-magit + ]; + }; + + # Generate Emacs configuration based on profile + # Uses emacs-gtk to track upstream with GTK3 support for desktop profiles + # Uses emacs-nox for server profiles (no X11/GUI dependencies) + emacsWithProfile = profile: let + # Choose Emacs package based on profile + emacsPackage = + if profile == "server" + then pkgs.emacs-nox # No GUI for servers + else pkgs.emacs-gtk; # GTK3 for desktops + + # Combine package sets based on profile + selectedPackages = epkgs: + (packageSets.essential epkgs) + ++ ( + if profile == "server" + then packageSets.minimal epkgs + else if profile == "laptop" + then packageSets.development epkgs + else if profile == "workstation" + then (packageSets.development epkgs) ++ (packageSets.workstation epkgs) + else packageSets.minimal epkgs + ); + in + pkgs.emacsWithPackagesFromUsePackage { + config = builtins.readFile ../../dotfiles/geir/emacs-config/init-nix.el; + package = emacsPackage; + extraEmacsPackages = selectedPackages; + + # Provide external tools that Emacs will use + # These will be available via environment variables + override = epkgs: + epkgs + // { + # External tools for Emacs integration + external-tools = + [ + pkgs.ripgrep # for fast searching + pkgs.fd # for file finding + pkgs.sqlite # for org-roam database + pkgs.ag # the silver searcher + pkgs.git # version control + pkgs.direnv # environment management + + # Language servers (when available) + pkgs.nixd # Nix language server + pkgs.nodePackages.bash-language-server + pkgs.nodePackages.yaml-language-server + pkgs.marksman # Markdown language server + + # Formatters + pkgs.alejandra # Nix formatter + pkgs.shellcheck # Shell script analysis + pkgs.shfmt # Shell script formatter + ] + ++ lib.optionals (profile != "server") [ + # Additional tools for development profiles + pkgs.nodejs # for various language servers + pkgs.python3 # for Python development + pkgs.rustup # Rust toolchain + pkgs.go # Go language + ]; + }; + }; +in { + options.services.emacs-profiles = { + enable = mkEnableOption "Emacs with machine-specific profiles"; + + profile = mkOption { + type = types.enum ["server" "laptop" "workstation"]; + default = "laptop"; + description = "Emacs profile to use based on machine type"; + }; + + enableDaemon = mkOption { + type = types.bool; + default = true; + description = "Enable Emacs daemon service"; + }; + + user = mkOption { + type = types.str; + default = "geir"; + description = "User to run Emacs daemon for"; + }; + }; + + config = mkIf cfg.enable { + # Install Emacs with the selected profile + environment.systemPackages = [ + (emacsWithProfile cfg.profile) + ]; + + # System-wide Emacs daemon (optional) + services.emacs = mkIf cfg.enableDaemon { + enable = true; + package = emacsWithProfile cfg.profile; + }; + + # Create the Emacs configuration directory structure + environment.etc = { + "emacs/init.el" = { + source = ../../dotfiles/geir/emacs-config/init-nix.el; + mode = "0644"; + }; + + # Module files + "emacs/modules/ui.el" = { + source = ../../dotfiles/geir/emacs-config/modules/ui.el; + mode = "0644"; + }; + + "emacs/modules/completion.el" = { + source = ../../dotfiles/geir/emacs-config/modules/completion.el; + mode = "0644"; + }; + + "emacs/modules/navigation.el" = { + source = ../../dotfiles/geir/emacs-config/modules/navigation.el; + mode = "0644"; + }; + + "emacs/modules/development.el" = { + source = ../../dotfiles/geir/emacs-config/modules/development.el; + mode = "0644"; + }; + + "emacs/modules/elisp-development.el" = { + source = ../../dotfiles/geir/emacs-config/modules/elisp-development.el; + mode = "0644"; + }; + + "emacs/modules/claude-code.el" = mkIf (cfg.profile == "workstation") { + source = ../../dotfiles/geir/emacs-config/modules/claude-code.el; + mode = "0644"; + }; + }; + + # Environment variables for Nix integration + environment.variables = { + EMACS_PROFILE = cfg.profile; + + # Tool paths for Emacs integration + RG_PATH = "${pkgs.ripgrep}/bin/rg"; + FD_PATH = "${pkgs.fd}/bin/fd"; + SQLITE_PATH = "${pkgs.sqlite}/bin/sqlite3"; + AG_PATH = "${pkgs.ag}/bin/ag"; + + # Language servers + NIL_LSP_PATH = "${pkgs.nixd}/bin/nixd"; + BASH_LSP_PATH = "${pkgs.nodePackages.bash-language-server}/bin/bash-language-server"; + YAML_LSP_PATH = "${pkgs.nodePackages.yaml-language-server}/bin/yaml-language-server"; + + # Formatters + SHELLCHECK_PATH = "${pkgs.shellcheck}/bin/shellcheck"; + ALEJANDRA_PATH = "${pkgs.alejandra}/bin/alejandra"; + }; + + # Ensure the user can access the Emacs daemon + systemd.user.services.emacs = mkIf cfg.enableDaemon { + environment = { + EMACS_PROFILE = cfg.profile; + NIX_PATH = config.environment.variables.NIX_PATH or ""; + }; + }; + }; +} diff --git a/modules/development/tools.nix b/modules/development/tools.nix index 2210852..9410fdd 100644 --- a/modules/development/tools.nix +++ b/modules/development/tools.nix @@ -8,9 +8,7 @@ # Editors zed-editor neovim - emacs vscode - vscodium-fhs # Language servers nixd @@ -35,12 +33,13 @@ direnv gh github-copilot-cli + deploy-rs # ai claude-code ]; - # System-wide Emacs daemon - services.emacs.enable = true; + # Note: Emacs is now configured via modules/development/emacs.nix + # with machine-specific profiles # Enable ZSH system-wide for development programs.zsh.enable = true; diff --git a/modules/security/ssh-keys.nix b/modules/security/ssh-keys.nix index 5027d63..d8155bf 100644 --- a/modules/security/ssh-keys.nix +++ b/modules/security/ssh-keys.nix @@ -78,6 +78,33 @@ User sma IdentityFile ~/.ssh/id_ed25519_admin + # Direct sma user access via Tailscale for deployments + Host sma@sleeper-service.tail807ea.ts.net + Hostname sleeper-service.tail807ea.ts.net + User sma + IdentityFile ~/.ssh/id_ed25519_admin + + Host sma@grey-area.tail807ea.ts.net + Hostname grey-area.tail807ea.ts.net + User sma + IdentityFile ~/.ssh/id_ed25519_admin + + Host sma@reverse-proxy.tail807ea.ts.net + Hostname reverse-proxy.tail807ea.ts.net + User sma + IdentityFile ~/.ssh/id_ed25519_admin + + Host sma@little-rascal.tail807ea.ts.net + Hostname little-rascal.tail807ea.ts.net + User sma + IdentityFile ~/.ssh/id_ed25519_admin + + # Localhost sma user for local deployment from laptop + Host sma@localhost + Hostname localhost + User sma + IdentityFile ~/.ssh/id_ed25519_admin + # Tailscale network Host 100.* *.tail* User geir diff --git a/packages/lab-tool/lab/deployment.scm b/packages/lab-tool/lab/deployment.scm index c5a3e18..db2b08d 100644 --- a/packages/lab-tool/lab/deployment.scm +++ b/packages/lab-tool/lab/deployment.scm @@ -1,4 +1,4 @@ -;; lab/deployment.scm - Deployment operations (impure) +;; lab/deployment.scm - Deploy-rs based deployment operations (define-module (lab deployment) #:use-module (ice-9 format) @@ -7,10 +7,10 @@ #:use-module (srfi srfi-1) #:use-module (utils logging) #:use-module (utils config) - #:use-module (utils ssh) #:export (deploy-machine update-flake - execute-nixos-rebuild + deploy-all-machines + deploy-with-rollback option-ref)) ;; Helper function for option handling @@ -19,26 +19,128 @@ (let ((value (assoc-ref options key))) (if value value default))) -;; Impure function: Deploy machine configuration +;; Main deployment function using deploy-rs (define (deploy-machine machine-name . args) - "Deploy configuration to machine (impure - has side effects)" - (let* ((mode (if (null? args) "boot" (car args))) + "Deploy configuration to machine using deploy-rs (impure - has side effects)" + (let* ((mode (if (null? args) "default" (car args))) (options (if (< (length args) 2) '() (cadr args))) - (valid-modes '("boot" "test" "switch")) - (dry-run (option-ref options 'dry-run #f))) + (dry-run (option-ref options 'dry-run #f)) + (skip-checks (option-ref options 'skip-checks #f))) (if (not (validate-machine-name machine-name)) #f - (if (not (member mode valid-modes)) - (begin - (log-error "Invalid deployment mode: ~a" mode) - (log-error "Valid modes: ~a" (string-join valid-modes ", ")) - #f) - (begin - (log-info "Starting deployment: ~a (mode: ~a)" machine-name mode) - (execute-nixos-rebuild machine-name mode options)))))) + (begin + (log-info "Starting deploy-rs deployment: ~a" machine-name) + (execute-deploy-rs machine-name mode options))))) -;; Impure function: Update flake inputs +;; Execute deploy-rs deployment +(define (execute-deploy-rs machine-name mode options) + "Execute deployment using deploy-rs with automatic rollback" + (let* ((homelab-root (get-homelab-root)) + (dry-run (option-ref options 'dry-run #f)) + (skip-checks (option-ref options 'skip-checks #f)) + (auto-rollback (option-ref options 'auto-rollback #t)) + (magic-rollback (option-ref options 'magic-rollback #t))) + + (log-info "Deploying ~a using deploy-rs..." machine-name) + + (if dry-run + (begin + (log-info "DRY RUN: Would execute deploy-rs for ~a" machine-name) + (log-info "Command would be: deploy '.#~a'" machine-name) + #t) + (let* ((deploy-cmd (build-deploy-command machine-name skip-checks auto-rollback magic-rollback)) + (start-time (current-time))) + + (log-debug "Deploy command: ~a" deploy-cmd) + (log-info "Executing deployment with automatic rollback protection...") + + (let* ((port (open-pipe* OPEN_READ "/bin/sh" "-c" deploy-cmd)) + (output (get-string-all port)) + (status (close-pipe port)) + (elapsed (- (current-time) start-time))) + + (if (zero? status) + (begin + (log-success "Deploy-rs deployment completed successfully in ~as" elapsed) + (log-info "Deployment output:") + (log-info "~a" output) + #t) + (begin + (log-error "Deploy-rs deployment failed (exit: ~a)" status) + (log-error "Error output:") + (log-error "~a" output) + (log-info "Deploy-rs automatic rollback should have been triggered") + #f))))))) + +;; Build deploy-rs command with options +(define (build-deploy-command machine-name skip-checks auto-rollback magic-rollback) + "Build the deploy-rs command with appropriate flags" + (let ((base-cmd (format #f "cd ~a && deploy '.#~a'" (get-homelab-root) machine-name)) + (flags '())) + + ;; Add flags based on options + (when skip-checks + (set! flags (cons "--skip-checks" flags))) + + (when auto-rollback + (set! flags (cons "--auto-rollback" flags))) + + (when magic-rollback + (set! flags (cons "--magic-rollback" flags))) + + ;; Combine command with flags + (if (null? flags) + base-cmd + (format #f "~a ~a" base-cmd (string-join flags " "))))) + +;; Deploy to all machines +(define (deploy-all-machines . args) + "Deploy to all machines using deploy-rs" + (let* ((options (if (null? args) '() (car args))) + (dry-run (option-ref options 'dry-run #f)) + (machines (get-all-machines))) + + (log-info "Starting deployment to all machines (~a total)" (length machines)) + + (let ((results + (map (lambda (machine) + (log-info "Deploying to ~a..." machine) + (let ((result (deploy-machine machine "default" options))) + (if result + (log-success "✓ ~a deployed successfully" machine) + (log-error "✗ ~a deployment failed" machine)) + (cons machine result))) + machines))) + + ;; Summary + (let ((successful (filter cdr results)) + (failed (filter (lambda (r) (not (cdr r))) results))) + (log-info "Deployment summary:") + (log-info " Successful: ~a/~a machines" (length successful) (length machines)) + (when (not (null? failed)) + (log-error " Failed: ~a" (string-join (map car failed) ", "))) + + ;; Return true if all succeeded + (= (length successful) (length machines)))))) + +;; Deploy with explicit rollback testing +(define (deploy-with-rollback machine-name . args) + "Deploy with explicit rollback capability testing" + (let* ((options (if (null? args) '() (car args))) + (test-rollback (option-ref options 'test-rollback #f))) + + (log-info "Deploying ~a with rollback testing..." machine-name) + + (if test-rollback + (begin + (log-info "Testing rollback mechanism (deploy will be reverted)") + ;; Deploy with magic rollback disabled to test manual rollback + (let ((modified-options (cons '(magic-rollback . #f) options))) + (execute-deploy-rs machine-name "default" modified-options))) + (execute-deploy-rs machine-name "default" options)))) + +;; Update flake inputs (moved from old deployment module) (define (update-flake . args) "Update flake inputs (impure - has side effects)" (let* ((options (if (null? args) '() (car args))) @@ -64,76 +166,3 @@ (log-error "Flake update failed (exit: ~a)" status) (log-error "Error output: ~a" output) #f)))))) - -;; Impure function: Execute nixos-rebuild -(define (execute-nixos-rebuild machine-name mode options) - "Execute nixos-rebuild command (impure - has side effects)" - (let* ((dry-run (option-ref options 'dry-run #f)) - (ssh-config (get-ssh-config machine-name)) - (is-local (and ssh-config (assoc-ref ssh-config 'is-local))) - (homelab-root (get-homelab-root))) - - (if is-local - ;; Local deployment - (let ((rebuild-cmd (format #f "sudo nixos-rebuild ~a --flake ~a#~a" - mode homelab-root machine-name))) - (log-debug "Local rebuild command: ~a" rebuild-cmd) - - (if dry-run - (begin - (log-info "DRY RUN: Would execute: ~a" rebuild-cmd) - #t) - (let* ((port (open-pipe* OPEN_READ "/bin/sh" "-c" rebuild-cmd)) - (output (get-string-all port)) - (status (close-pipe port))) - - (if (zero? status) - (begin - (log-success "Local nixos-rebuild completed") - #t) - (begin - (log-error "Local nixos-rebuild failed (exit: ~a)" status) - #f))))) - - ;; Remote deployment - (let* ((hostname (assoc-ref ssh-config 'hostname)) - (ssh-alias (or (assoc-ref ssh-config 'ssh-alias) hostname)) - (temp-dir "/tmp/homelab-deploy") - (sync-cmd (format #f "rsync -av --delete ~a/ ~a:~a/" - homelab-root ssh-alias temp-dir)) - (rebuild-cmd (format #f "ssh ~a 'cd ~a && sudo nixos-rebuild ~a --flake .#~a'" - ssh-alias temp-dir mode machine-name))) - - (log-debug "Remote sync command: ~a" sync-cmd) - (log-debug "Remote rebuild command: ~a" rebuild-cmd) - - (if dry-run - (begin - (log-info "DRY RUN: Would sync and rebuild remotely") - #t) - (begin - ;; Sync configuration - (log-info "Syncing configuration to ~a..." machine-name) - (let* ((sync-port (open-pipe* OPEN_READ "/bin/sh" "-c" sync-cmd)) - (sync-output (get-string-all sync-port)) - (sync-status (close-pipe sync-port))) - - (if (zero? sync-status) - (begin - (log-success "Configuration synced") - ;; Execute rebuild - (log-info "Executing nixos-rebuild on ~a..." machine-name) - (let* ((rebuild-port (open-pipe* OPEN_READ "/bin/sh" "-c" rebuild-cmd)) - (rebuild-output (get-string-all rebuild-port)) - (rebuild-status (close-pipe rebuild-port))) - - (if (zero? rebuild-status) - (begin - (log-success "Remote nixos-rebuild completed") - #t) - (begin - (log-error "Remote nixos-rebuild failed (exit: ~a)" rebuild-status) - #f)))) - (begin - (log-error "Configuration sync failed (exit: ~a)" sync-status) - #f))))))))) diff --git a/packages/lab-tool/main.scm b/packages/lab-tool/main.scm index 6a5abd0..15803b6 100755 --- a/packages/lab-tool/main.scm +++ b/packages/lab-tool/main.scm @@ -22,44 +22,48 @@ ;; Pure function: Command help text (define (get-help-text) "Pure function returning help text" - "Home Lab Tool - K.I.S.S Refactored Edition + "Home Lab Tool - Deploy-rs Edition USAGE: lab [args...] COMMANDS: status Show infrastructure status - machines List all machines - deploy [mode] Deploy configuration to machine - Available modes: boot (default), test, switch - deploy-all Deploy to all machines + machines List all machines + deploy [options] Deploy configuration to machine using deploy-rs + Options: --dry-run, --skip-checks + deploy-all [options] Deploy to all machines using deploy-rs update Update flake inputs auto-update Perform automatic system update with health checks auto-update-status Show auto-update service status and logs health [machine] Check machine health (all if no machine specified) - ssh SSH to machine - test-modules Test modular implementation + ssh SSH to machine (using sma user) + test-rollback Test deployment with rollback help Show this help EXAMPLES: lab status lab machines - lab deploy congenital-optimist # Deploy with boot mode (default) - lab deploy congenital-optimist switch # Deploy and activate immediately - lab deploy congenital-optimist test # Deploy temporarily for testing - lab deploy-all - lab update - lab auto-update # Perform automatic update with reboot - lab auto-update-status # Show auto-update logs and status - lab health - lab health sleeper-service - lab ssh sleeper-service - lab test-modules + lab deploy congenital-optimist # Deploy with deploy-rs safety + lab deploy sleeper-service --dry-run # Test deployment without applying + lab deploy grey-area --skip-checks # Deploy without health checks + lab deploy-all # Deploy to all machines + lab deploy-all --dry-run # Test deployment to all machines + lab update # Update flake inputs + lab test-rollback sleeper-service # Test rollback functionality + lab ssh sleeper-service # SSH to machine as sma user -This implementation follows K.I.S.S principles: -- Modular: Each module has single responsibility -- Functional: Pure functions separated from side effects -- Small: Individual modules under 50 lines -- Simple: One function does one thing well +Deploy-rs Features: +- Automatic rollback on deployment failure +- Health checks after deployment +- Magic rollback for network connectivity issues +- Atomic deployments with safety guarantees +- Consistent sma user for all deployments + +This implementation uses deploy-rs for all deployments: +- Robust: Automatic rollback protection +- Safe: Health checks and validation +- Consistent: Same deployment method for all machines +- Flexible: Dry-run and skip-checks options available ") ;; Pure function: Format machine list @@ -109,36 +113,33 @@ Home lab root: ~a (log-success "Machine list complete"))) (define (cmd-deploy machine-name . args) - "Deploy configuration to machine" - (let* ((mode (if (null? args) "boot" (car args))) - (valid-modes '("boot" "test" "switch"))) - (log-info "Deploying to machine: ~a (mode: ~a)" machine-name mode) - (if (not (member mode valid-modes)) + "Deploy configuration to machine using deploy-rs" + (let* ((options (parse-deploy-options args))) + (log-info "Deploying to machine: ~a using deploy-rs" machine-name) + (if (validate-machine-name machine-name) + (let ((result (deploy-machine machine-name "default" options))) + (if result + (log-success "Deploy-rs deployment to ~a completed successfully" machine-name) + (log-error "Deploy-rs deployment to ~a failed" machine-name))) (begin - (log-error "Invalid deployment mode: ~a" mode) - (log-error "Valid modes: ~a" (string-join valid-modes ", ")) - (format #t "Usage: lab deploy [boot|test|switch]\n")) - (if (validate-machine-name machine-name) - (let ((result (deploy-machine machine-name mode '()))) - (if result - (log-success "Deployment to ~a complete (mode: ~a)" machine-name mode) - (log-error "Deployment to ~a failed" machine-name))) - (begin - (log-error "Invalid machine: ~a" machine-name) - (log-info "Available machines: ~a" (string-join (get-all-machines) ", "))))))) + (log-error "Invalid machine: ~a" machine-name) + (log-info "Available machines: ~a" (string-join (get-all-machines) ", ")))))) (define (cmd-ssh machine-name) - "SSH to machine" - (log-info "Connecting to machine: ~a" machine-name) + "SSH to machine using sma user" + (log-info "Connecting to machine: ~a as sma user" machine-name) (if (validate-machine-name machine-name) (let ((ssh-config (get-ssh-config machine-name))) (if ssh-config (let ((hostname (assoc-ref ssh-config 'hostname)) (ssh-alias (assoc-ref ssh-config 'ssh-alias)) + (ssh-user (assoc-ref ssh-config 'ssh-user)) (is-local (assoc-ref ssh-config 'is-local))) (if is-local - (log-info "Machine ~a is local - no SSH needed" machine-name) - (let ((target (or ssh-alias hostname))) + (begin + (log-info "Machine ~a is local - switching to sma user locally" machine-name) + (system "sudo -u sma -i")) + (let ((target (format #f "~a@~a" (or ssh-user "sma") (or ssh-alias hostname)))) (log-info "Connecting to ~a via SSH..." target) (system (format #f "ssh ~a" target))))) (log-error "No SSH configuration found for ~a" machine-name))) @@ -171,20 +172,12 @@ Home lab root: ~a (log-error "Flake update failed")))) (define (cmd-deploy-all) - "Deploy to all machines" - (log-info "Deploying to all machines...") - (let* ((machines (list-machines)) - (results (map (lambda (machine) - (log-info "Deploying to ~a..." machine) - (let ((result (deploy-machine machine "boot" '()))) - (if result - (log-success "✓ ~a deployed" machine) - (log-error "✗ ~a failed" machine)) - result)) - machines)) - (successful (filter identity results))) - (log-info "Deployment summary: ~a/~a successful" - (length successful) (length machines)))) + "Deploy to all machines using deploy-rs" + (log-info "Deploying to all machines using deploy-rs...") + (let ((result (deploy-all-machines '()))) + (if result + (log-success "All deploy-rs deployments completed successfully") + (log-error "Some deploy-rs deployments failed")))) (define (cmd-health args) "Check machine health" @@ -219,6 +212,33 @@ Home lab root: ~a "Show auto-update status and logs" (auto-update-status)) +;; Parse deployment options from command line arguments +(define (parse-deploy-options args) + "Parse deployment options from command line arguments" + (let ((options '())) + (for-each + (lambda (arg) + (cond + ((string=? arg "--dry-run") + (set! options (cons '(dry-run . #t) options))) + ((string=? arg "--skip-checks") + (set! options (cons '(skip-checks . #t) options))) + (else + (log-warn "Unknown option: ~a" arg)))) + args) + options)) + +(define (cmd-test-rollback machine-name) + "Test deployment with rollback functionality" + (log-info "Testing rollback deployment for machine: ~a" machine-name) + (if (validate-machine-name machine-name) + (let ((options '((test-rollback . #t)))) + (let ((result (deploy-with-rollback machine-name options))) + (if result + (log-success "Rollback test completed for ~a" machine-name) + (log-error "Rollback test failed for ~a" machine-name)))) + (log-error "Invalid machine: ~a" machine-name))) + ;; Main command dispatcher (define (dispatch-command command args) "Dispatch command with appropriate handler" @@ -236,12 +256,20 @@ Home lab root: ~a (if (null? args) (begin (log-error "deploy command requires machine name") - (format #t "Usage: lab deploy [boot|test|switch]\n")) + (format #t "Usage: lab deploy [options]\n") + (format #t "Options: --dry-run, --skip-checks\n")) (apply cmd-deploy args))) ('deploy-all (cmd-deploy-all)) + ('test-rollback + (if (null? args) + (begin + (log-error "test-rollback command requires machine name") + (format #t "Usage: lab test-rollback \n")) + (cmd-test-rollback (car args)))) + ('update (cmd-update)) @@ -264,6 +292,13 @@ Home lab root: ~a ('test-modules (cmd-test-modules)) + ('test-rollback + (if (null? args) + (begin + (log-error "test-rollback command requires machine name") + (format #t "Usage: lab test-rollback \n")) + (cmd-test-rollback (car args)))) + (_ (log-error "Unknown command: ~a" command) (format #t "Use 'lab help' for available commands\n") @@ -272,7 +307,7 @@ Home lab root: ~a ;; Main entry point (define (main args) "Main entry point for lab tool" - (log-info "Home Lab Tool - K.I.S.S Refactored Edition") + (log-info "Home Lab Tool - Deploy-rs Edition") (let* ((parsed-cmd (if (> (length args) 1) (cdr args) '("help"))) (command (string->symbol (car parsed-cmd))) diff --git a/packages/lab-tool/utils/config.scm b/packages/lab-tool/utils/config.scm index 1389a39..c8838a0 100644 --- a/packages/lab-tool/utils/config.scm +++ b/packages/lab-tool/utils/config.scm @@ -22,26 +22,31 @@ (machines . ((congenital-optimist (type . local) (hostname . "localhost") + (ssh-user . "sma") (services . (workstation development))) (sleeper-service (type . remote) (hostname . "sleeper-service.tail807ea.ts.net") - (ssh-alias . "admin-sleeper") + (ssh-alias . "sleeper-service.tail807ea.ts.net") + (ssh-user . "sma") (services . (nfs zfs storage))) (grey-area (type . remote) (hostname . "grey-area.tail807ea.ts.net") - (ssh-alias . "admin-grey") + (ssh-alias . "grey-area.tail807ea.ts.net") + (ssh-user . "sma") (services . (ollama forgejo git))) (reverse-proxy (type . remote) (hostname . "reverse-proxy.tail807ea.ts.net") - (ssh-alias . "admin-reverse") + (ssh-alias . "reverse-proxy.tail807ea.ts.net") + (ssh-user . "sma") (services . (nginx proxy ssl))) (little-rascal (type . remote) (hostname . "little-rascal.tail807ea.ts.net") - (ssh-alias . "little-rascal") + (ssh-alias . "little-rascal.tail807ea.ts.net") + (ssh-user . "sma") (services . (development niri desktop ai-tools))))) (deployment . ((default-mode . "boot") (timeout . 300) @@ -124,10 +129,12 @@ (if machine-config (let ((type (assoc-ref machine-config 'type)) (hostname (assoc-ref machine-config 'hostname)) - (ssh-alias (assoc-ref machine-config 'ssh-alias))) + (ssh-alias (assoc-ref machine-config 'ssh-alias)) + (ssh-user (assoc-ref machine-config 'ssh-user))) `((type . ,type) (hostname . ,hostname) (ssh-alias . ,ssh-alias) + (ssh-user . ,ssh-user) (is-local . ,(eq? type 'local)))) #f))) diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..07b8340 --- /dev/null +++ b/shell.nix @@ -0,0 +1,34 @@ +# Nix shell for Home Lab development with deploy-rs and lab-tool + +{ + description = "Home Lab dev shell with deploy-rs and lab-tool"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + deploy-rs.url = "github:serokell/deploy-rs"; + }; + + outputs = { self, nixpkgs, deploy-rs, ... }@inputs: let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + in { + devShells.${system}.default = pkgs.mkShell { + buildInputs = [ + pkgs.git + pkgs.guile_3_0 + pkgs.guile-ssh + pkgs.guile-json + pkgs.guile-git + pkgs.guile-gcrypt + pkgs.openssh + pkgs.nixos-rebuild + deploy-rs.packages.${system}.deploy-rs + (import ./packages/lab-tool/default.nix { inherit (pkgs) lib stdenv makeWrapper guile_3_0 guile-ssh guile-json guile-git guile-gcrypt openssh git nixos-rebuild; }) + ]; + shellHook = '' + echo "Dev shell: deploy-rs and lab-tool available." + echo "Try: lab status, lab deploy , or deploy . " + ''; + }; + }; +}