aboutsummaryrefslogtreecommitdiff
path: root/tests/pypi.scm
blob: f421d6d9dfc00b0ee9fd126bc6c6c63d395a4dfa (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
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2014 David Thompson <davet@gnu.org>
;;; Copyright © 2016 Ricardo Wurmus <rekado@elephly.net>
;;; Copyright © 2019 Maxim Cournoyer <maxim.cournoyer@gmail.com>
;;;
;;; 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-pypi)
  #:use-module (guix import pypi)
  #:use-module (guix base32)
  #:use-module (guix memoization)
  #:use-module (gcrypt hash)
  #:use-module (guix tests)
  #:use-module (guix build-system python)
  #:use-module ((guix build utils) #:select (delete-file-recursively which mkdir-p))
  #:use-module (srfi srfi-64)
  #:use-module (ice-9 match))

(define test-json
  "{
  \"info\": {
    \"version\": \"1.0.0\",
    \"name\": \"foo\",
    \"license\": \"GNU LGPL\",
    \"summary\": \"summary\",
    \"home_page\": \"http://example.com\",
    \"classifiers\": [],
    \"download_url\": \"\"
  },
  \"urls\": [],
  \"releases\": {
    \"1.0.0\": [
      {
        \"url\": \"https://example.com/foo-1.0.0.egg\",
        \"packagetype\": \"bdist_egg\"
      }, {
        \"url\": \"https://example.com/foo-1.0.0.tar.gz\",
        \"packagetype\": \"sdist\"
      }, {
        \"url\": \"https://example.com/foo-1.0.0-py2.py3-none-any.whl\",
        \"packagetype\": \"bdist_wheel\"
      }
    ]
  }
}")

(define test-source-hash
  "")

(define test-specifications
  '("Fizzy [foo, bar]"
    "PickyThing<1.6,>1.9,!=1.9.6,<2.0a0,==2.4c1"
    "SomethingWithMarker[foo]>1.0;python_version<\"2.7\""
    "requests [security,tests] >= 2.8.1, == 2.8.* ; python_version < \"2.7\""
    "pip @ https://github.com/pypa/pip/archive/1.3.1.zip#\
sha1=da9234ee9982d4bbb3c72346a6de940a148ea686"))

