diff options
-rw-r--r-- | Makefile.am | 1 | ||||
-rw-r--r-- | configure.ac | 1 | ||||
-rw-r--r-- | daemon/Makefile.am | 1 | ||||
-rw-r--r-- | daemon/hotplug.c | 67 | ||||
-rw-r--r-- | fish/alloc.c | 5 | ||||
-rw-r--r-- | generator/actions.ml | 24 | ||||
-rw-r--r-- | po/POTFILES | 1 | ||||
-rw-r--r-- | src/MAX_PROC_NR | 2 | ||||
-rw-r--r-- | src/guestfs-internal.h | 3 | ||||
-rw-r--r-- | src/guestfs.pod | 38 | ||||
-rw-r--r-- | src/launch-libvirt.c | 99 | ||||
-rw-r--r-- | src/launch.c | 80 | ||||
-rw-r--r-- | tests/hotplug/Makefile.am | 26 | ||||
-rwxr-xr-x | tests/hotplug/test-hot-add.pl | 66 |
14 files changed, 386 insertions, 28 deletions
diff --git a/Makefile.am b/Makefile.am index 4e476ea9..50bfb322 100644 --- a/Makefile.am +++ b/Makefile.am @@ -52,6 +52,7 @@ SUBDIRS += tests/9p SUBDIRS += tests/rsync SUBDIRS += tests/bigdirs SUBDIRS += tests/disk-labels +SUBDIRS += tests/hotplug SUBDIRS += tests/regressions endif diff --git a/configure.ac b/configure.ac index 0422bcb4..b4720bce 100644 --- a/configure.ac +++ b/configure.ac @@ -1379,6 +1379,7 @@ AC_CONFIG_FILES([Makefile tests/disk-labels/Makefile tests/extra/Makefile tests/guests/Makefile + tests/hotplug/Makefile tests/luks/Makefile tests/lvm/Makefile tests/md/Makefile diff --git a/daemon/Makefile.am b/daemon/Makefile.am index 2a25c695..9ffff152 100644 --- a/daemon/Makefile.am +++ b/daemon/Makefile.am @@ -125,6 +125,7 @@ guestfsd_SOURCES = \ guestfsd.c \ headtail.c \ hexdump.c \ + hotplug.c \ hivex.c \ htonl.c \ initrd.c \ diff --git a/daemon/hotplug.c b/daemon/hotplug.c new file mode 100644 index 00000000..aae638e7 --- /dev/null +++ b/daemon/hotplug.c @@ -0,0 +1,67 @@ +/* libguestfs - the guestfsd daemon + * Copyright (C) 2012 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <errno.h> +#include <time.h> + +#include "guestfs_protocol.h" +#include "daemon.h" +#include "actions.h" + +#define HOT_ADD_TIMEOUT 30 /* seconds */ + +/* Wait for /dev/disk/guestfs/<label> to appear. Timeout (and error) + * if it doesn't appear after a reasonable length of time. + */ +int +do_internal_hot_add_drive (const char *label) +{ + time_t start_t, now_t; + size_t len = strlen (label); + char path[len+64]; + int r; + + snprintf (path, len+64, "/dev/disk/guestfs/%s", label); + + time (&start_t); + + while (time (&now_t) - start_t <= HOT_ADD_TIMEOUT) { + udev_settle (); + + r = access (path, F_OK); + if (r == -1 && errno != ENOENT) { + reply_with_perror ("%s", path); + return -1; + } + if (r == 0) + return 0; + + sleep (1); + } + + reply_with_error ("hot-add drive: '%s' did not appear after %d seconds: " + "this could mean that virtio-scsi (in qemu or kernel) " + "or udev is not working", + path, HOT_ADD_TIMEOUT); + return -1; +} diff --git a/fish/alloc.c b/fish/alloc.c index f6e5b8ff..7454fb73 100644 --- a/fish/alloc.c +++ b/fish/alloc.c @@ -72,11 +72,6 @@ alloc_disk (const char *filename, const char *size_str, int add, int sparse) if (parse_size (size_str, &size) == -1) return -1; - if (!guestfs_is_config (g)) { - fprintf (stderr, _("can't allocate or add disks after launching\n")); - return -1; - } - fd = open (filename, O_WRONLY|O_CREAT|O_NOCTTY|O_TRUNC|O_CLOEXEC, 0666); if (fd == -1) { perror (filename); diff --git a/generator/actions.ml b/generator/actions.ml index 25d28098..39c71ab8 100644 --- a/generator/actions.ml +++ b/generator/actions.ml @@ -1186,14 +1186,22 @@ not all belong to a single logical operating system name = "add_drive"; style = RErr, [String "filename"], [OBool "readonly"; OString "format"; OString "iface"; OString "name"; OString "label"]; once_had_no_optargs = true; - fish_alias = ["add"]; config_only = true; + fish_alias = ["add"]; shortdesc = "add an image to examine or modify"; longdesc = "\ This function adds a disk image called C<filename> to the handle. C<filename> may be a regular host file or a host device. -The first time you call this function, the disk appears as -C</dev/sda>, the second time as C</dev/sdb>, and so on. +When this function is called before C<guestfs_launch> (the +usual case) then the first time you call this function, +the disk appears in the API as C</dev/sda>, the second time +as C</dev/sdb>, and so on. + +In libguestfs E<ge> 1.20 you can also call this function +after launch (with some restrictions). This is called +\"hotplugging\". When hotplugging, you must specify a +C<label> so that the new disk gets a predictable name. +For more information see L<guestfs(3)/HOTPLUGGING>. You don't necessarily need to be root when using libguestfs. However you obviously do need sufficient permissions to access the filename @@ -9951,6 +9959,16 @@ This returns a hashtable, where keys are the disk labels are the full raw block device and partition names (eg. C</dev/sda> and C</dev/sda1>)." }; + { defaults with + name = "internal_hot_add_drive"; + style = RErr, [String "label"], []; + proc_nr = Some 370; + in_fish = false; in_docs = false; + tests = []; + shortdesc = "internal hotplugging operation"; + longdesc = "\ +This function is used internally when hotplugging drives." }; + ] (* Non-API meta-commands available only in guestfish. diff --git a/po/POTFILES b/po/POTFILES index 548156c4..22cd1486 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -42,6 +42,7 @@ daemon/guestfsd.c daemon/headtail.c daemon/hexdump.c daemon/hivex.c +daemon/hotplug.c daemon/htonl.c daemon/initrd.c daemon/inotify.c diff --git a/src/MAX_PROC_NR b/src/MAX_PROC_NR index 446dfcc5..5b0cffbc 100644 --- a/src/MAX_PROC_NR +++ b/src/MAX_PROC_NR @@ -1 +1 @@ -369 +370 diff --git a/src/guestfs-internal.h b/src/guestfs-internal.h index ed9ada48..db9818c3 100644 --- a/src/guestfs-internal.h +++ b/src/guestfs-internal.h @@ -166,6 +166,9 @@ struct attach_ops { int (*get_pid) (guestfs_h *g); /* get-pid API. */ int (*max_disks) (guestfs_h *g); /* max-disks API. */ + + /* Hotplugging drives. */ + int (*hot_add_drive) (guestfs_h *g, struct drive *drv, size_t drv_index); }; extern struct attach_ops attach_ops_appliance; extern struct attach_ops attach_ops_libvirt; diff --git a/src/guestfs.pod b/src/guestfs.pod index 48d810b0..624e743e 100644 --- a/src/guestfs.pod +++ b/src/guestfs.pod @@ -143,8 +143,11 @@ one you added), etc. Once L</guestfs_launch> has been called you cannot add any more images. You can call L</guestfs_list_devices> to get a list of the device -names, in the order that you added them. See also L</BLOCK DEVICE -NAMING> below. +names, in the order that you added them. +See also L</BLOCK DEVICE NAMING> below. + +There are slightly different rules when hotplugging disks (in +libguestfs E<ge> 1.20). See L</HOTPLUGGING> below. =head2 MOUNTING @@ -601,6 +604,35 @@ Libguestfs on top of FUSE performs quite poorly. For best performance do not use it. Use ordinary libguestfs filesystem calls, upload, download etc. instead. +=head2 HOTPLUGGING + +In libguestfs E<ge> 1.20, you may add drives after calling +L</guestfs_launch>. There are some restrictions, see below. +This is called I<hotplugging>. + +Only a subset of the attach-method backends support hotplugging +(currently only the libvirt attach-method has support). It also +requires that you use libvirt E<ge> 0.10.3 and qemu E<ge> 1.2. + +To hot-add a disk, simply call L</guestfs_add_drive_opts> after +L</guestfs_launch>. It is mandatory to specify the C<label> parameter +so that the newly added disk has a predictable name. For example: + + if (guestfs_launch (g) == -1) + error ("launch failed"); + + if (guestfs_add_drive_opts (g, filename, + GUESTFS_ADD_DRIVE_OPTS_LABEL, "newdisk", + -1) == -1) + error ("hot-add of disk failed"); + + if (guestfs_part_disk ("/dev/disk/guestfs/newdisk", "mbr") == -1) + error ("partitioning of hot-added disk failed"); + +Backends that support hotplugging do not require that you add +E<ge> 1 disk before calling launch. When hotplugging is supported +you don't need to add any disks. + =head2 INSPECTION Libguestfs has APIs for inspecting an unknown disk image to find out @@ -2639,7 +2671,7 @@ The guest may be killed by L</guestfs_kill_subprocess>, or may die asynchronously at any time (eg. due to some internal error), and that causes the state to transition back to CONFIG. -Configuration commands for qemu such as L</guestfs_add_drive> can only +Configuration commands for qemu such as L</guestfs_set_path> can only be issued when in the CONFIG state. The API offers one call that goes from CONFIG through LAUNCHING to diff --git a/src/launch-libvirt.c b/src/launch-libvirt.c index aa1c39f2..3eab5676 100644 --- a/src/launch-libvirt.c +++ b/src/launch-libvirt.c @@ -135,14 +135,6 @@ launch_libvirt (guestfs_h *g, const char *libvirt_uri) struct drive *drv; size_t i; - /* At present you must add drives before starting the appliance. In - * future when we enable hotplugging you won't need to do this. - */ - if (!g->nr_drives) { - error (g, _("you must call guestfs_add_drive before guestfs_launch")); - return -1; - } - /* XXX: It should be possible to make this work. */ if (g->direct) { error (g, _("direct mode flag is not supported yet for libvirt attach method")); @@ -1376,6 +1368,89 @@ max_disks_libvirt (guestfs_h *g) return 255; } +static xmlChar *construct_libvirt_xml_hot_add_disk (guestfs_h *g, struct drive *drv, size_t drv_index); + +/* Hot-add a drive. Note the appliance is up when this is called. */ +static int +hot_add_drive_libvirt (guestfs_h *g, struct drive *drv, size_t drv_index) +{ + virConnectPtr conn = g->virt.connv; + virDomainPtr dom = g->virt.domv; + xmlChar *xml = NULL; + + if (!conn || !dom) { + /* This is essentially an internal error if it happens. */ + error (g, "%s: conn == NULL or dom == NULL", __func__); + return -1; + } + + /* Create overlay for read-only drive. This works around lack of + * support for <transient/> disks in libvirt. + */ + if (make_qcow2_overlay_for_drive (g, drv) == -1) + return -1; + + /* Create the XML for the new disk. */ + xml = construct_libvirt_xml_hot_add_disk (g, drv, drv_index); + if (xml == NULL) + return -1; + + /* Attach it. */ + if (virDomainAttachDeviceFlags (dom, (char *) xml, + VIR_DOMAIN_DEVICE_MODIFY_LIVE) == -1) { + libvirt_error (g, _("could not attach disk to libvirt domain")); + goto error; + } + + free (xml); + return 0; + + error: + free (xml); + return -1; +} + +static xmlChar * +construct_libvirt_xml_hot_add_disk (guestfs_h *g, struct drive *drv, + size_t drv_index) +{ + xmlChar *ret = NULL; + xmlBufferPtr xb = NULL; + xmlOutputBufferPtr ob; + xmlTextWriterPtr xo = NULL; + + XMLERROR (NULL, xb = xmlBufferCreate ()); + XMLERROR (NULL, ob = xmlOutputBufferCreateBuffer (xb, NULL)); + XMLERROR (NULL, xo = xmlNewTextWriter (ob)); + + XMLERROR (-1, xmlTextWriterSetIndent (xo, 1)); + XMLERROR (-1, xmlTextWriterSetIndentString (xo, BAD_CAST " ")); + XMLERROR (-1, xmlTextWriterStartDocument (xo, NULL, NULL, NULL)); + + if (construct_libvirt_xml_disk (g, xo, drv, drv_index) == -1) + goto err; + + XMLERROR (-1, xmlTextWriterEndDocument (xo)); + XMLERROR (NULL, ret = xmlBufferDetach (xb)); /* caller frees ret */ + + debug (g, "hot-add disk XML:\n%s", ret); + + err: + if (xo) + xmlFreeTextWriter (xo); /* frees 'ob' too */ + if (xb) + xmlBufferFree (xb); + + return ret; +} + +struct attach_ops attach_ops_libvirt = { + .launch = launch_libvirt, + .shutdown = shutdown_libvirt, + .max_disks = max_disks_libvirt, + .hot_add_drive = hot_add_drive_libvirt, +}; + #else /* no libvirt or libxml2 at compile time */ #define NOT_IMPL(r) \ @@ -1403,10 +1478,16 @@ max_disks_libvirt (guestfs_h *g) NOT_IMPL (-1); } -#endif /* no libvirt or libxml2 at compile time */ +static int +hot_add_drive_libvirt (guestfs_h *g, struct drive *drv, size_t drv_index) +{ + NOT_IMPL (-1); +} struct attach_ops attach_ops_libvirt = { .launch = launch_libvirt, .shutdown = shutdown_libvirt, .max_disks = max_disks_libvirt, }; + +#endif /* no libvirt or libxml2 at compile time */ diff --git a/src/launch.c b/src/launch.c index 1b6cf4bd..a4e10fef 100644 --- a/src/launch.c +++ b/src/launch.c @@ -29,6 +29,7 @@ #include <sys/types.h> #include <sys/wait.h> #include <errno.h> +#include <assert.h> #include "c-ctype.h" @@ -57,14 +58,29 @@ guestfs___rollback_drives (guestfs_h *g, size_t old_i) g->nr_drives = old_i; } +/* Add struct drive to the g->drives vector at the given index. */ +static void +add_drive_to_handle_at (guestfs_h *g, struct drive *d, size_t drv_index) +{ + if (drv_index >= g->nr_drives) { + g->drives = safe_realloc (g, g->drives, + sizeof (struct drive *) * (drv_index + 1)); + while (g->nr_drives <= drv_index) { + g->drives[g->nr_drives] = NULL; + g->nr_drives++; + } + } + + assert (g->drives[drv_index] == NULL); + + g->drives[drv_index] = d; +} + /* Add struct drive to the end of the g->drives vector in the handle. */ static void add_drive_to_handle (guestfs_h *g, struct drive *d) { - g->nr_drives++; - g->drives = safe_realloc (g, g->drives, - sizeof (struct drive *) * g->nr_drives); - g->drives[g->nr_drives-1] = d; + add_drive_to_handle_at (g, d, g->nr_drives); } static struct drive * @@ -207,6 +223,48 @@ valid_disk_label (const char *str) return 1; } +/* The low-level function that adds a drive. */ +static int +add_drive (guestfs_h *g, struct drive *drv) +{ + size_t i, drv_index; + + if (g->state == CONFIG) { + /* Not hotplugging, so just add it to the handle. */ + add_drive_to_handle (g, drv); + return 0; + } + + /* ... else, hotplugging case. */ + if (!g->attach_ops || !g->attach_ops->hot_add_drive) { + error (g, _("the current attach-method does not support hotplugging drives")); + return -1; + } + + if (!drv->disk_label) { + error (g, _("'label' is required when hotplugging drives")); + return -1; + } + + /* Get the first free index, or add it at the end. */ + drv_index = g->nr_drives; + for (i = 0; i < g->nr_drives; ++i) + if (g->drives[i] == NULL) + drv_index = i; + + /* Hot-add the drive. */ + if (g->attach_ops->hot_add_drive (g, drv, drv_index) == -1) + return -1; + + add_drive_to_handle_at (g, drv, drv_index); + + /* Call into the appliance to wait for the new drive to appear. */ + if (guestfs_internal_hot_add_drive (g, drv->disk_label) == -1) + return -1; + + return 0; +} + /* Traditionally you have been able to use /dev/null as a filename, as * many times as you like. Ancient KVM (RHEL 5) cannot handle adding * /dev/null readonly. qemu 1.2 + virtio-scsi segfaults when you use @@ -220,7 +278,7 @@ add_null_drive (guestfs_h *g, int readonly, const char *format, const char *iface, const char *name, const char *disk_label) { char *tmpfile = NULL; - int fd = -1; + int fd = -1, r; struct drive *drv; if (format && STRNEQ (format, "raw")) { @@ -252,8 +310,12 @@ add_null_drive (guestfs_h *g, int readonly, const char *format, } drv = create_drive_struct (g, tmpfile, readonly, format, iface, name, disk_label, 0); - add_drive_to_handle (g, drv); + r = add_drive (g, drv); free (tmpfile); + if (r == -1) { + free_drive_struct (drv); + return -1; + } return 0; @@ -328,7 +390,11 @@ guestfs__add_drive_opts (guestfs_h *g, const char *filename, drv = create_drive_struct (g, filename, readonly, format, iface, name, disk_label, use_cache_none); - add_drive_to_handle (g, drv); + if (add_drive (g, drv) == -1) { + free_drive_struct (drv); + return -1; + } + return 0; } diff --git a/tests/hotplug/Makefile.am b/tests/hotplug/Makefile.am new file mode 100644 index 00000000..6644930a --- /dev/null +++ b/tests/hotplug/Makefile.am @@ -0,0 +1,26 @@ +# libguestfs +# Copyright (C) 2012 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +include $(top_srcdir)/subdir-rules.mk + +TESTS = \ + test-hot-add.pl + +TESTS_ENVIRONMENT = $(top_builddir)/run --test + +EXTRA_DIST = \ + $(TESTS) diff --git a/tests/hotplug/test-hot-add.pl b/tests/hotplug/test-hot-add.pl new file mode 100755 index 00000000..b1322e88 --- /dev/null +++ b/tests/hotplug/test-hot-add.pl @@ -0,0 +1,66 @@ +#!/usr/bin/perl +# Copyright (C) 2012 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +# Test hot-adding disks. + +use strict; +use warnings; + +use Sys::Guestfs; + +my $g = Sys::Guestfs->new (); + +# Skip the test if the default attach-method isn't libvirt, since only +# the libvirt backend supports hotplugging. +my $attach_method = $g->get_attach_method (); +unless ($attach_method eq "libvirt" || $attach_method =~ /^libvirt:/) { + print "$0: test skipped because attach-method ($attach_method) is not libvirt\n"; + exit 77 +} + +# We don't need to add disks before launch. +$g->launch (); + +# Create some temporary disks. +open FILE, ">test1.img" or die "test1.img: $!"; +truncate FILE, 512 * 1024 * 1024 or die "test1.img: truncate: $!"; +close FILE; + +open FILE, ">test2.img" or die "test2.img: $!"; +truncate FILE, 512 * 1024 * 1024 or die "test2.img: truncate: $!"; +close FILE; + +die unless system ("qemu-img create -f qcow2 test3.img 1G") == 0; + +# Hot-add them. Labels are required. +$g->add_drive ("test1.img", label => "a"); # autodetect format +$g->add_drive ("test2.img", label => "b", format => "raw", readonly => 1); +$g->add_drive ("test3.img", label => "c", format => "qcow2"); + +# Check we can use the disks immediately. +$g->part_disk ("/dev/disk/guestfs/a", "mbr"); +$g->mkfs ("ext2", "/dev/disk/guestfs/c"); +$g->mkfs ("ext2", "/dev/disk/guestfs/a1"); + +$g->shutdown (); +$g->close (); + +unlink "test1.img"; +unlink "test2.img"; +unlink "test3.img"; + +exit 0 |