[Libvir] PATCH: 10/16: general purpose helper APIs

Daniel P. Berrange berrange at redhat.com
Tue Feb 12 04:37:08 UTC 2008


This patch provides a couple of helper APIs used by the forthcoming
driver backends.

   - virStorageBackendUpdateVolInfo - take a filename and virStorageVolPtr
     and update the capacity/allocation information based on the file
     stat() results. Also update the permissions data on owner/mode
     and SELinux label.

   - virStorageBackendUpdateVolInfoFD - same as above but with a pre-opened
     filehandle

   - virStorageBackendStablePath - given a /dev/XXX node, and a directory
     of symlinks (eg /dev/disk/by-path), attempts to find a symlink pointing
     to the desired file.

   - virStorageBackendRunProgRegex - given one or more regexes and a command
     line argv[], execute the program and attempt to match its STDOUT against
     the regex. When complete matches are found invoke a callback.

   - virStorageBackendRunProgNul - given a command line argv[] and an expected
     number of fields per record, tokenize the STDOUT into records splitting
     on NULL, and invoke the callback per record.

The SELinux code is optional, and can be replaced with calls to any other
library which can provide MAC file labels, or just disabled completely..


 configure.in          |   39 ++++
 libvirt.spec.in       |    1 
 src/Makefile.am       |    3 
 src/storage_backend.c |  455 ++++++++++++++++++++++++++++++++++++++++++++++++++
 src/storage_backend.h |   38 ++++
 tests/Makefile.am     |    2 
 6 files changed, 537 insertions(+), 1 deletion(-)

diff -r 4f8d1242609a configure.in
--- a/configure.in	Wed Feb 06 22:14:01 2008 -0500
+++ b/configure.in	Thu Feb 07 11:14:53 2008 -0500
@@ -473,6 +473,40 @@ AC_SUBST(AVAHI_CFLAGS)
 AC_SUBST(AVAHI_CFLAGS)
 AC_SUBST(AVAHI_LIBS)
 
+dnl SELinux
+AC_ARG_WITH(selinux,
+  [  --with-selinux         use SELinux to manage security],
+  [],
+  [with_selinux=check])
+
+SELINUX_CFLAGS=
+SELINUX_LIBS=
+if test "$with_selinux" != "no"; then
+  old_cflags="$CFLAGS"
+  old_libs="$LIBS"
+  if test "$with_selinux" = "check"; then
+    AC_CHECK_HEADER([selinux/selinux.h],[],[with_selinux=no])
+    AC_CHECK_LIB(selinux, fgetfilecon,[],[with_selinux=no])
+    if test "$with_selinux" != "no"; then
+      with_selinux="yes"
+    fi
+  else
+    AC_CHECK_HEADER([selinux/selinux.h],[],
+       [AC_MSG_ERROR([You must install the SELinux development package in order to compile libvirt])])
+    AC_CHECK_LIB(selinux, fgetfilecon,[],
+       [AC_MSG_ERROR([You must install the SELinux development package in order to compile and run libvirt])])
+  fi
+  CFLAGS="$old_cflags"
+  LIBS="$old_libs"
+fi
+if test "$with_selinux" = "yes"; then
+  SELINUX_LIBS="-lselinux"
+  AC_DEFINE_UNQUOTED(HAVE_SELINUX, 1, [whether SELinux is available for security])
+fi
+AM_CONDITIONAL(HAVE_SELINUX, [test "$with_selinux" != "no"])
+AC_SUBST(SELINUX_CFLAGS)
+AC_SUBST(SELINUX_LIBS)
+
 dnl virsh libraries
 AC_CHECK_HEADERS([readline/readline.h])
 
@@ -745,6 +779,11 @@ else
 else
 AC_MSG_NOTICE([  polkit: no])
 fi
+if test "$with_selinux" = "yes" ; then
+AC_MSG_NOTICE([  selinux: $SELINUX_CFLAGS $SELINUX_LIBS])
+else
+AC_MSG_NOTICE([  selinux: no])
+fi
 AC_MSG_NOTICE([])
 AC_MSG_NOTICE([Miscellaneous])
 AC_MSG_NOTICE([])
