[Libguestfs] [nbdkit PATCH v2 5/5] sh, eval: Add .list_exports support

Eric Blake eblake at redhat.com
Thu Aug 6 02:25:22 UTC 2020


Exposing .list_exports through to shell scripts makes testing export
listing a lot more feasible.  The design chosen here is amenable to
'ls -1' or 'find' output provided there are no newlines in the files
being listed, while also being flexible enough to support a future
format addition if we find ourselves needing a way to express escape
sequences or parsing machine-readable code such as JSON.

Signed-off-by: Eric Blake <eblake at redhat.com>
---
 plugins/eval/nbdkit-eval-plugin.pod |   2 +
 plugins/sh/nbdkit-sh-plugin.pod     |  52 ++++++++++++++
 tests/Makefile.am                   |   2 +
 plugins/sh/methods.h                |   4 +-
 plugins/eval/eval.c                 |   2 +
 plugins/sh/methods.c                | 106 +++++++++++++++++++++++++++
 plugins/sh/sh.c                     |   1 +
 plugins/sh/example.sh               |   8 +++
 tests/test-eval-exports.sh          | 108 ++++++++++++++++++++++++++++
 9 files changed, 284 insertions(+), 1 deletion(-)
 create mode 100755 tests/test-eval-exports.sh

diff --git a/plugins/eval/nbdkit-eval-plugin.pod b/plugins/eval/nbdkit-eval-plugin.pod
index 7e25a01f..7126c6de 100644
--- a/plugins/eval/nbdkit-eval-plugin.pod
+++ b/plugins/eval/nbdkit-eval-plugin.pod
@@ -108,6 +108,8 @@ features):

 =item B<is_rotational=>SCRIPT

+=item B<list_exports=>SCRIPT
+
 =item B<open=>SCRIPT

 =item B<pread=>SCRIPT
diff --git a/plugins/sh/nbdkit-sh-plugin.pod b/plugins/sh/nbdkit-sh-plugin.pod
index 771c6bc0..678116f2 100644
--- a/plugins/sh/nbdkit-sh-plugin.pod
+++ b/plugins/sh/nbdkit-sh-plugin.pod
@@ -266,6 +266,58 @@ with status C<1>; unrecognized output is ignored.

  /path/to/script preconnect <readonly> <exportname>

+=item C<list_exports>
+
+ /path/to/script list_exports <readonly> <default_only>
+
+The C<readonly> parameter will be C<true> or C<false>.  The
+C<default_only> parameter will be C<true> if the caller is only
+interested in the canonical name of the default export, or C<false> to
+get a full list of export names; the script may safely ignore this
+parameter and always provide a full list if desired.
+
+The first line of output informs nbdkit how to parse the rest of the
+output, the remaining lines then supply the inputs of the C
+C<nbdkit_add_export> function (see L<nbdkit-plugin(3)>), as follows:
+
+=over 4
+
+=item NAMES
+
+The remaining output provides one export name per line, and no export
+will be given a description.  For convenience, this form is also
+assumed if the first output line does not match one of the recognized
+parse modes.
+
+=item INTERLEAVED
+
+The remaining output provides pairs of lines, the first line being an
+export name, and the second the corresponding description.
+
+=item NAMES+DESCRIPTIONS
+
+The number of remaining lines is counted, with the first half being
+used as export names, and the second half providing descriptions to
+pair with names from the first half.
+
+An example of using this form to list files in the current directory,
+followed by their L<ls(1)> long description, would be:
+
+ echo NAMES+DESCRIPTIONS
+ ls
+ ls -l
+
+=back
+
+Note that other output modes might be introduced in the future; in
+particular, none of the existing modes allow a literal newline in an
+export name or description, although this could be possible under a
+new mode supporting escape sequences.
+
+This method is I<not> required; if it is absent, the list of exports
+advertised by nbdkit will be the single export with the empty string
+as a name and no description.
+
 =item C<open>

  /path/to/script open <readonly> <exportname>
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 79be5639..186749e0 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -616,10 +616,12 @@ test_data_LDADD = libtest.la $(LIBGUESTFS_LIBS)
 TESTS += \
 	test-eval.sh \
 	test-eval-file.sh \
+	test-eval-exports.sh \
 	$(NULL)
 EXTRA_DIST += \
 	test-eval.sh \
 	test-eval-file.sh \
+	test-eval-exports.sh \
 	$(NULL)

 # file plugin test.
