Richard W.M. Jones
2019-Nov-25 10:03 UTC
[Libguestfs] [PATCH nbdkit v2 0/7] Implement nbdkit API v2 for Python plugins.
v3 was here: https://www.redhat.com/archives/libguestfs/2019-November/msg00209.html In v4: - Rebase on top of current master. Includes various fixes and updates required because of Nir's patches that went into master. - Fix api_version() -> API_VERSION in patch 2 noted previously on the mailing list. Rich.
Richard W.M. Jones
2019-Nov-25 10:03 UTC
[Libguestfs] [PATCH nbdkit v4 1/7] python: Add various constants to the API.
These are accessible from the plugin by: import nbdkit if flags & nbdkit.FLAG_MAY_TRIM: &c. Many (all?) of these are not yet useful for plugins, some will never be useful, but they only consume a tiny bit of memory and it's nice to have the complete set available for future use. --- plugins/python/python.c | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/plugins/python/python.c b/plugins/python/python.c index 2b4361a..ff96e0b 100644 --- a/plugins/python/python.c +++ b/plugins/python/python.c @@ -231,6 +231,36 @@ create_nbdkit_module (void) nbdkit_error ("could not create the nbdkit API module"); exit (EXIT_FAILURE); } + + /* Constants corresponding to various flags. */ +#define ADD_INT_CONSTANT(name) \ + if (PyModule_AddIntConstant (m, #name, NBDKIT_##name) == -1) { \ + nbdkit_error ("could not add constant %s to nbdkit API module", \ + #name); \ + exit (EXIT_FAILURE); \ + } + ADD_INT_CONSTANT (THREAD_MODEL_SERIALIZE_CONNECTIONS); + ADD_INT_CONSTANT (THREAD_MODEL_SERIALIZE_ALL_REQUESTS); + ADD_INT_CONSTANT (THREAD_MODEL_SERIALIZE_REQUESTS); + ADD_INT_CONSTANT (THREAD_MODEL_PARALLEL); + + ADD_INT_CONSTANT (FLAG_MAY_TRIM); + ADD_INT_CONSTANT (FLAG_FUA); + ADD_INT_CONSTANT (FLAG_REQ_ONE); + ADD_INT_CONSTANT (FLAG_FAST_ZERO); + + ADD_INT_CONSTANT (FUA_NONE); + ADD_INT_CONSTANT (FUA_EMULATE); + ADD_INT_CONSTANT (FUA_NATIVE); + + ADD_INT_CONSTANT (CACHE_NONE); + ADD_INT_CONSTANT (CACHE_EMULATE); + ADD_INT_CONSTANT (CACHE_NATIVE); + + ADD_INT_CONSTANT (EXTENT_HOLE); + ADD_INT_CONSTANT (EXTENT_ZERO); +#undef ADD_INT_CONSTANT + return m; } -- 2.23.0
Richard W.M. Jones
2019-Nov-25 10:03 UTC
[Libguestfs] [PATCH nbdkit v4 2/7] python: Implement nbdkit API version 2.
To avoid breaking existing plugins, Python plugins wishing to use version 2 of the API must opt in by declaring: API_VERSION = 2 (Plugins which do not do this are assumed to want API version 1). --- plugins/python/example.py | 14 ++- plugins/python/nbdkit-python-plugin.pod | 59 ++++++++----- plugins/python/python.c | 110 +++++++++++++++++++++--- tests/test.py | 19 ++-- 4 files changed, 155 insertions(+), 47 deletions(-) diff --git a/plugins/python/example.py b/plugins/python/example.py index 60f9d7f..c85d2f8 100644 --- a/plugins/python/example.py +++ b/plugins/python/example.py @@ -34,6 +34,12 @@ 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. +API_VERSION = 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 +60,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 3680fd6..fe2a542 100644 --- a/plugins/python/nbdkit-python-plugin.pod +++ b/plugins/python/nbdkit-python-plugin.pod @@ -33,11 +33,12 @@ To write a Python nbdkit plugin, you create a Python file which contains at least the following required functions (in the top level C<__main__> module): + API_VERSION = 2 def open(readonly): # see below def get_size(h): # see below - def pread(h, count, offset): + def pread(h, count, offset, flags): # see below Note that the subroutines must have those literal names (like C<open>), @@ -82,6 +83,18 @@ 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 constant in your +module: + + API_VERSION = 2 + +(where 2 is the latest version at the time this documentation was +written). All newly written Python modules must have this constant. + =head2 Executable script If you want you can make the script executable and include a "shebang" @@ -199,12 +212,12 @@ contents will be garbage collected. (Required) - def pread(h, count, offset): + def pread(h, count, offset, flags): # construct a buffer 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. The returned buffer can be any type compatible with the Python 3 buffer protocol, such as bytearray, bytes or memoryview @@ -219,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 buffer C<buf> 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 @@ -236,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. @@ -249,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 ff96e0b..68d01e8 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; @@ -285,9 +288,14 @@ py_dump_plugin (void) PyObject *fn; PyObject *r; + /* Python version and ABI. */ printf ("python_version=%s\n", PY_VERSION); printf ("python_pep_384_abi_version=%d\n", PYTHON_ABI_VERSION); + /* Maximum nbdkit API version supported. */ + printf ("nbdkit_python_maximum_api_version=%d\n", NBDKIT_API_VERSION); + + /* If the script has a dump_plugin function, call it. */ if (script && callback_defined ("dump_plugin", &fn)) { PyErr_Clear (); @@ -297,6 +305,30 @@ py_dump_plugin (void) } } +static int +get_py_api_version (void) +{ + PyObject *obj; + long value; + + obj = PyObject_GetAttrString (module, "API_VERSION"); + if (obj == NULL) + return 1; /* Default to API version 1. */ + + value = PyLong_AsLong (obj); + Py_DECREF (obj); + + if (value < 1 || value > NBDKIT_API_VERSION) { + nbdkit_error ("%s: API_VERSION requested unknown version: %ld. " + "This plugin supports API versions between 1 and %d.", + script, value, NBDKIT_API_VERSION); + return -1; + } + + nbdkit_debug ("module requested API_VERSION %ld", value); + return (int) value; +} + static int py_config (const char *key, const char *value) { @@ -359,6 +391,11 @@ py_config (const char *key, const char *value) "nbdkit requires these callbacks.", script); return -1; } + + /* Get the API version. */ + py_api_version = get_py_api_version (); + if (py_api_version == -1) + return -1; } else if (callback_defined ("config", &fn)) { /* Other parameters are passed to the Python .config callback. */ @@ -469,8 +506,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; @@ -485,7 +522,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 ret; @@ -515,8 +560,8 @@ out: } 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; @@ -525,9 +570,19 @@ py_pwrite (void *handle, const void *buf, if (callback_defined ("pwrite", &fn)) { PyErr_Clear (); - r = PyObject_CallFunction (fn, "ONL", obj, + switch (py_api_version) { + case 1: + r = PyObject_CallFunction (fn, "ONL", obj, PyMemoryView_FromMemory ((char *)buf, count, PyBUF_READ), offset, NULL); + break; + case 2: + r = PyObject_CallFunction (fn, "ONLI", obj, + PyMemoryView_FromMemory ((char *)buf, count, PyBUF_READ), + offset, flags, NULL); + break; + default: abort (); + } Py_DECREF (fn); if (check_python_failure ("pwrite") == -1) return -1; @@ -542,7 +597,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; @@ -551,7 +606,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; @@ -566,7 +629,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; @@ -575,7 +638,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; @@ -590,7 +661,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; @@ -600,9 +671,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..bc05a0b 100644 --- a/tests/test.py +++ b/tests/test.py @@ -3,6 +3,9 @@ import nbdkit disk = bytearray(1024*1024) +API_VERSION = 2 + + def config_complete(): print ("set_error = %r" % nbdkit.set_error) @@ -32,25 +35,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
Richard W.M. Jones
2019-Nov-25 10:03 UTC
[Libguestfs] [PATCH nbdkit v4 3/7] python: Implement cache.
However this does not implement can_cache, since that is not a simple boolean. --- plugins/python/nbdkit-python-plugin.pod | 14 ++++++++++- plugins/python/python.c | 31 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/plugins/python/nbdkit-python-plugin.pod b/plugins/python/nbdkit-python-plugin.pod index fe2a542..ebea0a7 100644 --- a/plugins/python/nbdkit-python-plugin.pod +++ b/plugins/python/nbdkit-python-plugin.pod @@ -293,6 +293,19 @@ because there is nothing to optimize if S<C<flags & nbdkit.FLAG_MAY_TRIM>> is false), use S<C<nbdkit.set_error (errno.EOPNOTSUPP)>>. +=item C<cache> + +(Optional) + + def cache(h, count, offset, flags): + # no return value + +The body of your C<cache> function should prefetch data in the +indicated range. + +If the cache operation fails, your function should throw an exception, +optionally using C<nbdkit.set_error> first. + =back =head2 Missing callbacks @@ -321,7 +334,6 @@ C<can_zero>, C<can_fast_zero>, C<can_extents>, C<can_multi_conn>, -C<cache>, C<extents>. These are not yet supported. diff --git a/plugins/python/python.c b/plugins/python/python.c index 68d01e8..5b0d0f5 100644 --- a/plugins/python/python.c +++ b/plugins/python/python.c @@ -707,6 +707,36 @@ py_zero (void *handle, uint32_t count, uint64_t offset, uint32_t flags) return -1; } +static int +py_cache (void *handle, uint32_t count, uint64_t offset, uint32_t flags) +{ + PyObject *obj = handle; + PyObject *fn; + PyObject *r; + + if (callback_defined ("cache", &fn)) { + PyErr_Clear (); + + switch (py_api_version) { + case 1: + case 2: + r = PyObject_CallFunction (fn, "OiLI", obj, count, offset, flags, NULL); + break; + default: abort (); + } + Py_DECREF (fn); + if (check_python_failure ("cache") == -1) + return -1; + Py_DECREF (r); + } + else { + nbdkit_error ("%s not implemented", "cache"); + return -1; + } + + return 0; +} + static int boolean_callback (void *handle, const char *can_fn, const char *plain_fn) { @@ -792,6 +822,7 @@ static struct nbdkit_plugin plugin = { .flush = py_flush, .trim = py_trim, .zero = py_zero, + .cache = py_cache, }; NBDKIT_REGISTER_PLUGIN (plugin) -- 2.23.0
Richard W.M. Jones
2019-Nov-25 10:03 UTC
[Libguestfs] [PATCH nbdkit v4 4/7] python: Implement can_zero, can_fast_zero.
--- plugins/python/nbdkit-python-plugin.pod | 16 ++++++++++++++-- plugins/python/python.c | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/plugins/python/nbdkit-python-plugin.pod b/plugins/python/nbdkit-python-plugin.pod index ebea0a7..0def7bb 100644 --- a/plugins/python/nbdkit-python-plugin.pod +++ b/plugins/python/nbdkit-python-plugin.pod @@ -208,6 +208,20 @@ contents will be garbage collected. def can_trim(h): # return a boolean +=item C<can_zero> + +(Optional) + + def can_zero(h): + # return a boolean + +=item C<can_fast_zero> + +(Optional) + + def can_fast_zero(h): + # return a boolean + =item C<pread> (Required) @@ -330,8 +344,6 @@ C<config_help>, C<magic_config_key>, C<can_fua>, C<can_cache>, -C<can_zero>, -C<can_fast_zero>, C<can_extents>, C<can_multi_conn>, C<extents>. diff --git a/plugins/python/python.c b/plugins/python/python.c index 5b0d0f5..01bd285 100644 --- a/plugins/python/python.c +++ b/plugins/python/python.c @@ -790,6 +790,18 @@ py_can_trim (void *handle) return boolean_callback (handle, "can_trim", "trim"); } +static int +py_can_zero (void *handle) +{ + return boolean_callback (handle, "can_zero", "zero"); +} + +static int +py_can_fast_zero (void *handle) +{ + return boolean_callback (handle, "can_fast_zero", NULL); +} + #define py_config_help \ "script=<FILENAME> (required) The Python plugin to run.\n" \ "[other arguments may be used by the plugin that you load]" @@ -816,6 +828,8 @@ static struct nbdkit_plugin plugin = { .can_write = py_can_write, .can_flush = py_can_flush, .can_trim = py_can_trim, + .can_zero = py_can_zero, + .can_fast_zero = py_can_fast_zero, .pread = py_pread, .pwrite = py_pwrite, -- 2.23.0
Richard W.M. Jones
2019-Nov-25 10:03 UTC
[Libguestfs] [PATCH nbdkit v4 5/7] python: Implement can_multi_conn.
--- plugins/python/nbdkit-python-plugin.pod | 8 +++++++- plugins/python/python.c | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/python/nbdkit-python-plugin.pod b/plugins/python/nbdkit-python-plugin.pod index 0def7bb..a0b4d90 100644 --- a/plugins/python/nbdkit-python-plugin.pod +++ b/plugins/python/nbdkit-python-plugin.pod @@ -187,6 +187,13 @@ contents will be garbage collected. def is_rotational(h): # return a boolean +=item C<can_multi_conn> + +(Optional) + + def can_multi_conn(h): + # return a boolean + =item C<can_write> (Optional) @@ -345,7 +352,6 @@ C<magic_config_key>, C<can_fua>, C<can_cache>, C<can_extents>, -C<can_multi_conn>, C<extents>. These are not yet supported. diff --git a/plugins/python/python.c b/plugins/python/python.c index 01bd285..12f45f7 100644 --- a/plugins/python/python.c +++ b/plugins/python/python.c @@ -772,6 +772,12 @@ py_is_rotational (void *handle) return boolean_callback (handle, "is_rotational", NULL); } +static int +py_can_multi_conn (void *handle) +{ + return boolean_callback (handle, "can_multi_conn", NULL); +} + static int py_can_write (void *handle) { @@ -825,6 +831,7 @@ static struct nbdkit_plugin plugin = { .get_size = py_get_size, .is_rotational = py_is_rotational, + .can_multi_conn = py_can_multi_conn, .can_write = py_can_write, .can_flush = py_can_flush, .can_trim = py_can_trim, -- 2.23.0
Richard W.M. Jones
2019-Nov-25 10:03 UTC
[Libguestfs] [PATCH nbdkit v4 6/7] python: Implement can_fua and can_cache.
--- plugins/python/nbdkit-python-plugin.pod | 18 +++++++- plugins/python/python.c | 58 +++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/plugins/python/nbdkit-python-plugin.pod b/plugins/python/nbdkit-python-plugin.pod index a0b4d90..0b31ebc 100644 --- a/plugins/python/nbdkit-python-plugin.pod +++ b/plugins/python/nbdkit-python-plugin.pod @@ -229,6 +229,22 @@ contents will be garbage collected. def can_fast_zero(h): # return a boolean +=item C<can_fua> + +(Optional) + + def can_fua(h): + # return nbdkit.FUA_NONE or nbdkit.FUA_EMULATE + # or nbdkit.FUA_NATIVE + +=item C<can_cache> + +(Optional) + + def can_cache(h): + # return nbdkit.CACHE_NONE or nbdkit.CACHE_EMULATE + # or nbdkit.CACHE_NATIVE + =item C<pread> (Required) @@ -349,8 +365,6 @@ C<longname>, C<description>, C<config_help>, C<magic_config_key>, -C<can_fua>, -C<can_cache>, C<can_extents>, C<extents>. diff --git a/plugins/python/python.c b/plugins/python/python.c index 12f45f7..252ca37 100644 --- a/plugins/python/python.c +++ b/plugins/python/python.c @@ -808,6 +808,62 @@ py_can_fast_zero (void *handle) return boolean_callback (handle, "can_fast_zero", NULL); } +static int +py_can_fua (void *handle) +{ + PyObject *obj = handle; + PyObject *fn; + PyObject *r; + int ret; + + if (callback_defined ("can_fua", &fn)) { + PyErr_Clear (); + + r = PyObject_CallFunctionObjArgs (fn, obj, NULL); + Py_DECREF (fn); + if (check_python_failure ("can_fua") == -1) + return -1; + ret = PyLong_AsLong (r); + Py_DECREF (r); + return ret; + } + /* No Python ‘can_fua’, but check if there's a Python ‘flush’ + * callback defined. (In C modules, nbdkit would do this). + */ + else if (callback_defined ("flush", NULL)) + return NBDKIT_FUA_EMULATE; + else + return NBDKIT_FUA_NONE; +} + +static int +py_can_cache (void *handle) +{ + PyObject *obj = handle; + PyObject *fn; + PyObject *r; + int ret; + + if (callback_defined ("can_cache", &fn)) { + PyErr_Clear (); + + r = PyObject_CallFunctionObjArgs (fn, obj, NULL); + Py_DECREF (fn); + if (check_python_failure ("can_cache") == -1) + return -1; + ret = PyLong_AsLong (r); + Py_DECREF (r); + return ret; + } + /* No Python ‘can_cache’, but check if there's a Python ‘cache’ + * callback defined. (In C modules, nbdkit would do this). + */ + else if (callback_defined ("cache", NULL)) + return NBDKIT_CACHE_NATIVE; + else + return NBDKIT_CACHE_NONE; +} + #define py_config_help \ "script=<FILENAME> (required) The Python plugin to run.\n" \ "[other arguments may be used by the plugin that you load]" @@ -837,6 +893,8 @@ static struct nbdkit_plugin plugin = { .can_trim = py_can_trim, .can_zero = py_can_zero, .can_fast_zero = py_can_fast_zero, + .can_fua = py_can_fua, + .can_cache = py_can_cache, .pread = py_pread, .pwrite = py_pwrite, -- 2.23.0
Richard W.M. Jones
2019-Nov-25 10:03 UTC
[Libguestfs] [PATCH nbdkit v4 7/7] tests: Test the Python plugin thoroughly.
This tests the Python plugin thoroughly by issuing client commands through libnbd and checking we get the expected results. --- .gitignore | 1 + README | 2 + tests/Makefile.am | 15 +-- tests/test-lang-plugins.c | 3 +- tests/test-python-plugin.py | 139 +++++++++++++++++++++ tests/test-python.sh | 49 ++++++++ tests/test.py | 59 --------- tests/test_python.py | 236 ++++++++++++++++++++++++++++++++++++ 8 files changed, 433 insertions(+), 71 deletions(-) diff --git a/.gitignore b/.gitignore index b25ac7f..e25bd99 100644 --- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ Makefile.in /server/synopsis.c /server/test-public /stamp-h1 +/tests/__pycache__/ /tests/disk /tests/disk.gz /tests/disk.xz diff --git a/README b/README index 7034fd8..9313f8a 100644 --- a/README +++ b/README @@ -130,6 +130,8 @@ For the Python plugin: - python development libraries + - python unittest to run the test suite + For the OCaml plugin: - OCaml >= 4.02.2 diff --git a/tests/Makefile.am b/tests/Makefile.am index 13cd8f3..848cc07 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -115,7 +115,9 @@ EXTRA_DIST = \ test-pattern-largest-for-qemu.sh \ test-python-exception.sh \ test.pl \ - test.py \ + test_python.py \ + test-python-plugin.py \ + test-python.sh \ test-rate.sh \ test-rate-dynamic.sh \ test.rb \ @@ -207,6 +209,7 @@ EXTRA_PROGRAMS TESTS_ENVIRONMENT = \ PATH=$(abs_top_builddir):$(PATH) \ SRCDIR=$(srcdir) \ + PYTHON=$(PYTHON) \ LIBGUESTFS_ATTACH_METHOD=appliance \ LIBGUESTFS_DEBUG=1 \ LIBGUESTFS_TRACE=1 \ @@ -787,18 +790,10 @@ endif HAVE_PERL if HAVE_PYTHON TESTS += \ + test-python.sh \ test-python-exception.sh \ test-shebang-python.sh \ $(NULL) -LIBGUESTFS_TESTS += test-python - -test_python_SOURCES = test-lang-plugins.c test.h -test_python_CFLAGS = \ - -DLANG='"python"' -DSCRIPT='"$(srcdir)/test.py"' \ - $(WARNINGS_CFLAGS) \ - $(LIBGUESTFS_CFLAGS) \ - $(NULL) -test_python_LDADD = libtest.la $(LIBGUESTFS_LIBS) endif HAVE_PYTHON diff --git a/tests/test-lang-plugins.c b/tests/test-lang-plugins.c index ffb1918..93f9938 100644 --- a/tests/test-lang-plugins.c +++ b/tests/test-lang-plugins.c @@ -56,8 +56,7 @@ main (int argc, char *argv[]) */ s = getenv ("NBDKIT_VALGRIND"); if (s && strcmp (s, "1") == 0 && - (strcmp (LANG, "python") == 0 || - strcmp (LANG, "ruby") == 0 || + (strcmp (LANG, "ruby") == 0 || strcmp (LANG, "tcl") == 0)) { fprintf (stderr, "%s test skipped under valgrind.\n", LANG); exit (77); /* Tells automake to skip the test. */ diff --git a/tests/test-python-plugin.py b/tests/test-python-plugin.py new file mode 100644 index 0000000..d7335bd --- /dev/null +++ b/tests/test-python-plugin.py @@ -0,0 +1,139 @@ +# nbdkit test plugin +# Copyright (C) 2019 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. + +"""See test-python.py.""" + +import nbdkit +import sys +import pickle +import base64 + +API_VERSION = 2 + +cfg = {} + +def config (k, v): + global cfg + if k == "cfg": + cfg = pickle.loads (base64.b64decode (v.encode())) + +def config_complete (): + print ("set_error = %r" % nbdkit.set_error) + +def open (readonly): + return { + 'disk': bytearray (cfg.get ('size', 0)) + } + +def get_size (h): + return len (h['disk']) + +def is_rotational (h): + return cfg.get ('is_rotational', False) + +def can_multi_conn (h): + return cfg.get ('can_multi_conn', False) + +def can_write (h): + return cfg.get ('can_write', True) + +def can_flush (h): + return cfg.get ('can_flush', False) + +def can_trim (h): + return cfg.get ('can_trim', False) + +def can_zero (h): + return cfg.get ('can_zero', False) + +def can_fast_zero (h): + return cfg.get ('can_fast_zero', False) + +def can_fua (h): + fua = cfg.get ('can_fua', "none") + if fua == "none": + return nbdkit.FUA_NONE + elif fua == "emulate": + return nbdkit.FUA_EMULATE + elif fua == "native": + return nbdkit.FUA_NATIVE + +def can_cache (h): + cache = cfg.get ('can_cache', "none") + if cache == "none": + return nbdkit.CACHE_NONE + elif cache == "emulate": + return nbdkit.CACHE_EMULATE + elif cache == "native": + return nbdkit.CACHE_NATIVE + +def pread (h, count, offset, flags): + assert flags == 0 + pread_result = cfg.get ('pread_result', "bytearray") + b = h['disk'][offset:offset+count] + if pread_result == "bytearray": + return b + elif pread_result == "bytes": + return bytes (b) + elif pread_result == "memoryview": + return memoryview (b) + +def pwrite (h, buf, offset, flags): + expect_fua = cfg.get ('pwrite_expect_fua', False) + actual_fua = bool (flags & nbdkit.FLAG_FUA) + assert expect_fua == actual_fua + end = offset + len(buf) + h['disk'][offset:end] = buf + +def flush (h, flags): + assert flags == 0 + +def trim (h, count, offset, flags): + expect_fua = cfg.get ('trim_expect_fua', False) + actual_fua = bool (flags & nbdkit.FLAG_FUA) + assert expect_fua == actual_fua + h['disk'][offset:offset+count] = bytearray(count) + +def zero (h, count, offset, flags): + expect_fua = cfg.get ('zero_expect_fua', False) + actual_fua = bool (flags & nbdkit.FLAG_FUA) + assert expect_fua == actual_fua + expect_may_trim = cfg.get ('zero_expect_may_trim', False) + actual_may_trim = bool (flags & nbdkit.FLAG_MAY_TRIM) + assert expect_may_trim == actual_may_trim + expect_fast_zero = cfg.get ('zero_expect_fast_zero', False) + actual_fast_zero = bool (flags & nbdkit.FLAG_FAST_ZERO) + assert expect_fast_zero == actual_fast_zero + h['disk'][offset:offset+count] = bytearray(count) + +def cache (h, count, offset, flags): + assert flags == 0 + # do nothing diff --git a/tests/test-python.sh b/tests/test-python.sh new file mode 100755 index 0000000..50324d0 --- /dev/null +++ b/tests/test-python.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# nbdkit +# Copyright (C) 2019 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. + +source ./functions.sh +set -e +set -x + +requires $PYTHON --version +requires $PYTHON -c 'import unittest' +requires $PYTHON -c 'import nbd' +requires test -f test_python.py +requires test -f test-python-plugin.py + +# Python has proven very difficult to valgrind, therefore it is disabled. +if [ "$NBDKIT_VALGRIND" = "1" ]; then + echo "$0: skipping Python test under valgrind." + exit 77 +fi + +$PYTHON -m unittest test_python diff --git a/tests/test.py b/tests/test.py deleted file mode 100644 index bc05a0b..0000000 --- a/tests/test.py +++ /dev/null @@ -1,59 +0,0 @@ -import nbdkit - -disk = bytearray(1024*1024) - - -API_VERSION = 2 - - -def config_complete(): - print ("set_error = %r" % nbdkit.set_error) - - -def open(readonly): - return 1 - - -def get_size(h): - global disk - return len(disk) - - -def can_write(h): - return True - - -def can_flush(h): - return True - - -def is_rotational(h): - return False - - -def can_trim(h): - return True - - -def pread(h, count, offset, flags): - global disk - return disk[offset:offset+count] - - -def pwrite(h, buf, offset, flags): - global disk - end = offset + len(buf) - disk[offset:end] = buf - - -def flush(h, flags): - pass - - -def trim(h, count, offset, flags): - pass - - -def zero(h, count, offset, flags): - global disk - disk[offset:offset+count] = bytearray(count) diff --git a/tests/test_python.py b/tests/test_python.py new file mode 100755 index 0000000..b2c60f0 --- /dev/null +++ b/tests/test_python.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +# nbdkit +# Copyright (C) 2019 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 tests the Python plugin thoroughly by issuing client commands +through libnbd and checking we get the expected results. It uses an +associated plugin (test-python-plugin.sh). +""" + +import os +import sys +import nbd +import unittest +import pickle +import base64 + +class Test (unittest.TestCase): + def setUp (self): + self.h = nbd.NBD () + + def tearDown (self): + del self.h + + def connect (self, cfg): + cfg = base64.b64encode (pickle.dumps (cfg)).decode() + cmd = ["nbdkit", "-v", "-s", "--exit-with-parent", + "python", "test-python-plugin.py", "cfg=" + cfg] + self.h.connect_command (cmd) + + def test_none (self): + """ + Test we can send an empty pickled test configuration and do + nothing else. This is just to ensure the machinery of the + test works. + """ + self.connect ({}) + + def test_size_512 (self): + """Test the size.""" + self.connect ({"size": 512}) + assert self.h.get_size() == 512 + + def test_size_1m (self): + """Test the size.""" + self.connect ({"size": 1024*1024}) + assert self.h.get_size() == 1024*1024 + + # Test each flag call. + def test_is_rotational_true (self): + self.connect ({"size": 512, "is_rotational": True}) + assert self.h.is_rotational() + + def test_is_rotational_false (self): + self.connect ({"size": 512, "is_rotational": False}) + assert not self.h.is_rotational() + + def test_can_multi_conn_true (self): + self.connect ({"size": 512, "can_multi_conn": True}) + assert self.h.can_multi_conn() + + def test_can_multi_conn_false (self): + self.connect ({"size": 512, "can_multi_conn": False}) + assert not self.h.can_multi_conn() + + def test_read_write (self): + self.connect ({"size": 512, "can_write": True}) + assert not self.h.is_read_only() + + def test_read_only (self): + self.connect ({"size": 512, "can_write": False}) + assert self.h.is_read_only() + + def test_can_flush_true (self): + self.connect ({"size": 512, "can_flush": True}) + assert self.h.can_flush() + + def test_can_flush_false (self): + self.connect ({"size": 512, "can_flush": False}) + assert not self.h.can_flush() + + def test_can_trim_true (self): + self.connect ({"size": 512, "can_trim": True}) + assert self.h.can_trim() + + def test_can_trim_false (self): + self.connect ({"size": 512, "can_trim": False}) + assert not self.h.can_trim() + + # nbdkit can always zero because it emulates it. + #self.connect ({"size": 512, "can_zero": True}) + #assert self.h.can_zero() + #self.connect ({"size": 512, "can_zero": False}) + #assert not self.h.can_zero() + + def test_can_fast_zero_true (self): + self.connect ({"size": 512, "can_fast_zero": True}) + assert self.h.can_fast_zero() + + def test_can_fast_zero_false (self): + self.connect ({"size": 512, "can_fast_zero": False}) + assert not self.h.can_fast_zero() + + def test_can_fua_none (self): + self.connect ({"size": 512, "can_fua": "none"}) + assert not self.h.can_fua() + + def test_can_fua_emulate (self): + self.connect ({"size": 512, "can_fua": "emulate"}) + assert self.h.can_fua() + + def test_can_fua_native (self): + self.connect ({"size": 512, "can_fua": "native"}) + assert self.h.can_fua() + + def test_can_cache_none (self): + self.connect ({"size": 512, "can_cache": "none"}) + assert not self.h.can_cache() + + def test_can_cache_emulate (self): + self.connect ({"size": 512, "can_cache": "emulate"}) + assert self.h.can_cache() + + def test_can_cache_native (self): + self.connect ({"size": 512, "can_cache": "native"}) + assert self.h.can_cache() + + # Not yet implemented: can_extents. + + def test_pread_bytearray (self): + """Test pread returning bytearray.""" + self.connect ({"size": 512}) + buf = self.h.pread (512, 0) + assert buf == bytearray (512) + + def test_pread_bytes (self): + """Test pread returning bytes.""" + self.connect ({"size": 512, + "pread_result": "bytes"}) + buf = self.h.pread (512, 0) + assert buf == bytearray (512) + + def test_pread_memoryview (self): + """Test pread returning memoryview.""" + self.connect ({"size": 512, + "pread_result": "memoryview"}) + buf = self.h.pread (512, 0) + assert buf == bytearray (512) + + # Test pwrite + flags. + def test_pwrite (self): + self.connect ({"size": 512}) + buf = bytearray (512) + self.h.pwrite (buf, 0) + + def test_pwrite_fua (self): + self.connect ({"size": 512, + "can_fua": "native", + "pwrite_expect_fua": True}) + buf = bytearray (512) + self.h.pwrite (buf, 0, nbd.CMD_FLAG_FUA) + + def test_flush (self): + """Test flush.""" + self.connect ({"size": 512, "can_flush": True}) + self.h.flush () + + # Test trim + flags. + def test_trim (self): + self.connect ({"size": 512, "can_trim": True}) + self.h.trim (512, 0) + + def test_trim_fua (self): + self.connect ({"size": 512, + "can_trim": True, + "can_fua": "native", + "trim_expect_fua": True}) + self.h.trim (512, 0, nbd.CMD_FLAG_FUA) + + # Test zero + flags. + def test_zero (self): + self.connect ({"size": 512, "can_zero": True}) + self.h.zero (512, 0, nbd.CMD_FLAG_NO_HOLE) + + def test_zero_fua (self): + self.connect ({"size": 512, + "can_zero": True, + "can_fua": "native", + "zero_expect_fua": True}) + self.h.zero (512, 0, nbd.CMD_FLAG_NO_HOLE | nbd.CMD_FLAG_FUA) + + def test_zero_may_trim (self): + self.connect ({"size": 512, + "can_zero": True, + "zero_expect_may_trim": True}) + self.h.zero (512, 0, 0) # absence of nbd.CMD_FLAG_NO_HOLE + + def test_zero_fast_zero (self): + self.connect ({"size": 512, + "can_zero": True, + "can_fast_zero": True, + "zero_expect_fast_zero": True}) + self.h.zero (512, 0, nbd.CMD_FLAG_NO_HOLE | nbd.CMD_FLAG_FAST_ZERO) + + def test_cache (self): + """Test cache.""" + self.connect ({"size": 512, "can_cache": "native"}) + self.h.cache (512, 0) -- 2.23.0
Possibly Parallel Threads
- [PATCH nbdkit v3 0/7] Implement nbdkit API v2 for Python plugins.
- [PATCH nbdkit v2 00/10] Implement nbdkit API v2 for Python plugins.
- [PATCH nbdkit 0/8] Implement nbdkit API v2 for Python plugins.
- [nbdkit PATCH 0/5] Counterproposal for python v2 interfaces
- [nbdkit PATCH 0/2] More caching of initial setup