Richard W.M. Jones
2013-Mar-15 13:56 UTC
[Libguestfs] [PATCH] lib: Add direct support for the NBD (Network Block Device) protocol.
From: "Richard W.M. Jones" <rjones at redhat.com> You can now add remote NBD drives using: ><fs> add-drive "" format:raw protocol:nbd server:localhost (Note that you also need to add port:NNNN if the server is running on a non-standard port). The corresponding qemu-nbd service can be started by doing: qemu-nbd disk.img -t This commit also adds a test. --- Makefile.am | 1 + README | 2 + configure.ac | 1 + generator/actions.ml | 36 +++++- src/drives.c | 302 ++++++++++++++++++++++++++++++++++++------------- src/guestfs-internal.h | 19 +++- src/guestfs.pod | 35 ++++++ src/launch-appliance.c | 81 +++++++------ src/launch-libvirt.c | 259 +++++++++++++++++++++++++++++------------- tests/nbd/Makefile.am | 26 +++++ tests/nbd/test-nbd.pl | 78 +++++++++++++ 11 files changed, 644 insertions(+), 196 deletions(-) create mode 100644 tests/nbd/Makefile.am create mode 100755 tests/nbd/test-nbd.pl diff --git a/Makefile.am b/Makefile.am index 1bb5ce0..9c649a9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -56,6 +56,7 @@ SUBDIRS += tests/rsync SUBDIRS += tests/bigdirs SUBDIRS += tests/disk-labels SUBDIRS += tests/hotplug +SUBDIRS += tests/nbd SUBDIRS += tests/regressions endif diff --git a/README b/README index 4f2fb7f..5284d58 100644 --- a/README +++ b/README @@ -155,6 +155,8 @@ The full requirements are described below. +--------------+-------------+---+-----------------------------------------+ | static glibc | | O | Used for testing only. | +--------------+-------------+---+-----------------------------------------+ +| qemu-nbd | | O | Used for testing only. | ++--------------+-------------+---+-----------------------------------------+ | findlib | | O | For the OCaml bindings. | +--------------+-------------+---+-----------------------------------------+ | ocaml-gettext| | O | For localizing OCaml virt-* tools. | diff --git a/configure.ac b/configure.ac index 7059867..64b4513 100644 --- a/configure.ac +++ b/configure.ac @@ -1574,6 +1574,7 @@ AC_CONFIG_FILES([Makefile tests/md/Makefile tests/mount-local/Makefile tests/mountable/Makefile + tests/nbd/Makefile tests/ntfsclone/Makefile tests/parallel/Makefile tests/protocol/Makefile diff --git a/generator/actions.ml b/generator/actions.ml index 958c606..30e2482 100644 --- a/generator/actions.ml +++ b/generator/actions.ml @@ -1247,7 +1247,7 @@ not all belong to a single logical operating system { defaults with name = "add_drive"; - style = RErr, [String "filename"], [OBool "readonly"; OString "format"; OString "iface"; OString "name"; OString "label"]; + style = RErr, [String "filename"], [OBool "readonly"; OString "format"; OString "iface"; OString "name"; OString "label"; OString "protocol"; OString "server"; OInt "port"]; once_had_no_optargs = true; blocking = false; fish_alias = ["add"]; @@ -1319,6 +1319,40 @@ the drive will also be named C</dev/disk/guestfs/I<label>>. See L<guestfs(3)/DISK LABELS>. +=item C<protocol> + +The optional protocol argument can be used to select an alternate +source protocol: + +=over 4 + +=item C<protocol = \"file\"> + +C<filename> is interpreted as a local file or device. +This is the default if the optional protocol parameter +is omitted. + +=item C<protocol = \"nbd\"> + +Connect to the Network Block Device server at C<server:port>. + +See: L<guestfs(3)/NETWORK BLOCK DEVICES>. + +=back + +=item C<server> + +For protocols which require access to a remote server, this +is the name of the server. + +=item C<port> + +For protocols which require access to a remote server, this +is the port number of the service. + +If not specified, this defaults to the standard port for +the protocol, eg. 10809 when C<protocol> is C<\"nbd\">. + =back" }; { defaults with diff --git a/src/drives.c b/src/drives.c index d082193..c3199e8 100644 --- a/src/drives.c +++ b/src/drives.c @@ -30,6 +30,8 @@ #include <inttypes.h> #include <unistd.h> #include <fcntl.h> +#include <netdb.h> +#include <arpa/inet.h> #include <assert.h> #include "c-ctype.h" @@ -41,21 +43,51 @@ /* Create and free the 'drive' struct. */ static struct drive * -create_drive_struct (guestfs_h *g, const char *path, - bool readonly, const char *format, - const char *iface, const char *name, - const char *disk_label, - bool use_cache_none) +create_drive_file (guestfs_h *g, const char *path, + bool readonly, const char *format, + const char *iface, const char *name, + const char *disk_label, + bool use_cache_none) { struct drive *drv = safe_malloc (g, sizeof (struct drive)); - drv->path = safe_strdup (g, path); + drv->protocol = drive_protocol_file; + drv->u.path = safe_strdup (g, path); + drv->readonly = readonly; drv->format = format ? safe_strdup (g, format) : NULL; drv->iface = iface ? safe_strdup (g, iface) : NULL; drv->name = name ? safe_strdup (g, name) : NULL; drv->disk_label = disk_label ? safe_strdup (g, disk_label) : NULL; drv->use_cache_none = use_cache_none; + + drv->priv = drv->free_priv = NULL; + + return drv; +} + +static struct drive * +create_drive_nbd (guestfs_h *g, + const char *server, int port, const char *exportname, + bool readonly, const char *format, + const char *iface, const char *name, + const char *disk_label, + bool use_cache_none) +{ + struct drive *drv = safe_malloc (g, sizeof (struct drive)); + + drv->protocol = drive_protocol_nbd; + drv->u.nbd.server = safe_strdup (g, server); + drv->u.nbd.port = port; + drv->u.nbd.exportname = safe_strdup (g, exportname); + + drv->readonly = readonly; + drv->format = format ? safe_strdup (g, format) : NULL; + drv->iface = iface ? safe_strdup (g, iface) : NULL; + drv->name = name ? safe_strdup (g, name) : NULL; + drv->disk_label = disk_label ? safe_strdup (g, disk_label) : NULL; + drv->use_cache_none = use_cache_none; + drv->priv = drv->free_priv = NULL; return drv; @@ -70,9 +102,9 @@ create_drive_struct (guestfs_h *g, const char *path, * a null drive. */ static struct drive * -create_null_drive_struct (guestfs_h *g, bool readonly, const char *format, - const char *iface, const char *name, - const char *disk_label) +create_drive_dev_null (guestfs_h *g, bool readonly, const char *format, + const char *iface, const char *name, + const char *disk_label) { CLEANUP_FREE char *tmpfile = NULL; int fd = -1; @@ -107,30 +139,82 @@ create_null_drive_struct (guestfs_h *g, bool readonly, const char *format, return NULL; } - return create_drive_struct (g, tmpfile, readonly, format, iface, name, - disk_label, 0); + return create_drive_file (g, tmpfile, readonly, format, iface, name, + disk_label, 0); } static struct drive * -create_dummy_drive_struct (guestfs_h *g) +create_drive_dummy (guestfs_h *g) { /* A special drive struct that is used as a dummy slot for the appliance. */ - return create_drive_struct (g, "", 0, NULL, NULL, NULL, NULL, 0); + return create_drive_file (g, "", 0, NULL, NULL, NULL, NULL, 0); } static void free_drive_struct (struct drive *drv) { - free (drv->path); + switch (drv->protocol) { + case drive_protocol_file: + free (drv->u.path); + break; + case drive_protocol_nbd: + free (drv->u.nbd.server); + free (drv->u.nbd.exportname); + break; + default: + abort (); + } + free (drv->format); free (drv->iface); free (drv->name); free (drv->disk_label); + if (drv->priv && drv->free_priv) drv->free_priv (drv->priv); + free (drv); } +/* Convert a struct drive to a string for debugging. The caller + * must free this string. + */ +static char * +drive_to_string (guestfs_h *g, const struct drive *drv) +{ + CLEANUP_FREE char *p = NULL; + + switch (drv->protocol) { + case drive_protocol_file: + p = safe_asprintf (g, "path=%s", drv->u.path); + break; + case drive_protocol_nbd: + if (STREQ (drv->u.nbd.exportname, "")) + p = safe_asprintf (g, "nbd=%s:%d", drv->u.nbd.server, drv->u.nbd.port); + else + p = safe_asprintf (g, "nbd=%s:%d:exportname=%s", + drv->u.nbd.server, drv->u.nbd.port, + drv->u.nbd.exportname); + break; + default: + abort (); + } + + return safe_asprintf + (g, "%s%s%s%s%s%s%s%s%s%s%s", + p, + drv->readonly ? " readonly" : "", + drv->format ? " format=" : "", + drv->format ? : "", + drv->iface ? " iface=" : "", + drv->iface ? : "", + drv->name ? " name=" : "", + drv->name ? : "", + drv->disk_label ? " label=" : "", + drv->disk_label ? : "", + drv->use_cache_none ? " cache=none" : ""); +} + /* 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) @@ -162,7 +246,7 @@ guestfs___add_dummy_appliance_drive (guestfs_h *g) { struct drive *drv; - drv = create_dummy_drive_struct (g); + drv = create_drive_dummy (g); add_drive_to_handle (g, drv); } @@ -183,6 +267,48 @@ guestfs___free_drives (guestfs_h *g) g->nr_drives = 0; } +/* 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; +} + /* cache=none improves reliability in the event of a host crash. * * However this option causes qemu to try to open the file with @@ -263,46 +389,35 @@ valid_disk_label (const char *str) return 1; } -/* The low-level function that adds a drive. */ +/* Check the server (hostname) is reasonable. */ static int -add_drive (guestfs_h *g, struct drive *drv) +valid_server (const char *str) { - size_t i, drv_index; + size_t len = strlen (str); - if (g->state == CONFIG) { - /* Not hotplugging, so just add it to the handle. */ - add_drive_to_handle (g, drv); + if (len == 0 || len > 255) 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; + while (len > 0) { + char c = *str++; + len--; + if (!c_isalnum (c) && + c != '-' && c != '.' && c != ':' && c != '[' && c != ']') + return 0; } + 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; +static int +nbd_port (void) +{ + struct servent *servent; - return 0; + servent = getservbyname ("nbd", "tcp"); + if (servent) + return ntohs (servent->s_port); + else + return 10809; } int @@ -314,6 +429,9 @@ guestfs__add_drive_opts (guestfs_h *g, const char *filename, const char *iface; const char *name; const char *disk_label; + const char *protocol; + const char *server; + int port; int use_cache_none; struct drive *drv; @@ -324,15 +442,27 @@ guestfs__add_drive_opts (guestfs_h *g, const char *filename, } readonly = optargs->bitmask & GUESTFS_ADD_DRIVE_OPTS_READONLY_BITMASK - ? optargs->readonly : false; + ? optargs->readonly : false; format = optargs->bitmask & GUESTFS_ADD_DRIVE_OPTS_FORMAT_BITMASK - ? optargs->format : NULL; + ? optargs->format : NULL; iface = optargs->bitmask & GUESTFS_ADD_DRIVE_OPTS_IFACE_BITMASK - ? optargs->iface : NULL; + ? optargs->iface : NULL; name = optargs->bitmask & GUESTFS_ADD_DRIVE_OPTS_NAME_BITMASK - ? optargs->name : NULL; + ? optargs->name : NULL; disk_label = optargs->bitmask & GUESTFS_ADD_DRIVE_OPTS_LABEL_BITMASK - ? optargs->label : NULL; + ? optargs->label : NULL; + protocol = optargs->bitmask & GUESTFS_ADD_DRIVE_OPTS_PROTOCOL_BITMASK + ? optargs->protocol : "file"; + server = optargs->bitmask & GUESTFS_ADD_DRIVE_OPTS_SERVER_BITMASK + ? optargs->server : NULL; + if (optargs->bitmask & GUESTFS_ADD_DRIVE_OPTS_PORT_BITMASK) { + port = optargs->port; + if (port <= 0 || port > 65535) { + error (g, _("invalid port number")); + return -1; + } + } else + port = 0; if (format && !valid_format_iface (format)) { error (g, _("%s parameter is empty or contains disallowed characters"), @@ -348,28 +478,50 @@ guestfs__add_drive_opts (guestfs_h *g, const char *filename, error (g, _("label parameter is empty, too long, or contains disallowed characters")); return -1; } + if (server && !valid_server (server)) { + error (g, _("server parameter is empty, too long, or contains disallowed characters")); + return -1; + } - if (STREQ (filename, "/dev/null")) - drv = create_null_drive_struct (g, readonly, format, iface, name, - disk_label); - else { - /* For writable files, see if we can use cache=none. This also - * checks for the existence of the file. For readonly we have - * to do the check explicitly. - */ - use_cache_none = readonly ? false : test_cache_none (g, filename); - if (use_cache_none == -1) - return -1; - - if (readonly) { - if (access (filename, R_OK) == -1) { - perrorf (g, "%s", filename); + if (STREQ (protocol, "file")) { + if (STREQ (filename, "/dev/null")) + drv = create_drive_dev_null (g, readonly, format, iface, name, + disk_label); + else { + /* For writable files, see if we can use cache=none. This also + * checks for the existence of the file. For readonly we have + * to do the check explicitly. + */ + use_cache_none = readonly ? false : test_cache_none (g, filename); + if (use_cache_none == -1) return -1; + + if (readonly) { + if (access (filename, R_OK) == -1) { + perrorf (g, "%s", filename); + return -1; + } } - } - drv = create_drive_struct (g, filename, readonly, format, iface, name, + drv = create_drive_file (g, filename, readonly, format, iface, name, disk_label, use_cache_none); + } + } + else if (STREQ (protocol, "nbd")) { + if (!server) { + error (g, _("protocol nbd: missing server name")); + return -1; + } + if (port == 0) + port = nbd_port (); + + drv = create_drive_nbd (g, server, port, filename, + readonly, format, iface, name, + disk_label, false); + } + else { + error (g, _("unknown protocol '%s'"), protocol); + return -1; } if (drv == NULL) @@ -530,17 +682,7 @@ guestfs__debug_drives (guestfs_h *g) count = 0; ITER_DRIVES (g, i, drv) { - ret[count++] - safe_asprintf (g, "path=%s%s%s%s%s%s%s%s%s", - drv->path, - drv->readonly ? " readonly" : "", - drv->format ? " format=" : "", - drv->format ? : "", - drv->iface ? " iface=" : "", - drv->iface ? : "", - drv->name ? " name=" : "", - drv->name ? : "", - drv->use_cache_none ? " cache=none" : ""); + ret[count++] = drive_to_string (g, drv); } ret[count] = NULL; diff --git a/src/guestfs-internal.h b/src/guestfs-internal.h index f5db4cf..71f18a3 100644 --- a/src/guestfs-internal.h +++ b/src/guestfs-internal.h @@ -113,8 +113,22 @@ struct event { }; /* Drives added to the handle. */ +enum drive_protocol { + drive_protocol_file, + drive_protocol_nbd, +}; +union drive_source { + char *path; /* protocol = "file" */ + struct { /* protocol = "nbd" */ + char *server; + int port; + char *exportname; + } nbd; +}; + struct drive { - char *path; + enum drive_protocol protocol; + union drive_source u; bool readonly; char *format; @@ -123,7 +137,8 @@ struct drive { char *disk_label; bool use_cache_none; - void *priv; /* Data used by attach method. */ + /* Data used by the attach method. */ + void *priv; void (*free_priv) (void *); }; diff --git a/src/guestfs.pod b/src/guestfs.pod index af7f276..6736f07 100644 --- a/src/guestfs.pod +++ b/src/guestfs.pod @@ -637,6 +637,41 @@ 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 NETWORK BLOCK DEVICES + +Libguestfs can access Network Block Device (NBD) disks remotely. + +To do this, set the optional C<protocol> and C<server> parameters of +L</guestfs_add_drive_opts> like this: + + guestfs_add_drive_opts (g, "" /* export name - see below */, + GUESTFS_ADD_DRIVE_OPTS_FORMAT, "raw", + GUESTFS_ADD_DRIVE_OPTS_PROTOCOL, "nbd", + GUESTFS_ADD_DRIVE_OPTS_SERVER, "localhost", + -1); + +You can also specify the optional C<port> number if the NBD server is +running on a non-default port. + +Notes: + +=over 4 + +=item * + +The C<filename> parameter is the NBD export name. Use an empty string +to mean the default export. + +Not all NBD servers support this feature, and the libvirt +attach-method also does not support it. + +=item * + +The libvirt backend requires that you set the C<format> parameter of +L</guestfs_add_drive_opts> accurately when you use writable NBD disks. + +=back + =head2 INSPECTION Libguestfs has APIs for inspecting an unknown disk image to find out diff --git a/src/launch-appliance.c b/src/launch-appliance.c index 122f143..4b70cc1 100644 --- a/src/launch-appliance.c +++ b/src/launch-appliance.c @@ -931,36 +931,46 @@ qemu_supports_virtio_scsi (guestfs_h *g) return g->app.virtio_scsi == 1; } +/* Convert a struct drive into a qemu -drive parameter. Note that if + * using virtio-scsi, then the code above adds a second -device + * parameter to connect this drive to the SCSI HBA, as is required by + * virtio-scsi. + */ static char * qemu_drive_param (guestfs_h *g, const struct drive *drv, size_t index) { - size_t i; - size_t len = 128; - const char *p; - char *r; + CLEANUP_FREE char *file = NULL, *escaped_file = NULL; + size_t i, len; const char *iface; + char *p, *ret; + + /* Make the file= parameter. */ + switch (drv->protocol) { + case drive_protocol_file: + file = safe_strdup (g, drv->u.path); + break; + case drive_protocol_nbd: + if (STREQ (drv->u.nbd.exportname, "")) + file = safe_asprintf (g, "nbd:%s:%d", drv->u.nbd.server, drv->u.nbd.port); + else + file = safe_asprintf (g, "nbd:%s:%d:exportname=%s", + drv->u.nbd.server, drv->u.nbd.port, + drv->u.nbd.exportname); + break; + default: + abort (); + } - len += strlen (drv->path) * 2; /* every "," could become ",," */ - if (drv->iface) - len += strlen (drv->iface); - if (drv->format) - len += strlen (drv->format); - if (drv->disk_label) - len += strlen (drv->disk_label); - - r = safe_malloc (g, len); - - strcpy (r, "file="); - i = 5; - - /* Copy the path in, escaping any "," as ",,". */ - for (p = drv->path; *p; p++) { - if (*p == ',') { - r[i++] = ','; - r[i++] = ','; - } else - r[i++] = *p; + /* Escape the file= parameter. Every ',' becomes ',,'. */ + len = strlen (file); + len = len*2 + 1; /* Maximum length of escaped filename + \0 */ + p = escaped_file = safe_malloc (g, len); + for (i = 0; i < len; ++i) { + *p++ = file[i]; + if (file[i] == ',') + *p++ = ','; } + *p = '\0'; if (drv->iface) iface = drv->iface; @@ -969,17 +979,18 @@ qemu_drive_param (guestfs_h *g, const struct drive *drv, size_t index) else iface = "virtio"; - snprintf (&r[i], len-i, "%s%s%s%s%s%s,id=hd%zu,if=%s", - drv->readonly ? ",snapshot=on" : "", - drv->use_cache_none ? ",cache=none" : "", - drv->format ? ",format=" : "", - drv->format ? drv->format : "", - drv->disk_label ? ",serial=" : "", - drv->disk_label ? drv->disk_label : "", - index, - iface); - - return r; /* caller frees */ + ret = safe_asprintf (g, "file=%s%s%s%s%s%s%s,id=hd%zu,if=%s", + escaped_file, + drv->readonly ? ",snapshot=on" : "", + drv->use_cache_none ? ",cache=none" : "", + drv->format ? ",format=" : "", + drv->format ? drv->format : "", + drv->disk_label ? ",serial=" : "", + drv->disk_label ? drv->disk_label : "", + index, + iface); + + return ret; } /* https://rwmj.wordpress.com/2011/01/09/how-are-linux-drives-named-beyond-drive-26-devsdz/ */ diff --git a/src/launch-libvirt.c b/src/launch-libvirt.c index 58ccb9e..bef3910 100644 --- a/src/launch-libvirt.c +++ b/src/launch-libvirt.c @@ -101,12 +101,13 @@ xmlBufferDetach (xmlBufferPtr buf) /* Pointed to by 'struct drive *' -> priv field. */ struct drive_libvirt { - /* This is either the original path, made absolute. Or for readonly - * drives, it is an (absolute path to) the overlay file that we - * create. This is always non-NULL. + /* The drive that we actually add. If using an overlay, then this + * might be different from drive->protocol. */ - char *path; - /* The format of priv->path. */ + enum drive_protocol protocol; + union drive_source u; + + /* The format of the drive we add. */ char *format; }; @@ -135,8 +136,8 @@ static int is_custom_qemu (guestfs_h *g); static int is_blk (const char *path); static int random_chars (char *ret, size_t len); static void ignore_errors (void *ignore, virErrorPtr ignore2); -static char *make_qcow2_overlay (guestfs_h *g, const char *path, const char *format, const char *selinux_imagelabel); -static int make_qcow2_overlay_for_drive (guestfs_h *g, struct drive *drv, const char *selinux_imagelabel); +static char *make_qcow2_overlay (guestfs_h *g, const char *backing_device, const char *format, const char *selinux_imagelabel); +static int make_drive_priv (guestfs_h *g, struct drive *drv, const char *selinux_imagelabel); static void drive_free_priv (void *); static void set_socket_create_context (guestfs_h *g); static void clear_socket_create_context (guestfs_h *g); @@ -235,18 +236,18 @@ launch_libvirt (guestfs_h *g, const char *libvirt_uri) guestfs___launch_send_progress (g, 3); TRACE0 (launch_build_libvirt_appliance_end); - /* Create overlays for read-only drives and the appliance. This - * works around lack of support for <transient/> disks in libvirt. - * Note that appliance can be NULL if using the old-style appliance. - */ + /* Note that appliance can be NULL if using the old-style appliance. */ if (appliance) { params.appliance_overlay = make_qcow2_overlay (g, appliance, "raw", NULL); if (!params.appliance_overlay) goto cleanup; } + /* Set up the drv->priv part of the struct. A side-effect of this + * may be that we create qcow2 overlays for drives. + */ ITER_DRIVES (g, i, drv) { - if (make_qcow2_overlay_for_drive (g, drv, g->virt_selinux_imagelabel) == -1) + if (make_drive_priv (g, drv, g->virt_selinux_imagelabel) == -1) goto cleanup; } @@ -1044,7 +1045,8 @@ construct_libvirt_xml_disk (guestfs_h *g, { char drive_name[64] = "sd"; char scsi_target[64]; - struct drive_libvirt *drv_priv; + char port_str[64]; + struct drive_libvirt *drv_priv = (struct drive_libvirt *) drv->priv; CLEANUP_FREE char *format = NULL; int is_host_device; @@ -1057,47 +1059,86 @@ construct_libvirt_xml_disk (guestfs_h *g, guestfs___drive_name (drv_index, &drive_name[2]); snprintf (scsi_target, sizeof scsi_target, "%zu", drv_index); - drv_priv = (struct drive_libvirt *) drv->priv; - - /* Change the libvirt XML according to whether the host path is - * a device or a file. For devices, use: - * <disk type=block device=disk> - * <source dev=[path]> - * For files, use: - * <disk type=file device=disk> - * <source file=[path]> - */ - is_host_device = is_blk (drv_priv->path); - XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "disk")); XMLERROR (-1, xmlTextWriterWriteAttribute (xo, BAD_CAST "device", BAD_CAST "disk")); - if (!is_host_device) { - XMLERROR (-1, - xmlTextWriterWriteAttribute (xo, BAD_CAST "type", - BAD_CAST "file")); - XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "source")); - XMLERROR (-1, - xmlTextWriterWriteAttribute (xo, BAD_CAST "file", - BAD_CAST drv_priv->path)); - if (construct_libvirt_xml_disk_source_seclabel (g, xo) == -1) + switch (drv_priv->protocol) { + case drive_protocol_file: + /* Change the libvirt XML according to whether the host path is + * a device or a file. For devices, use: + * <disk type=block device=disk> + * <source dev=[path]> + * For files, use: + * <disk type=file device=disk> + * <source file=[path]> + */ + is_host_device = is_blk (drv_priv->u.path); + + if (!is_host_device) { + XMLERROR (-1, + xmlTextWriterWriteAttribute (xo, BAD_CAST "type", + BAD_CAST "file")); + + XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "source")); + XMLERROR (-1, + xmlTextWriterWriteAttribute (xo, BAD_CAST "file", + BAD_CAST drv_priv->u.path)); + if (construct_libvirt_xml_disk_source_seclabel (g, xo) == -1) + return -1; + XMLERROR (-1, xmlTextWriterEndElement (xo)); + } + else { + XMLERROR (-1, + xmlTextWriterWriteAttribute (xo, BAD_CAST "type", + BAD_CAST "block")); + + XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "source")); + XMLERROR (-1, + xmlTextWriterWriteAttribute (xo, BAD_CAST "dev", + BAD_CAST drv_priv->u.path)); + if (construct_libvirt_xml_disk_source_seclabel (g, xo) == -1) + return -1; + XMLERROR (-1, xmlTextWriterEndElement (xo)); + } + break; + + case drive_protocol_nbd: + /* For NBD: + * <disk type=network device=disk> + * <source protocol=nbd> + * <host name='example.com' port='10809'/> + */ + if (STRNEQ (drv_priv->u.nbd.exportname, "")) { + error (g, _("libvirt does not support nbd 'exportname' feature")); return -1; - XMLERROR (-1, xmlTextWriterEndElement (xo)); - } - else { + } + XMLERROR (-1, xmlTextWriterWriteAttribute (xo, BAD_CAST "type", - BAD_CAST "block")); + BAD_CAST "network")); XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "source")); XMLERROR (-1, - xmlTextWriterWriteAttribute (xo, BAD_CAST "dev", - BAD_CAST drv_priv->path)); + xmlTextWriterWriteAttribute (xo, BAD_CAST "protocol", + BAD_CAST "nbd")); + XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "host")); + XMLERROR (-1, + xmlTextWriterWriteAttribute (xo, BAD_CAST "name", + BAD_CAST drv_priv->u.nbd.server)); + snprintf (port_str, sizeof port_str, "%d", drv_priv->u.nbd.port); + XMLERROR (-1, + xmlTextWriterWriteAttribute (xo, BAD_CAST "port", + BAD_CAST port_str)); + XMLERROR (-1, xmlTextWriterEndElement (xo)); if (construct_libvirt_xml_disk_source_seclabel (g, xo) == -1) return -1; XMLERROR (-1, xmlTextWriterEndElement (xo)); + break; + + default: + abort (); } XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "target")); @@ -1118,7 +1159,7 @@ construct_libvirt_xml_disk (guestfs_h *g, xmlTextWriterWriteAttribute (xo, BAD_CAST "type", BAD_CAST drv_priv->format)); } - else { + else if (drv_priv->protocol == drive_protocol_file) { /* libvirt has disabled the feature of detecting the disk format, * unless the administrator sets allow_disk_format_probing=1 in * qemu.conf. There is no way to detect if this option is set, so we @@ -1129,15 +1170,14 @@ construct_libvirt_xml_disk (guestfs_h *g, * the users pass the format to libguestfs which will faithfully pass * that to libvirt and this function won't be used. */ - format = guestfs_disk_format (g, drv_priv->path); + format = guestfs_disk_format (g, drv_priv->u.path); if (!format) return -1; if (STREQ (format, "unknown")) { - error (g, _("could not auto-detect the format of '%s'\n" + error (g, _("could not auto-detect the format.\n" "If the format is known, pass the format to libguestfs, eg. using the\n" - "'--format' option, or via the optional 'format' argument to 'add-drive'."), - drv->path); + "'--format' option, or via the optional 'format' argument to 'add-drive'.")); return -1; } @@ -1145,6 +1185,13 @@ construct_libvirt_xml_disk (guestfs_h *g, xmlTextWriterWriteAttribute (xo, BAD_CAST "type", BAD_CAST format)); } + else { + error (g, _("could not auto-detect the format when using a non-file protocol.\n" + "If the format is known, pass the format to libguestfs, eg. using the\n" + "'--format' option, or via the optional 'format' argument to 'add-drive'.")); + return -1; + } + if (drv->use_cache_none) { XMLERROR (-1, xmlTextWriterWriteAttribute (xo, BAD_CAST "cache", @@ -1265,7 +1312,7 @@ construct_libvirt_xml_appliance (guestfs_h *g, XMLERROR (-1, xmlTextWriterEndElement (xo)); /* We'd like to do this, but it's not supported by libvirt. - * See construct_libvirt_xml_qemu_cmdline for the workaround. + * See make_drive_priv for the workaround. * * XMLERROR (-1, xmlTextWriterStartElement (xo, BAD_CAST "transient")); * XMLERROR (-1, xmlTextWriterEndElement (xo)); @@ -1397,18 +1444,22 @@ ignore_errors (void *ignore, virErrorPtr ignore2) /* empty */ } -/* Create a temporary qcow2 overlay on top of 'path'. */ +/* Create a temporary qcow2 overlay on top of 'backing_device', which is + * either an absolute path or a qemu device name. + */ static char * -make_qcow2_overlay (guestfs_h *g, const char *path, const char *format, - const char *selinux_imagelabel) +make_qcow2_overlay (guestfs_h *g, const char *backing_device, + const char *format, const char *selinux_imagelabel) { char *tmpfile = NULL; CLEANUP_CMD_CLOSE struct command *cmd = guestfs___new_command (g); int r; - /* Path must be absolute. */ - assert (path); - assert (path[0] == '/'); + /* Assert that backing_device is an absolute path, or it's an + * nbd device name. + */ + assert (backing_device); + assert (backing_device[0] == '/' || STRPREFIX (backing_device, "nbd:")); tmpfile = safe_asprintf (g, "%s/snapshot%d", g->tmpdir, ++g->unique); @@ -1417,7 +1468,7 @@ make_qcow2_overlay (guestfs_h *g, const char *path, const char *format, guestfs___cmd_add_arg (cmd, "-f"); guestfs___cmd_add_arg (cmd, "qcow2"); guestfs___cmd_add_arg (cmd, "-b"); - guestfs___cmd_add_arg (cmd, path); + guestfs___cmd_add_arg (cmd, backing_device); if (format) { guestfs___cmd_add_arg (cmd, "-o"); guestfs___cmd_add_arg_format (cmd, "backing_fmt=%s", format); @@ -1427,7 +1478,7 @@ make_qcow2_overlay (guestfs_h *g, const char *path, const char *format, if (r == -1) goto error; if (!WIFEXITED (r) || WEXITSTATUS (r) != 0) { - guestfs___external_command_failed (g, r, "qemu-img create", path); + guestfs___external_command_failed (g, r, "qemu-img create", backing_device); goto error; } @@ -1448,9 +1499,14 @@ make_qcow2_overlay (guestfs_h *g, const char *path, const char *format, return NULL; } +/* This sets up the drv->priv structure, which contains the drive + * that we're actually going to add. If asked to make a drive readonly + * then because libvirt doesn't support <transient/> we have to add + * a qcow2 overlay here. + */ static int -make_qcow2_overlay_for_drive (guestfs_h *g, struct drive *drv, - const char *selinux_imagelabel) +make_drive_priv (guestfs_h *g, struct drive *drv, + const char *selinux_imagelabel) { char *path; struct drive_libvirt *drv_priv; @@ -1461,25 +1517,64 @@ make_qcow2_overlay_for_drive (guestfs_h *g, struct drive *drv, drv->priv = drv_priv = safe_calloc (g, 1, sizeof (struct drive_libvirt)); drv->free_priv = drive_free_priv; - /* Even for non-readonly paths, we need to make the paths absolute here. */ - path = realpath (drv->path, NULL); - if (path == NULL) { - perrorf (g, _("realpath: could not convert '%s' to absolute path"), - drv->path); - return -1; - } + switch (drv->protocol) { + case drive_protocol_file: - if (!drv->readonly) { - drv_priv->path = path; - drv_priv->format = drv->format ? safe_strdup (g, drv->format) : NULL; - } - else { - drv_priv->path = make_qcow2_overlay (g, path, drv->format, - selinux_imagelabel); - free (path); - if (!drv_priv->path) + /* Even for non-readonly paths, we need to make the paths absolute here. */ + path = realpath (drv->u.path, NULL); + if (path == NULL) { + perrorf (g, _("realpath: could not convert '%s' to absolute path"), + drv->u.path); return -1; - drv_priv->format = safe_strdup (g, "qcow2"); + } + + drv_priv->protocol = drive_protocol_file; + + if (!drv->readonly) { + drv_priv->u.path = path; + drv_priv->format = drv->format ? safe_strdup (g, drv->format) : NULL; + } + else { + drv_priv->u.path = make_qcow2_overlay (g, path, drv->format, + selinux_imagelabel); + free (path); + if (!drv_priv->u.path) + return -1; + drv_priv->format = safe_strdup (g, "qcow2"); + } + break; + + case drive_protocol_nbd: + if (!drv->readonly) { + drv_priv->protocol = drive_protocol_nbd; + drv_priv->u.nbd.server = safe_strdup (g, drv->u.nbd.server); + drv_priv->u.nbd.port = drv->u.nbd.port; + drv_priv->u.nbd.exportname = safe_strdup (g, drv->u.nbd.exportname); + drv_priv->format = drv->format ? safe_strdup (g, drv->format) : NULL; + } + else { + CLEANUP_FREE char *nbd_device; + + if (STREQ (drv->u.nbd.exportname, "")) + nbd_device + safe_asprintf (g, "nbd:%s:%d", drv->u.nbd.server, drv->u.nbd.port); + else + nbd_device + safe_asprintf (g, "nbd:%s:%d:exportname=%s", + drv->u.nbd.server, drv->u.nbd.port, + drv->u.nbd.exportname); + + drv_priv->protocol = drive_protocol_file; + drv_priv->u.path = make_qcow2_overlay (g, nbd_device, drv->format, + selinux_imagelabel); + if (!drv_priv->u.path) + return -1; + drv_priv->format = safe_strdup (g, "qcow2"); + } + break; + + default: + abort (); } return 0; @@ -1490,7 +1585,18 @@ drive_free_priv (void *priv) { struct drive_libvirt *drv_priv = priv; - free (drv_priv->path); + switch (drv_priv->protocol) { + case drive_protocol_file: + free (drv_priv->u.path); + break; + case drive_protocol_nbd: + free (drv_priv->u.nbd.server); + free (drv_priv->u.nbd.exportname); + break; + default: + abort (); + } + free (drv_priv->format); free (drv_priv); } @@ -1589,10 +1695,7 @@ hot_add_drive_libvirt (guestfs_h *g, struct drive *drv, size_t drv_index) 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, g->virt_selinux_imagelabel) == -1) + if (make_drive_priv (g, drv, g->virt_selinux_imagelabel) == -1) return -1; /* Create the XML for the new disk. */ diff --git a/tests/nbd/Makefile.am b/tests/nbd/Makefile.am new file mode 100644 index 0000000..09d79c8 --- /dev/null +++ b/tests/nbd/Makefile.am @@ -0,0 +1,26 @@ +# libguestfs +# Copyright (C) 2013 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-nbd.pl + +TESTS_ENVIRONMENT = $(top_builddir)/run --test + +EXTRA_DIST = \ + $(TESTS) diff --git a/tests/nbd/test-nbd.pl b/tests/nbd/test-nbd.pl new file mode 100755 index 0000000..4c14caf --- /dev/null +++ b/tests/nbd/test-nbd.pl @@ -0,0 +1,78 @@ +#!/usr/bin/perl +# Copyright (C) 2013 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. + +use strict; +use warnings; + +use Sys::Guestfs; + +my $disk = "../guests/fedora.img"; + +exit 77 if $ENV{SKIP_TEST_NBD_PL}; + +# Check we have qemu-nbd. +if (system ("qemu-nbd --help >/dev/null 2>&1") != 0) { + print "$0: test skipped because qemu-nbd program not found\n"; + exit 77 +} + +if (! -r $disk || -z $disk) { + print "$0: test skipped because $disk is not found\n"; + exit 77 +} + +# Since read-only and read-write paths are quite different, we have to +# test both separately. +my $readonly; +for $readonly (1, 0) { + # Choose a random port number. XXX Should check it is not in use. + my $port = int (60000 + rand (5000)); + + # Run the NBD server. + print "Starting qemu-nbd server on port $port ...\n"; + my $pid = fork (); + if ($pid == 0) { + exec ("qemu-nbd", $disk, "-p", $port, "-t"); + die "qemu-nbd: $!"; + } + + my $g = Sys::Guestfs->new (); + + # Add an NBD drive. + $g->add_drive ("", readonly => $readonly, format => "raw", + protocol => "nbd", server => "localhost", port => $port); + + # XXX qemu-nbd lacks any way to tell if it is awake and listening + # for connections. It could write a pid file or something. Could + # we check that the socket has been opened by looking in netstat? + sleep (2); + + # This dies if qemu cannot connect to the NBD server. + $g->launch (); + + # Inspection is quite a thorough test: + my $root = $g->inspect_os (); + die "$root != /dev/VG/Root" unless $root eq "/dev/VG/Root"; + + # Note we have to close the handle (hence killing qemu), and we + # have to kill qemu-nbd. + $g->close (); + kill 15, $pid; + waitpid ($pid, 0) or die "waitpid: $pid: $!"; +} + +exit 0 -- 1.8.1.4
Apparently Analagous Threads
- [PATCH INCOMPLETE] launch: libvirt: Use C macros to simplify XML generation.
- [PATCH v2 2/4] common/utils: Move libxml2 writer macros to a common header file.
- [PATCH v2 3/4] inspector: Use libxml writer macros.
- [PATCH 2/2] Fix whitespace.
- [PATCH v3 0/3] Add support for disk labels and hotplugging.