nbdkit-python-plugin(3)

🌸 nbdkit-python-plugin 🌸

ok so this page is nbdkit-python-plugin(3) from the NBDKIT manual, and bestie it is so good. buckle up. πŸ’–


✨ NAME ✨

nbdkit-python-plugin β€” nbdkit python plugin πŸ’…


✨ SYNOPSIS ✨

nbdkit python /path/to/plugin.py [arguments...]

✨ DESCRIPTION ✨

nbdkit-python-plugin is literally an embedded Python interpreter for nbdkit(1) β€” it's the whole reason you get to write nbdkit plugins in Python 3. iconic. 🐍✨

πŸ¦‹ If you have been given an nbdkit Python plugin πŸ¦‹

so like. say someone handed you a Python script that IS an nbdkit plugin. you run it like this:

nbdkit python /path/to/plugin.py

you might have to add extra key=value arguments to the command line, no cap. just read the Python script and see if it wants any. πŸ“–


✨ WRITING A PYTHON NBDKIT PLUGIN ✨

for example plugins written in Python, go look here:

https://gitlab.com/nbdkit/nbdkit/blob/master/plugins/python/examples

broadly speaking, Python nbdkit plugins work exactly like the C ones, so bestie, go read nbdkit-plugin(3) first. fr fr. 🫢

to write a Python nbdkit plugin, you make a Python file containing at least these required functions (they live in the top level __main__ module):

API_VERSION = 2
def open(readonly):
  # see below
def get_size(h):
  # see below
def pread(h, buf, offset, flags):
  # see below

heads up: the subroutines must have those literal names (like open), because the C part looks up and calls those functions directly. no renaming, no vibes-based aliases. you may want to include documentation and globals (eg. for storing global state). any other top level statements are run when the script is loaded, just like ordinary Python. πŸ’–

🐍 Python versions 🐍

since nbdkit β‰₯ 1.16, only Python 3 is supported. if you still want to keep using nbdkit plugins written in Python 2 then you must use nbdkit ≀ 1.14 β€” but honestly? we advise you to update your plugins. it's giving legacy. 😭

the version of Python 3 is chosen when nbdkit is built. this is compiled in and can't be changed at runtime, periodt. ./configure looks for (in order):

  • the PYTHON variable (eg. ./configure PYTHON=/usr/bin/python3.9)
  • python3 on $PATH
  • python on $PATH

./configure will fail if the first interpreter found is a Python 2 interpreter. it said no to that energy. πŸ’€

to find out which version of Python nbdkit-python-plugin was compiled for, use the --dump-plugin option:

$ nbdkit python --dump-plugin
...
python_version=3.7.0
python_pep_384_abi_version=3

πŸŽ€ 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. no exceptions bestie. ✨

🌈 Executable script 🌈

if you want, you can make the script executable and include a shebang at the top:

#!/usr/sbin/nbdkit python

see also "Shebang scripts" in nbdkit(1).

these scripts can also be installed in the $plugindir. see "WRITING PLUGINS IN OTHER PROGRAMMING LANGUAGES" in nbdkit-plugin(3).

πŸ’œ Module functions πŸ’œ

your script may use import nbdkit to have access to the following methods in the nbdkit module:

  • nbdkit.debug(msg) β€” send a debug message to stderr or syslog if verbose messages are enabled.

See: nbdkit_debug(3)

  • nbdkit.debug_hexdiff(buf1, buf2, prefix start), nbdkit.debug_hexdump(buf, prefix, start) β€” display a buffer in hexdump format.

See: nbdkit_debug_hexdiff(3) and nbdkit_debug_hexdump(3)

  • nbdkit.disconnect(force) β€” disconnect from the client. if force is True then nbdkit will disconnect the client immediately. bye bestie πŸ‘‹

See: nbdkit_disconnect(3)

  • nbdkit.export_name() β€” return the export name negotiated with the client as a Unicode string. note this should not be trusted because the client can send whatever it wants. πŸ™ˆ

See: nbdkit_export_name(3)

  • nbdkit.is_tls() β€” returns True if the client completed TLS authentication, or False if the connection is plaintext.

