[Libguestfs] [PATCH nbdkit v2 03/10] python: Implement nbdkit API version 2.

Richard W.M. Jones
Fri, 22 Nov 2019 19:53:58 -0000

To avoid breaking existing plugins, Python plugins wishing to use
version 2 of the API must opt in by declaring:

  def api_version():
    return 2

(Plugins which do not do this are assumed to want API version 1).
---
 plugins/python/example.py               |  15 +++-
 plugins/python/nbdkit-python-plugin.pod |  61 ++++++++++-----
 plugins/python/python.c                 | 100 ++++++++++++++++++++----
 tests/test.py                           |  20 +++--
 4 files changed, 148 insertions(+), 48 deletions(-)

diff --git a/plugins/python/example.py b/plugins/python/example.py
index 60f9d7f..25a0049 100644
--- a/plugins/python/example.py
+++ b/plugins/python/example.py
@@ -34,6 +34,13 @@ import errno
 disk = bytearray(1024 * 1024)


+# There are several variants of the API.  nbdkit will call this
+# function first to determine which one you want to use.  This is the
+# latest version at the time this example was written.
+def api_version():
+    return 2
+
+
 # This just prints the extra command line parameters, but real plugins
 # should parse them and reject any unknown parameters.
 def config(key, value):
@@ -54,20 +61,20 @@ def get_size(h):
     return len(disk)


-def pread(h, count, offset):
+def pread(h, count, offset, flags):
     global disk
     return disk[offset:offset+count]


-def pwrite(h, buf, offset):
+def pwrite(h, buf, offset, flags):
     global disk
     end = offset + len(buf)
     disk[offset:end] = buf


-def zero(h, count, offset, may_trim):
+def zero(h, count, offset, flags):
     global disk
-    if may_trim:
+    if flags & nbdkit.FLAG_MAY_TRIM:
         disk[offset:offset+count] = bytearray(count)
     else:
         nbdkit.set_error(errno.EOPNOTSUPP)
diff --git a/plugins/python/nbdkit-python-plugin.pod
b/plugins/python/nbdkit-python-plugin.pod
index 6453474..882c0d8 100644
--- a/plugins/python/nbdkit-python-plugin.pod
+++ b/plugins/python/nbdkit-python-plugin.pod
@@ -82,6 +82,19 @@ I<--dump-plugin> option, eg:
  python_version=3.7.0
  python_pep_384_abi_version=3

+=head2 API versions
+
+The nbdkit API has evolved and new versions are released periodically.
+To ensure backwards compatibility plugins have to opt in to the new
+version.  From Python you do this by declaring a function in your
+module:
+
+ def api_version():
+   return 2
+
+(where 2 is the latest version at the time this documentation was
+written).  All newly written Python modules must have this function.
+
 =head2 Executable script

 If you want you can make the script executable and include a
"shebang"
@@ -120,6 +133,10 @@ nbdkit-plugin(3).

 =over 4

+=item C<api_version>
+
+There are no arguments.  It must return C<2>.
+
 =item C<dump_plugin>

 (Optional)
@@ -199,12 +216,12 @@ contents will be garbage collected.

 (Required)

- def pread(h, count, offset):
+ def pread(h, count, offset, flags):
    # construct a bytearray of length count bytes and return it

 The body of your C<pread> function should construct a buffer of length
 (at least) C<count> bytes.  You should read C<count> bytes from the
-disk starting at C<offset>.
+disk starting at C<offset>.  C<flags> is always 0.

 NBD only supports whole reads, so your function should try to read
 the whole region (perhaps requiring a loop).  If the read fails or
@@ -215,13 +232,13 @@ C<nbdkit.set_error> first.

 (Optional)

- def pwrite(h, buf, offset):
+ def pwrite(h, buf, offset, flags):
    length = len (buf)
    # no return value

 The body of your C<pwrite> function should write the C<buf> string
to
 the disk.  You should write C<count> bytes to the disk starting at
-C<offset>.
+C<offset>.  C<flags> may contain C<nbdkit.FLAG_FUA>.

 NBD only supports whole writes, so your function should try to
 write the whole region (perhaps requiring a loop).  If the write
@@ -232,11 +249,12 @@ fails or is partial, your function should throw an
exception,

 (Optional)

- def flush(h):
+ def flush(h, flags):
    # no return value

 The body of your C<flush> function should do a L<sync(2)> or
 L<fdatasync(2)> or equivalent on the backing store.
+C<flags> is always 0.

 If the flush fails, your function should throw an exception, optionally
 using C<nbdkit.set_error> first.
@@ -245,32 +263,35 @@ using C<nbdkit.set_error> first.

 (Optional)

- def trim(h, count, offset):
+ def trim(h, count, offset, flags):
    # no return value

-The body of your C<trim> function should "punch a hole" in the
-backing store.  If the trim fails, your function should throw an
-exception, optionally using C<nbdkit.set_error> first.
+The body of your C<trim> function should "punch a hole" in the
backing
+store.  C<flags> may contain C<nbdkit.FLAG_FUA>.  If the trim
fails,
+your function should throw an exception, optionally using
+C<nbdkit.set_error> first.

 =item C<zero>

 (Optional)

