[libvirt] [PATCH v2 2/2] storage: btrfs subvolumes for directory pools

Daniel P. Berrange berrange at redhat.com
Thu Oct 10 15:57:54 UTC 2013


On Thu, Oct 10, 2013 at 05:52:22PM +0300, Oskari Saarenmaa wrote:
> This commit adds support for btrfs subvolumes as directory volumes.  The
> directory storage pool can be used to manage btrfs subvolumes on an existing
> btrfs filesystem.  The driver can create new blank subvolumes and snapshots
> of existing subvolumes as well as delete existing subvolumes.
> 
> The subvolumes created are automatically made visible on the host side
> and can be attached to domains using the <filesystem> tags as defined in
> 'format domain' documentation.
> 
> Subvolumes do not implement quotas at the moment because the current
> (btrfs-progs-0.20.rc1.20130501git7854c8b-4.fc20.x86_64) support for quota
> management in btrfs-progs is lacking the necessary features, for example
> it's not possible to see the quota assigned to a certain subvolume and
> usage information is only updated on syncfs(2).  Quota support will be
> implemented once the tools gain the necessary features.
> 
> Signed-off-by: Oskari Saarenmaa <os at ohmu.fi>
> ---
>  I did not add a new virDirIsExecutable function as it wasn't really needed
>  by this patch set, it's sufficient to just verify that the backing volume
>  is a directory before running btrfs subvolume snapshot.
>  
>  Part 1/2 still applies cleanly on today's git master so not resending it.
> 
>  configure.ac                                      |  25 ++-
>  docs/schemas/storagevol.rng                       |   1 +
>  docs/storage.html.in                              |  30 ++-
>  libvirt.spec.in                                   |   4 +
>  src/conf/storage_conf.c                           |   3 +
>  src/conf/storage_conf.h                           |   1 +
>  src/storage/storage_backend.c                     |  15 +-
>  src/storage/storage_backend_fs.c                  | 219 ++++++++++++++++++++--
>  src/util/virstoragefile.c                         |   4 +-
>  src/util/virstoragefile.h                         |   1 +
>  tests/storagevolxml2xmlin/vol-btrfs-snapshot.xml  |  13 ++
>  tests/storagevolxml2xmlin/vol-btrfs.xml           |   9 +
>  tests/storagevolxml2xmlout/vol-btrfs-snapshot.xml |  26 +++
>  tests/storagevolxml2xmlout/vol-btrfs.xml          |  17 ++
>  tests/storagevolxml2xmltest.c                     |   2 +
>  15 files changed, 340 insertions(+), 30 deletions(-)
>  create mode 100644 tests/storagevolxml2xmlin/vol-btrfs-snapshot.xml
>  create mode 100644 tests/storagevolxml2xmlin/vol-btrfs.xml
>  create mode 100644 tests/storagevolxml2xmlout/vol-btrfs-snapshot.xml
>  create mode 100644 tests/storagevolxml2xmlout/vol-btrfs.xml
> 
> diff --git a/configure.ac b/configure.ac
> index 1993fab..a82640c 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -1646,6 +1646,10 @@ AC_ARG_WITH([storage-sheepdog],
>    [AS_HELP_STRING([--with-storage-sheepdog],
>      [with Sheepdog backend for the storage driver @<:@default=check@:>@])],
>    [],[with_storage_sheepdog=check])
> +AC_ARG_WITH([storage-btrfs],
> +  [AS_HELP_STRING([--with-storage-btrfs],
> +    [with Btrfs backend for the storage driver @<:@default=check@:>@])],
> +  [],[with_storage_btrfs=check])

We no longer have a backend for btrfs - it is just a part of the fs driver.

