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