diff -r 4f8d1242609a libvirt.spec.in
--- a/libvirt.spec.in	Wed Feb 06 22:14:01 2008 -0500
+++ b/libvirt.spec.in	Thu Feb 07 11:14:53 2008 -0500
@@ -41,6 +41,7 @@ BuildRequires: gettext
 BuildRequires: gettext
 BuildRequires: gnutls-devel
 BuildRequires: avahi-devel
+BuildRequires: libselinux-devel
 BuildRequires: dnsmasq
 BuildRequires: bridge-utils
 BuildRequires: qemu
diff -r 4f8d1242609a src/Makefile.am
--- a/src/Makefile.am	Wed Feb 06 22:14:01 2008 -0500
+++ b/src/Makefile.am	Thu Feb 07 11:14:53 2008 -0500
@@ -8,6 +8,7 @@ INCLUDES = \
 	   $(LIBXML_CFLAGS) \
 	   $(GNUTLS_CFLAGS) \
 	   $(SASL_CFLAGS) \
+	   $(SELINUX_CFLAGS) \
 	   -DBINDIR=\""$(libexecdir)"\" \
 	   -DSBINDIR=\""$(sbindir)"\" \
 	   -DSYSCONF_DIR="\"$(sysconfdir)\"" \
@@ -68,7 +69,7 @@ SERVER_SOURCES = 						\
 		../qemud/remote_protocol.c ../qemud/remote_protocol.h
 
 libvirt_la_SOURCES = $(CLIENT_SOURCES) $(SERVER_SOURCES)
-libvirt_la_LIBADD = $(LIBXML_LIBS) $(GNUTLS_LIBS) $(SASL_LIBS) \
+libvirt_la_LIBADD = $(LIBXML_LIBS) $(GNUTLS_LIBS) $(SASL_LIBS) $(SELINUX_LIBS) \
 		    @CYGWIN_EXTRA_LIBADD@ ../gnulib/lib/libgnu.la
 libvirt_la_LDFLAGS = -Wl,--version-script=$(srcdir)/libvirt_sym.version \
                      -version-info @LIBVIRT_VERSION_INFO@ \
diff -r 4f8d1242609a src/storage_backend.c
--- a/src/storage_backend.c	Wed Feb 06 22:14:01 2008 -0500
+++ b/src/storage_backend.c	Thu Feb 07 11:14:53 2008 -0500
@@ -28,6 +28,14 @@
 #include <sys/types.h>
 #include <sys/wait.h>
 #include <unistd.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <sys/stat.h>
+#include <dirent.h>
+
+#if HAVE_SELINUX
+#include <selinux/selinux.h>
+#endif
 
 #include "util.h"
 
