[Libguestfs] [PATCH for discussion only 2/3] handle: Implement mutexes.

Richard W.M. Jones rjones at redhat.com
Mon Feb 18 21:20:44 UTC 2013


From: "Richard W.M. Jones" <rjones at redhat.com>

These limit the number of concurrent handles which can be used
at once.
---
 fish/guestfish.pod            |   8 ++
 generator/actions.ml          |  87 +++++++++++++++++
 generator/events.ml           |   4 +
 ocaml/t/guestfs_400_events.ml |   5 +-
 po/POTFILES                   |   1 +
 src/Makefile.am               |   1 +
 src/guestfs-internal.h        |  11 +++
 src/guestfs.pod               | 120 ++++++++++++++++++++++++
 src/handle.c                  |  20 +++-
 src/launch.c                  |   4 +
 src/mutex.c                   | 213 ++++++++++++++++++++++++++++++++++++++++++
 src/proto.c                   |   1 +
 test-tool/test-tool.c         |   3 +
 13 files changed, 476 insertions(+), 2 deletions(-)
 create mode 100644 src/mutex.c

diff --git a/fish/guestfish.pod b/fish/guestfish.pod
index 5fec2f2..28dde6c 100644
--- a/fish/guestfish.pod
+++ b/fish/guestfish.pod
@@ -1279,6 +1279,14 @@ example:
 
  LIBGUESTFS_MEMSIZE=700
 
+=item LIBGUESTFS_MUTEX_FILE
+
+=item LIBGUESTFS_MUTEX_LIMIT
+
+Set these in order to limit the number of concurrent guestfish
+sessions launched at once.  See L<guestfs(3)/MUTEX> and
+L</set-mutex-limit>.
+
 =item LIBGUESTFS_PATH
 
 Set the path that guestfish uses to search for kernel and initrd.img.
diff --git a/generator/actions.ml b/generator/actions.ml
index 4f18f41..d664a7a 100644
--- a/generator/actions.ml
+++ b/generator/actions.ml
@@ -2694,6 +2694,93 @@ the default.  Else C</var/tmp> is the default." };
     longdesc = "\
 Get the directory used by the handle to store the appliance cache." };
 