See: nbdkit_is_tls(3)

  • nbdkit.name() β€” return the name of the nbdkit instance, or None if it was not set.

See: nbdkit_name(3)

  • nbdkit.nanosleep(secs, nsecs) β€” sleep for seconds and nanoseconds.

See: nbdkit_nanosleep(3)

  • nbdkit.parse_bool(str) β€” parse a human-readable boolean (such as yes or false), returning True or False. wraps the nbdkit_parse_bool(3) function.

See: nbdkit_parse_bool(3)

  • nbdkit.parse_delay(what, str) β€” parse a delay or sleep (such as 10ms) into a pair (sec, nsec). wraps the nbdkit_parse_delay(3) function.

See: nbdkit_parse_delay(3)

  • nbdkit.parse_probability(what, str) β€” parse a string (such as 100%) into a probability, returning a floating point number.

See: nbdkit_parse_probability(3)

  • nbdkit.parse_size(str) β€” parse a string (such as 100M) into a size in bytes.

See: nbdkit_parse_size(3)

  • nbdkit.peer_pid(), nbdkit.peer_uid(), nbdkit.peer_gid(), nbdkit.peer_security_context() β€” return the client process ID, user ID, group ID or security context. the PID, UID and GID are only available when the client connects by Unix domain socket, and then only on some operating systems. the security context is usually the SELinux label, IPSEC label or NetLabel.

See: nbdkit_peer_pid(3), nbdkit_peer_uid(3), nbdkit_peer_gid(3) and nbdkit_peer_security_context(3)

  • nbdkit.peer_tls_dn() β€” return the client TLS Distinguished Name.

See: nbdkit_peer_tls_dn(3)

  • nbdkit.peer_tls_issuer_dn() β€” return the client certificate issuer's TLS Distinguished Name.

See: nbdkit_peer_tls_issuer_dn(3)

  • nbdkit.read_password(value) β€” read a password from a config parameter. this returns the password as a Python bytes object.

See: nbdkit_read_password(3)

  • nbdkit.set_error(err) β€” throwing a Python exception from a callback causes an error message to be sent back to the NBD client. the NBD protocol allows an error code (ie. errno) to be sent to the client, but by default the Python plugin always sends EIO. to control what error code is sent, call nbdkit.set_error:
def pread(h, buf, offset):
  if access_denied:
    nbdkit.set_error(errno.EPERM)
    raise RuntimeError()

See: nbdkit_set_error(3)

  • nbdkit.shutdown() β€” request asynchronous server shutdown.

See: nbdkit_shutdown(3)

  • nbdkit.stdio_safe() β€” returns True if it is safe to interact with stdin and stdout during the configuration phase.

See: nbdkit_stdio_safe(3)

  • nbdkit.timestamp() β€” generate a timestamp for log messages.

See: nbdkit_timestamp(3)

⭐ Module constants ⭐

after import nbdkit the following constants are available. these are used in the callbacks below. πŸ‘‘

  • nbdkit.THREAD_MODEL_SERIALIZE_CONNECTIONS
  • nbdkit.THREAD_MODEL_SERIALIZE_ALL_REQUESTS
  • nbdkit.THREAD_MODEL_SERIALIZE_REQUESTS
  • nbdkit.THREAD_MODEL_PARALLEL

possible return values from thread_model().

  • nbdkit.FLAG_MAY_TRIM
  • nbdkit.FLAG_FUA
  • nbdkit.FLAG_REQ_ONE
  • nbdkit.FLAG_FAST_ZERO

flags bitmap passed to certain plugin callbacks. not all callbacks with a flags parameter use all of these flags, consult the documentation below and nbdkit-plugin(3).

  • nbdkit.FUA_NONE
  • nbdkit.FUA_EMULATE
  • nbdkit.FUA_NATIVE

possible return values from can_fua().

  • nbdkit.CACHE_NONE
  • nbdkit.CACHE_EMULATE
  • nbdkit.CACHE_NATIVE

possible return values from can_cache().

  • nbdkit.EXTENT_HOLE
  • nbdkit.EXTENT_ZERO

