[Libguestfs] [libnbd PATCH v2 1/2] api: Add nbd_opt_list_meta_context

Eric Blake eblake at redhat.com
Fri Oct 2 13:20:12 UTC 2020


Right now, we require the user to supply potential metacontext names
in advance, without knowing what the server actually supports until
after the connection or nbd_opt_info call is complete.  But the NBD
protocol also supports a client being able to query what contexts a
server supports, including where a client asks with a prefix and the
server replies back with all answers that match the prefix.

Not to mention this will allow nbdinfo to gain full feature parity
with 'qemu-nbd --list' in an upcoming patch.

And as promised in commit 909af17e, this adds some multi-language
testsuite coverage of not only this API, but also the recent additions
related to the meta context request list.
---
 lib/internal.h                                |   1 +
 generator/API.ml                              |  84 ++++++++-
 generator/states-newstyle-opt-meta-context.c  |  83 +++++++--
 generator/states-newstyle.c                   |   3 +
 lib/opt.c                                     |  73 ++++++++
 python/t/240-opt-list-meta.py                 |  79 +++++++++
 ocaml/tests/Makefile.am                       |   3 +
 ocaml/tests/test_240_opt_list_meta.ml         |  80 +++++++++
 tests/Makefile.am                             |  10 ++
 tests/opt-list-meta.c                         | 167 ++++++++++++++++++
 .gitignore                                    |   1 +
 TODO                                          |  11 ++
 golang/Makefile.am                            |   1 +
 .../libnbd/libnbd_240_opt_list_meta_test.go   | 146 +++++++++++++++
 14 files changed, 724 insertions(+), 18 deletions(-)
 create mode 100644 python/t/240-opt-list-meta.py
 create mode 100644 ocaml/tests/test_240_opt_list_meta.ml
 create mode 100644 tests/opt-list-meta.c
 create mode 100644 golang/src/libguestfs.org/libnbd/libnbd_240_opt_list_meta_test.go

diff --git a/lib/internal.h b/lib/internal.h
index cde5dcd..ad1eeb9 100644
--- a/lib/internal.h
+++ b/lib/internal.h
@@ -77,6 +77,7 @@ struct command_cb {
     nbd_extent_callback extent;
     nbd_chunk_callback chunk;
     nbd_list_callback list;
+    nbd_context_callback context;
   } fn;
   nbd_completion_callback completion;
 };
diff --git a/generator/API.ml b/generator/API.ml
index bf21aff..d1e56e4 100644
--- a/generator/API.ml
+++ b/generator/API.ml
@@ -144,8 +144,13 @@ let list_closure = {
   cbname = "list";
   cbargs = [ CBString "name"; CBString "description" ]
 }
+let context_closure = {
+  cbname = "context";
+  cbargs = [ CBString "name" ]
+}
 let all_closures = [ chunk_closure; completion_closure;
-                     debug_closure; extent_closure; list_closure ]
+                     debug_closure; extent_closure; list_closure;
+                     context_closure ]

 (* Enums. *)
 let tls_enum = {
@@ -854,7 +859,7 @@ to end the connection without finishing negotiation.";
     example = Some "examples/list-exports.c";
     see_also = [Link "get_opt_mode"; Link "aio_is_negotiating";
                 Link "opt_abort"; Link "opt_go"; Link "opt_list";
-                Link "opt_info"];
+                Link "opt_info"; Link "opt_list_meta_context"];
   };

   "get_opt_mode", {
@@ -964,6 +969,56 @@ corresponding L<nbd_opt_go(3)> would succeed.";
                 Link "set_export_name"];
   };

