New API: add-domain
authorRichard Jones <rjones@redhat.com>
Tue, 9 Nov 2010 18:53:01 +0000 (18:53 +0000)
committerRichard W.M. Jones <rjones@redhat.com>
Thu, 11 Nov 2010 11:22:39 +0000 (11:22 +0000)
This new API allows you to add the disks from a libvirt
domain.

In guestfish you can use the 'domain' command to access the
API, eg:

><fs> domain Fedora14 libvirturi:qemu:///system
1

The returned number is the number of disks that were added.

Also here is a proposed (but commented out) low-level API
which would allow you to add a domain from a virDomainPtr.
However there are several problems with this API -- see discussion
on the list:
https://www.redhat.com/archives/libguestfs/2010-November/thread.html#00028

TODO
generator/generator_actions.ml
po/POTFILES.in
regressions/Makefile.am
regressions/test-add-domain.sh [new file with mode: 0755]
src/Makefile.am
src/virt.c [new file with mode: 0644]

diff --git a/TODO b/TODO
index 0f933fa..2301c27 100644 (file)
--- a/TODO
+++ b/TODO
@@ -347,11 +347,3 @@ Eric Sandeen pointed out the blktrace tool which is a better way of
 capturing traces than using patched qemu (see
 contrib/visualize-alignment).  We would still use the same
 visualization tools in conjunction with blktrace traces.
-
-Add-domain command
-------------------
-
-guestfs_add_domain (g, "libvirt-dom");
-
-However this would need to not depend on libvirt, eg. loading it
-on demand.
index a0a337b..12fda29 100644 (file)
@@ -1058,6 +1058,67 @@ Please read L<guestfs(3)/INSPECTION> for more details.");
 This returns the internal QEMU command line.  'debug' commands are
 not part of the formal API and can be removed or changed at any time.");
 