@@ -62,6 +70,453 @@ const char *virStorageBackendToString(in
 const char *virStorageBackendToString(int type) {
     virStorageReportError(NULL, VIR_ERR_INTERNAL_ERROR, "unknown storage backend type %d", type);
     return NULL;
+}
+
+
+int virStorageBackendUpdateVolInfo(virConnectPtr conn,
+                                   virStorageVolDefPtr vol,
+                                   int withCapacity)
+{
+    int ret, fd;
+
+    if ((fd = open(vol->target.path, O_RDONLY)) < 0) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              "cannot open volume '%s': %d (%s)",
+                              vol->target.path, errno, strerror(errno));
+        return -1;
+    }
+
+    ret = virStorageBackendUpdateVolInfoFD(conn,
+                                           vol,
+                                           fd,
+                                           withCapacity);
+
+    close(fd);
+
+    return ret;
+}
+
+int virStorageBackendUpdateVolInfoFD(virConnectPtr conn,
+                                     virStorageVolDefPtr vol,
+                                     int fd,
+                                     int withCapacity)
+{
+    struct stat sb;
+#if HAVE_SELINUX
+    security_context_t filecon = NULL;
+#endif
+
+    if (fstat(fd, &sb) < 0) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              "cannot stat file '%s': %d (%s)",
+                              vol->target.path, errno, strerror(errno));
+        return -1;
+    }
+
+    if (!S_ISREG(sb.st_mode) &&
+        !S_ISCHR(sb.st_mode) &&
+        !S_ISBLK(sb.st_mode))
+        return -2;
+
+    if (S_ISREG(sb.st_mode)) {
+        vol->allocation = (unsigned long long)sb.st_blocks * (unsigned long long)512;
+        /* Regular files may be sparse, so logical size (capacity) is not same
+         * as actual allocation above
+         */
+        if (withCapacity)
+            vol->capacity = sb.st_size;
+    } else {
+        off_t end;
+        /* XXX this is POSIX compliant, but doesn't work for for CHAR files,
+         * only BLOCK. There is a Linux specific ioctl() for getting
+         * size of both CHAR / BLOCK devices we should check for in
+         * configure
+         */
+        end = lseek(fd, 0, SEEK_END);
+        if (end == (off_t)-1) {
+            virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                                  "cannot seek to end of file '%s': %d (%s)",
+                                  vol->target.path, errno, strerror(errno));
+            return -1;
+        }
+        vol->allocation = end;
+        if (withCapacity) vol->capacity = end;
+    }
+
+    vol->target.perms.mode = sb.st_mode;
+    vol->target.perms.uid = sb.st_uid;
+    vol->target.perms.gid = sb.st_gid;
+
+    free(vol->target.perms.label);
+    vol->target.perms.label = NULL;
+
+#if HAVE_SELINUX
+    if (fgetfilecon(fd, &filecon) == -1) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              "cannot get file context of %s: %s (%d)",
+                              vol->target.path, strerror(errno), errno);
+        return -1;
+    }
+    vol->target.perms.label = strdup(filecon);
+    if (vol->target.perms.label == NULL) {
+        virStorageReportError(conn, VIR_ERR_NO_MEMORY, "context");
+        return -1;
+    }
+    freecon(filecon);
+#else
+    vol->target.perms.label = NULL;
+#endif
+
+    return 0;
+}
+
+/*
+ * Given a volume path directly in /dev/XXX, iterate over the
+ * entries in the directory pool->def->target.path and find the
+ * first symlink pointing to the volume path.
+ *
+ * If, the target.path is /dev/, then return the original volume
+ * path.
+ *
+ * If no symlink is found, then return the original volume path
+ *
+ * Typically target.path is one of the /dev/disk/by-XXX dirs
+ * with stable paths.
+ */
+char *virStorageBackendStablePath(virConnectPtr conn,
+                                  virStoragePoolObjPtr pool,
+                                  char *devpath)
+{
+    DIR *dh;
+    struct dirent *dent;
+
+    /* Short circuit if pool has no target, or if its /dev */
+    if (pool->def->target.path == NULL ||
+        STREQ(pool->def->target.path, "/dev") ||
+        STREQ(pool->def->target.path, "/dev/"))
+        return devpath;
+
+    /* The pool is pointing somewhere like /dev/disk/by-path
+     * or /dev/disk/by-id, so we need to check all symlinks in
+     * the target directory and figure out which one points
+     * to this device node
+     */
+    if ((dh = opendir(pool->def->target.path)) == NULL) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              "cannot read dir %s: %s (%d)",
+                              pool->def->target.path,
+                              strerror(errno), errno);
+        return NULL;
+    }
+
+    while ((dent = readdir(dh)) != NULL) {
+        char *stablepath;
+        if (dent->d_name[0] == '.')
+            continue;
+
+        stablepath = malloc(strlen(pool->def->target.path) + 1 + strlen(dent->d_name) + 1);
+        if (stablepath == NULL) {
+            virStorageReportError(conn, VIR_ERR_NO_MEMORY, "path");
+            closedir(dh);
+            return NULL;
+        }
+
+        strcpy(stablepath, pool->def->target.path);
+        strcat(stablepath, "/");
+        strcat(stablepath, dent->d_name);
+
+        if (virFileLinkPointsTo(stablepath, devpath)) {
+            closedir(dh);
+            return stablepath;
+        }
+
+        free(stablepath);
+    }
+
+    closedir(dh);
+
+    /* Couldn't find any matching stable link so give back
+     * the original non-stable dev path
+     */
+    return devpath;
+}
+
+/*
+ * Run an external program.
+ *
+ * Read its output and apply a series of regexes to each line
+ * When the entire set of regexes has matched consequetively
+ * then run a callback passing in all the matches
+ */
+int virStorageBackendRunProgRegex(virConnectPtr conn,
+                                  virStoragePoolObjPtr pool,
+                                  const char **prog,
+                                  int nregex,
+                                  const char **regex,
+                                  int *nvars,
+                                  virStorageBackendListVolRegexFunc func,
+                                  void *data)
+{
+    int child = 0, fd = -1, exitstatus, err, failed = 1;
+    FILE *list = NULL;
+    regex_t *reg;
+    regmatch_t *vars = NULL;
+    char line[1024];
+    int maxReg = 0, i, j;
+    int totgroups = 0, ngroup = 0, maxvars = 0;
+    char **groups;
+
+    /* Compile all regular expressions */
+    if ((reg = calloc(nregex, sizeof(*reg))) == NULL) {
+        virStorageReportError(conn, VIR_ERR_NO_MEMORY, "regex");
+        return -1;
+    }
+
+    for (i = 0 ; i < nregex ; i++) {
+        err = regcomp(&reg[i], regex[i], REG_EXTENDED);
+        if (err != 0) {
+            char error[100];
+            regerror(err, &reg[i], error, sizeof(error));
+            virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR, "Failed to compile regex %s", error);
+            for (j = 0 ; j <= i ; j++)
+                regfree(&reg[j]);
+            free(reg);
+            return -1;
+        }
+
+        totgroups += nvars[i];
+        if (nvars[i] > maxvars)
+            maxvars = nvars[i];
+
+    }
+
+    /* Storage for matched variables */
+    if ((groups = calloc(totgroups, sizeof(*groups))) == NULL) {
+        virStorageReportError(conn, VIR_ERR_NO_MEMORY, "regex groups");
+        goto cleanup;
+    }
+    if ((vars = calloc(maxvars+1, sizeof(*vars))) == NULL) {
+        virStorageReportError(conn, VIR_ERR_NO_MEMORY, "regex groups");
+        goto cleanup;
+    }
+
+
+    /* Run the program and capture its output */
+    if (virExec(conn, (char**)prog, &child, -1, &fd, NULL) < 0) {
+        goto cleanup;
+    }
+
+    if ((list = fdopen(fd, "r")) == NULL) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR, "cannot read fd");
+        goto cleanup;
+    }
+
+    while (fgets(line, sizeof(line), list) != NULL) {
+        /* Strip trailing newline */
+        int len = strlen(line);
+        if (len && line[len-1] == '\n')
+            line[len-1] = '\0';
+
+        for (i = 0 ; i <= maxReg && i < nregex ; i++) {
+            if (regexec(&reg[i], line, nvars[i]+1, vars, 0) == 0) {
+                maxReg++;
+
+                if (i == 0)
+                    ngroup = 0;
+
+                /* NULL terminate each captured group in the line */
+                for (j = 0 ; j < nvars[i] ; j++) {
+                    /* NB vars[0] is the full pattern, so we offset j by 1 */
+                    line[vars[j+1].rm_eo] = '\0';
+                    if ((groups[ngroup++] = strdup(line + vars[j+1].rm_so)) == NULL) {
+                        virStorageReportError(conn, VIR_ERR_NO_MEMORY, "regex groups");
+                        goto cleanup;
+                    }
+                }
+
+                /* We're matching on the last regex, so callback time */
+                if (i == (nregex-1)) {
+                    if (((*func)(conn, pool, groups, data)) < 0)
+                        goto cleanup;
+
+                    /* Release matches & restart to matching the first regex */
+                    for (j = 0 ; j < totgroups ; j++) {
+                        free(groups[j]);
+                        groups[j] = NULL;
+                    }
+                    maxReg = 0;
+                    ngroup = 0;
+                }
+            }
+        }
+    }
+
+    failed = 0;
+
+ cleanup:
+    if (groups) {
+        for (j = 0 ; j < totgroups ; j++)
+            free(groups[j]);
+        free(groups);
+    }
+    free(vars);
+
+    for (i = 0 ; i < nregex ; i++)
+        regfree(&reg[i]);
+
+    free(reg);
+
+    if (list)
+        fclose(list);
+    else
+        close(fd);
+
+    while ((err = waitpid(child, &exitstatus, 0) == -1) && errno == EINTR);
+
+    /* Don't bother checking exit status if we already failed */
+    if (failed)
+        return -1;
+
+    if (err == -1) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              "failed to wait for command: %s (%d)",
+                              strerror(errno), errno);
+        return -1;
+    } else {
+        if (WIFEXITED(exitstatus)) {
+            if (WEXITSTATUS(exitstatus) != 0) {
+                virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                                      "non-zero exit status from command %d",
+                                      WEXITSTATUS(exitstatus));
+                return -1;
+            }
+        } else {
+            virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                                  "command did not exit cleanly");
+            return -1;
+        }
+    }
+
+    return 0;
+}
+
+/*
+ * Run an external program and read from its standard output
+ * a stream of tokens from IN_STREAM, applying FUNC to
+ * each successive sequence of N_COLUMNS tokens.
+ * If FUNC returns < 0, stop processing input and return -1.
+ * Return -1 if N_COLUMNS == 0.
+ * Return -1 upon memory allocation error.
+ * If the number of input tokens is not a multiple of N_COLUMNS,
+ * then the final FUNC call will specify a number smaller than N_COLUMNS.
+ * If there are no input tokens (empty input), call FUNC with N_COLUMNS == 0.
+ */
+int virStorageBackendRunProgNul(virConnectPtr conn,
+                                virStoragePoolObjPtr pool,
+                                const char **prog,
+                                size_t n_columns,
+                                virStorageBackendListVolNulFunc func,
+                                void *data)
+{
+    size_t n_tok = 0;
+    int child = 0, fd = -1, exitstatus;
+    FILE *fp = NULL;
+    char **v;
+    int err = -1;
+    int w_err;
+    int i;
+
+    if (n_columns == 0)
+        return -1;
+
+    if (n_columns > SIZE_MAX / sizeof *v
+        || (v = malloc (n_columns * sizeof *v)) == NULL) {
+        virStorageReportError(conn, VIR_ERR_NO_MEMORY, "n_columns too large");
+        return -1;
+    }
+    for (i = 0; i < n_columns; i++)
+        v[i] = NULL;
+
+    /* Run the program and capture its output */
+    if (virExec(conn, (char**)prog, &child, -1, &fd, NULL) < 0) {
+        goto cleanup;
+    }
+
+    if ((fp = fdopen(fd, "r")) == NULL) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR, "cannot read fd");
+        goto cleanup;
+    }
+
+    while (1) {
+        char *buf = NULL;
+        size_t buf_len = 0;
+        /* Be careful: even when it returns -1,
+           this use of getdelim allocates memory.  */
+        ssize_t tok_len = getdelim (&buf, &buf_len, 0, fp);
+        v[n_tok] = buf;
+        if (tok_len < 0) {
+            /* Maybe EOF, maybe an error.
+               If n_tok > 0, then we know it's an error.  */
+            if (n_tok && func (conn, pool, n_tok, v, data) < 0)
+                goto cleanup;
+            break;
+        }
+        ++n_tok;
+        if (n_tok == n_columns) {
+            if (func (conn, pool, n_tok, v, data) < 0)
+                goto cleanup;
+            n_tok = 0;
+            for (i = 0; i < n_columns; i++) {
+                free (v[i]);
+                v[i] = NULL;
+            }
+        }
+    }
+
+    if (feof (fp))
+        err = 0;
+    else
+        virStorageReportError (conn, VIR_ERR_INTERNAL_ERROR,
+                               "read error: %s", strerror (errno));
+
+ cleanup:
+    for (i = 0; i < n_columns; i++)
+        free (v[i]);
+    free (v);
+
+    if (fp)
+        fclose (fp);
+    else
+        close (fd);
+
+    while ((w_err = waitpid (child, &exitstatus, 0) == -1) && errno == EINTR)
+        /* empty */ ;
+
+    /* Don't bother checking exit status if we already failed */
+    if (err < 0)
+        return -1;
+
+    if (w_err == -1) {
+        virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                              "failed to wait for command: %s (%d)",
+                              strerror(errno), errno);
+        return -1;
+    } else {
+        if (WIFEXITED(exitstatus)) {
+            if (WEXITSTATUS(exitstatus) != 0) {
+                virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                                      "non-zero exit status from command %d",
+                                      WEXITSTATUS(exitstatus));
+                return -1;
+            }
+        } else {
+            virStorageReportError(conn, VIR_ERR_INTERNAL_ERROR,
+                                  "command did not exit cleanly");
+            return -1;
+        }
+    }
+
+    return 0;
 }
 
 