used in the type field returned by extents().

πŸ’… Threads πŸ’…

the thread model for Python callbacks defaults to nbdkit.THREAD_MODEL_SERIALIZE_ALL_REQUESTS.

since nbdkit 1.22 it has been possible to set this by implementing a thread_model() function which returns one of the constants nbdkit.THREAD_MODEL_*.

the Python Global Interpreter Lock (GIL) is still used, so Python code does not run in parallel. however, if a plugin callback calls a library which blocks (eg. to make an HTTP request), then another callback might be executed in parallel. plugins which use nbdkit.THREAD_MODEL_SERIALIZE_REQUESTS or nbdkit.THREAD_MODEL_PARALLEL may need to use locks on shared data. πŸ”’ no cap, lock your data.

πŸ“ Exceptions πŸ“

Python callbacks should throw exceptions to indicate errors. remember to use nbdkit.set_error if you need to control which error is sent back to the client; if omitted, the client will see an error of EIO. 😭

🧚 Python callbacks 🧚

this just documents the arguments to the callbacks in Python, and any way that they differ from the C callbacks. in all other respects they work the same way as the C callbacks, so you should go and read nbdkit-plugin(3).

dump_plugin

(Optional)

there are no arguments or return value.

config

(Optional)

def config(key, value):
  # no return value

config_complete

(Optional)

there are no arguments or return value.

thread_model

(Optional, nbdkit β‰₯ 1.22)

def thread_model():
  return nbdkit.THEAD_MODEL_SERIALIZE_ALL_REQUESTS

see "Threads" above.

get_ready

(Optional)

there are no arguments or return value.

after_fork

(Optional, nbdkit β‰₯ 1.26)

there are no arguments or return value.

cleanup

(Optional, nbdkit β‰₯ 1.28)

there are no arguments or return value.

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), ... ]

default_export

(Optional)

def default_export(readonly, is_tls):
  # return a string
  return "name"

preconnect

(Optional, nbdkit β‰₯ 1.26)

def preconnect(readonly):
  # no return value

open

(Required)

def open(readonly):
  # return handle

you can return any Python value (even None) as the handle. it is passed back as the first arg 'h' in subsequent calls. to return an error from this method you must throw an exception. 🫢

close

(Optional)

def close(h):
  # no return value

after 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. ♻️

export_description

(Optional)

def export_description(h):
  # return a string
  return "description"

get_size

(Required)

def get_size(h):
  # return the size of the disk

block_size

(Option)

✨ "block_size" ✨

(Optional) πŸ’«

def block_size(h):
  # return triple (minimum, preferred, maximum) block size

✨ "is_rotational" ✨

(Optional) πŸŒ€

def is_rotational(h):
  # return a boolean

✨ "can_multi_conn" ✨

(Optional) πŸ”—

def can_multi_conn(h):
  # return a boolean

✨ "can_write" ✨

(Optional) ✍️

def can_write(h):
  # return a boolean

✨ "can_flush" ✨

(Optional) 🚽

def can_flush(h):
  # return a boolean

✨ "can_trim" ✨

(Optional) βœ‚οΈ

def can_trim(h):
  # return a boolean

✨ "can_zero" ✨

(Optional) 0️⃣

def can_zero(h):
  # return a boolean

✨ "can_fast_zero" ✨

(Optional) ⚑

def can_fast_zero(h):
  # return a boolean

✨ "can_fua" ✨

(Optional) πŸ’Ύ

def can_fua(h):
  # return nbdkit.FUA_NONE or nbdkit.FUA_EMULATE
  # or nbdkit.FUA_NATIVE

✨ "can_cache" ✨

(Optional) πŸ—„οΈ

def can_cache(h):
  # return nbdkit.CACHE_NONE or nbdkit.CACHE_EMULATE
  # or nbdkit.CACHE_NATIVE

✨ "can_extents" ✨

(Optional) πŸ—ΊοΈ

def can_extents(h):
  # return a boolean

✨ "pread" ✨

(Required) β€” like, this one you actually gotta have, bestie, no skipping 🫢

def pread(h, buf, offset, flags):
  # read into the buffer