+  ("add_domain", (RInt "nrdisks", [String "dom"], [String "libvirturi"; Bool "readonly"; String "iface"]), -1, [FishAlias "domain"],
+   [],
+   "add the disk(s) from a named libvirt domain",
+   "\
+This function adds the disk(s) attached to the named libvirt
+domain C<dom>.  It works by connecting to libvirt, requesting
+the domain and domain XML from libvirt, parsing it for disks,
+and calling C<guestfs_add_drive_opts> on each one.
+
+The number of disks added is returned.  This operation is atomic:
+if an error is returned, then no disks are added.
+
+This function does some minimal checks to make sure the libvirt
+domain is not running (unless C<readonly> is true).  In a future
+version we will try to acquire the libvirt lock on each disk.
+
+Disks must be accessible locally.  This often means that adding disks
+from a remote libvirt connection (see L<http://libvirt.org/remote.html>)
+will fail unless those disks are accessible via the same device path
+locally too.
+
+The optional C<libvirturi> parameter sets the libvirt URI
+(see L<http://libvirt.org/uri.html>).  If this is not set then
+we connect to the default libvirt URI (or one set through an
+environment variable, see the libvirt documentation for full
+details).  If you are using the C API directly then it is more
+flexible to create the libvirt connection object yourself, get
+the domain object, and call C<guestfs_add_libvirt_dom>.
+
+The other optional parameters are passed directly through to
+C<guestfs_add_drive_opts>.");
+
+(*
+This interface is not quite baked yet. -- RWMJ 2010-11-11
+  ("add_libvirt_dom", (RInt "nrdisks", [Pointer ("virDomainPtr", "dom")], [Bool "readonly"; String "iface"]), -1, [NotInFish],
+   [],
+   "add the disk(s) from a libvirt domain",
+   "\
+This function adds the disk(s) attached to the libvirt domain C<dom>.
+It works by requesting the domain XML from libvirt, parsing it for
+disks, and calling C<guestfs_add_drive_opts> on each one.
+
+In the C API we declare C<void *dom>, but really it has type
+C<virDomainPtr dom>.  This is so we don't need E<lt>libvirt.hE<gt>.
+
+The number of disks added is returned.  This operation is atomic:
+if an error is returned, then no disks are added.
+
+This function does some minimal checks to make sure the libvirt
+domain is not running (unless C<readonly> is true).  In a future
+version we will try to acquire the libvirt lock on each disk.
+
+Disks must be accessible locally.  This often means that adding disks
+from a remote libvirt connection (see L<http://libvirt.org/remote.html>)
+will fail unless those disks are accessible via the same device path
+locally too.
+
+The optional parameters are passed directly through to
+C<guestfs_add_drive_opts>.");
+*)
+
 ]
 
 (* daemon_functions are any functions which cause some action
index e8d9587..d76904d 100644 (file)
@@ -131,6 +131,7 @@ src/inspect.c
 src/launch.c
 src/listfs.c
 src/proto.c
+src/virt.c
 test-tool/helper.c
 test-tool/test-tool.c
 tools/virt-cat.pl
index c9156f9..10c5c48 100644 (file)
@@ -32,6 +32,7 @@ TESTS = \
        rhbz576879.sh \
        rhbz578407.sh \
        rhbz580246.sh \
+       test-add-domain.sh \
        test-cancellation-download-librarycancels.sh \
        test-cancellation-upload-daemoncancels.sh \
        test-copy.sh \
diff --git a/regressions/test-add-domain.sh b/regressions/test-add-domain.sh
new file mode 100755 (executable)
index 0000000..d124b62
--- /dev/null
@@ -0,0 +1,79 @@
+#!/bin/bash -
+# libguestfs
+# Copyright (C) 2010 Red Hat Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+# Test add-domain command.
+
+set -e
+
+rm -f test1.img test2.img test3.img test.xml test.out
+
+cwd="$(pwd)"
+
+truncate -s 1M test1.img test2.img test3.img
+
+# Libvirt test XML, see libvirt.git/examples/xml/test/testnode.xml
+cat > test.xml <<EOF
+<node>
+  <domain type="test">
+    <name>guest</name>
+    <os>
+      <type>hvm</type>
+      <boot dev='hd'/>
+    </os>
+    <memory>524288</memory>
+    <devices>
+      <disk type="file">
+        <source file="$cwd/test1.img"/>
+        <target dev="hda"/>
+      </disk>
+      <disk type="file">
+        <driver name="qemu" type="raw"/>
+        <source file="$cwd/test2.img"/>
+        <target dev="hdb"/>
+      </disk>
+      <disk type="file">
+        <driver name="qemu" type="qcow2"/>
+        <source file="$cwd/test3.img"/>
+        <target dev="hdc"/>
+      </disk>
+    </devices>
+  </domain>
+</node>
+EOF
+
+../fish/guestfish >test.out <<EOF
+  domain guest libvirturi:test://$cwd/test.xml readonly:true
+  debug-cmdline
+EOF
+grep -sq "test1.img.*snapshot=on" test.out
+! grep -sq "test1.img.*format" test.out
+grep -sq "test2.img.*snapshot=on.*format=raw" test.out
+grep -sq "test3.img.*snapshot=on.*format=qcow2" test.out
+
+# Test atomicity.
+rm test3.img
+
+../fish/guestfish >test.out <<EOF
+  -domain guest libvirturi:test://$cwd/test.xml readonly:true
+  debug-cmdline
+EOF
+! grep -sq "test1.img" test.out
+! grep -sq "test2.img" test.out
+! grep -sq "test3.img" test.out
+
+rm -f test1.img test2.img test3.img test.xml test.out
index 4be61a5..d8a7f55 100644 (file)
@@ -133,9 +133,13 @@ libguestfs_la_SOURCES = \
        launch.c \
        listfs.c \
        proto.c \
+       virt.c \
        libguestfs.syms
 
-libguestfs_la_LIBADD = $(HIVEX_LIBS) $(AUGEAS_LIBS) $(PCRE_LIBS) $(MAGIC_LIBS) $(LTLIBTHREAD) ../gnulib/lib/libgnu.la
+libguestfs_la_LIBADD = \
+       $(HIVEX_LIBS) $(AUGEAS_LIBS) $(PCRE_LIBS) $(MAGIC_LIBS) \
+       $(LIBVIRT_LIBS) $(LIBXML2_LIBS) \
+       $(LTLIBTHREAD) ../gnulib/lib/libgnu.la
 
 # Make libguestfs include the convenience libraries.
 noinst_LTLIBRARIES = liberrnostring.la libprotocol.la
@@ -144,6 +148,7 @@ libguestfs_la_LIBADD += liberrnostring.la libprotocol.la
 libguestfs_la_CFLAGS = \
   -DGUESTFS_DEFAULT_PATH='"$(libdir)/guestfs"' \
   $(HIVEX_CFLAGS) $(AUGEAS_CFLAGS) $(PCRE_CFLAGS) \
+  $(LIBVIRT_CFLAGS) $(LIBXML2_CFLAGS) \
   $(WARN_CFLAGS) $(WERROR_CFLAGS)
 
 libguestfs_la_CPPFLAGS = -I$(top_srcdir)/gnulib/lib
diff --git a/src/virt.c b/src/virt.c
new file mode 100644 (file)
index 0000000..8911148
--- /dev/null
@@ -0,0 +1,319 @@
+/* libguestfs
+ * Copyright (C) 2010 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 <assert.h>
+
+#ifdef HAVE_LIBVIRT
+#include <libvirt/libvirt.h>
+#include <libvirt/virterror.h>
+#endif
+
+#ifdef HAVE_LIBXML2
+#include <libxml/xpath.h>
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#endif
+
+#include "guestfs.h"
+#include "guestfs-internal.h"
+#include "guestfs-internal-actions.h"
+#include "guestfs_protocol.h"
+
+#if defined(HAVE_LIBVIRT) && defined(HAVE_LIBXML2)
+
+static void init_libxml2 (void) __attribute__((constructor));
+
+static void
+init_libxml2 (void)
+{
+  /* I am told that you don't really need to call virInitialize ... */
+
+  xmlInitParser ();
+  LIBXML_TEST_VERSION;
+}
+
+struct guestfs___add_libvirt_dom_argv {
+  uint64_t bitmask;
+#define GUESTFS___ADD_LIBVIRT_DOM_READONLY_BITMASK (UINT64_C(1)<<0)
+  int readonly;
+#define GUESTFS___ADD_LIBVIRT_DOM_IFACE_BITMASK (UINT64_C(1)<<1)
+  const char *iface;
+};
+
+static int guestfs___add_libvirt_dom (guestfs_h *g, virDomainPtr dom, const struct guestfs___add_libvirt_dom_argv *optargs);
+
+int
+guestfs__add_domain (guestfs_h *g, const char *domain_name,
+                     const struct guestfs_add_domain_argv *optargs)
+{
+  virErrorPtr err;
+  virConnectPtr conn = NULL;
+  virDomainPtr dom = NULL;
+  int r = -1;
+  const char *libvirturi;
+  int readonly;
+  const char *iface;
+  struct guestfs___add_libvirt_dom_argv optargs2 = { .bitmask = 0 };
+
+  libvirturi = optargs->bitmask & GUESTFS_ADD_DOMAIN_LIBVIRTURI_BITMASK
+               ? optargs->libvirturi : NULL;
+  readonly = optargs->bitmask & GUESTFS_ADD_DOMAIN_READONLY_BITMASK
+             ? optargs->readonly : 0;
+  iface = optargs->bitmask & GUESTFS_ADD_DOMAIN_IFACE_BITMASK
+          ? optargs->iface : NULL;
+
+  /* Connect to libvirt, find the domain. */
+  conn = virConnectOpenReadOnly (libvirturi);
+  if (!conn) {
+    err = virGetLastError ();
+    error (g, _("could not connect to libvirt (code %d, domain %d): %s"),
+           err->code, err->domain, err->message);
+    goto cleanup;
+  }
+
+  dom = virDomainLookupByName (conn, domain_name);
+  if (!dom) {
+    err = virGetLastError ();
+    error (g, _("no libvirt domain called '%s': %s"),
+           domain_name, err->message);
+    goto cleanup;
+  }
+
+  if (readonly) {
+    optargs2.bitmask |= GUESTFS___ADD_LIBVIRT_DOM_READONLY_BITMASK;
+    optargs2.readonly = readonly;
+  }
+  if (iface) {
+    optargs2.bitmask |= GUESTFS___ADD_LIBVIRT_DOM_IFACE_BITMASK;
+    optargs2.iface = iface;
+  }
+
+  r = guestfs___add_libvirt_dom (g, dom, &optargs2);
+
+ cleanup:
+  if (dom) virDomainFree (dom);
+  if (conn) virConnectClose (conn);
+
+  return r;
+}
+
+/* This was proposed as an external API, but it's not quite baked yet. */
+static int
+guestfs___add_libvirt_dom (guestfs_h *g, virDomainPtr dom,
+                           const struct guestfs___add_libvirt_dom_argv *optargs)
+{
+  int r = -1, nr_added = 0, i;
+  virErrorPtr err;
+  xmlDocPtr doc = NULL;
+  xmlXPathContextPtr xpathCtx = NULL;
+  xmlXPathObjectPtr xpathObj = NULL;
+  char *xml = NULL;
+  int readonly;
+  const char *iface;
+  int cmdline_pos;
+
+  cmdline_pos = guestfs___checkpoint_cmdline (g);
+
+  readonly = optargs->bitmask & GUESTFS___ADD_LIBVIRT_DOM_READONLY_BITMASK
+             ? optargs->readonly : 0;
+  iface = optargs->bitmask & GUESTFS___ADD_LIBVIRT_DOM_IFACE_BITMASK
+          ? optargs->iface : NULL;
+
+  if (!readonly) {
+    virDomainInfo info;
+    if (virDomainGetInfo (dom, &info) == -1) {
+      err = virGetLastError ();
+      error (g, _("error getting domain info: %s"), err->message);
+      goto cleanup;
+    }
+    if (info.state != VIR_DOMAIN_SHUTOFF) {
+      error (g, _("error: domain is a live virtual machine.\nYou must use readonly access because write access to a running virtual machine\ncan cause disk corruption."));
+      goto cleanup;
+    }
+  }
+
+  /* Domain XML. */
+  xml = virDomainGetXMLDesc (dom, 0);
+
+  if (!xml) {
+    err = virGetLastError ();
+    error (g, _("error reading libvirt XML information: %s"),
+           err->message);
+    goto cleanup;
+  }
+
+  /* Now the horrible task of parsing out the fields we need from the XML.
+   * http://www.xmlsoft.org/examples/xpath1.c
+   */
+  doc = xmlParseMemory (xml, strlen (xml));
+  if (doc == NULL) {
+    error (g, _("unable to parse XML information returned by libvirt"));
+    goto cleanup;
+  }
+
+  xpathCtx = xmlXPathNewContext (doc);
+  if (xpathCtx == NULL) {
+    error (g, _("unable to create new XPath context"));
+    goto cleanup;
+  }
+
+  /* This gives us a set of all the <disk> nodes. */
+  xpathObj = xmlXPathEvalExpression (BAD_CAST "//devices/disk", xpathCtx);
+  if (xpathObj == NULL) {
+    error (g, _("unable to evaluate XPath expression"));
+    goto cleanup;
+  }
+
+  xmlNodeSetPtr nodes = xpathObj->nodesetval;
+  for (i = 0; i < nodes->nodeNr; ++i) {
+    xmlXPathObjectPtr xptype;
+
+    /* Change the context to the current <disk> node.
+     * DV advises to reset this before each search since older versions of
+     * libxml2 might overwrite it.
+     */
+    xpathCtx->node = nodes->nodeTab[i];
+
+    /* Filename can be in <source dev=..> or <source file=..> attribute.
+     * Check the <disk type=..> attribute first to find out which one.
+     */
+    xptype = xmlXPathEvalExpression (BAD_CAST "./@type", xpathCtx);
+    if (xptype == NULL ||
+        xptype->nodesetval == NULL ||
+        xptype->nodesetval->nodeNr == 0) {
+      xmlXPathFreeObject (xptype);
+      continue;                 /* no type attribute, skip it */
+    }
+    assert (xptype->nodesetval->nodeTab[0]);
+    assert (xptype->nodesetval->nodeTab[0]->type == XML_ATTRIBUTE_NODE);
+    xmlAttrPtr attr = (xmlAttrPtr) xptype->nodesetval->nodeTab[0];
+    char *type = (char *) xmlNodeListGetString (doc, attr->children, 1);
+
+    xmlXPathObjectPtr xpfilename;
+
+    if (STREQ (type, "file")) { /* type = "file" so look at source/@file */
+      free (type);
+
+      xpathCtx->node = nodes->nodeTab[i];
+      xpfilename = xmlXPathEvalExpression (BAD_CAST "./source/@file", xpathCtx);
+      if (xpfilename == NULL ||
+          xpfilename->nodesetval == NULL ||
+          xpfilename->nodesetval->nodeNr == 0) {
+        xmlXPathFreeObject (xpfilename);
+        continue;             /* disk filename not found, skip this */
+      }
+    } else if (STREQ (type, "block")) { /* type = "block", use source/@dev */
+      free (type);
+
+      xpathCtx->node = nodes->nodeTab[i];
+      xpfilename = xmlXPathEvalExpression (BAD_CAST "./source/@dev", xpathCtx);
+      if (xpfilename == NULL ||
+          xpfilename->nodesetval == NULL ||
+          xpfilename->nodesetval->nodeNr == 0) {
+        xmlXPathFreeObject (xpfilename);
+        continue;             /* disk filename not found, skip this */
+      }
+    } else {
+      free (type);
+      continue;               /* type <> "file" or "block", skip it */
+    }
+
+    assert (xpfilename->nodesetval->nodeTab[0]);
+    assert (xpfilename->nodesetval->nodeTab[0]->type == XML_ATTRIBUTE_NODE);
+    attr = (xmlAttrPtr) xpfilename->nodesetval->nodeTab[0];
+    char *filename = (char *) xmlNodeListGetString (doc, attr->children, 1);
+
+    /* Get the disk format (may not be set). */
+    xmlXPathObjectPtr xpformat;
+
+    xpathCtx->node = nodes->nodeTab[i];
+    xpformat = xmlXPathEvalExpression (BAD_CAST "./driver/@type", xpathCtx);
+    char *format = NULL;
+    if (xpformat != NULL &&
+        xpformat->nodesetval &&
+        xpformat->nodesetval->nodeNr > 0) {
+      assert (xpformat->nodesetval->nodeTab[0]);
+      assert (xpformat->nodesetval->nodeTab[0]->type == XML_ATTRIBUTE_NODE);
+      attr = (xmlAttrPtr) xpformat->nodesetval->nodeTab[0];
+      format = (char *) xmlNodeListGetString (doc, attr->children, 1);
+    }
+
+    /* Add the disk, with optional format. */
+    struct guestfs_add_drive_opts_argv optargs2 = { .bitmask = 0 };
+    if (readonly) {
+      optargs2.bitmask |= GUESTFS_ADD_DRIVE_OPTS_READONLY_BITMASK;
+      optargs2.readonly = readonly;
+    }
+    if (format) {
+      optargs2.bitmask |= GUESTFS_ADD_DRIVE_OPTS_FORMAT_BITMASK;
+      optargs2.format = format;
+    }
+    if (iface) {
+      optargs2.bitmask |= GUESTFS_ADD_DRIVE_OPTS_IFACE_BITMASK;
+      optargs2.iface = iface;
+    }
+
+    int t = guestfs__add_drive_opts (g, filename, &optargs2);
+
+    xmlFree (filename);
+    xmlFree (format);
+    xmlXPathFreeObject (xpfilename);
+    xmlXPathFreeObject (xpformat);
+
+    if (t == -1)
+      goto cleanup;
+
+    nr_added++;
+  }
+
+  if (nr_added == 0) {
+    error (g, _("libvirt domain has no disks"));
+    goto cleanup;
+  }
+
+  /* Successful. */
+  r = nr_added;
+
+ cleanup:
+  if (r == -1) guestfs___rollback_cmdline (g, cmdline_pos);
+  free (xml);
+  if (xpathObj) xmlXPathFreeObject (xpathObj);
+  if (xpathCtx) xmlXPathFreeContext (xpathCtx);
+  if (doc) xmlFreeDoc (doc);
+
+  return r;
+}
+
+#else /* no libvirt or libxml2 at compile time */
+
+#define NOT_IMPL(r)                                                     \
+  error (g, _("add-domain API not available since this version of libguestfs was compiled without libvirt or libxml2")); \
+  return r
+
+int
+guestfs__add_domain (guestfs_h *g, const char *dom,
+                     const struct guestfs_add_domain_argv *optargs)
+{
+  NOT_IMPL(-1);
+}
+
+#endif /* no libvirt or libxml2 at compile time */