[Libguestfs] [libnbd PATCH 7/8] states: Add tests for nbd_pread_callback

Eric Blake
Tue, 18 Jun 2019 00:07:57 -0000

With a new enough qemu-nbd and a file system that supports a hole, we
can test that nbd_pread_callback for sane operation with
interop/structured-read.

It is also possible to test that the callback behaves sanely even for
a connection lacks structured replies, using nbdkit in tests/oldstyle.
---
 .gitignore                     |   1 +
 interop/Makefile.am            |  11 ++-
 interop/structured-read.c      | 165 +++++++++++++++++++++++++++++++++
 interop/structured-read.sh     |  57 ++++++++++++
 python/t/405-pread-callback.py |  36 +++++++
 tests/oldstyle.c               |  74 ++++++++++++++-
 6 files changed, 339 insertions(+), 5 deletions(-)
 create mode 100644 interop/structured-read.c
 create mode 100755 interop/structured-read.sh
 create mode 100644 python/t/405-pread-callback.py

diff --git a/.gitignore b/.gitignore
index 30438c1..ea496ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,6 +53,7 @@ Makefile.in
 /interop/interop-qemu-nbd
 /interop/interop-qemu-nbd-tls-certs
 /interop/interop-qemu-nbd-tls-psk
+/interop/structured-read
 /lib/api.c
 /lib/libnbd.pc
 /lib/libnbd.syms
diff --git a/interop/Makefile.am b/interop/Makefile.am
index eb4b52b..6e1156d 100644
--- a/interop/Makefile.am
+++ b/interop/Makefile.am
@@ -47,10 +47,12 @@ check_PROGRAMS += \
 	interop-qemu-nbd \
 	interop-qemu-nbd-tls-certs \
 	interop-qemu-nbd-tls-psk \
-	dirty-bitmap
+	dirty-bitmap \
+	structured-read
 TESTS += \
 	interop-qemu-nbd \
-	dirty-bitmap.sh
+	dirty-bitmap.sh \
+	structured-read.sh

 # tls tests assume the pre-existence of files created in ../tests/Makefile.am,
 # so we can only run them under the same conditions used there
@@ -107,6 +109,11 @@ dirty_bitmap_CPPFLAGS = -I$(top_srcdir)/include
 dirty_bitmap_CFLAGS = $(WARNINGS_CFLAGS)
 dirty_bitmap_LDADD = $(top_builddir)/lib/libnbd.la

+structured_read_SOURCES = structured-read.c
+structured_read_CPPFLAGS = -I$(top_srcdir)/include
+structured_read_CFLAGS = $(WARNINGS_CFLAGS)
+structured_read_LDADD = $(top_builddir)/lib/libnbd.la
+
 endif HAVE_QEMU_NBD

 check-valgrind:
