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 <[email protected]> --- 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 _______________________________________________ Libguestfs mailing list [email protected] https://www.redhat.com/mailman/listinfo/libguestfs