+  "opt_list_meta_context", {
+    default_call with
+    args = [ Closure context_closure ]; ret = RInt;
+    permitted_states = [ Negotiating ];
+    shortdesc = "request the server to list available meta contexts";
+    longdesc = "\
+Request that the server list available meta contexts associated with
+the export previously specified by the most recent
+L<nbd_set_export_name(3)> or L<nbd_connect_uri(3)>.  This can only be
+used if L<nbd_set_opt_mode(3)> enabled option mode.
+
+The NBD protocol allows a client to decide how many queries to ask
+the server.  Rather than taking that list of queries as a parameter
+to this function, libnbd reuses the current list of requested meta
+contexts as set by L<nbd_add_meta_context(3)>; you can use
+L<nbd_clear_meta_contexts(3)> to set up a different list of queries.
+When the list is empty, a server will typically reply with all
+contexts that it supports; when the list is non-empty, the server
+will reply only with supported contexts that match the client's
+request.  Note that a reply by the server might be encoded to
+represent several feasible contexts within one string, rather than
+multiple strings per actual context name that would actually succeed
+during L<nbd_opt_go(3)>; so it is still necessary to use
+L<nbd_can_meta_context(3)> after connecting to see which contexts
+are actually supported.
+
+The C<context> function is called once per server reply, with any
+C<user_data> passed to this function, and with C<name> supplied by
+the server.  Remember that it is not safe to call
+L<nbd_add_meta_context(3)> from within the context of the
+callback function; rather, your code must copy any C<name> needed for
+later use after this function completes.  At present, the return value
+of the callback is ignored, although a return of -1 should be avoided.
+
+For convenience, when this function succeeds, it returns the number
+of replies returned by the server.
+
+Not all servers understand this request, and even when it is understood,
+the server might intentionally send an empty list because it does not
+support the requested context, or may encounter a failure after
+delivering partial results.  Thus, this function may succeed even when
+no contexts are reported, or may fail but have a non-empty list.  Likewise,
+the NBD protocol does not specify an upper bound for the number of
+replies that might be advertised, so client code should be aware that
+a server may send a lengthy list.";
+    see_also = [Link "set_opt_mode"; Link "aio_opt_list_meta_context";
+                Link "add_meta_context"; Link "clear_meta_contexts";
+                Link "opt_go"; Link "set_export_name"];
+  };
+
   "add_meta_context", {
     default_call with
     args = [ String "name" ]; ret = RErr;
@@ -2237,6 +2292,29 @@ callback.";
     see_also = [Link "set_opt_mode"; Link "opt_info"; Link "is_read_only"];
   };

