Fairly straightforward. .list_exports uses the same idiom as .extents
for returning an iterable of tuples, with additional support for a
bare name rather than a name/desc tuple. .default_export and
.export_description are rather easy clients of nbdkit_strdup_intern.
Signed-off-by: Eric Blake <eblake(a)redhat.com>
---
plugins/python/nbdkit-python-plugin.pod | 25 ++++
tests/Makefile.am | 3 +
plugins/python/python.c | 185 ++++++++++++++++++++----
tests/test-python-export-list.sh | 71 +++++++++
tests/python-export-list.py | 59 ++++++++
5 files changed, 313 insertions(+), 30 deletions(-)
create mode 100755 tests/test-python-export-list.sh
create mode 100644 tests/python-export-list.py
diff --git a/plugins/python/nbdkit-python-plugin.pod
b/plugins/python/nbdkit-python-plugin.pod
index 79aed288..a3bd520c 100644
--- a/plugins/python/nbdkit-python-plugin.pod
+++ b/plugins/python/nbdkit-python-plugin.pod
@@ -178,6 +178,23 @@ See L</Threads> below.
There are no arguments or return value.
+=item C<list_exports>
+
+(Optional)
+
+ def list_exports(readonly, is_tls):
+ # return an iterable object (eg. list) of
+ # (name, description) tuples or bare names:
+ return [ (name1, desc1), name2, (name3, desc3), ... ]
+
+=item C<default_export>
+
+(Optional)
+
+ def default_export(readonly, is_tls):
+ # return a string
+ return "name"
+
=item C<open>
(Required)
@@ -199,6 +216,14 @@ After C<close> returns, the reference count of the handle is
decremented in the C part, which usually means that the handle and its
contents will be garbage collected.
+=item C<export_description>
+
+(Optional)
+
+ def export_description(h):
+ # return a string
+ return "description"
+
=item C<get_size>
(Required)
diff --git a/tests/Makefile.am b/tests/Makefile.am
index c2d668c1..390df711 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -1083,16 +1083,19 @@ TESTS += \
test-python.sh \
test-python-exception.sh \
test-python-export-name.sh \
+ test-python-export-list.sh \
test-python-thread-model.sh \
test-shebang-python.sh \
$(NULL)
EXTRA_DIST += \
python-exception.py \
python-export-name.py \
+ python-export-list.py \
python-thread-model.py \
shebang.py \
test-python-exception.sh \
test-python-export-name.sh \
+ test-python-export-list.sh \
test-python-plugin.py \
test-python-thread-model.sh \
test-python.sh \
diff --git a/plugins/python/python.c b/plugins/python/python.c
index 27c5ede2..91cebc16 100644
--- a/plugins/python/python.c
+++ b/plugins/python/python.c
@@ -536,6 +536,99 @@ py_get_ready (void)
return 0;
}
+static int
+py_list_exports (int readonly, int is_tls, struct nbdkit_exports *exports)
+{
+ ACQUIRE_PYTHON_GIL_FOR_CURRENT_SCOPE;
+ PyObject *fn;
+ PyObject *r;
+ PyObject *iter, *t;
+
+ if (!callback_defined ("list_exports", &fn))
+ /* Do the same as the core server */
+ return nbdkit_use_default_export (exports);
+
+ PyErr_Clear ();
+
+ r = PyObject_CallFunction (fn, "ii", readonly, is_tls);
+ Py_DECREF (fn);
+ if (check_python_failure ("list_exports") == -1)
+ return -1;
+
+ iter = PyObject_GetIter (r);
+ if (iter == NULL) {
+ nbdkit_error ("list_exports method did not return "
+ "something which is iterable");
+ Py_DECREF (r);
+ return -1;
+ }
+
+ while ((t = PyIter_Next (iter)) != NULL) {
+ PyObject *py_name, *py_desc;
+ CLEANUP_FREE char *name = NULL;
+ CLEANUP_FREE char *desc = NULL;
+
+ name = python_to_string (t);
+ if (!name) {
+ if (!PyTuple_Check (t) || PyTuple_Size (t) != 2) {
+ nbdkit_error ("list_exports method did not return an iterable of "
+ "2-tuples");
+ Py_DECREF (iter);
+ Py_DECREF (r);
+ return -1;
+ }
+ py_name = PyTuple_GetItem (t, 0);
+ py_desc = PyTuple_GetItem (t, 1);
+ name = python_to_string (py_name);
+ desc = python_to_string (py_desc);
+ if (name == NULL || desc == NULL) {
+ nbdkit_error ("list_exports method did not return an iterable of "
+ "string 2-tuples");
+ Py_DECREF (iter);
+ Py_DECREF (r);
+ return -1;
+ }
+ }
+ if (nbdkit_add_export (exports, name, desc) == -1) {
+ Py_DECREF (iter);
+ Py_DECREF (r);
+ return -1;
+ }
+ }
+
+ Py_DECREF (iter);
+ Py_DECREF (r);
+ return 0;
+}
+
+static const char *
+py_default_export (int readonly, int is_tls)
+{
+ ACQUIRE_PYTHON_GIL_FOR_CURRENT_SCOPE;
+ PyObject *fn;
+ PyObject *r;
+ CLEANUP_FREE char *name = NULL;
+
+ if (!callback_defined ("default_export", &fn))
+ return "";
+
+ PyErr_Clear ();
+
+ r = PyObject_CallFunction (fn, "ii", readonly, is_tls);
+ Py_DECREF (fn);
+ if (check_python_failure ("default_export") == -1)
+ return NULL;
+
+ name = python_to_string (r);
+ Py_DECREF (r);
+ if (!name) {
+ nbdkit_error ("default_export method did not return a string");
+ return NULL;
+ }
+
+ return nbdkit_strdup_intern (name);
+}
+
struct handle {
int can_zero;
PyObject *py_h;
@@ -595,6 +688,35 @@ py_close (void *handle)
free (h);
}
+static const char *
+py_export_description (void *handle)
+{
+ ACQUIRE_PYTHON_GIL_FOR_CURRENT_SCOPE;
+ struct handle *h = handle;
+ PyObject *fn;
+ PyObject *r;
+ CLEANUP_FREE char *desc = NULL;
+
+ if (!callback_defined ("export_description", &fn))
+ return NULL;
+
+ PyErr_Clear ();
+
+ r = PyObject_CallFunctionObjArgs (fn, h->py_h, NULL);
+ Py_DECREF (fn);
+ if (check_python_failure ("export_description") == -1)
+ return NULL;
+
+ desc = python_to_string (r);
+ Py_DECREF (r);
+ if (!desc) {
+ nbdkit_error ("export_description method did not return a string");
+ return NULL;
+ }
+
+ return nbdkit_strdup_intern (desc);
+}
+
static int64_t
py_get_size (void *handle)
{
@@ -1120,42 +1242,45 @@ py_extents (void *handle, uint32_t count, uint64_t offset,
#define THREAD_MODEL NBDKIT_THREAD_MODEL_PARALLEL
static struct nbdkit_plugin plugin = {
- .name = "python",
- .version = PACKAGE_VERSION,
+ .name = "python",
+ .version = PACKAGE_VERSION,
- .load = py_load,
- .unload = py_unload,
- .dump_plugin = py_dump_plugin,
+ .load = py_load,
+ .unload = py_unload,
+ .dump_plugin = py_dump_plugin,
- .config = py_config,
- .config_complete = py_config_complete,
- .config_help = py_config_help,
+ .config = py_config,
+ .config_complete = py_config_complete,
+ .config_help = py_config_help,
- .thread_model = py_thread_model,
- .get_ready = py_get_ready,
+ .thread_model = py_thread_model,
+ .get_ready = py_get_ready,
+ .list_exports = py_list_exports,
+ .default_export = py_default_export,
- .open = py_open,
- .close = py_close,
+ .open = py_open,
+ .close = py_close,
- .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,
- .can_zero = py_can_zero,
- .can_fast_zero = py_can_fast_zero,
- .can_fua = py_can_fua,
- .can_cache = py_can_cache,
- .can_extents = py_can_extents,
+ .export_description = py_export_description,
+ .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,
+ .can_zero = py_can_zero,
+ .can_fast_zero = py_can_fast_zero,
+ .can_fua = py_can_fua,
+ .can_cache = py_can_cache,
+ .can_extents = py_can_extents,
- .pread = py_pread,
- .pwrite = py_pwrite,
- .flush = py_flush,
- .trim = py_trim,
- .zero = py_zero,
- .cache = py_cache,
- .extents = py_extents,
+ .pread = py_pread,
+ .pwrite = py_pwrite,
+ .flush = py_flush,
+ .trim = py_trim,
+ .zero = py_zero,
+ .cache = py_cache,
+ .extents = py_extents,
};
NBDKIT_REGISTER_PLUGIN (plugin)
diff --git a/tests/test-python-export-list.sh b/tests/test-python-export-list.sh
new file mode 100755
index 00000000..f8569979
--- /dev/null
+++ b/tests/test-python-export-list.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+# nbdkit
+# Copyright (C) 2018-2020 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
+
+if test ! -d "$SRCDIR"; then
+ echo "$0: could not locate python-export-list.py"
+ exit 1
+fi
+
+# 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
+
+requires nbdinfo --version
+requires nbdsh -c 'print(h.set_full_info)'
+requires jq --version
+
+pid=test-python-export-list.pid
+sock=`mktemp -u`
+out=test-python-export-list.out
+files="$pid $sock $out"
+rm -f $files
+cleanup_fn rm -f $files
+
+start_nbdkit -P $pid -U $sock python $SRCDIR/python-export-list.py
+
+nbdinfo --list --json nbd+unix://\?socket=$sock > $out
+cat $out
+# libnbd 1.4.0 differs from 1.4.1 on whether --list grabs descriptions
+result=$(jq -c '[.exports[] | [."export-name", .description]]' $out)
+test "$result" = '[["hello","world"],["name
only",null]]' ||
+ test "$result" = '[["hello","world"],["name
only","=name only="]]'
+
+nbdinfo --json nbd+unix://\?socket=$sock > $out
+cat $out
+diff -u <(jq -c '[.exports[] | [."export-name", .description]]'
$out) \
+ <(printf %s\\n '[["hello","=hello="]]')
diff --git a/tests/python-export-list.py b/tests/python-export-list.py
new file mode 100644
index 00000000..6feb6782
--- /dev/null
+++ b/tests/python-export-list.py
@@ -0,0 +1,59 @@
+# nbdkit
+# Copyright (C) 2018-2020 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.
+
+# Create an nbdkit sh plugin which reflects the export name back to
+# the caller in the virtual device data and size.
+
+import nbdkit
+
+
+def list_exports(readonly, is_tls):
+ return [("hello", "world"), "name only"]
+
+
+def default_export(readonly, is_tls):
+ return "hello"
+
+
+def open(readonly):
+ return nbdkit.export_name()
+
+
+def export_description(h):
+ return "=%s=" % h
+
+
+def get_size(h):
+ return 0
+
+
+def pread(h, count, offset):
+ pass
--
2.28.0