+  { defaults with
+    name = "set_mutex_file";
+    style = RErr, [OptString "mutexfile"], [];
+    config_only = true; blocking = false;
+    shortdesc = "set mutex filename to limit concurrent handles";
+    longdesc = "\
+Set the name of the semaphore file used to limit concurrent handles.
+The name must start with a C</> character, and contain no other
+C</> characters (eg. C</guestfs.mutex>).  See L<sem_overview(7)>
+for an explanation.
+
+See L<guestfs(3)/MUTEX>." };
+
+  { defaults with
+    name = "get_mutex_file";
+    style = RConstOptString "mutexfile", [], [];
+    blocking = false;
+    shortdesc = "get mutex filename";
+    longdesc = "\
+Get the name of the semaphore file.
+
+See C<guestfs_set_mutex_file> and L<guestfs(3)/MUTEX>." };
+
+  { defaults with
+    name = "set_mutex_limit";
+    style = RErr, [Int "limit"], [];
+    config_only = true; blocking = false;
+    shortdesc = "set mutex to limit concurrent handles";
+    longdesc = "\
+This sets the limit of the number of concurrent handles that
+may be launched at the same time.  The intention is to limit
+the overall load that libguestfs places on the host.
+
+Note that in the current implementation which uses a POSIX
+semaphore, you have to delete the semaphore file (or reboot the
+host) in order to change this limit.  See L<sem_overview(7)> for
+the location of the semaphore file.  This may be fixed in future.
+
+As well as ordinary positive integers, meaning to limit
+the number of launched handles to C<limit>, various magic
+values are possible too:
+
+=over 4
+
+=item C<limit> E<ge> 1
+
+Limit the number of launched handles to at most C<limit> concurrently.
+
+=item C<0>
+
+Disable limits.
+
+=item C<-2>
+
+Let libguestfs choose a suitable limit based on the free memory
+available on the host when the semaphore is first created.
+
+If there is less memory available than libguestfs thinks is necessary,
+libguestfs will still allow one handle to run (otherwise that handle
+would be waiting forever).
+
+This is probably the best choice for most users.
+
+=item C<limit> E<le> -128
+
+Calculate the maximum number of concurrent handles by dividing
+the free memory by C<-limit>.  This calculation is done when the
+semaphore is first created.
+
+Libguestfs will always allow at least one handle to run
+(otherwise that handle would be waiting forever).
+
+=back
+
+For more information see L<guestfs(3)/MUTEX>." };
+
+  { defaults with
+    name = "get_mutex_limit";
+    style = RInt "limit", [], [];
+    blocking = false;
+    shortdesc = "get mutex limit";
+    longdesc = "\
+This returns the mutex limit.  If not previously set, the
+default is C<0> (meaning the mutex feature is disabled).
+
+See C<guestfs_set_mutex_limit> and L<guestfs(3)/MUTEX>." };
+
 ]
 
 (* daemon_functions are any functions which cause some action
diff --git a/generator/events.ml b/generator/events.ml
index 58c0c54..9c69705 100644
--- a/generator/events.ml
+++ b/generator/events.ml
@@ -39,6 +39,10 @@ let events = [
   "enter";                              (* enter a function *)
 
   "libvirt_auth";                       (* libvirt authentication request *)
+
+  "mutex_blocked";                      (* mutex events *)
+  "mutex_entered";
+  "mutex_released";
 ]
 
 let events = mapi (fun i name -> name, 1 lsl i) events
diff --git a/ocaml/t/guestfs_400_events.ml b/ocaml/t/guestfs_400_events.ml
index be40608..76d27a5 100644
--- a/ocaml/t/guestfs_400_events.ml
+++ b/ocaml/t/guestfs_400_events.ml
@@ -29,7 +29,10 @@ let log g ev eh buf array =
     | Guestfs.EVENT_LIBRARY -> "library"
     | Guestfs.EVENT_TRACE -> "trace"
     | Guestfs.EVENT_ENTER -> "enter"
-    | Guestfs.EVENT_LIBVIRT_AUTH -> "libvirt_auth" in
+    | Guestfs.EVENT_LIBVIRT_AUTH -> "libvirt_auth"
+    | Guestfs.EVENT_MUTEX_BLOCKED -> "mutex_blocked"
+    | Guestfs.EVENT_MUTEX_ENTERED -> "mutex_entered"
+    | Guestfs.EVENT_MUTEX_RELEASED -> "mutex_released" in
 
   let eh : int = Obj.magic eh in
 
diff --git a/po/POTFILES b/po/POTFILES
index 0686966..e3430f2 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -263,6 +263,7 @@ src/libvirt-domain.c
 src/listfs.c
 src/lpj.c
 src/match.c
+src/mutex.c
 src/osinfo.c
 src/private-data.c
 src/proto.c
diff --git a/src/Makefile.am b/src/Makefile.am
index 2c3af05..3410803 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -171,6 +171,7 @@ libguestfs_la_SOURCES = \
 	listfs.c \
 	lpj.c \
 	match.c \
+	mutex.c \
 	osinfo.c \
 	private-data.c \
 	proto.c \
diff --git a/src/guestfs-internal.h b/src/guestfs-internal.h
index 48ab745..00c029e 100644
--- a/src/guestfs-internal.h
+++ b/src/guestfs-internal.h
@@ -23,6 +23,8 @@
 
 #include <libintl.h>
 
+#include <semaphore.h>
+
 #include <rpc/types.h>
 #include <rpc/xdr.h>
 
@@ -292,6 +294,11 @@ struct guestfs_h
   virConnectCredentialPtr requested_credentials;
 #endif
 
+  /* Used by the handle mutex (see src/mutex.c). */
+  char *mutex_file;
+  int mutex_limit;
+  sem_t *semaphore;
+
   /**** Private data for attach-methods. ****/
   /* NB: This cannot be a union because of a pathological case where
    * the user changes attach-method while reusing the handle to launch
@@ -519,6 +526,10 @@ extern char *guestfs___appliance_command_line (guestfs_h *g, const char *applian
 /* launch-appliance.c */
 extern char *guestfs___drive_name (size_t index, char *ret);
 
+/* mutex.c */
+extern int guestfs___acquire_mutex (guestfs_h *g);
+extern void guestfs___release_mutex (guestfs_h *g);
+
 /* inspect.c */
 extern void guestfs___free_inspect_info (guestfs_h *g);
 extern int guestfs___feature_available (guestfs_h *g, const char *feature);
diff --git a/src/guestfs.pod b/src/guestfs.pod
index d21ac8c..bec1249 100644
--- a/src/guestfs.pod
+++ b/src/guestfs.pod
@@ -718,6 +718,100 @@ available from the L</guestfs_inspect_is_live>,
 L</guestfs_inspect_is_netinst> and L</guestfs_inspect_is_multipart>
 calls.
 
+=head2 MUTEX
+
+You can limit the number of launched handles (per host) to avoid
+having too many libguestfs instances running at once.  This is known
+as the handle "mutex".  This works by having a shared semaphore.  If
+the limit is reached, handles wait on this semaphore until other
+instances finish.
+
+The default is no lock file and no limit on the number of concurrent
+handles.
+
+=head3 SETTING THE MUTEX LOCK FILE AND LIMIT
+
+To use this feature, after you open each handle but before calling
+L</guestfs_launch>, set the semaphore filename and limit by calling
+L</guestfs_set_mutex_file> and L</guestfs_set_mutex_limit>.
+
+The semaphore filename is a special type of filename that starts with
+a C</> character and contains no other C</> characters
+(eg. C</guestfs.mutex>).  See L<sem_overview(7)> for an explanation.
+
+The same filename should be passed to every handle.
+
+The limit is simply an integer (the number of handles).  The limit can
+have various magic values too, read the documentation for
+L</guestfs_set_mutex_limit>.
+
+Alternatively, ensure that the following environment variables are set
+globally for every libguestfs user / process: C<LIBGUESTFS_MUTEX_FILE>
+and C<LIBGUESTFS_MUTEX_LIMIT>.
+
+The example below shows how to use the environment variables.  The
+special limit of C<-2> causes libguestfs to choose a suitable limit
+based on the amount of free memory on the host:
+
+ LIBGUESTFS_MUTEX_FILE=/guestfs.mutex
+ LIBGUESTFS_MUTEX_LIMIT=-2
+ export LIBGUESTFS_MUTEX_FILE LIBGUESTFS_MUTEX_LIMIT
+
+=head3 MUTEX CHANGES TO LAUNCH AND CLOSE
+
+When the mutex has been configured, L</guestfs_launch> may block if
+there are too many other handles running at the same time.  Similarly,
+L</guestfs_shutdown> or L</guestfs_close> may release the lock causing
+another handle that was waiting to start launching.
+
+You can see if handles are blocked by enabling debugging or by
+registering for events (see next section).
+
+=head3 MUTEX EVENTS
+
+The API allows callers to receive an event when a handle is blocked
+acquiring the lock, has acquired the lock, or has released the lock.
+Events only let you monitor this; they don't let you change the mutex
+behaviour on the fly.
+
+The three events are C<GUESTFS_EVENT_MUTEX_BLOCKED> (blocked when
+trying to acquire the mutex), C<GUESTFS_EVENT_MUTEX_ENTERED>
+(successfully acquired the mutex), and C<GUESTFS_EVENT_MUTEX_RELEASED>
+(have just released the mutex).  All events have a string payload
+which is the name of the semaphore file (ie. the same string passed to
+L</guestfs_set_mutex_file>).
+
+Note that the C<BLOCKED> event is only generated if the handle
+actually waits, not if the handle goes straight into the mutex.
+
+If mutexes are not configured, then none of these events will be
+generated.
+
+For further information about events, see L</EVENTS>.
+
+=head3 MUTEX IMPLEMENTATION NOTES
+
+Currently the mutex is implemented using POSIX semaphores (this
+implementation detail may change in future).
+
+The mutex is only enabled if the mutex semaphore file is set and the
+limit is set to a non-zero value.
+
+The mutex only affects when handles can be launched.  You can still
+create as many handles as you like, which should be safe because
+handles that are not launched just use a little memory in the current
+process.
+
+The mutex must be enabled on all handles.  If you don't enable it on a
+particular handle, then that handle will ignore the mutex (even if it
+is set on other handles).
+
+Currently, if you want to change the limit then you have to delete the
+semaphore file.
+
+For the magic negative values of the limit, libguestfs uses the output
+of the L<free(1)> command to determine free memory on the host.
+
 =head2 SPECIAL CONSIDERATIONS FOR WINDOWS GUESTS
 
 Libguestfs can mount NTFS partitions.  It does this using the
@@ -2486,6 +2580,24 @@ authentication information.  See L</LIBVIRT AUTHENTICATION> below.
 If no callback is registered: C<virConnectAuthPtrDefault> is
 used (suitable for command-line programs only).
 
+=item GUESTFS_EVENT_MUTEX_BLOCKED
+(payload type: lock file name)
+
+During launch, the handle is blocked waiting for the mutex.
+See L</MUTEX>.
+
+=item GUESTFS_EVENT_MUTEX_ENTERED
+(payload type: lock file name)
+
+During launch, the handle acquired the mutex lock.
+See L</MUTEX>.
+
+=item GUESTFS_EVENT_MUTEX_RELEASED
+(payload type: lock file name)
+
+During shutdown, the handle released the mutex lock.
+See L</MUTEX>.
+
 =back
 
 =head2 EVENT API
@@ -4131,6 +4243,14 @@ example:
 
  LIBGUESTFS_MEMSIZE=700
 
+=item LIBGUESTFS_MUTEX_FILE
+
+=item LIBGUESTFS_MUTEX_LIMIT
+
+Set these in order to limit the number of concurrent libguestfs
+handles running at once.  See L</MUTEX> and
+L</guestfs_set_mutex_limit>.
+
 =item LIBGUESTFS_PATH
 
 Set the path that libguestfs uses to search for a supermin appliance.
diff --git a/src/handle.c b/src/handle.c
index c630daf..6c9b311 100644
--- a/src/handle.c
+++ b/src/handle.c
@@ -144,6 +144,7 @@ guestfs_create_flags (unsigned flags, ...)
   return g;
 
  error:
+  free (g->mutex_file);
   free (g->attach_method_arg);
   free (g->path);
   free (g->qemu);
@@ -157,7 +158,7 @@ parse_environment (guestfs_h *g,
                    char *(*do_getenv) (const void *data, const char *),
                    const void *data)
 {
-  int memsize;
+  int memsize, limit;
   char *str;
 
   /* Don't bother checking the return values of functions
@@ -215,6 +216,20 @@ parse_environment (guestfs_h *g,
       return -1;
   }
 
+  str = do_getenv (data, "LIBGUESTFS_MUTEX_FILE");
+  if (str)
+    guestfs_set_mutex_file (g, str);
+
+  str = do_getenv (data, "LIBGUESTFS_MUTEX_LIMIT");
+  if (str) {
+    if (sscanf (str, "%d", &limit) != 1) {
+      error (g, _("non-numeric value for LIBGUESTFS_MUTEX_LIMIT"));
+      return -1;
+    }
+    if (guestfs_set_mutex_limit (g, limit) == -1)
+      return -1;
+  }
+
   return 0;
 }
 
@@ -326,6 +341,7 @@ guestfs_close (guestfs_h *g)
 
   if (g->pda)
     hash_free (g->pda);
+  free (g->mutex_file);
   free (g->tmpdir);
   free (g->env_tmpdir);
   free (g->int_tmpdir);
@@ -378,6 +394,8 @@ shutdown_backend (guestfs_h *g, int check_for_errors)
   if (g->attach_ops->shutdown (g, check_for_errors) == -1)
     ret = -1;
 
+  guestfs___release_mutex (g);
+
   guestfs___free_drives (g);
 
   g->state = CONFIG;
diff --git a/src/launch.c b/src/launch.c
index 7c37667..0401c42 100644
--- a/src/launch.c
+++ b/src/launch.c
@@ -597,6 +597,10 @@ guestfs__launch (guestfs_h *g)
     return -1;
   }
 
+  /* Acquire mutex if we need to. */
+  if (guestfs___acquire_mutex (g) == -1)
+    return -1;
+
   /* Start the clock ... */
   gettimeofday (&g->launch_t, NULL);
   TRACE0 (launch_start);
diff --git a/src/mutex.c b/src/mutex.c
new file mode 100644
index 0000000..82efc9b
--- /dev/null
+++ b/src/mutex.c
@@ -0,0 +1,213 @@
+/* libguestfs
+ * Copyright (C) 2013 Red Hat Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <semaphore.h>
+#include <errno.h>
+
+#include "guestfs.h"
+#include "guestfs-internal.h"
+#include "guestfs-internal-actions.h"
+#include "guestfs_protocol.h"
+
+int
+guestfs__set_mutex_file (guestfs_h *g, const char *file)
+{
+  free (g->mutex_file);
+  g->mutex_file = NULL;
+
+  if (file)
+    g->mutex_file = safe_strdup (g, file);
+
+  return 0;
+}
+
+const char *
+guestfs__get_mutex_file (guestfs_h *g)
+{
+  return g->mutex_file;
+}
+
+int
+guestfs__set_mutex_limit (guestfs_h *g, int limit)
+{
+  if (limit > 1000) {           /* that would be > 1000 concurrent handles */
+    error (g, _("mutex limit is too large.  If you want it to be unlimited set it to 0.  If you want a larger-but-finite limit then you have to recompile libguestfs."));
+    return -1;
+  }
+  if (limit == -1) {
+    error (g, _("mutex limit cannot be set to -1 (did you mean -2?)"));
+    return -1;
+  }
+  if (limit >= -127 && limit <= -3) {
+    error (g, _("mutex limit cannot be set to -3..-127"));
+    return -1;
+  }
+  if (limit < -10000) {         /* that would be a > 10 GB process limit */
+    error (g, _("mutex limit is too small"));
+    return -1;
+  }
+
+  g->mutex_limit = limit;
+  return 0;
+}
+
+int
+guestfs__get_mutex_limit (guestfs_h *g)
+{
+  return g->mutex_limit;
+}
+
+static int
+mutex_is_enabled (guestfs_h *g)
+{
+  return g->mutex_file != NULL && g->mutex_limit != 0;
+}
+
+static void
+read_free_memory (guestfs_h *g, void *datav, const char *line, size_t len)
+{
+  int *mbytesp = (int *) datav;
+
+  if (sscanf (line, "%d", mbytesp) == -1)
+    *mbytesp = -1;
+}
+
+static int
+get_free_memory (guestfs_h *g)
+{
+  CLEANUP_CMD_CLOSE struct command *cmd = guestfs___new_command (g);
+  int r, mbytes = -1;
+
+  guestfs___cmd_add_string_unquoted (cmd, "free -m | "
+                                     "grep 'buffers/cache' | "
+                                     "awk '{print $NF}'");
+  guestfs___cmd_set_stdout_callback (cmd, read_free_memory, &mbytes, 0);
+  r = guestfs___cmd_run (cmd);
+  if (r == -1)
+    return -1;
+  if (!WIFEXITED (r) || WEXITSTATUS (r) != 0) {
+    error (g, _("failed when trying to read free memory"));
+    return -1;
+  }
+
+  if (mbytes == -1) {
+    error (g, _("unexpected output from 'free -m' command"));
+    return -1;
+  }
+
+  debug (g, "get_free_memory: %d MB free", mbytes);
+
+  return mbytes;
+}
+
+static int
+get_initial_value (guestfs_h *g)
+{
+  int free_mbytes;
+
+  if (g->mutex_limit >= 1)
+    return g->mutex_limit;
+
+  free_mbytes = get_free_memory (g);
+  if (free_mbytes == -1)
+    return -1;
+
+  if (g->mutex_limit == -2) {
+    /* This is an overestimate ... */
+    int estimate_size_per_appliance = DEFAULT_MEMSIZE + 350;
+    return MAX (1, free_mbytes / estimate_size_per_appliance);
+  }
+
+  return MAX (1, free_mbytes / -g->mutex_limit);
+}
+
+/* This is called very early in 'launch' to acquire the mutex. */
+int
+guestfs___acquire_mutex (guestfs_h *g)
+{
+  sem_t *semaphore;
+  int v;
+
+  if (!mutex_is_enabled (g))
+    return 0;
+
+  v = get_initial_value (g);
+  if (v == -1)
+    return -1;
+
+  debug (g, "mutex: initializing semaphore %s with value %d (note that if the semaphore exists already, then this value is ignored)", g->mutex_file, v);
+
+  /* XXX umask */
+  semaphore = sem_open (g->mutex_file, O_CREAT|O_CLOEXEC, 0777, (unsigned) v);
+  if (semaphore == SEM_FAILED) {
+    perrorf (g, _("mutex: sem_open: %s"), g->mutex_file);
+    return -1;
+  }
+
+  if (sem_trywait (semaphore) == -1) {
+    if (errno == EAGAIN) {
+      debug (g, _("mutex: blocked waiting for semaphore %s "
+                  "(see \"MUTEX\" in guestfs(3))"),
+             g->mutex_file);
+
+      guestfs___call_callbacks_message (g, GUESTFS_EVENT_MUTEX_BLOCKED,
+                                        g->mutex_file, strlen (g->mutex_file));
+
+      if (sem_wait (semaphore) == -1)
+        goto error;
+    }
+    else {
+    error:
+      perrorf (g, _("mutex: sem_wait: %s"), g->mutex_file);
+      sem_close (semaphore);
+      return -1;
+    }
+  }
+
+  g->semaphore = semaphore;
+
+  guestfs___call_callbacks_message (g, GUESTFS_EVENT_MUTEX_ENTERED,
+                                    g->mutex_file, strlen (g->mutex_file));
+
+  return 0;
+}
+
+/* This is called late in 'shutdown' to release the mutex. */
+void
+guestfs___release_mutex (guestfs_h *g)
+{
+  if (!g->semaphore)
+    return;
+
+  if (sem_post (g->semaphore) == -1)
+    debug (g, "sem_post: %s", g->mutex_file);
+  if (sem_close (g->semaphore) == -1)
+    debug (g, "sem_close: %s", g->mutex_file);
+  g->semaphore = NULL;
+
+  guestfs___call_callbacks_message (g, GUESTFS_EVENT_MUTEX_RELEASED,
+                                    g->mutex_file, strlen (g->mutex_file));
+}
diff --git a/src/proto.c b/src/proto.c
index 2e3b480..f909f6e 100644
--- a/src/proto.c
+++ b/src/proto.c
@@ -168,6 +168,7 @@ child_cleanup (guestfs_h *g)
   g->fd[1] = -1;
   g->sock = -1;
   memset (&g->launch_t, 0, sizeof g->launch_t);
+  guestfs___release_mutex (g);
   guestfs___free_drives (g);
   g->state = CONFIG;
   guestfs___call_callbacks_void (g, GUESTFS_EVENT_SUBPROCESS_QUIT);
diff --git a/test-tool/test-tool.c b/test-tool/test-tool.c
index d71caed..621f625 100644
--- a/test-tool/test-tool.c
+++ b/test-tool/test-tool.c
@@ -248,6 +248,9 @@ main (int argc, char *argv[])
   free (p);
   printf ("guestfs_get_direct: %d\n", guestfs_get_direct (g));
   printf ("guestfs_get_memsize: %d\n", guestfs_get_memsize (g));
+  printf ("guestfs_get_mutex_file: %s\n",
+          guestfs_get_mutex_file (g) ? : "(null)");
+  printf ("guestfs_get_mutex_limit: %d\n", guestfs_get_mutex_limit (g));
   printf ("guestfs_get_network: %d\n", guestfs_get_network (g));
   printf ("guestfs_get_path: %s\n", guestfs_get_path (g) ? : "(null)");
   printf ("guestfs_get_pgroup: %d\n", guestfs_get_pgroup (g));
-- 
1.8.1.2




More information about the Libguestfs mailing list