Since v1: - patch 1: check size limits - patch 2: better handling of default export name canonicalization - patch 3: support filters as well as plugins - patch 4: new - patch 5: rewrite sh parser, fix testsuite to actually work and cover more cases (now that libnbd.git is fixed) Eric Blake (4): server: Add exports list functions server: Prepare to use export list from plugin log: Add .list_exports support sh, eval: Add .list_exports support Richard W.M. Jones (1): server: Implement list_exports. docs/nbdkit-filter.pod | 92 +++++++++++++++-- docs/nbdkit-plugin.pod | 64 +++++++++++- docs/nbdkit-protocol.pod | 4 +- filters/log/nbdkit-log-filter.pod | 18 ++-- plugins/eval/nbdkit-eval-plugin.pod | 2 + plugins/sh/nbdkit-sh-plugin.pod | 52 ++++++++++ include/nbdkit-common.h | 4 + include/nbdkit-filter.h | 18 ++++ include/nbdkit-plugin.h | 3 + server/Makefile.am | 2 + tests/Makefile.am | 2 + server/internal.h | 10 ++ common/utils/cleanup.h | 3 + server/backend.c | 43 ++++++++ server/exports.c | 149 +++++++++++++++++++++++++++ server/filters.c | 13 +++ server/nbdkit.syms | 5 + server/plugins.c | 15 +++ server/protocol-handshake-newstyle.c | 80 ++++++++------ common/utils/cleanup-nbdkit.c | 6 ++ 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 ++ filters/log/log.c | 50 ++++++++- tests/test-eval-exports.sh | 108 +++++++++++++++++++ tests/test-layers-filter.c | 10 ++ tests/test-layers-plugin.c | 9 ++ tests/test-layers.c | 15 +++ 30 files changed, 839 insertions(+), 59 deletions(-) create mode 100644 server/exports.c create mode 100755 tests/test-eval-exports.sh -- 2.28.0
Eric Blake
2020-Aug-06 02:25 UTC
[Libguestfs] [nbdkit PATCH v2 1/5] server: Add exports list functions
Add exports.c for manipulating an exports list, borrowing heavily from similar code in managing an extents list. The new functions are exposed through nbdkit-filter.h, because filters will eventually need a way to grab the export list from a plugin, but actually wiring that up will be in a later patch. For now, we enforce string length but not strict UTF-8 content. Signed-off-by: Eric Blake <eblake@redhat.com> --- include/nbdkit-common.h | 4 + include/nbdkit-filter.h | 12 +++ server/Makefile.am | 2 + common/utils/cleanup.h | 3 + server/exports.c | 149 ++++++++++++++++++++++++++++++++++ server/nbdkit.syms | 5 ++ common/utils/cleanup-nbdkit.c | 6 ++ 7 files changed, 181 insertions(+) create mode 100644 server/exports.c diff --git a/include/nbdkit-common.h b/include/nbdkit-common.h index 671cd4a4..d38b37d2 100644 --- a/include/nbdkit-common.h +++ b/include/nbdkit-common.h @@ -117,6 +117,10 @@ struct nbdkit_extents; extern int nbdkit_add_extent (struct nbdkit_extents *, uint64_t offset, uint64_t length, uint32_t type); +struct nbdkit_exports; +extern int nbdkit_add_export (struct nbdkit_exports *, + const char *name, const char *description); + /* A static non-NULL pointer which can be used when you don't need a * per-connection handle. */ diff --git a/include/nbdkit-filter.h b/include/nbdkit-filter.h index cec12db7..01ff3dce 100644 --- a/include/nbdkit-filter.h +++ b/include/nbdkit-filter.h @@ -123,6 +123,18 @@ extern int nbdkit_extents_aligned (struct nbdkit_next_ops *next_ops, uint32_t flags, uint32_t align, struct nbdkit_extents *extents, int *err); +/* Export functions. */ +struct nbdkit_export { + char *name; + char *description; +}; + +extern struct nbdkit_exports *nbdkit_exports_new (int default_only); +extern void nbdkit_exports_free (struct nbdkit_exports *); +extern size_t nbdkit_exports_count (const struct nbdkit_exports *); +extern const struct nbdkit_export nbdkit_get_export (const struct nbdkit_exports *, + size_t); + /* Filter struct. */ struct nbdkit_filter { /* Do not set these fields directly; use NBDKIT_REGISTER_FILTER. diff --git a/server/Makefile.am b/server/Makefile.am index 4c789934..58b22341 100644 --- a/server/Makefile.am +++ b/server/Makefile.am @@ -43,6 +43,7 @@ nbdkit_SOURCES = \ crypto.c \ debug.c \ debug-flags.c \ + exports.c \ extents.c \ filters.c \ internal.h \ @@ -139,6 +140,7 @@ check_PROGRAMS = test-public test_public_SOURCES = \ test-public.c \ public.c \ + exports.c \ extents.c \ $(NULL) test_public_CPPFLAGS = \ diff --git a/common/utils/cleanup.h b/common/utils/cleanup.h index bcb65f7b..6b59556b 100644 --- a/common/utils/cleanup.h +++ b/common/utils/cleanup.h @@ -76,5 +76,8 @@ extern void cleanup_rwlock_unlock (pthread_rwlock_t **ptr); struct nbdkit_extents; extern void cleanup_extents_free (struct nbdkit_extents **ptr); #define CLEANUP_EXTENTS_FREE __attribute__((cleanup (cleanup_extents_free))) +struct nbdkit_exports; +extern void cleanup_exports_free (struct nbdkit_exports **ptr); +#define CLEANUP_EXPORTS_FREE __attribute__((cleanup (cleanup_exports_free))) #endif /* NBDKIT_CLEANUP_H */ diff --git a/server/exports.c b/server/exports.c new file mode 100644 index 00000000..3f819622 --- /dev/null +++ b/server/exports.c @@ -0,0 +1,149 @@ +/* nbdkit + * Copyright (C) 2019-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. + */ + +#include <config.h> + +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> +#include <errno.h> +#include <assert.h> + +#include "vector.h" + +#include "internal.h" + +/* Cap nr_exports to avoid sending over-large replies to the client, + * and to avoid a plugin with large list consuming too much memory. + */ +#define MAX_EXPORTS 10000 + +/* Appendable list of exports. */ +DEFINE_VECTOR_TYPE(exports, struct nbdkit_export); + +struct nbdkit_exports { + exports exports; + + bool default_only; +}; + +struct nbdkit_exports * +nbdkit_exports_new (int default_only) +{ + struct nbdkit_exports *r; + + r = malloc (sizeof *r); + if (r == NULL) { + nbdkit_error ("nbdkit_exports_new: malloc: %m"); + return NULL; + } + r->exports = (exports) empty_vector; + r->default_only = default_only != 0; + return r; +} + +static void +nbdkit_export_clear (struct nbdkit_export exp) +{ + free (exp.name); + free (exp.description); +} + +void +nbdkit_exports_free (struct nbdkit_exports *exps) +{ + if (exps) { + exports_iter (&exps->exports, nbdkit_export_clear); + free (exps->exports.ptr); + free (exps); + } +} + +size_t +nbdkit_exports_count (const struct nbdkit_exports *exps) +{ + return exps->exports.size; +} + +const struct nbdkit_export +nbdkit_get_export (const struct nbdkit_exports *exps, size_t i) +{ + assert (i < exps->exports.size); + return exps->exports.ptr[i]; +} + +int +nbdkit_add_export (struct nbdkit_exports *exps, + const char *name, const char *description) +{ + struct nbdkit_export e = { NULL, NULL }; + + if (exps->default_only && exps->exports.size == 1) + return 0; + + if (exps->exports.size == MAX_EXPORTS) { + nbdkit_error ("nbdkit_add_export: too many exports"); + errno = EINVAL; + return -1; + } + if (strlen (name) > NBD_MAX_STRING || + (description && strlen (description) > NBD_MAX_STRING)) { + nbdkit_error ("nbdkit_add_export: string too long"); + errno = EINVAL; + return -1; + } + + e.name = strdup (name); + if (e.name == NULL) { + nbdkit_error ("nbdkit_add_export: strdup: %m"); + return -1; + } + if (description) { + e.description = strdup (description); + if (e.description == NULL) { + nbdkit_error ("nbdkit_add_export: strdup: %m"); + free (e.name); + errno = ENOMEM; + return -1; + } + } + + if (exports_append (&exps->exports, e) == -1) { + nbdkit_error ("nbdkit_add_export: realloc: %m"); + free (e.name); + free (e.description); + errno = ENOMEM; + return -1; + } + + return 0; +} diff --git a/server/nbdkit.syms b/server/nbdkit.syms index d62ad484..6cc6ed32 100644 --- a/server/nbdkit.syms +++ b/server/nbdkit.syms @@ -39,10 +39,15 @@ # The functions we want plugins and filters to call. global: nbdkit_absolute_path; + nbdkit_add_export; nbdkit_add_extent; nbdkit_debug; nbdkit_error; nbdkit_export_name; + nbdkit_exports_count; + nbdkit_exports_free; + nbdkit_exports_new; + nbdkit_get_export; nbdkit_extents_aligned; nbdkit_extents_count; nbdkit_extents_free; diff --git a/common/utils/cleanup-nbdkit.c b/common/utils/cleanup-nbdkit.c index aaaf14a0..e7553c73 100644 --- a/common/utils/cleanup-nbdkit.c +++ b/common/utils/cleanup-nbdkit.c @@ -43,3 +43,9 @@ cleanup_extents_free (struct nbdkit_extents **ptr) { nbdkit_extents_free (*ptr); } + +void +cleanup_exports_free (struct nbdkit_exports **ptr) +{ + nbdkit_exports_free (*ptr); +} -- 2.28.0
Eric Blake
2020-Aug-06 02:25 UTC
[Libguestfs] [nbdkit PATCH v2 2/5] server: Prepare to use export list from plugin
Wire up everything in the server to query the export list from the plugin. Actual patches to let plugins and filters return a non-default list will come later, so for now, the behavior is unchanged: NBD_OPT_LIST still lists just "", and a client requesting "" still lets the plugin see an export name of "". If the client calls NBD_OPT_LIST, we cache the default export name for later use in case the client then requests export "", to avoid having to re-call .list_exports a second time. But there is no caching if the client calls NBD_OPT_LIST more than once, and we still have to resolve the default name even if the client does not use NBD_OPT_LIST. Signed-off-by: Eric Blake <eblake@redhat.com> --- server/internal.h | 10 ++++ server/backend.c | 43 +++++++++++++++ server/filters.c | 9 ++++ server/plugins.c | 9 ++++ server/protocol-handshake-newstyle.c | 80 ++++++++++++++++------------ 5 files changed, 117 insertions(+), 34 deletions(-) diff --git a/server/internal.h b/server/internal.h index 1acbbb06..f84696ca 100644 --- a/server/internal.h +++ b/server/internal.h @@ -202,6 +202,8 @@ struct handle { unsigned char state; /* Bitmask of HANDLE_* values */ + char *default_exportname; + uint64_t exportsize; int can_write; int can_flush; @@ -220,6 +222,8 @@ reset_handle (struct handle *h) { h->handle = NULL; h->state = 0; + free (h->default_exportname); + h->default_exportname = NULL; h->exportsize = -1; h->can_write = -1; h->can_flush = -1; @@ -361,6 +365,8 @@ struct backend { void (*get_ready) (struct backend *); void (*after_fork) (struct backend *); int (*preconnect) (struct backend *, int readonly); + int (*list_exports) (struct backend *, int readonly, int default_only, + struct nbdkit_exports *exports); void *(*open) (struct backend *, int readonly, const char *exportname); int (*prepare) (struct backend *, void *handle, int readonly); int (*finalize) (struct backend *, void *handle); @@ -405,6 +411,10 @@ extern void backend_load (struct backend *b, const char *name, extern void backend_unload (struct backend *b, void (*unload) (void)) __attribute__((__nonnull__ (1))); +extern int backend_list_exports (struct backend *b, int readonly, + int default_only, + struct nbdkit_exports *exports) + __attribute__((__nonnull__ (1, 4))); /* exportname is only valid for this call and almost certainly will be * freed on return of this function, so backends must save the * exportname if they need to refer to it later. diff --git a/server/backend.c b/server/backend.c index d39fdeaf..18e44590 100644 --- a/server/backend.c +++ b/server/backend.c @@ -151,6 +151,35 @@ backend_unload (struct backend *b, void (*unload) (void)) free (b->name); } +int +backend_list_exports (struct backend *b, int readonly, int default_only, + struct nbdkit_exports *exports) +{ + GET_CONN; + struct handle *h = get_handle (conn, b->i); + int r; + + controlpath_debug ("%s: list_exports readonly=%d default_only=%d", + b->name, readonly, default_only); + + assert (h->handle == NULL); + assert ((h->state & HANDLE_OPEN) == 0); + if (default_only && h->default_exportname) + return nbdkit_add_export (exports, h->default_exportname, NULL); + + r = b->list_exports (b, readonly, default_only, exports); + if (r == -1) + controlpath_debug ("%s: list_exports failed", b->name); + else { + size_t count = nbdkit_exports_count (exports); + controlpath_debug ("%s: list_exports returned %zu names", b->name, count); + /* Best effort caching of default export name */ + if (!h->default_exportname && count) + h->default_exportname = strdup (nbdkit_get_export (exports, 0).name); + } + return r; +} + int backend_open (struct backend *b, int readonly, const char *exportname) { @@ -166,6 +195,20 @@ backend_open (struct backend *b, int readonly, const char *exportname) if (readonly) h->can_write = 0; + /* Best-effort determination of the canonical name for default export */ + if (!*exportname) { + if (!h->default_exportname) { + CLEANUP_EXPORTS_FREE struct nbdkit_exports *exps = NULL; + + exps = nbdkit_exports_new (true); + if (b->list_exports (b, readonly, true, exps) == 0 && + nbdkit_exports_count (exps)) + h->default_exportname = strdup (nbdkit_get_export (exps, 0).name); + } + if (h->default_exportname) + exportname = h->default_exportname; + } + /* Most filters will call next_open first, resulting in * inner-to-outer ordering. */ diff --git a/server/filters.c b/server/filters.c index 7d268096..e5b5b860 100644 --- a/server/filters.c +++ b/server/filters.c @@ -237,6 +237,14 @@ plugin_magic_config_key (struct backend *b) return b->next->magic_config_key (b->next); } +static int +filter_list_exports (struct backend *b, int readonly, int default_only, + struct nbdkit_exports *exports) +{ + /* XXX No filter override yet... */ + return backend_list_exports (b->next, readonly, default_only, exports); +} + static void * filter_open (struct backend *b, int readonly, const char *exportname) { @@ -540,6 +548,7 @@ static struct backend filter_functions = { .get_ready = filter_get_ready, .after_fork = filter_after_fork, .preconnect = filter_preconnect, + .list_exports = filter_list_exports, .open = filter_open, .prepare = filter_prepare, .finalize = filter_finalize, diff --git a/server/plugins.c b/server/plugins.c index 8449b1d9..8020046b 100644 --- a/server/plugins.c +++ b/server/plugins.c @@ -277,6 +277,14 @@ plugin_preconnect (struct backend *b, int readonly) return p->plugin.preconnect (readonly); } +static int +plugin_list_exports (struct backend *b, int readonly, int default_only, + struct nbdkit_exports *exports) +{ + /* XXX No plugin support yet, so for now just advertise "" */ + return nbdkit_add_export (exports, "", NULL); +} + static void * plugin_open (struct backend *b, int readonly, const char *exportname) { @@ -730,6 +738,7 @@ static struct backend plugin_functions = { .get_ready = plugin_get_ready, .after_fork = plugin_after_fork, .preconnect = plugin_preconnect, + .list_exports = plugin_list_exports, .open = plugin_open, .prepare = plugin_prepare, .finalize = plugin_finalize, diff --git a/server/protocol-handshake-newstyle.c b/server/protocol-handshake-newstyle.c index 4025a645..8f41c7a8 100644 --- a/server/protocol-handshake-newstyle.c +++ b/server/protocol-handshake-newstyle.c @@ -75,44 +75,59 @@ send_newstyle_option_reply (uint32_t option, uint32_t reply) return 0; } -/* Reply to NBD_OPT_LIST with a single empty export name. - * TODO: Ask the plugin for the list of exports. +/* Reply to NBD_OPT_LIST with the plugin's list of export names. */ static int -send_newstyle_option_reply_exportname (uint32_t option, uint32_t reply) +send_newstyle_option_reply_exportnames (uint32_t option) { GET_CONN; struct nbd_fixed_new_option_reply fixed_new_option_reply; - const size_t name_len = 0; /* length of export name */ - uint32_t len; + size_t i; + CLEANUP_EXPORTS_FREE struct nbdkit_exports *exps = NULL; - fixed_new_option_reply.magic = htobe64 (NBD_REP_MAGIC); - fixed_new_option_reply.option = htobe32 (option); - fixed_new_option_reply.reply = htobe32 (reply); - fixed_new_option_reply.replylen = htobe32 (name_len + sizeof (len)); + exps = nbdkit_exports_new (false); + if (exps == NULL) + return send_newstyle_option_reply (option, NBD_REP_ERR_TOO_BIG); + if (backend_list_exports (top, read_only, false, exps) == -1) + return send_newstyle_option_reply (option, NBD_REP_ERR_PLATFORM); - if (conn->send (&fixed_new_option_reply, - sizeof fixed_new_option_reply, SEND_MORE) == -1) { - nbdkit_error ("write: %s: %m", name_of_nbd_opt (option)); - return -1; - } + for (i = 0; i < nbdkit_exports_count (exps); i++) { + const struct nbdkit_export export = nbdkit_get_export (exps, i); + size_t name_len = strlen (export.name); + size_t desc_len = export.description ? strlen (export.description) : 0; + uint32_t len; - len = htobe32 (name_len); - if (conn->send (&len, sizeof len, SEND_MORE) == -1) { - nbdkit_error ("write: %s: %s: %m", - name_of_nbd_opt (option), "sending length"); - return -1; - } -#if 0 - /* If we were sending a non-"" export name, this is what we'd use. */ - if (conn->send (exportname, name_len, 0) == -1) { - nbdkit_error ("write: %s: %s: %m", - name_of_nbd_opt (option), "sending export name"); - return -1; + fixed_new_option_reply.magic = htobe64 (NBD_REP_MAGIC); + fixed_new_option_reply.option = htobe32 (option); + fixed_new_option_reply.reply = htobe32 (NBD_REP_SERVER); + fixed_new_option_reply.replylen = htobe32 (name_len + sizeof (len) + + desc_len); + + if (conn->send (&fixed_new_option_reply, + sizeof fixed_new_option_reply, SEND_MORE) == -1) { + nbdkit_error ("write: %s: %m", name_of_nbd_opt (option)); + return -1; + } + + len = htobe32 (name_len); + if (conn->send (&len, sizeof len, SEND_MORE) == -1) { + nbdkit_error ("write: %s: %s: %m", + name_of_nbd_opt (option), "sending length"); + return -1; + } + if (conn->send (export.name, name_len, SEND_MORE) == -1) { + nbdkit_error ("write: %s: %s: %m", + name_of_nbd_opt (option), "sending export name"); + return -1; + } + if (conn->send (export.description, desc_len, 0) == -1) { + nbdkit_error ("write: %s: %s: %m", + name_of_nbd_opt (option), "sending export description"); + return -1; + } } -#endif - return 0; + return send_newstyle_option_reply (option, NBD_REP_ACK); } static int @@ -384,13 +399,10 @@ negotiate_handshake_newstyle_options (void) continue; } - /* Send back the exportname. */ - debug ("newstyle negotiation: %s: advertising export \"\"", + /* Send back the exportname list. */ + debug ("newstyle negotiation: %s: advertising exports", name_of_nbd_opt (option)); - if (send_newstyle_option_reply_exportname (option, NBD_REP_SERVER) == -1) - return -1; - - if (send_newstyle_option_reply (option, NBD_REP_ACK) == -1) + if (send_newstyle_option_reply_exportnames (option) == -1) return -1; break; -- 2.28.0
Eric Blake
2020-Aug-06 02:25 UTC
[Libguestfs] [nbdkit PATCH v2 3/5] server: Implement list_exports.
From: "Richard W.M. Jones" <rjones@redhat.com> See also: https://www.redhat.com/archives/libguestfs/2020-July/msg00090.html This adds the hooks for filters and plugins; later patches will put them to use. For now, a filter can only modify export names when a client requests; a later patch will probably add a way for filters to open an arbitrary connection on the plugin independent of any connection from the client, which will give filters much more flexibility on how they manage export names. Signed-off-by: Eric Blake <eblake@redhat.com> --- docs/nbdkit-filter.pod | 92 ++++++++++++++++++++++++++++++++++---- docs/nbdkit-plugin.pod | 64 ++++++++++++++++++++++++-- docs/nbdkit-protocol.pod | 4 +- include/nbdkit-filter.h | 6 +++ include/nbdkit-plugin.h | 3 ++ server/filters.c | 6 ++- server/plugins.c | 10 ++++- tests/test-layers-filter.c | 10 +++++ tests/test-layers-plugin.c | 9 ++++ tests/test-layers.c | 15 +++++++ 10 files changed, 202 insertions(+), 17 deletions(-) diff --git a/docs/nbdkit-filter.pod b/docs/nbdkit-filter.pod index bb4f0269..12343dbf 100644 --- a/docs/nbdkit-filter.pod +++ b/docs/nbdkit-filter.pod @@ -129,15 +129,17 @@ which is required. F<nbdkit-filter.h> defines some function types (C<nbdkit_next_config>, C<nbdkit_next_config_complete>, C<nbdkit_next_get_ready>, C<nbdkit_next_after_fork>, C<nbdkit_next_preconnect>, -C<nbdkit_next_open>) and a structure called C<struct nbdkit_next_ops>. -These abstract the next plugin or filter in the chain. There is also -an opaque pointer C<nxdata> which must be passed along when calling -these functions. The value of C<nxdata> passed to C<.open> has a -stable lifetime that lasts to the corresponding C<.close>, with all -intermediate functions (such as C<.pread>) receiving the same value -for convenience; the only exceptions where C<nxdata> is not reused are -C<.config>, C<.config_complete>, C<.get_ready>, C<.after_fork> and -C<.preconnect>, which are called outside the lifetime of a connection. +C<nbdkit_next_list_exports>, C<nbdkit_next_open>) and a structure +called C<struct nbdkit_next_ops>. These abstract the next plugin or +filter in the chain. There is also an opaque pointer C<nxdata> which +must be passed along when calling these functions. The value of +C<nxdata> passed to C<.open> has a stable lifetime that lasts to the +corresponding C<.close>, with all intermediate functions (such as +C<.pread>) receiving the same value for convenience; the only +exceptions where C<nxdata> is not reused are C<.config>, +C<.config_complete>, C<.get_ready>, C<.after_fork>, C<.preconnect> and +C<.list_exports>, which are called outside the lifetime of a +connection. =head2 Next config, open and close @@ -326,6 +328,78 @@ filter access to the server. If there is an error, C<.preconnect> should call C<nbdkit_error> with an error message and return C<-1>. +=head2 C<.list_exports> + + int (*list_exports) (nbdkit_next_list_exports *next, void *nxdata, + int readonly, int default_only, + struct nbdkit_exports *exports); + +This intercepts the plugin C<.list_exports> method and can be used to +filter which exports are advertised. + +It is possible for filters to transform the exports list received back +from the layer below. Without error checking it would look like this: + + myfilter_list_exports (...) + { + size_t i; + struct nbdkit_exports *exports2; + struct nbdkit_export e; + char *name, *desc; + + exports2 = nbdkit_exports_new (default_only); + next_list_exports (nxdata, readonly, default_only, exports); + for (i = 0; i < nbdkit_exports_count (exports2); ++i) { + e = nbdkit_get_export (exports2, i); + name = adjust (e.name); + desc = adjust (e.desc); + nbdkit_add_export (exports, name, desc); + free (name); + free (desc); + } + nbdkit_exports_free (exports2); + } + +If there is an error, C<.list_exports> should call C<nbdkit_error> with +an error message and return C<-1>. + +=head3 Allocating and freeing nbdkit_exports list + +Two functions are provided to filters only for allocating and freeing +the list: + + struct nbdkit_exports *nbdkit_exports_new (int default_only); + +Allocates and returns a new, empty exports list. The C<default_only> +parameter should match whether the list is intended to grab the +canonical name of the default export, or all exports. + +On error this function can return C<NULL>. In this case it calls +C<nbdkit_error> as required. C<errno> will be set to a suitable +value. + + void nbdkit_exports_free (struct nbdkit_exports *); + +Frees an existing exports list. + +=head3 Iterating over nbdkit_exports list + +Two functions are provided to filters only to iterate over the exports +in order: + + size_t nbdkit_exports_count (const struct nbdkit_exports *); + +Returns the number of exports in the list. + + struct nbdkit_export { + char *name; + char *description; + }; + const struct nbdkit_export nbdkit_get_export (const struct nbdkit_exports *, + size_t i); + +Returns a copy of the C<i>'th export. + =head2 C<.open> void * (*open) (nbdkit_next_open *next, void *nxdata, diff --git a/docs/nbdkit-plugin.pod b/docs/nbdkit-plugin.pod index f8e9962a..9341f282 100644 --- a/docs/nbdkit-plugin.pod +++ b/docs/nbdkit-plugin.pod @@ -152,6 +152,9 @@ the plugin: │ preconnect │ │ └──────┬─────┘ │ ┌──────┴─────┐ │ + │list_exports│ │ + └──────┬─────┘ │ + ┌──────┴─────┐ │ │ open │ │ └──────┬─────┘ │ ┌──────┴─────┐ NBD option │ @@ -160,10 +163,10 @@ the plugin: ┌──────┴─────┐ ┌──────┴─────┐ client #2 │ get_size │ │ preconnect │ └──────┬─────┘ └──────┬─────┘ - ┌──────┴─────┐ data ┌──────┴─────┐ - │ pread │ serving │ open │ - └──────┬─────┘↺ └──────┬─────┘ - ┌──────┴─────┐ ... + ┌──────┴─────┐ data + │ pread │ serving + └──────┬─────┘↺ ... + ┌──────┴─────┐ │ pwrite │ └──────┬─────┘↺ ┌──────┴─────┐ ┌──────┴─────┐ │ close │ @@ -236,6 +239,12 @@ L<getpid(2)>. Called when a TCP connection has been made to the server. This happens early, before NBD or TLS negotiation. +=item C<.list_exports> + +Early in option negotiation the client may try to list the exports +served by the plugin, and plugins can optionally implement this +callback to answer the client. See L</EXPORT NAME> below. + =item C<.open> A new client has connected and finished the NBD handshake. TLS @@ -652,6 +661,53 @@ Returning C<0> will allow the connection to continue. If there is an error or you want to deny the connection, call C<nbdkit_error> with an error message and return C<-1>. +=head2 C<.list_exports> + + int list_exports (int readonly, int default_only, + struct nbdkit_exports *exports); + +This optional callback is called if the client tries to list the +exports served by the plugin (using C<NBD_OPT_LIST>). If the plugin +does not supply this callback then a single export called C<""> is +returned. The NBD protocol defines C<""> as the default export, so +this is suitable for plugins which ignore the export name and always +serve the same content. See also L</EXPORT NAME> below. + +The C<readonly> flag informs the plugin that the server was started +with the I<-r> flag on the command line, which is the same value +passed to C<.preconnect> and C<.open>. However, the NBD protocol does +not yet have a way to let the client advertise an intent to be +read-only even when the server allows writes, so this parameter may +not be as useful as it appears. + +If the C<default_only> flag is set then the client is querying for the +name of the default export, and the plugin may optimize by adding only +a single export to the returned list (the default export name, usually +C<"">). The plugin can ignore this flag and return all exports if it +wants. + +The C<exports> parameter is an opaque object for collecting the list +of exports. Call C<nbdkit_add_export> to add a single export to the +list. If the plugin has a concept of a default export (usually but +not always called C<"">) then it should return that first in the list. + + int nbdkit_add_export (struct nbdkit_export *exports, + const char *name, const char *description); + +The C<name> must be a non-NULL, UTF-8 string between 0 and 4096 bytes +in length. Export names must be unique. C<description> is an +optional description of the export which some clients can display but +which is otherwise unused (if you don't want a description, you can +pass this parameter as C<NULL>). The string(s) are copied into the +exports list so you may free them immediately after calling this +function. C<nbdkit_add_export> returns C<0> on success or C<-1> on +failure; on failure C<nbdkit_error> has already been called, with +C<errno> set to a suitable value. + +Returning C<0> will send the list of exports back to the client. If +there is an error, C<.list_exports> should call C<nbdkit_error> with +an error message and return C<-1>. + =head2 C<.open> void *open (int readonly); diff --git a/docs/nbdkit-protocol.pod b/docs/nbdkit-protocol.pod index 8fe8c67e..b923d367 100644 --- a/docs/nbdkit-protocol.pod +++ b/docs/nbdkit-protocol.pod @@ -104,7 +104,9 @@ read the client export name added in nbdkit E<ge> 1.15.2. Versions of nbdkit before 1.16 could advertise a single export name to clients, via a now deprecated side effect of the I<-e> option. In nbdkit 1.15.2, plugins could read the client requested export name using -C<nbdkit_export_name()> and serve different content. +C<nbdkit_export_name()> and serve different content. In nbdkit +1.21.22, plugins could implement C<.list_exports> to answer +C<NBD_OPT_LIST> queries. =item C<NBD_FLAG_NO_ZEROES> diff --git a/include/nbdkit-filter.h b/include/nbdkit-filter.h index 01ff3dce..08c6abc4 100644 --- a/include/nbdkit-filter.h +++ b/include/nbdkit-filter.h @@ -65,6 +65,9 @@ typedef int nbdkit_next_config_complete (nbdkit_backend *nxdata); typedef int nbdkit_next_get_ready (nbdkit_backend *nxdata); typedef int nbdkit_next_after_fork (nbdkit_backend *nxdata); typedef int nbdkit_next_preconnect (nbdkit_backend *nxdata, int readonly); +typedef int nbdkit_next_list_exports (nbdkit_backend *nxdata, int readonly, + int default_only, + struct nbdkit_exports *exports); typedef int nbdkit_next_open (nbdkit_backend *nxdata, int readonly, const char *exportname); @@ -167,6 +170,9 @@ struct nbdkit_filter { int (*after_fork) (nbdkit_next_after_fork *next, nbdkit_backend *nxdata); int (*preconnect) (nbdkit_next_preconnect *next, nbdkit_backend *nxdata, int readonly); + int (*list_exports) (nbdkit_next_list_exports *next, nbdkit_backend *nxdata, + int readonly, int default_only, + struct nbdkit_exports *exports); void * (*open) (nbdkit_next_open *next, nbdkit_backend *nxdata, int readonly, const char *exportname); diff --git a/include/nbdkit-plugin.h b/include/nbdkit-plugin.h index c6c1256c..86b8565d 100644 --- a/include/nbdkit-plugin.h +++ b/include/nbdkit-plugin.h @@ -139,6 +139,9 @@ struct nbdkit_plugin { int (*get_ready) (void); int (*after_fork) (void); + + int (*list_exports) (int readonly, int default_only, + struct nbdkit_exports *exports); }; extern void nbdkit_set_error (int err); diff --git a/server/filters.c b/server/filters.c index e5b5b860..20518354 100644 --- a/server/filters.c +++ b/server/filters.c @@ -241,7 +241,11 @@ static int filter_list_exports (struct backend *b, int readonly, int default_only, struct nbdkit_exports *exports) { - /* XXX No filter override yet... */ + struct backend_filter *f = container_of (b, struct backend_filter, backend); + + if (f->filter.list_exports) + return f->filter.list_exports (backend_list_exports, b->next, + readonly, default_only, exports); return backend_list_exports (b->next, readonly, default_only, exports); } diff --git a/server/plugins.c b/server/plugins.c index 8020046b..d4364cd2 100644 --- a/server/plugins.c +++ b/server/plugins.c @@ -161,6 +161,7 @@ plugin_dump_fields (struct backend *b) HAS (get_ready); HAS (after_fork); HAS (preconnect); + HAS (list_exports); HAS (open); HAS (close); HAS (get_size); @@ -281,8 +282,13 @@ static int plugin_list_exports (struct backend *b, int readonly, int default_only, struct nbdkit_exports *exports) { - /* XXX No plugin support yet, so for now just advertise "" */ - return nbdkit_add_export (exports, "", NULL); + GET_CONN; + struct backend_plugin *p = container_of (b, struct backend_plugin, backend); + + if (!p->plugin.list_exports) + return nbdkit_add_export (exports, "", NULL); + + return p->plugin.list_exports (readonly, default_only, exports); } static void * diff --git a/tests/test-layers-filter.c b/tests/test-layers-filter.c index 29d8b669..397af575 100644 --- a/tests/test-layers-filter.c +++ b/tests/test-layers-filter.c @@ -106,6 +106,15 @@ test_layers_filter_preconnect (nbdkit_next_preconnect *next, return next (nxdata, readonly); } +static int +test_layers_filter_list_exports (nbdkit_next_list_exports *next, void *nxdata, + int readonly, int default_only, + struct nbdkit_exports *exports) +{ + DEBUG_FUNCTION; + return next (nxdata, readonly, default_only, exports); +} + static void * test_layers_filter_open (nbdkit_next_open *next, void *nxdata, int readonly, const char *exportname) @@ -370,6 +379,7 @@ static struct nbdkit_filter filter = { .get_ready = test_layers_filter_get_ready, .after_fork = test_layers_filter_after_fork, .preconnect = test_layers_filter_preconnect, + .list_exports = test_layers_filter_list_exports, .open = test_layers_filter_open, .close = test_layers_filter_close, .prepare = test_layers_filter_prepare, diff --git a/tests/test-layers-plugin.c b/tests/test-layers-plugin.c index ccb67fe4..1dfd069e 100644 --- a/tests/test-layers-plugin.c +++ b/tests/test-layers-plugin.c @@ -93,6 +93,14 @@ test_layers_plugin_preconnect (int readonly) return 0; } +static int +test_layers_plugin_list_exports (int readonly, int default_only, + struct nbdkit_exports *exports) +{ + DEBUG_FUNCTION; + return nbdkit_add_export (exports, "", NULL); +} + static void * test_layers_plugin_open (int readonly) { @@ -248,6 +256,7 @@ static struct nbdkit_plugin plugin = { .get_ready = test_layers_plugin_get_ready, .after_fork = test_layers_plugin_after_fork, .preconnect = test_layers_plugin_preconnect, + .list_exports = test_layers_plugin_list_exports, .open = test_layers_plugin_open, .close = test_layers_plugin_close, .get_size = test_layers_plugin_get_size, diff --git a/tests/test-layers.c b/tests/test-layers.c index 9a0279f1..dd826f36 100644 --- a/tests/test-layers.c +++ b/tests/test-layers.c @@ -326,6 +326,21 @@ main (int argc, char *argv[]) "test_layers_plugin_preconnect", NULL); + /* list_exports methods called in outer-to-inner order, complete + * in inner-to-outer order. But since we didn't send NBD_OPT_LIST, + * the outer filter does not expose a list; rather, the rest of the + * chain is used to resolve the canonical name of the default export. + */ + log_verify_seen_in_order + ("filter3: test_layers_filter_list_exports", + "testlayersfilter2: list_exports", + "filter2: test_layers_filter_list_exports", + "testlayersfilter1: list_exports", + "filter1: test_layers_filter_list_exports", + "testlayersplugin: list_exports", + "test_layers_plugin_list_exports", + NULL); + /* open methods called in outer-to-inner order, but thanks to next * pointer, complete in inner-to-outer order. */ log_verify_seen_in_order -- 2.28.0
Eric Blake
2020-Aug-06 02:25 UTC
[Libguestfs] [nbdkit PATCH v2 4/5] log: Add .list_exports support
Log the list of exports queried by the client, or any attempt by nbdkit to resolve the canonical name of the default export. The documentation example was created with libnbd.git (note that libnbd 1.4 may change from the experimental API shown here...): $ path/to/libnbd/run ./nbdkit --filter=log memory $((0x400)) \ logfile=/dev/stderr --run 'nbdsh -c "h.set_list_exports(True)" \ -c "h.connect_uri(\"$uri\")" -c "h.pread(512,0)"' Signed-off-by: Eric Blake <eblake@redhat.com> --- filters/log/nbdkit-log-filter.pod | 18 ++++++----- filters/log/log.c | 50 +++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/filters/log/nbdkit-log-filter.pod b/filters/log/nbdkit-log-filter.pod index 15d61cad..5f690457 100644 --- a/filters/log/nbdkit-log-filter.pod +++ b/filters/log/nbdkit-log-filter.pod @@ -62,20 +62,22 @@ the impact of the caching. This filter writes to the file specified by the C<logfile=FILE> parameter. All lines include a timestamp, a connection counter, then -details about the command. The following actions are logged: Connect, -Read, Write, Zero, Trim, Extents, Cache, Flush, and Disconnect. +details about the command. The following actions are logged: ListExports, +Connect, Read, Write, Zero, Trim, Extents, Cache, Flush, and Disconnect. Except for Connect and Disconnect, an event is logged across two lines for call and return value, to allow tracking duration and tracing any parallel execution, using id for correlation (incremented per action on the connection). -An example logging session of a client that performs a single -successful read is: +An example logging session of a client that requests an export list +before performing a single successful read is: - 2018-01-27 20:38:22.959984 connection=1 Connect export='' size=0x400 write=1 flush=1 rotational=0 trim=0 zero=1 fua=1 extents=1 cache=0 fast_zero=0 - 2018-01-27 20:38:23.001720 connection=1 Read id=1 offset=0x0 count=0x100 ... - 2018-01-27 20:38:23.001995 connection=1 ...Read id=1 return=0 (Success) - 2018-01-27 20:38:23.044259 connection=1 Disconnect transactions=1 + 2020-08-06 02:07:23.080415 ListExports id=1 readonly=0 default_only=0 ... + 2020-08-06 02:07:23.080502 ...ListExports id=1 exports=[""] return=0 + 2020-08-06 02:07:23.080712 connection=1 Connect export='' size=0x400 write=1 flush=1 rotational=0 trim=1 zero=2 fua=2 extents=1 cache=2 fast_zero=1 + 2020-08-06 02:07:23.080907 connection=1 Read id=1 offset=0x0 count=0x200 ... + 2020-08-06 02:07:23.080927 connection=1 ...Read id=1 return=0 (Success) + 2020-08-06 02:07:23.081255 connection=1 Disconnect transactions=1 =item F<$filterdir/nbdkit-log-filter.so> diff --git a/filters/log/log.c b/filters/log/log.c index 2078d0c2..c89f5001 100644 --- a/filters/log/log.c +++ b/filters/log/log.c @@ -48,6 +48,7 @@ #include <nbdkit-filter.h> #include "cleanup.h" +#include "utils.h" static uint64_t connections; static char *logfilename; @@ -166,8 +167,11 @@ output (struct handle *h, const char *act, uint64_t id, const char *fmt, ...) 0L + tv.tv_usec); } flockfile (logfile); - fprintf (logfile, "%s connection=%" PRIu64 " %s ", timestamp, h->connection, - act); + if (h) + fprintf (logfile, "%s connection=%" PRIu64 " %s ", timestamp, + h->connection, act); + else + fprintf (logfile, "%s %s ", timestamp, act); if (id) fprintf (logfile, "id=%" PRIu64 " ", id); va_start (args, fmt); @@ -235,6 +239,47 @@ output_return (struct handle *h, const char *act, uint64_t id, int r, int *err) output (h, act, id, "return=%d (%s)", r, s); } +/* List exports. */ +static int +log_list_exports (nbdkit_next_list_exports *next, void *nxdata, + int readonly, int default_only, + struct nbdkit_exports *exports) +{ + static uint64_t id; + int r; + int err; + + output (NULL, "ListExports", ++id, "readonly=%d default_only=%d ...", + readonly, default_only); + r = next (nxdata, readonly, default_only, exports); + if (r == -1) { + err = errno; + output_return (NULL, "...ListExports", id, r, &err); + } + else { + FILE *fp; + CLEANUP_FREE char *exports_str = NULL; + size_t i, n, len = 0; + + fp = open_memstream (&exports_str, &len); + if (fp != NULL) { + n = nbdkit_exports_count (exports); + for (i = 0; i < n; ++i) { + struct nbdkit_export e = nbdkit_get_export (exports, i); + if (i > 0) + fprintf (fp, ", "); + shell_quote (e.name, fp); + } + + fclose (fp); + } + + output (NULL, "...ListExports", id, "exports=[%s] return=0", + exports_str ? exports_str : "(null)"); + } + return r; +} + /* Open a connection. */ static void * log_open (nbdkit_next_open *next, void *nxdata, @@ -476,6 +521,7 @@ static struct nbdkit_filter filter = { .config_help = log_config_help, .unload = log_unload, .get_ready = log_get_ready, + .list_exports = log_list_exports, .open = log_open, .close = log_close, .prepare = log_prepare, -- 2.28.0
Eric Blake
2020-Aug-06 02:25 UTC
[Libguestfs] [nbdkit PATCH v2 5/5] sh, eval: Add .list_exports support
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@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
Richard W.M. Jones
2020-Aug-06 07:41 UTC
Re: [Libguestfs] [nbdkit PATCH v2 5/5] sh, eval: Add .list_exports support
Patch series seems reasonable. I'm happy if you want to push this and we can sort out any other problems with it later. Rich. -- Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones Read my programming and virtualization blog: http://rwmj.wordpress.com Fedora Windows cross-compiler. Compile Windows programs, test, and build Windows installers. Over 100 libraries supported. http://fedoraproject.org/wiki/MinGW
Seemingly Similar Threads
- [nbdkit PATCH 0/5] Implement .default_export, nbdkit_string_intern
- [nbdkit PATCH v2 0/8] exportname filter
- [nbdkit PATCH v3 00/14] exportname filter
- [RFC nbdkit PATCH 0/4] Progress towards .list_exports
- [PATCH nbdkit 0/2] Temporarily remove .list_exports for nbdkit 1.22