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