- def zero(h, count, offset, may_trim):
+ def zero(h, count, offset, flags):
    # no return value

-The body of your C<zero> function should ensure that C<count> bytes
-of the disk, starting at C<offset>, will read back as zero.  If
-C<may_trim> is true, the operation may be optimized as a trim as long
-as subsequent reads see zeroes.
+The body of your C<zero> function should ensure that C<count> bytes
of
+the disk, starting at C<offset>, will read back as zero.  C<flags>
is
+a bitmask which may include C<nbdkit.FLAG_MAY_TRIM>,
+C<nbdkit.FLAG_FUA>, C<nbdkit.FLAG_FAST_ZERO>.

 NBD only supports whole writes, so your function should try to
-write the whole region (perhaps requiring a loop).  If the write
-fails or is partial, your function should throw an exception,
-optionally using C<nbdkit.set_error> first.  In particular, if
-you would like to automatically fall back to C<pwrite> (perhaps
-because there is nothing to optimize if C<may_trim> is false),
-use C<nbdkit.set_error(errno.EOPNOTSUPP)>.
+write the whole region (perhaps requiring a loop).
+
+If the write fails or is partial, your function should throw an
+exception, optionally using C<nbdkit.set_error> first.  In particular,
+if you would like to automatically fall back to C<pwrite> (perhaps
+because there is nothing to optimize if
+S<C<flags & nbdkit.FLAG_MAY_TRIM>> is false), use
+S<C<nbdkit.set_error (errno.EOPNOTSUPP)>>.

 =back

diff --git a/plugins/python/python.c b/plugins/python/python.c
index 69cf4e9..20e05e0 100644
--- a/plugins/python/python.c
+++ b/plugins/python/python.c
@@ -46,6 +46,8 @@
 #define PY_SSIZE_T_CLEAN 1
 #include <Python.h>

+#define NBDKIT_API_VERSION 2
+
 #include <nbdkit-plugin.h>

 #include "cleanup.h"
@@ -60,6 +62,7 @@
  */
 static const char *script;
 static PyObject *module;
+static int py_api_version = 1;

 static int last_error;

@@ -356,6 +359,26 @@ py_config (const char *key, const char *value)
                     "nbdkit requires these callbacks.", script);
       return -1;
     }
+
+    /* Get the API version. */
+    if (callback_defined ("api_version", &fn)) {
+      PyErr_Clear ();
+
+      r = PyObject_CallObject (fn, NULL);
+      Py_DECREF (fn);
+      if (check_python_failure ("api_version") == -1)
+        return -1;
+      py_api_version = (int) PyLong_AsLong (r);
+      Py_DECREF (r);
+      if (check_python_failure ("PyLong_AsLong") == -1)
+        return -1;
+      if (py_api_version < 1 || py_api_version > NBDKIT_API_VERSION) {
+        nbdkit_error ("%s: api_version() requested unknown version: %d.
"
+                      "This plugin supports API versions between 1 and
%d.",
+                      script, py_api_version, NBDKIT_API_VERSION);
+        return -1;
+      }
+    }
   }
   else if (callback_defined ("config", &fn)) {
     /* Other parameters are passed to the Python .config callback. */
@@ -466,8 +489,8 @@ py_get_size (void *handle)
 }

 static int
