---
TODO | 3 +
configure.ac | 1 +
docs/nbdkit.pod | 1 +
filters/Makefile.am | 1 +
filters/cache/Makefile.am | 62 ++++
filters/cache/cache.c | 525 ++++++++++++++++++++++++++++++++++
filters/cache/nbdkit-cache-filter.pod | 122 ++++++++
tests/Makefile.am | 3 +
tests/test-cache.sh | 88 ++++++
9 files changed, 806 insertions(+)
diff --git a/TODO b/TODO
index a00d4fb..b8ca74c 100644
--- a/TODO
+++ b/TODO
@@ -39,6 +39,9 @@ Suggestions for filters
* injecting artificial errors for testing clients
+* nbdkit-cache-filter needs limits on the maximum size of the cache;
+ it could also do with a sensible replacement policy, etc.
+
Composing nbdkit
----------------
diff --git a/configure.ac b/configure.ac
index 1091d27..2e69688 100644
--- a/configure.ac
+++ b/configure.ac
@@ -513,6 +513,7 @@ AC_CONFIG_FILES([Makefile
plugins/vddk/Makefile
plugins/xz/Makefile
filters/Makefile
+ filters/cache/Makefile
filters/cow/Makefile
filters/delay/Makefile
filters/offset/Makefile
diff --git a/docs/nbdkit.pod b/docs/nbdkit.pod
index c7a1bd7..7674138 100644
--- a/docs/nbdkit.pod
+++ b/docs/nbdkit.pod
@@ -898,6 +898,7 @@ L<nbdkit-xz-plugin(1)>.
Filters:
+L<nbdkit-cache-filter(1)>,
L<nbdkit-cow-filter(1)>,
L<nbdkit-delay-filter(1)>,
L<nbdkit-offset-filter(1)>,
diff --git a/filters/Makefile.am b/filters/Makefile.am
index 7e6fe5a..9996d77 100644
--- a/filters/Makefile.am
+++ b/filters/Makefile.am
@@ -31,6 +31,7 @@
# SUCH DAMAGE.
SUBDIRS = \
+ cache \
cow \
delay \
offset \
diff --git a/filters/cache/Makefile.am b/filters/cache/Makefile.am
new file mode 100644
index 0000000..a371c37
--- /dev/null
+++ b/filters/cache/Makefile.am
@@ -0,0 +1,62 @@
+# nbdkit
+# Copyright (C) 2018 Red Hat Inc.
+# All rights reserved.
+#
+# 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.
+
+EXTRA_DIST = nbdkit-cache-filter.pod
+
+CLEANFILES = *~
+
+filterdir = $(libdir)/nbdkit/filters
+
+filter_LTLIBRARIES = nbdkit-cache-filter.la
+
+nbdkit_cache_filter_la_SOURCES = \
+ cache.c \
+ $(top_srcdir)/include/nbdkit-filter.h
+
+nbdkit_cache_filter_la_CPPFLAGS = \
+ -I$(top_srcdir)/include
+nbdkit_cache_filter_la_CFLAGS = \
+ $(WARNINGS_CFLAGS)
+nbdkit_cache_filter_la_LDFLAGS = \
+ -module -avoid-version -shared
+
+if HAVE_POD2MAN
+
+man_MANS = nbdkit-cache-filter.1
+CLEANFILES += $(man_MANS)
+
+nbdkit-cache-filter.1: nbdkit-cache-filter.pod
+ $(POD2MAN) $(POD2MAN_ARGS) --section=1 --name=`basename $@ .1` $< $@.t && \
+ if grep 'POD ERROR' $@.t; then rm $@.t; exit 1; fi && \
+ mv $@.t $@
+
+endif
diff --git a/filters/cache/cache.c b/filters/cache/cache.c
new file mode 100644
index 0000000..7410f0d
--- /dev/null
+++ b/filters/cache/cache.c
@@ -0,0 +1,525 @@
+/* nbdkit
+ * Copyright (C) 2018 Red Hat Inc.
+ * All rights reserved.
+ *
+ * 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.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <inttypes.h>
+#include <alloca.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <sys/types.h>
+#include <sys/ioctl.h>
+
+#include <nbdkit-filter.h>
+
+#define DIV_ROUND_UP(n, d) (((n) + (d) - 1) / (d))
+
+/* XXX See design comment in filters/cow/cow.c. */
+#define THREAD_MODEL NBDKIT_THREAD_MODEL_SERIALIZE_ALL_REQUESTS
+
+/* Size of a block in the cache. A 4K block size means that we need
+ * 64 MB of memory to store the bitmaps for a 1 TB underlying image.
+ */
+#define BLKSIZE 4096
+
+/* The cache. */
+static int fd = -1;
+
+/* Bitmap. There are two bits per block which are updated as we read,
+ * write back or write through blocks.
+ *
+ * 00 = not in cache
+ * 01 = block cached and clean
+ * 10 = <unused>
+ * 11 = block cached and dirty
+ */
+static uint8_t *bitmap;
+
+/* Size of the bitmap in bytes. */
+static uint64_t bm_size;
+
+enum bm_entry {
+ BLOCK_NOT_CACHED = 0,
+ BLOCK_CLEAN = 1,
+ BLOCK_DIRTY = 3,
+};
+
+/* Caching mode. */
+static enum cache_mode {
+ CACHE_MODE_WRITEBACK,
+ CACHE_MODE_WRITETHROUGH,
+ CACHE_MODE_UNSAFE,
+} cache_mode = CACHE_MODE_WRITEBACK;
+
+static void
+cache_load (void)
+{
+ const char *tmpdir;
+ size_t len;
+ char *template;
+
+ tmpdir = getenv ("TMPDIR");
+ if (!tmpdir)
+ tmpdir = "/var/tmp";
+
+ nbdkit_debug ("cache: temporary directory for cache: %s", tmpdir);
+
+ len = strlen (tmpdir) + 8;
+ template = alloca (len);
+ snprintf (template, len, "%s/XXXXXX", tmpdir);
+
+ fd = mkostemp (template, O_CLOEXEC);
+ if (fd == -1) {
+ nbdkit_error ("mkostemp: %s: %m", tmpdir);
+ exit (EXIT_FAILURE);
+ }
+
+ unlink (template);
+}
+
+static void
+cache_unload (void)
+{
+ if (fd >= 0)
+ close (fd);
+}
+
+static int
+cache_config (nbdkit_next_config *next, void *nxdata,
+ const char *key, const char *value)
+{
+ if (strcmp (key, "cache") == 0) {
+ if (strcmp (value, "writeback") == 0) {
+ cache_mode = CACHE_MODE_WRITEBACK;
+ return 0;
+ }
+ else if (strcmp (value, "writethrough") == 0) {
+ cache_mode = CACHE_MODE_WRITETHROUGH;
+ return 0;
+ }
+ else if (strcmp (value, "unsafe") == 0) {
+ cache_mode = CACHE_MODE_UNSAFE;
+ return 0;
+ }
+ else {
+ nbdkit_error ("invalid cache parameter, should be
writeback|writethrough|unsafe");
+ return -1;
+ }
+ }
+ else {
+ return next (nxdata, key, value);
+ }
+}
+
+static void *
+cache_open (nbdkit_next_open *next, void *nxdata, int readonly)
+{
+ /* We don't use the handle, so this just provides a non-NULL
+ * pointer that we can return.
+ */
+ static int handle;
+
+ if (next (nxdata, readonly) == -1)
+ return NULL;
+
+ return &handle;
+}
+
+/* Allocate or resize the cache file and bitmap. */
+static int
+blk_set_size (uint64_t new_size)
+{
+ uint8_t *new_bm;
+ const size_t old_bm_size = bm_size;
+ size_t new_bm_size = DIV_ROUND_UP (new_size, BLKSIZE*8/2);
+
+ new_bm = realloc (bitmap, new_bm_size);
+ if (new_bm == NULL) {
+ nbdkit_error ("realloc: %m");
+ return -1;
+ }
+ bitmap = new_bm;
+ bm_size = new_bm_size;
+ if (old_bm_size < new_bm_size)
+ memset (&bitmap[old_bm_size], 0, new_bm_size-old_bm_size);
+
+ nbdkit_debug ("cache: bitmap resized to %" PRIu64 " bytes",
new_bm_size);
+
+ if (ftruncate (fd, new_size) == -1) {
+ nbdkit_error ("ftruncate: %m");
+ return -1;
+ }
+
+ return 0;
+}
+
+/* Get the file size and ensure the cache is the correct size. */
+static int64_t
+cache_get_size (struct nbdkit_next_ops *next_ops, void *nxdata,
+ void *handle)
+{
+ int64_t size;
+
+ size = next_ops->get_size (nxdata);
+ if (size == -1)
+ return -1;
+
+ nbdkit_debug ("cache: underlying file size: %" PRIi64, size);
+
+ if (blk_set_size (size))
+ return -1;
+
+ return size;
+}
+
+/* Force an early call to cache_get_size, consequently truncating the
+ * cache to the correct size.
+ */
+static int
+cache_prepare (struct nbdkit_next_ops *next_ops, void *nxdata,
+ void *handle)
+{
+ int64_t r;
+
+ r = cache_get_size (next_ops, nxdata, handle);
+ return r >= 0 ? 0 : -1;
+}
+
+/* Return true if the block is allocated. Consults the bitmap. */
+static enum bm_entry
+blk_get_bitmap_entry (uint64_t blknum)
+{
+ uint64_t bm_offset = blknum / 4;
+ uint64_t bm_bit = 2 * (blknum % 4);
+
+ if (bm_offset >= bm_size) {
+ nbdkit_debug ("blk_get_bitmap_entry: block number is out of range");
+ return BLOCK_NOT_CACHED;
+ }
+
+ return (bitmap[bm_offset] & (3 << bm_bit)) >> bm_bit;
+}
+
+/* Update cache state of a block. */
+static void
+blk_set_bitmap_entry (uint64_t blknum, enum bm_entry state)
+{
+ uint64_t bm_offset = blknum / 4;
+ uint64_t bm_bit = 2 * (blknum % 4);
+
+ if (bm_offset >= bm_size) {
+ nbdkit_debug ("blk_set_bitmap_entry: block number is out of range");
+ return;
+ }
+
+ bitmap[bm_offset] |= (unsigned) state << bm_bit;
+}
+
+/* These are the block operations. They always read or write a single
+ * whole block of size ‘blksize’.
+ */
+static int
+blk_read (struct nbdkit_next_ops *next_ops, void *nxdata,
+ uint64_t blknum, uint8_t *block)
+{
+ off_t offset = blknum * BLKSIZE;
+ enum bm_entry state = blk_get_bitmap_entry (blknum);
+
+ nbdkit_debug ("cache: blk_read block %" PRIu64 " (offset %" PRIu64
") is %s",
+ blknum, (uint64_t) offset,
+ state == BLOCK_NOT_CACHED ? "not cached" :
+ state == BLOCK_CLEAN ? "clean" :
+ state == BLOCK_DIRTY ? "dirty" :
+ "unknown");
+
+ if (state == BLOCK_NOT_CACHED) /* Read underlying plugin. */
+ return next_ops->pread (nxdata, block, BLKSIZE, offset);
+ else { /* Read cache. */
+ if (pread (fd, block, BLKSIZE, offset) == -1) {
+ nbdkit_error ("pread: %m");
+ return -1;
+ }
+ return 0;
+ }
+}
+
+/* Write to the cache and the plugin. */
+static int
+blk_writethrough (struct nbdkit_next_ops *next_ops, void *nxdata,
+ uint64_t blknum, const uint8_t *block)
+{
+ off_t offset = blknum * BLKSIZE;
+
+ nbdkit_debug ("cache: blk_writethrough block %" PRIu64
+ " (offset %" PRIu64 ")",
+ blknum, (uint64_t) offset);
+
+ if (pwrite (fd, block, BLKSIZE, offset) == -1) {
+ nbdkit_error ("pwrite: %m");
+ return -1;
+ }
+
+ if (next_ops->pwrite (nxdata, block, BLKSIZE, offset) == -1)
+ return -1;
+
+ blk_set_bitmap_entry (blknum, BLOCK_CLEAN);
+
+ return 0;
+}
+
+/* Write to the cache only. */
+static int
+blk_writeback (struct nbdkit_next_ops *next_ops, void *nxdata,
+ uint64_t blknum, const uint8_t *block)
+{
+ off_t offset;
+
+ if (cache_mode == CACHE_MODE_WRITETHROUGH)
+ return blk_writethrough (next_ops, nxdata, blknum, block);
+
+ offset = blknum * BLKSIZE;
+
+ nbdkit_debug ("cache: blk_writeback block %" PRIu64
+ " (offset %" PRIu64 ")",
+ blknum, (uint64_t) offset);
+
+ if (pwrite (fd, block, BLKSIZE, offset) == -1) {
+ nbdkit_error ("pwrite: %m");
+ return -1;
+ }
+ blk_set_bitmap_entry (blknum, BLOCK_DIRTY);
+
+ return 0;
+}
+
+/* Read data. */
+static int
+cache_pread (struct nbdkit_next_ops *next_ops, void *nxdata,
+ void *handle, void *buf, uint32_t count, uint64_t offset)
+{
+ uint8_t *block;
+
+ block = malloc (BLKSIZE);
+ if (block == NULL) {
+ nbdkit_error ("malloc: %m");
+ return -1;
+ }
+
+ while (count > 0) {
+ uint64_t blknum, blkoffs, n;
+
+ blknum = offset / BLKSIZE; /* block number */
+ blkoffs = offset % BLKSIZE; /* offset within the block */
+ n = BLKSIZE - blkoffs; /* max bytes we can read from this block */
+ if (n > count)
+ n = count;
+
+ if (blk_read (next_ops, nxdata, blknum, block) == -1) {
+ free (block);
+ return -1;
+ }
+
+ memcpy (buf, &block[blkoffs], n);
+
+ buf += n;
+ count -= n;
+ offset += n;
+ }
+
+ free (block);
+ return 0;
+}
+
+/* Write data. */
+static int
+cache_pwrite (struct nbdkit_next_ops *next_ops, void *nxdata,
+ void *handle, const void *buf, uint32_t count, uint64_t offset)
+{
+ uint8_t *block;
+
+ block = malloc (BLKSIZE);
+ if (block == NULL) {
+ nbdkit_error ("malloc: %m");
+ return -1;
+ }
+
+ while (count > 0) {
+ uint64_t blknum, blkoffs, n;
+
+ blknum = offset / BLKSIZE; /* block number */
+ blkoffs = offset % BLKSIZE; /* offset within the block */
+ n = BLKSIZE - blkoffs; /* max bytes we can read from this block */
+ if (n > count)
+ n = count;
+
+ /* Do a read-modify-write operation on the current block. */
+ if (blk_read (next_ops, nxdata, blknum, block) == -1) {
+ free (block);
+ return -1;
+ }
+ memcpy (&block[blkoffs], buf, n);
+ if (blk_writeback (next_ops, nxdata, blknum, block) == -1) {
+ free (block);
+ return -1;
+ }
+
+ buf += n;
+ count -= n;
+ offset += n;
+ }
+
+ free (block);
+ return 0;
+}
+
+/* Zero data. */
+static int
+cache_zero (struct nbdkit_next_ops *next_ops, void *nxdata,
+ void *handle, uint32_t count, uint64_t offset, int may_trim)
+{
+ uint8_t *block;
+
+ block = malloc (BLKSIZE);
+ if (block == NULL) {
+ nbdkit_error ("malloc: %m");
+ return -1;
+ }
+
+ while (count > 0) {
+ uint64_t blknum, blkoffs, n;
+
+ blknum = offset / BLKSIZE; /* block number */
+ blkoffs = offset % BLKSIZE; /* offset within the block */
+ n = BLKSIZE - blkoffs; /* max bytes we can read from this block */
+ if (n > count)
+ n = count;
+
+ if (blk_read (next_ops, nxdata, blknum, block) == -1) {
+ free (block);
+ return -1;
+ }
+ memset (&block[blkoffs], 0, n);
+ if (blk_writeback (next_ops, nxdata, blknum, block) == -1) {
+ free (block);
+ return -1;
+ }
+
+ count -= n;
+ offset += n;
+ }
+
+ free (block);
+ return 0;
+}
+
+/* Flush: Go through all the dirty blocks, flushing them to disk. */
+static int
+cache_flush (struct nbdkit_next_ops *next_ops, void *nxdata, void *handle)
+{
+ uint8_t *block = NULL;
+ uint64_t i, j;
+ uint64_t blknum;
+ enum bm_entry state;
+ unsigned errors = 0;
+
+ if (cache_mode == CACHE_MODE_UNSAFE)
+ return 0;
+
+ /* In theory if cache_mode == CACHE_MODE_WRITETHROUGH then there
+ * should be no dirty blocks. However we go through the cache here
+ * to be sure. Also we still need to issue the flush to the
+ * underlying storage.
+ */
+
+ for (i = 0; i < bm_size; ++i) {
+ if (bitmap[i] != 0) {
+ /* The bitmap stores information about 4 blocks per byte,
+ * therefore ...
+ */
+ for (j = 0; j < 4; ++j) {
+ blknum = i*4+j;
+ state = blk_get_bitmap_entry (blknum);
+ if (state == BLOCK_DIRTY) {
+ /* Lazily allocate the bounce buffer. */
+ if (!block) {
+ block = malloc (BLKSIZE);
+ if (block == NULL) {
+ nbdkit_error ("malloc: %m");
+ return -1;
+ }
+ }
+ /* Perform a read + writethrough which will read from the
+ * cache and write it through to the underlying storage.
+ */
+ if (blk_read (next_ops, nxdata, blknum, block) == -1 ||
+ blk_writethrough (next_ops, nxdata, blknum, block)) {
+ nbdkit_error ("cache: flush of block %" PRIu64 " failed",
blknum);
+ errors++;
+ }
+ }
+ }
+ }
+ }
+
+ free (block);
+
+ /* Now issue a flush request to the underlying storage. */
+ if (next_ops->flush (nxdata) == -1)
+ errors++;
+
+ return errors == 0 ? 0 : -1;
+}
+
+static struct nbdkit_filter filter = {
+ .name = "cache",
+ .longname = "nbdkit caching filter",
+ .version = PACKAGE_VERSION,
+ .load = cache_load,
+ .unload = cache_unload,
+ .config = cache_config,
+ .open = cache_open,
+ .prepare = cache_prepare,
+ .get_size = cache_get_size,
+ .pread = cache_pread,
+ .pwrite = cache_pwrite,
+ .zero = cache_zero,
+ .flush = cache_flush,
+};
+
+NBDKIT_REGISTER_FILTER(filter)
diff --git a/filters/cache/nbdkit-cache-filter.pod
b/filters/cache/nbdkit-cache-filter.pod
new file mode 100644
index 0000000..a59c54e
--- /dev/null
+++ b/filters/cache/nbdkit-cache-filter.pod
@@ -0,0 +1,122 @@
+=encoding utf8
+
+=head1 NAME
+
+nbdkit-cache-filter - nbdkit caching filter
+
+=head1 SYNOPSIS
+
+ nbdkit --filter=cache plugin [cache=writeback|writethrough|unsafe]
+ [plugin-args...]
+
+=head1 DESCRIPTION
+
+C<nbdkit-cache-filter> is a filter that adds caching on top of a
+plugin. This is useful if a plugin is slow or expensive to use,
+because nbdkit will try to minimize requests to the plugin by caching
+previous requests.
+
+Note that many NBD I<clients> are able to do caching, and because the
+caching happens on the client side it will usually be more effective
+than caching inside the server. This filter can be used if the client
+does not have effective caching, or (with C<cache=unsafe>) to defeat
+flush requests from the client (which is unsafe and can cause data
+loss, as the name suggests).
+
+=head1 PARAMETERS
+
+=over 4
+
+=item B<cache=writeback>
+
+Store writes in the cache. They are not written to the plugin unless
+an explicit flush is done by the client.
+
+This is the default caching mode, and is safe if your client issues
+flush requests correctly (which is true for modern Linux and other
+well-written NBD clients).
+
+=item B<cache=writethrough>
+
+Always force writes through to the plugin.
+
+This makes the cache less effective, but is necessary if your client
+does not issue correct flush requests.
+
+=item B<cache=unsafe>
+
+Ignore flush requests. Never write to the plugin unless the cache
+grows too large.
+
+This is dangerous and can cause data loss, but this may be acceptable
+if you only use it for testing or with data that you don't care about
+or can cheaply reconstruct.
+
+=back
+
+=head1 ENVIRONMENT VARIABLES
+
+=over 4
+
+=item C<TMPDIR>
+
+The cache is stored in a temporary file located in C</var/tmp> by
+default. You can override this location by setting the C<TMPDIR>
+environment variable before starting nbdkit.
+
+=back
+
+=head1 SEE ALSO
+
+L<nbdkit(1)>,
+L<nbdkit-file-plugin(1)>,
+L<nbdkit-filter(3)>,
+L<qemu-img(1)>.
+
+=head1 AUTHORS
+
+Richard W.M. Jones
+
+=head1 COPYRIGHT
+
+Copyright (C) 2018 Red Hat Inc.
+
+=head1 LICENSE
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+=over 4
+
+=item *
+
+Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+=item *
+
+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.
+
+=item *
+
+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.
+
+=back
+
+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.
diff --git a/tests/Makefile.am b/tests/Makefile.am
index b073f22..950f711 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -412,6 +412,9 @@ endif HAVE_RUBY
#----------------------------------------------------------------------
# Tests of filters.
+# cache filter test.
+TESTS += test-cache.sh
+
# cow filter test.
TESTS += test-cow.sh
diff --git a/tests/test-cache.sh b/tests/test-cache.sh
new file mode 100755
index 0000000..c950236
--- /dev/null
+++ b/tests/test-cache.sh
@@ -0,0 +1,88 @@
+#!/bin/bash -
+# nbdkit
+# Copyright (C) 2018 Red Hat Inc.
+# All rights reserved.
+#
+# 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.
+
+set -e
+
+files="cache.img cache.sock cache.pid"
+rm -f $files
+
+# Create an empty base image.
+truncate -s 1G cache.img
+
+# Run nbdkit with the caching filter.
+nbdkit -P cache.pid -U cache.sock --filter cache file file=cache.img
+
+# We may have to wait a short time for the pid file to appear.
+for i in `seq 1 10`; do
+ if test -f cache.pid; then
+ break
+ fi
+ sleep 1
+done
+if ! test -f cache.pid; then
+ echo "$0: PID file was not created"
+ exit 1
+fi
+
+pid="$(cat cache.pid)"
+
+# Kill the nbdkit process on exit.
+cleanup ()
+{
+ status=$?
+
+ kill $pid
+ rm -f $files
+
+ exit $status
+}
+trap cleanup INT QUIT TERM EXIT ERR
+
+# Open the overlay and perform some operations.
+guestfish --format=raw -a 'nbd://?socket=cache.sock' <<'EOF'
+ run
+ part-disk /dev/sda gpt
+ mkfs ext4 /dev/sda1
+ mount /dev/sda1 /
+ fill-dir / 10000
+ fill-pattern "abcde" 5M /large
+ write /hello "hello, world"
+EOF
+
+# Check the last files we created exist.
+guestfish --ro -a cache.img -m /dev/sda1 <<'EOF'
+ cat /hello
+ cat /large | cat >/dev/null
+EOF
+
+# The cleanup() function is called implicitly on exit.
--
2.15.1