[Libguestfs] [nbdkit PATCH 2/4] file: Add .list_exports support

Eric Blake eblake at redhat.com
Fri Aug 7 02:23:46 UTC 2020


Add a new mode to the file plugin, using directory=DIR instead of
[file=]FILE, to allow it to serve all regular/block files in a given
directory, as well as advertising the names of those files it will be
serving.

Signed-off-by: Eric Blake <eblake at redhat.com>
---
 plugins/file/nbdkit-file-plugin.pod |  28 +++++-
 tests/Makefile.am                   |   4 +-
 plugins/file/file.c                 | 119 +++++++++++++++++++----
 tests/test-file-dir.sh              | 143 ++++++++++++++++++++++++++++
 4 files changed, 271 insertions(+), 23 deletions(-)
 create mode 100755 tests/test-file-dir.sh

diff --git a/plugins/file/nbdkit-file-plugin.pod b/plugins/file/nbdkit-file-plugin.pod
index dac673ae..f9ae6e97 100644
--- a/plugins/file/nbdkit-file-plugin.pod
+++ b/plugins/file/nbdkit-file-plugin.pod
@@ -6,27 +6,47 @@ nbdkit-file-plugin - nbdkit file plugin

  nbdkit file [file=]FILENAME

+ nbdkit file directory=DIRNAME
+
 =head1 DESCRIPTION

 C<nbdkit-file-plugin> is a file serving plugin for L<nbdkit(1)>.

 It serves the named C<FILENAME> over NBD.  Local block devices
-(eg. F</dev/sda>) may also be served.
+(eg. F</dev/sda>) may also be served.  It may also be used to serve
+any file within a given C<DIRECTORY>, according to which export name
+the guest requests.

 =head1 PARAMETERS

+One of B<file> or B<directory> must be given to determine which mode
+the server will use.
+
 =over 4

 =item [B<file=>]FILENAME

 Serve the file named C<FILENAME>.  A local block device name can also
-be used here.
-
-This parameter is required.
+be used here.  When this mode is used, the export name requested by
+the client is ignored.

 C<file=> is a magic config key and may be omitted in most cases.
 See L<nbdkit(1)/Magic parameters>.

+=item B<directory=>DIRNAME
+
+(nbdkt E<ge> 1.22)
+
+Serve all regular files and block devices located directly within the
+directory named C<DIRNAME>, including those found by following
+symbolic links.  Other special files in the directory (such as
+subdirectories, fifos, or Unix sockets) are ignored.  When this mode
+is used, the file to be served is chosen by the export name passed by
+the client; a client that requests the default export (C<"">) will be
+served whichever file appears first in the L<readdir(3)> listing.  For
+security, when using directory mode, this plugin will not accept
+export names containing slash (C</>).
+
 =back

 =head1 NOTES
diff --git a/tests/Makefile.am b/tests/Makefile.am
index b5ef96a7..d9074ba9 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -636,8 +636,8 @@ test_file_block_SOURCES = test-file-block.c test.h
 test_file_block_CFLAGS = $(WARNINGS_CFLAGS) $(LIBGUESTFS_CFLAGS)
 test_file_block_LDADD = libtest.la $(LIBGUESTFS_LIBS)

-TESTS += test-file-extents.sh
-EXTRA_DIST += test-file-extents.sh
+TESTS += test-file-extents.sh test-file-dir.sh
+EXTRA_DIST += test-file-extents.sh test-file-dir.sh

 # floppy plugin test.
 TESTS += test-floppy.sh
diff --git a/plugins/file/file.c b/plugins/file/file.c
index e049864a..4afcad11 100644
--- a/plugins/file/file.c
+++ b/plugins/file/file.c
@@ -43,6 +43,7 @@
 #include <sys/stat.h>
 #include <sys/ioctl.h>
 #include <errno.h>
+#include <dirent.h>

 #include <pthread.h>

@@ -66,9 +67,11 @@
 #endif

 static char *filename = NULL;
+static char *directory = NULL;
+DIR *dir = NULL;

