Subject: Re: bug#45905: [PATCH] IPFS service definition

From: Maxime Devos <[email-masked]>
Date: Sun, 28 Mar 2021 18:36:13 +0200

On Tue, 2021-03-23 at 14:08 +0100, Ludovic Courtès wrote:
> [...]
> Yes, I think so.  We “just” need to package ‘fs-repo-migrations’ first.
> 
> Perhaps it’d be okay, as a first step, to provide an IPFS service that
> doesn’t handle migrations automatically.
> 
> [...]

Punt for later.

> > > The patch LGTM.  However, we usually commit services along with a system
> > > test under (gnu tests …).  The manual has info on how to run individual
> > > system tests:
> > > 
> > >   https://guix.gnu.org/manual/en/html_node/Running-the-Test-Suite.html
> > > 
> > > Could you write a test that ensures that basic functionality works?  It
> > > could be as simple as waiting for the service to be up, then invoking
> > > ‘ipfs add’ and ‘ipfs get’.  WDYT?
> > 
> > [...]

I have attached a revised patch series testing such basic functionality.
However, I tested the functionality with the HTTP interface instead of
with the command line, as the CLI tools assume the IPFS daemon is run
as the same user as the CLI tools.

(IIRC there is an implementation of the CLI tools somewhere that uses
the API endpoint instead of reading/writing to ~/.ipfs, but I don't
recall where.)

I have a problem: I can't run the test I wrote.

$ make && make check-system TESTS="ipfs"
> [compilation bla bla]
> Compiling Scheme modules...
> Selected 1 system tests...
> (end of output)

For some reason, the test I wrote seems to be ignored.

(Also available from my public git repo:
<https://notabug.org/mdevos/guix-gnunet/commit/732c018b9d24f0f36700c6f8715e989ee3d94663>)

Greetings,
Maxime.
From 1c3182dab19394748a09264b543874de1b777ec5 Mon Sep 17 00:00:00 2001
From: Maxime Devos <[email-masked]>
Date: Fri, 15 Jan 2021 21:46:42 +0100
Subject: [PATCH 1/3] services: Add ipfs-service-type

* gnu/services/networking.scm (ipfs-service-type)
  (%ipfs-home-mapping, %ipfs-environment)
  (%ipfs-accounts, %ipfs-home): New variables.
  (ipfs-configuration, ipfs-configuration?)
  (ipfs-configuration-package, ipfs-configuration-gateway)
  (ipfs-configuration-api, ipfs-shepherd-service)
  (ipfs-binary, %ipfs-activation): New procedures.
* doc/guix.texi (Networking Services): Document it.
---
 doc/guix.texi               |  33 +++++++++
 gnu/services/networking.scm | 141 ++++++++++++++++++++++++++++++++++++
 2 files changed, 174 insertions(+)

diff --git a/doc/guix.texi b/doc/guix.texi
index 74f3fbd299..9314327039 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -87,6 +87,7 @@ Copyright @copyright{} 2020 Daniel Brooks@*
 Copyright @copyright{} 2020 John Soo@*
 Copyright @copyright{} 2020 Jonathan Brielmaier@*
 Copyright @copyright{} 2020 Edgar Vincent@*
+Copyright @copyright{} 2021 Maxime Devos@*
 
 Permission is granted to copy, distribute and/or modify this document
 under the terms of the GNU Free Documentation License, Version 1.3 or
@@ -17462,6 +17463,38 @@ address, delete everything except these options:
 @end table
 @end deftp
 
+@cindex IPFS
+@defvr {Scheme Variable} ipfs-service-type
+The service type for connecting to the @uref{https://ipfs.io,IPFS network},
+a global, versioned, peer-to-peer file system. Pass it a
+@code{ipfs-configuration} to change the ports used for the gateway and API.
+
+Here's an example configuration, using some non-standard ports:
+
+@lisp
+;; part of the operating-system declaration
+(service ipfs-service-type
+              (ipfs-configuration
+               (gateway "/ip4/127.0.0.1/tcp/8880")
+               (api "/ip4/127.0.0.1/tcp/8881")))
+@end lisp
+@end defvr
+
+@deftp {Data Type} ipfs-configuration
+Data type representing the configuration of IPFS.
+
+@table @asis
+@item @code{package} (default: @code{go-ipfs})
+Package object of IPFS.
+
+@item @code{gateway} (default: @code{"/ip4/127.0.0.1/tcp/8082"})
+Address of the gateway, in ‘multiaddress’ format.
+
+@item @code{api} (default: @code{"/ip4/127.0.0.1/tcp/5001"})
+Address of the API endpoint, in ‘multiaddress’ format.
+@end table
+@end deftp
+
 @cindex keepalived
 @deffn {Scheme Variable} keepalived-service-type
 This is the type for the @uref{https://www.keepalived.org/, Keepalived}
diff --git a/gnu/services/networking.scm b/gnu/services/networking.scm
index 231a9f66c7..6e93b56717 100644
--- a/gnu/services/networking.scm
+++ b/gnu/services/networking.scm
@@ -16,6 +16,7 @@
 ;;; Copyright © 2020 Brice Waegeneire <[email-masked]>
 ;;; Copyright © 2021 Oleg Pykhalov <[email-masked]>
 ;;; Copyright © 2021 Christopher Lemmer Webber <[email-masked]>
+;;; Copyright © 2021 Maxime Devos <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -55,6 +56,8 @@
   #:use-module (gnu packages ntp)
   #:use-module (gnu packages wicd)
   #:use-module (gnu packages gnome)
+  #:use-module (gnu packages ipfs)
+  #:use-module (gnu build linux-container)
   #:use-module (guix gexp)
   #:use-module (guix records)
   #:use-module (guix modules)
@@ -197,6 +200,13 @@
             yggdrasil-configuration-json-config
             yggdrasil-configuration-package
 
+            ipfs-service-type
+            ipfs-configuration
+            ipfs-configuration?
+            ipfs-configuration-package
+            ipfs-configuration-gateway
+            ipfs-configuration-api
+
             keepalived-configuration
             keepalived-configuration?
             keepalived-service-type))
@@ -1876,6 +1886,137 @@ See yggdrasil -genconf for config options.")
           (service-extension profile-service-type
                              (compose list yggdrasil-configuration-package))))))
 