-py_pread (void *handle, void *buf,
-          uint32_t count, uint64_t offset)
+py_pread (void *handle, void *buf, uint32_t count, uint64_t offset,
+          uint32_t flags)
 {
   PyObject *obj = handle;
   PyObject *fn;
@@ -480,7 +503,15 @@ py_pread (void *handle, void *buf,

   PyErr_Clear ();

-  r = PyObject_CallFunction (fn, "OiL", obj, count, offset, NULL);
+  switch (py_api_version) {
+  case 1:
+    r = PyObject_CallFunction (fn, "OiL", obj, count, offset, NULL);
+    break;
+  case 2:
+    r = PyObject_CallFunction (fn, "OiLI", obj, count, offset, flags,
NULL);
+    break;
+  default: abort ();
+  }
   Py_DECREF (fn);
   if (check_python_failure ("pread") == -1)
     return -1;
@@ -505,8 +536,8 @@ py_pread (void *handle, void *buf,
 }

 static int
-py_pwrite (void *handle, const void *buf,
-           uint32_t count, uint64_t offset)
+py_pwrite (void *handle, const void *buf, uint32_t count, uint64_t offset,
+           uint32_t flags)
 {
   PyObject *obj = handle;
   PyObject *fn;
@@ -515,9 +546,19 @@ py_pwrite (void *handle, const void *buf,
   if (callback_defined ("pwrite", &fn)) {
     PyErr_Clear ();

-    r = PyObject_CallFunction (fn, "ONL", obj,
-                               PyByteArray_FromStringAndSize (buf, count),
-                               offset, NULL);
+    switch (py_api_version) {
+    case 1:
+      r = PyObject_CallFunction (fn, "ONL", obj,
+                                 PyByteArray_FromStringAndSize (buf, count),
+                                 offset, NULL);
+      break;
+    case 2:
+      r = PyObject_CallFunction (fn, "ONLI", obj,
+                                 PyByteArray_FromStringAndSize (buf, count),
+                                 offset, flags, NULL);
+      break;
+    default: abort ();
+    }
     Py_DECREF (fn);
     if (check_python_failure ("pwrite") == -1)
       return -1;
@@ -532,7 +573,7 @@ py_pwrite (void *handle, const void *buf,
 }

 static int
-py_flush (void *handle)
+py_flush (void *handle, uint32_t flags)
 {
   PyObject *obj = handle;
   PyObject *fn;
@@ -541,7 +582,15 @@ py_flush (void *handle)
   if (callback_defined ("flush", &fn)) {
     PyErr_Clear ();

-    r = PyObject_CallFunctionObjArgs (fn, obj, NULL);
+    switch (py_api_version) {
+    case 1:
+      r = PyObject_CallFunctionObjArgs (fn, obj, NULL);
+      break;
+    case 2:
+      r = PyObject_CallFunction (fn, "OI", obj, flags, NULL);
+      break;
+    default: abort ();
+    }
     Py_DECREF (fn);
     if (check_python_failure ("flush") == -1)
       return -1;
@@ -556,7 +605,7 @@ py_flush (void *handle)
 }

 static int
-py_trim (void *handle, uint32_t count, uint64_t offset)
+py_trim (void *handle, uint32_t count, uint64_t offset, uint32_t flags)
 {
   PyObject *obj = handle;
   PyObject *fn;
@@ -565,7 +614,15 @@ py_trim (void *handle, uint32_t count, uint64_t offset)
   if (callback_defined ("trim", &fn)) {
     PyErr_Clear ();

-    r = PyObject_CallFunction (fn, "OiL", obj, count, offset, NULL);
+    switch (py_api_version) {
+    case 1:
+      r = PyObject_CallFunction (fn, "OiL", obj, count, offset,
NULL);
+      break;
+    case 2:
+      r = PyObject_CallFunction (fn, "OiLI", obj, count, offset,
flags, NULL);
+      break;
+    default: abort ();
+    }
     Py_DECREF (fn);
     if (check_python_failure ("trim") == -1)
       return -1;
@@ -580,7 +637,7 @@ py_trim (void *handle, uint32_t count, uint64_t offset)
 }

 static int
-py_zero (void *handle, uint32_t count, uint64_t offset, int may_trim)
+py_zero (void *handle, uint32_t count, uint64_t offset, uint32_t flags)
 {
   PyObject *obj = handle;
   PyObject *fn;
@@ -590,9 +647,20 @@ py_zero (void *handle, uint32_t count, uint64_t offset, int
may_trim)
     PyErr_Clear ();

     last_error = 0;
-    r = PyObject_CallFunction (fn, "OiLO",
-                               obj, count, offset,
-                               may_trim ? Py_True : Py_False, NULL);
+    switch (py_api_version) {
+    case 1: {
+      int may_trim = flags & NBDKIT_FLAG_MAY_TRIM;
+      r = PyObject_CallFunction (fn, "OiLO",
+                                 obj, count, offset,
+                                 may_trim ? Py_True : Py_False, NULL);
+      break;
+    }
+    case 2:
+      r = PyObject_CallFunction (fn, "OiLI",
+                                 obj, count, offset, flags, NULL);
+      break;
+    default: abort ();
+    }
     Py_DECREF (fn);
     if (last_error == EOPNOTSUPP || last_error == ENOTSUP) {
       /* When user requests this particular error, we want to
diff --git a/tests/test.py b/tests/test.py
index 9a2e947..ac80d96 100644
--- a/tests/test.py
+++ b/tests/test.py
@@ -3,6 +3,10 @@ import nbdkit
 disk = bytearray(1024*1024)


+def api_version():
+    return 2
+
+
 def config_complete():
     print ("set_error = %r" % nbdkit.set_error)

@@ -32,25 +36,25 @@ def can_trim(h):
     return True


-def pread(h, count, offset):
+def pread(h, count, offset, flags):
     global disk
     return disk[offset:offset+count]


-def pwrite(h, buf, offset):
+def pwrite(h, buf, offset, flags):
     global disk
     end = offset + len(buf)
     disk[offset:end] = buf


-def zero(h, count, offset, may_trim=False):
-    global disk
-    disk[offset:offset+count] = bytearray(count)
+def flush(h, flags):
+    pass


-def flush(h):
+def trim(h, count, offset, flags):
     pass


-def trim(h, count, offset):
-    pass
+def zero(h, count, offset, flags):
+    global disk
+    disk[offset:offset+count] = bytearray(count)
--
2.23.0