>  
>  if test "$with_libvirtd" = "no"; then
>    with_storage_dir=no
> @@ -1858,6 +1862,24 @@ fi
>  AM_CONDITIONAL([WITH_STORAGE_SHEEPDOG],
>    [test "$with_storage_sheepdog" = "yes"])
>  
> +if test "$with_storage_btrfs" = "yes" || test "$with_storage_btrfs" = "check"; then
> +  AC_PATH_PROG([BTRFS], [btrfs], [], [$PATH:/sbin:/usr/sbin])
> +
> +  if test "$with_storage_btrfs" = "yes" ; then
> +    if test -z "$BTRFS" ; then AC_MSG_ERROR([We need btrfs -command for btrfs storage driver]) ; fi
> +  else
> +    if test -z "$BTRFS" ; then with_storage_btrfs=no ; fi
> +    if test "$with_storage_btrfs" = "check" ; then with_storage_btrfs=yes ; fi
> +  fi
> +
> +  if test "$with_storage_btrfs" = "yes" ; then
> +    AC_DEFINE_UNQUOTED([WITH_STORAGE_BTRFS], 1, [whether Btrfs backend for storage driver is enabled])
> +    AC_DEFINE_UNQUOTED([BTRFS],["$BTRFS"],[Location of btrfs program])
> +  fi
> +fi
> +AM_CONDITIONAL([WITH_STORAGE_BTRFS], [test "$with_storage_btrfs" = "yes"])

Just use  'WITH_BTRFS' here - the WITH_STORAGE_XXXX is for actual
storage backends.

> +
> +
>  
>  LIBPARTED_CFLAGS=
>  LIBPARTED_LIBS=
> @@ -1945,7 +1967,7 @@ AC_SUBST([DEVMAPPER_CFLAGS])
>  AC_SUBST([DEVMAPPER_LIBS])
>  
>  with_storage=no
> -for backend in dir fs lvm iscsi scsi mpath rbd disk; do
> +for backend in dir fs lvm iscsi scsi mpath rbd disk btrfs; do
>      if eval test \$with_storage_$backend = yes; then
>          with_storage=yes
>          break
> @@ -2670,6 +2692,7 @@ AC_MSG_NOTICE([   mpath: $with_storage_mpath])
>  AC_MSG_NOTICE([    Disk: $with_storage_disk])
>  AC_MSG_NOTICE([     RBD: $with_storage_rbd])
>  AC_MSG_NOTICE([Sheepdog: $with_storage_sheepdog])
> +AC_MSG_NOTICE([   Btrfs: $with_storage_btrfs])

Again not needed here.

>  AC_MSG_NOTICE([])
>  AC_MSG_NOTICE([Security Drivers])
>  AC_MSG_NOTICE([])
> diff --git a/docs/schemas/storagevol.rng b/docs/schemas/storagevol.rng
> index 8bc5907..8814973 100644
> --- a/docs/schemas/storagevol.rng
> +++ b/docs/schemas/storagevol.rng
> @@ -201,6 +201,7 @@
>        <value>qed</value>
>        <value>vmdk</value>
>        <value>vpc</value>
> +      <value>volume</value>
>      </choice>
>    </define>
>  

> diff --git a/src/conf/storage_conf.c b/src/conf/storage_conf.c
> index 975e662..b349c3f 100644
> --- a/src/conf/storage_conf.c
> +++ b/src/conf/storage_conf.c
> @@ -314,11 +314,14 @@ virStorageVolDefFree(virStorageVolDefPtr def)
>      VIR_FREE(def->source.extents);
>  
>      VIR_FREE(def->target.compat);
> +    VIR_FREE(def->target.uuid);
>      virBitmapFree(def->target.features);
>      VIR_FREE(def->target.path);
>      VIR_FREE(def->target.perms.label);
>      VIR_FREE(def->target.timestamps);
>      virStorageEncryptionFree(def->target.encryption);
> +    VIR_FREE(def->backingStore.compat);
> +    VIR_FREE(def->backingStore.uuid);
>      VIR_FREE(def->backingStore.path);
>      VIR_FREE(def->backingStore.perms.label);
>      VIR_FREE(def->backingStore.timestamps);
> diff --git a/src/conf/storage_conf.h b/src/conf/storage_conf.h
> index 62ff1fd..6414c74 100644
> --- a/src/conf/storage_conf.h
> +++ b/src/conf/storage_conf.h
> @@ -90,6 +90,7 @@ struct _virStorageVolTarget {
>      virStorageEncryptionPtr encryption;
>      virBitmapPtr features;
>      char *compat;
> +    char *uuid;
>  };

This struct represents the public XML data format - adding random fields
to it for individual driver use is not allowed.

