aboutsummaryrefslogtreecommitdiff
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2019 Mathieu Othacehe <m.othacehe@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-processes)
  #:use-module (guix scripts processes)
  #:use-module (guix store)
  #:use-module (guix derivations)
  #:use-module (guix packages)
  #:use-module (guix gexp)
  #:use-module ((guix utils) #:select (call-with-temporary-directory))
  #:use-module (gnu packages bootstrap)
  #:use-module (guix tests)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-64)
  #:use-module (rnrs bytevectors)
  #:use-module (rnrs io ports)
  #:use-module (ice-9 match)
  #:use-module (ice-9 threads))

;; When using --system argument, binfmt-misc mechanism may be used. In that
;; case, (guix script processes) won't work because:
;;
;; * ARGV0 is qemu-user and not guix-daemon.
;; * Guix-daemon won't be able to stuff client PID in ARGV1 of forked
;;   processes.
;;
;; See: https://lists.gnu.org/archive/html/bug-guix/2019-12/msg00017.html.
;;
;; If we detect that we are running with binfmt emulation, all the following
;; tests must be skipped.

(define (binfmt-misc?)
  (let ((pid (getpid))
        (cmdline (call-with-input-file "/proc/self/cmdline" get-string-all)))
    (match (primitive-fork)
      (0 (dynamic-wind
           (const #t)
           (lambda ()
             (exit
              (not (equal?
                    (call-with-input-file (format #f "/proc/~a/cmdline" pid)
                      get-string-all)
                    cmdline))))
           (const #t)))
      (x (zero? (cdr (waitpid x)))))))

(define-syntax-rule (test-assert* description exp)
  (begin
    (when (binfmt-misc?)
      (test-skip 1))
    (test-assert description exp)))

(test-begin "processes")

(test-assert* "not a client"
  (not (find (lambda (session)
               (= (getpid)
                  (process-id (daemon-session-client session))))
             (daemon-sessions))))

(test-assert* "client"
  (with-store store
    (let* ((session (find (lambda (session)
                            (= (getpid)
                               (process-id (daemon-session-client session))))
                          (daemon-sessions)))
           (daemon  (daemon-session-process session)))
      (and (kill (process-id daemon) 0)
           (string-suffix? "guix-daemon" (first (process-command daemon)))))))

(test-assert* "client + lock"
  (with-store store
    (call-with-temporary-directory
     (lambda (directory)
       (let* ((token1  (string-append directory "/token1"))
              (token2  (string-append directory "/token2"))
              (exp     #~(begin #$(random-text)
                                (mkdir #$token1)
                                (let loop ()
                                  (unless (file-exists? #$token2)
                                    (sleep 1)
                                    (loop)))
                                (mkdir #$output)))
              (guile   (package-derivation store %bootstrap-guile))
              (drv     (run-with-store store
                         (gexp->derivation "foo" exp
                                           #:guile-for-build guile)))
              (thread  (call-with-new-thread
                        (lambda ()
                          (build-derivations store (list drv)))))
              (_       (let loop ()
                         (unless (file-exists? token1)
                           (usleep 200)
                           (loop))))
              (session (find (lambda (session)
                               (= (getpid)
                                  (process-id (daemon-session-client session))))
                             (daemon-sessions)))
              (locks   (daemon-session-locks-held (pk 'session session))))
         (call-with-output-file token2 (const #t))
         (equal? (list (string-append (derivation->output-path drv) ".lock"))
                 locks))))))

(test-end "processes")
#n138'>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
;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2017, 2018 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 (gnu build shepherd)
  #:use-module (gnu system file-systems)
  #:use-module (gnu build linux-container)
  #:use-module (guix build utils)
  #:use-module (srfi srfi-1)
  #:use-module (ice-9 match)
  #:export (make-forkexec-constructor/container))

;;; Commentary:
;;;
;;; This module provides extensions to the GNU Shepherd.  In particular, it
;;; provides a helper to start services in a container.
;;;
;;; Code:

(define (clean-up file)
  (when file
    (catch 'system-error
      (lambda ()
        (delete-file file))
      (lambda args
        (unless (= ENOENT (system-error-errno args))
          (apply throw args))))))

(define-syntax-rule (catch-system-error exp)
  (catch 'system-error
    (lambda ()
      exp)
    (const #f)))

(define (default-namespaces args)
  ;; Most daemons are here to talk to the network, and most of them expect to
  ;; run under a non-zero UID.
  (fold delq %namespaces '(net user)))

(define* (default-mounts #:key (namespaces (default-namespaces '())))
  (define (tmpfs directory)
    (file-system
      (device "none")
      (mount-point directory)
      (type "tmpfs")
      (check? #f)))

  (define accounts
    ;; This is for processes in the default user namespace but living in a
    ;; different mount namespace, so that they can lookup users.
    (list (file-system-mapping
           (source "/etc/passwd") (target source))
          (file-system-mapping
           (source "/etc/group") (target source))))

  (define nscd-socket
    (file-system-mapping
     (source "/var/run/nscd") (target source)
     (writable? #t)))

  (append (cons (tmpfs "/tmp") %container-file-systems)
          (let ((mappings `(,@(if (memq 'net namespaces)
                                  '()
                                  (cons nscd-socket
                                        %network-file-mappings))
                            ,@(if (and (memq 'mnt namespaces)
                                       (not (memq 'user namespaces)))
                                  accounts
                                  '())
                            ,%store-mapping)))    ;XXX: coarse-grain
            (map file-system-mapping->bind-mount
                 (filter (lambda (mapping)
                           (file-exists? (file-system-mapping-source mapping)))
                         mappings)))))

;; XXX: Lazy-bind the Shepherd to avoid a compile-time dependency.
(module-autoload! (current-module)
                  '(shepherd service) '(read-pid-file exec-command))

(define* (read-pid-file/container pid pid-file #:key (max-delay 5))
  "Read PID-FILE in the container namespaces of PID, which exists in a
separate mount and PID name space.  Return the \"outer\" PID. "
  (match (container-excursion* pid
           (lambda ()
             (read-pid-file pid-file
                            #:max-delay max-delay)))
    (#f
     (catch-system-error (kill pid SIGTERM))
     #f)
    ((? integer? container-pid)
     ;; XXX: When COMMAND is started in a separate PID namespace, its
     ;; PID is always 1, but that's not what Shepherd needs to know.
     pid)))

(define* (make-forkexec-constructor/container command
                                              #:key
                                              (namespaces
                                               (default-namespaces args))
                                              (mappings '())
                                              (user #f)
                                              (group #f)
                                              (log-file #f)
                                              pid-file
                                              (pid-file-timeout 5)
                                              (directory "/")
                                              (environment-variables
                                               (environ))
                                              #:rest args)
  "This is a variant of 'make-forkexec-constructor' that starts COMMAND in
NAMESPACES, a list of Linux namespaces such as '(mnt ipc).  MAPPINGS is the
list of <file-system-mapping> to make in the case of a separate mount
namespace, in addition to essential bind-mounts such /proc."
  (define container-directory
    (match command
      ((program _  ...)
       (string-append "/var/run/containers/" (basename program)))))

  (define auto-mappings
    `(,@(if log-file
            (list (file-system-mapping
                   (source log-file)
                   (target source)
                   (writable? #t)))
            '())))

  (define mounts
    (append (map file-system-mapping->bind-mount
                 (append auto-mappings mappings))
            (default-mounts #:namespaces namespaces)))

  (lambda args
    (mkdir-p container-directory)

    (when log-file
      ;; Create LOG-FILE so we can map it in the container.
      (unless (file-exists? log-file)
        (call-with-output-file log-file (const #t))))

    (let ((pid (run-container container-directory
                              mounts namespaces 1
                              (lambda ()
                                (mkdir-p "/var/run")
                                (clean-up pid-file)
                                (clean-up log-file)

                                (exec-command command
                                              #:user user
                                              #:group group
                                              #:log-file log-file
                                              #:directory directory
                                              #:environment-variables
                                              environment-variables)))))
      (if pid-file
          (if (or (memq 'mnt namespaces) (memq 'pid namespaces))
              (read-pid-file/container pid pid-file
                                       #:max-delay pid-file-timeout)
              (read-pid-file pid-file #:max-delay pid-file-timeout))
          pid))))

;; Local Variables:
;; eval: (put 'container-excursion* 'scheme-indent-function 1)
;; End:

;;; shepherd.scm ends here