-/* Any callbacks using lseek must be protected by this lock. */
-static pthread_mutex_t lseek_lock = PTHREAD_MUTEX_INITIALIZER;
+/* Any callbacks using readdir or lseek must be protected by this lock. */
+static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

 /* to enable: -D file.zero=1 */
 int file_debug_zero;
@@ -83,10 +86,14 @@ static void
 file_unload (void)
 {
   free (filename);
+  free (directory);
+  if (dir)
+    closedir (dir);
 }

 /* Called for each key=value passed on the command line.  This plugin
- * only accepts file=<filename>, which is required.
+ * only accepts file=<filename> and directory=<dirname>, where exactly
+ * one is required.
  */
 static int
 file_config (const char *key, const char *value)
@@ -98,6 +105,12 @@ file_config (const char *key, const char *value)
     if (!filename)
       return -1;
   }
+  else if (strcmp (key, "directory") == 0) {
+    free (directory);
+    directory = nbdkit_realpath (value);
+    if (!directory)
+      return -1;
+  }
   else if (strcmp (key, "rdelay") == 0 ||
            strcmp (key, "wdelay") == 0) {
     nbdkit_error ("add --filter=delay on the command line");
@@ -111,13 +124,19 @@ file_config (const char *key, const char *value)
   return 0;
 }

-/* Check the user did pass a file=<FILENAME> parameter. */
+/* Check the user did pass exactly one parameter. */
 static int
 file_config_complete (void)
 {
-  if (filename == NULL) {
-    nbdkit_error ("you must supply the file=<FILENAME> parameter "
-                  "after the plugin name on the command line");
+  if (!filename == !directory) {
+    nbdkit_error ("you must supply exactly one file=<FILENAME> or "
+                  "directory=<DIRNAME> parameter after the plugin name "
+                  "on the command line");
+    return -1;
+  }
+
+  if (directory && (dir = opendir (directory)) == NULL) {
+    nbdkit_error ("opendir: %m");
     return -1;
   }

@@ -125,7 +144,8 @@ file_config_complete (void)
 }

 #define file_config_help \
-  "file=<FILENAME>     (required) The filename to serve." \
+  "file=<FILENAME>       The filename to serve." \
+  "directory=<DIRNAME>   A directory containing files to serve." \

 /* Print some extra information about how the plugin was compiled. */
 static void
@@ -145,8 +165,47 @@ file_dump_plugin (void)
 #endif
 }

+static int file_list_exports (int readonly, int default_only,
+                              struct nbdkit_exports *exports)
+{
+  struct dirent *entry;
+  struct stat sb;
+  int fd;
+
+  if (!directory)
+    return nbdkit_add_export (exports, "", NULL);
+
+  ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lock);
+  rewinddir (dir);
+  fd = dirfd (dir);
+  if (fd == -1) {
+    nbdkit_error ("dirfd: %m");
+    return -1;
+  }
+  errno = 0;
+  while ((entry = readdir (dir)) != NULL) {
+    /* TODO: Optimize with d_type and/or statx when present? */
+    if (fstatat (fd, entry->d_name, &sb, 0) == 0 &&
+        (S_ISREG (sb.st_mode) || S_ISBLK (sb.st_mode))) {
+      if (nbdkit_add_export (exports, entry->d_name, NULL) == -1) {
+        close (fd);
+        return -1;
+      }
+    }
+    errno = 0;
+  }
+  if (errno) {
+    nbdkit_error ("readdir: %m");
+    close (fd);
+    return -1;
+  }
+  close (fd);
+  return 0;
+}
+
 /* The per-connection handle. */
 struct handle {
+  char *file;
   int fd;
   bool is_block_device;
   int sector_size;
@@ -170,21 +229,44 @@ file_open (int readonly)
     return NULL;
   }

+  if (directory) {
+    const char *exportname = nbdkit_export_name ();
+
+    if (strchr (exportname, '/')) {
+      nbdkit_error ("exportname cannot contain /");
+      errno = EINVAL;
+      free (h);
+      return NULL;
+    }
+    if (asprintf (&h->file, "%s/%s", directory, exportname) == -1) {
+      nbdkit_error ("asprintf: %m");
+      free (h);
+      return NULL;
+    }
+  }
+  else
+    h->file = strdup (filename);
+  if (h->file == NULL) {
+    nbdkit_error ("malloc: %m");
+    free (h);
+    return NULL;
+  }
+
   flags = O_CLOEXEC|O_NOCTTY;
   if (readonly)
     flags |= O_RDONLY;
   else
     flags |= O_RDWR;