So inside your pread you read exactly len(buf) bytes of data, starting at disk offset, and write them into the buffer buf. And flags? It's always 0. Every time. Periodt.

NBD only supports whole reads, so your function should try to read the whole region (which might mean you need a loop, ik, annoying). And if the read fails or comes back partial, throw an exception β€” you can optionally call nbdkit.set_error first to set the vibe (er, the errno). πŸ’…


✨ "pwrite" ✨

(Optional)

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

The body of your pwrite writes the buffer buf to the disk. You should write count bytes to the disk starting at offset. And flags may contain nbdkit.FLAG_FUA πŸ‘€

NBD only supports whole writes, so try to write the whole region β€” yes, possibly with a loop, it's giving "why is this so manual" but that's the deal. If the write fails or is partial, throw an exception, optionally using nbdkit.set_error first. 🧚


✨ "flush" ✨

(Optional)

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

Your flush body should do a sync(2) or fdatasync(2) or an equivalent on the backing store. flags is always 0.

If the flush fails, throw an exception, optionally using nbdkit.set_error first. πŸ’–


✨ "trim" ✨

(Optional)

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

Your trim body should "punch a hole" in the backing store (iconic behaviour honestly). flags may contain nbdkit.FLAG_FUA. If the trim fails, throw an exception, optionally using nbdkit.set_error first. 🌸


✨ "zero" ✨

(Optional)

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

Your zero body should make sure that count bytes of the disk, starting at offset, will read back as zero. flags is a bitmask which may include nbdkit.FLAG_MAY_TRIM, nbdkit.FLAG_FUA, 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, throw an exception, optionally using nbdkit.set_error first. And listen β€” specifically, if you wanna automatically fall back to pwrite (maybe because there's literally nothing to optimize if flags & nbdkit.FLAG_MAY_TRIM is false), use nbdkit.set_error(errno.EOPNOTSUPP). That's the cheat code right there. πŸ”₯


✨ "cache" ✨

(Optional)

def cache(h, count, offset, flags):
  # no return value

Your cache body should prefetch data in the indicated range. Get ahead of it, main character energy. πŸ¦‹

If the cache operation fails, throw an exception, optionally using nbdkit.set_error first. 😭


✨ "extents" ✨

(Optional)

def extents(h, count, offset, flags):
  # return an iterable object (eg. list) of
  # (offset, length, type) tuples:
  return [ (off1, len1, type1), (off2, len2, type2), ... ]

🌈 Missing callbacks 🌈

Things that just... aren't here. Not me listing them but it's important, iykyk:

  • Missing: "load" β€” you don't need this one, because you can just use regular Python mechanisms like top level statements to run code when the module is loaded. Slay, honestly. ✨
  • Missing: "unload" β€” also missing! But in nbdkit β‰₯ 1.28 you can put code in the cleanup() function and it'll run when nbdkit exits. On earlier versions of nbdkit, using a Python atexit handler is recommended. πŸ’œ
  • Missing: "name", "version", "longname", "description", "config_help", "magic_config_key" β€” these are not yet supported. That's it, that's the tea. πŸ“

πŸ“ FILES πŸ“

  • $plugindir/nbdkit-python-plugin.so β€” the plugin itself, the whole point πŸŽ€

Use nbdkit --dump-config to find the location of $plugindir.


⭐ VERSION ⭐

"nbdkit-python-plugin" first appeared in nbdkit 1.2. Baby plugin, grew up so fast πŸ₯Ή


πŸ‘€ SEE ALSO πŸ‘€

nbdkit(1), nbdkit-plugin(3), python(1).


πŸ‘‘ AUTHORS πŸ‘‘

  • Eric Blake
  • Richard W.M. Jones
  • Nir Soffer

Legends, all three. Respect. πŸ’…


Copyright Red Hat


πŸ“œ LICENSE πŸ“œ

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.

And that's a wrap bestie πŸ’– this page is from nbdkit-1.48.1, dated 2026-08-29, and you've been reading nbdkit-python-plugin(3). go write something iconic 🫢✨

πŸ“¦ where this page comes from