aboutsummaryrefslogtreecommitdiff
path: root/tests/containers.scm
blob: 70d5ba2d3093774357ebfcbcc063e76462f32aa5 (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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2015 David Thompson <davet@gnu.org>
;;; Copyright © 2016, 2017, 2019, 2023 Ludovic Courtès <ludo@gnu.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 (test-containers)
  #:use-module (guix utils)
  #:use-module (guix build syscalls)
  #:use-module (gnu build linux-container)
  #:use-module ((gnu system linux-container)
                #:select (eval/container))
  #:use-module (gnu system file-systems)
  #:use-module (guix store)
  #:use-module (guix monads)
  #:use-module (guix gexp)
  #:use-module (guix derivations)
  #:use-module (guix tests)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-64)
  #:use-module (ice-9 match)
  #:use-module ((ice-9 ftw) #:select (scandir)))

(define (assert-exit x)
  (primitive-exit (if x 0 1)))

(test-begin "containers")

;; Skip these tests unless user namespaces are available and the setgroups
;; file (introduced in Linux 3.19 to address a security issue) exists.
(define (skip-if-unsupported)
  (unless (and (user-namespace-supported?)
               (unprivileged-user-namespace-supported?)
               (setgroups-supported?))
    (test-skip 1)))

(skip-if-unsupported)
(test-assert "call-with-container, exit with 0 when there is no error"
  (zero?
   (call-with-container '() (const #t) #:namespaces '(user))))

(skip-if-unsupported)
(test-assert "call-with-container, user namespace"
  (zero?
   (call-with-container '()
     (lambda ()
       ;; The user is root within the new user namespace.
       (assert-exit (and (zero? (getuid)) (zero? (getgid)))))
     #:namespaces '(user))))

(skip-if-unsupported)
(test-assert "call-with-container, user namespace, guest UID/GID"
  (zero?
   (call-with-container '()
     (lambda ()
       (assert-exit (and (= 42 (getuid)) (= 77 (getgid)))))
     #:guest-uid 42
     #:guest-gid 77
     #:namespaces '(user))))

(skip-if-unsupported)
(test-assert "call-with-container, uts namespace"
  (zero?
   (call-with-container '()
     (lambda ()
       ;; The user is root within the container and should be able to change
       ;; the hostname of that container.
       (sethostname "test-container")
       (primitive-exit 0))
     #:namespaces '(user uts))))

(skip-if-unsupported)
(test-assert "call-with-container, pid namespace"
  (zero?
   (call-with-container '()
     (lambda ()
       (match (primitive-fork)
         (0
          ;; The first forked process in the new pid namespace is pid 2.
          (assert-exit (= 2 (getpid))))
         (pid
          (primitive-exit
           (match (waitpid pid)
             ((_ . status)
              (status:exit-val status)))))))
     #:namespaces '(user pid))))

(skip-if-unsupported)
(test-assert "call-with-container, mnt namespace"
  (zero?
   (call-with-container (list (file-system
                                (device "none")
                                (mount-point "/testing")
                                (type "tmpfs")
                                (check? #f)))
     (lambda ()
       (assert-exit (file-exists? "/testing")))
     #:namespaces '(user mnt))))

(skip-if-unsupported)
(test-equal "call-with-container, mnt namespace, wrong bind mount"
  `(system-error ,ENOENT)
  ;; An exception should be raised; see <http://bugs.gnu.org/23306>.
  (catch 'system-error
    (lambda ()
      (call-with-container (list (file-system
                                   (device "/does-not-exist")
                                   (mount-point "/foo")
                                   (type "none")
                                   (flags '(bind-mount))
                                   (check? #f)))
        (const #t)
        #:namespaces '(user mnt)))
    (lambda args
      (list 'system-error (system-error-errno args)))))

(skip-if-unsupported)
(test-assert "call-with-container, all namespaces"
  (zero?
   (call-with-container '()
     (lambda ()
       (primitive-exit 0)))))

(skip-if-unsupported)
(test-assert "call-with-container, mnt namespace, root permissions"
  (zero?
   (call-with-container '()
     (lambda ()
       (assert-exit (= #o755 (stat:perms (lstat "/")))))
     #:namespaces '(user mnt))))

(skip-if-unsupported)
(test-assert "container-excursion"
  (call-with-temporary-directory
   (lambda (root)
     ;; Two pipes: One for the container to signal that the test can begin,
     ;; and one for the parent to signal to the container that the test is
     ;; over.
     (match (list (pipe) (pipe))
       (((start-in . start-out) (end-in . end-out))
        (define (container)
          (close end-out)
          (close start-in)
          ;; Signal for the test to start.
          (write 'ready start-out)
          (close start-out)
          ;; Wait for test completion.
          (read end-in)
          (close end-in))

        (define (namespaces pid)
          (let ((pid (number->string pid)))
            (map (lambda (ns)
                   (readlink (string-append "/proc/" pid "/ns/" ns)))
                 '("user" "ipc" "uts" "net" "pid" "mnt"))))

        (let* ((pid (run-container root '() %namespaces 1 container))
               (container-namespaces (namespaces pid))
               (result
                (begin
                  (close start-out)
                  ;; Wait for container to be ready.
                  (read start-in)
                  (close start-in)
                  (container-excursion pid
                    (lambda ()
                      ;; Check that all of the namespace identifiers are
                      ;; the same as the container process.
                      (assert-exit
                       (equal? container-namespaces
                               (namespaces (getpid)))))))))
          (close end-in)
          ;; Stop the container.
          (write 'done end-out)
          (close end-out)
          (waitpid pid)
          (zero? result)))))))

(skip-if-unsupported)
(test-equal "container-excursion, same namespaces"
  42
  ;; The parent and child are in the same namespaces.  'container-excursion'
  ;; should notice that and avoid calling 'setns' since that would fail.
  (status:exit-val
   (container-excursion (getpid)
     (lambda ()
       (primitive-exit 42)))))

(skip-if-unsupported)
(test-assert "container-excursion*"
  (call-with-temporary-directory
   (lambda (root)
     (define (namespaces pid)
       (let ((pid (number->string pid)))
         (map (lambda (ns)
                (readlink (string-append "/proc/" pid "/ns/" ns)))
              '("user" "ipc" "uts" "net" "pid" "mnt"))))

     (let* ((pid    (run-container root '()
                                   %namespaces 1
                                   (lambda ()
                                     (sleep 100))))
            (expected (namespaces pid))
            (result (container-excursion* pid
                      (lambda ()
                        (namespaces 1)))))
       (kill pid SIGKILL)
       (equal? result expected)))))

(skip-if-unsupported)
(test-equal "container-excursion*, same namespaces"
  42
  (container-excursion* (getpid)
    (lambda ()
      (* 6 7))))

(skip-if-unsupported)
(test-equal "container-excursion*, /proc"
  '("1" "2")
  (call-with-temporary-directory
   (lambda (root)
     (let* ((pid    (run-container root '()
                                   %namespaces 1
                                   (lambda ()
                                     (sleep 100))))
            (result (container-excursion* pid
                      (lambda ()
                        ;; We expect to see exactly two processes in this
                        ;; namespace.
                        (scandir "/proc"
                                 (lambda (file)
                                   (char-set-contains?
                                    char-set:digit
                                    (string-ref file 0))))))))
       (kill pid SIGKILL)
       result))))

(skip-if-unsupported)
(test-equal "eval/container, exit status"
  42
  (let* ((store  (open-connection-for-tests))
         (status (run-with-store store
                   (eval/container #~(exit 42)))))
    (close-connection store)
    (status:exit-val status)))

(skip-if-unsupported)
(test-assert "eval/container, writable user mapping"
  (call-with-temporary-directory
   (lambda (directory)
     (define store
       (open-connection-for-tests))
     (define result
       (string-append directory "/r"))
     (define requisites*
       (store-lift requisites))

     (call-with-output-file result (const #t))
     (run-with-store store
       (mlet %store-monad ((status (eval/container
                                    #~(begin
                                        (use-modules (ice-9 ftw))
                                        (call-with-output-file "/result"
                                          (lambda (port)
                                            (write (scandir #$(%store-prefix))
                                                   port))))
                                    #:mappings
                                    (list (file-system-mapping
                                           (source result)
                                           (target "/result")
                                           (writable? #t)))))
                           (reqs   (requisites*
                                    (list (derivation->output-path
                                           (%guile-for-build))))))
         (close-connection store)
         (return (and (zero? (pk 'status status))
                      (lset= string=? (cons* "." ".." (map basename reqs))
                             (pk (call-with-input-file result read))))))))))

(skip-if-unsupported)
(test-assert "eval/container, non-empty load path"
  (call-with-temporary-directory
   (lambda (directory)
     (define store
       (open-connection-for-tests))
     (define result
       (string-append directory "/r"))
     (define requisites*
       (store-lift requisites))

     (mkdir result)
     (run-with-store store
       (mlet %store-monad ((status (eval/container
                                    (with-imported-modules '((guix build utils))
                                      #~(begin
                                          (use-modules (guix build utils))
                                          (mkdir-p "/result/a/b/c")))
                                    #:mappings
                                    (list (file-system-mapping
                                           (source result)
                                           (target "/result")
                                           (writable? #t))))))
         (close-connection store)
         (return (and (zero? status)
                      (file-is-directory?
                       (string-append result "/a/b/c")))))))))

(test-end)
-parted-object user-partitions partition))) (partition-description partition user-partition))) partitions)) (padded-descriptions (if (null? partitions) '() (pad-descriptions descriptions)))) (map (cut string-join <> " ") padded-descriptions))) (define (user-partition-description user-partition) "Return a string describing the given USER-PARTITION record." (let* ((partition (user-partition-parted-object user-partition)) (disk (partition-disk partition)) (disk-type (disk-disk-type disk)) (device (disk-device disk)) (has-name? (disk-type-check-feature disk-type DISK-TYPE-FEATURE-PARTITION-NAME)) (has-extended? (disk-type-check-feature disk-type DISK-TYPE-FEATURE-EXTENDED)) (name (user-partition-name user-partition)) (type (user-partition-type user-partition)) (type-name (symbol->string type)) (fs-type (user-partition-fs-type user-partition)) (fs-type-name (user-fs-type-name fs-type)) (bootable? (user-partition-bootable? user-partition)) (esp? (user-partition-esp? user-partition)) (need-formatting? (user-partition-need-formatting? user-partition)) (crypt-label (user-partition-crypt-label user-partition)) (size (user-partition-size user-partition)) (mount-point (user-partition-mount-point user-partition))) `(,@(if has-name? `((name . ,(format #f (G_ "Name: ~a") (or name (G_ "None"))))) '()) ,@(if (and has-extended? (freespace-partition? partition) (not (eq? type 'logical))) `((type . ,(format #f (G_ "Type: ~a") type-name))) '()) ,@(if (eq? type 'extended) '() `((fs-type . ,(format #f (G_ "File system type: ~a") fs-type-name)))) ,@(if (or (eq? type 'extended) (eq? fs-type 'swap) (not has-extended?)) '() `((bootable . ,(format #f (G_ "Bootable flag: ~:[off~;on~]") bootable?)))) ,@(if (and (not has-extended?) (not (eq? fs-type 'swap))) `((esp? . ,(format #f (G_ "ESP flag: ~:[off~;on~]") esp?))) '()) ,@(if (freespace-partition? partition) (let ((size-formatted (or size (unit-format device ;XXX: i18n (partition-length partition))))) `((size . ,(format #f (G_ "Size: ~a") size-formatted)))) '()) ,@(if (or (eq? type 'extended) (eq? fs-type 'swap)) '() `((crypt-label . ,(format #f (G_ "Encryption: ~:[No~a~;Yes (label '~a')~]") crypt-label (or crypt-label ""))))) ,@(if (or (freespace-partition? partition) (eq? fs-type 'swap)) '() `((need-formatting? . ,(format #f (G_ "Format the partition? ~:[No~;Yes~]") need-formatting?)))) ,@(if (or (eq? type 'extended) (eq? fs-type 'swap)) '() `((mount-point . ,(format #f (G_ "Mount point: ~a") (or mount-point (and esp? (default-esp-mount-point)) (G_ "None"))))))))) ;; ;; Partition table creation. ;; (define (mklabel device type-name) "Create a partition table on DEVICE. TYPE-NAME is the type of the partition table, \"msdos\" or \"gpt\"." (let* ((type (disk-type-get type-name)) (disk (disk-new-fresh device type))) (or disk (raise (condition (&error) (&message (message (format #f "Cannot create partition table of type ~a on device ~a." type-name (device-path device))))))))) ;; ;; Partition creation. ;; ;; The maximum count of primary partitions is exceeded. (define-condition-type &max-primary-exceeded &condition max-primary-exceeded?) ;; It is not possible to create an extended partition. (define-condition-type &extended-creation-error &condition extended-creation-error?) ;; It is not possible to create a logical partition. (define-condition-type &logical-creation-error &condition logical-creation-error?) (define (can-create-primary? disk) "Return #t if it is possible to create a primary partition on DISK, return #f otherwise." (let ((max-primary (disk-get-max-primary-partition-count disk))) (find (lambda (number) (not (disk-get-partition disk number))) (iota max-primary 1)))) (define (can-create-extended? disk) "Return #t if it is possible to create an extended partition on DISK, return #f otherwise." (let* ((disk-type (disk-disk-type disk)) (has-extended? (disk-type-check-feature disk-type DISK-TYPE-FEATURE-EXTENDED))) (and (can-create-primary? disk) has-extended? (not (disk-extended-partition disk))))) (define (can-create-logical? disk) "Return #t is it is possible to create a logical partition on DISK, return #f otherwise." (let* ((disk-type (disk-disk-type disk)) (has-extended? (disk-type-check-feature disk-type DISK-TYPE-FEATURE-EXTENDED))) (and has-extended? (disk-extended-partition disk)))) (define (can-create-partition? user-part) "Return #t if it is possible to create the given USER-PART record, return #f otherwise." (let* ((type (user-partition-type user-part)) (partition (user-partition-parted-object user-part)) (disk (partition-disk partition))) (case type ((normal) (or (can-create-primary? disk) (raise (condition (&max-primary-exceeded))))) ((extended) (or (can-create-extended? disk) (raise (condition (&extended-creation-error))))) ((logical) (or (can-create-logical? disk) (raise (condition (&logical-creation-error)))))))) (define* (mkpart disk user-partition #:key (previous-partition #f)) "Create the given USER-PARTITION on DISK. The PREVIOUS-PARTITION argument as to be set to the partition preceding USER-PARTITION if any." (define (parse-start-end start end) "Parse start and end strings as positions on DEVICE expressed with a unit, like '100GB' or '12.2KiB'. Return a list of 4 elements, the start sector, its range (1 unit large area centered on start sector), the end sector and its range." (let ((device (disk-device disk))) (call-with-values (lambda () (unit-parse start device)) (lambda (start-sector start-range) (call-with-values (lambda () (unit-parse end device)) (lambda (end-sector end-range) (list start-sector start-range end-sector end-range))))))) (define* (extend-ranges! start-range end-range #:key (offset 0)) "Try to extend START-RANGE by 1 MEBIBYTE to the right and END-RANGE by 1 MEBIBYTE to the left. This way, if the disk is aligned on 2048 sectors of 512KB (like frequently), we will have a chance for the 'optimal-align-constraint' to succeed. Do not extend ranges if that would cause them to cross." (let* ((device (disk-device disk)) (start-range-end (geometry-end start-range)) (end-range-start (geometry-start end-range)) (mebibyte-sector-size (/ MEBIBYTE-SIZE (device-sector-size device))) (new-start-range-end (+ start-range-end mebibyte-sector-size offset)) (new-end-range-start (- end-range-start mebibyte-sector-size offset))) (when (< new-start-range-end new-end-range-start) (geometry-set-end start-range new-start-range-end) (geometry-set-start end-range new-end-range-start)))) (match (parse-start-end (user-partition-start user-partition) (user-partition-end user-partition)) ((start-sector start-range end-sector end-range) (let* ((prev-end (if previous-partition (partition-end previous-partition) 0)) (start-distance (- start-sector prev-end)) (type (user-partition-type user-partition)) ;; There should be at least 2 unallocated sectors in front of each ;; logical partition, otherwise parted will fail badly: ;; https://gparted.org/h2-fix-msdos-pt.php#apply-action-fail. (start-offset (if previous-partition (- 3 start-distance) 0)) (start-sector* (if (and (eq? type 'logical) (< start-distance 3)) (+ start-sector start-offset) start-sector))) ;; This is a hack. Parted almost always fails to create optimally ;; aligned partitions (unless specifying percentages) because the ;; default range of 1MB centered on the start sector is not enough when ;; the optimal alignment is 2048 sectors of 512KB. (extend-ranges! start-range end-range #:offset start-offset) (let* ((device (disk-device disk)) (disk-type (disk-disk-type disk)) (length (device-length device)) (name (user-partition-name user-partition)) (filesystem-type (filesystem-type-get (user-fs-type-name (user-partition-fs-type user-partition)))) (flags `(,@(if (user-partition-bootable? user-partition) `(,PARTITION-FLAG-BOOT) '()) ,@(if (user-partition-esp? user-partition) `(,PARTITION-FLAG-ESP) '()) ,@(if (user-partition-bios-grub? user-partition) `(,PARTITION-FLAG-BIOS-GRUB) '()))) (has-name? (disk-type-check-feature disk-type DISK-TYPE-FEATURE-PARTITION-NAME)) (partition-type (partition-type->int type)) (partition (partition-new disk #:type partition-type #:filesystem-type filesystem-type #:start start-sector* #:end end-sector)) (user-constraint (constraint-new #:start-align 'any #:end-align 'any #:start-range start-range #:end-range end-range #:min-size 1 #:max-size length)) (dev-constraint (device-get-optimal-aligned-constraint device)) (final-constraint (constraint-intersect user-constraint dev-constraint)) (no-constraint (constraint-any device)) ;; Try to create a partition with an optimal alignment ;; constraint. If it fails, fallback to creating a partition ;; with no specific constraint. (partition-constraint-ok? (disk-add-partition disk partition final-constraint)) (partition-no-contraint-ok? (or partition-constraint-ok? (disk-add-partition disk partition no-constraint))) (partition-ok? (or partition-constraint-ok? partition-no-contraint-ok?))) (installer-log-line "Creating partition:") (installer-log-line "~/type: ~a" partition-type) (installer-log-line "~/filesystem-type: ~a" (filesystem-type-name filesystem-type)) (installer-log-line "~/flags: ~a" flags) (installer-log-line "~/start: ~a" start-sector*) (installer-log-line "~/end: ~a" end-sector) (installer-log-line "~/start-range: [~a, ~a]" (geometry-start start-range) (geometry-end start-range)) (installer-log-line "~/end-range: [~a, ~a]" (geometry-start end-range) (geometry-end end-range)) (installer-log-line "~/constraint: ~a" partition-constraint-ok?) (installer-log-line "~/no-constraint: ~a" partition-no-contraint-ok?) ;; Set the partition name if supported. (when (and partition-ok? has-name? name) (partition-set-name partition name)) ;; Both partition-set-system and partition-set-flag calls can affect ;; the partition type. Their order is important, see: ;; https://issues.guix.gnu.org/55549. (partition-set-system partition filesystem-type) ;; Set flags if required. (for-each (lambda (flag) (and (partition-is-flag-available? partition flag) (partition-set-flag partition flag 1))) flags) (and partition-ok? partition)))))) ;; ;; Partition destruction. ;; (define (rmpart disk number) "Remove the partition with the given NUMBER on DISK." (let ((partition (disk-get-partition disk number))) (disk-remove-partition* disk partition))) ;; ;; Auto partitionning. ;; (define* (create-adjacent-partitions! disk partitions #:key (last-partition-end 0)) "Create the given PARTITIONS on DISK. LAST-PARTITION-END is the sector from which we want to start creating partitions. The START and END of each created partition are computed from its SIZE value and the position of the last partition." (let ((device (disk-device disk))) (let loop ((partitions partitions) (remaining-space (- (device-length device) last-partition-end)) (start last-partition-end)) (match partitions (() '()) ((partition . rest) (let* ((size (user-partition-size partition)) (percentage-size (and (string? size) (read-percentage size))) (sector-size (device-sector-size device)) (partition-size (if percentage-size (exact->inexact (* (/ percentage-size 100) remaining-space)) size)) (end-partition (min (- (device-length device) 1) (nearest-exact-integer (+ start partition-size 1)))) (name (user-partition-name partition)) (type (user-partition-type partition)) (fs-type (user-partition-fs-type partition)) (start-formatted (unit-format-custom device start UNIT-SECTOR)) (end-formatted (unit-format-custom device end-partition UNIT-SECTOR)) (new-user-partition (user-partition (inherit partition) (start start-formatted) (end end-formatted))) (new-partition (mkpart disk new-user-partition))) (if new-partition (cons (user-partition (inherit new-user-partition) (file-name (partition-get-path new-partition)) (disk-file-name (device-path device)) (parted-object new-partition)) (loop rest (if (eq? type 'extended) remaining-space (- remaining-space (partition-length new-partition))) (if (eq? type 'extended) (+ start 1) (+ (partition-end new-partition) 1)))) (error (format #f "Unable to create partition ~a~%" name))))))))) (define (force-user-partitions-formatting user-partitions) "Set the NEED-FORMATTING? fields to #t on all <user-partition> records of USER-PARTITIONS list and return the updated list." (map (lambda (p) (user-partition (inherit p) (need-formatting? #t))) user-partitions)) (define* (auto-partition! disk #:key (scheme 'entire-root)) "Automatically create partitions on DISK. All the previous partitions (except the ESP on a GPT disk, if present) are wiped. SCHEME is the desired partitioning scheme. It can be 'entire-root or 'entire-root-home. 'entire-root will create a swap partition and a root partition occupying all the remaining space. 'entire-root-home will create a swap partition, a root partition and a home partition. Return the complete list of partitions on DISK, including the ESP when it exists." (let* ((device (disk-device disk)) (disk-type (disk-disk-type disk)) (has-extended? (disk-type-check-feature disk-type DISK-TYPE-FEATURE-EXTENDED)) (partitions (filter data-partition? (disk-partitions disk))) (esp-partition (find-esp-partition partitions)) ;; According to ;; https://wiki.archlinux.org/index.php/EFI_system_partition, the ESP ;; size should be at least 550MiB. (new-esp-size (nearest-exact-integer (/ (* 550 MEBIBYTE-SIZE) (device-sector-size device)))) (end-esp-partition (and esp-partition (partition-end esp-partition))) (non-boot-partitions (remove esp-partition? partitions)) (bios-grub-size (/ (* 3 MEBIBYTE-SIZE) (device-sector-size device))) (five-percent-disk (nearest-exact-integer (* 0.05 (device-length device)))) (default-swap-size (nearest-exact-integer (/ (* 4 GIGABYTE-SIZE) (device-sector-size device)))) ;; Use a 4GB size for the swap if it represents less than 5% of the ;; disk space. Otherwise, set the swap size to 5% of the disk space. (swap-size (min default-swap-size five-percent-disk))) ;; Remove everything but esp if it exists. (for-each (lambda (partition) (and (data-partition? partition) ;; Do not remove logical partitions ourselves, since ;; disk-remove-partition* will remove all the logical partitions ;; residing on an extended partition, which would lead to a ;; double-remove and ensuing SEGFAULT. (not (logical-partition? partition)) (disk-remove-partition* disk partition))) non-boot-partitions) (let* ((start-partition (cond ((target-hurd?) #f) ((efi-installation?) (and (not esp-partition) (user-partition (fs-type 'fat32) (esp? #t) (size new-esp-size) (mount-point (default-esp-mount-point))))) (else (user-partition (fs-type 'ext4) (bootable? #t) (bios-grub? #t) (size bios-grub-size))))) (new-partitions (cond ((or (eq? scheme 'entire-root) (eq? scheme 'entire-encrypted-root)) (let ((encrypted? (eq? scheme 'entire-encrypted-root))) `(,@(if start-partition `(,start-partition) '()) ,@(if (or encrypted? (target-hurd?)) '() `(,(user-partition (fs-type 'swap) (size swap-size)))) ,(user-partition (fs-type (if (target-hurd?) 'ext2 'ext4)) (bootable? has-extended?) (crypt-label (and encrypted? "cryptroot")) (size "100%") (mount-point "/"))))) ((or (eq? scheme 'entire-root-home) (eq? scheme 'entire-encrypted-root-home)) (let ((encrypted? (eq? scheme 'entire-encrypted-root-home))) `(,@(if start-partition `(,start-partition) '()) ,(user-partition (fs-type (if (target-hurd?) 'ext2 'ext4)) (bootable? has-extended?) (crypt-label (and encrypted? "cryptroot")) (size "33%") (mount-point "/")) ,@(if has-extended? `(,(user-partition (type 'extended) (size "100%"))) '()) ,@(if encrypted? '() `(,(user-partition (type (if has-extended? 'logical 'normal)) (fs-type 'swap) (size swap-size)))) ,(user-partition (type (if has-extended? 'logical 'normal)) (fs-type (if (target-hurd?) 'ext2 'ext4)) (crypt-label (and encrypted? "crypthome")) (size "100%") (mount-point "/home"))))))) (new-partitions* (force-user-partitions-formatting new-partitions))) (append (if esp-partition (list (partition->user-partition esp-partition)) '()) (create-adjacent-partitions! disk new-partitions* #:last-partition-end (or end-esp-partition 0)))))) ;; ;; Convert user-partitions. ;; ;; No root mount point found. (define-condition-type &no-root-mount-point &condition no-root-mount-point?) ;; Cannot not read the partition UUID. (define-condition-type &cannot-read-uuid &condition cannot-read-uuid? (partition cannot-read-uuid-partition)) (define (check-user-partitions user-partitions) "Check the following statements: The USER-PARTITIONS list contains one <user-partition> record with a mount-point set to '/'. Raise &no-root-mount-point condition otherwise. All the USER-PARTITIONS with a mount point and that will not be formatted have a valid UUID. Raise a &cannot-read-uuid condition specifying the faulty partition otherwise. Return #t if all the statements are valid." (define (check-root) (let ((mount-points (map user-partition-mount-point user-partitions))) (or (member "/" mount-points) (raise (condition (&no-root-mount-point)))))) (define (check-uuid) (let ((mount-partitions (filter user-partition-mount-point user-partitions))) (every (lambda (user-partition) (let ((file-name (user-partition-file-name user-partition)) (need-formatting? (user-partition-need-formatting? user-partition))) (or need-formatting? (read-partition-uuid/retry file-name) (raise (condition (&cannot-read-uuid (partition file-name))))))) mount-partitions))) (and (check-root) (check-uuid) #t)) (define (set-user-partitions-file-name user-partitions) "Set the partition file-name of <user-partition> records in USER-PARTITIONS list and return the updated list." (map (lambda (p) (let* ((partition (user-partition-parted-object p)) (file-name (partition-get-path partition))) (user-partition (inherit p) (file-name file-name)))) user-partitions)) (define (create-btrfs-file-system partition) "Create a btrfs file-system for PARTITION file-name." ((%run-command-in-installer) "mkfs.btrfs" "-f" partition)) (define (create-ext2-file-system partition) "Create an ext2 file-system for PARTITION file-name, when TARGET-HURD?, for the Hurd." (apply (%run-command-in-installer) `("mkfs.ext2" ,@(if (target-hurd?) '("-o" "hurd") '()) "-F" ,partition))) (define (create-ext4-file-system partition) "Create an ext4 file-system for PARTITION file-name." ;; Enable the 'large_dir' feature so users can have a store of several TiBs. ;; Failing to do that, the directory index (enabled by 'dir_index') can fill ;; up and adding new files would fail with ENOSPC despite there being plenty ;; of free space and inodes: ;; <https://blog.merovius.de/posts/2013-10-20-ext4-mysterious-no-space-left-on/>. ((%run-command-in-installer) "mkfs.ext4" "-F" partition "-O" "large_dir")) (define (create-fat16-file-system partition) "Create a fat16 file-system for PARTITION file-name." ((%run-command-in-installer) "mkfs.fat" "-F16" partition)) (define (create-fat32-file-system partition) "Create a fat32 file-system for PARTITION file-name." ((%run-command-in-installer) "mkfs.fat" "-F32" partition)) (define (create-jfs-file-system partition) "Create a JFS file-system for PARTITION file-name." ((%run-command-in-installer) "jfs_mkfs" "-f" partition)) (define (create-ntfs-file-system partition) "Create a JFS file-system for PARTITION file-name." ((%run-command-in-installer) "mkfs.ntfs" "-F" "-f" partition)) (define (create-xfs-file-system partition) "Create an XFS file-system for PARTITION file-name." ((%run-command-in-installer) "mkfs.xfs" "-f" partition)) (define (create-swap-partition partition) "Set up swap area on PARTITION file-name." ((%run-command-in-installer) "mkswap" "-f" partition)) (define (call-with-luks-key-file password proc) "Write PASSWORD in a temporary file and pass it to PROC as argument." (call-with-temporary-output-file (lambda (file port) (put-string port password) (close port) (proc file)))) (define (user-partition-upper-file-name user-partition) "Return the file-name of the virtual block device corresponding to USER-PARTITION if it is encrypted, or the plain file-name otherwise." (let ((crypt-label (user-partition-crypt-label user-partition)) (file-name (user-partition-file-name user-partition))) (if crypt-label (string-append "/dev/mapper/" crypt-label) file-name))) (define (luks-format-and-open user-partition) "Format and open the encrypted partition pointed by USER-PARTITION." (let* ((file-name (user-partition-file-name user-partition)) (label (user-partition-crypt-label user-partition)) (password (secret-content (user-partition-crypt-password user-partition)))) (call-with-luks-key-file password (lambda (key-file) (installer-log-line "formatting and opening LUKS entry ~s at ~s" label file-name) ((%run-command-in-installer) "cryptsetup" "-q" "luksFormat" file-name key-file) ((%run-command-in-installer) "cryptsetup" "open" "--type" "luks" "--key-file" key-file file-name label))))) (define (luks-ensure-open user-partition) "Ensure partition pointed by USER-PARTITION is opened." (unless (file-exists? (user-partition-upper-file-name user-partition)) (let* ((file-name (user-partition-file-name user-partition)) (label (user-partition-crypt-label user-partition)) (password (secret-content (user-partition-crypt-password user-partition)))) (call-with-luks-key-file password (lambda (key-file) (installer-log-line "opening LUKS entry ~s at ~s" label file-name) ((%run-command-in-installer) "cryptsetup" "open" "--type" "luks" "--key-file" key-file file-name label)))))) (define (luks-close user-partition) "Close the encrypted partition pointed by USER-PARTITION." (let ((label (user-partition-crypt-label user-partition))) (installer-log-line "closing LUKS entry ~s" label) ((%run-command-in-installer) "cryptsetup" "close" label))) (define (format-user-partitions user-partitions) "Format the <user-partition> records in USER-PARTITIONS list with NEED-FORMATTING? field set to #t." (for-each (lambda (user-partition) (let* ((need-formatting? (user-partition-need-formatting? user-partition)) (type (user-partition-type user-partition)) (crypt-label (user-partition-crypt-label user-partition)) (file-name (user-partition-upper-file-name user-partition)) (fs-type (user-partition-fs-type user-partition))) (when crypt-label (luks-format-and-open user-partition)) (case fs-type ((btrfs) (and need-formatting? (not (eq? type 'extended)) (create-btrfs-file-system file-name))) ((ext2) (and need-formatting? (not (eq? type 'extended)) (create-ext2-file-system file-name))) ((ext4) (and need-formatting? (not (eq? type 'extended)) (create-ext4-file-system file-name))) ((fat16) (and need-formatting? (not (eq? type 'extended)) (create-fat16-file-system file-name))) ((fat32) (and need-formatting? (not (eq? type 'extended)) (create-fat32-file-system file-name))) ((jfs) (and need-formatting? (not (eq? type 'extended)) (create-jfs-file-system file-name))) ((ntfs) (and need-formatting? (not (eq? type 'extended)) (create-ntfs-file-system file-name))) ((xfs) (and need-formatting? (not (eq? type 'extended)) (create-xfs-file-system file-name))) ((swap) (create-swap-partition file-name)) (else ;; TODO: Add support for other file-system types. #t)))) user-partitions)) (define (sort-partitions user-partitions) "Sort USER-PARTITIONS by mount-points, so that the more nested mount-point comes last. This is useful to mount/umount partitions in a coherent order." (sort user-partitions (lambda (a b) (let ((mount-point-a (user-partition-mount-point a)) (mount-point-b (user-partition-mount-point b))) (string-prefix? mount-point-a mount-point-b))))) (define (mount-user-partitions user-partitions) "Mount the <user-partition> records in USER-PARTITIONS list on their respective mount-points." (let* ((mount-partitions (filter user-partition-mount-point user-partitions)) (sorted-partitions (sort-partitions mount-partitions))) (for-each (lambda (user-partition) (let* ((mount-point (user-partition-mount-point user-partition)) (target (string-append (%installer-target-dir) mount-point)) (fs-type (user-partition-fs-type user-partition)) (crypt-label (user-partition-crypt-label user-partition)) (mount-type (user-fs-type->mount-type fs-type)) (file-name (user-partition-upper-file-name user-partition))) (when crypt-label (luks-ensure-open user-partition)) (mkdir-p target) (installer-log-line "mounting ~s on ~s" file-name target) (mount file-name target mount-type))) sorted-partitions))) (define (umount-user-partitions user-partitions) "Unmount all the <user-partition> records in USER-PARTITIONS list." (let* ((mount-partitions (filter user-partition-mount-point user-partitions)) (sorted-partitions (sort-partitions mount-partitions))) (for-each (lambda (user-partition) (let* ((mount-point (user-partition-mount-point user-partition)) (crypt-label (user-partition-crypt-label user-partition)) (target (string-append (%installer-target-dir) mount-point))) (installer-log-line "unmounting ~s" target) (umount target) (when crypt-label (luks-close user-partition)))) (reverse sorted-partitions)))) (define (find-swap-user-partitions user-partitions) "Return the subset of <user-partition> records in USER-PARTITIONS list with the FS-TYPE field set to 'swap, return the empty list if none found." (filter (lambda (user-partition) (let ((fs-type (user-partition-fs-type user-partition))) (eq? fs-type 'swap))) user-partitions)) (define (start-swapping user-partitions) "Start swapping on <user-partition> records with FS-TYPE equal to 'swap." (let* ((swap-user-partitions (find-swap-user-partitions user-partitions)) (swap-devices (map user-partition-file-name swap-user-partitions))) (for-each swapon swap-devices))) (define (stop-swapping user-partitions) "Stop swapping on <user-partition> records with FS-TYPE equal to 'swap." (let* ((swap-user-partitions (find-swap-user-partitions user-partitions)) (swap-devices (map user-partition-file-name swap-user-partitions))) (for-each swapoff swap-devices))) (define-syntax-rule (with-mounted-partitions user-partitions exp ...) "Mount USER-PARTITIONS and start swapping within the dynamic extent of EXP." (dynamic-wind (lambda () (mount-user-partitions user-partitions) (start-swapping user-partitions)) (lambda () exp ...) (lambda () (umount-user-partitions user-partitions) (stop-swapping user-partitions) #f))) (define (user-partition->file-system user-partition) "Convert the given USER-PARTITION record in a FILE-SYSTEM record from (gnu system file-systems) module and return it." (let* ((mount-point (user-partition-mount-point user-partition)) (fs-type (user-partition-fs-type user-partition)) (crypt-label (user-partition-crypt-label user-partition)) (mount-type (user-fs-type->mount-type fs-type)) (file-name (user-partition-file-name user-partition)) (upper-file-name (user-partition-upper-file-name user-partition)) ;; Only compute uuid if partition is not encrypted. (uuid (or crypt-label (uuid->string (read-partition-uuid file-name) fs-type)))) `(file-system (mount-point ,mount-point) (device ,@(if crypt-label `(,upper-file-name) `((uuid ,uuid (quote ,fs-type))))) (type ,mount-type) ,@(if crypt-label '((dependencies mapped-devices)) '())))) (define (user-partitions->file-systems user-partitions) "Convert the given USER-PARTITIONS list of <user-partition> records into a list of <file-system> records." (filter-map (lambda (user-partition) (let ((mount-point (user-partition-mount-point user-partition))) (and mount-point (user-partition->file-system user-partition)))) user-partitions)) (define (user-partition->mapped-device user-partition) "Convert the given USER-PARTITION record into a MAPPED-DEVICE record from (gnu system mapped-devices) and return it." (let ((label (user-partition-crypt-label user-partition)) (file-name (user-partition-file-name user-partition))) `(mapped-device (source (uuid ,(uuid->string (read-luks-partition-uuid file-name) 'luks))) (target ,label) (type luks-device-mapping)))) (define (root-user-partition? partition) "Return true if PARTITION is the root partition." (let ((mount-point (user-partition-mount-point partition))) (and mount-point (string=? mount-point "/")))) (define (bootloader-configuration user-partitions) "Return the bootloader configuration field for USER-PARTITIONS." (let ((root-partition (find root-user-partition? user-partitions))) (match user-partitions (() (if (target-hurd?) '(bootloader-configuration (bootloader grub-minimal-bootloader) (targets "/dev/sdaX")) '())) (_ (let ((root-partition-disk (user-partition-disk-file-name root-partition))) `((bootloader-configuration ,@(if (efi-installation?) `((bootloader grub-efi-bootloader) (targets (list ,(default-esp-mount-point)))) `((bootloader ,(if (target-hurd?) 'grub-minimal-bootloader 'grub-bootloader)) (targets (list ,root-partition-disk)))) ;; XXX: Assume we defined the 'keyboard-layout' field of ;; <operating-system> right above. (keyboard-layout keyboard-layout)))))))) (define (user-partition-missing-modules user-partitions) "Return the list of kernel modules missing from the default set of kernel modules to access USER-PARTITIONS." (let ((devices (filter user-partition-crypt-label user-partitions)) (root (find root-user-partition? user-partitions))) (delete-duplicates (append-map (lambda (device) (catch 'system-error (lambda () (missing-modules device %base-initrd-modules)) (const '()))) (delete-duplicates (map user-partition-file-name (filter identity (cons root devices)))))))) (define (initrd-configuration user-partitions) "Return an 'initrd-modules' field with everything needed for USER-PARTITIONS, or return nothing." (if (target-hurd?) '((initrd #f) (initrd-modules '())) (match (user-partition-missing-modules user-partitions) (() '()) ((modules ...) `((initrd-modules (append ',modules %base-initrd-modules))))))) (define (user-partitions->configuration user-partitions) "Return the configuration field for USER-PARTITIONS." (let* ((swap-user-partitions (find-swap-user-partitions user-partitions)) (swap-devices (if (target-hurd?) '() (map user-partition-file-name swap-user-partitions))) (encrypted-partitions (filter user-partition-crypt-label user-partitions))) `((bootloader ,@(bootloader-configuration user-partitions)) ,@(initrd-configuration user-partitions) ,@(if (null? swap-devices) '() (let* ((uuids (map (lambda (file) (uuid->string (read-partition-uuid file))) swap-devices))) `((swap-devices (list ,@(map (lambda (uuid) `(swap-space (target (uuid ,uuid)))) uuids)))))) ,@(if (null? encrypted-partitions) '() `((mapped-devices (list ,@(map user-partition->mapped-device encrypted-partitions))))) ,(vertical-space 1) ,(let-syntax ((G_ (syntax-rules () ((_ str) str)))) (comment (G_ "\ ;; The list of file systems that get \"mounted\". The unique ;; file system identifiers there (\"UUIDs\") can be obtained ;; by running 'blkid' in a terminal.\n"))) (file-systems (cons* ,@(user-partitions->file-systems user-partitions) %base-file-systems))))) ;; ;; Initialization. ;; (define (init-parted) "Initialize libparted support." (probe-all-devices!) ;; Remove all logical devices, otherwise "device-is-busy?" will report true ;; on all devices containaing active logical volumes. (remove-logical-devices) (exception-set-handler (lambda (exception) EXCEPTION-OPTION-UNHANDLED))) (define (free-parted devices) "Deallocate memory used for DEVICES in parted, force sync them and wait for the devices not to be used before returning." ;; XXX: Formatting and further operations on disk partition table may fail ;; because the partition table changes are not synced, or because the device ;; is still in use, even if parted should have finished editing ;; partitions. This is not well understood, but syncing devices and waiting ;; them to stop returning EBUSY to BLKRRPART ioctl seems to be enough. The ;; same kind of issue is described here: ;; https://mail.gnome.org/archives/commits-list/2013-March/msg18423.html. (let ((device-file-names (map device-path devices))) (for-each force-device-sync devices) (for-each (lambda (file-name) (let/time ((time in-use? (with-delay-device-in-use? file-name))) (if in-use? (error (format #f (G_ "Device ~a is still in use.") file-name)) (installer-log-line "Syncing ~a took ~a seconds." file-name (time-second time))))) device-file-names)))