[Libguestfs] [PATCH nbdkit 3/4] plugins: Add new "ext2" plugin, for accessing ext2, ext3 or ext4 filesystems.

Richard W.M. Jones rjones at redhat.com
Thu Jun 7 15:48:56 UTC 2018


---
 README                              |   6 +
 configure.ac                        |  24 +++
 docs/nbdkit.pod                     |   1 +
 plugins/Makefile.am                 |   1 +
 plugins/ext2/Makefile.am            |  68 +++++++
 plugins/ext2/ext2.c                 | 359 ++++++++++++++++++++++++++++++++++++
 plugins/ext2/nbdkit-ext2-plugin.pod | 130 +++++++++++++
 7 files changed, 589 insertions(+)

diff --git a/README b/README
index baa29fc..e58df8b 100644
--- a/README
+++ b/README
@@ -65,6 +65,12 @@ For the libguestfs plugin, and to run the test suite:
 
  - guestfish (from libguestfs)
 
+For the ext2 plugin:
+
+ - ext2fs
+
+ - com_err
+
 For the VDDK plugin:
 
  - VDDK (see plugins/vddk/README.VDDK)
diff --git a/configure.ac b/configure.ac
index 4bd9aac..c86c6ce 100644
--- a/configure.ac
+++ b/configure.ac
@@ -426,6 +426,29 @@ dnl Check for guestfish (only needed for some of the tests).
 AC_CHECK_PROG([GUESTFISH], [guestfish], [guestfish], [no])
 AM_CONDITIONAL([HAVE_GUESTFISH], [test "x$GUESTFISH" != "xno"])
 
+dnl Check for ext2fs and com_err, for the ext2 plugin.
+AC_ARG_WITH([ext2],[
+    AS_HELP_STRING([--without-ext2],
+                   [disable ext2 plugin @<:@default=check@:>@])],
+    [],
+    [with_ext2=check])
+AS_IF([test "$with_ext2" != "no"], [
+    PKG_CHECK_MODULES([EXT2FS], [ext2fs], [
+        AC_SUBST([EXT2FS_CFLAGS])
+        AC_SUBST([EXT2FS_LIBS])
+        AC_DEFINE([HAVE_EXT2FS],[1],[ext2fs found at compile time.])
+    ],
+    [AC_MSG_WARN([ext2fs not found, ext2 plugin will be disabled])])
+    PKG_CHECK_MODULES([COM_ERR], [com_err], [
+        AC_SUBST([COM_ERR_CFLAGS])
+        AC_SUBST([COM_ERR_LIBS])
+        AC_DEFINE([HAVE_COM_ERR],[1],[com_err found at compile time.])
+    ],
+    [AC_MSG_WARN([com_err not found, ext2 plugin will be disabled])])
+])
+AM_CONDITIONAL([HAVE_EXT2],
+               [test "x$EXT2FS_LIBS" != "x" && test "x$COM_ERR_LIBS" != "x"])
+
 dnl See plugins/vddk/README.VDDK.
 AC_CHECK_SIZEOF([size_t])
 AS_IF([test "x$ac_cv_sizeof_size_t" = "x4"],[bits=32],[bits=64])