+  "aio_opt_list_meta_context", {
+    default_call with
+    args = [ Closure context_closure ]; ret = RInt;
+    optargs = [ OClosure completion_closure ];
+    permitted_states = [ Negotiating ];
+    shortdesc = "request the server to list available meta contexts";
+    longdesc = "\
+Request that the server list available meta contexts associated with
+the export previously specified by the most recent
+L<nbd_set_export_name(3)> or L<nbd_connect_uri(3)>.  This can only be
+used if L<nbd_set_opt_mode(3)> enabled option mode.
+
+To determine when the request completes, wait for
+L<nbd_aio_is_connecting(3)> to return false.  Or supply the optional
+C<completion_callback> which will be invoked as described in
+L<libnbd(3)/Completion callbacks>, except that it is automatically
+retired regardless of return value.  Note that detecting whether the
+server returns an error (as is done by the return value of the
+synchronous counterpart) is only possible with a completion
+callback.";
+    see_also = [Link "set_opt_mode"; Link "opt_list_meta_context"];
+  };
+
   "aio_pread", {
     default_call with
     args = [ BytesPersistOut ("buf", "count"); UInt64 "offset" ];
@@ -2888,6 +2966,8 @@ let first_version = [
   "get_nr_meta_contexts", (1, 6);
   "get_meta_context", (1, 6);
   "clear_meta_contexts", (1, 6);
+  "opt_list_meta_context", (1, 6);
+  "aio_opt_list_meta_context", (1, 6);

   (* These calls are proposed for a future version of libnbd, but
    * have not been added to any released version so far.
diff --git a/generator/states-newstyle-opt-meta-context.c b/generator/states-newstyle-opt-meta-context.c
index 0dc48af..fe82252 100644
--- a/generator/states-newstyle-opt-meta-context.c
+++ b/generator/states-newstyle-opt-meta-context.c
@@ -21,18 +21,28 @@
 STATE_MACHINE {
  NEWSTYLE.OPT_META_CONTEXT.START:
   size_t i, nr_queries;
-  uint32_t len;
+  uint32_t len, opt;

   /* If the server doesn't support SRs then we must skip this group.
    * Also we skip the group if the client didn't request any metadata
-   * contexts.
+   * contexts, when doing SET (but an empty LIST is okay).
    */
   assert (h->gflags & LIBNBD_HANDSHAKE_FLAG_FIXED_NEWSTYLE);
   nbd_internal_reset_size_and_flags (h);
-  if (!h->structured_replies ||
-      nbd_internal_string_list_length (h->request_meta_contexts) == 0) {
-    SET_NEXT_STATE (%^OPT_GO.START);
-    return 0;
+  if (h->opt_current == NBD_OPT_LIST_META_CONTEXT) {
+    assert (h->opt_mode);
+    assert (h->structured_replies);
+    assert (CALLBACK_IS_NOT_NULL (h->opt_cb.fn.context));
+    opt = h->opt_current;
+  }
+  else {
+    assert (CALLBACK_IS_NULL (h->opt_cb.fn.context));
+    opt = NBD_OPT_SET_META_CONTEXT;
+    if (!h->structured_replies ||
+        nbd_internal_string_list_length (h->request_meta_contexts) == 0) {
+      SET_NEXT_STATE (%^OPT_GO.START);
+      return 0;
+    }
   }

   assert (h->meta_contexts == NULL);
@@ -44,7 +54,7 @@ STATE_MACHINE {
     len += 4 /* length of query */ + strlen (h->request_meta_contexts[i]);

   h->sbuf.option.version = htobe64 (NBD_NEW_VERSION);
-  h->sbuf.option.option = htobe32 (NBD_OPT_SET_META_CONTEXT);
+  h->sbuf.option.option = htobe32 (opt);
   h->sbuf.option.optlen = htobe32 (len);
   h->wbuf = &h->sbuf;
   h->wlen = sizeof (h->sbuf.option);
@@ -98,7 +108,8 @@ STATE_MACHINE {
   return 0;

  NEWSTYLE.OPT_META_CONTEXT.PREPARE_NEXT_QUERY:
-  const char *query = h->request_meta_contexts[h->querynum];
+  const char *query = !h->request_meta_contexts ? NULL
+    : h->request_meta_contexts[h->querynum];

   if (query == NULL) { /* end of list of requested meta contexts */
     SET_NEXT_STATE (%PREPARE_FOR_REPLY);
@@ -140,10 +151,17 @@ STATE_MACHINE {
   return 0;

  NEWSTYLE.OPT_META_CONTEXT.RECV_REPLY:
+  uint32_t opt;
+
+  if (h->opt_current == NBD_OPT_LIST_META_CONTEXT)
+    opt = h->opt_current;
+  else
+    opt = NBD_OPT_SET_META_CONTEXT;
+
   switch (recv_into_rbuf (h)) {
   case -1: SET_NEXT_STATE (%.DEAD); return 0;
   case 0:
-    if (prepare_for_reply_payload (h, NBD_OPT_SET_META_CONTEXT) == -1) {
+    if (prepare_for_reply_payload (h, opt) == -1) {
       SET_NEXT_STATE (%.DEAD);
       return 0;
     }
@@ -163,12 +181,25 @@ STATE_MACHINE {
   uint32_t len;
   const size_t maxpayload = sizeof h->sbuf.or.payload.context;
   struct meta_context *meta_context;
+  uint32_t opt;
+  int err = 0;
+
+  if (h->opt_current == NBD_OPT_LIST_META_CONTEXT)
+    opt = h->opt_current;
+  else
+    opt = NBD_OPT_SET_META_CONTEXT;

   reply = be32toh (h->sbuf.or.option_reply.reply);
   len = be32toh (h->sbuf.or.option_reply.replylen);
   switch (reply) {
   case NBD_REP_ACK:           /* End of list of replies. */
-    SET_NEXT_STATE (%^OPT_GO.START);
+    if (opt == NBD_OPT_LIST_META_CONTEXT) {
+      SET_NEXT_STATE (%.NEGOTIATING);
+      CALL_CALLBACK (h->opt_cb.completion, &err);
+      nbd_internal_free_option (h);
+    }
+    else
+      SET_NEXT_STATE (%^OPT_GO.START);
     break;
   case NBD_REP_META_CONTEXT:  /* A context. */
     if (len > maxpayload)
@@ -194,21 +225,41 @@ STATE_MACHINE {
       }
       debug (h, "negotiated %s with context ID %" PRIu32,
              meta_context->name, meta_context->context_id);
-      meta_context->next = h->meta_contexts;
-      h->meta_contexts = meta_context;
+      if (opt == NBD_OPT_LIST_META_CONTEXT) {
+        CALL_CALLBACK (h->opt_cb.fn.context, meta_context->name);
+        free (meta_context->name);
+        free (meta_context);
+      }
+      else {
+        meta_context->next = h->meta_contexts;
+        h->meta_contexts = meta_context;
+      }
     }
     SET_NEXT_STATE (%PREPARE_FOR_REPLY);
     break;
   default:
-    /* Anything else is an error, ignore it */
+    /* Anything else is an error, ignore it for SET, report it for LIST */
     if (handle_reply_error (h) == -1) {
       SET_NEXT_STATE (%.DEAD);
       return 0;
     }

-    debug (h, "handshake: unexpected error from "
-           "NBD_OPT_SET_META_CONTEXT (%" PRIu32 ")", reply);
-    SET_NEXT_STATE (%^OPT_GO.START);
+    if (opt == NBD_OPT_LIST_META_CONTEXT) {
+      /* XXX Should we decode specific expected errors, like
+       * REP_ERR_UNKNOWN to ENOENT or REP_ERR_TOO_BIG to ERANGE?
+       */
+      err = ENOTSUP;
+      set_error (err, "unexpected response, possibly the server does not "
+                 "support listing contexts");
+      CALL_CALLBACK (h->opt_cb.completion, &err);
+      nbd_internal_free_option (h);
+      SET_NEXT_STATE (%.NEGOTIATING);
+    }
+    else {
+      debug (h, "handshake: unexpected error from "
+             "NBD_OPT_SET_META_CONTEXT (%" PRIu32 ")", reply);
+      SET_NEXT_STATE (%^OPT_GO.START);
+    }
     break;
   }
   return 0;
diff --git a/generator/states-newstyle.c b/generator/states-newstyle.c
index a0a5928..6fb7548 100644
--- a/generator/states-newstyle.c
+++ b/generator/states-newstyle.c
@@ -136,6 +136,9 @@ STATE_MACHINE {
       }
       SET_NEXT_STATE (%PREPARE_OPT_ABORT);
       return 0;
+    case NBD_OPT_LIST_META_CONTEXT:
+      SET_NEXT_STATE (%OPT_META_CONTEXT.START);
+      return 0;
     case 0:
       break;
     default:
diff --git a/lib/opt.c b/lib/opt.c
index 6ea8326..2317b72 100644
--- a/lib/opt.c
+++ b/lib/opt.c
@@ -32,6 +32,8 @@ nbd_internal_free_option (struct nbd_handle *h)
 {
   if (h->opt_current == NBD_OPT_LIST)
     FREE_CALLBACK (h->opt_cb.fn.list);
+  else if (h->opt_current == NBD_OPT_LIST_META_CONTEXT)
+    FREE_CALLBACK (h->opt_cb.fn.context);
   FREE_CALLBACK (h->opt_cb.completion);
 }

@@ -166,6 +168,51 @@ nbd_unlocked_opt_list (struct nbd_handle *h, nbd_list_callback *list)
   return s.count;
 }

+struct context_helper {
+  int count;
+  nbd_context_callback context;
+  int err;
+};
+static int
+context_visitor (void *opaque, const char *name)
+{
+  struct context_helper *h = opaque;
+  if (h->count < INT_MAX)
+    h->count++;
+  CALL_CALLBACK (h->context, name);
+  return 0;
+}
+static int
+context_complete (void *opaque, int *err)
+{
+  struct context_helper *h = opaque;
+  h->err = *err;
+  FREE_CALLBACK (h->context);
+  return 0;
+}
+
+/* Issue NBD_OPT_LIST_META_CONTEXT and wait for the reply. */
+int
+nbd_unlocked_opt_list_meta_context (struct nbd_handle *h,
+                                    nbd_context_callback *context)
+{
+  struct context_helper s = { .context = *context };
+  nbd_context_callback l = { .callback = context_visitor, .user_data = &s };
+  nbd_completion_callback c = { .callback = context_complete, .user_data = &s };
+
+  if (nbd_unlocked_aio_opt_list_meta_context (h, &l, &c) == -1)
+    return -1;
+
+  SET_CALLBACK_TO_NULL (*context);
+  if (wait_for_option (h) == -1)
+    return -1;
+  if (s.err) {
+    set_error (s.err, "server replied with error to list meta context request");
+    return -1;
+  }
+  return s.count;
+}
+
 /* Issue NBD_OPT_GO (or NBD_OPT_EXPORT_NAME) without waiting. */
 int
 nbd_unlocked_aio_opt_go (struct nbd_handle *h,
@@ -230,3 +277,29 @@ nbd_unlocked_aio_opt_list (struct nbd_handle *h, nbd_list_callback *list,
     debug (h, "option queued, ignoring state machine failure");
   return 0;
 }
+
+/* Issue NBD_OPT_LIST_META_CONTEXT without waiting. */
+int
+nbd_unlocked_aio_opt_list_meta_context (struct nbd_handle *h,
+                                        nbd_context_callback *context,
+                                        nbd_completion_callback *complete)
+{
+  if ((h->gflags & LIBNBD_HANDSHAKE_FLAG_FIXED_NEWSTYLE) == 0) {
+    set_error (ENOTSUP, "server is not using fixed newstyle protocol");
+    return -1;
+  }
+  if (!h->structured_replies) {
+    set_error (ENOTSUP, "server lacks structured replies");
+    return -1;
+  }
+
+  assert (CALLBACK_IS_NULL (h->opt_cb.fn.context));
+  h->opt_cb.fn.context = *context;
+  SET_CALLBACK_TO_NULL (*context);
+  h->opt_cb.completion = *complete;
+  SET_CALLBACK_TO_NULL (*complete);
+  h->opt_current = NBD_OPT_LIST_META_CONTEXT;
+  if (nbd_internal_run (h, cmd_issue) == -1)
+    debug (h, "option queued, ignoring state machine failure");
+  return 0;
+}
diff --git a/python/t/240-opt-list-meta.py b/python/t/240-opt-list-meta.py
new file mode 100644
index 0000000..8341f9c
--- /dev/null
+++ b/python/t/240-opt-list-meta.py
@@ -0,0 +1,79 @@
+# libnbd Python bindings
+# Copyright (C) 2010-2020 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+import nbd
+
+
+count = 0
+seen = False
+
+
+def f(user_data, name):
+    global count
+    global seen
+    assert user_data == 42
+    count = count + 1
+    if name == nbd.CONTEXT_BASE_ALLOCATION:
+        seen = True
+
+
+h = nbd.NBD()
+h.set_opt_mode(True)
+h.connect_command(["nbdkit", "-s", "--exit-with-parent", "-v",
+                   "memory", "size=1M"])
+
+# First pass: empty query should give at least "base:allocation".
+count = 0
+seen = False
+r = h.opt_list_meta_context(lambda *args: f(42, *args))
+assert r == count
+assert r >= 1
+assert seen
+max = count
+
+# Second pass: bogus query has no response.
+count = 0
+seen = False
+h.add_meta_context("x-nosuch:")
+r = h.opt_list_meta_context(lambda *args: f(42, *args))
+assert r == 0
+assert r == count
+assert not seen
+
+# Third pass: specific query should have one match.
+count = 0
+seen = False
+h.add_meta_context(nbd.CONTEXT_BASE_ALLOCATION)
+assert h.get_nr_meta_contexts() == 2
+assert h.get_meta_context(1) == nbd.CONTEXT_BASE_ALLOCATION
+r = h.opt_list_meta_context(lambda *args: f(42, *args))
+assert r == 1
+assert count == 1
+assert seen
+
+# Final pass: "base:" query should get at least "base:allocation"
+count = 0
+seen = False
+h.clear_meta_contexts()
+h.add_meta_context("base:")
+r = h.opt_list_meta_context(lambda *args: f(42, *args))
+assert r >= 1
+assert r <= max
+assert r == count
+assert seen
+
+h.opt_abort()
diff --git a/ocaml/tests/Makefile.am b/ocaml/tests/Makefile.am
index 8fbf38f..c5dfe9e 100644
--- a/ocaml/tests/Makefile.am
+++ b/ocaml/tests/Makefile.am
@@ -28,6 +28,7 @@ EXTRA_DIST = \
 	test_210_opt_abort.ml \
 	test_220_opt_list.ml \
 	test_230_opt_info.ml \
+	test_240_opt_list_meta.ml \
 	test_300_get_size.ml \
 	test_400_pread.ml \
 	test_405_pread_structured.ml \
@@ -53,6 +54,7 @@ tests_bc = \
 	test_210_opt_abort.bc \
 	test_220_opt_list.bc \
 	test_230_opt_info.bc \
+	test_240_opt_list_meta.bc \
 	test_300_get_size.bc \
 	test_400_pread.bc \
 	test_405_pread_structured.bc \
@@ -75,6 +77,7 @@ tests_opt = \
 	test_210_opt_abort.opt \
 	test_220_opt_list.opt \
 	test_230_opt_info.opt \
+	test_240_opt_list_meta.opt \
 	test_300_get_size.opt \
 	test_400_pread.opt \
 	test_405_pread_structured.opt \
diff --git a/ocaml/tests/test_240_opt_list_meta.ml b/ocaml/tests/test_240_opt_list_meta.ml
new file mode 100644
index 0000000..87a2dfc
--- /dev/null
+++ b/ocaml/tests/test_240_opt_list_meta.ml
@@ -0,0 +1,80 @@
+(* hey emacs, this is OCaml code: -*- tuareg -*- *)
+(* libnbd OCaml test case
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *)
+
+let count = ref 0
+let seen = ref false
+let f user_data name =
+  assert (user_data = 42);
+  count := !count + 1;
+  if name = NBD.context_base_allocation then
+    seen := true;
+  0
+
+let () =
+  let nbd = NBD.create () in
+  NBD.set_opt_mode nbd true;
+  NBD.connect_command nbd
+                      ["nbdkit"; "-s"; "--exit-with-parent"; "-v";
+                       "memory"; "size=1M"];
+
+  (* First pass: empty query should give at least "base:allocation". *)
+  count := 0;
+  seen := false;
+  let r = NBD.opt_list_meta_context nbd (f 42) in
+  assert (r = !count);
+  assert (r >= 1);
+  assert !seen;
+  let max = !count in
+
+  (* Second pass: bogus query has no response. *)
+  count := 0;
+  seen := false;
+  NBD.add_meta_context nbd "x-nosuch:";
+  let r = NBD.opt_list_meta_context nbd (f 42) in
+  assert (r = 0);
+  assert (r = !count);
+  assert (!seen = false);
+
+  (* Third pass: specific query should have one match. *)
+  count := 0;
+  seen := false;
+  NBD.add_meta_context nbd NBD.context_base_allocation;
+  let c = NBD.get_nr_meta_contexts nbd in
+  assert (c = 2);
+  let n = NBD.get_meta_context nbd 1 in
+  assert (n = NBD.context_base_allocation);
+  let r = NBD.opt_list_meta_context nbd (f 42) in
+  assert (r = 1);
+  assert (r = !count);
+  assert !seen;
+
+  (* Final pass: "base:" query should get at least "base:allocation" *)
+  count := 0;
+  seen := false;
+  NBD.clear_meta_contexts nbd;
+  NBD.add_meta_context nbd "base:";
+  let r = NBD.opt_list_meta_context nbd (f 42) in
+  assert (r >= 1);
+  assert (r <= max);
+  assert (r = !count);
+  assert !seen;
+
+  NBD.opt_abort nbd
+
+let () = Gc.compact ()
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 007c69e..9d36c43 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -38,6 +38,7 @@ EXTRA_DIST = \
 	meta-base-allocation.sh \
 	opt-info.sh \
 	opt-list.sh \
+	opt-list-meta.sh \
 	synch-parallel.sh \
 	synch-parallel-tls.sh \
 	$(NULL)
@@ -169,6 +170,7 @@ check_PROGRAMS += \
 	opt-abort \
 	opt-list \
 	opt-info \
+	opt-list-meta \
 	connect-unix \
 	connect-tcp \
 	aio-parallel \
@@ -208,6 +210,7 @@ TESTS += \
 	opt-abort \
 	opt-list \
 	opt-info \
+	opt-list-meta \
 	connect-unix \
 	connect-tcp \
 	aio-parallel.sh \
@@ -409,6 +412,13 @@ opt_info_CPPFLAGS = \
 opt_info_CFLAGS = $(WARNINGS_CFLAGS)
 opt_info_LDADD = $(top_builddir)/lib/libnbd.la

+opt_list_meta_SOURCES = opt-list-meta.c
+opt_list_meta_CPPFLAGS = \
+	-I$(top_srcdir)/include \
+	$(NULL)
+opt_list_meta_CFLAGS = $(WARNINGS_CFLAGS)
+opt_list_meta_LDADD = $(top_builddir)/lib/libnbd.la
+
 connect_unix_SOURCES = connect-unix.c
 connect_unix_CPPFLAGS = -I$(top_srcdir)/include
 connect_unix_CFLAGS = $(WARNINGS_CFLAGS)
diff --git a/tests/opt-list-meta.c b/tests/opt-list-meta.c
new file mode 100644
index 0000000..2a4b3c0
--- /dev/null
+++ b/tests/opt-list-meta.c
@@ -0,0 +1,167 @@
+/* NBD client library in userspace
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/* Test behavior of nbd_opt_list_meta_context. */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <string.h>
+#include <errno.h>
+
+#include <libnbd.h>
+
+struct progress {
+  int count;
+  bool seen;
+};
+
+static int
+check (void *user_data, const char *name)
+{
+  struct progress *p = user_data;
+
+  p->count++;
+  if (strcmp (name, LIBNBD_CONTEXT_BASE_ALLOCATION) == 0)
+    p->seen = true;
+  return 0;
+}
+
+int
+main (int argc, char *argv[])
+{
+  struct nbd_handle *nbd;
+  int r;
+  struct progress p;
+  char *args[] = { "nbdkit", "-s", "--exit-with-parent", "-v",
+                   "memory", "size=1M", NULL };
+  int max;
+  char *tmp;
+
+  /* Get into negotiating state. */
+  nbd = nbd_create ();
+  if (nbd == NULL ||
+      nbd_set_opt_mode (nbd, true) == -1 ||
+      nbd_connect_command (nbd, args) == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+
+  /* First pass: empty query should give at least "base:allocation". */
+  p = (struct progress) { .count = 0 };
+  r = nbd_opt_list_meta_context (nbd,
+                                 (nbd_context_callback) { .callback = check,
+                                                          .user_data = &p});
+  if (r == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  if (r != p.count) {
+    fprintf (stderr, "inconsistent return value %d, expected %d\n", r, p.count);
+    exit (EXIT_FAILURE);
+  }
+  if (r < 1 || !p.seen) {
+    fprintf (stderr, "server did not reply with base:allocation\n");
+    exit (EXIT_FAILURE);
+  }
+  max = p.count;
+
+  /* Second pass: bogus query has no response. */
+  p = (struct progress) { .count = 0 };
+  r = nbd_add_meta_context (nbd, "x-nosuch:");
+  if (r == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  r = nbd_opt_list_meta_context (nbd,
+                                 (nbd_context_callback) { .callback = check,
+                                                          .user_data = &p});
+  if (r == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  if (r != 0 || p.count != 0 || p.seen) {
+    fprintf (stderr, "expecting no contexts, got %d\n", r);
+    exit (EXIT_FAILURE);
+  }
+
+  /* Third pass: specific query should have one match. */
+  p = (struct progress) { .count = 0 };
+  r = nbd_add_meta_context (nbd, LIBNBD_CONTEXT_BASE_ALLOCATION);
+  if (r == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  if (nbd_get_nr_meta_contexts (nbd) != 2) {
+    fprintf (stderr, "expecting 2 meta requests\n");
+    exit (EXIT_FAILURE);
+  }
+  tmp = nbd_get_meta_context (nbd, 1);
+  if (!tmp) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  if (strcmp (tmp, LIBNBD_CONTEXT_BASE_ALLOCATION) != 0) {
+    fprintf (stderr, "expecting base:allocation, got %s\n", tmp);
+    exit (EXIT_FAILURE);
+  }
+  free (tmp);
+  r = nbd_opt_list_meta_context (nbd,
+                                 (nbd_context_callback) { .callback = check,
+                                                          .user_data = &p});
+  if (r == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  if (r != 1 || p.count != 1 || !p.seen) {
+    fprintf (stderr, "expecting exactly one context, got %d\n", r);
+    exit (EXIT_FAILURE);
+  }
+
+  /* Final pass: "base:" query should get at least "base:allocation" */
+  p = (struct progress) { .count = 0 };
+  r = nbd_clear_meta_contexts (nbd);
+  if (r == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  r = nbd_add_meta_context (nbd, "base:");
+  if (r == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  r = nbd_opt_list_meta_context (nbd,
+                                 (nbd_context_callback) { .callback = check,
+                                                          .user_data = &p});
+  if (r == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  if (r < 1 || r > max || r != p.count || !p.seen) {
+    fprintf (stderr, "expecting at least one context, got %d\n", r);
+    exit (EXIT_FAILURE);
+  }
+
+  nbd_opt_abort (nbd);
+  nbd_close (nbd);
+  exit (EXIT_SUCCESS);
+}
diff --git a/.gitignore b/.gitignore
index aa9e0bb..3afa9f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -175,6 +175,7 @@ Makefile.in
 /tests/opt-abort
 /tests/opt-info
 /tests/opt-list
+/tests/opt-list-meta
 /tests/pki/
 /tests/read-only-flag
 /tests/read-write-flag
diff --git a/TODO b/TODO
index 6dff4df..10b1280 100644
--- a/TODO
+++ b/TODO
@@ -13,6 +13,17 @@ NBD resize extension.

 TLS should properly shut down the session (calling gnutls_bye).

+Potential deadlock with nbd_add_meta_context: if a client sends enough
+requests to the server that it blocks while writing, but the server
+replies to requests as they come in rather than waiting for the end of
+the client request, then the server can likewise block in writing
+replies that libnbd is not yet reading.  Not an issue for existing
+servers that don't have enough contexts to reply with enough data to
+fill buffers, but could be an issue with qemu-nbd if it is taught to
+exports many dirty bitmaps simultaneously.  Revamping the
+states-newstyle-meta-context.c state machine to let libnbd handle
+NBD_REP_META_CONTEXT while still writing queries could be hairy.
+
 Performance: Chart it over various buffer sizes and threads, as that
   should make it easier to identify systematic issues.

diff --git a/golang/Makefile.am b/golang/Makefile.am
index c4fa564..fbc7c8c 100644
--- a/golang/Makefile.am
+++ b/golang/Makefile.am
@@ -36,6 +36,7 @@ source_files = \
 	src/$(pkg)/libnbd_210_opt_abort_test.go \
 	src/$(pkg)/libnbd_220_opt_list_test.go \
 	src/$(pkg)/libnbd_230_opt_info_test.go \
+	src/$(pkg)/libnbd_240_opt_list_meta_test.go \
 	src/$(pkg)/libnbd_300_get_size_test.go \
 	src/$(pkg)/libnbd_400_pread_test.go \
 	src/$(pkg)/libnbd_405_pread_structured_test.go \
diff --git a/golang/src/libguestfs.org/libnbd/libnbd_240_opt_list_meta_test.go b/golang/src/libguestfs.org/libnbd/libnbd_240_opt_list_meta_test.go
new file mode 100644
index 0000000..afddff6
--- /dev/null
+++ b/golang/src/libguestfs.org/libnbd/libnbd_240_opt_list_meta_test.go
@@ -0,0 +1,146 @@
+/* libnbd golang tests
+ * Copyright (C) 2013-2020 Red Hat Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package libnbd
+
+import (
+	"testing"
+)
+
+var count uint
+var seen bool
+
+func listmetaf(user_data int, name string) int {
+	if user_data != 42 {
+		panic("expected user_data == 42")
+	}
+	count++
+	if (name == context_base_allocation) {
+		seen = true
+	}
+	return 0
+}
+
+func Test240OptListMeta(t *testing.T) {
+	h, err := Create()
+	if err != nil {
+		t.Fatalf("could not create handle: %s", err)
+	}
+	defer h.Close()
+
+	err = h.SetOptMode(true)
+	if err != nil {
+		t.Fatalf("could not set opt mode: %s", err)
+	}
+
+	err = h.ConnectCommand([]string{
+		"nbdkit", "-s", "--exit-with-parent", "-v",
+		"memory", "size=1M",
+	})
+	if err != nil {
+		t.Fatalf("could not connect: %s", err)
+	}
+
+	/* First pass: empty query should give at least "base:allocation". */
+	count = 0
+	seen = false
+	r, err := h.OptListMetaContext(func(name string) int {
+	        return listmetaf(42, name)
+	})
+	if err != nil {
+		t.Fatalf("could not request opt_list_meta_context: %s", err)
+	}
+	if r != count || r < 1 || !seen {
+		t.Fatalf("unexpected count after opt_list_meta_context")
+	}
+	max := count
+
+	/* Second pass: bogus query has no response. */
+	count = 0
+	seen = false
+	err = h.AddMetaContext("x-nosuch:")
+	if err != nil {
+		t.Fatalf("could not request add_meta_context: %s", err)
+	}
+	r, err = h.OptListMetaContext(func(name string) int {
+	        return listmetaf(42, name)
+	})
+	if err != nil {
+		t.Fatalf("could not request opt_list_meta_context: %s", err)
+	}
+	if r != 0 || r != count || seen {
+		t.Fatalf("unexpected count after opt_list_meta_context")
+	}
+
+	/* Third pass: specific query should have one match. */
+	count = 0
+	seen = false
+	err = h.AddMetaContext(context_base_allocation)
+	if err != nil {
+		t.Fatalf("could not request add_meta_context: %s", err)
+	}
+	r, err = h.GetNrMetaContexts()
+	if err != nil {
+		t.Fatalf("could not request get_nr_meta_contexts: %s", err)
+	}
+	if r != 2 {
+		t.Fatalf("wrong number of meta_contexts: %d", r)
+	}
+	tmp, err := h.GetMetaContext(1)
+	if err != nil {
+		t.Fatalf("could not request get_meta_context: %s", err)
+	}
+	if *tmp != context_base_allocation {
+		t.Fatalf("wrong result of get_meta_context: %s", *tmp)
+	}
+	r, err = h.OptListMetaContext(func(name string) int {
+	        return listmetaf(42, name)
+	})
+	if err != nil {
+		t.Fatalf("could not request opt_list_meta_context: %s", err)
+	}
+	if r != 1 || r != count || !seen {
+		t.Fatalf("unexpected count after opt_list_meta_context")
+	}
+
+	/* Final pass: "base:" query should get at least "base:allocation" */
+	count = 0
+	seen = false
+	err = h.ClearMetaContexts()
+	if err != nil {
+		t.Fatalf("could not request clear_meta_contexts: %s", err)
+	}
+	err = h.AddMetaContext("base:")
+	if err != nil {
+		t.Fatalf("could not request add_meta_context: %s", err)
+	}
+	r, err = h.OptListMetaContext(func(name string) int {
+	        return listmetaf(42, name)
+	})
+	if err != nil {
+		t.Fatalf("could not request opt_list_meta_context: %s", err)
+	}
+	if r < 1 || r > max || r != count || !seen {
+		t.Fatalf("unexpected count after opt_list_meta_context")
+	}
+
+	err = h.OptAbort()
+	if err != nil {
+		t.Fatalf("could not request opt_abort: %s", err)
+	}
+}
-- 
2.28.0




More information about the Libguestfs mailing list