[Libguestfs] [PATCH libnbd] nbdfuse: New tool to present a network block device in a FUSE filesystem.

Richard W.M. Jones rjones at redhat.com
Sat Oct 12 14:21:56 UTC 2019


This program allows you to turn a network block device source into a
FUSE filesystem containing a virtual file:

  $ nbdkit memory 128M
  $ mkdir mp
  $ nbdfuse mp/ramdisk nbd://localhost &
  $ ls -l mp
  total 0
  -rw-rw-rw-. 1 rjones rjones 134217728 Oct 12 15:09 ramdisk
  $ dd if=/dev/urandom bs=1M count=128 of=mp/ramdisk conv=notrunc,nocreat
  128+0 records in
  128+0 records out
  134217728 bytes (134 MB, 128 MiB) copied, 3.10171 s, 43.3 MB/s
  $ fusermount -u mp

There are still some shortcomings, such as lack of zero and trim
support.  These are documented in the TODO file.

Now for some history:

In libguestfs which is where most of this code derives from we have a
program called ‘guestmount’ which is a FUSE interface to libguestfs:

  http://libguestfs.org/guestmount.1.html

Originally that was a standalone program like nbdfuse, but after some
time we realized that the ability to mount libguestfs under a
directory was generally useful to all guestfs API users and we created
new APIs for it.  guestmount has now become a thin wrapper around
those APIs.

This of course argues that we should do the same thing for libnbd.
But ...

(1) For NBD this is a little less useful than for libguestfs.

(2) We can always do this in future if we need to.

Most importantly:

(3) the libguestfs FUSE API turned out to have a problem - still
unresolved - with handling threads and SELinux and it may not be a
good idea to bake this into the libnbd API until that problem has been
solved.  For more details about (3), read this:

  https://bugzilla.redhat.com/show_bug.cgi?id=1060423#c2
---
 .gitignore          |   2 +
 Makefile.am         |   3 +-
 README              |   2 +
 TODO                |   7 +
 configure.ac        |  18 ++
 docs/libnbd.pod     |   1 +
 fuse/Makefile.am    |  60 +++++
 fuse/nbdfuse.c      | 590 ++++++++++++++++++++++++++++++++++++++++++++
 fuse/nbdfuse.pod    | 262 ++++++++++++++++++++
 fuse/test-nbdkit.sh |  63 +++++
 fuse/test-qcow2.sh  |  64 +++++
 run.in              |   1 +
 sh/nbdsh.pod        |   1 +
 13 files changed, 1073 insertions(+), 1 deletion(-)

diff --git a/.gitignore b/.gitignore
index 1970e6c..f2654a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -57,6 +57,8 @@ Makefile.in
 /examples/server-flags
 /examples/strict-structured-reads
 /examples/threaded-reads-and-writes
+/fuse/nbdfuse
+/fuse/nbdfuse.1
 /fuzzing/libnbd-fuzz-wrapper
 /fuzzing/sync_dir/
 /generator/generator-cache.v1
diff --git a/Makefile.am b/Makefile.am
index b2d9dca..568e735 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -39,6 +39,7 @@ SUBDIRS = \
 	tests \
 	python \
 	sh \
+	fuse \
 	ocaml \
 	ocaml/examples \
 	ocaml/tests \
@@ -64,7 +65,7 @@ maintainer-check-extra-dist:
 	@echo PASS: EXTRA_DIST tests
 
 check-valgrind: all
-	@for d in tests ocaml/tests interop; do \
+	@for d in tests fuse ocaml/tests interop; do \
 	    $(MAKE) -C $$d check-valgrind || exit 1; \
 	done
 
diff --git a/README b/README
index 1c9d816..8d6b563 100644
--- a/README
+++ b/README
@@ -82,6 +82,8 @@ Optional:
 
  * Python >= 3.3 to build the Python 3 bindings and NBD shell (nbdsh).
 
+ * FUSE to build the nbdfuse program.
+
 Optional, only needed to run the test suite:
 
  * nbdkit >= 1.12, the nbdkit basic plugins and the nbdkit basic
diff --git a/TODO b/TODO
index 71d678b..8b7dbe4 100644
--- a/TODO
+++ b/TODO
@@ -27,6 +27,13 @@ Should we ship a "nbdcp" copying tool?
  - Could upload, download or copy between servers.
  - Duplicates functionality already available in qemu-img convert.
 
+nbdfuse:
+ - If you write beyond the end of the virtual file, it returns EIO.
+ - Implement trim/discard.
+ - Implement write_zeroes.
+ - Implement block_status.
+ - Could be made multithreaded for improved performance.
+
 Suggested API improvements:
   general:
   - synchronous APIs that have a timeout or can be cancelled
diff --git a/configure.ac b/configure.ac
index fde43dc..96cb4bd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -200,6 +200,23 @@ PKG_CHECK_MODULES([GLIB], [glib-2.0], [
 ])
 AM_CONDITIONAL([HAVE_GLIB], [test "x$GLIB_LIBS" != "x"])
 
