[Libguestfs] [libnbd PATCH v2 13/13] wip: api: add nbd_opt_info, nbd_aio_opt_info

Eric Blake
Fri, 14 Aug 2020 22:00:32 -0000

Make it possible to query details about a potential export without
actually connecting.  This resues the NBD_OPT_GO state machine, with
the main difference being that we can now return to negotiating state
even on success; thus checking for progression to Ready state is no
longer sufficient for detecting failure, and we have to rely on the
completion callback.

And now that we can get to Negotiation after a successful query, we
have to enable a lot more functions for use in that state to read the
results of the info query.

The next patch will then update nbdinfo to take advantage of this.

[TODO: fix tests/opt-info to actually test what is needed...]
---
 docs/libnbd.pod                    |   4 ++
 generator/API.ml                   | 108 +++++++++++++++++++++--------
 generator/states-newstyle-opt-go.c |  29 +++++---
 lib/opt.c                          |  35 ++++++++++
 tests/Makefile.am                  |   7 ++
 tests/newstyle-limited.c           |   8 +++
 tests/opt-info.c                   | 107 ++++++++++++++++++++++++++++
 .gitignore                         |   1 +
 8 files changed, 262 insertions(+), 37 deletions(-)
 create mode 100644 tests/opt-info.c

diff --git a/docs/libnbd.pod b/docs/libnbd.pod
index 4ee0815..9cdf152 100644
--- a/docs/libnbd.pod
+++ b/docs/libnbd.pod
@@ -493,6 +493,10 @@ Note that there are some servers (like L<nbdkit(1)>
E<le> 1.14) which
 ignore this, and other servers (like L<qemu-nbd(8)>) which require it
 to be set correctly but cannot serve different content.

+These APIs are also available after a successful L<nbd_opt_info(3)>
+during the negotiation phase, if you used L<nbd_set_opt_mode(3)> prior
+to connecting.
+
 =head2 Flag calls

 After connecting the server will send back a set of flags describing
diff --git a/generator/API.ml b/generator/API.ml
index bf50030..e703de7 100644
--- a/generator/API.ml
+++ b/generator/API.ml
@@ -294,12 +294,14 @@ before beginning a connection.  However, when
L<nbd_set_opt_mode(3)>
 has enabled option mode, it is possible to change the export
 name prior to L<nbd_opt_go(3)>.  In particular, the use of
 L<nbd_opt_list(3)> during negotiation can be used to determine
-a name the server is likely to accept.
+a name the server is likely to accept, and L<nbd_opt_info(3)> can
+be used to learn details about an export before connecting.

 This call may be skipped if using L<nbd_connect_uri(3)> to connect
 to a URI that includes an export name.";
     see_also = [Link "get_export_name"; Link "connect_uri";
-                Link "set_opt_mode"; Link "opt_go"; Link
"opt_list"];
+                Link "set_opt_mode"; Link "opt_go"; Link
"opt_list";
+                Link "opt_info"];
   };

   "get_export_name", {
@@ -353,7 +355,7 @@ Return the state of the full info request flag on this
handle.";
   "get_canonical_export_name", {
     default_call with
     args = []; ret = RString;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "return the canonical export name, if the server has
one";
     longdesc = "\
 The NBD protocol permits a server to report an optional canonical
@@ -373,7 +375,7 @@ client specifically hinted about wanting it, via
L<nbd_set_full_info(3)>.";
   "get_export_description", {
     default_call with
     args = []; ret = RString;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "return the export description, if the server has
one";
     longdesc = "\
 The NBD protocol permits a server to report an optional export
@@ -715,7 +717,8 @@ export name before trying again.  You may also use
L<nbd_opt_abort(3)>
 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_abort"; Link "opt_go"; Link
"opt_list";
+                Link "opt_info"];
   };

   "get_opt_mode", {
@@ -743,7 +746,7 @@ possible to attempt another option such as a different
export name;
 although older servers will instead have killed the connection.";
     example = Some "examples/list-exports.c";
     see_also = [Link "set_opt_mode"; Link "aio_opt_go";
Link "opt_abort";
-                Link "set_export_name"];
+                Link "set_export_name"; Link "opt_info"];
   };

   "opt_abort", {
@@ -798,6 +801,29 @@ description is set with I<-D>.";
                 Link "set_export_name"];
   };