(define test-requires.txt "\
# A comment
 # A comment after a space
foo ~= 3
bar != 2

[test]
pytest (>=2.5.0)
")

;; Beaker contains only optional dependencies.
(define test-requires.txt-beaker "\
[crypto]
pycryptopp>=0.5.12

[cryptography]
cryptography

[testsuite]
Mock
coverage
")

(define test-metadata "\
Classifier: Programming Language :: Python :: 3.7
Requires-Dist: baz ~= 3
Requires-Dist: bar != 2
Provides-Extra: test
Requires-Dist: pytest (>=2.5.0) ; extra == 'test'
")

(define test-metadata-with-extras "
Classifier: Programming Language :: Python :: 3.7
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
Requires-Dist: wrapt (<2,>=1)
Requires-Dist: bar

Provides-Extra: dev
Requires-Dist: tox ; extra == 'dev'
Requires-Dist: bumpversion (<1) ; extra == 'dev'
")

;;; Provides-Extra can appear before Requires-Dist.
(define test-metadata-with-extras-jedi "\
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*
Provides-Extra: testing
Requires-Dist: parso (>=0.3.0)
Provides-Extra: testing
Requires-Dist: pytest (>=3.1.0); extra == 'testing'
")


(test-begin "pypi")

(test-equal "guix-package->pypi-name, old URL style"
  "psutil"
  (guix-package->pypi-name
   (dummy-package "foo"
                  (source (dummy-origin
                           (uri
                            "https://pypi.org/packages/source/p/psutil/psutil-4.3.0.tar.gz"))))))

(test-equal "guix-package->pypi-name, new URL style"
  "certbot"
  (guix-package->pypi-name
   (dummy-package "foo"
                  (source (dummy-origin
                           (uri
                            "https://pypi.org/packages/a2/3b/4756e6a0ceb14e084042a2a65c615d68d25621c6fd446d0fc10d14c4ce7d/certbot-0.8.1.tar.gz"))))))

(test-equal "guix-package->pypi-name, several URLs"
  "cram"
  (guix-package->pypi-name
   (dummy-package "foo"
                  (source
                   (dummy-origin
                    (uri (list "https://bitheap.org/cram/cram-0.7.tar.gz"
                               (pypi-uri "cram" "0.7"))))))))

(test-equal "specification->requirement-name"
  '("Fizzy" "PickyThing" "SomethingWithMarker" "requests" "pip")
  (map specification->requirement-name test-specifications))

(test-equal "parse-requires.txt"
  (list '("foo" "bar") '("pytest"))
  (mock ((ice-9 ports) call-with-input-file
         call-with-input-string)
        (parse-requires.txt test-requires.txt)))

(test-equal "parse-requires.txt - Beaker"
  (list '() '("Mock" "coverage"))
  (mock ((ice-9 ports) call-with-input-file
         call-with-input-string)
        (parse-requires.txt test-requires.txt-beaker)))

(test-equal "parse-wheel-metadata, with extras"
  (list '("wrapt" "bar") '("tox" "bumpversion"))
  (mock ((ice-9 ports) call-with-input-file
         call-with-input-string)
        (parse-wheel-metadata test-metadata-with-extras)))

(test-equal "parse-wheel-metadata, with extras - Jedi"
  (list '("parso") '("pytest"))
  (mock ((ice-9 ports) call-with-input-file
         call-with-input-string)
        (parse-wheel-metadata test-metadata-with-extras-jedi)))

(test-assert "pypi->guix-package, no wheel"
  ;; Replace network resources with sample data.
    (mock ((guix import utils) url-fetch
           (lambda (url file-name)
             (match url
               ("https://example.com/foo-1.0.0.tar.gz"
                (begin
                  ;; Unusual requires.txt location should still be found.
                  (mkdir-p "foo-1.0.0/src/bizarre.egg-info")
                  (with-output-to-file "foo-1.0.0/src/bizarre.egg-info/requires.txt"
                    (lambda ()
                      (display test-requires.txt)))
                  (parameterize ((current-output-port (%make-void-port "rw+")))
                    (system* "tar" "czvf" file-name "foo-1.0.0/"))
                  (delete-file-recursively "foo-1.0.0")
                  (set! test-source-hash
                    (call-with-input-file file-name port-sha256))))
               ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
               (_ (error "Unexpected URL: " url)))))
          (mock ((guix http-client) http-fetch
                 (lambda (url . rest)
                   (match url
                     ("https://pypi.org/pypi/foo/json"
                      (values (open-input-string test-json)
                              (string-length test-json)))
                     ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
                     (_ (error "Unexpected URL: " url)))))
                (match (pypi->guix-package "foo")
                  (('package
                     ('name "python-foo")
                     ('version "1.0.0")
                     ('source ('origin
                                ('method 'url-fetch)
                                ('uri ('pypi-uri "foo" 'version))
                                ('sha256
                                 ('base32
                                  (? string? hash)))))
                     ('build-system 'python-build-system)
                     ('propagated-inputs
                      ('quasiquote
                       (("python-bar" ('unquote 'python-bar))
                        ("python-foo" ('unquote 'python-foo)))))
                     ('native-inputs
                      ('quasiquote
                       (("python-pytest" ('unquote 'python-pytest)))))
                     ('home-page "http://example.com")
                     ('synopsis "summary")
                     ('description "summary")
                     ('license 'license:lgpl2.0))
                   (string=? (bytevector->nix-base32-string
                              test-source-hash)
                             hash))
                  (x
                   (pk 'fail x #f))))))

(test-skip (if (which "zip") 0 1))
(test-assert "pypi->guix-package, wheels"
  ;; Replace network resources with sample data.
  (mock ((guix import utils) url-fetch
         (lambda (url file-name)
           (match url
             ("https://example.com/foo-1.0.0.tar.gz"
              (begin
                (mkdir-p "foo-1.0.0/foo.egg-info/")
                (with-output-to-file "foo-1.0.0/foo.egg-info/requires.txt"
                  (lambda ()
                    (display "wrong data to make sure we're testing wheels ")))
                (parameterize ((current-output-port (%make-void-port "rw+")))
                  (system* "tar" "czvf" file-name "foo-1.0.0/"))
                (delete-file-recursively "foo-1.0.0")
                (set! test-source-hash
                  (call-with-input-file file-name port-sha256))))
             ("https://example.com/foo-1.0.0-py2.py3-none-any.whl"
              (begin
                (mkdir "foo-1.0.0.dist-info")
                (with-output-to-file "foo-1.0.0.dist-info/METADATA"
                  (lambda ()
                    (display test-metadata)))
                (let ((zip-file (string-append file-name ".zip")))
                  ;; zip always adds a "zip" extension to the file it creates,
                  ;; so we need to rename it.
                  (system* "zip" "-q" zip-file "foo-1.0.0.dist-info/METADATA")
                  (rename-file zip-file file-name))
                (delete-file-recursively "foo-1.0.0.dist-info")))
             (_ (error "Unexpected URL: " url)))))
        (mock ((guix http-client) http-fetch
               (lambda (url . rest)
                 (match url
                   ("https://pypi.org/pypi/foo/json"
                    (values (open-input-string test-json)
                            (string-length test-json)))
                   ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
                   (_ (error "Unexpected URL: " url)))))
              ;; Not clearing the memoization cache here would mean returning the value
              ;; computed in the previous test.
              (invalidate-memoization! pypi->guix-package)
              (match (pypi->guix-package "foo")
                (('package
                   ('name "python-foo")
                   ('version "1.0.0")
                   ('source ('origin
                              ('method 'url-fetch)
                              ('uri ('pypi-uri "foo" 'version))
                              ('sha256
                               ('base32
                                (? string? hash)))))
                   ('build-system 'python-build-system)
                   ('propagated-inputs
                    ('quasiquote
                     (("python-bar" ('unquote 'python-bar))
                      ("python-baz" ('unquote 'python-baz)))))
                   ('native-inputs
                    ('quasiquote
                     (("python-pytest" ('unquote 'python-pytest)))))
                   ('home-page "http://example.com")
                   ('synopsis "summary")
                   ('description "summary")
                   ('license 'license:lgpl2.0))
                 (string=? (bytevector->nix-base32-string
                            test-source-hash)
                           hash))
                (x
                 (pk 'fail x #f))))))

(test-assert "pypi->guix-package, no usable requirement file."
  ;; Replace network resources with sample data.
  (mock ((guix import utils) url-fetch
         (lambda (url file-name)
           (match url
             ("https://example.com/foo-1.0.0.tar.gz"
              (mkdir-p "foo-1.0.0/foo.egg-info/")
              (parameterize ((current-output-port (%make-void-port "rw+")))
                (system* "tar" "czvf" file-name "foo-1.0.0/"))
              (delete-file-recursively "foo-1.0.0")
              (set! test-source-hash
                (call-with-input-file file-name port-sha256)))
             ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
             (_ (error "Unexpected URL: " url)))))
        (mock ((guix http-client) http-fetch
               (lambda (url . rest)
                 (match url
                   ("https://pypi.org/pypi/foo/json"
                    (values (open-input-string test-json)
                            (string-length test-json)))
                   ("https://example.com/foo-1.0.0-py2.py3-none-any.whl" #f)
                   (_ (error "Unexpected URL: " url)))))
              ;; Not clearing the memoization cache here would mean returning the value
              ;; computed in the previous test.
              (invalidate-memoization! pypi->guix-package)
              (match (pypi->guix-package "foo")
                (('package
                   ('name "python-foo")
                   ('version "1.0.0")
                   ('source ('origin
                              ('method 'url-fetch)
                              ('uri ('pypi-uri "foo" 'version))
                              ('sha256
                               ('base32
                                (? string? hash)))))
                   ('build-system 'python-build-system)
                   ('home-page "http://example.com")
                   ('synopsis "summary")
                   ('description "summary")
                   ('license 'license:lgpl2.0))
                 (string=? (bytevector->nix-base32-string
                            test-source-hash)
                           hash))
                (x
                 (pk 'fail x #f))))))

(test-end "pypi")
00\x02\x00\x14")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) (define %ppc64 (qemu-platform (name "ppc64") (family "ppc") (magic (bv "\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x15")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) (define %ppc64le (qemu-platform (name "ppc64le") (family "ppcle") (magic (bv "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x15\x00")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\x00")))) (define %m68k (qemu-platform (name "m68k") (family "m68k") (magic (bv "\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x04")) (mask (bv "\xff\xff\xff\xff\xff\xff\xfe\xfe\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) ;; XXX: We could use the other endianness on a MIPS host. (define %mips (qemu-platform (name "mips") (family "mips") (magic (bv "\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) (define %mipsel (qemu-platform (name "mipsel") (family "mips") (magic (bv "\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) (define %mipsn32 (qemu-platform (name "mipsn32") (family "mips") (magic (bv "\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) (define %mipsn32el (qemu-platform (name "mipsn32el") (family "mips") (magic (bv "\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) (define %mips64 (qemu-platform (name "mips64") (family "mips") (magic (bv "\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) (define %mips64el (qemu-platform (name "mips64el") (family "mips") (magic (bv "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) (define %sh4 (qemu-platform (name "sh4") (family "sh4") (magic (bv "\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x2a\x00")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) (define %sh4eb (qemu-platform (name "sh4eb") (family "sh4") (magic (bv "\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x2a")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) (define %s390x (qemu-platform (name "s390x") (family "s390x") (magic (bv "\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x16")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) (define %aarch64 (qemu-platform (name "aarch64") (family "arm") (magic (bv "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) (define %aarch64be (qemu-platform (name "aarch64be") (family "armeb") (magic (bv "\x7fELF\x02\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) (define %hppa (qemu-platform (name "hppa") (family "hppa") (magic (bv "\x7f\x45\x4c\x46\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x0f")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) (define %riscv32 (qemu-platform (name "riscv32") (family "riscv") (magic (bv "\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) (define %riscv64 (qemu-platform (name "riscv64") (family "riscv") (magic (bv "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) (define %xtensa (qemu-platform (name "xtensa") (family "xtensa") (magic (bv "\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x5e\x00")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) (define %xtensaeb (qemu-platform (name "xtensaeb") (family "xtensaeb") (magic (bv "\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x5e")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) (define %microblaze (qemu-platform (name "microblaze") (family "microblaze") (magic (bv "\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xba\xab")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) (define %microblazeel (qemu-platform (name "microblazeel") (family "microblazeel") (magic (bv "\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xab\xba")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) (define %or1k (qemu-platform (name "or1k") (family "or1k") (magic (bv "\x7fELF\x01\x02\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x5c")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff")))) (define %hexagon (qemu-platform (name "hexagon") (family "hexagon") (magic (bv "\x7fELF\x01\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xa4\x00")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) (define %loongarch64 (qemu-platform (name "loongarch64") (family "loongarch") (magic (bv "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x02\x01")) (mask (bv "\xff\xff\xff\xff\xff\xff\xff\xfc\x00\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff")))) ;; binfmt_misc: register: failed to install interpreter file /gnu/store/... ;; i486 and aarch64be aren't supported by the qemu-binfmt service. (define %qemu-platforms (list %i386 %x86_64 %alpha %arm %sparc32plus %sparc64 %ppc %ppc64 %ppc64le %m68k %mips %mipsel %mipsn32 %mipsn32el %mips64 %mips64el %sh4 %sh4eb %s390x %aarch64 %hppa %riscv32 %riscv64 %xtensa %xtensaeb %microblaze %microblazeel %or1k %hexagon %loongarch64)) (define (lookup-qemu-platforms . names) "Return the list of QEMU platforms that match NAMES--a list of names such as \"arm\", \"hppa\", etc." (filter (lambda (platform) (member (qemu-platform-name platform) names)) %qemu-platforms)) (define-record-type* <qemu-binfmt-configuration> qemu-binfmt-configuration make-qemu-binfmt-configuration qemu-binfmt-configuration? (qemu qemu-binfmt-configuration-qemu (default qemu)) (platforms qemu-binfmt-configuration-platforms (default '()))) ;safest default (define (qemu-platform->binfmt qemu platform) "Return a gexp that evaluates to a binfmt string for PLATFORM, using the given QEMU package." (define (bytevector->binfmt-string bv) ;; Return a binfmt-friendly string representing BV. Hex-encode every ;; character, in particular because the doc notes "that you must escape ;; any NUL bytes; parsing halts at the first one". (string-concatenate (map (lambda (n) (string-append "\\x" (string-pad (number->string n 16) 2 #\0))) (bytevector->u8-list bv)))) (match platform (($ <qemu-platform> name family magic mask flags) ;; See 'Documentation/binfmt_misc.txt' in the kernel. #~(string-append ":qemu-" #$name ":M::" #$(bytevector->binfmt-string magic) ":" #$(bytevector->binfmt-string mask) ":" #$qemu:static "/bin/qemu-" #$name ":" #$flags)))) (define %binfmt-mount-point (file-system-mount-point %binary-format-file-system)) (define %binfmt-register-file (string-append %binfmt-mount-point "/register")) (define qemu-binfmt-shepherd-services (match-lambda (($ <qemu-binfmt-configuration> qemu platforms) (list (shepherd-service (provision '(qemu-binfmt)) (documentation "Install binfmt_misc handlers for QEMU.") (requirement '(file-system-/proc/sys/fs/binfmt_misc)) (start #~(lambda () ;; Register the handlers for all of PLATFORMS. (for-each (lambda (str) (call-with-output-file #$%binfmt-register-file (lambda (port) (display str port)))) (list #$@(map (cut qemu-platform->binfmt qemu <>) platforms))) #t)) (stop #~(lambda (_) ;; Unregister the handlers. (for-each (lambda (name) (let ((file (string-append #$%binfmt-mount-point "/qemu-" name))) (call-with-output-file file (lambda (port) (display "-1" port))))) '#$(map qemu-platform-name platforms)) #f))))))) (define qemu-binfmt-service-type ;; TODO: Make a separate binfmt_misc service out of this? (service-type (name 'qemu-binfmt) (extensions (list (service-extension file-system-service-type (const (list %binary-format-file-system))) (service-extension shepherd-root-service-type qemu-binfmt-shepherd-services))) (default-value (qemu-binfmt-configuration)) (description "This service supports transparent emulation of binaries compiled for other architectures using QEMU and the @code{binfmt_misc} functionality of the kernel Linux."))) ;;; ;;; QEMU guest agent service. ;;; (define-configuration qemu-guest-agent-configuration (qemu (file-like qemu-minimal) "QEMU package.") (device (string "") "Path to device or socket used to communicate with the host. If not specified, the QEMU default path is used.")) (define (qemu-guest-agent-shepherd-service config) (let ((qemu (qemu-guest-agent-configuration-qemu config)) (device (qemu-guest-agent-configuration-device config))) (list (shepherd-service (provision '(qemu-guest-agent)) ;; The service needs to depend on udev, which brings up devices like ;; those under /dev/virtio-ports. (requirement '(user-processes udev)) (documentation "Run the QEMU guest agent.") (start #~(make-forkexec-constructor `(,(string-append #$qemu "/bin/qemu-ga") "--statedir" "/var/run" ,@(if (string-null? #$device) '() (list "--path" #$device))) #:log-file "/var/log/qemu-ga.log")) (stop #~(make-kill-destructor)))))) (define qemu-guest-agent-service-type (service-type (name 'qemu-guest-agent) (extensions (list (service-extension shepherd-root-service-type qemu-guest-agent-shepherd-service))) (default-value (qemu-guest-agent-configuration)) (description "Run the QEMU guest agent."))) ;;; ;;; Secrets for guest VMs. ;;; (define (secret-service-shepherd-services address) "Return a Shepherd service that fetches sensitive material at local PORT, over TCP. Reboot upon failure." ;; This is a Shepherd service, rather than an activation snippet, to make ;; sure it is started once 'networking' is up so it can accept incoming ;; connections. (list (shepherd-service (documentation "Fetch secrets from the host at startup time.") (provision '(secret-service-client)) (requirement '(loopback networking)) (modules '((gnu build secret-service) (guix build utils))) (start (with-imported-modules '((gnu build secret-service) (guix build utils)) #~(lambda () ;; Since shepherd's output port goes to /dev/log, write this ;; message to stderr so it's visible on the Mach console. (format (current-error-port) "receiving secrets from the host...~%") (force-output (current-error-port)) (let ((sent (secret-service-receive-secrets #$address))) (unless sent (sleep 3) (reboot)))))) (stop #~(const #f))))) (define secret-service-type (service-type (name 'secret-service) (extensions (list (service-extension shepherd-root-service-type secret-service-shepherd-services) ;; Make every Shepherd service depend on ;; 'secret-service-client'. (service-extension user-processes-service-type (const '(secret-service-client))))) (description "This service fetches secret key and other sensitive material over TCP at boot time. This service is meant to be used by virtual machines (VMs) that can only be accessed by their host."))) (define* (secret-service-operating-system os #:optional (address #~(make-socket-address AF_INET INADDR_ANY 1004))) "Return an operating system based on OS that includes the secret-service, that will be listening to receive secret keys on ADDRESS." (operating-system (inherit os) (services ;; Turn off SSH and Guix key generation that normally happens during ;; activation: that requires entropy and thus takes time during boot, and ;; those keys are going to be overwritten by secrets received from the ;; host anyway. (cons (service secret-service-type address) (modify-services (operating-system-user-services os) (openssh-service-type config => (openssh-configuration (inherit config) (generate-host-keys? #f))) (guix-service-type config => (guix-configuration (inherit config) (generate-substitute-key? #f)))))))) ;;; ;;; Offloading-as-a-service. ;;; (define-record-type* <virtual-build-machine> virtual-build-machine make-virtual-build-machine virtual-build-machine? this-virtual-build-machine (name virtual-build-machine-name (default 'build-vm)) (image virtual-build-machine-image (thunked) (default (virtual-build-machine-default-image this-virtual-build-machine))) (qemu virtual-build-machine-qemu (default qemu-minimal)) (cpu virtual-build-machine-cpu (thunked) (default (qemu-cpu-model-for-date (virtual-build-machine-systems this-virtual-build-machine) (virtual-build-machine-date this-virtual-build-machine)))) (cpu-count virtual-build-machine-cpu-count (default 4)) (memory-size virtual-build-machine-memory-size ;integer (MiB) (default 2048)) (date virtual-build-machine-date ;; Default to a date "in the past" assuming a common use case ;; is to rebuild old packages. (default (make-date 0 0 00 00 01 01 2020 0))) (port-forwardings virtual-build-machine-port-forwardings (default `((,%build-vm-ssh-port . 22) (,%build-vm-secrets-port . 1004)))) (systems virtual-build-machine-systems (default (list (%current-system)))) (auto-start? virtual-build-machine-auto-start? (default #f))) (define %build-vm-ssh-port ;; Default host port where the guest's SSH port is forwarded. 11022) (define %build-vm-secrets-port ;; Host port to communicate secrets to the build VM. ;; FIXME: Anyone on the host can talk to it; use virtio ports or AF_VSOCK ;; instead. 11044) (define %x86-64-intel-cpu-models ;; List of release date/CPU model pairs representing Intel's x86_64 models. ;; The list is taken from ;; <https://en.wikipedia.org/wiki/List_of_Intel_CPU_microarchitectures>. ;; CPU model strings are those found in 'qemu-system-x86_64 -cpu help'. (letrec-syntax ((cpu-models (syntax-rules () ((_ (date model) rest ...) (alist-cons (date->time-utc (string->date date "~Y-~m-~d")) model (cpu-models rest ...))) ((_) '())))) (reverse (cpu-models ("2006-01-01" "core2duo") ("2010-01-01" "Westmere") ("2008-01-01" "Nehalem") ("2011-01-01" "SandyBridge") ("2012-01-01" "IvyBridge") ("2013-01-01" "Haswell") ("2014-01-01" "Broadwell") ("2015-01-01" "Skylake-Client"))))) (define (qemu-cpu-model-for-date systems date) "Return the QEMU name of a CPU model for SYSTEMS that was current at DATE." (if (any (cut string-prefix? "x86_64-" <>) systems) (let ((time (date->time-utc date))) (any (match-lambda ((release-date . model) (and (time<? release-date time) model))) %x86-64-intel-cpu-models)) ;; TODO: Add models for other architectures. "host")) (define (virtual-build-machine-ssh-port config) "Return the host port where CONFIG has its VM's SSH port forwarded." (any (match-lambda ((host-port . 22) host-port) (_ #f)) (virtual-build-machine-port-forwardings config))) (define (virtual-build-machine-secrets-port config) "Return the host port where CONFIG has its VM's secrets port forwarded." (any (match-lambda ((host-port . 1004) host-port) (_ #f)) (virtual-build-machine-port-forwardings config))) (define %minimal-vm-syslog-config ;; Minimal syslog configuration for a VM. (plain-file "vm-syslog.conf" "\ # Log most messages to the console, which goes to the serial # output, allowing the host to log it. *.info;auth.notice;authpriv.none -/dev/console # The rest. *.=debug -/var/log/debug authpriv.*;auth.info /var/log/secure ")) (define %virtual-build-machine-operating-system (operating-system (host-name "build-machine") (locale "en_US.utf8") (locale-definitions ;; Save space by providing only one locale. (list (locale-definition (name "en_US.utf8") (source "en_US") (charset "UTF-8")))) (bootloader (bootloader-configuration ;unused (bootloader grub-minimal-bootloader) (targets '("/dev/null")))) (file-systems (cons (file-system ;unused (mount-point "/") (device "none") (type "tmpfs")) %base-file-systems)) (users (cons (user-account (name "offload") (group "users") (supplementary-groups '("kvm")) (comment "Account used for offloading")) %base-user-accounts)) (services (cons* (service static-networking-service-type (list %qemu-static-networking)) (service openssh-service-type (openssh-configuration (openssh openssh-sans-x))) ;; Run GC once per hour. (simple-service 'perdiodic-gc mcron-service-type (list #~(job "12 * * * *" "guix gc -F 2G"))) (modify-services %base-services ;; By default, the secret service introduces a ;; pre-initialized /etc/guix/acl file in the VM. Thus, ;; clear 'authorize-key?' so that it's not overridden ;; at activation time. ;; Since it's used for offloading, disable substitutes ;; (this avoids network issues). (guix-service-type config => (guix-configuration (inherit config) (authorize-key? #f) (use-substitutes? #f))) (syslog-service-type config => (syslog-configuration (config-file %minimal-vm-syslog-config))) (delete mingetty-service-type) (delete console-font-service-type)))))) (define %default-virtual-build-machine-image-size ;; Size of the default disk image of virtual build machines. It should be ;; large enough to let users build a few things. (* 20 (expt 2 30))) (define (virtual-build-machine-default-image config) (let* ((type (lookup-image-type-by-name 'mbr-raw)) (base (os->image %virtual-build-machine-operating-system #:type type))) (image (inherit base) (name (symbol-append 'build-vm- (virtual-build-machine-name config))) (format 'compressed-qcow2) (partition-table-type 'mbr) (volatile-root? #f) (shared-store? #f) (size %default-virtual-build-machine-image-size) (partitions (match (image-partitions base) ((root) ;; Increase the size of the root partition to match ;; that of the disk image. (let ((root-size (- size (* 50 (expt 2 20))))) (list (partition (inherit root) (size root-size)))))))))) (define (virtual-build-machine-account-name config) (string-append "build-vm-" (symbol->string (virtual-build-machine-name config)))) (define (virtual-build-machine-accounts config) (let ((name (virtual-build-machine-account-name config))) (list (user-group (name name) (system? #t)) (user-account (name name) (group name) (supplementary-groups '("kvm")) (comment "Privilege separation user for the virtual build machine") (home-directory "/var/empty") (shell (file-append shadow "/sbin/nologin")) (system? #t))))) (define (build-vm-shepherd-services config) (define transform (compose secret-service-operating-system operating-system-with-locked-root-account operating-system-with-offloading-account (lambda (os) (virtualized-operating-system os #:full-boot? #t)))) (define transformed-image (let ((base (virtual-build-machine-image config))) (image (inherit base) (operating-system (transform (image-operating-system base)))))) (define command (linux-image-startup-command transformed-image #:qemu (virtual-build-machine-qemu config) #:cpu (virtual-build-machine-cpu config) #:cpu-count (virtual-build-machine-cpu-count config) #:memory-size (virtual-build-machine-memory-size config) #:port-forwardings (virtual-build-machine-port-forwardings config) #:date (virtual-build-machine-date config))) (define user (virtual-build-machine-account-name config)) (list (shepherd-service (documentation "Run the build virtual machine service.") (provision (list (virtual-build-machine-name config))) (requirement '(user-processes)) (modules `((gnu build secret-service) (guix build utils) ,@%default-modules)) (start (with-imported-modules (source-module-closure '((gnu build secret-service) (guix build utils))) #~(lambda arguments (let* ((pid (fork+exec-command (append #$command arguments) #:user #$user #:group "kvm" #:environment-variables ;; QEMU tries to write to /var/tmp ;; by default. '("TMPDIR=/tmp"))) (port #$(virtual-build-machine-secrets-port config)) (root #$(virtual-build-machine-secret-root config)) (address (make-socket-address AF_INET INADDR_LOOPBACK port))) (catch #t (lambda _ (if (secret-service-send-secrets address root) pid (begin (kill (- pid) SIGTERM) #f))) (lambda (key . args) (kill (- pid) SIGTERM) (apply throw key args))))))) (stop #~(make-kill-destructor)) (actions (list (shepherd-action (name 'configuration) (documentation "Display the configuration of this virtual build machine.") (procedure #~(lambda (_) (format #t "CPU: ~a~%" #$(virtual-build-machine-cpu config)) (format #t "number of CPU cores: ~a~%" #$(virtual-build-machine-cpu-count config)) (format #t "memory size: ~a MiB~%" #$(virtual-build-machine-memory-size config)) (format #t "initial date: ~a~%" #$(date->string (virtual-build-machine-date config)))))))) (auto-start? (virtual-build-machine-auto-start? config))))) (define (authorize-guest-substitutes-on-host) "Return a program that authorizes the guest's archive signing key (passed as an argument) on the host." (define not-config? (match-lambda ('(guix config) #f) (('guix _ ...) #t) (('gnu _ ...) #t) (_ #f))) (define run (with-extensions (list guile-gcrypt) (with-imported-modules `(((guix config) => ,(make-config.scm)) ,@(source-module-closure '((guix pki) (guix build utils)) #:select? not-config?)) #~(begin (use-modules (ice-9 match) (ice-9 textual-ports) (gcrypt pk-crypto) (guix pki) (guix build utils)) (match (command-line) ((_ guest-config-directory) (let ((guest-key (string-append guest-config-directory "/signing-key.pub"))) (if (file-exists? guest-key) ;; Add guest key to the host's ACL. (let* ((key (string->canonical-sexp (call-with-input-file guest-key get-string-all))) (acl (public-keys->acl (cons key (acl->public-keys (current-acl)))))) (with-atomic-file-replacement %acl-file (lambda (_ port) (write-acl acl port)))) (format (current-error-port) "warning: guest key missing from '~a'~%" guest-key))))))))) (program-file "authorize-guest-substitutes-on-host" run)) (define (initialize-build-vm-substitutes) "Initialize the Hurd VM's key pair and ACL and store it on the host." (define run (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils) (ice-9 match)) (define host-key "/etc/guix/signing-key.pub") (define host-acl "/etc/guix/acl") (match (command-line) ((_ guest-config-directory) (setenv "GUIX_CONFIGURATION_DIRECTORY" guest-config-directory) (invoke #+(file-append guix "/bin/guix") "archive" "--generate-key") (when (file-exists? host-acl) ;; Copy the host ACL. (copy-file host-acl (string-append guest-config-directory "/acl"))) (when (file-exists? host-key) ;; Add the host key to the childhurd's ACL. (let ((key (open-fdes host-key O_RDONLY))) (close-fdes 0) (dup2 key 0) (execl #+(file-append guix "/bin/guix") "guix" "archive" "--authorize")))))))) (program-file "initialize-build-vm-substitutes" run)) (define* (build-vm-activation secret-directory #:key offloading-ssh-key (offloading? #t)) (with-imported-modules '((guix build utils)) #~(begin (use-modules (guix build utils)) (define secret-directory #$secret-directory) (define ssh-directory (string-append secret-directory "/etc/ssh")) (define guix-directory (string-append secret-directory "/etc/guix")) (define offloading-ssh-key #$offloading-ssh-key) (unless (file-exists? ssh-directory) ;; Generate SSH host keys under SSH-DIRECTORY. (mkdir-p ssh-directory) (invoke #$(file-append openssh "/bin/ssh-keygen") "-A" "-f" secret-directory)) (unless (or (not #$offloading?) (file-exists? offloading-ssh-key)) ;; Generate a user SSH key pair for the host to use when offloading ;; to the guest. (mkdir-p (dirname offloading-ssh-key)) (invoke #$(file-append openssh "/bin/ssh-keygen") "-t" "ed25519" "-N" "" "-f" offloading-ssh-key) ;; Authorize it in the guest for user 'offloading'. (let ((authorizations (string-append ssh-directory "/authorized_keys.d/offloading"))) (mkdir-p (dirname authorizations)) (copy-file (string-append offloading-ssh-key ".pub") authorizations) (chmod (dirname authorizations) #o555))) (unless (file-exists? guix-directory) (invoke #$(initialize-build-vm-substitutes) guix-directory)) (when #$offloading? ;; Authorize the archive signing key from GUIX-DIRECTORY in the host. (invoke #$(authorize-guest-substitutes-on-host) guix-directory))))) (define (virtual-build-machine-offloading-ssh-key config) "Return the name of the file containing the SSH key of user 'offloading'." (string-append "/etc/guix/offload/ssh/virtual-build-machine/" (symbol->string (virtual-build-machine-name config)))) (define (virtual-build-machine-activation config) "Return a gexp to activate the build VM according to CONFIG." (build-vm-activation (virtual-build-machine-secret-root config) #:offloading? #t #:offloading-ssh-key (virtual-build-machine-offloading-ssh-key config))) (define (virtual-build-machine-secret-root config) (string-append "/etc/guix/virtual-build-machines/" (symbol->string (virtual-build-machine-name config)))) (define (check-vm-availability config) "Return a Scheme file that evaluates to true if the service corresponding to CONFIG, a <virtual-build-machine>, is up and running." (define service-name (virtual-build-machine-name config)) (scheme-file "check-build-vm-availability.scm" #~(begin (use-modules (gnu services herd) (srfi srfi-34)) (guard (c ((service-not-found-error? c) #f)) (->bool (live-service-running (current-service '#$service-name))))))) (define (build-vm-guix-extension config) (define vm-ssh-key (string-append (virtual-build-machine-secret-root config) "/etc/ssh/ssh_host_ed25519_key.pub")) (define host-ssh-key (virtual-build-machine-offloading-ssh-key config)) (guix-extension (build-machines (list #~(if (primitive-load #$(check-vm-availability config)) (list (build-machine (name "localhost") (port #$(virtual-build-machine-ssh-port config)) (systems '#$(virtual-build-machine-systems config)) (user "offloading") (host-key (call-with-input-file #$vm-ssh-key (@ (ice-9 textual-ports) get-string-all))) (private-key #$host-ssh-key))) '()))))) (define virtual-build-machine-service-type (service-type (name 'build-vm) (extensions (list (service-extension shepherd-root-service-type build-vm-shepherd-services) (service-extension guix-service-type build-vm-guix-extension) (service-extension account-service-type virtual-build-machine-accounts) (service-extension activation-service-type virtual-build-machine-activation))) (description "Create a @dfn{virtual build machine}: a virtual machine (VM) that builds can be offloaded to. By default, the virtual machine starts with a clock running at some point in the past.") (default-value (virtual-build-machine)))) ;;; ;;; The Hurd in VM service: a Childhurd. ;;; (define (operating-system-with-offloading-account os) (define accounts (list (user-group (name "offloading") (system? #t)) (user-account (name "offloading") (group "offloading") (system? #t) (comment "Offloading privilege separation user") (home-directory "/var/run/offloading") (shell (file-append bash-minimal "/bin/sh"))))) (operating-system (inherit os) (services (cons (simple-service 'offloading-account account-service-type accounts) (operating-system-user-services os))))) (define (operating-system-with-locked-root-account os) "Return OS with a 'root' account whose password is uninitialized, thereby preventing password-based authentication as 'root'." (define root ;; %ROOT-ACCOUNT has an empty password; change that to an uninitialized ;; password. (user-account (inherit %root-account) (password #f))) (operating-system (inherit os) (users (cons root (operating-system-users os))))) (define %hurd-vm-operating-system (operating-system (inherit %hurd-default-operating-system) (host-name "childhurd") (timezone "Europe/Amsterdam") (bootloader (bootloader-configuration (bootloader grub-minimal-bootloader) (targets '("/dev/vda")) (timeout 0))) (packages (cons* gdb-minimal (operating-system-packages %hurd-default-operating-system))) (services (cons* (service openssh-service-type (openssh-configuration (openssh openssh-sans-x) (use-pam? #f) (permit-root-login 'prohibit-password) (allow-empty-passwords? #t) (password-authentication? #t))) ;; By default, the secret service introduces a pre-initialized ;; /etc/guix/acl file in the childhurd. Thus, clear ;; 'authorize-key?' so that it's not overridden at activation ;; time. (modify-services %base-services/hurd (guix-service-type config => (guix-configuration (inherit config) (authorize-key? #f)))))))) (define-record-type* <hurd-vm-configuration> hurd-vm-configuration make-hurd-vm-configuration hurd-vm-configuration? (os hurd-vm-configuration-os ;<operating-system> (default %hurd-vm-operating-system)) (qemu hurd-vm-configuration-qemu ;file-like (default qemu-minimal)) (image hurd-vm-configuration-image ;<image> (thunked) (default (hurd-vm-disk-image this-record))) (disk-size hurd-vm-configuration-disk-size ;number or 'guess (default 'guess)) (memory-size hurd-vm-configuration-memory-size ;number (default 2048)) (options hurd-vm-configuration-options ;list of string (default `("--snapshot"))) (id hurd-vm-configuration-id ;#f or integer [1..] (default #f)) (net-options hurd-vm-configuration-net-options ;list of string (thunked) (default (hurd-vm-net-options this-record))) (offloading? hurd-vm-configuration-offloading? ;Boolean (default #t)) (secret-root hurd-vm-configuration-secret-root ;string (default "/etc/childhurd"))) (define (hurd-vm-disk-image config) "Return a disk-image for the Hurd according to CONFIG. The secret-service is added to the OS specified in CONFIG." (define transform (compose secret-service-operating-system ;; When offloading is enabled, (1) add the 'offloading' account, ;; and (2) prevent users from logging in as 'root' without a ;; password as this would allow any user on the host to populate ;; the host's store indirectly (for example by logging in as root ;; in the Hurd VM over VNC). (if (hurd-vm-configuration-offloading? config) (compose operating-system-with-locked-root-account operating-system-with-offloading-account) identity))) (let* ((os (transform (hurd-vm-configuration-os config))) (disk-size (hurd-vm-configuration-disk-size config)) (type (lookup-image-type-by-name 'hurd-qcow2)) (os->image (image-type-constructor type))) (image (inherit (os->image os)) (size disk-size)))) (define (hurd-vm-port config base) "Return the forwarded vm port for this childhurd config." (let ((id (or (hurd-vm-configuration-id config) 0))) (+ base (* 1000 id)))) (define %hurd-vm-secrets-port 11004) (define %hurd-vm-ssh-port 10022) (define %hurd-vm-vnc-port 15900) (define (hurd-vm-net-options config) `("--device" "rtl8139,netdev=net0" "--netdev" ,(string-append "user,id=net0" ",hostfwd=tcp:127.0.0.1:" (number->string (hurd-vm-port config %hurd-vm-secrets-port)) "-:1004" ",hostfwd=tcp:127.0.0.1:" (number->string (hurd-vm-port config %hurd-vm-ssh-port)) "-:22" ",hostfwd=tcp:127.0.0.1:" (number->string (hurd-vm-port config %hurd-vm-vnc-port)) "-:5900"))) (define (hurd-vm-shepherd-service config) "Return a <shepherd-service> for a Hurd in a Virtual Machine with CONFIG." (let ((image (hurd-vm-configuration-image config)) (qemu (hurd-vm-configuration-qemu config)) (memory-size (hurd-vm-configuration-memory-size config)) (options (hurd-vm-configuration-options config)) (id (hurd-vm-configuration-id config)) (net-options (hurd-vm-configuration-net-options config)) (provisions '(hurd-vm childhurd))) (define vm-command ;; XXX: Use the x86_64 emulator instead of the i386 one to work around ;; "Bad ram pointer" issues: <https://issues.guix.gnu.org/66053>. #~(append (list #$(file-append qemu "/bin/qemu-system-x86_64") "-m" (number->string #$memory-size) #$@net-options #$@options "--hda" #+(system-image image) ;; Cause the service to be respawned if the guest ;; reboots (it can reboot for instance if it did not ;; receive valid secrets, or if it crashed.) "--no-reboot") (if (file-exists? "/dev/kvm") '("--enable-kvm") '()))) (list (shepherd-service (documentation "Run the Hurd in a Virtual Machine: a Childhurd.") (provision (if id (map (cute symbol-append <> (string->symbol (number->string id))) provisions) provisions)) (requirement '(loopback networking user-processes)) (start (with-imported-modules (source-module-closure '((gnu build secret-service) (guix build utils))) #~(lambda () (let* ((pid (fork+exec-command #$vm-command #:user "childhurd" ;; XXX TODO: use "childhurd" after ;; updating Shepherd #:group "kvm" #:environment-variables ;; QEMU tries to write to /var/tmp ;; by default. '("TMPDIR=/tmp"))) (port #$(hurd-vm-port config %hurd-vm-secrets-port)) (root #$(hurd-vm-configuration-secret-root config)) (address (make-socket-address AF_INET INADDR_LOOPBACK port))) (catch #t (lambda _ ;; XXX: 'secret-service-send-secrets' won't complete until ;; the guest has booted and its secret service server is ;; running, which could take 20+ seconds during which PID 1 ;; is stuck waiting. (if (secret-service-send-secrets address root) pid (begin (kill (- pid) SIGTERM) #f))) (lambda (key . args) (kill (- pid) SIGTERM) (apply throw key args))))))) (modules `((gnu build secret-service) (guix build utils) ,@%default-modules)) (stop #~(make-kill-destructor)))))) (define %hurd-vm-accounts (list (user-group (name "childhurd") (system? #t)) (user-account (name "childhurd") (group "childhurd") (supplementary-groups '("kvm")) (comment "Privilege separation user for the childhurd") (home-directory "/var/empty") (shell (file-append shadow "/sbin/nologin")) (system? #t)))) (define (hurd-vm-activation config) "Return a gexp to activate the Hurd VM according to CONFIG." (build-vm-activation (hurd-vm-configuration-secret-root config) #:offloading? (hurd-vm-configuration-offloading? config) #:offloading-ssh-key (hurd-vm-configuration-offloading-ssh-key config))) (define (hurd-vm-configuration-offloading-ssh-key config) "Return the name of the file containing the SSH key of user 'offloading'." (string-append "/etc/guix/offload/ssh/childhurd" (or (and=> (hurd-vm-configuration-id config) number->string) ""))) (define (hurd-vm-guix-extension config) "When offloading is enabled, add this childhurd to the list of offlading machines in /etc/guix/machines.scm." (if (hurd-vm-configuration-offloading? config) (let* ((image (hurd-vm-configuration-image config)) (platform (image-platform image)) (system (platform-system platform)) (vm-ssh-key (string-append (hurd-vm-configuration-secret-root config) "/etc/ssh/ssh_host_ed25519_key.pub")) (host-ssh-key (hurd-vm-configuration-offloading-ssh-key config))) (guix-extension (build-machines (list #~(build-machine (name "localhost") (port #$(hurd-vm-port config %hurd-vm-ssh-port)) (systems '(#$system)) (host-key (call-with-input-file #$vm-ssh-key (@ (ice-9 textual-ports) get-string-all))) (user "offloading") (private-key #$host-ssh-key)))))) (guix-extension))) (define hurd-vm-service-type (service-type (name 'hurd-vm) (extensions (list (service-extension shepherd-root-service-type hurd-vm-shepherd-service) (service-extension account-service-type (const %hurd-vm-accounts)) (service-extension guix-service-type hurd-vm-guix-extension) (service-extension activation-service-type hurd-vm-activation))) (default-value (hurd-vm-configuration)) (description "Provide a virtual machine (VM) running GNU/Hurd, also known as a @dfn{childhurd}.")))