aboutsummaryrefslogtreecommitdiff
path: root/gnu/machine.scm
blob: 60be67497273dc08fba325dc0357043f36049937 (about) (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2019 David Thompson <davet@gnu.org>
;;; Copyright © 2019 Jakob L. Kreuze <zerodaysfordays@sdf.org>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu machine)
  #:use-module (gnu system)
  #:use-module (guix derivations)
  #:use-module (guix monads)
  #:use-module (guix records)
  #:use-module (guix store)
  #:use-module ((guix diagnostics) #:select (source-properties->location))
  #:use-module (srfi srfi-35)
  #:export (environment-type
            environment-type?
            environment-type-name
            environment-type-description
            environment-type-location

            machine
            machine?

            machine-operating-system
            machine-environment
            machine-configuration
            machine-display-name

            deploy-machine
            roll-back-machine
            machine-remote-eval

            &deploy-error
            deploy-error?
            deploy-error-should-roll-back
            deploy-error-captured-args))

;;; Commentary:
;;;
;;; This module provides the types used to declare individual machines in a
;;; heterogeneous Guix deployment. The interface allows users to specify system
;;; configurations and the means by which resources should be provisioned on a
;;; per-host basis.
;;;
;;; Code:


;;;
;;; Declarations for resources that can be provisioned.
;;;