diff --git a/interop/structured-read.c b/interop/structured-read.c
new file mode 100644
index 0000000..b740e98
--- /dev/null
+++ b/interop/structured-read.c
@@ -0,0 +1,165 @@
+/* NBD client library in userspace
+ * Copyright (C) 2013-2019 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 structured reply read callback. */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <assert.h>
+#include <stdbool.h>
+#include <errno.h>
+
+#include <libnbd.h>
+
+static const char *unixsocket;
+
+/* Depends on structured-read.sh setting things up so that qemu-nbd
+ * exposes an image with a 512-byte hole at offset 2048 followed by a
+ * 512-byte data section containing all '1' bytes at offset 2560
+ * (non-zero offsets to test that everything is calculated correctly).
+ */
+static char rbuf[1024];
+
+struct data {
+  //XXX  bool df;    /* input: true if DF flag was passed to request */
+  int count;       /* input: count of expected remaining calls */
+  bool fail;       /* input: true to return failure */
+  bool seen_hole;  /* output: true if hole encountered */
+  bool seen_data;  /* output: true if data encountered */
+};
+
+static int
+read_cb (void *opaque, const void *bufv, size_t count, uint64_t offset,
+         int status)
+{
+  struct data *data = opaque;
+  const char *buf = bufv;
+
+  /* The NBD spec allows chunks to be reordered; we are relying on the
+   * fact that qemu-nbd does not do so.
+   */
+  assert (data->count-- > 0);
+
+  switch (status) {
+  case LIBNBD_READ_DATA:
+    // XXX if (df...)
+    assert (buf == rbuf + 512);
+    assert (count == 512);
+    assert (offset == 2048 + 512);
+    assert (buf[0] == 1 && memcmp (buf, buf + 1, 511) == 0);
+    assert (!data->seen_data);
+    data->seen_data = true;
+    break;
+  case LIBNBD_READ_HOLE:
+    assert (buf == rbuf);
+    assert (count == 512);
+    assert (offset == 2048);
+    assert (buf[0] == 0 && memcmp (buf, buf + 1, 511) == 0);
+    assert (!data->seen_hole);
+    data->seen_hole = true;
+    break;
+  case LIBNBD_READ_ERROR:
+    /* For now, qemu-nbd cannot provoke this status. */
+  default:
+    assert (false);
+  }
+
+  if (data->fail) {
+    /* Something NBD servers can't send */
+    errno = data->count == 1 ? EPROTO : ECONNREFUSED;
+    return -1;
+  }
+  return 0;
+}
+
+int
+main (int argc, char *argv[])
+{
+  struct nbd_handle *nbd;
+  int64_t exportsize;
+  struct data data;
+  char c;
+
+  if (argc != 2) {
+    fprintf (stderr, "%s unixsocket\n", argv[0]);
+    exit (EXIT_FAILURE);
+  }
+  unixsocket = argv[1];
+
+  nbd = nbd_create ();
+  if (nbd == NULL) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+
+  if (nbd_connect_unix (nbd, unixsocket) == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+
+  exportsize = nbd_get_size (nbd);
+  if (exportsize == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  if (exportsize != 3072) {
+    fprintf (stderr, "unexpected file size\n");
+    exit (EXIT_FAILURE);
+  }
+
+  memset (rbuf, 2, sizeof rbuf);
+  data = (struct data) { .count = 2, };
+  if (nbd_pread_callback (nbd, rbuf, sizeof rbuf, 2048, &data, read_cb,
+                          0) == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+  assert (data.seen_data && data.seen_hole);
+
+  // XXX Repeat with DF flag
+
+  /* Trigger a failed callback, to prove connection stays up. With
+   * reads, all chunks trigger a callback even after failure, but the
+   * first errno sticks.
+   */
+  memset (rbuf, 2, sizeof rbuf);
+  data = (struct data) { .count = 2, .fail = true, };
+  if (nbd_pread_callback (nbd, rbuf, sizeof rbuf, 2048, &data, read_cb,
+                          0) != -1) {
+    fprintf (stderr, "unexpected pread callback success\n");
+    exit (EXIT_FAILURE);
+  }
+  assert (nbd_get_errno () == EPROTO && nbd_aio_is_ready (nbd));
+  assert (data.seen_data && data.seen_hole);
+
+  if (nbd_pread (nbd, &c, 1, 0, 0) == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+
+  if (nbd_shutdown (nbd) == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+
+  nbd_close (nbd);
+
+  exit (EXIT_SUCCESS);
+}
diff --git a/interop/structured-read.sh b/interop/structured-read.sh
new file mode 100755
index 0000000..15a81c0
--- /dev/null
+++ b/interop/structured-read.sh
@@ -0,0 +1,57 @@
+#!/usr/bin/env bash
+# nbd client library in userspace
+# Copyright (C) 2019 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 structured read callbacks.
+
+source ../tests/functions.sh
+set -e
+set -x
+
+requires qemu-img --version
+requires qemu-io --version
+requires qemu-nbd --version
+
+files="structured-read.sock structured-read.qcow2"
+rm -f $files
+cleanup_fn rm -f $files
+
+# Create file with cluster size 512 and contents all 1 except for single
+# 512-byte hole at offset 2048
+qemu-img create -f qcow2 -o cluster_size=512,compat=v3 structured-read.qcow2 3k
+qemu-io -d unmap -f qcow2 -c 'w -P 1 0 3k' -c 'w -zu 2k 512' \
+	structured-read.qcow2
+
+qemu-nbd -k $PWD/structured-read.sock -f qcow2 structured-read.qcow2 &
+qemu_pid=$!
+cleanup_fn kill $qemu_pid
+
+# qemu-nbd --pid not available before 4.1, so ...
+for ((i = 0; i < 300; i++)); do
+  if [ -r $PWD/structured-read.sock ]; then
+    break
+  fi
+  kill -s 0 $qemu_pid 2>/dev/null
+  if test $? != 0; then
+    echo "qemu-nbd unexpectedly quit" 2>&1
+    exit 1
+  fi
+  sleep 0.1
+done
+
+# Run the test.
+$VG ./structured-read structured-read.sock
diff --git a/python/t/405-pread-callback.py b/python/t/405-pread-callback.py
new file mode 100644
index 0000000..7946dac
--- /dev/null
+++ b/python/t/405-pread-callback.py
@@ -0,0 +1,36 @@
+# libnbd Python bindings
+# Copyright (C) 2010-2019 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
+
+h = nbd.NBD ()
+h.connect_command (["nbdkit", "-s",
"--exit-with-parent", "-v",
+                    "pattern", "size=512"])
+
+expected =
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08\x00\x00\x00\x00\x00\x00\x00\x10\x00\x00\x00\x00\x00\x00\x00\x18\x00\x00\x00\x00\x00\x00\x00
\x00\x00\x00\x00\x00\x00\x00(\x00\x00\x00\x00\x00\x00\x000\x00\x00\x00\x00\x00\x00\x008\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00H\x00\x00\x00\x00\x00\x00\x00P\x00\x00\x00\x00\x00\x00\x00X\x00\x00\x00\x00\x00\x00\x00`\x00\x00\x00\x00\x00\x00\x00h\x00\x00\x00\x00\x00\x00\x00p\x00\x00\x00\x00\x00\x00\x00x\x00\x00\x00\x00\x00\x00\x00\x80\x00\x00\x00\x00\x00\x00\x00\x88\x00\x00\x00\x00\x00\x00\x00\x90\x00\x00\x00\x00\x00\x00\x00\x98\x00\x00\x00\x00\x00\x00\x00\xa0\x00\x00\x00\x00\x00\x00\x00\xa8\x00\x00\x00\x00\x00\x00\x00\xb0\x00\x00\x00\x00\x00\x00\x00\xb8\x00\x00\x00\x00\x00\x00\x00\xc0\x00\x00\x00\x00\x00\x00\x00\xc8\x00\x00\x00\x00\x00\x00\x00\xd0\x00\x00\x00\x00\x00\x00\x00\xd8\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00\xe8\x00\x00\x00\x00\x00\x00\x00\xf0\x00\x00\x00\x00\x00\x00\x00\xf8\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x08\x00\x00\x00\x00\x00\x00\x01\x10\x00\x00\x00\x00\x00\x00\x01\x18\x00\x00\x00\x00\x00\x00\x01
\x00\x00\x00\x00\x00\x00\x01(\x00\x00\x00\x00\x00\x00\x010\x00\x00\x00\x00\x00\x00\x018\x00\x00\x00\x00\x00\x00\x01@\x00\x00\x00\x00\x00\x00\x01H\x00\x00\x00\x00\x00\x00\x01P\x00\x00\x00\x00\x00\x00\x01X\x00\x00\x00\x00\x00\x00\x01`\x00\x00\x00\x00\x00\x00\x01h\x00\x00\x00\x00\x00\x00\x01p\x00\x00\x00\x00\x00\x00\x01x\x00\x00\x00\x00\x00\x00\x01\x80\x00\x00\x00\x00\x00\x00\x01\x88\x00\x00\x00\x00\x00\x00\x01\x90\x00\x00\x00\x00\x00\x00\x01\x98\x00\x00\x00\x00\x00\x00\x01\xa0\x00\x00\x00\x00\x00\x00\x01\xa8\x00\x00\x00\x00\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x00\x01\xb8\x00\x00\x00\x00\x00\x00\x01\xc0\x00\x00\x00\x00\x00\x00\x01\xc8\x00\x00\x00\x00\x00\x00\x01\xd0\x00\x00\x00\x00\x00\x00\x01\xd8\x00\x00\x00\x00\x00\x00\x01\xe0\x00\x00\x00\x00\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x00\x01\xf0\x00\x00\x00\x00\x00\x00\x01\xf8'
+
+def f (data, buf2, offset, s):
+    assert data == 42
+    assert buf2 == expected
+    assert offset == 0
+    assert s == nbd.READ_DATA
+
+buf = h.pread_callback (512, 0, 42, f)
+
+print ("%r" % buf)
+
+assert buf == expected
diff --git a/tests/oldstyle.c b/tests/oldstyle.c
index a0b594c..920136a 100644
--- a/tests/oldstyle.c
+++ b/tests/oldstyle.c
@@ -23,6 +23,7 @@
 #include <stdint.h>
 #include <inttypes.h>
 #include <string.h>
+#include <errno.h>

 #include <libnbd.h>

@@ -30,14 +31,52 @@
 #define XSTR(s) #s
 #define STR(s) XSTR(s)

+static char wbuf[512] = { 1, 2, 3, 4 }, rbuf[512];
+static const char *progname;
+
+static int
+pread_cb (void *data, const void *buf, size_t count, uint64_t offset,
+          int status)
+{
+  int *calls = data;
+  ++*calls;
+
+  if (buf != rbuf || count != sizeof rbuf) {
+    fprintf (stderr, "%s: callback called with wrong buffer\n",
progname);
+    exit (EXIT_FAILURE);
+  }
+  if (offset != 2 * sizeof rbuf) {
+    fprintf (stderr, "%s: callback called with wrong offset\n",
progname);
+    exit (EXIT_FAILURE);
+  }
+  if (status != LIBNBD_READ_DATA) {
+    fprintf (stderr, "%s: callback called with wrong status\n",
progname);
+    exit (EXIT_FAILURE);
+  }
+
+  if (memcmp (rbuf, wbuf, sizeof rbuf) != 0) {
+    fprintf (stderr, "%s: DATA INTEGRITY ERROR!\n", progname);
+    exit (EXIT_FAILURE);
+  }
+
+  if (*calls > 1) {
+    errno = EPROTO; /* Something NBD servers can't send */
+    return -1;
+  }
+
+  return 0;
+}
+
 int
 main (int argc, char *argv[])
 {
   struct nbd_handle *nbd;
-  char wbuf[512] = { 1, 2, 3, 4 }, rbuf[512];
   int64_t r;
   char *args[] = { "nbdkit", "-s", "-o",
"--exit-with-parent", "-v",
                    "memory", "size=" STR(SIZE), NULL };
+  int calls = 0;
+
+  progname = argv[0];

   nbd = nbd_create ();
   if (nbd == NULL) {
@@ -61,12 +100,13 @@ main (int argc, char *argv[])
     exit (EXIT_FAILURE);
   }

-  if (nbd_pwrite (nbd, wbuf, sizeof wbuf, 0, 0) == -1) {
+  /* Plain I/O */
+  if (nbd_pwrite (nbd, wbuf, sizeof wbuf, 2 * sizeof wbuf, 0) == -1) {
     fprintf (stderr, "%s\n", nbd_get_error ());
     exit (EXIT_FAILURE);
   }

-  if (nbd_pread (nbd, rbuf, sizeof rbuf, 0, 0) == -1) {
+  if (nbd_pread (nbd, rbuf, sizeof rbuf, 2 * sizeof rbuf, 0) == -1) {
     fprintf (stderr, "%s\n", nbd_get_error ());
     exit (EXIT_FAILURE);
   }
@@ -76,6 +116,34 @@ main (int argc, char *argv[])
     exit (EXIT_FAILURE);
   }

+  /* Test again for callback operation. */
+  memset (rbuf, 0, sizeof rbuf);
+  if (nbd_pread_callback (nbd, rbuf, sizeof rbuf, 2 * sizeof rbuf,
+                          &calls, pread_cb, 0) == -1) {
+    fprintf (stderr, "%s\n", nbd_get_error ());
+    exit (EXIT_FAILURE);
+  }
+
+  if (calls != 1) {
+    fprintf (stderr, "%s: callback called wrong number of times\n",
argv[0]);
+    exit (EXIT_FAILURE);
+  }
+  if (memcmp (rbuf, wbuf, sizeof rbuf) != 0) {
+    fprintf (stderr, "%s: DATA INTEGRITY ERROR!\n", argv[0]);
+    exit (EXIT_FAILURE);
+  }
+
+  /* Also test that callback errors are reflected correctly. */
+  if (nbd_pread_callback (nbd, rbuf, sizeof rbuf, 2 * sizeof rbuf,
+                          &calls, pread_cb, 0) != -1) {
+    fprintf (stderr, "%s: expected failure from callback\n",
argv[0]);
+    exit (EXIT_FAILURE);
+  }
+  if (nbd_get_errno () != EPROTO) {
+    fprintf (stderr, "%s: wrong errno value after failed callback\n",
argv[0]);
+    exit (EXIT_FAILURE);
+  }
+
   if (nbd_shutdown (nbd) == -1) {
     fprintf (stderr, "%s\n", nbd_get_error ());
     exit (EXIT_FAILURE);
--
2.20.1