@@ -511,6 +534,7 @@ AC_CONFIG_FILES([Makefile
                  plugins/example2/Makefile
                  plugins/example3/Makefile
                  plugins/example4/Makefile
+                 plugins/ext2/Makefile
                  plugins/file/Makefile
                  plugins/guestfs/Makefile
                  plugins/gzip/Makefile
diff --git a/docs/nbdkit.pod b/docs/nbdkit.pod
index a515353..6ba981b 100644
--- a/docs/nbdkit.pod
+++ b/docs/nbdkit.pod
@@ -909,6 +909,7 @@ L<nbdkit-example1-plugin(1)>,
 L<nbdkit-example2-plugin(1)>,
 L<nbdkit-example3-plugin(1)>,
 L<nbdkit-example4-plugin(1)>,
+L<nbdkit-ext2-plugin(1)>,
 L<nbdkit-file-plugin(1)>,
 L<nbdkit-guestfs-plugin(1)>,
 L<nbdkit-gzip-plugin(1)>,
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index ba67feb..328bc01 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -36,6 +36,7 @@ SUBDIRS = \
 	example2 \
 	example3 \
 	example4 \
+	ext2 \
 	file \
 	guestfs \
 	gzip \
diff --git a/plugins/ext2/Makefile.am b/plugins/ext2/Makefile.am
new file mode 100644
index 0000000..525013f
--- /dev/null
+++ b/plugins/ext2/Makefile.am
@@ -0,0 +1,68 @@
+# nbdkit
+# Copyright (C) 2017-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-ext2-plugin.pod
+
+CLEANFILES = *~
+
+plugindir = $(libdir)/nbdkit/plugins
+
+if HAVE_EXT2
+
+plugin_LTLIBRARIES = nbdkit-ext2-plugin.la
+
+nbdkit_ext2_plugin_la_SOURCES = \
+	ext2.c \
+	$(top_srcdir)/include/nbdkit-plugin.h
+
+nbdkit_ext2_plugin_la_CPPFLAGS = \
+	-I$(top_srcdir)/include
+nbdkit_ext2_plugin_la_CFLAGS = \
+	$(WARNINGS_CFLAGS) \
+	$(EXT2FS_CFLAGS) $(COM_ERR_CFLAGS)
+nbdkit_ext2_plugin_la_LIBADD = \
+	$(EXT2FS_LIBS) $(COM_ERR_LIBS)
+nbdkit_ext2_plugin_la_LDFLAGS = \
+	-module -avoid-version -shared
+
+if HAVE_POD2MAN
+
+man_MANS = nbdkit-ext2-plugin.1
+CLEANFILES += $(man_MANS)
+
+nbdkit-ext2-plugin.1: nbdkit-ext2-plugin.pod
+	$(POD2MAN) $(POD2MAN_ARGS) --section=1 --name=`basename $@ .1` $< $@.t && \
+	if grep 'POD ERROR' $@.t; then rm $@.t; exit 1; fi && \
+	mv $@.t $@
+
+endif
+endif
diff --git a/plugins/ext2/ext2.c b/plugins/ext2/ext2.c
new file mode 100644
index 0000000..1e8f6dd
--- /dev/null
+++ b/plugins/ext2/ext2.c
@@ -0,0 +1,359 @@
+/* nbdkit
+ * Copyright (C) 2017-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 <inttypes.h>
+
+/* Inlining is broken in the ext2fs header file.  Disable it by
+ * defining the following:
+ */
+#define NO_INLINE_FUNCS
+#include <ext2fs.h>
+
+#define NBDKIT_API_VERSION 2
+
+#include <nbdkit-plugin.h>
+
+/* Disk image and filename parameters. */
+static char *disk;
+static char *file;
+
+static void
+ext2_load (void)
+{
+  initialize_ext2_error_table ();
+}
+
+static void
+ext2_unload (void)
+{
+  free (disk);
+  free (file);
+}
+
+static int
+ext2_config (const char *key, const char *value)
+{
+  if (strcmp (key, "disk") == 0) {
+    if (disk != NULL) {
+      nbdkit_error ("disk parameter specified more than once");
+      return -1;
+    }
+    disk = nbdkit_absolute_path (value);
+    if (disk == NULL)
+      return -1;
+  }
+  else if (strcmp (key, "file") == 0) {
+    if (file != NULL) {
+      nbdkit_error ("file parameter specified more than once");
+      return -1;
+    }
+    file = strdup (value);
+    if (file == NULL) {
+      nbdkit_error ("strdup: %m");
+      return -1;
+    }
+  }
+  else {
+    nbdkit_error ("unknown parameter '%s'", key);
+    return -1;
+  }
+
+  return 0;
+}
+
+static int
+ext2_config_complete (void)
+{
+  if (disk == NULL || file == NULL) {
+    nbdkit_error ("you must supply disk=<DISK> and file=<FILE> parameters "
+                  "after the plugin name on the command line");
+    return -1;
+  }
+
+  if (file[0] != '/') {
+    nbdkit_error ("the file parameter must refer to an absolute path");
+    return -1;
+  }
+
+  return 0;
+}
+
+#define ext2_config_help \
+  "disk=<FILENAME>  (required) Raw ext2, ext3 or ext4 filesystem.\n" \
+  "file=<FILENAME>  (required) File to serve inside the disk image."
+
+/* The per-connection handle. */
+struct handle {
+  int readonly;
+  ext2_filsys fs;               /* Filesystem handle. */
+  ext2_ino_t ino;               /* Inode of open file. */
+  ext2_file_t file;             /* File handle. */
+};
+
+/* Create the per-connection handle. */
+static void *
+ext2_open (int readonly)
+{
+  struct handle *h;
+  errcode_t err;
+  int fs_flags;
+  int file_flags;
+  struct ext2_inode inode;
+
+  h = malloc (sizeof *h);
+  if (h == NULL) {
+    nbdkit_error ("malloc: %m");
+    return NULL;
+  }
+
+  h->readonly = readonly;
+
+  fs_flags = 0;
+#ifdef EXT2_FLAG_64BITS
+  fs_flags |= EXT2_FLAG_64BITS;
+#endif
+  if (!readonly)
+    fs_flags |= EXT2_FLAG_RW;
+
+  err = ext2fs_open (disk, fs_flags, 0, 0, unix_io_manager, &h->fs);
+  if (err != 0) {
+    nbdkit_error ("%s: open: %s", disk, error_message (err));
+    goto err0;
+  }
+
+  if (strcmp (file, "/") == 0)
+    /* probably gonna fail, but we'll catch it later */
+    h->ino = EXT2_ROOT_INO;
+  else {
+    err = ext2fs_namei (h->fs, EXT2_ROOT_INO, EXT2_ROOT_INO,
+                        &file[1], &h->ino);
+    if (err != 0) {
+      nbdkit_error ("%s: %s: namei: %s", disk, file, error_message (err));
+      goto err1;
+    }
+  }
+
+  /* Check the file is a regular file.
+   * XXX This won't follow symlinks, we'd have to do that manually.
+   */
+  err = ext2fs_read_inode (h->fs, h->ino, &inode);
+  if (err != 0) {
+    nbdkit_error ("%s: %s: inode: %s", disk, file, error_message (err));
+    goto err1;
+  }
+  if (!LINUX_S_ISREG (inode.i_mode)) {
+    nbdkit_error ("%s: %s: must be a regular file in the disk image",
+                  disk, file);
+    goto err1;
+  }
+
+  file_flags = 0;
+  if (!readonly)
+    file_flags |= EXT2_FILE_WRITE;
+  err = ext2fs_file_open2 (h->fs, h->ino, NULL, file_flags, &h->file);
+  if (err != 0) {
+    nbdkit_error ("%s: %s: open: %s", disk, file, error_message (err));
+    goto err1;
+  }
+
+  return h;
+
+ err1:
+  ext2fs_close (h->fs);
+ err0:
+  free (h);
+  return NULL;
+}
+
+/* Free up the per-connection handle. */
+static void
+ext2_close (void *handle)
+{
+  struct handle *h = handle;
+
+  ext2fs_file_close (h->file);
+  ext2fs_close (h->fs);
+  free (h);
+}
+
+static int
+ext2_can_fua (void *handle)
+{
+  return NBDKIT_FUA_NATIVE;
+}
+
+/* It might be possible to relax this, but it's complicated.
+ *
+ * It's desirable for ‘nbdkit -r’ to behave the same way as
+ * ‘mount -o ro’.  But we don't know the state of the readonly flag
+ * until ext2_open is called (because the NBD client can also request
+ * a readonly connection).  So we could not set the "ro" flag if we
+ * opened the filesystem any earlier (eg in ext2_config).
+ *
+ * So out of necessity we have one ext2_filsys handle per connection,
+ * but if we allowed parallel work on those handles then we would get
+ * data corruption, so we need to serialize connections.
+ */
+#define THREAD_MODEL NBDKIT_THREAD_MODEL_SERIALIZE_CONNECTIONS
+
+/* Get the disk size. */
+static int64_t
+ext2_get_size (void *handle)
+{
+  struct handle *h = handle;
+  errcode_t err;
+  uint64_t size;
+
+  err = ext2fs_file_get_lsize (h->file, (__u64 *) &size);
+  if (err != 0) {
+    nbdkit_error ("%s: %s: lsize: %s", disk, file, error_message (err));
+    return -1;
+  }
+  return (int64_t) size;
+}
+
+/* Read data. */
+static int
+ext2_pread (void *handle, void *buf, uint32_t count, uint64_t offset,
+            uint32_t flags)
+{
+  struct handle *h = handle;
+  errcode_t err;
+  unsigned int got;
+
+  while (count > 0) {
+    /* Although this function weirdly can return the new offset,
+     * examination of the code shows that it never returns anything
+     * different from what we set, so NULL out that parameter.
+     */
+    err = ext2fs_file_llseek (h->file, offset, EXT2_SEEK_SET, NULL);
+    if (err != 0) {
+      nbdkit_error ("%s: %s: llseek: %s", disk, file, error_message (err));
+      return -1;
+    }
+
+    err = ext2fs_file_read (h->file, buf, (unsigned int) count, &got);
+    if (err != 0) {
+      nbdkit_error ("%s: %s: read: %s", disk, file, error_message (err));
+      return -1;
+    }
+
+    buf += got;
+    count -= got;
+    offset += got;
+  }
+
+  return 0;
+}
+
+/* Write data to the file. */
+static int
+ext2_pwrite (void *handle, const void *buf, uint32_t count, uint64_t offset,
+             uint32_t flags)
+{
+  struct handle *h = handle;
+  errcode_t err;
+  unsigned int written;
+
+  while (count > 0) {
+    err = ext2fs_file_llseek (h->file, offset, EXT2_SEEK_SET, NULL);
+    if (err != 0) {
+      nbdkit_error ("%s: %s: llseek: %s", disk, file, error_message (err));
+      return -1;
+    }
+
+    err = ext2fs_file_write (h->file, buf, (unsigned int) count, &written);
+    if (err != 0) {
+      nbdkit_error ("%s: %s: write: %s", disk, file, error_message (err));
+      return -1;
+    }
+
+    buf += written;
+    count -= written;
+    offset += written;
+  }
+
+  if ((flags & NBDKIT_FLAG_FUA) != 0) {
+    err = ext2fs_file_flush (h->file);
+    if (err != 0) {
+      nbdkit_error ("%s: %s: flush: %s", disk, file, error_message (err));
+      return -1;
+    }
+  }
+
+  return 0;
+}
+
+static int
+ext2_flush (void *handle, uint32_t flags)
+{
+  struct handle *h = handle;
+  errcode_t err;
+
+  err = ext2fs_file_flush (h->file);
+  if (err != 0) {
+    nbdkit_error ("%s: %s: flush: %s", disk, file, error_message (err));
+    return -1;
+  }
+
+  return 0;
+}
+
+/* XXX It seems as if we should be able to support trim and zero, if
+ * we could work out how those are implemented in the ext2fs API which
+ * is very obscure.
+ */
+
+static struct nbdkit_plugin plugin = {
+  .name              = "ext2",
+  .version           = PACKAGE_VERSION,
+  .load              = ext2_load,
+  .unload            = ext2_unload,
+  .config            = ext2_config,
+  .config_complete   = ext2_config_complete,
+  .config_help       = ext2_config_help,
+  .open              = ext2_open,
+  .close             = ext2_close,
+  .can_fua           = ext2_can_fua,
+  .get_size          = ext2_get_size,
+  .pread             = ext2_pread,
+  .pwrite            = ext2_pwrite,
+  .flush             = ext2_flush,
+  .errno_is_preserved = 1,
+};
+
+NBDKIT_REGISTER_PLUGIN(plugin)
diff --git a/plugins/ext2/nbdkit-ext2-plugin.pod b/plugins/ext2/nbdkit-ext2-plugin.pod
new file mode 100644
index 0000000..29c76aa
--- /dev/null
+++ b/plugins/ext2/nbdkit-ext2-plugin.pod
@@ -0,0 +1,130 @@
+=encoding utf8
+
+=head1 NAME
+
+nbdkit-ext2-plugin - Read and write files inside ext2, ext3 or ext4 filesystems
+
+=head1 SYNOPSIS
+
+ nbdkit ext2 disk=fs.img file=/disks/disk.raw
+
+ nbdkit --filter=partition ext2 \
+                           disk=disk.img file=/disks/disk.raw \
+                           partition=1
+
+=head1 DESCRIPTION
+
+C<nbdkit-ext2-plugin> is an nbdkit plugin which can read and
+write files inside ext2, ext3 or ext4 filesystem images.
+
+Suppose you have an ext2/3/4 filesystem image called F<fs.img>
+which contains inside itself a file called F<disk.raw>
+inside a directory on the filesystem called F</disks>, then
+you could serve that file over NBD using:
+
+ nbdkit ext2 disk=fs.img file=/disks/disk.raw
+
+Commonly disk images are partitioned.  In that case you must
+use L<nbdkit-partition-filter(1)> to select the partition:
+
+ nbdkit --filter=partition ext2 disk=.. file=.. partition=1
+
+This plugin can both read and write to the file inside the filesystem.
+Use the I<-r> flag to force a readonly connection, but note this does
+I<not> guarantee that no writes are made to the filesystem.  In
+particular we may have to replay the ext3 journal in order to open a
+filesystem even read-only.
+
+The plugin does I<not> support multiple parallel connections, because
+there is a risk of corrupting the filesystem (as if the filesystem was
+mounted by multiple machines).  If a second connection is made to
+nbdkit, it will block until the first connection closes.
+
+The plugin is implemented using the ext2fs library which is provided
+in most Linux distros, and also available as part of the e2fsprogs
+project.
+
+L<nbdkit-guestfs-plugin(1)> is a more generic plugin which can read
+files from all kinds of different filesystem types, even if they are
+partitioned or use logical volumes.  It uses libguestfs instead of
+e2fsprogs.
+
+=head1 PARAMETERS
+
+=over 4
+
+=item B<disk=FILENAME>
+
+The ext2, ext3 or ext4 filesystem, a file on the host.
+
+You could also use a device name here if the filesystem is located on
+a device.  Be careful that the filesystem is not being accessed in
+parallel by another program and is not mounted, as that will almost
+certainly result in disk corruption in the filesystem.
+
+The plugin expects a raw filesystem.  If the file/device is
+partitioned, use L<nbdkit-partition-filter(1)>.
+
+=item B<file=PATH>
+
+The full path of the file within the filesystem that will be exposed
+over NBD.  The path must be absolute (starts with C</>).
+
+=back
+
+=head1 SEE ALSO
+
+L<nbdkit(1)>,
+L<nbdkit-plugin(3)>,
+L<nbdkit-partition-filter(1)>,
+L<nbdkit-guestfs-plugin(1)>,
+L<http://e2fsprogs.sourceforge.net/>,
+L<fuse2fs(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.
-- 
2.16.2




More information about the Libguestfs mailing list