+
+;;;
+;;; IPFS
+;;;
+
+(define-record-type* <ipfs-configuration>
+  ipfs-configuration
+  make-ipfs-configuration
+  ipfs-configuration?
+  (package ipfs-configuration-package
+           (default go-ipfs))
+  (gateway ipfs-configuration-gateway
+           (default "/ip4/127.0.0.1/tcp/8082"))
+  (api     ipfs-configuration-api
+           (default "/ip4/127.0.0.1/tcp/5001")))
+
+(define %ipfs-home "/var/lib/ipfs")
+
+(define %ipfs-accounts
+  (list (user-account
+         (name "ipfs")
+         (group "ipfs")
+         (system? #t)
+         (comment "IPFS daemon user")
+         (home-directory "/var/lib/ipfs")
+         (shell (file-append shadow "/sbin/nologin")))
+        (user-group
+         (name "ipfs")
+         (system? #t))))
+
+(define (ipfs-binary config)
+  (file-append (ipfs-configuration-package config) "/bin/ipfs"))
+
+(define %ipfs-home-mapping
+  #~(file-system-mapping
+     (source #$%ipfs-home)
+     (target #$%ipfs-home)
+     (writable? #t)))
+
+(define %ipfs-environment
+  #~(list #$(string-append "HOME=" %ipfs-home)))
+
+(define (ipfs-shepherd-service config)
+  "Return a <shepherd-service> for IPFS with CONFIG."
+  (define ipfs-daemon-command
+    #~(list #$(ipfs-binary config) "daemon"))
+  (list
+   (with-imported-modules (source-module-closure
+                           '((gnu build shepherd)
+                             (gnu system file-systems)))
+     (shepherd-service
+      (provision '(ipfs))
+      ;; While IPFS is most useful when the machine is connected
+      ;; to the network, only loopback is required for starting
+      ;; the service.
+      (requirement '(loopback))
+      (documentation "Connect to the IPFS network")
+      (modules '((gnu build shepherd)
+                 (gnu system file-systems)))
+      (start #~(make-forkexec-constructor/container
+                #$ipfs-daemon-command
+                #:namespaces '#$(fold delq %namespaces '(user net))
+                #:mappings (list #$%ipfs-home-mapping)
+                #:log-file "/var/log/ipfs.log"
+                #:user "ipfs"
+                #:group "ipfs"
+                #:environment-variables #$%ipfs-environment))
+      (stop #~(make-kill-destructor))))))
+
+(define (%ipfs-activation config)
+  "Return an activation gexp for IPFS with CONFIG"
+  (define (ipfs-config-command setting value)
+    #~(#$(ipfs-binary config) "config" #$setting #$value))
+  (define (set-config!-gexp setting value)
+    #~(system* #$@(ipfs-config-command setting value)))
+  (define settings
+    `(("Addresses.API" ,(ipfs-configuration-api config))
+      ("Addresses.Gateway" ,(ipfs-configuration-gateway config))))
+  (define inner-gexp
+    #~(begin
+        (umask #o077)
+        ;; Create $HOME/.ipfs structure
+        (system* #$(ipfs-binary config) "init")
+        ;; Apply settings
+        #$@(map (cute apply set-config!-gexp <>) settings)))
+  (define inner-script
+    (program-file "ipfs-activation-inner" inner-gexp))
+  ;; Run ipfs init and ipfs config from a container,
+  ;; in case the IPFS daemon was compromised at some point
+  ;; and ~/.ipfs is now a symlink to somewhere outside
+  ;; %ipfs-home.
+  (define container-gexp
+    (with-extensions (list shepherd)
+      (with-imported-modules (source-module-closure
+                              '((gnu build shepherd)
+                                (gnu system file-systems)))
+        #~(begin
+            (use-modules (gnu build shepherd)
+                         (gnu system file-systems))
+            (let* ((constructor
+                    (make-forkexec-constructor/container
+                     (list #$inner-script)
+                     #:namespaces '#$(fold delq %namespaces '(user))
+                     #:mappings (list #$%ipfs-home-mapping)
+                     #:user "ipfs"
+                     #:group "ipfs"
+                     #:environment-variables #$%ipfs-environment))
+                   (pid (constructor)))
+              (waitpid pid))))))
+  ;; The activation may happen from the initrd, which uses
+  ;; a statically-linked guile, while the guix container
+  ;; procedures require a working dynamic-link.
+  (define container-script
+    (program-file "ipfs-activation-container" container-gexp))
+  #~(system* #$container-script))
+
+(define ipfs-service-type
+  (service-type
+   (name 'ipfs)
+   (extensions
+    (list (service-extension account-service-type
+                             (const %ipfs-accounts))
+          (service-extension activation-service-type
+                             %ipfs-activation)
+          (service-extension shepherd-root-service-type
+                             ipfs-shepherd-service)))
+   (default-value (ipfs-configuration))
+   (description
+    "Run @command{ipfs daemon}, the reference implementation
+of the IPFS p2p storage network.")))
+
 
 ;;;
 ;;; Keepalived
-- 
2.31.0

From c000adc1da40a460943dabcab6142981adfad9e9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ludovic=20Court=C3=A8s?= <[email-masked]>
Date: Fri, 28 Dec 2018 01:07:58 +0100
Subject: [PATCH 2/3] Add (guix ipfs).

This module allows for communicating with the IPFS
gateway over the HTTP interface.  The commit has been
cherry-picked from <https://issues.guix.gnu.org/33899>.

The procedures for adding and restoring file trees have
been removed as according to a reply issue 33899, a different
format will be used.  The procedure 'add-data' has been
exported as it will be used in the system test for IPFS.

* guix/ipfs.scm: New file.
* Makefile.am (MODULES): Add it.
---
 Makefile.am    |   1 +
 guix/ipfs.scm  | 183 +++++++++++++++++++++++++++++++++++++++++++++++++
 tests/ipfs.scm |  55 +++++++++++++++
 3 files changed, 239 insertions(+)
 create mode 100644 guix/ipfs.scm
 create mode 100644 tests/ipfs.scm

diff --git a/Makefile.am b/Makefile.am
index 1c2d45527c..17ad236655 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -126,6 +126,7 @@ MODULES =					\
   guix/cache.scm				\
   guix/cve.scm					\
   guix/workers.scm				\
+  guix/ipfs.scm					\
   guix/build-system.scm				\
   guix/build-system/android-ndk.scm		\
   guix/build-system/ant.scm			\
diff --git a/guix/ipfs.scm b/guix/ipfs.scm
new file mode 100644
index 0000000000..31a89888a7
--- /dev/null
+++ b/guix/ipfs.scm
@@ -0,0 +1,183 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2018 Ludovic Courtès <[email-masked]>
+;;;
+;;; 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 (guix ipfs)
+  #:use-module (json)
+  #:use-module (guix base64)
+  #:use-module ((guix build utils) #:select (dump-port))
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-11)
+  #:use-module (srfi srfi-26)
+  #:use-module (rnrs io ports)
+  #:use-module (rnrs bytevectors)
+  #:use-module (ice-9 match)
+  #:use-module (ice-9 ftw)
+  #:use-module (web uri)
+  #:use-module (web client)
+  #:use-module (web response)
+  #:export (%ipfs-base-url
+            add-data
+            add-file
+
+            content?
+            content-name
+            content-hash
+            content-size
+
+            add-empty-directory
+            add-to-directory
+            read-contents
+            publish-name))
+
+;;; Commentary:
+;;;
+;;; This module implements bindings for the HTTP interface of the IPFS
+;;; gateway, documented here: <https://docs.ipfs.io/reference/api/http/>.  It
+;;; allows you to add and retrieve files over IPFS, and a few other things.
+;;;
+;;; Code:
+
+(define %ipfs-base-url
+  ;; URL of the IPFS gateway.
+  (make-parameter "http://localhost:5001"))
+
+(define* (call url decode #:optional (method http-post)
+               #:key body (false-if-404? #t) (headers '()))
+  "Invoke the endpoint at URL using METHOD.  Decode the resulting JSON body
+using DECODE, a one-argument procedure that takes an input port; when DECODE
+is false, return the input port.  When FALSE-IF-404? is true, return #f upon
+404 responses."
+  (let*-values (((response port)
+                 (method url #:streaming? #t
+                         #:body body
+
+                         ;; Always pass "Connection: close".
+                         #:keep-alive? #f
+                         #:headers `((connection close)
+                                     ,@headers))))
+    (cond ((= 200 (response-code response))
+           (if decode
+               (let ((result (decode port)))
+                 (close-port port)
+                 result)
+               port))
+          ((and false-if-404?
+                (= 404 (response-code response)))
+           (close-port port)
+           #f)
+          (else
+           (close-port port)
+           (throw 'ipfs-error url response)))))
+
+;; Result of a file addition.
+(define-json-mapping <content> make-content content?
+  json->content
+  (name   content-name "Name")
+  (hash   content-hash "Hash")
+  (bytes  content-bytes "Bytes")
+  (size   content-size "Size" string->number))
+
+;; Result of a 'patch/add-link' operation.
+(define-json-mapping <directory> make-directory directory?
+  json->directory
+  (hash   directory-hash "Hash")
+  (links  directory-links "Links" json->links))
+
+;; A "link".
+(define-json-mapping <link> make-link link?
+  json->link
+  (name   link-name "Name")
+  (hash   link-hash "Hash")
+  (size   link-size "Size" string->number))
+
+;; A "binding", also known as a "name".
+(define-json-mapping <binding> make-binding binding?
+  json->binding
+  (name   binding-name "Name")
+  (value  binding-value "Value"))
+
+(define (json->links json)
+  (match json
+    (#f    '())
+    (links (map json->link links))))
+
+(define %multipart-boundary
+  ;; XXX: We might want to find a more reliable boundary.
+  (string-append (make-string 24 #\-) "2698127afd7425a6"))
+
+(define (bytevector->form-data bv port)
+  "Write to PORT a 'multipart/form-data' representation of BV."
+  (display (string-append "--" %multipart-boundary "\r\n"
+                          "Content-Disposition: form-data\r\n"
+                          "Content-Type: application/octet-stream\r\n\r\n")
+           port)
+  (put-bytevector port bv)
+  (display (string-append "\r\n--" %multipart-boundary "--\r\n")
+           port))
+
+(define* (add-data data #:key (name "file.txt") recursive?)
+  "Add DATA, a bytevector, to IPFS.  Return a content object representing it."
+  (call (string-append (%ipfs-base-url)
+                       "/api/v0/add?arg=" (uri-encode name)
+                       "&recursive="
+                       (if recursive? "true" "false"))
+        json->content
+        #:headers
+        `((content-type
+           . (multipart/form-data
+              (boundary . ,%multipart-boundary))))
+        #:body
+        (call-with-bytevector-output-port
+         (lambda (port)
+           (bytevector->form-data data port)))))
+
+(define (not-dot? entry)
+  (not (member entry '("." ".."))))
+
+(define* (add-file file #:key (name (basename file)))
+  "Add FILE under NAME to the IPFS and return a content object for it."
+  (add-data (match (call-with-input-file file get-bytevector-all)
+              ((? eof-object?) #vu8())
+              (bv bv))
+            #:name name))
+
+(define* (add-empty-directory #:key (name "directory"))
+  "Return a content object for an empty directory."
+  (add-data #vu8() #:recursive? #t #:name name))
+
+(define* (add-to-directory directory file name)
+  "Add FILE to DIRECTORY under NAME, and return the resulting directory.
+DIRECTORY and FILE must be hashes identifying objects in the IPFS store."
+  (call (string-append (%ipfs-base-url)
+                       "/api/v0/object/patch/add-link?arg="
+                       (uri-encode directory)
+                       "&arg=" (uri-encode name) "&arg=" (uri-encode file)
+                       "&create=true")
+        json->directory))
+
+(define* (read-contents object #:key offset length)
+  "Return an input port to read the content of OBJECT from."
+  (call (string-append (%ipfs-base-url)
+                       "/api/v0/cat?arg=" object)
+        #f))
+
+(define* (publish-name object)
+  "Publish OBJECT under the current peer ID."
+  (call (string-append (%ipfs-base-url)
+                       "/api/v0/name/publish?arg=" object)
+        json->binding))
diff --git a/tests/ipfs.scm b/tests/ipfs.scm
new file mode 100644
index 0000000000..3b662b22bd
--- /dev/null
+++ b/tests/ipfs.scm
@@ -0,0 +1,55 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2018 Ludovic Courtès <[email-masked]>
+;;;
+;;; 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-ipfs)
+  #:use-module (guix ipfs)
+  #:use-module ((guix utils) #:select (call-with-temporary-directory))
+  #:use-module (guix tests)
+  #:use-module (web uri)
+  #:use-module (srfi srfi-64))
+
+;; Test the (guix ipfs) module.
+
+(define (ipfs-gateway-running?)
+  "Return true if the IPFS gateway is running at %IPFS-BASE-URL."
+  (let* ((uri    (string->uri (%ipfs-base-url)))
+         (socket (socket AF_INET SOCK_STREAM 0)))
+    (define connected?
+      (catch 'system-error
+        (lambda ()
+          (format (current-error-port)
+                  "probing IPFS gateway at localhost:~a...~%"
+                  (uri-port uri))
+          (connect socket AF_INET INADDR_LOOPBACK (uri-port uri))
+          #t)
+        (const #f)))
+
+    (close-port socket)
+    connected?))
+
+(unless (ipfs-gateway-running?)
+  (test-skip 1))
+
+(test-assert "add-file-tree + restore-file-tree"
+  (call-with-temporary-directory
+   (lambda (directory)
+     (let* ((source  (dirname (search-path %load-path "guix/base32.scm")))
+            (target  (string-append directory "/r"))
+            (content (pk 'content (add-file-tree source))))
+       (restore-file-tree (content-name content) target)
+       (file=? source target)))))
-- 
2.31.0

From 732c018b9d24f0f36700c6f8715e989ee3d94663 Mon Sep 17 00:00:00 2001
From: Maxime Devos <[email-masked]>
Date: Sun, 28 Mar 2021 17:01:49 +0200
Subject: [PATCH 3/3] gnu: tests: Test basic funtionality of the IPFS service.

It is tested whether the IPFS service listens
at the gateway and API ports and whether it
is possible to upload and download a bytevector.

TODO: this test isn't run for some reason:

$ make && make check-system TESTS="ipfs"
> [compilation bla bla]
> Compiling Scheme modules...
> Selected 1 system tests...
> (end of output)

??? why isn't the IPFS test executed?

* gnu/tests/networking.scm
  (%ipfs-os): New variable.
  (run-ipfs-test): New procedure.
  (%test-ipfs): New system test.
---
 gnu/tests/networking.scm | 81 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 80 insertions(+), 1 deletion(-)

diff --git a/gnu/tests/networking.scm b/gnu/tests/networking.scm
index 022663aa67..f886eac881 100644
--- a/gnu/tests/networking.scm
+++ b/gnu/tests/networking.scm
@@ -3,6 +3,7 @@
 ;;; Copyright © 2017, 2020 Marius Bakke <[email-masked]>
 ;;; Copyright © 2018 Chris Marusich <[email-masked]>
 ;;; Copyright © 2018 Arun Isaac <[email-masked]>
+;;; Copyright © 2021 Maxime Devos <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -34,7 +35,8 @@
   #:use-module (gnu packages networking)
   #:use-module (gnu services shepherd)
   #:use-module (ice-9 match)
-  #:export (%test-inetd %test-openvswitch %test-dhcpd %test-tor %test-iptables))
+  #:export (%test-inetd %test-openvswitch %test-dhcpd %test-tor %test-iptables
+                        %test-ipfs))
 
 (define %inetd-os
   ;; Operating system with 2 inetd services.
@@ -563,3 +565,80 @@ COMMIT
    (name "iptables")
    (description "Test a running iptables daemon.")
    (value (run-iptables-test))))
+
+
+;;;
+;;; IPFS service
+;;;
+
+(define %ipfs-os
+  (simple-operating-system
+   (service ipfs-service-type)))
+
+(define (run-ipfs-test)
+  (define os
+    (marionette-operating-system %ipfs-os
+                                 #:imported-modules '((gnu services herd)
+                                                      (guix ipfs))
+                                 #:requirements '(ipfs)))
+
+  (define test
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (gnu build marionette)
+                       (srfi srfi-64))
+
+          (define marionette
+            (make-marionette (list #$(virtual-machine os))))
+
+          (define (ipfs-is-alive?)
+            (marionette-eval
+             '(begin
+                (use-modules (gnu services herd)
+                             (srfi srfi-1))
+                (live-service-running
+                 (find (lambda (live)
+                         (memq 'ipfs
+                               (live-service-provision live)))
+                       (current-services))))
+             marionette))
+
+          ;; The default API endpoint port 5001 is used,
+          ;; so there is no need to parameterize %ipfs-base-url.
+          ;; By running this within the VM instead of outside the VM
+          ;; this system test does not have to forward any ports. 
+          (define (add-data data)
+            (marionette-eval
+             `((@ (guix ipfs) add-contents) ,data)))
+          (define (read-contents object)
+            (marionette-eval
+             `((@ (guix ipfs) read-contents) ,object)))
+
+          (test-begin "ipfs")
+
+          ;; Test the IPFS service.
+
+          (test-assert "ipfs is alive" (ipfs-is-alive?))
+
+          (test-assert "ipfs is listening on the gateway"
+            (let ((default-port 8082))
+              (wait-for-tcp-port default-port marionette)))
+
+          (test-assert "ipfs is listening on the API endpoint"
+            (let ((default-port 5001))
+              (wait-for-tcp-port default-port marionette)))
+
+          (define test-bv (string->utf8 "hello ipfs!"))
+          (test-equal "can upload and download a file to/from ipfs"
+            test-bv
+            (read-contents (add-data test-bv)))
+
+          (test-end)
+          (exit (= (test-runner-fail-count (test-runner-current)) 0)))))
+  (gexp->derivation "ipfs-test" test))
+
+(define %test-ipfs
+  (system-test
+   (name "ipfs")
+   (description "Test a running IPFS daemon configuration.")
+   (value (run-ipfs-test))))
-- 
2.31.0