diff -r 4f8d1242609a src/storage_backend.h
--- a/src/storage_backend.h	Wed Feb 06 22:14:01 2008 -0500
+++ b/src/storage_backend.h	Thu Feb 07 11:14:53 2008 -0500
@@ -95,6 +95,44 @@ int virStorageBackendFromString(const ch
 int virStorageBackendFromString(const char *type);
 const char *virStorageBackendToString(int type);
 
+int virStorageBackendUpdateVolInfo(virConnectPtr conn,
+                                   virStorageVolDefPtr vol,
+                                   int withCapacity);
+
+int virStorageBackendUpdateVolInfoFD(virConnectPtr conn,
+                                     virStorageVolDefPtr vol,
+                                     int fd,
+                                     int withCapacity);
+
+char *virStorageBackendStablePath(virConnectPtr conn,
+                                  virStoragePoolObjPtr pool,
+                                  char *devpath);
+
+typedef int (*virStorageBackendListVolRegexFunc)(virConnectPtr conn,
+                                                 virStoragePoolObjPtr pool,
+                                                 char **const groups,
+                                                 void *data);
+typedef int (*virStorageBackendListVolNulFunc)(virConnectPtr conn,
+                                               virStoragePoolObjPtr pool,
+                                               size_t n_tokens,
+                                               char **const groups,
+                                               void *data);
+
+int virStorageBackendRunProgRegex(virConnectPtr conn,
+                                  virStoragePoolObjPtr pool,
+                                  const char **prog,
+                                  int nregex,
+                                  const char **regex,
+                                  int *nvars,
+                                  virStorageBackendListVolRegexFunc func,
+                                  void *data);
+
+int virStorageBackendRunProgNul(virConnectPtr conn,
+                                virStoragePoolObjPtr pool,
+                                const char **prog,
+                                size_t n_columns,
+                                virStorageBackendListVolNulFunc func,
+                                void *data);
 
 #endif /* __VIR_STORAGE_BACKEND_H__ */
 
diff -r 4f8d1242609a tests/Makefile.am
--- a/tests/Makefile.am	Wed Feb 06 22:14:01 2008 -0500
+++ b/tests/Makefile.am	Thu Feb 07 11:14:53 2008 -0500
@@ -20,6 +20,7 @@ INCLUDES = \
 	$(LIBXML_CFLAGS) \
 	$(GNUTLS_CFLAGS) \
 	$(SASL_CFLAGS) \
+	$(SELINUX_CFLAGS) \
         -D_XOPEN_SOURCE=600 -D_POSIX_C_SOURCE=199506L \
         -DGETTEXT_PACKAGE=\"$(PACKAGE)\" \
          $(COVERAGE_CFLAGS) \
@@ -31,6 +32,7 @@ LDADDS = \
 	$(LIBXML_LIBS) \
         $(GNUTLS_LIBS) \
         $(SASL_LIBS) \
+        $(SELINUX_LIBS) \
         $(WARN_CFLAGS) \
 	$(LIBVIRT) \
 	../gnulib/lib/libgnu.la \

-- 
|=- Red Hat, Engineering, Emerging Technologies, Boston.  +1 978 392 2496 -=|
|=-           Perl modules: http://search.cpan.org/~danberr/              -=|
|=-               Projects: http://freshmeat.net/~danielpb/               -=|
|=-  GnuPG: 7D3B9505   F3C9 553F A1DA 4AC2 5648 23C1 B3DF F742 7D3B 9505  -=| 




More information about the libvir-list mailing list