-  h->fd = open (filename, flags);
+  h->fd = open (h->file, flags);
   if (h->fd == -1) {
-    nbdkit_error ("open: %s: %m", filename);
+    nbdkit_error ("open: %s: %m", h->file);
     free (h);
     return NULL;
   }

   if (fstat (h->fd, &statbuf) == -1) {
-    nbdkit_error ("fstat: %s: %m", filename);
+    nbdkit_error ("fstat: %s: %m", h->file);
     free (h);
     return NULL;
   }
@@ -194,7 +276,8 @@ file_open (int readonly)
   else if (S_ISREG (statbuf.st_mode))
     h->is_block_device = false;
   else {
-    nbdkit_error ("file is not regular or block device: %s", filename);
+    nbdkit_error ("file is not regular or block device: %s", h->file);
+    free (h->file);
     close (h->fd);
     free (h);
     return NULL;
@@ -204,7 +287,7 @@ file_open (int readonly)
 #ifdef BLKSSZGET
   if (h->is_block_device) {
     if (ioctl (h->fd, BLKSSZGET, &h->sector_size))
-      nbdkit_debug ("cannot get sector size: %s: %m", filename);
+      nbdkit_debug ("cannot get sector size: %s: %m", h->file);
   }
 #endif

@@ -232,6 +315,7 @@ file_close (void *handle)
 {
   struct handle *h = handle;

+  free (h->file);
   close (h->fd);
   free (h);
 }
@@ -239,7 +323,7 @@ file_close (void *handle)
 #define THREAD_MODEL NBDKIT_THREAD_MODEL_PARALLEL

 /* For block devices, stat->st_size is not the true size.  The caller
- * grabs the lseek_lock.
+ * grabs the lock.
  */
 static int64_t
 block_device_size (int fd)
@@ -262,7 +346,7 @@ file_get_size (void *handle)
   struct handle *h = handle;

   if (h->is_block_device) {
-    ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lseek_lock);
+    ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lock);
     return block_device_size (h->fd);
   } else {
     /* Regular file. */
@@ -554,7 +638,7 @@ file_can_extents (void *handle)
   /* A simple test to see whether SEEK_HOLE etc is likely to work on
    * the current filesystem.
    */
-  ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lseek_lock);
+  ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lock);
   r = lseek (h->fd, 0, SEEK_HOLE);
   if (r == -1) {
     nbdkit_debug ("extents disabled: lseek: SEEK_HOLE: %m");
@@ -628,7 +712,7 @@ static int
 file_extents (void *handle, uint32_t count, uint64_t offset,
               uint32_t flags, struct nbdkit_extents *extents)
 {
-  ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lseek_lock);
+  ACQUIRE_LOCK_FOR_CURRENT_SCOPE (&lock);
   return do_extents (handle, count, offset, flags, extents);
 }
 #endif /* SEEK_HOLE */