(define-record-type* <environment-type> environment-type
  make-environment-type
  environment-type?

  ;; Interface to the environment type's deployment code. Each procedure
  ;; should take the same arguments as the top-level procedure of this file
  ;; that shares the same name. For example, 'machine-remote-eval' should be
  ;; of the form '(machine-remote-eval machine exp)'.
  (machine-remote-eval environment-type-machine-remote-eval) ; procedure
  (deploy-machine      environment-type-deploy-machine)      ; procedure
  (roll-back-machine   environment-type-roll-back-machine)   ; procedure

  ;; Metadata.
  (name        environment-type-name)       ; symbol
  (description environment-type-description ; string
               (default #f))
  (location    environment-type-location    ; <location>
               (default (and=> (current-source-location)
                               source-properties->location))
               (innate)))


;;;
;;; Declarations for machines in a deployment.
;;;

(define-record-type* <machine> machine make-machine
  machine?
  (operating-system %machine-operating-system); <operating-system>
  (environment      machine-environment)      ; symbol
  (configuration    machine-configuration     ; configuration object
                    (default #f)))            ; specific to environment

(define (machine-operating-system machine)
  "Return the operating system of MACHINE."
  (operating-system-with-provenance
   (%machine-operating-system machine)))

(define (machine-display-name machine)
  "Return the host-name identifying MACHINE."
  (operating-system-host-name (machine-operating-system machine)))

(define (machine-remote-eval machine exp)
  "Evaluate EXP, a gexp, on MACHINE. Ensure that all the elements EXP refers to
are built and deployed to MACHINE beforehand."
  (let ((environment (machine-environment machine)))
    ((environment-type-machine-remote-eval environment) machine exp)))

(define (deploy-machine machine)
  "Monadic procedure transferring the new system's OS closure to the remote
MACHINE, activating it on MACHINE and switching MACHINE to the new generation."
  (let ((environment (machine-environment machine)))
    ((environment-type-deploy-machine environment) machine)))

(define (roll-back-machine machine)
  "Monadic procedure rolling back to the previous system generation on
MACHINE. Return the number of the generation that was current before switching
and the new generation number."
  (let ((environment (machine-environment machine)))
    ((environment-type-roll-back-machine environment) machine)))


;;;
;;; Error types.
;;;

(define-condition-type &deploy-error &error
  deploy-error?
  (should-roll-back deploy-error-should-roll-back)
  (captured-args deploy-error-captured-args))
ll port)) (result (read port))) (seek port start SEEK_SET) result)) (define (surrounding-sexp port line-no) "Return the top-level S-expression surrounding the change at line number LINE-NO in PORT." (let loop ((i (1- line-no)) (last-top-level-sexp #f)) (if (zero? i) last-top-level-sexp (match (peek-char port) (#\( (let ((sexp (read-excursion port))) (read-line port) (loop (1- i) sexp))) (_ (read-line port) (loop (1- i) last-top-level-sexp)))))) (define-record-type <hunk> (make-hunk file-name old-line-number new-line-number diff) hunk? (file-name hunk-file-name) ;; Line number before the change (old-line-number hunk-old-line-number) ;; Line number after the change (new-line-number hunk-new-line-number) ;; The full diff to be used with "git apply --cached" (diff hunk-diff)) (define* (hunk->patch hunk #:optional (port (current-output-port))) (let ((file-name (hunk-file-name hunk))) (format port "diff --git a/~a b/~a~%--- a/~a~%+++ b/~a~%~a" file-name file-name file-name file-name (hunk-diff hunk)))) (define (diff-info) "Read the diff and return a list of <hunk> values." (let ((port (open-pipe* OPEN_READ "git" "diff" "--no-prefix" ;; Do not include any context lines. This makes it ;; easier to find the S-expression surrounding the ;; change. "--unified=0"))) (define (extract-line-number line-tag) (abs (string->number (car (string-split line-tag #\,))))) (define (read-hunk) (reverse (let loop ((lines '())) (let ((line (read-line port 'concat))) (cond ((eof-object? line) lines) ((or (string-prefix? "@@ " line) (string-prefix? "diff --git" line)) (unget-string port line) lines) (else (loop (cons line lines)))))))) (define info (let loop ((acc '()) (file-name #f)) (let ((line (read-line port))) (cond ((eof-object? line) acc) ((string-prefix? "--- " line) (match (string-split line #\space) ((_ file-name) (loop acc file-name)))) ((string-prefix? "@@ " line) (match (string-split line #\space) ((_ old-start new-start . _) (loop (cons (make-hunk file-name (extract-line-number old-start) (extract-line-number new-start) (string-join (cons* line "\n" (read-hunk)) "")) acc) file-name)))) (else (loop acc file-name)))))) (close-pipe port) info)) (define (old-sexp hunk) "Using the diff information in HUNK return the unmodified S-expression corresponding to the top-level definition containing the staged changes." ;; TODO: We can't seek with a pipe port... (let* ((port (open-pipe* OPEN_READ "git" "show" (string-append "HEAD:" (hunk-file-name hunk)))) (contents (get-string-all port))) (close-pipe port) (call-with-input-string contents (lambda (port) (surrounding-sexp port (hunk-old-line-number hunk)))))) (define (new-sexp hunk) "Using the diff information in HUNK return the modified S-expression corresponding to the top-level definition containing the staged changes." (call-with-input-file (hunk-file-name hunk) (lambda (port) (surrounding-sexp port (hunk-new-line-number hunk))))) (define* (commit-message file-name old new #:optional (port (current-output-port))) "Print ChangeLog commit message for changes between OLD and NEW." (define (get-values expr field) (match ((sxpath `(// ,field quasiquote *)) expr) (() '()) ((first . rest) (map cadadr first)))) (define (listify items) (match items ((one) one) ((one two) (string-append one " and " two)) ((one two . more) (string-append (string-join (drop-right items 1) ", ") ", and " (first (take-right items 1)))))) (define variable-name (second old)) (define version (and=> ((sxpath '(// version *any*)) new) first)) (format port "gnu: ~a: Update to ~a.~%~%* ~a (~a): Update to ~a.~%" variable-name version file-name variable-name version) (for-each (lambda (field) (let ((old-values (get-values old field)) (new-values (get-values new field))) (or (equal? old-values new-values) (let ((removed (lset-difference equal? old-values new-values)) (added (lset-difference equal? new-values old-values))) (format port "[~a]: ~a~%" field (match (list (map symbol->string removed) (map symbol->string added)) ((() added) (format #f "Add ~a." (listify added))) ((removed ()) (format #f "Remove ~a." (listify removed))) ((removed added) (format #f "Remove ~a; add ~a." (listify removed) (listify added))))))))) '(inputs propagated-inputs native-inputs))) (define (group-hunks-by-sexp hunks) "Return a list of pairs associating all hunks with the S-expression they are modifying." (fold (lambda (sexp hunk acc) (match acc (((previous-sexp . hunks) . rest) (if (equal? sexp previous-sexp) (cons (cons previous-sexp (cons hunk hunks)) rest) (cons (cons sexp (list hunk)) acc))) (_ (cons (cons sexp (list hunk)) acc)))) '() (map new-sexp hunks) hunks)) (define (new+old+hunks hunks) (map (match-lambda ((new . hunks) (cons* new (old-sexp (first hunks)) hunks))) (group-hunks-by-sexp hunks))) (define (main . args) (match (diff-info) (() (display "Nothing to be done." (current-error-port))) (hunks (for-each (match-lambda ((new old . hunks) (for-each (lambda (hunk) (let ((port (open-pipe* OPEN_WRITE "git" "apply" "--cached" "--unidiff-zero"))) (hunk->patch hunk port) (unless (eqv? 0 (status:exit-val (close-pipe port))) (error "Cannot apply"))) (sleep 1)) hunks) (commit-message (hunk-file-name (first hunks)) old new (current-output-port)) (let ((port (open-pipe* OPEN_WRITE "git" "commit" "-F" "-"))) (commit-message (hunk-file-name (first hunks)) old new port) (sleep 1) (unless (eqv? 0 (status:exit-val (close-pipe port))) (error "Cannot commit"))))) (new+old+hunks hunks))))) (main)