diff --git a/plugins/sh/methods.h b/plugins/sh/methods.h
index 08a5ed17..69017fa4 100644
--- a/plugins/sh/methods.h
+++ b/plugins/sh/methods.h
@@ -1,5 +1,5 @@
 /* nbdkit
- * Copyright (C) 2018 Red Hat Inc.
+ * 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
@@ -44,6 +44,8 @@ extern int sh_thread_model (void);
 extern int sh_get_ready (void);
 extern int sh_after_fork (void);
 extern int sh_preconnect (int readonly);
+extern int sh_list_exports (int readonly, int default_only,
+                            struct nbdkit_exports *exports);
 extern void *sh_open (int readonly);
 extern void sh_close (void *handle);
 extern int64_t sh_get_size (void *handle);
diff --git a/plugins/eval/eval.c b/plugins/eval/eval.c
index 54c5029e..2bd5e79f 100644
--- a/plugins/eval/eval.c
+++ b/plugins/eval/eval.c
@@ -74,6 +74,7 @@ static const char *known_methods[] = {
   "get_ready",
   "get_size",
   "is_rotational",
+  "list_exports",
   "missing",
   "open",
   "pread",
@@ -393,6 +394,7 @@ static struct nbdkit_plugin plugin = {
   .after_fork        = sh_after_fork,

   .preconnect        = sh_preconnect,
+  .list_exports      = sh_list_exports,
   .open              = sh_open,
   .close             = sh_close,

diff --git a/plugins/sh/methods.c b/plugins/sh/methods.c
index 8257103e..9f247524 100644
--- a/plugins/sh/methods.c
+++ b/plugins/sh/methods.c
@@ -225,6 +225,112 @@ struct sh_handle {
   int can_zero;
 };

+/* If @s begins with @prefix, return the next offset, else NULL */
+static const char *
+skip_prefix (const char *s, const char *prefix)
+{
+  size_t len = strlen (prefix);
+  if (strncmp (s, prefix, len) == 0)
+    return s + len;
+  return NULL;
+}
+
+static int
+parse_exports (const char *script,
+               const char *s, size_t slen, struct nbdkit_exports *exports)
+{
+  const char *n, *d, *p, *q;
+
+  /* The first line determines how to parse the rest of s */
+  if ((p = skip_prefix (s, "INTERLEAVED\n")) != NULL) {
+    n = p;
+    while ((d = strchr (n, '\n')) != NULL) {
+      p = strchr (d + 1, '\n') ?: d + 1;
+      CLEANUP_FREE char *name = strndup (n, d - n);
+      CLEANUP_FREE char *desc = strndup (d + 1, p - d - 1);
+      if (!name || !desc) {
+        nbdkit_error ("%s: strndup: %m", script);
+        return -1;
+      }
+      if (nbdkit_add_export (exports, name, desc) == -1)
+        return -1;
+      n = p + 1;
+    }
+  }
+  else if ((p = skip_prefix (s, "NAMES+DESCRIPTIONS\n")) != NULL) {
+    n = d = p;
+    /* Searching from both ends, using memrchr, would be less work, but
+     * memrchr is not widely portable. Multiple passes isn't too bad.
+     */
+    while (p && (p = strchr (p, '\n')) != NULL) {
+      p = strchr (p + 1, '\n');
+      if (p)
+        p++;
+      d = strchr (d, '\n') + 1;
+    }
+    s = d;
+    while (n < s) {
+      p = strchr (n, '\n');
+      q = strchr (d, '\n') ?: d;
+      CLEANUP_FREE char *name = strndup (n, p - n);
+      CLEANUP_FREE char *desc = strndup (d, q - d);
+      if (!name || !desc) {
+        nbdkit_error ("%s: strndup: %m", script);
+        return -1;
+      }
+      if (nbdkit_add_export (exports, name, desc) == -1)
+        return -1;
+      n = p + 1;
+      d = q + 1;
+    }
+  }
+  else {
+    n = skip_prefix (s, "NAMES\n") ?: s;
+    while ((p = strchr (n, '\n')) != NULL) {
+      CLEANUP_FREE char *name = strndup (n, p - n);
+      if (!name) {
+        nbdkit_error ("%s: strndup: %m", script);
+        return -1;
+      }
+      if (nbdkit_add_export (exports, name, NULL) == -1)
+        return -1;
+      n = p + 1;
+    }
+  }
+  return 0;
+}
+
+int
+sh_list_exports (int readonly, int default_only,
+                 struct nbdkit_exports *exports)
+{
+  const char *method = "list_exports";
+  const char *script = get_script (method);
+  const char *args[] = { script, method, readonly ? "true" : "false",
+                         default_only ? "true" : "false", NULL };
+  CLEANUP_FREE char *s = NULL;
+  size_t slen;
+
+  switch (call_read (&s, &slen, args)) {
+  case OK:
+    return parse_exports (script, s, slen, exports);
+
+  case MISSING:
+    return nbdkit_add_export (exports, "", NULL);
+
+  case ERROR:
+    return -1;
+
+  case RET_FALSE:
+    nbdkit_error ("%s: %s method returned unexpected code (3/false)",
+                  script, method);
+    errno = EIO;
+    return -1;
+
+  default: abort ();
+  }
+}
+
 void *
 sh_open (int readonly)
 {
diff --git a/plugins/sh/sh.c b/plugins/sh/sh.c
index 9e484823..374888a4 100644
--- a/plugins/sh/sh.c
+++ b/plugins/sh/sh.c
@@ -300,6 +300,7 @@ static struct nbdkit_plugin plugin = {
   .after_fork        = sh_after_fork,

   .preconnect        = sh_preconnect,
+  .list_exports      = sh_list_exports,
   .open              = sh_open,
   .close             = sh_close,

diff --git a/plugins/sh/example.sh b/plugins/sh/example.sh
index 99e4e890..4f547db0 100755
--- a/plugins/sh/example.sh
+++ b/plugins/sh/example.sh
@@ -85,6 +85,14 @@ case "$1" in
         echo parallel
         ;;

+    list_exports)
+        # The following lists the names of all files in the current
+        # directory that do not contain whitespace, backslash, or single
+        # quotes.  No description accompanies the export names.
+        # The first file listed is used when a client requests export ''.
+        find . -type f \! -name "*['\\\\[:space:]]*"
+        ;;
+
     open)
         # Open a new client connection.