+  "opt_info", {
+    default_call with
+    args = []; ret = RErr;
+    permitted_states = [ Negotiating ];
+    shortdesc = "request the server for information about an export";
+    longdesc = "\
+Request that the server supply information about the most recent
+export name set by L<nbd_set_export_name(3)>.  This can
+only be used if L<nbd_set_opt_mode(3)> enabled option mode.
+
+If successful, functions like L<nbd_is_read_only(3)> and
+L<nbd_get_size(3)> will report details about that export.  In
+general, if L<nbd_opt_go(3)> is called next, that call will
+likely succeed with the details remaining the same, although this
+is not guaranteed by all servers.
+
+Not all servers understand this request, and even when it is
+understood, the server might fail the request even when a
+corresponding L<nbd_opt_go(3)> would succeed.";
+    see_also = [Link "set_opt_mode"; Link "aio_opt_info";
Link "opt_go";
+                Link "set_export_name"];
+  };
+
   "add_meta_context", {
     default_call with
     args = [ String "name" ]; ret = RErr;
@@ -1180,27 +1206,27 @@ is killed.";
   "is_read_only", {
     default_call with
     args = []; ret = RBool;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "is the NBD export read-only?";
     longdesc = "\
 Returns true if the NBD export is read-only; writes and
 write-like operations will fail."
 ^ non_blocking_test_call_description;
-    see_also = [SectionLink "Flag calls"];
+    see_also = [SectionLink "Flag calls"; Link "opt_info"];
     example = Some "examples/server-flags.c";
   };

   "can_flush", {
     default_call with
     args = []; ret = RBool;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "does the server support the flush command?";
     longdesc = "\
 Returns true if the server supports the flush command
 (see L<nbd_flush(3)>, L<nbd_aio_flush(3)>).  Returns false if
 the server does not."
 ^ non_blocking_test_call_description;
-    see_also = [SectionLink "Flag calls";
+    see_also = [SectionLink "Flag calls"; Link "opt_info";
                 Link "flush"; Link "aio_flush"];
     example = Some "examples/server-flags.c";
   };
@@ -1208,13 +1234,13 @@ the server does not."
   "can_fua", {
     default_call with
     args = []; ret = RBool;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "does the server support the FUA flag?";
     longdesc = "\
 Returns true if the server supports the FUA flag on
 certain commands (see L<nbd_pwrite(3)>)."
 ^ non_blocking_test_call_description;
-    see_also = [SectionLink "Flag calls"; Link "pwrite";
+    see_also = [SectionLink "Flag calls"; Link "opt_info";
Link "pwrite";
                 Link "zero"; Link "trim"];
     example = Some "examples/server-flags.c";
   };
@@ -1222,7 +1248,7 @@ certain commands (see L<nbd_pwrite(3)>)."
   "is_rotational", {
     default_call with
     args = []; ret = RBool;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "is the NBD disk rotational (like a disk)?";
     longdesc = "\
 Returns true if the disk exposed over NBD is rotational
@@ -1230,21 +1256,21 @@ Returns true if the disk exposed over NBD is rotational
 the disk has no penalty for random access (like an SSD or
 RAM disk)."
 ^ non_blocking_test_call_description;
-    see_also = [SectionLink "Flag calls"];
+    see_also = [SectionLink "Flag calls"; Link "opt_info"];
     example = Some "examples/server-flags.c";
   };

   "can_trim", {
     default_call with
     args = []; ret = RBool;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "does the server support the trim command?";
     longdesc = "\
 Returns true if the server supports the trim command
 (see L<nbd_trim(3)>, L<nbd_aio_trim(3)>).  Returns false if
 the server does not."
 ^ non_blocking_test_call_description;
-    see_also = [SectionLink "Flag calls";
+    see_also = [SectionLink "Flag calls"; Link "opt_info";
                 Link "trim"; Link "aio_trim"];
     example = Some "examples/server-flags.c";
   };
@@ -1252,14 +1278,14 @@ the server does not."
   "can_zero", {
     default_call with
     args = []; ret = RBool;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "does the server support the zero command?";
     longdesc = "\
 Returns true if the server supports the zero command
 (see L<nbd_zero(3)>, L<nbd_aio_zero(3)>).  Returns false if
 the server does not."
 ^ non_blocking_test_call_description;
-    see_also = [SectionLink "Flag calls";
+    see_also = [SectionLink "Flag calls"; Link "opt_info";
                 Link "zero"; Link "aio_zero";
                 Link "can_fast_zero"];
     example = Some "examples/server-flags.c";
@@ -1268,7 +1294,7 @@ the server does not."
   "can_fast_zero", {
     default_call with
     args = []; ret = RBool;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "does the server support the fast zero flag?";
     longdesc = "\
 Returns true if the server supports the use of the
@@ -1276,7 +1302,7 @@ C<LIBNBD_CMD_FLAG_FAST_ZERO> flag to the zero
command
 (see L<nbd_zero(3)>, L<nbd_aio_zero(3)>).  Returns false if
 the server does not."
 ^ non_blocking_test_call_description;
-    see_also = [SectionLink "Flag calls";
+    see_also = [SectionLink "Flag calls"; Link "opt_info";
                 Link "zero"; Link "aio_zero"; Link
"can_zero"];
     example = Some "examples/server-flags.c";
   };
@@ -1284,7 +1310,7 @@ the server does not."
   "can_df", {
     default_call with
     args = []; ret = RBool;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "does the server support the don't fragment flag to
pread?";
     longdesc = "\
 Returns true if the server supports structured reads with an
@@ -1292,7 +1318,7 @@ ability to request a non-fragmented read (see
L<nbd_pread_structured(3)>,
 L<nbd_aio_pread_structured(3)>).  Returns false if the server either
lacks
 structured reads or if it does not support a non-fragmented read request."
 ^ non_blocking_test_call_description;
-    see_also = [SectionLink "Flag calls";
+    see_also = [SectionLink "Flag calls"; Link "opt_info";
                 Link "pread_structured";
                 Link "aio_pread_structured"];
     example = Some "examples/server-flags.c";
@@ -1301,7 +1327,7 @@ structured reads or if it does not support a
non-fragmented read request."
   "can_multi_conn", {
     default_call with
     args = []; ret = RBool;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "does the server support multi-conn?";
     longdesc = "\
 Returns true if the server supports multi-conn.  Returns
@@ -1314,21 +1340,22 @@ way to check for this is to open one connection, check
 this flag is true, then open further connections as
 required."
 ^ non_blocking_test_call_description;
-    see_also = [SectionLink "Multi-conn"];
+    see_also = [SectionLink "Flag calls"; SectionLink
"Multi-conn";
+                Link "opt_info"];
     example = Some "examples/server-flags.c";
   };

   "can_cache", {
     default_call with
     args = []; ret = RBool;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "does the server support the cache command?";
     longdesc = "\
 Returns true if the server supports the cache command
 (see L<nbd_cache(3)>, L<nbd_aio_cache(3)>).  Returns false if
 the server does not."
 ^ non_blocking_test_call_description;
-    see_also = [SectionLink "Flag calls";
+    see_also = [SectionLink "Flag calls"; Link "opt_info";
                 Link "cache"; Link "aio_cache"];
     example = Some "examples/server-flags.c";
   };
@@ -1336,7 +1363,7 @@ the server does not."
   "can_meta_context", {
     default_call with
     args = [String "metacontext"]; ret = RBool;
-    permitted_states = [ Connected; Closed ];
+    permitted_states = [ Negotiating; Connected; Closed ];
     shortdesc = "does the server support a specific meta context?";
     longdesc = "\
 Returns true if the server supports the given meta context
@@ -1349,7 +1376,7 @@ B<E<lt>libnbd.hE<gt>> includes defined
constants for well-known
 namespace contexts beginning with C<LIBNBD_CONTEXT_>, but you
 are free to pass in other contexts."
 ^ non_blocking_test_call_description;
-    see_also = [SectionLink "Flag calls";
+    see_also = [SectionLink "Flag calls"; Link "opt_info";
                 Link "add_meta_context";
                 Link "block_status"; Link
"aio_block_status"];
   };
@@ -1934,6 +1961,29 @@ callback.";
     see_also = [Link "set_opt_mode"; Link "opt_list"];
   };

+  "aio_opt_info", {
+    default_call with
+    args = [];
+    optargs = [ OClosure completion_closure ];
+    ret = RErr;
+    permitted_states = [ Negotiating ];
+    shortdesc = "request the server for information about an export";
+    longdesc = "\
+Request that the server supply information about the most recent
+export name set by L<nbd_set_export_name(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>.  Note that direct detection of
+whether the server returns an error (as is done by the return value
+of the synchronous counterpart) is only possible with a completion
+callback; however, indirect detection of failure is possible by
+checking whether L<nbd_is_read_only(3)> and similar also fail.";
+    see_also = [Link "set_opt_mode"; Link "opt_info"; Link
"is_read_only"];
+  };
+
   "aio_pread", {
     default_call with
     args = [ BytesPersistOut ("buf", "count"); UInt64
"offset" ];
@@ -2553,9 +2603,11 @@ let first_version = [
   "opt_go", (1, 4);
   "opt_abort", (1, 4);
   "opt_list", (1, 4);
+  "opt_info", (1, 4);
   "aio_opt_go", (1, 4);
   "aio_opt_abort", (1, 4);
   "aio_opt_list", (1, 4);
+  "aio_opt_info", (1, 4);

   (* 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-go.c
b/generator/states-newstyle-opt-go.c
index 95cc041..54e83ab 100644
--- a/generator/states-newstyle-opt-go.c
+++ b/generator/states-newstyle-opt-go.c
@@ -23,8 +23,12 @@ STATE_MACHINE {
   uint16_t nrinfos = h->full_info ? 3 : 1;

   assert (h->gflags & LIBNBD_HANDSHAKE_FLAG_FIXED_NEWSTYLE);
+  if (h->current_opt == NBD_OPT_INFO)
+    assert (h->opt_mode);
+  else
+    assert (h->current_opt == NBD_OPT_GO);
   h->sbuf.option.version = htobe64 (NBD_NEW_VERSION);
-  h->sbuf.option.option = htobe32 (NBD_OPT_GO);
+  h->sbuf.option.option = htobe32 (h->current_opt);
   h->sbuf.option.optlen      htobe32 (/* exportnamelen */ 4 + strlen
(h->export_name)
              + sizeof nrinfos + 2 * nrinfos);
@@ -101,7 +105,7 @@ STATE_MACHINE {
   switch (recv_into_rbuf (h)) {
   case -1: SET_NEXT_STATE (%.DEAD); return 0;
   case 0:
-    if (prepare_for_reply_payload (h, NBD_OPT_GO) == -1) {
+    if (prepare_for_reply_payload (h, h->current_opt) == -1) {
       SET_NEXT_STATE (%.DEAD);
       return 0;
     }
@@ -206,9 +210,12 @@ STATE_MACHINE {
     SET_NEXT_STATE (%RECV_REPLY);
     return 0;
   case NBD_REP_ERR_UNSUP:
-    debug (h, "server is confused by NBD_OPT_GO, continuing anyway");
-    SET_NEXT_STATE (%^OPT_EXPORT_NAME.START);
-    return 0;
+    if (h->current_opt == NBD_OPT_GO) {
+      debug (h, "server is confused by NBD_OPT_GO, continuing
anyway");
+      SET_NEXT_STATE (%^OPT_EXPORT_NAME.START);
+      return 0;
+    }
+    /* fallthrough */
   default:
     if (handle_reply_error (h) == -1) {
       SET_NEXT_STATE (%.DEAD);
@@ -216,6 +223,10 @@ STATE_MACHINE {
     }
     /* Decode expected known errors into a nicer string */
     switch (reply) {
+    case NBD_REP_ERR_UNSUP:
+      assert (h->current_opt == NBD_OPT_INFO);
+      set_error (ENOTSUP, "handshake: server lacks NBD_OPT_INFO
support");
+      break;
     case NBD_REP_ERR_POLICY:
     case NBD_REP_ERR_PLATFORM:
       set_error (0, "handshake: server policy prevents NBD_OPT_GO");
@@ -242,21 +253,21 @@ STATE_MACHINE {
                  reply);
     }
     nbd_internal_reset_size_and_flags (h);
-    err = nbd_get_errno ();
+    err = nbd_get_errno () ? : ENOTSUP;
     break;
   case NBD_REP_ACK:
     err = 0;
     break;
   }

-  CALL_CALLBACK (h->opt_cb.completion, &err);
-  nbd_internal_free_option (h);
-  if (err == 0)
+  if (err == 0 && h->current_opt == NBD_OPT_GO)
     SET_NEXT_STATE (%^FINISHED);
   else if (h->opt_mode)
     SET_NEXT_STATE (%.NEGOTIATING);
   else
     SET_NEXT_STATE (%^PREPARE_OPT_ABORT);
+  CALL_CALLBACK (h->opt_cb.completion, &err);
+  nbd_internal_free_option (h);
   return 0;

 } /* END STATE MACHINE */
diff --git a/lib/opt.c b/lib/opt.c
index 17f2508..d0b9f78 100644
--- a/lib/opt.c
+++ b/lib/opt.c
@@ -87,6 +87,23 @@ nbd_unlocked_opt_go (struct nbd_handle *h)
   return r;
 }

+/* Issue NBD_OPT_INFO and wait for the reply. */
+int
+nbd_unlocked_opt_info (struct nbd_handle *h)
+{
+  int err;
+  nbd_completion_callback c = { .callback = go_complete, .user_data = &err
};
+  int r = nbd_unlocked_aio_opt_info (h, c);
+
+  if (r == -1)
+    return r;
+
+  r = wait_for_option (h);
+  if (r == 0)
+    r = err;
+  return r;
+}
+
 /* Issue NBD_OPT_ABORT and wait for the state change. */
 int
 nbd_unlocked_opt_abort (struct nbd_handle *h)
@@ -155,6 +172,24 @@ nbd_unlocked_aio_opt_go (struct nbd_handle *h,
   return 0;
 }

+/* Issue NBD_OPT_INFO without waiting. */
+int
+nbd_unlocked_aio_opt_info (struct nbd_handle *h,
+                           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;
+  }
+
+  h->current_opt = NBD_OPT_INFO;
+  h->opt_cb.completion = complete;
+
+  if (nbd_internal_run (h, cmd_issue) == -1)
+    debug (h, "option queued, ignoring state machine failure");
+  return 0;
+}
+
 /* Issue NBD_OPT_ABORT without waiting. */
 int
 nbd_unlocked_aio_opt_abort (struct nbd_handle *h)
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 76b370a..81cd30e 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -164,6 +164,7 @@ check_PROGRAMS += \
 	oldstyle \
 	newstyle-limited \
 	opt-abort \
+	opt-info \
 	connect-unix \
 	connect-tcp \
 	aio-parallel \
@@ -200,6 +201,7 @@ TESTS += \
 	oldstyle \
 	newstyle-limited \
 	opt-abort \
+	opt-info \
 	connect-unix \
 	connect-tcp \
 	aio-parallel.sh \
@@ -380,6 +382,11 @@ opt_abort_CPPFLAGS = -I$(top_srcdir)/include
 opt_abort_CFLAGS = $(WARNINGS_CFLAGS)
 opt_abort_LDADD = $(top_builddir)/lib/libnbd.la

+opt_info_SOURCES = opt-info.c
+opt_info_CPPFLAGS = -I$(top_srcdir)/include
+opt_info_CFLAGS = $(WARNINGS_CFLAGS)
+opt_info_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/newstyle-limited.c b/tests/newstyle-limited.c
index ab6c1c2..ce62bdb 100644
--- a/tests/newstyle-limited.c
+++ b/tests/newstyle-limited.c
@@ -152,6 +152,14 @@ main (int argc, char *argv[])
     fprintf (stderr, "%s: wrong errno value after failed opt_list\n",
argv[0]);
     exit (EXIT_FAILURE);
   }
+  if (nbd_opt_info (nbd) != -1) {
+    fprintf (stderr, "nbd_opt_info: expected failure\n");
+    exit (EXIT_FAILURE);
+  }
+  if (nbd_get_errno () != ENOTSUP) {
+    fprintf (stderr, "%s: wrong errno value after failed opt_info\n",
argv[0]);
+    exit (EXIT_FAILURE);
+  }
   if (nbd_opt_abort (nbd) == -1) {
     fprintf (stderr, "%s\n", nbd_get_error ());
     exit (EXIT_FAILURE);
diff --git a/tests/opt-info.c b/tests/opt-info.c
new file mode 100644
index 0000000..d237351
--- /dev/null
+++ b/tests/opt-info.c
@@ -0,0 +1,107 @@
+/* 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_info. */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <inttypes.h>
+#include <string.h>
+#include <errno.h>
+
+#include <libnbd.h>
+
+int
+main (int argc, char *argv[])
+{
+  struct nbd_handle *nbd;
+  int64_t r;
+  const char *s;
+  /* A rather convoluted plugin: export '' is advertised but
+   * unavailable, export 'a' is unadvertised but available,
+   * export 'b' is unadvertised and unavailable, and all
+   * other exports are unadvertised but available.  Available
+   * exports have a size matching the name length.
+   */
+  char *args[] = { "nbdkit", "-s",
"--exit-with-parent", "-v",
+                   "memory", "size=512", NULL };
+
+  /* Quick check that nbdkit is new enough */
+  if (system ("nbdkit eval --dump-plugin | grep -q
has_list_exports=1")) {
+    fprintf (stderr, "skipping: nbdkit too old for this test\n");
+    exit (77);
+  }
+
+  /* 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);
+  }
+
+  /* Protocol should be "newstyle-fixed", with structured replies
already
+   * negotiated.
+   */
+  if (nbd_aio_is_negotiating (nbd) != true) {
+    fprintf (stderr, "unexpected state after connection\n");
+    exit (EXIT_FAILURE);
+  }
+  s = nbd_get_protocol (nbd);
+  if (strcmp (s, "newstyle-fixed") != 0) {
+    fprintf (stderr,
+             "incorrect protocol \"%s\", expected
\"newstyle-fixed\"\n", s);
+    exit (EXIT_FAILURE);
+  }
+  if ((r = nbd_get_structured_replies_negotiated (nbd)) != 1) {
+    fprintf (stderr,
+             "incorrect structured replies %" PRId64 ", expected
1\n", r);
+    exit (EXIT_FAILURE);
+  }
+
+  /* Quitting negotiation should be graceful. */
+  if (nbd_opt_abort (nbd) == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  if (nbd_aio_is_closed (nbd) != true) {
+    fprintf (stderr, "unexpected state after abort\n");
+    exit (EXIT_FAILURE);
+  }
+
+  /* As negotiation never finished, we have no size. */
+  if ((r = nbd_get_size (nbd)) != -1) {
+    fprintf (stderr, "%s: test failed: incorrect size, "
+             "actual %" PRIi64 ", expected -1",
+             argv[0], r);
+    exit (EXIT_FAILURE);
+  }
+  if ((r = nbd_get_errno ()) != EINVAL) {
+    fprintf (stderr, "%s: test failed: unexpected errno, "
+             "actual %" PRIi64 ", expected %d EINVAL",
+             argv[0], r, EINVAL);
+    exit (EXIT_FAILURE);
+  }
+
+  nbd_close (nbd);
+  exit (EXIT_SUCCESS);
+}
diff --git a/.gitignore b/.gitignore
index 6fdb362..1e49413 100644
--- a/.gitignore
+++ b/.gitignore
@@ -173,6 +173,7 @@ Makefile.in
 /tests/newstyle-limited
 /tests/oldstyle
 /tests/opt-abort
+/tests/opt-info
 /tests/pki/
 /tests/read-only-flag
 /tests/read-write-flag
--
2.28.0