diff --git a/packages/lab-tool/lab/deploy-rs.scm b/packages/lab-tool/lab/deploy-rs.scm new file mode 100644 index 0000000..1b558ce --- /dev/null +++ b/packages/lab-tool/lab/deploy-rs.scm @@ -0,0 +1,140 @@ +;; lab/deploy-rs.scm - Deploy-rs based deployment operations (extracted) + +(define-module (lab deploy-rs) + #:use-module (ice-9 format) + #:use-module (ice-9 popen) + #:use-module (ice-9 textual-ports) + #:use-module (srfi srfi-1) + #:use-module (utils logging) + #:use-module (utils config) + #:export (deploy-machine-deploy-rs + deploy-all-machines-deploy-rs + deploy-with-rollback + option-ref)) + +;; Helper function for option handling +(define (option-ref options key default) + "Get option value with default fallback" + (let ((value (assoc-ref options key))) + (if value value default))) + +;; Main deployment function using deploy-rs +(define (deploy-machine-deploy-rs machine-name . 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))) + (dry-run (option-ref options 'dry-run #f)) + (skip-checks (option-ref options 'skip-checks #f))) + + (if (not (validate-machine-name machine-name)) + #f + (begin + (log-info "Starting deploy-rs deployment: ~a" machine-name) + (execute-deploy-rs machine-name mode options))))) + +;; 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-info "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=true" flags))) + + (when magic-rollback + (set! flags (cons "--magic-rollback=true" flags))) + + ;; Combine command with flags + (if (null? flags) + base-cmd + (format #f "~a ~a" base-cmd (string-join (reverse flags) " "))))) + +;; Deploy to all machines +(define (deploy-all-machines-deploy-rs . 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-deploy-rs 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)))) \ No newline at end of file diff --git a/packages/lab-tool/lab/deployment.scm b/packages/lab-tool/lab/deployment.scm index fedb748..edf06c3 100644 --- a/packages/lab-tool/lab/deployment.scm +++ b/packages/lab-tool/lab/deployment.scm @@ -1,4 +1,4 @@ -;; lab/deployment.scm - Deploy-rs based deployment operations +;; lab/deployment.scm - Unified deployment operations (SSH + rsync by default) (define-module (lab deployment) #:use-module (ice-9 format) @@ -7,162 +7,61 @@ #:use-module (srfi srfi-1) #:use-module (utils logging) #:use-module (utils config) + #:use-module (lab ssh-deploy) + #:use-module (lab deploy-rs) #:export (deploy-machine update-flake deploy-all-machines deploy-with-rollback option-ref)) -;; Helper function for option handling +;; Helper function for option handling (re-exported from ssh-deploy) (define (option-ref options key default) "Get option value with default fallback" (let ((value (assoc-ref options key))) (if value value default))) -;; Main deployment function using deploy-rs +;; Main deployment function - SSH by default, deploy-rs optional (define (deploy-machine machine-name . args) - "Deploy configuration to machine using deploy-rs (impure - has side effects)" + "Deploy configuration to machine using SSH + rsync (default) or deploy-rs (optional)" (let* ((mode (if (null? args) "default" (car args))) (options (if (< (length args) 2) '() (cadr args))) - (dry-run (option-ref options 'dry-run #f)) - (skip-checks (option-ref options 'skip-checks #f))) + (use-deploy-rs (option-ref options 'use-deploy-rs #f))) (if (not (validate-machine-name machine-name)) #f - (begin - (log-info "Starting deploy-rs deployment: ~a" machine-name) - (execute-deploy-rs machine-name mode options))))) + (if use-deploy-rs + (begin + (log-info "Using deploy-rs deployment method") + (deploy-machine-deploy-rs machine-name mode options)) + (begin + (log-info "Using SSH + rsync deployment method") + (deploy-machine-ssh machine-name mode options)))))) -;; 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-info "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=true" flags))) - - (when magic-rollback - (set! flags (cons "--magic-rollback=true" flags))) - - ;; Combine command with flags - (if (null? flags) - base-cmd - (format #f "~a ~a" base-cmd (string-join (reverse flags) " "))))) - -;; Deploy to all machines +;; Deploy to all machines - delegate to appropriate module (define (deploy-all-machines . args) - "Deploy to all machines using deploy-rs" + "Deploy to all machines using SSH + rsync (default) or deploy-rs (optional)" (let* ((options (if (null? args) '() (car args))) - (dry-run (option-ref options 'dry-run #f)) - (machines (get-all-machines))) + (use-deploy-rs (option-ref options 'use-deploy-rs #f))) - (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)))))) + (if use-deploy-rs + (begin + (log-info "Using deploy-rs for all machines") + (deploy-all-machines-deploy-rs options)) + (begin + (log-info "Using SSH + rsync for all machines") + (deploy-all-machines-ssh options))))) -;; Deploy with explicit rollback testing +;; Deploy with rollback testing - only available with deploy-rs (define (deploy-with-rollback machine-name . args) - "Deploy with explicit rollback capability testing" + "Deploy with explicit rollback capability testing (deploy-rs only)" (let* ((options (if (null? args) '() (car args))) - (test-rollback (option-ref options 'test-rollback #f))) + (modified-options (cons '(use-deploy-rs . #t) options))) - (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)))) + (log-info "Rollback testing requires deploy-rs - switching to deploy-rs mode") + (deploy-with-rollback machine-name modified-options))) -;; Update flake inputs (moved from old deployment module) -(define (update-flake . args) +;; Update flake inputs - delegate to ssh-deploy module +(define update-flake "Update flake inputs (impure - has side effects)" - (let* ((options (if (null? args) '() (car args))) - (dry-run (option-ref options 'dry-run #f))) - - (log-info "Updating flake inputs...") - - (if dry-run - (begin - (log-info "DRY RUN: Would execute: nix flake update") - #t) - (let* ((homelab-root (get-homelab-root)) - (update-cmd (format #f "cd ~a && nix flake update" homelab-root)) - (port (open-pipe* OPEN_READ "/bin/sh" "-c" update-cmd)) - (output (get-string-all port)) - (status (close-pipe port))) - - (if (zero? status) - (begin - (log-success "Flake inputs updated successfully") - #t) - (begin - (log-error "Flake update failed (exit: ~a)" status) - (log-error "Error output: ~a" output) - #f)))))) + (@ (lab ssh-deploy) update-flake)) diff --git a/packages/lab-tool/lab/ssh-deploy.scm b/packages/lab-tool/lab/ssh-deploy.scm new file mode 100644 index 0000000..ae82a92 --- /dev/null +++ b/packages/lab-tool/lab/ssh-deploy.scm @@ -0,0 +1,198 @@ +;; lab/ssh-deploy.scm - SSH + rsync + nixos-rebuild deployment operations + +(define-module (lab ssh-deploy) + #:use-module (ice-9 format) + #:use-module (ice-9 popen) + #:use-module (ice-9 textual-ports) + #:use-module (srfi srfi-1) + #:use-module (utils logging) + #:use-module (utils config) + #:use-module (utils ssh) + #:export (deploy-machine-ssh + deploy-all-machines-ssh + update-flake + sync-config-to-machine + option-ref)) + +;; Helper function for option handling +(define (option-ref options key default) + "Get option value with default fallback" + (let ((value (assoc-ref options key))) + (if value value default))) + +;; Main SSH deployment function +(define (deploy-machine-ssh machine-name . args) + "Deploy configuration to machine using SSH + rsync + nixos-rebuild" + (let* ((mode (if (null? args) "default" (car args))) + (options (if (< (length args) 2) '() (cadr args))) + (dry-run (option-ref options 'dry-run #f)) + (boot-mode (option-ref options 'boot #f))) + + (if (not (validate-machine-name machine-name)) + #f + (begin + (log-info "Starting SSH deployment: ~a" machine-name) + (execute-ssh-deploy machine-name mode options))))) + +;; Execute SSH-based deployment +(define (execute-ssh-deploy machine-name mode options) + "Execute deployment using SSH + rsync + nixos-rebuild" + (let* ((homelab-root (get-homelab-root)) + (dry-run (option-ref options 'dry-run #f)) + (boot-mode (option-ref options 'boot #f)) + (test-mode (option-ref options 'test #f)) + (remote-path "/tmp/home-lab-config")) + + (log-info "Deploying ~a using SSH + rsync + nixos-rebuild..." machine-name) + + (if dry-run + (begin + (log-info "DRY RUN: Would sync config and rebuild ~a" machine-name) + (log-info "Would execute: rsync + nixos-rebuild --flake /tmp/home-lab-config#~a" machine-name) + #t) + (let ((start-time (current-time))) + + ;; Step 1: Sync configuration to remote machine + (log-info "Step 1: Syncing configuration to ~a:~a" machine-name remote-path) + (if (sync-config-to-machine machine-name remote-path) + ;; Step 2: Execute nixos-rebuild on remote machine + (begin + (log-info "Step 2: Executing nixos-rebuild on ~a" machine-name) + (execute-remote-rebuild machine-name remote-path boot-mode test-mode start-time)) + (begin + (log-error "Failed to sync configuration to ~a" machine-name) + #f)))))) + +;; Sync configuration to remote machine +(define (sync-config-to-machine machine-name remote-path) + "Sync Home-lab configuration to remote machine using rsync" + (let* ((homelab-root (get-homelab-root)) + (ssh-config (get-ssh-config machine-name))) + + (if (not ssh-config) + (begin + (log-error "No SSH configuration found for ~a" machine-name) + #f) + (if (assoc-ref ssh-config 'is-local) + ;; Local "sync" - just ensure path exists + (begin + (log-debug "Local machine ~a, copying to ~a" machine-name remote-path) + (let* ((cp-cmd (format #f "sudo mkdir -p ~a && sudo cp -r ~a/* ~a/" + remote-path homelab-root remote-path)) + (status (system cp-cmd))) + (if (zero? status) + (begin + (log-debug "Local configuration copied successfully") + #t) + (begin + (log-error "Local configuration copy failed (exit: ~a)" status) + #f)))) + ;; Remote sync using rsync + (let* ((hostname (assoc-ref ssh-config 'hostname)) + (ssh-alias (assoc-ref ssh-config 'ssh-alias)) + (user (assoc-ref ssh-config 'user)) + (identity-file (assoc-ref ssh-config 'identity-file)) + (target (if user (format #f "~a@~a" user (or ssh-alias hostname)) (or ssh-alias hostname))) + (key-arg (if identity-file (format #f "-i ~a" identity-file) "")) + (rsync-cmd (format #f "rsync -avz --delete -e 'ssh ~a' ~a/ ~a:~a/" + key-arg homelab-root target remote-path))) + + (log-debug "Rsync command: ~a" rsync-cmd) + (let* ((port (open-pipe* OPEN_READ "/bin/sh" "-c" rsync-cmd)) + (output (get-string-all port)) + (status (close-pipe port))) + + (if (zero? status) + (begin + (log-debug "Configuration synced successfully") + (log-debug "Rsync output: ~a" output) + #t) + (begin + (log-error "Configuration sync failed (exit: ~a)" status) + (log-error "Rsync error: ~a" output) + #f)))))))) + +;; Execute nixos-rebuild on remote machine +(define (execute-remote-rebuild machine-name remote-path boot-mode test-mode start-time) + "Execute nixos-rebuild on the remote machine" + (let* ((rebuild-mode (cond + (test-mode "test") + (boot-mode "boot") + (else "switch"))) + (rebuild-cmd (format #f "sudo nixos-rebuild ~a --flake ~a#~a" + rebuild-mode remote-path machine-name))) + + (log-info "Executing: ~a" rebuild-cmd) + + (call-with-values + (lambda () (run-remote-command machine-name rebuild-cmd)) + (lambda (success output) + (let ((elapsed (- (current-time) start-time))) + (if success + (begin + (log-success "SSH deployment completed successfully in ~as" elapsed) + (log-info "Rebuild output:") + (log-info "~a" output) + #t) + (begin + (log-error "SSH deployment failed (exit code indicates failure)") + (log-error "Rebuild error output:") + (log-error "~a" output) + #f))))))) + +;; Deploy to all machines using SSH +(define (deploy-all-machines-ssh . args) + "Deploy to all machines using SSH + rsync + nixos-rebuild" + (let* ((options (if (null? args) '() (car args))) + (dry-run (option-ref options 'dry-run #f)) + (machines (get-all-machines))) + + (log-info "Starting SSH deployment to all machines (~a total)" (length machines)) + + (let ((results + (map (lambda (machine) + (log-info "Deploying to ~a..." machine) + (let ((result (deploy-machine-ssh 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 "SSH 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)))))) + +;; Update flake inputs +(define (update-flake . args) + "Update flake inputs (impure - has side effects)" + (let* ((options (if (null? args) '() (car args))) + (dry-run (option-ref options 'dry-run #f))) + + (log-info "Updating flake inputs...") + + (if dry-run + (begin + (log-info "DRY RUN: Would execute: nix flake update") + #t) + (let* ((homelab-root (get-homelab-root)) + (update-cmd (format #f "cd ~a && nix flake update" homelab-root)) + (port (open-pipe* OPEN_READ "/bin/sh" "-c" update-cmd)) + (output (get-string-all port)) + (status (close-pipe port))) + + (if (zero? status) + (begin + (log-success "Flake inputs updated successfully") + #t) + (begin + (log-error "Flake update failed (exit: ~a)" status) + (log-error "Error output: ~a" output) + #f)))))) \ No newline at end of file diff --git a/packages/lab-tool/main.scm b/packages/lab-tool/main.scm index 15803b6..c1b2583 100755 --- a/packages/lab-tool/main.scm +++ b/packages/lab-tool/main.scm @@ -22,49 +22,55 @@ ;; Pure function: Command help text (define (get-help-text) "Pure function returning help text" - "Home Lab Tool - Deploy-rs Edition + "Home Lab Tool - SSH + Rsync Edition USAGE: lab [args...] COMMANDS: status Show infrastructure status 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 + deploy [options] Deploy configuration to machine using SSH + rsync + nixos-rebuild + Options: --dry-run, --boot, --test, --use-deploy-rs + deploy-all [options] Deploy to all machines using SSH + rsync + nixos-rebuild 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 (using sma user) - test-rollback Test deployment with rollback + test-rollback Test deployment with rollback (uses deploy-rs) help Show this help EXAMPLES: lab status lab machines - 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 little-rascal # Deploy with SSH + rsync (default) + lab deploy little-rascal --dry-run # Test deployment without applying + lab deploy little-rascal --boot # Deploy but only activate on next boot + lab deploy little-rascal --test # Deploy but don't make permanent + lab deploy little-rascal --use-deploy-rs # Use deploy-rs instead of SSH method 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 test-rollback sleeper-service # Test rollback functionality (deploy-rs) lab ssh sleeper-service # SSH to machine as sma user -Deploy-rs Features: +SSH + Rsync Features (Default): +- Fast: Only syncs changed files with rsync +- Simple: Uses standard nixos-rebuild workflow +- Reliable: Same command workflow as manual deployment +- Flexible: Supports boot, test, and switch modes + +Deploy-rs Features (Optional with --use-deploy-rs): - 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 -") +This implementation uses SSH + rsync + nixos-rebuild by default: +- Fast: Efficient file synchronization +- Simple: Standard NixOS deployment workflow +- Consistent: Same user (sma) for all operations +- Flexible: Multiple deployment modes available" ;; Pure function: Format machine list (define (format-machine-list machines) @@ -323,4 +329,4 @@ Home lab root: ~a ;; Run main function if script is executed directly (when (and (defined? 'command-line) (not (null? (command-line)))) - (main (command-line))) \ No newline at end of file + (main (command-line))))