Richard W.M. Jones
2019-Oct-12 14:21 UTC
[Libguestfs] [PATCH libnbd] nbdfuse: New tool to present a network block device in a FUSE filesystem.
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
Richard W.M. Jones
2019-Oct-12 20:16 UTC
Re: [Libguestfs] [PATCH libnbd] nbdfuse: New tool to present a network block device in a FUSE filesystem.
As it's a new feature with interesting use cases, I have pushed this one now. Rich. -- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones Read my programming and virtualization blog: http://rwmj.wordpress.com virt-df lists disk usage of guests without needing to install any software inside the virtual machine. Supports Linux and Windows. http://people.redhat.com/~rjones/virt-df/
Eric Blake
2019-Oct-14 21:06 UTC
Re: [Libguestfs] [PATCH libnbd] nbdfuse: New tool to present a network block device in a FUSE filesystem.
On 10/12/19 9:21 AM, Richard W.M. Jones wrote:> 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 >Cool!> There are still some shortcomings, such as lack of zero and trim > support. These are documented in the TODO file. >> +++ 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.Minimum version?> +++ 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)>,Worth sorting these two alphabetically?> L<qemu(1)>. >> +++ b/fuse/nbdfuse.c> + > +#define FUSE_USE_VERSION 26 > + > +#include <fuse.h> > +#include <fuse_lowlevel.h> > + > +#include <libnbd.h> > + > +#define MAX_REQUEST_SIZE (64 * 1024 * 1024)Although this works with nbdkit, qemu-nbd doesn't like more than 32M. (We really should find time to teach nbdkit/libnbd about block size reporting, but that's a bigger project...)> + > +static struct nbd_handle *nbd; > +static bool readonly = false;Looks funny to initialize a static variable to 0 in a block of static variables with no initializers (C guarantees 0-initialization even if you aren't explicit).> +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 void __attribute__((noreturn)) > +usage (FILE *fp, int exitcode) > +{ > + fprintf (fp, > +" nbdfuse [-r] MOUNTPOINT[/FILENAME] URI\n"Do we want to use any #ifdefs to avoid advertising URI support on the command line when libnbd is compiled without libxml2?> +"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"No mention of nbdfuse -o or -P.> +"\n" > +"Please read the nbdfuse(1) manual page for full usage.\n" > +); > + exit (exitcode);nbdfuse --help > /dev/full exits with status 0 because we didn't check for error on stdout/stderr. That's a corner case, and many programs don't care about it, but it's worth deciding if we want to care.> +} > + > +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);Accepts a symlink-to-directory, but that's fine by me.> +} > + > +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' },Worth a long-option synonym for -o?> + /* 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++; > + }Is it worth using getopt_long() in this section for allowing unambiguous prefix spellings (--c for example) and/or a short option (-c for example)?> + else if (strcmp (argv[optind], "--socket-activation") == 0 || > + strcmp (argv[optind], "--systemd-socket-activation") == 0) { > + mode = MODE_SOCKET_ACTIVATION; > + optind++; > + }On the same theme, '--socket' as a synonym is easier to type than --socket-activation.> + 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_FD: > + if (sscanf (argv[optind], "%d", &fd) != 1) {Overflow is undetected.> + 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; > +> + > + /* 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);Doesn't check for (unlikely) error.> + } > + > + /* 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);Looks deceptively simple :)> +} > + > +/* Wraps calls to libnbd functions and automatically checks for a > + * returns errors in the format required by FUSE. It also prints outMissing a word or two after 'checks for a'> + * 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 ();Comment is interesting if true. However, a google search for 'man fuse_main' pulls up https://man.openbsd.org/fuse_main.3 as its first hit, so I think FUSE has graduated to non-Linux systems, so we may have to revisit this later.> +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); > +Does FUSE have a way to populate d_type during readdir (DT_DIR for '.', '..', DT_REG for filename)?> +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;Is EROFS any better here?> +++ 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] URIThis synopsis looks better than the one in usage().> + > +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.Does umount(8) call into fusermount correctly?> + > +This program is similar in concept to L<nbd-client(8)> (which turns > +NBD into F</dev/nbdX> device nodes), except:Is it worth mentioning that qemu-nbd(8) alongside nbd-client(8)?> + > +=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 > + > +=backDecent list.> + > +=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 dirWhat a fun way to use memory :)> + > +=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 > +The real power shines through - we have used the FUSE kernel module for user-space mounting of a qcow2 image, instead of the nbd kernel module for root-only mounting of a qcow2 image ;)> +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.Does this line up with the stats we reported earlier in getattr()?> + > +=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)>.How hard would it be to support encryption? Obviously, the fuse-mounted file will be unencrypted, but libnbd connect to an encrypted nbd server could prove useful.> + > +=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)>,Worth mentioning qemu-nbd(8) alongside nbd-client(8)?> +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)>. > +Overall looks like a fun wrapper, to demonstrate how many layers we can shuffle data through to produce/consume it in the format of interest ;) -- Eric Blake, Principal Software Engineer Red Hat, Inc. +1-919-301-3226 Virtualization: qemu.org | libvirt.org
Richard W.M. Jones
2019-Oct-14 22:12 UTC
Re: [Libguestfs] [PATCH libnbd] nbdfuse: New tool to present a network block device in a FUSE filesystem.
On Mon, Oct 14, 2019 at 04:06:27PM -0500, Eric Blake wrote:> >+ * FUSE to build the nbdfuse program. > > Minimum version?libguestfs contains a workaround for missing fuse_opt_add_opt_escaped, but that function has been present in libfuse since 2008, so I'm not exactly sure why that was needed. RHEL 5?? FUSE_USE_VERSION 26 has been present since at least 2006 when the current libfuse git repo runs out. So I guess "any" :-) until proven otherwise.> >+ 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' }, > > Worth a long-option synonym for -o?As it stands this is the same as guestmount and indeed plain mount(8).> >+ else if (strcmp (argv[optind], "--socket-activation") == 0 || > >+ strcmp (argv[optind], "--systemd-socket-activation") == 0) { > >+ mode = MODE_SOCKET_ACTIVATION; > >+ optind++; > >+ } > > On the same theme, '--socket' as a synonym is easier to type than > --socket-activation.Hmm, sounds like that should be a synonym for --fd !> >+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); > >+ > > Does FUSE have a way to populate d_type during readdir (DT_DIR for > '.', '..', DT_REG for filename)?Yes. IIRC that is the fourth field. However we have got away with never setting this in libguestfs, and the usage here is barely performance sensitive so ...> >+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; > > Is EROFS any better here?This case should probably never happen. However it does remind me of a thing we might one day want to implement in libnbd: connections which are forced (at the client end) to be read-only. Libnbd would implement this by intercepting any write operation and immediately returning EROFS or similar.> >+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. > > Does umount(8) call into fusermount correctly?I think the other way around. AFAICT fusermount is a slightly verbose wrapper around umount2(2). However it is quite problematic for various reasons, some outside the control of FUSE (eg. GNOME utilities jumping into new mountpoints and preventing them from being unmounted, which is especially a problem in scripts). In libguestfs we wrote a quite sophisticated wrapper around fusermount: https://github.com/libguestfs/libguestfs/blob/master/fuse/guestunmount.c> >+=item B<-o> B<uid=>N B<-o> B<gid=>N > >+ > >+Use these options to map UIDs and GIDs. > > Does this line up with the stats we reported earlier in getattr()?I didn't test - I might drop this bit.> >+=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)>. > > How hard would it be to support encryption? Obviously, the > fuse-mounted file will be unencrypted, but libnbd connect to an > encrypted nbd server could prove useful.URI mode should support encryption I think (I didn't actually test that). The reason to have the --tcp and --unix modes is to allow shell scripts to use the tool without having to worry about URI escaping. I pushed a few other changes based on the rest of your feedback. Thanks, Rich. -- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones Read my programming and virtualization blog: http://rwmj.wordpress.com virt-p2v converts physical machines to virtual machines. Boot with a live CD or over the network (PXE) and turn machines into KVM guests. http://libguestfs.org/virt-v2v
Seemingly Similar Threads
- Re: [PATCH libnbd] nbdfuse: New tool to present a network block device in a FUSE filesystem.
- [PATCH libnbd 2/2] api: Add support for AF_VSOCK.
- [PATCH libnbd 0/2] api: Add support for AF_VSOCK.
- [PATCH libnbd] copy: Allowing copying from NBD server to NBD server.
- ANNOUNCE: libnbd 1.2 & nbdkit 1.16 - high performance NBD client and server