@@ -662,6 +746,7 @@ static struct nbdkit_plugin plugin = {
   .config_help       = file_config_help,
   .magic_config_key  = "file",
   .dump_plugin       = file_dump_plugin,
+  .list_exports      = file_list_exports,
   .open              = file_open,
   .close             = file_close,
   .get_size          = file_get_size,
diff --git a/tests/test-file-dir.sh b/tests/test-file-dir.sh
new file mode 100755
index 00000000..efe3b6cd
--- /dev/null
+++ b/tests/test-file-dir.sh
@@ -0,0 +1,143 @@
+#!/usr/bin/env bash
+# nbdkit
+# Copyright (C) 2018-2020 Red Hat Inc.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# * Neither the name of Red Hat nor the names of its contributors may be
+# used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY RED HAT AND CONTRIBUTORS ''AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL RED HAT OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
+# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+
+# Test the use of the directory mode of the file plugin.
+
+source ./functions.sh
+set -e
+set -x
+
+# Hack: This rejects libnbd 1.3.9, but we really need libnbd >= 1.3.11,
+# which does not have its own decent witness...
+requires nbdsh -c 'print (h.get_list_export_description)'
+
+requires nbdinfo --version
+requires jq --version
+
+files="file-dir file-dir.out file-dir.witness"
+rm -rf $files
+cleanup_fn rm -rf $files
+fail=0
+
+# do_nbdkit_list [--no-sort] EXPOUT
+# Check that the advertised list of exports matches EXPOUT
+do_nbdkit_list ()
+{
+    sort=' | sort'
+    if [ "$1" = --no-sort ]; then
+	sort=
+	shift
+    fi
+    nbdkit -U - -v file directory=file-dir \
+        --run 'nbdinfo --list --json "$uri"' >file-dir.out
+    cat file-dir.out
+    diff -u <(jq -c '[.exports[]."export-name"]'"$sort" file-dir.out) \
+        <(printf %s\\n "$1") || fail=1
+}
+
+# do_nbdkit_fail NAME
+# Check that attempting to connect to export NAME fails
+do_nbdkit_fail ()
+{
+    # The --run script occurs only if nbdkit gets past .config_complete;
+    # testing for witness proves that our failure was during .open and
+    # not at some earlier point
+    rm -f file-dir.witness
+    nbdkit -U - -v -e "$1" file directory=file-dir \
+        --run 'touch file-dir.witness; nbdsh -u "$uri" -c "quit()"' && fail=1
+    test -f file-dir.witness || fail=1
+}
+
+# do_nbdkit_pass NAME DATA
+# Check that export NAME serves DATA as its first byte
+do_nbdkit_pass ()
+{
+    out=$(nbdkit -U - -v -e "$1" file directory=file-dir \
+	--run 'nbdsh -u "$uri" -c "print (h.pread (1, 0).decode (\"utf-8\"))"')
+    test "$out" = "$2" || fail=1
+}
+
+# Not possible to serve a missing directory
+nbdkit -vf file directory=nosuchdir && fail=1
+
+# Serving an empty directory
+mkdir file-dir
+do_nbdkit_list '[]'
+do_nbdkit_fail ''
+do_nbdkit_fail 'a'
+do_nbdkit_fail '..'
+do_nbdkit_fail '/'
+
+# Serving a directory with one file
+echo 1 > file-dir/a
+do_nbdkit_list '["a"]'
+do_nbdkit_pass '' 1
+do_nbdkit_pass a 1
+do_nbdkit_fail b
+
+# Serving a directory with multiple files.
+# Use 'find' to match readdir's raw order (a is not always first!)
+echo 2 > file-dir/b
+raw=$(find file-dir -type f | xargs echo)
+exp=$(echo $raw | sed 's,file-dir/\(.\),"\1",g; s/ /,/')
+do_nbdkit_list --no-sort "[$exp]"
+do_nbdkit_list '["a","b"]'
+case $raw in
+    file-dir/a*) byte=1 ;;
+    file-dir/b*) byte=2 ;;
+    *) fail=1 ;;
+esac
+do_nbdkit_pass '' $byte
+do_nbdkit_pass 'a' 1
+do_nbdkit_pass 'b' 2
+do_nbdkit_fail 'c'
+
+# Serving a directory with non-regular files
+ln -s b file-dir/c
+mkfifo file-dir/d
+mkdir file-dir/e
+ln -s /dev/null file-dir/f
+ln -s . file-dir/g
+ln -s dangling file-dir/h
+do_nbdkit_list '["a","b","c"]'
+do_nbdkit_pass 'a' 1
+do_nbdkit_pass 'b' 2
+do_nbdkit_pass 'c' 2
+do_nbdkit_fail 'd'
+do_nbdkit_fail 'e'
+do_nbdkit_fail 'f'
+do_nbdkit_fail 'g'
+do_nbdkit_fail 'h'
+do_nbdkit_fail './a'
+do_nbdkit_fail '../file-dir/a'
+
+exit $fail
-- 
2.28.0




More information about the Libguestfs mailing list