diff --git a/tests/test-eval-exports.sh b/tests/test-eval-exports.sh
new file mode 100755
index 00000000..543774b6
--- /dev/null
+++ b/tests/test-eval-exports.sh
@@ -0,0 +1,108 @@
+#!/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.
+
+# This is an example from the nbdkit-eval-plugin(1) manual page.
+# Check here that it doesn't regress.
+
+source ./functions.sh
+set -e
+set -x
+
+requires nbdsh -c 'print (h.get_list_export_description)'
+requires nbdinfo --help
+requires jq --version
+
+files="eval-exports.list eval-exports.out"
+rm -f $files
+cleanup_fn rm -f $files
+
+# do_nbdkit [skip_list] EXPOUT
+do_nbdkit ()
+{
+    # Hack: since we never pass args that would go through .config, we can
+    # define a dummy .config to avoid defining .list_export
+    hack=
+    if test $1 = skip_list; then
+        hack=config=
+        shift
+    else
+        cat eval-exports.list
+    fi
+    nbdkit -U - -v eval ${hack}list_exports='cat eval-exports.list' \
+      get_size='echo 0' --run 'nbdinfo --list --json "$uri"' >eval-exports.out
+    cat eval-exports.out
+    diff -u <(jq -c '[.exports[] | [."export-name", .description]]' \
+              eval-exports.out) <(printf %s\\n "$1")
+}
+
+# Control case: no .list_exports, which defaults to advertising ""
+rm -f eval-exports.list
+do_nbdkit skip_list '[["",null]]'
+
+# Various spellings of empty lists, producing 0 exports
+for fmt in '' 'NAMES\n' 'INTERLEAVED\n' 'NAMES+DESCRIPTIONS\n'; do
+    printf "$fmt" >eval-exports.list
+    do_nbdkit '[]'
+done
+
+# Various spellings of explicit list for the default export, no description
+for fmt in '\n' 'NAMES\n\n' 'INTERLEAVED\n\n' 'INTERLEAVED\n\n\n' \
+           'NAMES+DESCRIPTIONS\n\n' 'NAMES+DESCRIPTIONS\n\n\n'; do
+    printf "$fmt" >eval-exports.list
+    do_nbdkit '[["",null]]'
+done
+
+# A non-default name
+for fmt in 'name\n' 'NAMES\nname\n'; do
+    printf "$fmt" >eval-exports.list
+    do_nbdkit '[["name",null]]'
+done
+
+# One export with a description
+for fmt in 'INTERLEAVED\nname\ndesc\n' 'NAMES+DESCRIPTIONS\nname\ndesc\n'; do
+    printf "$fmt" >eval-exports.list
+    do_nbdkit '[["name","desc"]]'
+done
+
+# Multiple exports, with correct number of lines
+for fmt in 'INTERLEAVED\nname 1\ndesc 1\nname 2\ndesc 2\n' \
+           'NAMES+DESCRIPTIONS\nname 1\nname 2\ndesc 1\ndesc 2\n'; do
+    printf "$fmt" >eval-exports.list
+    do_nbdkit '[["name 1","desc 1"],["name 2","desc 2"]]'
+done
+
+# Multiple exports, with final description line missing
+for fmt in 'INTERLEAVED\nname 1\ndesc 1\nname 2\n' \
+           'NAMES+DESCRIPTIONS\nname 1\nname 2\ndesc 1\n'; do
+    printf "$fmt" >eval-exports.list
+    do_nbdkit '[["name 1","desc 1"],["name 2",null]]'
+done
-- 
2.28.0




More information about the Libguestfs mailing list