+dnl FUSE is optional to build the FUSE module.
+AC_ARG_ENABLE([fuse],
+    AS_HELP_STRING([--disable-fuse], [disable FUSE (guestmount) support]),
+    [],
+    [enable_fuse=yes])
+AS_IF([test "x$enable_fuse" != "xno"],[
+    PKG_CHECK_MODULES([FUSE],[fuse],[
+        AC_SUBST([FUSE_CFLAGS])
+        AC_SUBST([FUSE_LIBS])
+        AC_DEFINE([HAVE_FUSE],[1],[Define to 1 if you have FUSE.])
+    ],[
+        enable_fuse=no
+        AC_MSG_WARN([FUSE library and headers are missing, so optional FUSE module won't be built])
+    ])
+])
+AM_CONDITIONAL([HAVE_FUSE],[test "x$enable_fuse" != "xno"])
+
 dnl Check we have enough to run podwrapper.
 AC_CHECK_PROG([PERL],[perl],[perl],[no])
 AS_IF([test "x$PERL" != "xno"],[
@@ -353,6 +370,7 @@ AC_CONFIG_FILES([Makefile
                  common/include/Makefile
                  docs/Makefile
                  examples/Makefile
+                 fuse/Makefile
                  fuzzing/Makefile
                  generator/Makefile
                  include/Makefile
diff --git a/docs/libnbd.pod b/docs/libnbd.pod
index 2c3eaf4..9ab6150 100644
--- a/docs/libnbd.pod
+++ b/docs/libnbd.pod
@@ -840,6 +840,7 @@ L<https://github.com/NetworkBlockDevice/nbd/blob/master/doc/uri.md>.
 
 L<libnbd-security(3)>,
 L<nbdsh(1)>,
+L<nbdfuse(1)>,
 L<qemu(1)>.
 
 =head1 AUTHORS
diff --git a/fuse/Makefile.am b/fuse/Makefile.am
new file mode 100644
index 0000000..3d827aa
--- /dev/null
+++ b/fuse/Makefile.am
@@ -0,0 +1,60 @@
+# nbd client library in userspace
+# Copyright (C) 2013-2019 Red Hat Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+include $(top_srcdir)/subdir-rules.mk
+
+EXTRA_DIST = \
+	nbdfuse.pod \
+	test-nbdkit.sh \
+	test-qcow2.sh \
+	$(NULL)
+
+TESTS_ENVIRONMENT = LIBNBD_DEBUG=1
+LOG_COMPILER = $(top_builddir)/run
+TESTS =
+
+if HAVE_FUSE
+
+bin_PROGRAMS = nbdfuse
+
+nbdfuse_SOURCES = nbdfuse.c
+nbdfuse_CPPFLAGS = -I$(top_srcdir)/include
+nbdfuse_CFLAGS = $(WARNINGS_CFLAGS) $(FUSE_CFLAGS)
+nbdfuse_LDADD = $(top_builddir)/lib/libnbd.la $(FUSE_LIBS)
+
+if HAVE_POD
+
+man_MANS = \
+	nbdfuse.1 \
+	$(NULL)
+
+nbdfuse.1: nbdfuse.pod $(top_builddir)/podwrapper.pl
+	$(PODWRAPPER) --section=1 --man $@ \
+	    --html $(top_builddir)/html/$@.html \
+	    $<
+
+endif HAVE_POD
+
+TESTS += \
+	test-nbdkit.sh \
+	test-qcow2.sh \
+	$(NULL)
+
+check-valgrind:
+	LIBNBD_VALGRIND=1 $(MAKE) check
+
+endif HAVE_FUSE
diff --git a/fuse/nbdfuse.c b/fuse/nbdfuse.c
new file mode 100644
index 0000000..5703b95
--- /dev/null
+++ b/fuse/nbdfuse.c
@@ -0,0 +1,590 @@
+/* NBD client library in userspace
+ * Copyright (C) 2013-2019 Red Hat Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/* FUSE support. */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <getopt.h>
+#include <limits.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#include <signal.h>
+#include <time.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#define FUSE_USE_VERSION 26
+
+#include <fuse.h>
+#include <fuse_lowlevel.h>
+
+#include <libnbd.h>
+
+#define MAX_REQUEST_SIZE (64 * 1024 * 1024)
+
+static struct nbd_handle *nbd;
+static bool readonly = false;
+static char *mountpoint, *filename;
+static const char *pidfile;
+static char *fuse_options;
+static struct fuse_chan *ch;
+static struct fuse *fuse;
+static struct timespec start_t;
+static uint64_t size;
+
+static int nbdfuse_getattr (const char *path, struct stat *stbuf);
+static int nbdfuse_readdir (const char *path, void *buf,
+                            fuse_fill_dir_t filler,
+                            off_t offset, struct fuse_file_info *fi);
+static int nbdfuse_open (const char *path, struct fuse_file_info *fi);
+static int nbdfuse_read (const char *path, char *buf,
+                         size_t count, off_t offset,
+                         struct fuse_file_info *fi);
+static int nbdfuse_write (const char *path, const char *buf,
+                          size_t count, off_t offset,
+                          struct fuse_file_info *fi);
+static int nbdfuse_fsync (const char *path, int datasync,
+                          struct fuse_file_info *fi);
+static int nbdfuse_release (const char *path, struct fuse_file_info *fi);
+
+static struct fuse_operations fuse_operations = {
+  .getattr           = nbdfuse_getattr,
+  .readdir           = nbdfuse_readdir,
+  .open              = nbdfuse_open,
+  .read              = nbdfuse_read,
+  .write             = nbdfuse_write,
+  .fsync             = nbdfuse_fsync,
+  .release           = nbdfuse_release,
+};
+
+static void __attribute__((noreturn))
+usage (FILE *fp, int exitcode)
+{
+  fprintf (fp,
+"    nbdfuse [-r] MOUNTPOINT[/FILENAME] URI\n"
+"Other modes:\n"
+"    nbdfuse MOUNTPOINT[/FILENAME] --command CMD [ARGS ...]\n"
+"    nbdfuse MOUNTPOINT[/FILENAME] --socket-activation CMD [ARGS ...]\n"
+"    nbdfuse MOUNTPOINT[/FILENAME] --fd N\n"
+"    nbdfuse MOUNTPOINT[/FILENAME] --tcp HOST PORT\n"
+"    nbdfuse MOUNTPOINT[/FILENAME] --unix SOCKET\n"
+"\n"
+"Please read the nbdfuse(1) manual page for full usage.\n"
+);
+  exit (exitcode);
+}
+
+static void
+display_version (void)
+{
+  printf ("%s %s\n", PACKAGE_NAME, PACKAGE_VERSION);
+}
+
+static void
+fuse_help (const char *prog)
+{
+  static struct fuse_operations null_operations;
+  const char *tmp_argv[] = { prog, "--help", NULL };
+  fuse_main (2, (char **) tmp_argv, &null_operations, NULL);
+  exit (EXIT_SUCCESS);
+}
+
+static bool
+is_directory (const char *path)
+{
+  struct stat statbuf;
+
+  if (stat (path, &statbuf) == -1)
+    return false;
+  return S_ISDIR (statbuf.st_mode);
+}
+
+int
+main (int argc, char *argv[])
+{
+  enum {
+    MODE_URI,
+    MODE_COMMAND,
+    MODE_FD,
+    MODE_SOCKET_ACTIVATION,
+    MODE_TCP,
+    MODE_UNIX,
+  } mode = MODE_URI;
+  enum {
+    HELP_OPTION = CHAR_MAX + 1,
+    FUSE_HELP_OPTION,
+  };
+  /* Note the "+" means we stop processing as soon as we get to the
+   * first non-option argument (the mountpoint) and then we parse the
+   * rest of the command line without getopt.
+   */
+  const char *short_options = "+o:P:rV";
+  const struct option long_options[] = {
+    { "fuse-help",          no_argument,       NULL, FUSE_HELP_OPTION },
+    { "help",               no_argument,       NULL, HELP_OPTION },
+    { "pidfile",            required_argument, NULL, 'P' },
+    { "pid-file",           required_argument, NULL, 'P' },
+    { "readonly",           no_argument,       NULL, 'r' },
+    { "read-only",          no_argument,       NULL, 'r' },
+    { "version",            no_argument,       NULL, 'V' },
+
+    { NULL }
+  };
+  int c, fd, r;
+  int64_t ssize;
+  const char *s;
+  struct fuse_args fuse_args = FUSE_ARGS_INIT (0, NULL);
+  struct sigaction sa;
+  FILE *fp;
+
+  for (;;) {
+    c = getopt_long (argc, argv, short_options, long_options, NULL);
+    if (c == -1)
+      break;
+
+    switch (c) {
+    case HELP_OPTION:
+      usage (stdout, EXIT_SUCCESS);
+
+    case FUSE_HELP_OPTION:
+      fuse_help (argv[0]);
+      exit (EXIT_SUCCESS);
+
+    case 'o':
+      fuse_opt_add_opt_escaped (&fuse_options, optarg);
+      break;
+
+    case 'P':
+      pidfile = optarg;
+      break;
+
+    case 'r':
+      readonly = true;
+      break;
+
+    case 'V':
+      display_version ();
+      exit (EXIT_SUCCESS);
+
+    default:
+      fprintf (stderr, "\n");
+      usage (stderr, EXIT_FAILURE);
+    }
+  }
+
+  /* There must be at least 2 parameters (mountpoint and
+   * URI/--command/etc).
+   */
+  if (argc - optind < 2)
+    usage (stderr, EXIT_FAILURE);
+
+  /* Parse and check the mountpoint.  It might be MOUNTPOINT or
+   * MOUNTPOINT/FILENAME.  In either case MOUNTPOINT must be an
+   * existing directory.
+   */
+  s = argv[optind++];
+  if (is_directory (s)) {
+    mountpoint = strdup (s);
+    filename = strdup ("nbd");
+    if (mountpoint == NULL || filename == NULL) {
+    strdup_error:
+      perror ("strdup");
+      exit (EXIT_FAILURE);
+    }
+  }
+  else {
+    const char *p = strrchr (s, '/');
+
+    if (p == NULL) {
+    mp_error:
+      fprintf (stderr, "%s: %s: "
+               "mountpoint must be \"directory\" or \"directory/filename\"\n",
+               argv[0], s);
+      exit (EXIT_FAILURE);
+    }
+    mountpoint = strndup (s, p-s);
+    if (mountpoint == NULL) goto strdup_error;
+    if (! is_directory (mountpoint)) goto mp_error;
+    if (strlen (p+1) == 0) goto mp_error;
+    filename = strdup (p+1);
+    if (filename == NULL) goto strdup_error;
+  }
+
+  /* The next parameter is either a URI or a mode switch. */
+  if (strcmp (argv[optind], "--command") == 0 ||
+      strcmp (argv[optind], "--cmd") == 0) {
+    mode = MODE_COMMAND;
+    optind++;
+  }
+  else if (strcmp (argv[optind], "--socket-activation") == 0 ||
+           strcmp (argv[optind], "--systemd-socket-activation") == 0) {
+    mode = MODE_SOCKET_ACTIVATION;
+    optind++;
+  }
+  else if (strcmp (argv[optind], "--fd") == 0) {
+    mode = MODE_FD;
+    optind++;
+  }
+  else if (strcmp (argv[optind], "--tcp") == 0) {
+    mode = MODE_TCP;
+    optind++;
+  }
+  else if (strcmp (argv[optind], "--unix") == 0) {
+    mode = MODE_UNIX;
+    optind++;
+  }
+  else if (argv[optind][0] == '-') {
+    fprintf (stderr, "%s: unknown mode: %s\n\n", argv[0], argv[optind]);
+    usage (stderr, EXIT_FAILURE);
+  }
+
+  /* Check there are enough parameters following given the mode. */
+  switch (mode) {
+  case MODE_URI:
+  case MODE_FD:
+  case MODE_UNIX:
+    if (argc - optind != 1)
+      usage (stderr, EXIT_FAILURE);
+    break;
+  case MODE_TCP:
+    if (argc - optind != 2)
+      usage (stderr, EXIT_FAILURE);
+    break;
+  case MODE_COMMAND:
+  case MODE_SOCKET_ACTIVATION:
+    if (argc - optind < 1)
+      usage (stderr, EXIT_FAILURE);
+    break;
+  }
+  /* At this point we know the command line is valid, and so can start
+   * opening FUSE and libnbd.
+   */
+
+  /* Create the libnbd handle. */
+  nbd = nbd_create ();
+  if (nbd == NULL) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+
+  /* Connect to the NBD server synchronously. */
+  switch (mode) {
+  case MODE_URI:
+    if (nbd_connect_uri (nbd, argv[optind]) == -1) {
+      fprintf (stderr, "%s\n", nbd_get_error ());
+      exit (EXIT_FAILURE);
+    }
+    break;
+
+  case MODE_COMMAND:
+    if (nbd_connect_command (nbd, &argv[optind]) == -1) {
+      fprintf (stderr, "%s\n", nbd_get_error ());
+      exit (EXIT_FAILURE);
+    }
+    break;
+
+  case MODE_SOCKET_ACTIVATION:
+    if (nbd_connect_systemd_socket_activation (nbd, &argv[optind]) == -1) {
+      fprintf (stderr, "%s\n", nbd_get_error ());
+      exit (EXIT_FAILURE);
+    }
+    break;
+
+  case MODE_FD:
+    if (sscanf (argv[optind], "%d", &fd) != 1) {
+      fprintf (stderr, "%s: could not parse file descriptor: %s\n\n",
+               argv[0], argv[optind]);
+      exit (EXIT_FAILURE);
+    }
+    if (nbd_connect_socket (nbd, fd) == -1) {
+      fprintf (stderr, "%s\n", nbd_get_error ());
+      exit (EXIT_FAILURE);
+    }
+    break;
+
+  case MODE_TCP:
+    if (nbd_connect_tcp (nbd, argv[optind], argv[optind+1]) == -1) {
+      fprintf (stderr, "%s\n", nbd_get_error ());
+      exit (EXIT_FAILURE);
+    }
+    break;
+
+  case MODE_UNIX:
+    if (nbd_connect_unix (nbd, argv[optind]) == -1) {
+      fprintf (stderr, "%s\n", nbd_get_error ());
+      exit (EXIT_FAILURE);
+    }
+    break;
+  }
+
+  ssize = nbd_get_size (nbd);
+  if (ssize == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  size = (uint64_t) ssize;
+
+  /* This is just used to give an unchanging time when they stat in
+   * the mountpoint.
+   */
+  clock_gettime (CLOCK_REALTIME, &start_t);
+
+  /* Create the FUSE args. */
+  if (fuse_opt_add_arg (&fuse_args, argv[0]) == -1) {
+  fuse_opt_error:
+    perror ("fuse_opt_add_arg");
+    exit (EXIT_FAILURE);
+  }
+
+  if (fuse_options) {
+    if (fuse_opt_add_arg (&fuse_args, "-o") == -1 ||
+        fuse_opt_add_arg (&fuse_args, fuse_options) == -1)
+      goto fuse_opt_error;
+  }
+
+  /* Create the FUSE mountpoint. */
+  ch = fuse_mount (mountpoint, &fuse_args);
+  if (ch == NULL) {
+    fprintf (stderr,
+             "%s: fuse_mount failed: see error messages above", argv[0]);
+    exit (EXIT_FAILURE);
+  }
+
+  /* Set F_CLOEXEC on the channel.  Some versions of libfuse don't do
+   * this.
+   */
+  fd = fuse_chan_fd (ch);
+  if (fd >= 0) {
+    int flags = fcntl (fd, F_GETFD, 0);
+    if (flags >= 0)
+      fcntl (fd, F_SETFD, flags & ~FD_CLOEXEC);
+  }
+
+  /* Create the FUSE handle. */
+  fuse = fuse_new (ch, &fuse_args,
+                   &fuse_operations, sizeof fuse_operations, NULL);
+  if (!fuse) {
+    perror ("fuse_new");
+    exit (EXIT_FAILURE);
+  }
+  fuse_opt_free_args (&fuse_args);
+
+  /* Catch signals since they can leave the mountpoint in a funny
+   * state.  To exit the program callers must use ‘fusermount -u’.  We
+   * also must be careful not to call exit(2) in this program until we
+   * have unmounted the filesystem below.
+   */
+  memset (&sa, 0, sizeof sa);
+  sa.sa_handler = SIG_IGN;
+  sa.sa_flags = SA_RESTART;
+  sigaction (SIGPIPE, &sa, NULL);
+  sigaction (SIGINT, &sa, NULL);
+  sigaction (SIGQUIT, &sa, NULL);
+
+  /* Ready to serve, write pidfile. */
+  if (pidfile) {
+    fp = fopen (pidfile, "w");
+    if (fp) {
+      fprintf (fp, "%ld", (long) getpid ());
+      fclose (fp);
+    }
+  }
+
+  /* Enter the main loop. */
+  r = fuse_loop (fuse);
+  if (r != 0)
+    perror ("fuse_loop");
+
+  /* Close FUSE. */
+  fuse_unmount (mountpoint, ch);
+  fuse_destroy (fuse);
+
+  /* Close NBD handle. */
+  nbd_close (nbd);
+
+  free (mountpoint);
+  free (filename);
+  free (fuse_options);
+
+  exit (r == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
+}
+
+/* Wraps calls to libnbd functions and automatically checks for a
+ * returns errors in the format required by FUSE.  It also prints out
+ * the full error message on stderr, so that we don't lose it.
+ */
+#define CHECK_NBD_ERROR(CALL)                                   \
+  do { if ((CALL) == -1) return check_nbd_error (); } while (0)
+static int
+check_nbd_error (void)
+{
+  int err;
+
+  fprintf (stderr, "%s\n", nbd_get_error ());
+  err = nbd_get_errno ();
+  if (err != 0)
+    return -err;
+  else
+    return -EIO;
+}
+
+static int
+nbdfuse_getattr (const char *path, struct stat *statbuf)
+{
+  const int mode = readonly ? 0444 : 0666;
+
+  memset (statbuf, 0, sizeof (struct stat));
+
+  /* We're probably making some Linux-specific assumptions here, but
+   * this file is not compiled on non-Linux systems.
+   */
+  statbuf->st_atim = start_t;
+  statbuf->st_mtim = start_t;
+  statbuf->st_ctim = start_t;
+  statbuf->st_uid = geteuid ();
+  statbuf->st_gid = getegid ();
+
+  if (strcmp (path, "/") == 0) {
+    /* getattr "/" */
+    statbuf->st_mode = S_IFDIR | (mode & 0111);
+    statbuf->st_nlink = 2;
+  }
+  else if (path[0] == '/' && strcmp (path+1, filename) == 0) {
+    /* getattr "/filename" */
+    statbuf->st_mode = S_IFREG | mode;
+    statbuf->st_nlink = 1;
+    statbuf->st_size = size;
+  }
+  else
+    return -ENOENT;
+
+  return 0;
+}
+
+static int
+nbdfuse_readdir (const char *path, void *buf,
+                 fuse_fill_dir_t filler,
+                 off_t offset, struct fuse_file_info *fi)
+{
+  if (strcmp (path, "/") != 0)
+    return -ENOENT;
+
+  filler (buf, ".", NULL, 0);
+  filler (buf, "..", NULL, 0);
+  filler (buf, filename, NULL, 0);
+
+  return 0;
+}
+
+/* This function checks the O_RDONLY/O_RDWR flags passed to the
+ * open(2) call, so we have to check the open mode is compatible with
+ * the readonly flag.
+ */
+static int
+nbdfuse_open (const char *path, struct fuse_file_info *fi)
+{
+  if (path[0] != '/' || strcmp (path+1, filename) != 0)
+    return -ENOENT;
+
+  if (readonly && (fi->flags & O_ACCMODE) != O_RDONLY)
+    return -EACCES;
+
+  return 0;
+}
+
+static int
+nbdfuse_read (const char *path, char *buf,
+              size_t count, off_t offset,
+              struct fuse_file_info *fi)
+{
+  if (path[0] != '/' || strcmp (path+1, filename) != 0)
+    return -ENOENT;
+
+  if (offset >= size)
+    return 0;
+
+  if (count > MAX_REQUEST_SIZE)
+    count = MAX_REQUEST_SIZE;
+
+  if (offset + count > size)
+    count = size - offset;
+
+  CHECK_NBD_ERROR (nbd_pread (nbd, buf, count, offset, 0));
+
+  return (int) count;
+}
+
+static int
+nbdfuse_write (const char *path, const char *buf,
+               size_t count, off_t offset,
+               struct fuse_file_info *fi)
+{
+  /* Probably shouldn't happen because of nbdfuse_open check. */
+  if (readonly)
+    return -EACCES;
+
+  if (path[0] != '/' || strcmp (path+1, filename) != 0)
+    return -ENOENT;
+
+  if (offset >= size)
+    return 0;
+
+  if (count > MAX_REQUEST_SIZE)
+    count = MAX_REQUEST_SIZE;
+
+  if (offset + count > size)
+    count = size - offset;
+
+  CHECK_NBD_ERROR (nbd_pwrite (nbd, buf, count, offset, 0));
+
+  return (int) count;
+}
+
+static int
+nbdfuse_fsync (const char *path, int datasync, struct fuse_file_info *fi)
+{
+  if (readonly)
+    return 0;
+
+  /* If the server doesn't support flush then the operation is
+   * silently ignored.
+   */
+  if (nbd_can_flush (nbd))
+    CHECK_NBD_ERROR (nbd_flush (nbd, 0));
+
+  return 0;
+}
+
+/* This is called on the last close of a file.  We do a flush here to
+ * be on the safe side, but it's not strictly necessary.
+ */
+static int
+nbdfuse_release (const char *path, struct fuse_file_info *fi)
+{
+  if (readonly)
+    return 0;
+
+  return nbdfuse_fsync (path, 0, fi);
+}
diff --git a/fuse/nbdfuse.pod b/fuse/nbdfuse.pod
new file mode 100644
index 0000000..e43e23c
--- /dev/null
+++ b/fuse/nbdfuse.pod
@@ -0,0 +1,262 @@
+=head1 NAME
+
+nbdfuse - present a network block device in a FUSE filesystem
+
+=head1 SYNOPSIS
+
+ nbdfuse [-o FUSE-OPTION] [-P PIDFILE] [-r]
+         MOUNTPOINT[/FILENAME] URI
+
+Other modes:
+
+ nbdfuse MOUNTPOINT[/FILENAME] --command CMD [ARGS ...]
+
+ nbdfuse MOUNTPOINT[/FILENAME] --socket-activation CMD [ARGS ...]
+
+ nbdfuse MOUNTPOINT[/FILENAME] --fd N
+
+ nbdfuse MOUNTPOINT[/FILENAME] --tcp HOST PORT
+
+ nbdfuse MOUNTPOINT[/FILENAME] --unix SOCKET
+
+=head1 DESCRIPTION
+
+nbdfuse presents a Network Block Device as a local file inside a FUSE
+filesystem.
+
+The FUSE filesystem is mounted at F<MOUNTPOINT> and contains a single
+virtual file called F<FILENAME> (defaulting to F<nbd>).  Reads and
+writes to the virtual file or device are turned into reads and writes
+to the NBD device.
+
+The NBD device itself can be local or remote and is specified by an
+NBD URI (like C<nbd://localhost>, see L<nbd_connect_uri(3)>) or
+various other modes.
+
+Use C<fusermount -u MOUNTPOINT> to unmount the filesystem after you
+have used it.
+
+This program is similar in concept to L<nbd-client(8)> (which turns
+NBD into F</dev/nbdX> device nodes), except:
+
+=over 4
+
+=item *
+
+nbd-client is faster because it uses a special kernel module
+
+=item *
+
+nbd-client requires root, but nbdfuse can be used by any user
+
+=item *
+
+nbdfuse virtual files can be mounted anywhere in the filesystem
+
+=item *
+
+nbdfuse uses libnbd to talk to the NBD server
+
+=item *
+
+nbdfuse requires FUSE support in the kernel
+
+=back
+
+=head1 EXAMPLES
+
+=head2 Present a remote NBD server as a local file
+
+If there is a remote NBD server running on C<example.com> at the
+default NBD port number (10809) then you can turn it into a local file
+by doing:
+
+ $ mkdir dir
+ $ nbdfuse dir nbd://example.com &
+ $ ls -l dir/
+ total 0
+ -rw-rw-rw-. 1 nbd nbd 1073741824 Jan  1 10:10 nbd
+
+The file is called F<dir/nbd> and you can read and write to it as if
+it is a normal file.  Note that writes to the file will write to the
+remote NBD server.  After using it, unmount it:
+
+ $ fusermount -u dir
+ $ rmdir dir
+
+=head2 Use nbdkit to create a file backed by a temporary RAM disk
+
+L<nbdkit(1)> has an I<-s> option allowing it to serve over
+stdin/stdout.  You can combine this with nbdfuse as follows:
+
+ $ mkdir dir
+ $ nbdfuse dir/ramdisk --command nbdkit -s memory 1G &
+ $ ls -l dir/
+ total 0
+ -rw-rw-rw-. 1 nbd nbd 1073741824 Jan  1 10:10 ramdisk
+ $ dd if=/dev/urandom bs=1M count=100 of=mp/ramdisk conv=notrunc,nocreat
+ 100+0 records in
+ 100+0 records out
+ 104857600 bytes (105 MB, 100 MiB) copied, 2.08319 s, 50.3 MB/s
+
+When you have finished with the RAM disk, you can unmount it as below
+which will cause nbdkit to exit and the RAM disk contents to be
+discarded:
+
+ $ fusermount -u dir
+ $ rmdir dir
+
+=head2 Use qemu-nbd to read and modify a qcow2 file
+
+L<qemu-nbd(8)> cannot serve over stdin/stdout, but it can use systemd
+socket activation.  You can combine this with nbdfuse and use it to
+open any file format which qemu understands:
+
+ $ mkdir dir
+ $ nbdfuse dir/file.raw \
+           --socket-activation qemu-nbd -f qcow2 file.qcow2 &
+ $ ls -l dir/
+ total 0
+ -rw-rw-rw-. 1 nbd nbd 1073741824 Jan  1 10:10 file.raw
+
+File F<dir/file.raw> is in raw format, backed by F<file.qcow2>.  Any
+changes made to F<dir/file.raw> are reflected into the qcow2 file.  To
+unmount the file do:
+
+ $ fusermount -u dir
+ $ rmdir dir
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<--help>
+
+Display brief command line help and exit.
+
+=item B<--fuse-help>
+
+Display FUSE options and exit.  See I<-o> below.
+
+=item B<--command> CMD [ARGS ...]
+
+Select command mode.  In this mode an NBD server can be run directly
+from the command line with nbdfuse communicating with the server over
+the server’s stdin/stdout.  Normally you would use this with
+C<nbdkit -s>.  See L</EXAMPLES> above and L<nbd_connect_command(3)>.
+
+=item B<--fd> N
+
+Select file descriptor mode.  In this mode a connected socket is
+passed to nbdfuse.  nbdfuse connects to the socket on the numbered
+file descriptor.  See also L<nbd_connect_socket(3)>.
+
+=item B<-o> FUSE-OPTION
+
+Pass extra options to FUSE.  To get a list of all the extra options
+supported by FUSE, use I<--fuse-help>.
+
+Some potentially useful FUSE options:
+
+=over 4
+
+=item B<-o> B<allow_other>
+
+Allow other users to see the filesystem.  This option has no effect
+unless you enable it globally in F</etc/fuse.conf>.
+
+=item B<-o> B<kernel_cache>
+
+Allow the kernel to cache files (reduces the number of reads that have
+to go through the L<libnbd(3)> API).  This is generally a good idea if
+you can afford the extra memory usage.
+
+=item B<-o> B<uid=>N B<-o> B<gid=>N
+
+Use these options to map UIDs and GIDs.
+
+=back
+
+=item B<-P> PIDFILE
+
+=item B<--pidfile> PIDFILE
+
+When nbdfuse is ready to serve, write the nbdfuse process ID (PID) to
+F<PIDFILE>.  This can be used in scripts to wait until nbdfuse is
+ready.  Note you mustn't try to kill nbdfuse.  Use C<fusermount -u> to
+unmount the mountpoint which will cause nbdfuse to exit cleanly.
+
+=item B<-r>
+
+=item B<--readonly>
+
+Access the network block device read-only.  The virtual file will have
+read-only permissions, and any writes will return errors.
+
+=item B<--socket-activation> CMD [ARGS ...]
+
+Select systemd socket activation mode.  This is similar to
+I<--command>, but is used for servers like L<qemu-nbd(8)> which
+support systemd socket activation.  See L</EXAMPLES> above and
+L<nbd_connect_systemd_socket_activation(3)>.
+
+=item B<--tcp> HOST PORT
+
+Select TCP mode.  Connect to an NBD server on a host and port over an
+unencrypted TCP socket.  See also L<nbd_connect_tcp(3)>.
+
+=item B<--unix> SOCKET
+
+Select Unix mode.  Connect to an NBD server on a Unix domain socket.
+See also L<nbd_connect_unix(3)>.
+
+=item B<-V>
+
+=item B<--version>
+
+Display the package name and version and exit.
+
+=back
+
+=head1 NOTES
+
+=head2 Loop mounting
+
+It is tempting (and possible) to loop mount the file.  However this
+will be very slow and may sometimes deadlock.  Better alternatives are
+to use either L<nbd-client(8)>, or more securely L<libguestfs(3)>,
+L<guestfish(1)> or L<guestmount(1)> which can all access NBD servers.
+
+=head2 As a way to access NBD servers
+
+You can use this to access NBD servers, but it is usually better (and
+definitely much faster) to use L<libnbd(3)> directly instead.  To
+access NBD servers from the command line, look at L<nbdsh(1)>.
+
+=head1 SEE ALSO
+
+L<libnbd(3)>,
+L<nbdsh(1)>,
+L<fusermount(1)>,
+L<mount.fuse(8)>,
+L<nbd_connect_uri(3)>,
+L<nbd_connect_command(3)>,
+L<nbd_connect_socket(3)>,
+L<nbd_connect_systemd_socket_activation(3)>,
+L<nbd_connect_tcp(3)>,
+L<nbd_connect_unix(3)>,
+L<libguestfs(3)>,
+L<guestfish(1)>,
+L<guestmount(1)>,
+L<nbdkit(1)>,
+L<nbdkit-loop(1)>,
+L<qemu-nbd(8)>,
+L<nbd-client(8)>.
+
+=head1 AUTHORS
+
+Richard W.M. Jones
+
+=head1 COPYRIGHT
+
+Copyright (C) 2019 Red Hat Inc.
diff --git a/fuse/test-nbdkit.sh b/fuse/test-nbdkit.sh
new file mode 100755
index 0000000..fe7279b
--- /dev/null
+++ b/fuse/test-nbdkit.sh
@@ -0,0 +1,63 @@
+#!/usr/bin/env bash
+# nbd client library in userspace
+# Copyright (C) 2019 Red Hat Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+# Test nbdfuse + nbdkit.
+
+. ../tests/functions.sh
+
+set -e
+set -x
+
+requires nbdkit --exit-with-parent --version
+requires cmp --version
+requires dd --version
+
+if ! test -r /dev/urandom; then
+    echo "$0: test skipped: /dev/urandom not readable"
+    exit 77
+fi
+
+pidfile=test-nbdkit.pid
+mp=test-nbdkit.d
+data=test-nbdkit.data
+cleanup_fn fusermount -u $mp
+cleanup_fn rm -rf $mp
+cleanup_fn rm -f $pidfile $data
+
+mkdir -p $mp
+$VG nbdfuse -P $pidfile $mp \
+        --command nbdkit -s --exit-with-parent memory 10M &
+
+# Wait for the pidfile to appear.
+for i in {1..60}; do
+    if test -f $pidfile; then
+        break
+    fi
+    sleep 1
+done
+if ! test -f $pidfile; then
+    echo "$0: nbdfuse PID file $pidfile was not created"
+    exit 1
+fi
+
+dd if=/dev/urandom of=$data bs=1M count=10
+# Use a weird block size when writing.  It's a bit pointless because
+# something in the Linux/FUSE stack turns these into exact 4096 byte
+# writes.
+dd if=$data of=$mp/nbd bs=65519 conv=nocreat,notrunc
+cmp $data $mp/nbd
diff --git a/fuse/test-qcow2.sh b/fuse/test-qcow2.sh
new file mode 100755
index 0000000..95f97b5
--- /dev/null
+++ b/fuse/test-qcow2.sh
@@ -0,0 +1,64 @@
+#!/usr/bin/env bash
+# nbd client library in userspace
+# Copyright (C) 2019 Red Hat Inc.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+# The nbdfuse documentation describes how you can use nbdfuse +
+# qemu-nbd to open qcow2 files.  This claim is tested here.
+
+. ../tests/functions.sh
+
+set -e
+set -x
+
+requires qemu-nbd --version
+requires qemu-img --version
+requires cmp --version
+requires dd --version
+
+if ! test -r /dev/urandom; then
+    echo "$0: test skipped: /dev/urandom not readable"
+    exit 77
+fi
+
+pidfile=test-qcow2.pid
+mp=test-qcow2.d
+data=test-qcow2.data
+qcow2=test-qcow2.qcow2
+cleanup_fn fusermount -u $mp
+cleanup_fn rm -rf $mp
+cleanup_fn rm -f $pidfile $data $qcow2
+
+dd if=/dev/urandom of=$data bs=1M count=1
+qemu-img convert -f raw $data -O qcow2 $qcow2
+
+mkdir -p $mp
+$VG nbdfuse -r -P $pidfile $mp \
+        --socket-activation qemu-nbd -f qcow2 $qcow2 &
+
+# Wait for the pidfile to appear.
+for i in {1..60}; do
+    if test -f $pidfile; then
+        break
+    fi
+    sleep 1
+done
+if ! test -f $pidfile; then
+    echo "$0: nbdfuse PID file $pidfile was not created"
+    exit 1
+fi
+
+cmp $data $mp/nbd
diff --git a/run.in b/run.in
index 83c92a7..599752d 100755
--- a/run.in
+++ b/run.in
@@ -51,6 +51,7 @@ s="$(cd @abs_srcdir@ && pwd)"
 b="$(cd @abs_builddir@ && pwd)"
 
 # Set the PATH to contain all libnbd binaries.
+prepend PATH "$b/fuse"
 prepend PATH "$b/sh"
 export PATH
 
diff --git a/sh/nbdsh.pod b/sh/nbdsh.pod
index 0037dc3..c1d93ba 100644
--- a/sh/nbdsh.pod
+++ b/sh/nbdsh.pod
@@ -97,6 +97,7 @@ Display the package name and version and exit.
 
 L<libnbd(3)>,
 L<libnbd-security(3)>,
+L<nbdfuse(1)>,
 L<qemu-img(1)>.
 
 =head1 AUTHORS
-- 
2.23.0




More information about the Libguestfs mailing list