[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

[Libguestfs] [PATCH 5/7 version 2] New APIs: add-domain and add-libvirt-dom.



Updated with Dan's suggested code changes.

This declares the function to use void* instead of virDomainPtr,
bypassing the question of whether and what <libvirt.h> might define.

Rich.

-- 
Richard Jones, Virtualization Group, Red Hat http://people.redhat.com/~rjones
virt-top is 'top' for virtual machines.  Tiny program with many
powerful monitoring features, net stats, disk stats, logging, etc.
http://et.redhat.com/~rjones/virt-top
>From a75caef192579fafe30e4cd20fabb2a1e7f3c9c0 Mon Sep 17 00:00:00 2001
From: Richard Jones <rjones redhat com>
Date: Tue, 9 Nov 2010 18:53:01 +0000
Subject: [PATCH 5/7] New APIs: add-domain and add-libvirt-dom.

These two new APIs allow you to add the disks from a libvirt
domain.  The higher level add-domain API takes the name of
the libvirt domain as a string and connects to libvirt itself.
The lower level add-libvirt-dom API relies on the program to
connect to libvirt and pass the virDomainPtr into the API call.

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

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

The returned number is the number of disks that were added.
---
 TODO                           |    8 -
 generator/generator_actions.ml |   58 ++++++++
 perl/typemap                   |   11 ++
 po/POTFILES.in                 |    1 +
 regressions/Makefile.am        |    1 +
 regressions/test-add-domain.sh |   79 ++++++++++
 src/Makefile.am                |    7 +-
 src/guestfs.h                  |    1 +
 src/virt.c                     |  316 ++++++++++++++++++++++++++++++++++++++++
 9 files changed, 473 insertions(+), 9 deletions(-)
 create mode 100755 regressions/test-add-domain.sh
 create mode 100644 src/virt.c

diff --git a/TODO b/TODO
index 0f933fa..2301c27 100644
--- 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.
diff --git a/generator/generator_actions.ml b/generator/generator_actions.ml
index a0a337b..05ef00e 100644
--- a/generator/generator_actions.ml
+++ b/generator/generator_actions.ml
@@ -1058,6 +1058,64 @@ 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_libvirt_dom", (RInt "nrdisks", [Pointer ("void *", "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.
+
+C<dom> really has type C<virDomainPtr> but we use C<void *> so that
+we don't depend on E<lt>libvirt.hE<gt> being present.
+
+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>.");
+
+  ("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>.");
+
 ]
 
 (* daemon_functions are any functions which cause some action
diff --git a/perl/typemap b/perl/typemap
index d978e60..223aee1 100644
--- a/perl/typemap
+++ b/perl/typemap
@@ -3,6 +3,7 @@ char *		T_PV
 const char *	T_PV
 guestfs_h *	O_OBJECT_guestfs_h
 int64_t	  	T_IV
+virDomainPtr O_OBJECT_domain
 
 INPUT
 O_OBJECT_guestfs_h
@@ -18,6 +19,16 @@ O_OBJECT_guestfs_h
         croak (\"${Package}::$func_name(): $var is not a blessed HV reference\");
     }
 
+# This comes from the Sys::Virt bindings.
+INPUT
+O_OBJECT_domain
+    if (sv_isobject ($arg) && (SvTYPE (SvRV ($arg)) == SVt_PVMG))
+        $var = ($type)SvIV ((SV*) SvRV ($arg));
+    else {
+        warn(\"${Package}::$func_name() -- $var is not a blessed SV reference\");
+        XSRETURN_UNDEF;
+    }
+
 OUTPUT
 O_OBJECT_guestfs_h
     sv_setiv ($arg, PTR2IV ($var));
diff --git a/po/POTFILES.in b/po/POTFILES.in
index e8d9587..d76904d 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -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
diff --git a/regressions/Makefile.am b/regressions/Makefile.am
index c9156f9..10c5c48 100644
--- a/regressions/Makefile.am
+++ b/regressions/Makefile.am
@@ -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
index 0000000..d124b62
--- /dev/null
+++ b/regressions/test-add-domain.sh
@@ -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
diff --git a/src/Makefile.am b/src/Makefile.am
index 4be61a5..d8a7f55 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -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/guestfs.h b/src/guestfs.h
index 0f4f9fd..83d4ad8 100644
--- a/src/guestfs.h
+++ b/src/guestfs.h
@@ -79,6 +79,7 @@ extern void guestfs_set_private (guestfs_h *g, const char *key, void *data);
 extern void *guestfs_get_private (guestfs_h *g, const char *key);
 
 /*--- Structures and actions ---*/
+
 #include <rpc/types.h>
 #include <rpc/xdr.h>
 #include <guestfs-structs.h>
diff --git a/src/virt.c b/src/virt.c
new file mode 100644
index 0000000..c2211b0
--- /dev/null
+++ b/src/virt.c
@@ -0,0 +1,316 @@
+/* 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;
+}
+
+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;
+}
+
+int
+guestfs__add_libvirt_dom (guestfs_h *g, void * /* really virDomainPtr */ domvp,
+                          const struct guestfs_add_libvirt_dom_argv *optargs)
+{
+  virDomainPtr dom = domvp;
+  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 APIs 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);
+}
+
+int
+guestfs__add_libvirt_dom (guestfs_h *g, void *dom,
+                          const struct guestfs_add_libvirt_dom_argv *optargs)
+{
+  NOT_IMPL(-1);
+}
+
+#endif /* no libvirt or libxml2 at compile time */
-- 
1.7.3.2


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]