> diff --git a/src/storage/storage_backend_fs.c b/src/storage/storage_backend_fs.c
> index f9b0326..2cf300f 100644
> --- a/src/storage/storage_backend_fs.c
> +++ b/src/storage/storage_backend_fs.c
> @@ -51,6 +51,7 @@
>  #include "virfile.h"
>  #include "virlog.h"
>  #include "virstring.h"
> +#include "dirname.h"
>  
>  #define VIR_FROM_THIS VIR_FROM_STORAGE
>  
> @@ -801,6 +802,33 @@ error:
>  }
>  
>  
> +#if WITH_STORAGE_BTRFS
> +static int
> +btrfsVolInfo(virStoragePoolObjPtr pool ATTRIBUTE_UNUSED,
> +             char **groups,
> +             void *data)
> +{
> +    virStorageVolDefPtr vol = data;
> +
> +    if (STREQ(groups[0], "uuid")) {
> +        vol->type = VIR_STORAGE_VOL_DIR;
> +        vol->target.format = VIR_STORAGE_FILE_VOLUME;
> +        VIR_FREE(vol->target.uuid);
> +        if (VIR_STRDUP(vol->target.uuid, groups[1]) < 0)
> +            return -1;
> +    } else if (STREQ(groups[0], "Parent uuid") && STRNEQ(groups[1], "-")) {
> +        vol->backingStore.format = VIR_STORAGE_FILE_VOLUME;
> +        VIR_FREE(vol->backingStore.path);
> +        vol->backingStore.path = NULL;
> +        VIR_FREE(vol->backingStore.uuid);
> +        if (VIR_STRDUP(vol->backingStore.uuid, groups[1]) < 0)
> +            return -1;
> +    }
> +
> +    return 0;
> +}
> +#endif
> +
>  /**
>   * Iterate over the pool's directory and enumerate all disk images
>   * within it. This is non-recursive.
> @@ -809,10 +837,17 @@ static int
>  virStorageBackendFileSystemRefresh(virConnectPtr conn ATTRIBUTE_UNUSED,
>                                     virStoragePoolObjPtr pool)
>  {
> -    DIR *dir;
> +    int ret = -1;
> +    DIR *dir = NULL;
>      struct dirent *ent;
>      struct statvfs sb;
>      virStorageVolDefPtr vol = NULL;
> +#if WITH_STORAGE_BTRFS
> +    int missing_subvolume_info = 0;
> +    char *fstype = NULL;
> +
> +    fstype = virFileFsType(pool->def->target.path);
> +#endif
>  
>      if (!(dir = opendir(pool->def->target.path))) {
>          virReportSystemError(errno,
> @@ -822,7 +857,7 @@ virStorageBackendFileSystemRefresh(virConnectPtr conn ATTRIBUTE_UNUSED,
>      }
>  
>      while ((ent = readdir(dir)) != NULL) {
> -        int ret;
> +        int probe;
>          char *backingStore;
>          int backingStoreFormat;
>  
> @@ -842,19 +877,19 @@ virStorageBackendFileSystemRefresh(virConnectPtr conn ATTRIBUTE_UNUSED,
>          if (VIR_STRDUP(vol->key, vol->target.path) < 0)
>              goto cleanup;
>  
> -        if ((ret = virStorageBackendProbeTarget(&vol->target,
> -                                                &backingStore,
> -                                                &backingStoreFormat,
> -                                                &vol->allocation,
> -                                                &vol->capacity,
> -                                                &vol->target.encryption)) < 0) {
> -            if (ret == -2) {
> +        if ((probe = virStorageBackendProbeTarget(&vol->target,
> +                                                  &backingStore,
> +                                                  &backingStoreFormat,
> +                                                  &vol->allocation,
> +                                                  &vol->capacity,
> +                                                  &vol->target.encryption)) < 0) {
> +            if (probe == -2) {
>                  /* Silently ignore non-regular files,
>                   * eg '.' '..', 'lost+found', dangling symbolic link */
>                  virStorageVolDefFree(vol);
>                  vol = NULL;
>                  continue;
> -            } else if (ret == -3) {
> +            } else if (probe == -3) {
>                  /* The backing file is currently unavailable, its format is not
>                   * explicitly specified, the probe to auto detect the format
>                   * failed: continue with faked RAW format, since AUTO will
> @@ -865,6 +900,23 @@ virStorageBackendFileSystemRefresh(virConnectPtr conn ATTRIBUTE_UNUSED,
>                  goto cleanup;
>          }
>  
> +#if WITH_STORAGE_BTRFS
> +        /* check for subvolumes */
> +        if (vol->target.format == VIR_STORAGE_FILE_DIR &&
> +            fstype != NULL && STREQ(fstype, "btrfs")) {
> +            int vars[] = {2};
> +            const char *regexes[] = {"^\\s*([A-Za-z ]+):\\s*(.+)\\s*$"};
> +            virCommandPtr cmd = virCommandNewArgList(
> +                "btrfs", "subvolume", "show", vol->target.path, NULL);
> +            if (cmd == NULL)
> +                goto cleanup;
> +            virStorageBackendRunProgRegex(NULL, cmd, 1, regexes, vars,
> +                                          btrfsVolInfo, vol, NULL);
> +            if (vol->backingStore.format == VIR_STORAGE_FILE_VOLUME)
> +                missing_subvolume_info ++;

You're incrementing this multiple times

> +        }
> +#endif
> +
>          /* directory based volume */
>          if (vol->target.format == VIR_STORAGE_FILE_DIR)
>              vol->type = VIR_STORAGE_VOL_DIR;
> @@ -896,13 +948,44 @@ virStorageBackendFileSystemRefresh(virConnectPtr conn ATTRIBUTE_UNUSED,
>          vol = NULL;
>      }
>      closedir(dir);
> -
> +    dir = NULL;
> +
> +#if WITH_STORAGE_BTRFS
> +    if (missing_subvolume_info) {

But only checking for non-zero, so this should be a 'bool' not 'int'

> +        /* update snapshot information */
> +        virStorageVolDefPtr volv, volb;
> +        int idxv, idxb;

Use 'i' and 'j' for loop iterators and make them size_t.

> +
> +        for (idxv = 0; idxv < pool->volumes.count; idxv ++) {

No whitespace before '++' operator

> +            volv = pool->volumes.objs[idxv];
> +            if (volv->backingStore.format == VIR_STORAGE_FILE_VOLUME &&
> +                volv->backingStore.uuid != NULL) {
> +                for (idxb = 0; idxb < pool->volumes.count; idxb ++) {

Again whitespace.

> +                    volb = pool->volumes.objs[idxb];
> +                    if (volb->target.format == VIR_STORAGE_FILE_VOLUME &&
> +                        volb->target.uuid != NULL &&
> +                        STREQ(volb->target.uuid, volv->backingStore.uuid)) {
> +                        if (VIR_STRDUP(volv->backingStore.path, volb->target.path) < 0)
> +                            goto cleanup;
> +                        break;
> +                    }
> +                }
> +                if (volv->backingStore.path == NULL) {
> +                    /* backing store not in the pool, ignore it */

Backing stores for volumes are not required to be in the same pool as
the source. For example it is valid to have a qcow2 file backed by
a lvm volume.


> +static int createVolumeDir(virConnectPtr conn ATTRIBUTE_UNUSED,
> +                           virStoragePoolObjPtr pool ATTRIBUTE_UNUSED,
> +                           virStorageVolDefPtr vol,
> +                           virStorageVolDefPtr inputvol,
> +                           unsigned int flags)
> +{
> +    int ret = -1;
> +    char *vol_dir_name = NULL;
> +    char *fstype = NULL;
> +    virCommandPtr cmd = NULL;
> +    struct stat st;
> +
> +    virCheckFlags(0, -1);
> +
> +    if (inputvol) {
> +        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> +                _("cannot copy from volume to a subvolume"));
> +        return -1;
> +    }
> +
> +    if ((vol_dir_name = mdir_name(vol->target.path)) == NULL)
> +        goto cleanup;
> +
> +    fstype = virFileFsType(vol_dir_name);
> +    if (fstype == NULL) {
> +        virReportSystemError(errno,
> +                             _("cannot get filesystem type for %s"),
> +                             vol->target.path);
> +        goto cleanup;
> +    }
> +#if WITH_STORAGE_BTRFS
> +    else if (STREQ(fstype, "btrfs")) {
> +        cmd = virCommandNew("btrfs");
> +        if (!cmd)
> +            goto cleanup;
> +
> +        if (vol->backingStore.path == NULL) {
> +            virCommandAddArgList(cmd, "subvolume", "create", vol->target.path, NULL);
> +        } else {
> +            if (!virFileIsDir(vol->backingStore.path)) {
> +                virReportError(VIR_ERR_OPERATION_INVALID,
> +                               _("backing store %s is not a directory"),
> +                               vol->backingStore.path);
> +                goto cleanup;
> +            }
> +
> +            virCommandAddArgList(cmd, "subvolume", "snapshot", vol->backingStore.path,
> +                                 vol->target.path, NULL);
> +        }
> +        if (virCommandRun(cmd, NULL) < 0)
> +            goto cleanup;
> +    }
> +#endif
> +    else {
> +        virReportError(VIR_ERR_CONFIG_UNSUPPORTED,
> +                       _("subvolumes are not supported in %s"),
> +                       fstype);
> +        goto cleanup;
> +    }
> +
> +    if (stat(vol->target.path, &st) < 0) {
> +        virReportSystemError(errno,
> +                             _("failed to create %s"), vol->target.path);
> +        goto cleanup;
> +    }
> +    ret = 0;
> +
> +cleanup:
> +    VIR_FREE(vol_dir_name);
> +    VIR_FREE(fstype);
> +    virCommandFree(cmd);
> +    return ret;
> +}
> +
>  static int createFileDir(virConnectPtr conn ATTRIBUTE_UNUSED,
>                           virStoragePoolObjPtr pool,
>                           virStorageVolDefPtr vol,
> @@ -1061,6 +1229,8 @@ _virStorageBackendFileSystemVolBuild(virConnectPtr conn,
>          create_func = virStorageBackendCreateRaw;
>      } else if (vol->target.format == VIR_STORAGE_FILE_DIR) {
>          create_func = createFileDir;
> +    } else if (vol->target.format == VIR_STORAGE_FILE_VOLUME) {
> +        create_func = createVolumeDir;
>      } else if ((tool_type = virStorageBackendFindFSImageTool(NULL)) != -1) {
>          create_func = virStorageBackendFSImageToolTypeToFunc(tool_type);
>  
> @@ -1133,6 +1303,23 @@ virStorageBackendFileSystemVolDelete(virConnectPtr conn ATTRIBUTE_UNUSED,
>          }
>          break;
>      case VIR_STORAGE_VOL_DIR:
> +#if WITH_STORAGE_BTRFS
> +        if (vol->target.format == VIR_STORAGE_FILE_VOLUME) {
> +            char *fstype = virFileFsType(vol->target.path);
> +            if (fstype != NULL && STREQ(fstype, "btrfs")) {
> +                virCommandPtr cmd = NULL;
> +                int ret;
> +
> +                cmd = virCommandNewArgList("btrfs", "subvolume", "delete",
> +                                           vol->target.path, NULL);
> +                ret = (virCommandRun(cmd, NULL) < 0) ? -1 : 0;
> +                virCommandFree(cmd);
> +                VIR_FREE(fstype);
> +                return ret;
> +            }

I'm thinking it would be nice to have a dedicate file with APIs for btrfs
eg  src/util/virbtrfs.{h,c} and have it contain things like

   virBtrFSCreateVolume(....)
   virBtrFSCreateVolume(....)
   virBtrFS...(....)

and so on, so we don't have all these virCommand calls littering this
file.

> diff --git a/src/util/virstoragefile.c b/src/util/virstoragefile.c
> index 48d5fbb..d0231b6 100644
> --- a/src/util/virstoragefile.c
> +++ b/src/util/virstoragefile.c
> @@ -57,7 +57,7 @@ VIR_ENUM_IMPL(virStorageFileFormat,
>                "raw", "dir", "bochs",
>                "cloop", "cow", "dmg", "iso",
>                "qcow", "qcow2", "qed", "vmdk", "vpc",
> -              "fat", "vhd", "vdi")
> +              "fat", "vhd", "vdi", "volume")
>  
>  VIR_ENUM_IMPL(virStorageFileFeature,
>                VIR_STORAGE_FILE_FEATURE_LAST,
> @@ -232,6 +232,8 @@ static struct FileTypeInfo const fileTypeInfo[] = {
>                                 -1, {0}, 0, 0, 0, 0, NULL, NULL },
>      [VIR_STORAGE_FILE_VHD] = { 0, NULL, NULL, LV_LITTLE_ENDIAN,
>                                 -1, {0}, 0, 0, 0, 0, NULL, NULL },
> +    [VIR_STORAGE_FILE_VOLUME] = { 0, NULL, NULL, LV_LITTLE_ENDIAN,
> +                                  -1, {0}, 0, 0, 0, 0, NULL, NULL },
>  };
>  verify(ARRAY_CARDINALITY(fileTypeInfo) == VIR_STORAGE_FILE_LAST);
>  
> diff --git a/src/util/virstoragefile.h b/src/util/virstoragefile.h
> index d5effa4..4dc6c81 100644
> --- a/src/util/virstoragefile.h
> +++ b/src/util/virstoragefile.h
> @@ -46,6 +46,7 @@ enum virStorageFileFormat {
>      VIR_STORAGE_FILE_FAT,
>      VIR_STORAGE_FILE_VHD,
>      VIR_STORAGE_FILE_VDI,
> +    VIR_STORAGE_FILE_VOLUME,

This isn't right - volumes already have a type={file,block,dir,volume}
and we shouldn't duplicate this in the format attribute too.

>  
>      VIR_STORAGE_FILE_LAST,
>  };
> diff --git a/tests/storagevolxml2xmlin/vol-btrfs-snapshot.xml b/tests/storagevolxml2xmlin/vol-btrfs-snapshot.xml
> new file mode 100644
> index 0000000..5a58b56
> --- /dev/null
> +++ b/tests/storagevolxml2xmlin/vol-btrfs-snapshot.xml
> @@ -0,0 +1,13 @@
> +<volume>
> +  <name>clone</name>
> +  <capacity unit="bytes">0</capacity>
> +  <allocation unit="bytes">0</allocation>
> +  <target>
> +    <path>/lxc/clone</path>
> +    <format type='volume'/>
> +  </target>
> +  <backingStore>
> +    <path>/lxc/vanilla</path>
> +    <format type='volume'/>
> +  </backingStore>
> +</volume>

Using 'format' in this way is wrong. What we should be doing
is exposing the volume 'type' as an attribute eg

  <volume type='volume'>
  ...
  </volume>

> diff --git a/tests/storagevolxml2xmlout/vol-btrfs-snapshot.xml b/tests/storagevolxml2xmlout/vol-btrfs-snapshot.xml
> new file mode 100644
> index 0000000..75830d3
> --- /dev/null
> +++ b/tests/storagevolxml2xmlout/vol-btrfs-snapshot.xml
> @@ -0,0 +1,26 @@
> +<volume>
> +  <name>clone</name>
> +  <key>(null)</key>

If you're getting (null) here, then something has gone wrong

> +  <source>
> +  </source>
> +  <capacity unit='bytes'>0</capacity>
> +  <allocation unit='bytes'>0</allocation>
> +  <target>
> +    <path>/lxc/clone</path>
> +    <format type='volume'/>
> +    <permissions>
> +      <mode>0600</mode>
> +      <owner>4294967295</owner>
> +      <group>4294967295</group>
> +    </permissions>
> +  </target>
> +  <backingStore>
> +    <path>/lxc/vanilla</path>
> +    <format type='volume'/>
> +    <permissions>
> +      <mode>0600</mode>
> +      <owner>4294967295</owner>
> +      <group>4294967295</group>
> +    </permissions>
> +  </backingStore>
> +</volume>
> diff --git a/tests/storagevolxml2xmlout/vol-btrfs.xml b/tests/storagevolxml2xmlout/vol-btrfs.xml
> new file mode 100644
> index 0000000..fbad915
> --- /dev/null
> +++ b/tests/storagevolxml2xmlout/vol-btrfs.xml
> @@ -0,0 +1,17 @@
> +<volume>
> +  <name>vanilla</name>
> +  <key>(null)</key>

Again.

> +  <source>
> +  </source>
> +  <capacity unit='bytes'>0</capacity>
> +  <allocation unit='bytes'>0</allocation>
> +  <target>
> +    <path>/lxc/vanilla</path>
> +    <format type='volume'/>
> +    <permissions>
> +      <mode>0600</mode>
> +      <owner>4294967295</owner>
> +      <group>4294967295</group>
> +    </permissions>
> +  </target>
> +</volume>


Daniel
-- 
|: http://berrange.com      -o-    http://www.flickr.com/photos/dberrange/ :|
|: http://libvirt.org              -o-             http://virt-manager.org :|
|: http://autobuild.org       -o-         http://search.cpan.org/~danberr/ :|
|: http://entangle-photo.org       -o-       http://live.gnome.org/gtk-vnc :|




More information about the libvir-list mailing list