From fd82bb12fd5fc8233955b8ffd4a42c3d6f8d748b Mon Sep 17 00:00:00 2001 From: "Richard W.M. Jones" Date: Wed, 23 Apr 2014 21:27:52 +0100 Subject: [PATCH] New tool: virt-p2v. This is a graphical standalone front-end to virt-v2v which can be run on physical machines (usually linked into a ISO or PXE boot image) to convert the physical machine to a virtual machine. --- .gitignore | 4 + Makefile.am | 3 + README | 2 + configure.ac | 16 + fish/guestfish.pod | 1 + p2v/Makefile.am | 91 ++++ p2v/authors.c | 30 ++ p2v/config.c | 89 ++++ p2v/conversion.c | 475 ++++++++++++++++++++ p2v/copying.c | 38 ++ p2v/gui.c | 1052 ++++++++++++++++++++++++++++++++++++++++++++ p2v/kernel.c | 184 ++++++++ p2v/main.c | 502 +++++++++++++++++++++ p2v/miniexpect.c | 379 ++++++++++++++++ p2v/miniexpect.h | 82 ++++ p2v/p2v.h | 103 +++++ p2v/ssh.c | 633 ++++++++++++++++++++++++++ p2v/virt-p2v.pod | 219 +++++++++ po/POTFILES | 9 + src/guestfs.pod | 5 + src/utils.c | 1 + v2v/virt-v2v.pod | 5 +- 22 files changed, 3921 insertions(+), 2 deletions(-) create mode 100644 p2v/Makefile.am create mode 100644 p2v/authors.c create mode 100644 p2v/config.c create mode 100644 p2v/conversion.c create mode 100644 p2v/copying.c create mode 100644 p2v/gui.c create mode 100644 p2v/kernel.c create mode 100644 p2v/main.c create mode 100644 p2v/miniexpect.c create mode 100644 p2v/miniexpect.h create mode 100644 p2v/p2v.h create mode 100644 p2v/ssh.c create mode 100644 p2v/virt-p2v.pod diff --git a/.gitignore b/.gitignore index 25e935867..f97318be2 100644 --- a/.gitignore +++ b/.gitignore @@ -245,6 +245,7 @@ Makefile.in /html/virt-list-partitions.1.html /html/virt-ls.1.html /html/virt-make-fs.1.html +/html/virt-p2v.1.html /html/virt-rescue.1.html /html/virt-resize.1.html /html/virt-sparsify.1.html @@ -314,6 +315,9 @@ Makefile.in /ocaml/stamp-mlguestfs /ocaml/t/*.bc /ocaml/t/*.opt +/p2v/stamp-virt-p2v.pod +/p2v/virt-p2v +/p2v/virt-p2v.1 /perl/bindtests.pl /perl/blib /perl/examples/guestfs-perl.3 diff --git a/Makefile.am b/Makefile.am index 3102e0bb6..67b1fa8e7 100644 --- a/Makefile.am +++ b/Makefile.am @@ -81,6 +81,9 @@ SUBDIRS += fish # virt-tools in C. SUBDIRS += align cat diff df edit format inspector make-fs rescue +if HAVE_P2V +SUBDIRS += p2v +endif # bash-completion SUBDIRS += bash diff --git a/README b/README index 8fdc04146..2d8acfd09 100644 --- a/README +++ b/README @@ -176,6 +176,8 @@ The full requirements are described below. | liblzma | | O | Can be used by virt-builder for fast | | | | | uncompression of templates. | +--------------+-------------+---+-----------------------------------------+ +| gtk2 | | O | Used by virt-p2v user interface. | ++--------------+-------------+---+-----------------------------------------+ | findlib | | O | For the OCaml bindings. | +--------------+-------------+---+-----------------------------------------+ | ocaml-gettext| | O | For localizing OCaml virt-* tools. | diff --git a/configure.ac b/configure.ac index 63a3d092f..2239e448c 100644 --- a/configure.ac +++ b/configure.ac @@ -118,6 +118,9 @@ gl_INIT AC_PROG_LIBTOOL AC_PROG_LN_S +dnl Define the host CPU architecture (defines 'host_cpu') +AC_CANONICAL_HOST + # Define $(SED). m4_ifdef([AC_PROG_SED],[ AC_PROG_SED @@ -933,6 +936,16 @@ PKG_CHECK_MODULES([LIBCONFIG], [libconfig],[ [AC_MSG_WARN([libconfig not found, some features will be disabled])]) AM_CONDITIONAL([HAVE_LIBCONFIG],[test "x$LIBCONFIG_LIBS" != "x"]) +dnl Check for gtk2 library, used by virt-p2v. +PKG_CHECK_MODULES([GTK2], [gtk+-2.0], [ + AC_SUBST([GTK2_CFLAGS]) + AC_SUBST([GTK2_LIBS]) +], + [AC_MSG_WARN([gtk2 not found, virt-p2v will be disabled])]) + +dnl Can we build virt-p2v? +AM_CONDITIONAL([HAVE_P2V], [test "x$GTK2_LIBS" != "x"]) + dnl hivex library (highly recommended) dnl This used to be a part of libguestfs, but was spun off into its dnl own separate upstream project in libguestfs 1.0.85. @@ -1645,6 +1658,7 @@ AC_CONFIG_FILES([Makefile ocaml/META ocaml/Makefile ocaml/examples/Makefile + p2v/Makefile perl/Makefile perl/Makefile.PL perl/examples/Makefile @@ -1723,6 +1737,8 @@ echo "guestfish and C-based virt tools .... yes" echo "FUSE filesystem ..................... $enable_fuse" AS_ECHO_N(["GNU gettext for i18n ................ "]) if test "x$HAVE_GNU_GETTEXT_TRUE" = "x"; then echo "yes"; else echo "no"; fi +AS_ECHO_N(["virt-p2v ............................ "]) +if test "x$HAVE_P2V_TRUE" = "x"; then echo "yes"; else echo "no"; fi AS_ECHO_N(["OCaml bindings ...................... "]) if test "x$HAVE_OCAML_TRUE" = "x"; then echo "yes"; else echo "no"; fi AS_ECHO_N(["OCaml-based virt tools .............. "]) diff --git a/fish/guestfish.pod b/fish/guestfish.pod index 5cf6ebc34..cf52f866a 100644 --- a/fish/guestfish.pod +++ b/fish/guestfish.pod @@ -1617,6 +1617,7 @@ L, L, L, L, +L, L, L, L, diff --git a/p2v/Makefile.am b/p2v/Makefile.am new file mode 100644 index 000000000..e57b571be --- /dev/null +++ b/p2v/Makefile.am @@ -0,0 +1,91 @@ +# libguestfs virt-p2v +# Copyright (C) 2009-2014 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 + +EXTRA_DIST = \ + virt-p2v.pod + +CLEANFILES = stamp-virt-p2v.pod + +# Although virt-p2v is a regular binary, it is not usually installed +# since it only functions when contained in an ISO or PXE image which +# is used to boot the physical machine (since otherwise virt-p2v would +# not be able to get a consistent snapshot of the physical disks). +noinst_PROGRAMS = virt-p2v + +# Note that miniexpect comes from here: +# http://git.annexia.org/?p=miniexpect.git;a=summary +virt_p2v_SOURCES = \ + authors.c \ + config.c \ + conversion.c \ + copying.c \ + gui.c \ + kernel.c \ + main.c \ + miniexpect.c \ + miniexpect.h \ + p2v.h \ + ssh.c + +virt_p2v_CPPFLAGS = \ + -DLOCALEBASEDIR=\""$(datadir)/locale"\" \ + -I$(top_srcdir)/src -I$(top_builddir)/src \ + -I$(srcdir)/../gnulib/lib -I../gnulib/lib + +virt_p2v_CFLAGS = \ + -pthread \ + $(WARN_CFLAGS) $(WERROR_CFLAGS) \ + $(PCRE_CFLAGS) \ + $(LIBXML2_CFLAGS) \ + $(GTK2_CFLAGS) + +virt_p2v_LDADD = \ + $(PCRE_LIBS) \ + $(LIBXML2_LIBS) \ + $(GTK2_LIBS) \ + $(top_builddir)/src/libutils.la \ + ../gnulib/lib/libgnu.la + +# Manual pages and HTML files for the website. +man_MANS = virt-p2v.1 + +noinst_DATA = \ + $(top_builddir)/html/virt-p2v.1.html + +virt-p2v.1 $(top_builddir)/html/virt-p2v.1.html: stamp-virt-p2v.pod + +stamp-virt-p2v.pod: virt-p2v.pod + $(PODWRAPPER) \ + --man virt-p2v.1 \ + --html $(top_builddir)/html/virt-p2v.1.html \ + --license GPLv2+ \ + $< + touch $@ + +# Tests. + +TESTS_ENVIRONMENT = $(top_builddir)/run --test + +#if ENABLE_APPLIANCE +#TESTS = \ +# test-virt-p2v.sh +#endif ENABLE_APPLIANCE +# +#check-valgrind: +# $(MAKE) VG="$(top_builddir)/run @VG@" check diff --git a/p2v/authors.c b/p2v/authors.c new file mode 100644 index 000000000..40f2777ad --- /dev/null +++ b/p2v/authors.c @@ -0,0 +1,30 @@ +/* virt-p2v + * Copyright (C) 2009-2014 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 + +#include "p2v.h" + +/* The list of authors of virt-p2v and virt-v2v, for the About dialog. */ + +const char *authors[] = { + "Matthew Booth", + "Richard W.M. Jones", + "Mike Latimer", + NULL +}; diff --git a/p2v/config.c b/p2v/config.c new file mode 100644 index 000000000..dafb68748 --- /dev/null +++ b/p2v/config.c @@ -0,0 +1,89 @@ +/* virt-p2v + * Copyright (C) 2009-2014 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "p2v.h" + +struct config * +new_config (void) +{ + struct config *c; + + c = calloc (1, sizeof *c); + if (c == NULL) { + perror ("calloc"); + exit (EXIT_FAILURE); + } + +#if FORCE_REMOTE_DEBUG + c->verbose = 1; +#endif + c->port = 22; + + return c; +} + +struct config * +copy_config (struct config *old) +{ + struct config *c = new_config (); + + memcpy (c, old, sizeof *c); + + /* Need to deep copy strings and string lists. */ + if (c->server) + c->server = strdup (c->server); + if (c->username) + c->username = strdup (c->username); + if (c->password) + c->password = strdup (c->password); + if (c->guestname) + c->guestname = strdup (c->guestname); + if (c->disks) + c->disks = guestfs___copy_string_list (c->disks); + if (c->removable) + c->removable = guestfs___copy_string_list (c->removable); + if (c->interfaces) + c->interfaces = guestfs___copy_string_list (c->interfaces); + + return c; +} + +void +free_config (struct config *c) +{ + free (c->server); + free (c->username); + free (c->password); + free (c->guestname); + guestfs___free_string_list (c->disks); + guestfs___free_string_list (c->removable); + guestfs___free_string_list (c->interfaces); + free (c); +} diff --git a/p2v/conversion.c b/p2v/conversion.c new file mode 100644 index 000000000..d2b6c4481 --- /dev/null +++ b/p2v/conversion.c @@ -0,0 +1,475 @@ +/* virt-p2v + * Copyright (C) 2009-2014 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "miniexpect.h" +#include "p2v.h" + +/* Data per NBD connection / physical disk. */ +struct data_conn { + mexp_h *h; /* miniexpect handle to ssh */ + pid_t nbd_pid; /* qemu pid */ + int nbd_local_port; /* local NBD port on physical machine */ + int nbd_remote_port; /* remote NBD port on conversion server */ +}; + +static pid_t start_qemu_nbd (int nbd_local_port, const char *device); +static void cleanup_data_conns (struct data_conn *data_conns, size_t nr); +static char *generate_libvirt_xml (struct config *, struct data_conn *); +static void debug_parameters (struct config *); + +static char *conversion_error; + +static void set_conversion_error (const char *fs, ...) + __attribute__((format(printf,1,2))); + +static void +set_conversion_error (const char *fs, ...) +{ + va_list args; + char *msg; + int len; + + va_start (args, fs); + len = vasprintf (&msg, fs, args); + va_end (args); + + if (len < 0) { + perror ("vasprintf"); + fprintf (stderr, "original error format string: %s\n", fs); + exit (EXIT_FAILURE); + } + + free (conversion_error); + conversion_error = msg; +} + +const char * +get_conversion_error (void) +{ + return conversion_error; +} + +#pragma GCC diagnostic ignored "-Wsuggest-attribute=noreturn" +int +start_conversion (struct config *config, + void (*notify_ui) (int type, const char *data)) +{ + int ret = -1; + size_t i, len; + size_t nr_disks = guestfs___count_strings (config->disks); + struct data_conn data_conns[nr_disks]; + CLEANUP_FREE char *remote_dir = NULL, *libvirt_xml = NULL; + time_t now; + struct tm tm; + mexp_h *control_h = NULL; + + debug_parameters (config); + + for (i = 0; config->disks[i] != NULL; ++i) { + data_conns[i].h = NULL; + data_conns[i].nbd_pid = 0; + data_conns[i].nbd_local_port = -1; + data_conns[i].nbd_remote_port = -1; + } + + /* Start the data connections and qemu-nbd processes, one per disk. */ + for (i = 0; config->disks[i] != NULL; ++i) { + CLEANUP_FREE char *device = NULL; + + if (notify_ui) { + CLEANUP_FREE char *msg; + if (asprintf (&msg, + _("Opening data connection for %s ..."), + config->disks[i]) == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + notify_ui (NOTIFY_STATUS, msg); + } + + data_conns[i].h = open_data_connection (config, + &data_conns[i].nbd_local_port, + &data_conns[i].nbd_remote_port); + if (data_conns[i].h == NULL) { + const char *err = get_ssh_error (); + + set_conversion_error ("could not open data connection over SSH to the conversion server: %s", err); + goto out; + } + + if (asprintf (&device, "/dev/%s", config->disks[i]) == -1) { + perror ("asprintf"); + cleanup_data_conns (data_conns, nr_disks); + exit (EXIT_FAILURE); + } + + /* Start qemu-nbd listening on the given port number. */ + data_conns[i].nbd_pid = + start_qemu_nbd (data_conns[i].nbd_local_port, device); + if (data_conns[i].nbd_pid == 0) + goto out; + +#if DEBUG_STDERR + fprintf (stderr, + "%s: data connection for %s: SSH remote port %d, local port %d\n", + program_name, device, + data_conns[i].nbd_remote_port, data_conns[i].nbd_local_port); +#endif + } + + /* Create a remote directory name which will be used for libvirt + * XML, log files and other stuff. We don't delete this directory + * after the run because (a) it's useful for debugging and (b) it + * only contains small files. + * + * NB: This path MUST NOT require shell quoting. + */ + time (&now); + gmtime_r (&now, &tm); + if (asprintf (&remote_dir, + "/tmp/virt-p2v-%04d%02d%02d-XXXXXXXX", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday) == -1) { + perror ("asprintf"); + cleanup_data_conns (data_conns, nr_disks); + exit (EXIT_FAILURE); + } + len = strlen (remote_dir); + guestfs___random_string (&remote_dir[len-8], 8); + if (notify_ui) + notify_ui (NOTIFY_LOG_DIR, remote_dir); + + /* Generate the libvirt XML. */ + libvirt_xml = generate_libvirt_xml (config, data_conns); + if (libvirt_xml == NULL) + goto out; + +#if DEBUG_STDERR + fprintf (stderr, "%s: libvirt XML:\n%s", program_name, libvirt_xml); +#endif + + /* Open the control connection and start conversion */ + if (notify_ui) + notify_ui (NOTIFY_STATUS, _("Setting up the control connection ...")); + + control_h = start_remote_connection (config, remote_dir, libvirt_xml); + if (control_h == NULL) { + const char *err = get_ssh_error (); + + set_conversion_error ("could not open control connection over SSH to the conversion server: %s", err); + goto out; + } + + /* Do the conversion. This runs until virt-v2v exits. */ + if (notify_ui) + notify_ui (NOTIFY_STATUS, _("Doing conversion ...")); + + if (mexp_printf (control_h, + "( " + "%s" + "virt-v2v" + "%s" + " -i libvirtxml" + " -o local -os /tmp" /* XXX */ + " --root first" + " %s/libvirt.conf" + " &1" /* output */ + " ;" + " echo $? > %s/status" + " )" + " | tee %s/virt-v2v-conversion-log.txt" + " ;" + " exit" + "\n", + config->sudo ? "sudo " : "", + config->verbose ? " -v -x" : "", + remote_dir, + remote_dir, + remote_dir) == -1) { + set_conversion_error ("mexp_printf: virt-v2v command: %m"); + goto out; + } + + /* Read output from the virt-v2v process and echo it through the + * notify function, until virt-v2v closes the connection. + */ + for (;;) { + char buf[257]; + ssize_t r; + + r = read (control_h->fd, buf, sizeof buf - 1); + if (r == -1) { + set_conversion_error ("read: %m"); + goto out; + } + if (r == 0) + break; + buf[r] = '\0'; + if (notify_ui) + notify_ui (NOTIFY_REMOTE_MESSAGE, buf); + } + + ret = 0; + out: + if (control_h) + mexp_close (control_h); + cleanup_data_conns (data_conns, nr_disks); + return ret; +} + +/* Note: returns process ID (> 0) or 0 if there is an error. */ +static pid_t +start_qemu_nbd (int port, const char *device) +{ + pid_t pid; + char port_str[64]; + + snprintf (port_str, sizeof port_str, "%d", port); + + pid = fork (); + if (pid == -1) { + set_conversion_error ("fork: %m"); + return 0; + } + + if (pid == 0) { /* Child. */ + close (0); + open ("/dev/null", O_RDONLY); + + execlp ("qemu-nbd", + "qemu-nbd", + "-r", /* readonly (vital!) */ + "-p", port_str, /* listening port */ + "-t", /* persistent */ + "-f", "raw", /* force raw format */ + "-b", "localhost", /* listen only on loopback interface */ + "--cache=unsafe", /* use unsafe caching for speed */ + device, /* a device like /dev/sda */ + NULL); + perror ("qemu-nbd"); + _exit (EXIT_FAILURE); + } + + /* Parent. */ + return pid; +} + +static void +cleanup_data_conns (struct data_conn *data_conns, size_t nr) +{ + size_t i; + + for (i = 0; i < nr; ++i) { + if (data_conns[i].h != NULL) { + /* Because there is no SSH prompt (ssh -N), the only way to kill + * these ssh connections is to send a signal. Just closing the + * pipe doesn't do anything. + */ + kill (data_conns[i].h->pid, SIGTERM); + mexp_close (data_conns[i].h); + } + + if (data_conns[i].nbd_pid > 0) { + /* Kill qemu-nbd process and clean up. */ + kill (data_conns[i].nbd_pid, SIGTERM); + waitpid (data_conns[i].nbd_pid, NULL, 0); + } + } +} + +/* Write the libvirt XML for this physical machine. Note this is not + * actually input for libvirt. It's input for virt-v2v on the + * conversion server, and virt-v2v will (if necessary) generate the + * final libvirt XML. + */ +static char * +generate_libvirt_xml (struct config *config, struct data_conn *data_conns) +{ + uint64_t memkb; + FILE *fp; + char *ret = NULL; + size_t len = 0; + size_t i; + + fp = open_memstream (&ret, &len); + if (fp == NULL) { + set_conversion_error ("open_memstream: %m"); + return NULL; + } + + memkb = config->memory / 1024; + + fprintf (fp, + "\n" + "\n", + __FILE__); + + /* XXX quoting needs to be improved here XXX */ + fprintf (fp, + "\n" + " %s\n" + " %" PRIu64 "\n" + " %" PRIu64 "\n" + " %d\n" + " \n" + " hvm\n" + " \n" + " \n", + config->guestname, + memkb, memkb, + config->vcpus); + + /* XXX features: acpi, apic, pae */ + + for (i = 0; config->disks[i] != NULL; ++i) { + fprintf (fp, + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n", + data_conns[i].nbd_remote_port, config->disks[i]); + } + + if (config->removable) { + for (i = 0; config->removable[i] != NULL; ++i) { + fprintf (fp, + " \n" + " \n" + " \n" + " \n", + config->removable[i]); + } + } + + if (config->interfaces) { + for (i = 0; config->interfaces[i] != NULL; ++i) { + CLEANUP_FREE char *mac_filename = NULL; + CLEANUP_FREE char *mac = NULL; + + if (asprintf (&mac_filename, "/sys/class/net/%s/address", + config->interfaces[i]) == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + if (g_file_get_contents (mac_filename, &mac, NULL, NULL)) { + size_t len = strlen (mac); + + if (len > 0 && mac[len-1] == '\n') + mac[len-1] = '\0'; + } + + fprintf (fp, + " \n" + " \n" + " \n", + config->interfaces[i]); + if (mac) + fprintf (fp, " \n", mac); + fprintf (fp, + " \n"); + } + } + + fprintf (fp, + " \n" + "\n"); + fclose (fp); + + return ret; +} + +static void +debug_parameters (struct config *config) +{ +#if DEBUG_STDERR + size_t i; + + /* Print the conversion parameters and other important information. */ + fprintf (stderr, "local version . %s\n", PACKAGE_VERSION); + fprintf (stderr, "remote version . %d.%d.%d\n", + v2v_major, v2v_minor, v2v_release); + fprintf (stderr, "remote debugging %s\n", + config->verbose ? "true" : "false"); + fprintf (stderr, "conversion server %s\n", + config->server ? config->server : "none"); + fprintf (stderr, "port . . . . . . %d\n", config->port); + fprintf (stderr, "username . . . . %s\n", + config->username ? config->username : "none"); + fprintf (stderr, "password . . . . %s\n", + config->password && strlen (config->password) > 0 ? "***" : "none"); + fprintf (stderr, "sudo . . . . . . %s\n", + config->sudo ? "true" : "false"); + fprintf (stderr, "guest name . . . %s\n", + config->guestname ? config->guestname : "none"); + fprintf (stderr, "vcpus . . . . . %d\n", config->vcpus); + fprintf (stderr, "memory . . . . . %" PRIu64 "\n", config->memory); + fprintf (stderr, "disks . . . . . "); + if (config->disks != NULL) { + for (i = 0; config->disks[i] != NULL; ++i) + fprintf (stderr, " %s", config->disks[i]); + } + fprintf (stderr, "\n"); + fprintf (stderr, "removable . . . "); + if (config->removable != NULL) { + for (i = 0; config->removable[i] != NULL; ++i) + fprintf (stderr, " %s", config->removable[i]); + } + fprintf (stderr, "\n"); + fprintf (stderr, "interfaces . . . "); + if (config->interfaces != NULL) { + for (i = 0; config->interfaces[i] != NULL; ++i) + fprintf (stderr, " %s", config->interfaces[i]); + } + fprintf (stderr, "\n"); + fprintf (stderr, "\n"); +#endif +} diff --git a/p2v/copying.c b/p2v/copying.c new file mode 100644 index 000000000..90e8830f3 --- /dev/null +++ b/p2v/copying.c @@ -0,0 +1,38 @@ +/* virt-p2v + * Copyright (C) 2009-2014 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 + +#include "p2v.h" + +/* The license of virt-p2v, for the About dialog. */ + +const char *gplv2plus = + "This program is free software; you can redistribute it and/or modify\n" + "it under the terms of the GNU General Public License as published by\n" + "the Free Software Foundation; either version 2 of the License, or\n" + "(at your option) any later version.\n" + "\n" + "This program is distributed in the hope that it will be useful,\n" + "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" + "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" + "GNU General Public License for more details.\n" + "\n" + "You should have received a copy of the GNU General Public License\n" + "along with this program; if not, write to the Free Software\n" + "Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n"; diff --git a/p2v/gui.c b/p2v/gui.c new file mode 100644 index 000000000..96c020ebb --- /dev/null +++ b/p2v/gui.c @@ -0,0 +1,1052 @@ +/* virt-p2v + * Copyright (C) 2009-2014 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#pragma GCC diagnostic ignored "-Wstrict-prototypes" /* error in */ +#include + +#include "p2v.h" + +/* Interactive GUI configuration. */ + +static void create_connection_dialog (struct config *); +static void create_conversion_dialog (struct config *); +static void create_running_dialog (void); +static void show_connection_dialog (void); +static void show_conversion_dialog (void); +static void show_running_dialog (void); + +/* The connection dialog. */ +static GtkWidget *conn_dlg, + *server_entry, *port_entry, + *username_entry, *password_entry, *sudo_button, + *spinner_hbox, *spinner, *spinner_message, *next_button; + +/* The conversion dialog. */ +static GtkWidget *conv_dlg, + *guestname_entry, *vcpus_entry, *memory_entry, *debug_button, + *disks_list, *removable_list, *interfaces_list, + *start_button; + +/* The running dialog which is displayed when virt-v2v is running. */ +static GtkWidget *run_dlg, + *v2v_output_sw, *v2v_output, *log_label, *status_label, + *cancel_button; + +/* The entry point from the main program. + * Note that gtk_init etc have already been called in main(). + */ +void +gui_application (struct config *config) +{ + /* Create the dialogs. */ + create_connection_dialog (config); + create_conversion_dialog (config); + create_running_dialog (); + + /* Start by displaying the connection dialog. */ + show_connection_dialog (); + + gtk_main (); + gdk_threads_leave (); +} + +/*----------------------------------------------------------------------*/ +/* Connection dialog. */ + +static void test_connection_clicked (GtkWidget *w, gpointer data); +static void *test_connection_thread (void *data); +static void about_button_clicked (GtkWidget *w, gpointer data); +static void connection_next_clicked (GtkWidget *w, gpointer data); + +static void +create_connection_dialog (struct config *config) +{ + GtkWidget *intro, *table; + GtkWidget *server_label; + GtkWidget *port_label; + GtkWidget *username_label; + GtkWidget *password_label; + GtkWidget *test_hbox, *test; + GtkWidget *about; + char port_str[64]; + + conn_dlg = gtk_dialog_new (); + gtk_window_set_title (GTK_WINDOW (conn_dlg), program_name); + gtk_window_set_resizable (GTK_WINDOW (conn_dlg), FALSE); + + /* The main dialog area. */ + intro = gtk_label_new (_("Connect to a virt-v2v conversion server over SSH:")); + gtk_label_set_line_wrap (GTK_LABEL (intro), TRUE); + gtk_misc_set_padding (GTK_MISC (intro), 10, 10); + + table = gtk_table_new (5, 2, FALSE); + server_label = gtk_label_new (_("Conversion server:")); + gtk_misc_set_alignment (GTK_MISC (server_label), 1., 0.5); + gtk_table_attach (GTK_TABLE (table), server_label, + 0, 1, 0, 1, GTK_FILL, GTK_FILL, 4, 4); + server_entry = gtk_entry_new (); + if (config->server != NULL) + gtk_entry_set_text (GTK_ENTRY (server_entry), config->server); + gtk_table_attach (GTK_TABLE (table), server_entry, + 1, 2, 0, 1, GTK_FILL, GTK_FILL, 4, 4); + + port_label = gtk_label_new (_("SSH port:")); + gtk_misc_set_alignment (GTK_MISC (port_label), 1., 0.5); + gtk_table_attach (GTK_TABLE (table), port_label, + 0, 1, 1, 2, GTK_FILL, GTK_FILL, 4, 4); + port_entry = gtk_entry_new (); + gtk_entry_set_width_chars (GTK_ENTRY (port_entry), 6); + snprintf (port_str, sizeof port_str, "%d", config->port); + gtk_entry_set_text (GTK_ENTRY (port_entry), port_str); + gtk_table_attach (GTK_TABLE (table), port_entry, + 1, 2, 1, 2, GTK_FILL, GTK_FILL, 4, 4); + + username_label = gtk_label_new (_("User name:")); + gtk_misc_set_alignment (GTK_MISC (username_label), 1., 0.5); + gtk_table_attach (GTK_TABLE (table), username_label, + 0, 1, 2, 3, GTK_FILL, GTK_FILL, 4, 4); + username_entry = gtk_entry_new (); + if (config->username != NULL) + gtk_entry_set_text (GTK_ENTRY (username_entry), config->username); + else + gtk_entry_set_text (GTK_ENTRY (username_entry), "root"); + gtk_table_attach (GTK_TABLE (table), username_entry, + 1, 2, 2, 3, GTK_FILL, GTK_FILL, 4, 4); + + password_label = gtk_label_new (_("Password:")); + gtk_misc_set_alignment (GTK_MISC (password_label), 1., 0.5); + gtk_table_attach (GTK_TABLE (table), password_label, + 0, 1, 3, 4, GTK_FILL, GTK_FILL, 4, 4); + password_entry = gtk_entry_new (); + gtk_entry_set_visibility (GTK_ENTRY (password_entry), FALSE); +#ifdef GTK_INPUT_PURPOSE_PASSWORD + gtk_entry_set_input_purpose (GTK_ENTRY (password_entry), + GTK_INPUT_PURPOSE_PASSWORD); +#endif + if (config->password != NULL) + gtk_entry_set_text (GTK_ENTRY (password_entry), config->password); + gtk_table_attach (GTK_TABLE (table), password_entry, + 1, 2, 3, 4, GTK_FILL, GTK_FILL, 4, 4); + + sudo_button = + gtk_check_button_new_with_label (_("Use sudo when running virt-v2v")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (sudo_button), + config->sudo); + gtk_table_attach (GTK_TABLE (table), sudo_button, + 1, 2, 4, 5, GTK_FILL, GTK_FILL, 4, 4); + + test_hbox = gtk_hbox_new (FALSE, 0); + test = gtk_button_new_with_label (_("Test connection")); + gtk_box_pack_start (GTK_BOX (test_hbox), test, TRUE, FALSE, 0); + + spinner_hbox = gtk_hbox_new (FALSE, 10); + spinner = gtk_spinner_new (); + gtk_box_pack_start (GTK_BOX (spinner_hbox), spinner, FALSE, FALSE, 0); + spinner_message = gtk_label_new (NULL); + gtk_label_set_line_wrap (GTK_LABEL (spinner_message), TRUE); + gtk_misc_set_padding (GTK_MISC (spinner_message), 10, 10); + gtk_box_pack_start (GTK_BOX (spinner_hbox), spinner_message, TRUE, TRUE, 0); + + gtk_box_pack_start (GTK_BOX (GTK_DIALOG (conn_dlg)->vbox), + intro, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (GTK_DIALOG (conn_dlg)->vbox), + table, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (GTK_DIALOG (conn_dlg)->vbox), + test_hbox, FALSE, FALSE, 0); + gtk_box_pack_start (GTK_BOX (GTK_DIALOG (conn_dlg)->vbox), + spinner_hbox, TRUE, TRUE, 0); + + /* Buttons. */ + gtk_dialog_add_buttons (GTK_DIALOG (conn_dlg), + _("Configure network ..."), 1, + _("About ..."), 2, + _("Next"), 3, + NULL); + + next_button = gtk_dialog_get_widget_for_response (GTK_DIALOG (conn_dlg), 3); + gtk_widget_set_sensitive (next_button, FALSE); + + about = gtk_dialog_get_widget_for_response (GTK_DIALOG (conn_dlg), 2); + + /* Signals. */ + g_signal_connect_swapped (G_OBJECT (conn_dlg), "destroy", + G_CALLBACK (gtk_main_quit), NULL); + g_signal_connect (G_OBJECT (test), "clicked", + G_CALLBACK (test_connection_clicked), config); + g_signal_connect (G_OBJECT (about), "clicked", + G_CALLBACK (about_button_clicked), NULL); + g_signal_connect (G_OBJECT (next_button), "clicked", + G_CALLBACK (connection_next_clicked), NULL); +} + +static void +show_connection_dialog (void) +{ + /* Hide the other dialogs. */ + gtk_widget_hide (conv_dlg); + gtk_widget_hide (run_dlg); + + /* Show everything except the spinner. */ + gtk_widget_show_all (conn_dlg); + gtk_widget_hide_all (spinner_hbox); +} + +static void +test_connection_clicked (GtkWidget *w, gpointer data) +{ + struct config *config = data; + const gchar *port_str; + size_t errors = 0; + struct config *copy; + int err; + pthread_t tid; + pthread_attr_t attr; + + gtk_label_set_text (GTK_LABEL (spinner_message), ""); + gtk_widget_show_all (spinner_hbox); + + /* Get the fields from the various widgets. */ + free (config->server); + config->server = strdup (gtk_entry_get_text (GTK_ENTRY (server_entry))); + if (STREQ (config->server, "")) { + gtk_label_set_text (GTK_LABEL (spinner_message), + _("error: No conversion server given.")); + gtk_widget_grab_focus (server_entry); + errors++; + } + port_str = gtk_entry_get_text (GTK_ENTRY (port_entry)); + if (sscanf (port_str, "%d", &config->port) != 1 || + config->port <= 0 || config->port >= 65536) { + gtk_label_set_text (GTK_LABEL (spinner_message), + _("error: Invalid port number. If in doubt, use \"22\".")); + gtk_widget_grab_focus (port_entry); + errors++; + } + free (config->username); + config->username = strdup (gtk_entry_get_text (GTK_ENTRY (username_entry))); + if (STREQ (config->username, "")) { + gtk_label_set_text (GTK_LABEL (spinner_message), + _("error: No user name. If in doubt, use \"root\".")); + gtk_widget_grab_focus (username_entry); + errors++; + } + free (config->password); + config->password = strdup (gtk_entry_get_text (GTK_ENTRY (password_entry))); + + config->sudo = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (sudo_button)); + + if (errors) + return; + + /* Give the testing thread its own copy of the config in case we + * update the config in the main thread. + */ + copy = copy_config (config); + + /* No errors so far, so test the connection in a background thread. */ + pthread_attr_init (&attr); + pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED); + err = pthread_create (&tid, &attr, test_connection_thread, copy); + if (err != 0) { + fprintf (stderr, "pthread_create: %s\n", strerror (err)); + exit (EXIT_FAILURE); + } + pthread_attr_destroy (&attr); +} + +/* Run test_connection (in a detached background thread). Once it + * finishes stop the spinner and set the spinner message + * appropriately. If the test is successful then we enable the "Next" + * button. + */ +static void * +test_connection_thread (void *data) +{ + struct config *copy = data; + int r; + + gdk_threads_enter (); + gtk_label_set_text (GTK_LABEL (spinner_message), + _("Testing the connection to the conversion server ...")); + gtk_spinner_start (GTK_SPINNER (spinner)); + gdk_threads_leave (); + r = test_connection (copy); + free_config (copy); + gdk_threads_enter (); + gtk_spinner_stop (GTK_SPINNER (spinner)); + + if (r == -1) { + /* Error testing the connection. */ + const char *err = get_ssh_error (); + + gtk_label_set_text (GTK_LABEL (spinner_message), err); + /* Disable the Next button. */ + gtk_widget_set_sensitive (next_button, FALSE); + } + else { + /* Connection is good. */ + gtk_label_set_text (GTK_LABEL (spinner_message), + _("Connected to the conversion server.\n" + "Press the \"Next\" button to configure the conversion process.")); + /* Enable the Next button. */ + gtk_widget_set_sensitive (next_button, TRUE); + gtk_widget_grab_focus (next_button); + } + gdk_threads_leave (); + + /* Thread is detached anyway, so no one is waiting for the status. */ + return NULL; +} + +static void +about_button_clicked (GtkWidget *w, gpointer data) +{ + gtk_show_about_dialog (GTK_WINDOW (conn_dlg), + "program-name", program_name, + "version", PACKAGE_VERSION, + "copyright", "\u00A9 2009-2014 Red Hat Inc.", + "comments", "Convert a physical machine to use KVM", + "license", gplv2plus, + "website", "http://libguestfs.org/", + "authors", authors, + NULL); +} + +/* The connection dialog Next button has been clicked. */ +static void +connection_next_clicked (GtkWidget *w, gpointer data) +{ + /* Switch to the conversion dialog. */ + show_conversion_dialog (); +} + +/*----------------------------------------------------------------------*/ +/* Conversion dialog. */ + +static void populate_disks (GtkTreeView *disks_list); +static void populate_removable (GtkTreeView *removable_list); +static void populate_interfaces (GtkTreeView *interfaces_list); +static void toggled (GtkCellRendererToggle *cell, gchar *path_str, gpointer data); +static void set_disks_from_ui (struct config *); +static void set_removable_from_ui (struct config *); +static void set_interfaces_from_ui (struct config *); +static void conversion_back_clicked (GtkWidget *w, gpointer data); +static void start_conversion_clicked (GtkWidget *w, gpointer data); +static void notify_ui_callback (int type, const char *data); + +enum { + DISKS_COL_CONVERT = 0, + DISKS_COL_DEVICE, + DISKS_COL_SIZE, + DISKS_COL_MODEL, + NUM_DISKS_COLS, +}; + +enum { + REMOVABLE_COL_CONVERT = 0, + REMOVABLE_COL_DEVICE, + NUM_REMOVABLE_COLS, +}; + +enum { + INTERFACES_COL_CONVERT = 0, + INTERFACES_COL_DEVICE, + NUM_INTERFACES_COLS, +}; + +static void +create_conversion_dialog (struct config *config) +{ + GtkWidget *back; + GtkWidget *hbox, *right_vbox; + GtkWidget *target, *target_vbox, *target_tbl; + GtkWidget *guestname_label, *vcpus_label, *memory_label; + GtkWidget *disks_frame, *disks_sw; + GtkWidget *removable_frame, *removable_sw; + GtkWidget *interfaces_frame, *interfaces_sw; + char vcpus_str[64]; + char memory_str[64]; + + conv_dlg = gtk_dialog_new (); + gtk_window_set_title (GTK_WINDOW (conv_dlg), program_name); + gtk_window_set_resizable (GTK_WINDOW (conv_dlg), FALSE); + /* XXX It would be nice not to have to set this explicitly, but + * if we don't then Gtk chooses a very small window. + */ + gtk_widget_set_size_request (conv_dlg, 800, 500); + + /* The main dialog area. */ + hbox = gtk_hbox_new (TRUE, 1); + right_vbox = gtk_vbox_new (TRUE, 1); + + /* The left column: target properties. */ + target = gtk_frame_new (_("Target properties")); + + target_vbox = gtk_vbox_new (FALSE, 1); + + target_tbl = gtk_table_new (3, 2, FALSE); + guestname_label = gtk_label_new (_("Name:")); + gtk_misc_set_alignment (GTK_MISC (guestname_label), 1., 0.5); + gtk_table_attach (GTK_TABLE (target_tbl), guestname_label, + 0, 1, 0, 1, GTK_FILL, GTK_FILL, 1, 1); + guestname_entry = gtk_entry_new (); + if (config->guestname != NULL) + gtk_entry_set_text (GTK_ENTRY (guestname_entry), config->guestname); + gtk_table_attach (GTK_TABLE (target_tbl), guestname_entry, + 1, 2, 0, 1, GTK_FILL, GTK_FILL, 1, 1); + + vcpus_label = gtk_label_new (_("# vCPUs:")); + gtk_misc_set_alignment (GTK_MISC (vcpus_label), 1., 0.5); + gtk_table_attach (GTK_TABLE (target_tbl), vcpus_label, + 0, 1, 1, 2, GTK_FILL, GTK_FILL, 1, 1); + vcpus_entry = gtk_entry_new (); + snprintf (vcpus_str, sizeof vcpus_str, "%d", config->vcpus); + gtk_entry_set_text (GTK_ENTRY (vcpus_entry), vcpus_str); + gtk_table_attach (GTK_TABLE (target_tbl), vcpus_entry, + 1, 2, 1, 2, GTK_FILL, GTK_FILL, 1, 1); + + memory_label = gtk_label_new (_("Memory (MB):")); + gtk_misc_set_alignment (GTK_MISC (memory_label), 1., 0.5); + gtk_table_attach (GTK_TABLE (target_tbl), memory_label, + 0, 1, 2, 3, GTK_FILL, GTK_FILL, 1, 1); + memory_entry = gtk_entry_new (); + snprintf (memory_str, sizeof memory_str, "%" PRIu64, + config->memory / 1024 / 1024); + gtk_entry_set_text (GTK_ENTRY (memory_entry), memory_str); + gtk_table_attach (GTK_TABLE (target_tbl), memory_entry, + 1, 2, 2, 3, GTK_FILL, GTK_FILL, 1, 1); + + debug_button = + gtk_check_button_new_with_label (_("Enable server-side debugging\n" + "(This is saved in /tmp on the conversion server)")); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (debug_button), + config->verbose); + + gtk_box_pack_start (GTK_BOX (target_vbox), target_tbl, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (target_vbox), debug_button, TRUE, TRUE, 0); + gtk_container_add (GTK_CONTAINER (target), target_vbox); + + /* The right column: select devices to be converted. */ + disks_frame = gtk_frame_new (_("Fixed hard disks")); + disks_sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (disks_sw), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + disks_list = gtk_tree_view_new (); + populate_disks (GTK_TREE_VIEW (disks_list)); + gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (disks_sw), + disks_list); + gtk_container_add (GTK_CONTAINER (disks_frame), disks_sw); + + removable_frame = gtk_frame_new (_("Removable media")); + removable_sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (removable_sw), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + removable_list = gtk_tree_view_new (); + populate_removable (GTK_TREE_VIEW (removable_list)); + gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (removable_sw), + removable_list); + gtk_container_add (GTK_CONTAINER (removable_frame), removable_sw); + + interfaces_frame = gtk_frame_new (_("Network interfaces")); + interfaces_sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (interfaces_sw), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + interfaces_list = gtk_tree_view_new (); + populate_interfaces (GTK_TREE_VIEW (interfaces_list)); + gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (interfaces_sw), + interfaces_list); + gtk_container_add (GTK_CONTAINER (interfaces_frame), interfaces_sw); + + gtk_box_pack_start (GTK_BOX (right_vbox), disks_frame, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (right_vbox), removable_frame, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (right_vbox), interfaces_frame, TRUE, TRUE, 0); + + gtk_box_pack_start (GTK_BOX (hbox), target, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (hbox), right_vbox, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (GTK_DIALOG (conv_dlg)->vbox), + hbox, TRUE, TRUE, 0); + + /* Buttons. */ + gtk_dialog_add_buttons (GTK_DIALOG (conv_dlg), + _("Back"), 1, + _("Start conversion"), 2, + NULL); + back = gtk_dialog_get_widget_for_response (GTK_DIALOG (conv_dlg), 1); + start_button = gtk_dialog_get_widget_for_response (GTK_DIALOG (conv_dlg), 2); + + /* Signals. */ + g_signal_connect_swapped (G_OBJECT (conv_dlg), "destroy", + G_CALLBACK (gtk_main_quit), NULL); + g_signal_connect (G_OBJECT (back), "clicked", + G_CALLBACK (conversion_back_clicked), NULL); + g_signal_connect (G_OBJECT (start_button), "clicked", + G_CALLBACK (start_conversion_clicked), config); +} + +static void +show_conversion_dialog (void) +{ + /* Hide the other dialogs. */ + gtk_widget_hide (conn_dlg); + gtk_widget_hide (run_dlg); + + /* Show the conversion dialog. */ + gtk_widget_show_all (conv_dlg); +} + +static void +populate_disks (GtkTreeView *disks_list) +{ + GtkListStore *disks_store; + GtkCellRenderer *disks_col_convert, *disks_col_device, + *disks_col_size, *disks_col_model; + GtkTreeIter iter; + size_t i; + + disks_store = gtk_list_store_new (NUM_DISKS_COLS, + G_TYPE_BOOLEAN, G_TYPE_STRING, + G_TYPE_STRING, G_TYPE_STRING); + if (all_disks != NULL) { + for (i = 0; all_disks[i] != NULL; ++i) { + CLEANUP_FREE char *size_filename = NULL; + CLEANUP_FREE char *model_filename = NULL; + CLEANUP_FREE char *size_str = NULL; + CLEANUP_FREE char *size_gb = NULL; + CLEANUP_FREE char *model = NULL; + uint64_t size; + + if (asprintf (&size_filename, "/sys/block/%s/size", + all_disks[i]) == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + if (g_file_get_contents (size_filename, &size_str, NULL, NULL) && + sscanf (size_str, "%" SCNu64, &size) == 1) { + size /= 2*1024*1024; /* size from kernel is given in sectors? */ + if (asprintf (&size_gb, "%" PRIu64, size) == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + } + + if (asprintf (&model_filename, "/sys/block/%s/device/model", + all_disks[i]) == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + if (g_file_get_contents (model_filename, &model, NULL, NULL)) { + /* Need to chomp trailing \n from the content. */ + size_t len = strlen (model); + if (len > 0 && model[len-1] == '\n') + model[len-1] = '\0'; + } else { + model = strdup (""); + } + + gtk_list_store_append (disks_store, &iter); + gtk_list_store_set (disks_store, &iter, + DISKS_COL_CONVERT, TRUE, + DISKS_COL_DEVICE, all_disks[i], + DISKS_COL_SIZE, size_gb, + DISKS_COL_MODEL, model, + -1); + } + } + gtk_tree_view_set_model (disks_list, + GTK_TREE_MODEL (disks_store)); + gtk_tree_view_set_headers_visible (disks_list, TRUE); + disks_col_convert = gtk_cell_renderer_toggle_new (); + gtk_tree_view_insert_column_with_attributes (disks_list, + -1, + _("Convert"), + disks_col_convert, + "active", DISKS_COL_CONVERT, + NULL); + disks_col_device = gtk_cell_renderer_text_new (); + gtk_tree_view_insert_column_with_attributes (disks_list, + -1, + _("Device"), + disks_col_device, + "text", DISKS_COL_DEVICE, + NULL); + disks_col_size = gtk_cell_renderer_text_new (); + gtk_tree_view_insert_column_with_attributes (disks_list, + -1, + _("Size (GB)"), + disks_col_size, + "text", DISKS_COL_SIZE, + NULL); + disks_col_model = gtk_cell_renderer_text_new (); + gtk_tree_view_insert_column_with_attributes (disks_list, + -1, + _("Model"), + disks_col_model, + "text", DISKS_COL_MODEL, + NULL); + + g_signal_connect (disks_col_convert, "toggled", + G_CALLBACK (toggled), disks_store); +} + +static void +populate_removable (GtkTreeView *removable_list) +{ + GtkListStore *removable_store; + GtkCellRenderer *removable_col_convert, *removable_col_device; + GtkTreeIter iter; + size_t i; + + removable_store = gtk_list_store_new (NUM_REMOVABLE_COLS, + G_TYPE_BOOLEAN, G_TYPE_STRING); + if (all_removable != NULL) { + for (i = 0; all_removable[i] != NULL; ++i) { + gtk_list_store_append (removable_store, &iter); + gtk_list_store_set (removable_store, &iter, + REMOVABLE_COL_CONVERT, TRUE, + REMOVABLE_COL_DEVICE, all_removable[i], + -1); + } + } + gtk_tree_view_set_model (removable_list, + GTK_TREE_MODEL (removable_store)); + gtk_tree_view_set_headers_visible (removable_list, TRUE); + removable_col_convert = gtk_cell_renderer_toggle_new (); + gtk_tree_view_insert_column_with_attributes (removable_list, + -1, + _("Convert"), + removable_col_convert, + "active", REMOVABLE_COL_CONVERT, + NULL); + removable_col_device = gtk_cell_renderer_text_new (); + gtk_tree_view_insert_column_with_attributes (removable_list, + -1, + _("Device"), + removable_col_device, + "text", REMOVABLE_COL_DEVICE, + NULL); + + g_signal_connect (removable_col_convert, "toggled", + G_CALLBACK (toggled), removable_store); +} + +static void +populate_interfaces (GtkTreeView *interfaces_list) +{ + GtkListStore *interfaces_store; + GtkCellRenderer *interfaces_col_convert, *interfaces_col_device; + GtkTreeIter iter; + size_t i; + + interfaces_store = gtk_list_store_new (NUM_INTERFACES_COLS, + G_TYPE_BOOLEAN, G_TYPE_STRING); + if (all_interfaces) { + for (i = 0; all_interfaces[i] != NULL; ++i) { + gtk_list_store_append (interfaces_store, &iter); + gtk_list_store_set (interfaces_store, &iter, + INTERFACES_COL_CONVERT, TRUE, + INTERFACES_COL_DEVICE, all_interfaces[i], + -1); + } + } + gtk_tree_view_set_model (interfaces_list, + GTK_TREE_MODEL (interfaces_store)); + gtk_tree_view_set_headers_visible (interfaces_list, TRUE); + interfaces_col_convert = gtk_cell_renderer_toggle_new (); + gtk_tree_view_insert_column_with_attributes (interfaces_list, + -1, + _("Convert"), + interfaces_col_convert, + "active", INTERFACES_COL_CONVERT, + NULL); + interfaces_col_device = gtk_cell_renderer_text_new (); + gtk_tree_view_insert_column_with_attributes (interfaces_list, + -1, + _("Device"), + interfaces_col_device, + "text", INTERFACES_COL_DEVICE, + NULL); + + g_signal_connect (interfaces_col_convert, "toggled", + G_CALLBACK (toggled), interfaces_store); +} + +static void +toggled (GtkCellRendererToggle *cell, gchar *path_str, gpointer data) +{ + GtkTreeModel *model = data; + GtkTreePath *path = gtk_tree_path_new_from_string (path_str); + GtkTreeIter iter; + gboolean v; + + gtk_tree_model_get_iter (model, &iter, path); + gtk_tree_model_get (model, &iter, 0 /* CONVERT */, &v, -1); + v ^= 1; + gtk_list_store_set (GTK_LIST_STORE (model), &iter, 0 /* CONVERT */, v, -1); + gtk_tree_path_free (path); +} + +static void +set_from_ui_generic (char **all, char ***ret, GtkTreeView *list) +{ + GtkTreeModel *model; + GtkTreeIter iter; + gboolean b, v; + size_t i, j; + + if (all == NULL) { + guestfs___free_string_list (*ret); + *ret = NULL; + return; + } + + model = gtk_tree_view_get_model (list); + + guestfs___free_string_list (*ret); + *ret = malloc ((1 + guestfs___count_strings (all)) * sizeof (char *)); + if (*ret == NULL) { + perror ("malloc"); + exit (EXIT_FAILURE); + } + i = j = 0; + + b = gtk_tree_model_get_iter_first (model, &iter); + while (b) { + gtk_tree_model_get (model, &iter, 0 /* CONVERT */, &v, -1); + if (v) { + assert (all[i] != NULL); + (*ret)[j++] = strdup (all[i]); + } + b = gtk_tree_model_iter_next (model, &iter); + ++i; + } + + (*ret)[j] = NULL; +} + +static void +set_disks_from_ui (struct config *config) +{ + set_from_ui_generic (all_disks, &config->disks, + GTK_TREE_VIEW (disks_list)); +} + +static void +set_removable_from_ui (struct config *config) +{ + set_from_ui_generic (all_removable, &config->removable, + GTK_TREE_VIEW (removable_list)); +} + +static void +set_interfaces_from_ui (struct config *config) +{ + set_from_ui_generic (all_interfaces, &config->interfaces, + GTK_TREE_VIEW (interfaces_list)); +} + +/* The conversion dialog Back button has been clicked. */ +static void +conversion_back_clicked (GtkWidget *w, gpointer data) +{ + /* Switch to the connection dialog. */ + show_connection_dialog (); + + /* Better disable the Next button so the user is forced to + * do "Test connection" again. + */ + gtk_widget_set_sensitive (next_button, FALSE); +} + +/*----------------------------------------------------------------------*/ +/* Running dialog. */ + +static void set_log_dir (const char *remote_dir); +static void set_status (const char *msg); +static void add_v2v_output (const char *msg); +static void *start_conversion_thread (void *data); + +static void +create_running_dialog (void) +{ + run_dlg = gtk_dialog_new (); + gtk_window_set_title (GTK_WINDOW (run_dlg), program_name); + gtk_window_set_resizable (GTK_WINDOW (run_dlg), FALSE); + + /* The main dialog area. */ + v2v_output_sw = gtk_scrolled_window_new (NULL, NULL); + gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (v2v_output_sw), + GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + v2v_output = gtk_text_view_new (); + gtk_text_view_set_editable (GTK_TEXT_VIEW (v2v_output), FALSE); + gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (v2v_output), GTK_WRAP_CHAR); + gtk_widget_set_size_request (v2v_output, 700, 400); + log_label = gtk_label_new (NULL); + gtk_misc_set_alignment (GTK_MISC (log_label), 0., 0.5); + gtk_misc_set_padding (GTK_MISC (log_label), 10, 10); + set_log_dir (NULL); + status_label = gtk_label_new (NULL); + gtk_misc_set_alignment (GTK_MISC (status_label), 0., 0.5); + gtk_misc_set_padding (GTK_MISC (status_label), 10, 10); + + gtk_container_add (GTK_CONTAINER (v2v_output_sw), v2v_output); + + gtk_box_pack_start (GTK_BOX (GTK_DIALOG (run_dlg)->vbox), + v2v_output_sw, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (GTK_DIALOG (run_dlg)->vbox), + log_label, TRUE, TRUE, 0); + gtk_box_pack_start (GTK_BOX (GTK_DIALOG (run_dlg)->vbox), + status_label, TRUE, TRUE, 0); + + /* Buttons. */ + gtk_dialog_add_buttons (GTK_DIALOG (run_dlg), + _("Cancel conversion"), 1, + NULL); + cancel_button = gtk_dialog_get_widget_for_response (GTK_DIALOG (conv_dlg), 1); + gtk_widget_set_sensitive (cancel_button, FALSE); + + /* Signals. */ + g_signal_connect_swapped (G_OBJECT (run_dlg), "destroy", + G_CALLBACK (gtk_main_quit), NULL); +} + +static void +show_running_dialog (void) +{ + /* Hide the other dialogs. */ + gtk_widget_hide (conn_dlg); + gtk_widget_hide (conv_dlg); + + /* Show the running dialog. */ + gtk_widget_show_all (run_dlg); + gtk_widget_set_sensitive (cancel_button, FALSE); +} + +static void +set_log_dir (const char *remote_dir) +{ + CLEANUP_FREE char *msg; + + if (asprintf (&msg, + _("Log files and debug information " + "is saved to this directory " + "on the conversion server:\n" + "%s"), + remote_dir ? remote_dir : "") == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + + gtk_label_set_text (GTK_LABEL (log_label), msg); +} + +static void +set_status (const char *msg) +{ + gtk_label_set_text (GTK_LABEL (status_label), msg); +} + +/* Append output from the virt-v2v process to the buffer, and scroll + * to ensure it is visible. + */ +static void +add_v2v_output (const char *msg) +{ + GtkTextBuffer *buf; + GtkTextIter iter; + + /* Insert it at the end. */ + buf = gtk_text_view_get_buffer (GTK_TEXT_VIEW (v2v_output)); + gtk_text_buffer_get_end_iter (buf, &iter); + gtk_text_buffer_insert (buf, &iter, msg, -1); + + /* Scroll to the end of the buffer. */ + gtk_text_buffer_get_end_iter (buf, &iter); + gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (v2v_output), &iter, + 0, FALSE, 0., 1.); +} + +/* User clicked the Start conversion button. */ +static void +start_conversion_clicked (GtkWidget *w, gpointer data) +{ + struct config *config = data; + int i; + const char *vcpus_str; + const char *memory_str; + GtkWidget *dlg; + struct config *copy; + int err; + pthread_t tid; + pthread_attr_t attr; + + /* Unpack dialog fields and check them. */ + free (config->guestname); + config->guestname = strdup (gtk_entry_get_text (GTK_ENTRY (guestname_entry))); + + if (STREQ (config->guestname, "")) { + dlg = gtk_message_dialog_new (GTK_WINDOW (conv_dlg), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + _("The guest \"Name\" field is empty.")); + gtk_window_set_title (GTK_WINDOW (dlg), _("Error")); + gtk_dialog_run (GTK_DIALOG (dlg)); + gtk_widget_destroy (dlg); + gtk_widget_grab_focus (guestname_entry); + return; + } + + vcpus_str = gtk_entry_get_text (GTK_ENTRY (vcpus_entry)); + if (sscanf (vcpus_str, "%d", &i) == 1 && i > 0) + config->vcpus = i; + else + config->vcpus = 1; + + memory_str = gtk_entry_get_text (GTK_ENTRY (memory_entry)); + if (sscanf (memory_str, "%d", &i) == 1 && i >= 256) + config->memory = (uint64_t) i * 1024 * 1024; + else + config->memory = 1024 * 1024 * 1024; + + config->verbose = + gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (debug_button)); + + /* Get the list of disks to be converted. */ + set_disks_from_ui (config); + + /* The list of disks must be non-empty. */ + if (config->disks == NULL || guestfs___count_strings (config->disks) == 0) { + dlg = gtk_message_dialog_new (GTK_WINDOW (conv_dlg), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + _("No disks were selected for conversion.\n" + "At least one fixed hard disk must be selected.\n")); + gtk_window_set_title (GTK_WINDOW (dlg), _("Error")); + gtk_dialog_run (GTK_DIALOG (dlg)); + gtk_widget_destroy (dlg); + return; + } + + /* List of removable media and network interfaces. */ + set_removable_from_ui (config); + set_interfaces_from_ui (config); + + /* Display the UI for conversion. */ + show_running_dialog (); + + /* Do the conversion, in a background thread. */ + + /* Give the conversion (background) thread its own copy of the + * config in case we update the config in the main thread. + */ + copy = copy_config (config); + + pthread_attr_init (&attr); + pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED); + err = pthread_create (&tid, &attr, start_conversion_thread, copy); + if (err != 0) { + fprintf (stderr, "pthread_create: %s\n", strerror (err)); + exit (EXIT_FAILURE); + } + pthread_attr_destroy (&attr); +} + +static void * +start_conversion_thread (void *data) +{ + struct config *copy = data; + int r; + GtkWidget *dlg; + + r = start_conversion (copy, notify_ui_callback); + free_config (copy); + + gdk_threads_enter (); + + if (r == -1) { + const char *err = get_conversion_error (); + + dlg = gtk_message_dialog_new (GTK_WINDOW (run_dlg), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + _("Conversion failed: %s"), err); + gtk_window_set_title (GTK_WINDOW (dlg), _("Conversion failed")); + gtk_dialog_run (GTK_DIALOG (dlg)); + gtk_widget_destroy (dlg); + } + else { + dlg = gtk_message_dialog_new (GTK_WINDOW (run_dlg), + GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + _("The conversion was successful.")); + gtk_window_set_title (GTK_WINDOW (dlg), _("Conversion was successful")); + gtk_dialog_run (GTK_DIALOG (dlg)); + gtk_widget_destroy (dlg); + } + + gdk_threads_leave (); + + /* Thread is detached anyway, so no one is waiting for the status. */ + return NULL; +} + +static void +notify_ui_callback (int type, const char *data) +{ + gdk_threads_enter (); + + switch (type) { + case NOTIFY_LOG_DIR: + set_log_dir (data); + break; + + case NOTIFY_REMOTE_MESSAGE: + add_v2v_output (data); + break; + + case NOTIFY_STATUS: + set_status (data); + break; + + default: + fprintf (stderr, + "%s: unknown message during conversion: type=%d data=%s\n", + program_name, type, data); + } + + gdk_threads_leave (); +} diff --git a/p2v/kernel.c b/p2v/kernel.c new file mode 100644 index 000000000..202c2650e --- /dev/null +++ b/p2v/kernel.c @@ -0,0 +1,184 @@ +/* virt-p2v + * Copyright (C) 2009-2014 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. + */ + +/* Kernel-driven configuration, non-interactive. */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "p2v.h" + +static void notify_ui_callback (int type, const char *data); + +void +kernel_configuration (struct config *config, const char *cmdline) +{ + const char *r; + size_t len; + + r = strstr (cmdline, "p2v.server="); + assert (r); /* checked by caller */ + r += 5+6; + len = strcspn (r, " "); + free (config->server); + config->server = strndup (r, len); + + r = strstr (cmdline, "p2v.port="); + if (r) { + r += 5+4; + if (sscanf (r, "%d", &config->port) != 1) { + fprintf (stderr, "%s: cannot parse p2v.port from kernel command line", + program_name); + exit (EXIT_FAILURE); + } + } + + r = strstr (cmdline, "p2v.username="); + if (r) { + r += 5+8; + len = strcspn (r, " "); + free (config->username); + config->username = strndup (r, len); + } + + r = strstr (cmdline, "p2v.password="); + if (r) { + r += 5+8; + len = strcspn (r, " "); + free (config->password); + config->password = strndup (r, len); + } + + r = strstr (cmdline, "p2v.sudo"); + if (r) + config->sudo = 1; + + /* We should now be able to connect and interrogate virt-v2v + * on the conversion server. + */ + if (test_connection (config) == -1) { + const char *err = get_ssh_error (); + + fprintf (stderr, "%s: error opening control connection to %s:%d: %s\n", + program_name, config->server, config->port, err); + exit (EXIT_FAILURE); + } + + r = strstr (cmdline, "p2v.name="); + if (r) { + r += 5+4; + len = strcspn (r, " "); + free (config->guestname); + config->guestname = strndup (r, len); + } + + r = strstr (cmdline, "p2v.vcpus="); + if (r) { + r += 5+5; + if (sscanf (r, "%d", &config->vcpus) != 1) { + fprintf (stderr, "%s: cannot parse p2v.vcpus from kernel command line\n", + program_name); + exit (EXIT_FAILURE); + } + } + + r = strstr (cmdline, "p2v.memory="); + if (r) { + char mem_code[2]; + + r += 5+6; + if (sscanf (r, "%" SCNu64 "%c", &config->memory, mem_code) != 1) { + fprintf (stderr, "%s: cannot parse p2v.memory from kernel command line\n", + program_name); + exit (EXIT_FAILURE); + } + config->memory *= 1024; + if (mem_code[0] == 'M' || mem_code[0] == 'G') + config->memory *= 1024; + if (mem_code[0] == 'G') + config->memory *= 1024; + if (mem_code[0] != 'M' && mem_code[0] != 'G') { + fprintf (stderr, "%s: p2v.memory on kernel command line must be followed by 'G' or 'M'\n", + program_name); + exit (EXIT_FAILURE); + } + } + + r = strstr (cmdline, "p2v.disks="); + if (r) { + r += 5+5; + len = strcspn (r, " "); + guestfs___free_string_list (config->disks); + config->disks = guestfs___split_string (',', r); + } + + r = strstr (cmdline, "p2v.removable="); + if (r) { + r += 5+9; + len = strcspn (r, " "); + guestfs___free_string_list (config->removable); + config->removable = guestfs___split_string (',', r); + } + + r = strstr (cmdline, "p2v.interfaces="); + if (r) { + r += 5+10; + len = strcspn (r, " "); + guestfs___free_string_list (config->interfaces); + config->interfaces = guestfs___split_string (',', r); + } + + if (start_conversion (config, notify_ui_callback) == -1) { + const char *err = get_conversion_error (); + + fprintf (stderr, "%s: error during conversion: %s\n", + program_name, err); + exit (EXIT_FAILURE); + } +} + +static void +notify_ui_callback (int type, const char *data) +{ + switch (type) { + case NOTIFY_LOG_DIR: + printf ("%s: remote log directory location: %s\n", program_name, data); + break; + + case NOTIFY_REMOTE_MESSAGE: + printf ("%s", data); + break; + + case NOTIFY_STATUS: + printf ("%s: %s\n", program_name, data); + break; + + default: + printf ("%s: unknown message during conversion: type=%d data=%s\n", + program_name, type, data); + } +} diff --git a/p2v/main.c b/p2v/main.c new file mode 100644 index 000000000..f4f9429bb --- /dev/null +++ b/p2v/main.c @@ -0,0 +1,502 @@ +/* virt-p2v + * Copyright (C) 2009-2014 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma GCC diagnostic ignored "-Wstrict-prototypes" /* error in */ +#include + +#include "p2v.h" + +char **all_disks; +char **all_removable; +char **all_interfaces; + +static void set_config_defaults (struct config *config); +static void find_all_disks (void); +static void find_all_interfaces (void); +static char *read_cmdline (void); + +enum { HELP_OPTION = CHAR_MAX + 1 }; +static const char *options = "Vv"; +static const struct option long_options[] = { + { "help", 0, 0, HELP_OPTION }, + { "cmdline", 1, 0, 0 }, + { "long-options", 0, 0, 0 }, + { "verbose", 0, 0, 'v' }, + { "version", 0, 0, 'V' }, + { 0, 0, 0, 0 } +}; + +static void __attribute__((noreturn)) +usage (int status) +{ + if (status != EXIT_SUCCESS) + fprintf (stderr, _("Try `%s --help' for more information.\n"), + program_name); + else { + fprintf (stdout, + _("%s: Convert a physical machine to use KVM\n" + "Copyright (C) 2009-2014 Red Hat Inc.\n" + "Usage:\n" + " %s [--options]\n" + "Options:\n" + " --help Display brief help\n" + " --cmdline=CMDLINE Used to debug command line parsing\n" + " -v|--verbose Verbose messages\n" + " -V|--version Display version and exit\n" + "For more information, see the manpage %s(1).\n"), + program_name, program_name, program_name); + } + exit (status); +} + +/* XXX Copied from fish/options.c. */ +static void +display_long_options (const struct option *long_options) +{ + while (long_options->name) { + if (STRNEQ (long_options->name, "long-options")) + printf ("--%s\n", long_options->name); + long_options++; + } + exit (EXIT_SUCCESS); +} + +int +main (int argc, char *argv[]) +{ + gboolean gui_possible; + int c; + int option_index; + char *cmdline = NULL; + struct config *config = new_config (); + + setlocale (LC_ALL, ""); + bindtextdomain (PACKAGE, LOCALEBASEDIR); + textdomain (PACKAGE); + + gdk_threads_init (); + gdk_threads_enter (); + gui_possible = gtk_init_check (&argc, &argv); + + for (;;) { + c = getopt_long (argc, argv, options, long_options, &option_index); + if (c == -1) break; + + switch (c) { + case 0: /* options which are long only */ + if (STREQ (long_options[option_index].name, "long-options")) { + display_long_options (long_options); + } + else if (STREQ (long_options[option_index].name, "cmdline")) { + cmdline = strdup (optarg); + } + else { + fprintf (stderr, _("%s: unknown long option: %s (%d)\n"), + program_name, long_options[option_index].name, option_index); + exit (EXIT_FAILURE); + } + break; + + case 'v': + config->verbose = 1; + break; + + case 'V': + printf ("%s %s\n", program_name, PACKAGE_VERSION); + exit (EXIT_SUCCESS); + + case HELP_OPTION: + usage (EXIT_SUCCESS); + + default: + usage (EXIT_FAILURE); + } + } + + if (optind != argc) { + fprintf (stderr, _("%s: unused arguments on the command line\n"), + program_name); + usage (EXIT_FAILURE); + } + + set_config_defaults (config); + + /* If /proc/cmdline exists and contains "p2v.server=" then we enable + * non-interactive configuration. + * If /proc/cmdline contains p2v.debug then we enable verbose mode + * even for interactive configuration. + */ + if (cmdline == NULL) + cmdline = read_cmdline (); + if (cmdline == NULL) + goto gui; + + if (strstr (cmdline, "p2v.debug")) + config->verbose = 1; + + if (strstr (cmdline, "p2v.server=")) + kernel_configuration (config, cmdline); + else { + gui: + if (!gui_possible) + /* Gtk has already printed an error. */ + exit (EXIT_FAILURE); + gui_application (config); + } + + free (cmdline); + + exit (EXIT_SUCCESS); +} + +static void +set_config_defaults (struct config *config) +{ + long i; + char hostname[257]; + + /* Default guest name is derived from the source hostname. If we + * assume that the p2v ISO gets its IP address and hostname from + * DHCP, then there is at better than average chance that + * gethostname will return the real hostname here. It's better than + * trying to fish around in the guest filesystem anyway. + */ + if (gethostname (hostname, sizeof hostname) == -1) { + perror ("gethostname"); + /* Generate a simple random name. */ + if (guestfs___random_string (hostname, 8) == -1) { + perror ("/dev/urandom"); + exit (EXIT_FAILURE); + } + } else { + char *p; + + /* If the hostname is an FQDN, truncate before the first dot. */ + p = strchr (hostname, '.'); + if (p && p > hostname) + *p = '\0'; + } + config->guestname = strdup (hostname); + + /* Defaults for #vcpus and memory are taken from the physical machine. */ + i = sysconf (_SC_NPROCESSORS_ONLN); + if (i == -1) { + perror ("sysconf: _SC_NPROCESSORS_ONLN"); + config->vcpus = 1; + } + else if (i == 0) + config->vcpus = 1; + else + config->vcpus = i; + + i = sysconf (_SC_PHYS_PAGES); + if (i == -1) { + perror ("sysconf: _SC_PHYS_PAGES"); + config->memory = 1024 * 1024 * 1024; + } + else + config->memory = i; + + i = sysconf (_SC_PAGESIZE); + if (i == -1) { + perror ("sysconf: _SC_PAGESIZE"); + config->memory *= 4096; + } + else + config->memory *= i; + + /* Round up the default memory to a power of 2, since the kernel + * memory is not included in the total physical pages returned + * above. + * http://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + */ + config->memory--; + config->memory |= config->memory >> 1; + config->memory |= config->memory >> 2; + config->memory |= config->memory >> 4; + config->memory |= config->memory >> 8; + config->memory |= config->memory >> 16; + config->memory |= config->memory >> 32; + config->memory++; + + find_all_disks (); + config->disks = guestfs___copy_string_list (all_disks); + if (all_removable) + config->removable = guestfs___copy_string_list (all_removable); + find_all_interfaces (); + if (all_interfaces) + config->interfaces = guestfs___copy_string_list (all_interfaces); +} + +static int +compare (const void *vp1, const void *vp2) +{ + char * const *p1 = (char * const *) vp1; + char * const *p2 = (char * const *) vp2; + return strcmp (*p1, *p2); +} + +static int +device_contains (const char *dev, dev_t root_device) +{ + CLEANUP_FREE char *dev_name; + struct stat statbuf; + + if (asprintf (&dev_name, "/dev/%s", dev) == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + + if (stat (dev_name, &statbuf) == -1) + return 0; + + if (statbuf.st_rdev == root_device) + return 1; + + /* Could be a partition. XXX Very hacky and incorrect way to get + * the device from its partition. + */ + if (minor (root_device) < 8 /* any major:minor where minor is "small" */ && + statbuf.st_rdev == makedev (major (root_device), 0)) + return 1; + if (major (statbuf.st_rdev) == 8 /* SCSI */ && + statbuf.st_rdev == makedev (major (root_device), minor (root_device) & 0xf)) + return 1; + + return 0; +} + +static void +find_all_disks (void) +{ + DIR *dir; + struct dirent *d; + size_t nr_disks = 0, nr_removable = 0; + dev_t root_device = 0; + struct stat statbuf; + + if (stat ("/", &statbuf) == 0) + root_device = statbuf.st_dev; + + /* The default list of disks is everything in /sys/block which + * matches the common patterns for disk names. + */ + dir = opendir ("/sys/block"); + if (!dir) { + perror ("opendir"); + exit (EXIT_FAILURE); + } + + for (;;) { + errno = 0; + d = readdir (dir); + if (!d) break; + + if (STRPREFIX (d->d_name, "cciss!") || + STRPREFIX (d->d_name, "hd") || + STRPREFIX (d->d_name, "sd") || + STRPREFIX (d->d_name, "ubd") || + STRPREFIX (d->d_name, "vd")) { + char *p; + + /* Skip the device containing the root filesystem. This is only + * an approximate test -- for example it doesn't work if the + * root filesystem is on an LV. However it doesn't need to be + * completely accurate, and we only really care that it works on + * the p2v ISO. + */ + if (device_contains (d->d_name, root_device)) + continue; + + nr_disks++; + all_disks = realloc (all_disks, sizeof (char *) * (nr_disks + 1)); + if (!all_disks) { + perror ("realloc"); + exit (EXIT_FAILURE); + } + + all_disks[nr_disks-1] = strdup (d->d_name); + + /* cciss device /dev/cciss/c0d0 will be /sys/block/cciss!c0d0 */ + p = strchr (all_disks[nr_disks-1], '!'); + if (p) *p = '/'; + + all_disks[nr_disks] = NULL; + } + else if (STRPREFIX (d->d_name, "sr")) { + nr_removable++; + all_removable = realloc (all_removable, + sizeof (char *) * (nr_removable + 1)); + if (!all_removable) { + perror ("realloc"); + exit (EXIT_FAILURE); + } + all_removable[nr_removable-1] = strdup (d->d_name); + all_removable[nr_removable] = NULL; + } + } + + /* Check readdir didn't fail */ + if (errno != 0) { + perror ("readdir: /sys/block"); + exit (EXIT_FAILURE); + } + + /* Close the directory handle */ + if (closedir (dir) == -1) { + perror ("closedir: /sys/block"); + exit (EXIT_FAILURE); + } + + if (all_disks == NULL) { + fprintf (stderr, "%s: error: no non-removable disks were discovered on this machine.\n", + program_name); + fprintf (stderr, "virt-p2v looked in /sys/block.\n"); + fprintf (stderr, "This is a fatal error and virt-p2v cannot continue.\n"); + exit (EXIT_FAILURE); + } + + qsort (all_disks, nr_disks, sizeof (char *), compare); + if (all_removable) + qsort (all_removable, nr_removable, sizeof (char *), compare); +} + +static void +find_all_interfaces (void) +{ + DIR *dir; + struct dirent *d; + size_t nr_interfaces = 0; + + /* The default list of network interfaces is everything in + * /sys/class/net which matches some common patterns. + */ + dir = opendir ("/sys/class/net"); + if (!dir) { + perror ("opendir"); + exit (EXIT_FAILURE); + } + + for (;;) { + errno = 0; + d = readdir (dir); + if (!d) break; + + /* For systemd predictable names, see: + * http://cgit.freedesktop.org/systemd/systemd/tree/src/udev/udev-builtin-net_id.c#n20 + * biosdevname is also a possibility here. + * Ignore PPP, SLIP, WWAN, bridges, etc. + */ + if (STRPREFIX (d->d_name, "em") || + STRPREFIX (d->d_name, "en") || + STRPREFIX (d->d_name, "eth") || + STRPREFIX (d->d_name, "wl")) { + nr_interfaces++; + all_interfaces = + realloc (all_interfaces, sizeof (char *) * (nr_interfaces + 1)); + if (!all_interfaces) { + perror ("realloc"); + exit (EXIT_FAILURE); + } + all_interfaces[nr_interfaces-1] = strdup (d->d_name); + all_interfaces[nr_interfaces] = NULL; + } + } + + /* Check readdir didn't fail */ + if (errno != 0) { + perror ("readdir: /sys/class/net"); + exit (EXIT_FAILURE); + } + + /* Close the directory handle */ + if (closedir (dir) == -1) { + perror ("closedir: /sys/class/net"); + exit (EXIT_FAILURE); + } + + if (all_interfaces) + qsort (all_interfaces, nr_interfaces, sizeof (char *), compare); +} + +/* Read /proc/cmdline. */ +static char * +read_cmdline (void) +{ + int fd; + size_t len = 0; + ssize_t n; + char buf[256]; + char *r = NULL, *newr; + + fd = open ("/proc/cmdline", O_RDONLY|O_CLOEXEC); + if (fd == -1) { + perror ("/proc/cmdline"); + return NULL; + } + + for (;;) { + n = read (fd, buf, sizeof buf); + if (n == -1) { + perror ("read"); + free (r); + close (fd); + return NULL; + } + if (n == 0) + break; + newr = realloc (r, len + n + 1); /* + 1 is for terminating NUL */ + if (newr == NULL) { + perror ("realloc"); + free (r); + close (fd); + return NULL; + } + r = newr; + memcpy (&r[len], buf, n); + len += n; + } + + if (r) + r[len] = '\0'; + + if (close (fd) == -1) { + perror ("close"); + free (r); + return NULL; + } + + return r; +} diff --git a/p2v/miniexpect.c b/p2v/miniexpect.c new file mode 100644 index 000000000..9e9cc0ee3 --- /dev/null +++ b/p2v/miniexpect.c @@ -0,0 +1,379 @@ +/* miniexpect + * Copyright (C) 2014 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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "miniexpect.h" + +#define DEBUG 0 + +static mexp_h * +create_handle (void) +{ + mexp_h *h = malloc (sizeof *h); + if (h == NULL) + return NULL; + + /* Initialize the fields to default values. */ + h->fd = -1; + h->pid = 0; + h->timeout = 60000; + h->read_size = 1024; + h->pcre_error = 0; + h->buffer = NULL; + h->len = h->alloc = 0; + h->next_match = -1; + h->user1 = h->user2 = h->user3 = NULL; + + return h; +} + +static void +clear_buffer (mexp_h *h) +{ + free (h->buffer); + h->buffer = NULL; + h->alloc = h->len = 0; + h->next_match = -1; +} + +int +mexp_close (mexp_h *h) +{ + int status = 0; + + free (h->buffer); + + if (h->fd >= 0) + close (h->fd); + if (h->pid > 0) { + if (waitpid (h->pid, &status, 0) == -1) + return -1; + } + + free (h); + + return status; +} + +mexp_h * +mexp_spawnl (const char *file, const char *arg, ...) +{ + char **argv, **new_argv; + size_t i; + va_list args; + mexp_h *h; + + argv = malloc (sizeof (char *)); + if (argv == NULL) + return NULL; + argv[0] = (char *) arg; + + va_start (args, arg); + for (i = 1; arg != NULL; ++i) { + arg = va_arg (args, const char *); + new_argv = realloc (argv, sizeof (char *) * (i+1)); + if (new_argv == NULL) { + free (argv); + return NULL; + } + argv = new_argv; + argv[i] = (char *) arg; + } + + h = mexp_spawnv (file, argv); + free (argv); + return h; +} + +mexp_h * +mexp_spawnv (const char *file, char **argv) +{ + mexp_h *h; + int fd = -1; + int err; + char slave[1024]; + pid_t pid = 0; + + fd = posix_openpt (O_RDWR|O_NOCTTY); + if (fd == -1) + goto error; + + if (grantpt (fd) == -1) + goto error; + + if (unlockpt (fd) == -1) + goto error; + + /* Get the slave pty name now, but don't open it in the parent. */ + if (ptsname_r (fd, slave, sizeof slave) != 0) + goto error; + + /* Create the handle last before we fork. */ + h = create_handle (); + if (h == NULL) + goto error; + + pid = fork (); + if (pid == -1) + goto error; + + if (pid == 0) { /* Child. */ + struct termios terminal_settings; + int slave_fd; + + setsid (); + + /* Open the slave side of the pty. We must do this in the child + * after setsid so it becomes our controlling tty. + */ + slave_fd = open (slave, O_RDWR); + if (slave_fd == -1) + goto error; + + /* Set raw mode. */ + tcgetattr (slave_fd, &terminal_settings); + cfmakeraw (&terminal_settings); + tcsetattr (slave_fd, TCSANOW, &terminal_settings); + + /* Set up stdin, stdout, stderr to point to the pty. */ + dup2 (slave_fd, 0); + dup2 (slave_fd, 1); + dup2 (slave_fd, 2); + close (slave_fd); + + /* Close the master side of the pty - do this late to avoid a + * kernel bug, see sshpass source code. + */ + close (fd); + + /* Run the subprocess. */ + execvp (file, argv); + perror (file); + _exit (EXIT_FAILURE); + } + + /* Parent. */ + + h->fd = fd; + h->pid = pid; + return h; + + error: + err = errno; + if (fd >= 0) + close (fd); + if (pid > 0) + waitpid (pid, NULL, 0); + errno = err; + return NULL; +} + +enum mexp_status +mexp_expect (mexp_h *h, const mexp_regexp *regexps, int *ovector, int ovecsize) +{ + time_t start_t, now_t; + int timeout; + struct pollfd pfds[1]; + int r; + ssize_t rs; + + time (&start_t); + + if (h->next_match == -1) { + /* Fully clear the buffer, then read. */ + clear_buffer (h); + } else { + /* See the comment in the manual about h->next_match. We have + * some data remaining in the buffer, so begin by matching that. + */ + memmove (&h->buffer[0], &h->buffer[h->next_match], h->len - h->next_match); + h->len -= h->next_match; + h->buffer[h->len] = '\0'; + h->next_match = -1; + goto try_match; + } + + for (;;) { + /* If we've got a timeout then work out how many seconds are left. + * Timeout == 0 is not particularly well-defined, but it probably + * means "return immediately if there's no data to be read". + */ + if (h->timeout >= 0) { + time (&now_t); + timeout = h->timeout - ((now_t - start_t) * 1000); + if (timeout < 0) + timeout = 0; + } + else + timeout = 0; + + pfds[0].fd = h->fd; + pfds[0].events = POLLIN; + pfds[0].revents = 0; + r = poll (pfds, 1, timeout); +#if DEBUG + fprintf (stderr, "DEBUG: poll returned %d\n", r); +#endif + if (r == -1) + return MEXP_ERROR; + + if (r == 0) + return MEXP_TIMEOUT; + + /* Otherwise we expect there is something to read from the file + * descriptor. + */ + if (h->alloc - h->len <= h->read_size) { + char *new_buffer; + /* +1 here allows us to store \0 after the data read */ + new_buffer = realloc (h->buffer, h->alloc + h->read_size + 1); + if (new_buffer == NULL) + return MEXP_ERROR; + h->buffer = new_buffer; + h->alloc += h->read_size; + } + rs = read (h->fd, h->buffer + h->len, h->read_size); +#if DEBUG + fprintf (stderr, "DEBUG: read returned %zd\n", rs); +#endif + if (rs == -1) { + /* Annoyingly on Linux (I'm fairly sure this is a bug) if the + * writer closes the connection, the entire pty is destroyed, + * and read returns -1 / EIO. Handle that special case here. + */ + if (errno == EIO) + return MEXP_EOF; + return MEXP_ERROR; + } + if (rs == 0) + return MEXP_EOF; + + /* We read something. */ + h->len += rs; + h->buffer[h->len] = '\0'; +#if DEBUG + fprintf (stderr, "DEBUG: read %zd bytes from pty\n", rs); + fprintf (stderr, "DEBUG: buffer content: %s\n", h->buffer); +#endif + + try_match: + /* See if there is a full or partial match against any regexp. */ + if (regexps) { + size_t i; + int can_clear_buffer = 1; + + assert (h->buffer != NULL); + + for (i = 0; regexps[i].r > 0; ++i) { + int options = regexps[i].options | PCRE_PARTIAL_SOFT; + + r = pcre_exec (regexps[i].re, regexps[i].extra, + h->buffer, (int)h->len, 0, + options, + ovector, ovecsize); + h->pcre_error = r; + + if (r >= 0) { + /* A full match. */ + if (ovector != NULL && ovecsize >= 1 && ovector[1] >= 0) + h->next_match = ovector[1]; + else + h->next_match = -1; + return regexps[i].r; + } + + else if (r == PCRE_ERROR_NOMATCH) { + /* No match at all. */ + /* (nothing here) */ + } + + else if (r == PCRE_ERROR_PARTIAL) { + /* Partial match. Keep the buffer and keep reading. */ + can_clear_buffer = 0; + } + + else { + /* An actual PCRE error. */ + return MEXP_PCRE_ERROR; + } + } + + /* If none of the regular expressions matched (not partially) + * then we can clear the buffer. This is an optimization. + */ + if (can_clear_buffer) + clear_buffer (h); + + } /* if (regexps) */ + } +} + +int +mexp_printf (mexp_h *h, const char *fs, ...) +{ + va_list args; + char *msg; + int len; + size_t n; + ssize_t r; + char *p; + + va_start (args, fs); + len = vasprintf (&msg, fs, args); + va_end (args); + + if (len < 0) + return -1; + +#if DEBUG + fprintf (stderr, "DEBUG: writing: %s\n", msg); +#endif + + n = len; + p = msg; + while (n > 0) { + r = write (h->fd, p, n); + if (r == -1) { + free (msg); + return -1; + } + n -= r; + p += r; + } + + free (msg); + return len; +} diff --git a/p2v/miniexpect.h b/p2v/miniexpect.h new file mode 100644 index 000000000..f98765573 --- /dev/null +++ b/p2v/miniexpect.h @@ -0,0 +1,82 @@ +/* miniexpect + * Copyright (C) 2014 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 + */ + +/* ** NOTE ** All API documentation is in the manual page. + * + * To read the manual page from the source directory, do: + * man ./miniexpect.3 + * If you have installed miniexpect, do: + * man 3 miniexpect + * + * The source for the manual page is miniexpect.pod. + */ + +#ifndef MINIEXPECT_H_ +#define MINIEXPECT_H_ + +#include + +#include + +/* This handle is created per subprocess that is spawned. */ +struct mexp_h { + int fd; + pid_t pid; + int timeout; + char *buffer; + size_t len; + size_t alloc; + ssize_t next_match; + size_t read_size; + int pcre_error; + void *user1; + void *user2; + void *user3; +}; +typedef struct mexp_h mexp_h; + +/* Spawn a subprocess. */ +extern mexp_h *mexp_spawnv (const char *file, char **argv); +extern mexp_h *mexp_spawnl (const char *file, const char *arg, ...); + +/* Close the handle. */ +extern int mexp_close (mexp_h *h); + +/* Expect. */ +struct mexp_regexp { + int r; + const pcre *re; + const pcre_extra *extra; + int options; +}; +typedef struct mexp_regexp mexp_regexp; + +enum mexp_status { + MEXP_EOF = 0, + MEXP_ERROR = -1, + MEXP_PCRE_ERROR = -2, + MEXP_TIMEOUT = -3, +}; + +extern int mexp_expect (mexp_h *h, const mexp_regexp *regexps, + int *ovector, int ovecsize); + +extern int mexp_printf (mexp_h *h, const char *fs, ...) + __attribute__((format(printf,2,3))); + +#endif /* MINIEXPECT_H_ */ diff --git a/p2v/p2v.h b/p2v/p2v.h new file mode 100644 index 000000000..8e5ec4372 --- /dev/null +++ b/p2v/p2v.h @@ -0,0 +1,103 @@ +/* virt-p2v + * Copyright (C) 2009-2014 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. + */ + +#ifndef P2V_H +#define P2V_H + +/* Send various debug information to stderr. Harmless and useful, so + * can be left enabled in production builds. + */ +#define DEBUG_STDERR 1 + +/* Force remote debugging even if user doesn't enable it. Since + * remote debugging is mostly free, we might as well enable this even + * in production. + */ +#define FORCE_REMOTE_DEBUG 1 + +#include "miniexpect.h" + +/* We don't use libguestfs directly here, and we don't link to it + * either (in fact, we don't want libguestfs on the ISO). However + * we include this just so that we can use the convenience macros in + * guestfs-internal-frontend.h. + */ +#include "guestfs.h" +#include "guestfs-internal-frontend.h" + +/* Ensure we don't use libguestfs. */ +#define guestfs_h DO_NOT_USE + +/* All disks / removable media / network interfaces discovered + * when the program started. Do not change these. + */ +extern char **all_disks; +extern char **all_removable; +extern char **all_interfaces; + +/* config.c */ +struct config { + int verbose; + char *server; + int port; + char *username; + char *password; + int sudo; + char *guestname; + int vcpus; + uint64_t memory; + char **disks; + char **removable; + char **interfaces; +}; + +extern struct config *new_config (void); +extern struct config *copy_config (struct config *); +extern void free_config (struct config *); + +/* kernel.c */ +extern void kernel_configuration (struct config *, const char *cmdline); + +/* gui.c */ +extern void gui_application (struct config *); + +/* conversion.c */ +extern int start_conversion (struct config *, void (*notify_ui) (int type, const char *data)); +#define NOTIFY_LOG_DIR 1 /* location of remote log directory */ +#define NOTIFY_REMOTE_MESSAGE 2 /* log message from remote virt-v2v */ +#define NOTIFY_STATUS 3 /* stage in conversion process */ +extern const char *get_conversion_error (void); + +/* ssh.c */ +extern int test_connection (struct config *); +extern mexp_h *open_data_connection (struct config *, int *local_port, int *remote_port); +extern mexp_h *start_remote_connection (struct config *, const char *remote_dir, const char *libvirt_xml); +extern const char *get_ssh_error (void); + +/* virt-v2v version and features (read from remote). */ +extern int v2v_major; +extern int v2v_minor; +extern int v2v_release; + +/* authors.c */ +extern const char *authors[]; + +/* copying.c */ +extern const char *gplv2plus; + +#endif /* P2V_H */ diff --git a/p2v/ssh.c b/p2v/ssh.c new file mode 100644 index 000000000..1e9b05c7d --- /dev/null +++ b/p2v/ssh.c @@ -0,0 +1,633 @@ +/* virt-p2v + * Copyright (C) 2009-2014 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. + */ + +/* This file handles the ssh connections to the conversion server. + * + * virt-p2v will open several connections over the lifetime of + * the conversion process. + * + * In 'test_connection', it will first open a connection (to check it + * is possible) and query virt-v2v on the server to ensure it exists, + * it is the right version, and so on. This connection is then + * closed, because in the GUI case we don't want to deal with keeping + * it alive in case the administrator has set up an autologout. + * + * Once we start conversion, we will open a control connection to send + * the libvirt configuration data and to start up virt-v2v, and we + * will open up one data connection per local hard disk. The data + * connection(s) have a reverse port forward to the local qemu-nbd + * server which is serving the content of that hard disk. The remote + * port for each data connection is assigned by ssh. See + * 'open_data_connection' and 'start_remote_conversion'. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ignore-value.h" + +#include "miniexpect.h" +#include "p2v.h" + +int v2v_major; +int v2v_minor; +int v2v_release; + +static char *ssh_error; + +static void set_ssh_error (const char *fs, ...) + __attribute__((format(printf,1,2))); + +static void +set_ssh_error (const char *fs, ...) +{ + va_list args; + char *msg; + int len; + + va_start (args, fs); + len = vasprintf (&msg, fs, args); + va_end (args); + + if (len < 0) { + perror ("vasprintf"); + fprintf (stderr, "original error format string: %s\n", fs); + exit (EXIT_FAILURE); + } + + free (ssh_error); + ssh_error = msg; +} + +const char * +get_ssh_error (void) +{ + return ssh_error; +} + +static void compile_regexps (void) __attribute__((constructor)); +static void free_regexps (void) __attribute__((destructor)); + +static pcre *password_re; +static pcre *prompt_re; +static pcre *version_re; +static pcre *libguestfs_rewrite_re; +static pcre *portfwd_re; + +static void +compile_regexps (void) +{ + const char *err; + int offset; + +#define COMPILE(re,pattern,options) \ + do { \ + re = pcre_compile ((pattern), (options), &err, &offset, NULL); \ + if (re == NULL) { \ + ignore_value (write (2, err, strlen (err))); \ + abort (); \ + } \ + } while (0) + + COMPILE (password_re, "assword", 0); + /* The magic synchronization strings all match this expression. See + * start_ssh function below. + */ + COMPILE (prompt_re, "###([0123456789abcdefghijklmnopqrstuvwxyz]{8})### ", 0); + COMPILE (version_re, + "virt-v2v ([1-9]\\d*)\\.([1-9]\\d*)\\.([1-9]\\d*)", 0); + COMPILE (libguestfs_rewrite_re, "libguestfs-rewrite", 0); + COMPILE (portfwd_re, "Allocated port (\\d+) for remote forward", 0); +} + +static void +free_regexps (void) +{ + pcre_free (password_re); + pcre_free (prompt_re); + pcre_free (version_re); + pcre_free (libguestfs_rewrite_re); + pcre_free (portfwd_re); +} + +/* Start ssh subprocess with the standard arguments and possibly some + * optional arguments. Also handles password authentication. + */ +static mexp_h * +start_ssh (struct config *config, char **extra_args, int wait_prompt) +{ + size_t i, j, nr_args, count; + char port_str[64]; + CLEANUP_FREE /* [sic] */ const char **args = NULL; + mexp_h *h; + const int ovecsize = 12; + int ovector[ovecsize]; + int saved_timeout; + + /* Create the ssh argument array. */ + nr_args = 0; + if (extra_args != NULL) + nr_args = guestfs___count_strings (extra_args); + + nr_args += 11; + args = malloc (sizeof (char *) * nr_args); + if (args == NULL) { + perror ("malloc"); + exit (EXIT_FAILURE); + } + j = 0; + args[j++] = "ssh"; + args[j++] = "-p"; /* Port. */ + snprintf (port_str, sizeof port_str, "%d", config->port); + args[j++] = port_str; + args[j++] = "-l"; /* Username. */ + args[j++] = config->username ? config->username : "root"; + args[j++] = "-o"; /* Host key will always be novel. */ + args[j++] = "StrictHostKeyChecking=no"; + args[j++] = "-o"; /* Only use password authentication. */ + args[j++] = "PreferredAuthentications=keyboard-interactive,password"; + if (extra_args != NULL) { + for (i = 0; extra_args[i] != NULL; ++i) + args[j++] = extra_args[i]; + } + args[j++] = config->server; /* Conversion server. */ + args[j++] = NULL; + assert (j == nr_args); + + h = mexp_spawnv ("ssh", (char **) args); + if (h == NULL) + return NULL; + + if (config->password && strlen (config->password) > 0) { + /* Wait for the password prompt. */ + switch (mexp_expect (h, + (mexp_regexp[]) { + { 100, .re = password_re }, + { 0 } + }, ovector, ovecsize)) { + case 100: /* Got password prompt. */ + if (mexp_printf (h, "%s\n", config->password) == -1) { + set_ssh_error ("mexp_printf: %m"); + mexp_close (h); + return NULL; + } + break; + + case MEXP_EOF: + mexp_close (h); + set_ssh_error ("unexpected end of file waiting for password prompt"); + return NULL; + + case MEXP_TIMEOUT: + mexp_close (h); + set_ssh_error ("timeout waiting for password prompt"); + return NULL; + + case MEXP_ERROR: + set_ssh_error ("mexp_expect: %m"); + mexp_close (h); + return NULL; + + case MEXP_PCRE_ERROR: + set_ssh_error ("PCRE error: %d\n", h->pcre_error); + mexp_close (h); + return NULL; + } + } + + if (!wait_prompt) + return h; + + /* Synchronize with the command prompt and set it to a known string. */ + + /* Note that we cannot control the initial prompt. It would involve + * changing the remote SSH configuration (AcceptEnv). However what + * we can do is to repeatedly send 'export PS1=' commands + * until we synchronize with the remote shell. + */ + saved_timeout = h->timeout; + h->timeout = 2000; + + for (count = 0; count < 30; ++count) { + char magic[9]; + const char *matched; + int r; + + if (guestfs___random_string (magic, 8) == -1) { + set_ssh_error ("random_string: %m"); + mexp_close (h); + return NULL; + } + + /* The purpose of the '' inside the string is to ensure we don't + * mistake the command echo for the prompt. + */ + if (mexp_printf (h, "export PS1='###''%s''### '\n", magic) == -1) { + set_ssh_error ("random_string: %m"); + mexp_close (h); + return NULL; + } + + /* Wait for the prompt. */ + wait_again: + switch (mexp_expect (h, + (mexp_regexp[]) { + { 100, .re = password_re }, + { 101, .re = prompt_re }, + { 0 } + }, ovector, ovecsize)) { + case 100: /* Got password prompt unexpectedly. */ + if (mexp_printf (h, "%s\n", config->password) == -1) { + mexp_close (h); + set_ssh_error ("unexpected password prompt: probably the password supplied is wrong"); + return NULL; + } + break; + + case 101: + /* Got a prompt. However it might be an earlier prompt. If it + * doesn't match the PS1 string we sent, then repeat the expect. + */ + r = pcre_get_substring (h->buffer, ovector, h->pcre_error, 1, &matched); + if (r < 0) { + fprintf (stderr, "error: PCRE error reading substring (%d)\n", r); + exit (EXIT_FAILURE); + } + r = STREQ (magic, matched); + pcre_free_substring (matched); + if (!r) + goto wait_again; + goto got_prompt; + + case MEXP_EOF: + mexp_close (h); + set_ssh_error ("unexpected end of file waiting for command prompt"); + return NULL; + + case MEXP_TIMEOUT: + /* Timeout here is not an error, since ssh may "eat" commands that + * we send before the shell at the other end is ready. Just loop. + */ + break; + + case MEXP_ERROR: + set_ssh_error ("mexp_expect: %m"); + mexp_close (h); + return NULL; + + case MEXP_PCRE_ERROR: + set_ssh_error ("PCRE error: %d\n", h->pcre_error); + mexp_close (h); + return NULL; + } + } + + mexp_close (h); + set_ssh_error ("failed to synchronize with remote shell after 60 seconds"); + return NULL; + + got_prompt: + h->timeout = saved_timeout; + + return h; +} + +#pragma GCC diagnostic ignored "-Wsuggest-attribute=noreturn" /* WTF? */ +int +test_connection (struct config *config) +{ + mexp_h *h; + CLEANUP_FREE char *major_str = NULL, *minor_str = NULL, *release_str = NULL; + int feature_libguestfs_rewrite = 0; + int status; + const int ovecsize = 12; + int ovector[ovecsize]; + + h = start_ssh (config, NULL, 1); + if (h == NULL) + return -1; + + /* Send 'virt-v2v -V' command and hope we get back a version string. */ + if (mexp_printf (h, "%svirt-v2v -V\n", config->sudo ? "sudo " : "") == -1) { + set_ssh_error ("mexp_printf: %m"); + mexp_close (h); + return -1; + } + + switch (mexp_expect (h, + (mexp_regexp[]) { + { 100, .re = version_re }, + { 101, .re = prompt_re }, + { 0 } + }, ovector, ovecsize)) { + case 100: /* Got version string. */ + major_str = strndup (&h->buffer[ovector[2]], ovector[3]-ovector[2]); + minor_str = strndup (&h->buffer[ovector[4]], ovector[5]-ovector[4]); + release_str = strndup (&h->buffer[ovector[6]], ovector[7]-ovector[6]); + sscanf (major_str, "%d", &v2v_major); + sscanf (minor_str, "%d", &v2v_minor); + sscanf (release_str, "%d", &v2v_release); +#if DEBUG_STDERR + fprintf (stderr, "%s: remote virt-v2v version: %d.%d.%d\n", + program_name, v2v_major, v2v_minor, v2v_release); +#endif + if (v2v_major < 1 || v2v_major > 1) { + mexp_close (h); + set_ssh_error ("invalid version major (%d)", v2v_major); + return -1; + } + break; + + case 101: /* Got the prompt, but no version string. */ + mexp_close (h); + set_ssh_error ("virt-v2v is not installed on the conversion server, " + "or it might be a too old version"); + return -1; + + case MEXP_EOF: + mexp_close (h); + set_ssh_error ("unexpected end of file waiting virt-v2v -V output"); + return -1; + + case MEXP_TIMEOUT: + mexp_close (h); + set_ssh_error ("timeout waiting for virt-v2v -V output"); + return -1; + + case MEXP_ERROR: + set_ssh_error ("mexp_expect: %m"); + mexp_close (h); + return -1; + + case MEXP_PCRE_ERROR: + set_ssh_error ("PCRE error: %d\n", h->pcre_error); + mexp_close (h); + return -1; + } + + /* Get virt-v2v features. See: v2v/cmdline.ml */ + if (mexp_printf (h, "%svirt-v2v --machine-readable\n", + config->sudo ? "sudo " : "") == -1) { + set_ssh_error ("mexp_printf: %m"); + mexp_close (h); + return -1; + } + + switch (mexp_expect (h, + (mexp_regexp[]) { + { 100, .re = libguestfs_rewrite_re }, + { 0 } + }, ovector, ovecsize)) { + case 100: /* Got feature: libguestfs-rewrite. */ + feature_libguestfs_rewrite = 1; + break; + + case MEXP_EOF: + mexp_close (h); + set_ssh_error ("unexpected end of file waiting virt-v2v --machine-readable output"); + return -1; + + case MEXP_TIMEOUT: + mexp_close (h); + set_ssh_error ("timeout waiting virt-v2v --machine-readable output"); + return -1; + + case MEXP_ERROR: + set_ssh_error ("mexp_expect: %m"); + mexp_close (h); + return -1; + + case MEXP_PCRE_ERROR: + set_ssh_error ("PCRE error: %d\n", h->pcre_error); + mexp_close (h); + return -1; + } + + if (!feature_libguestfs_rewrite) { + mexp_close (h); + set_ssh_error ("invalid output of virt-v2v --machine-readable command"); + return -1; + } + + /* Test finished, shut down ssh. */ + if (mexp_printf (h, "exit\n") == -1) { + set_ssh_error ("mexp_printf: %m"); + mexp_close (h); + return -1; + } + + switch (mexp_expect (h, NULL, NULL, 0)) { + case MEXP_EOF: + break; + + case MEXP_TIMEOUT: + mexp_close (h); + set_ssh_error ("timeout waiting for end of ssh session"); + return -1; + + case MEXP_ERROR: + set_ssh_error ("mexp_expect: %m"); + mexp_close (h); + return -1; + + case MEXP_PCRE_ERROR: + set_ssh_error ("PCRE error: %d\n", h->pcre_error); + mexp_close (h); + return -1; + } + + status = mexp_close (h); + if (!((WIFEXITED (status) && WEXITSTATUS (status) == 0) + || (WIFSIGNALED (status) && WTERMSIG (status) == SIGHUP))) { + set_ssh_error ("unexpected close status from ssh subprocess (%d)", status); + return -1; + } + + return 0; +} + +/* The p2v ISO should allow us to open up just about any port. */ +static int nbd_local_port = 50123; + +mexp_h * +open_data_connection (struct config *config, int *local_port, int *remote_port) +{ + mexp_h *h; + char remote_arg[32]; + const char *extra_args[] = { + "-R", remote_arg, + "-N", + NULL + }; + CLEANUP_FREE char *port_str = NULL; + const int ovecsize = 12; + int ovector[ovecsize]; + + snprintf (remote_arg, sizeof remote_arg, "0:localhost:%d", nbd_local_port); + *local_port = nbd_local_port; + nbd_local_port++; + + h = start_ssh (config, (char **) extra_args, 0); + if (h == NULL) + return NULL; + + switch (mexp_expect (h, + (mexp_regexp[]) { + { 100, .re = portfwd_re }, + { 0 } + }, ovector, ovecsize)) { + case 100: /* Ephemeral port. */ + port_str = strndup (&h->buffer[ovector[2]], ovector[3]-ovector[2]); + sscanf (port_str, "%d", remote_port); + break; + + case MEXP_EOF: + mexp_close (h); + set_ssh_error ("unexpected end of file waiting ssh -R output"); + return NULL; + + case MEXP_TIMEOUT: + mexp_close (h); + set_ssh_error ("timeout waiting for ssh -R output"); + return NULL; + + case MEXP_ERROR: + set_ssh_error ("mexp_expect: %m"); + mexp_close (h); + return NULL; + + case MEXP_PCRE_ERROR: + set_ssh_error ("PCRE error: %d\n", h->pcre_error); + mexp_close (h); + return NULL; + } + + return h; +} + +/* Wait for the prompt. */ +static int +wait_for_prompt (mexp_h *h) +{ + const int ovecsize = 12; + int ovector[ovecsize]; + + switch (mexp_expect (h, + (mexp_regexp[]) { + { 100, .re = prompt_re }, + { 0 } + }, ovector, ovecsize)) { + case 100: /* Got the prompt. */ + return 0; + + case MEXP_EOF: + set_ssh_error ("unexpected end of file waiting for prompt"); + return -1; + + case MEXP_TIMEOUT: + set_ssh_error ("timeout waiting for prompt"); + return -1; + + case MEXP_ERROR: + set_ssh_error ("mexp_expect: %m"); + return -1; + + case MEXP_PCRE_ERROR: + set_ssh_error ("PCRE error: %d\n", h->pcre_error); + return -1; + } + + return 0; +} + +mexp_h * +start_remote_connection (struct config *config, + const char *remote_dir, const char *libvirt_xml) +{ + mexp_h *h; + char magic[9]; + + if (guestfs___random_string (magic, 8) == -1) { + perror ("random_string"); + return NULL; + } + + h = start_ssh (config, NULL, 1); + if (h == NULL) + return NULL; + + /* Create the remote directory. */ + if (mexp_printf (h, "mkdir %s\n", remote_dir) == -1) { + set_ssh_error ("mexp_printf: %m"); + goto error; + } + + if (wait_for_prompt (h) == -1) + goto error; + + /* Write some useful config information to files in the remote directory. */ + if (mexp_printf (h, "echo '%s' > %s/name\n", + config->guestname, remote_dir) == -1) { + set_ssh_error ("mexp_printf: %m"); + goto error; + } + + if (wait_for_prompt (h) == -1) + goto error; + + if (mexp_printf (h, "date > %s/time\n", remote_dir) == -1) { + set_ssh_error ("mexp_printf: %m"); + goto error; + } + + if (wait_for_prompt (h) == -1) + goto error; + + /* Upload the libvirt configuration file to the remote directory. */ + if (mexp_printf (h, + "cat > '%s/libvirt.conf' << '__%s__'\n" + "%s" + "__%s__\n", + remote_dir, magic, + libvirt_xml, + magic) == -1) { + set_ssh_error ("mexp_printf: %m"); + goto error; + } + + if (wait_for_prompt (h) == -1) + goto error; + + return h; + + error: + mexp_close (h); + return NULL; +} diff --git a/p2v/virt-p2v.pod b/p2v/virt-p2v.pod new file mode 100644 index 000000000..0837abd11 --- /dev/null +++ b/p2v/virt-p2v.pod @@ -0,0 +1,219 @@ +=head1 NAME + +virt-p2v - Convert a physical machine to use KVM + +=head1 SYNOPSIS + + virt-p2v + + virt-p2v.iso + +=head1 DESCRIPTION + +Virt-p2v converts a physical machine to run virtualized on KVM, +managed by libvirt or Red Hat Enterprise Virtualisation (RHEV) version +2.2 or later. + +Normally you don't run the virt-p2v program directly. Instead you +have to boot the physical machine using the bootable CD-ROM, ISO or +PXE image. This bootable image contains the virt-p2v binary and runs +it automatically. This manual page documents both the binary and the +bootable image. + +=head1 NETWORK SETUP + +Virt-p2v runs on the physical machine which you want to convert. It +has to talk to another server called the "conversion server" which +must have L installed on it. It always talks to the +conversion server over SSH: + + +-----------+ +-------------+ + | virt-p2v | | virt-v2v | + | (physical | ssh connection | (conversion | + | server) -----------------> server) | + +-----------+ +-------------+ + +The virt-v2v program on the conversion server does the actual +conversion (physical to virtual, and virtual to virtual conversions +are sufficiently similar that we use the same program to do both). + +The SSH connection is always initiated from the physical server. All +data is transferred over the SSH connection. In terms of firewall and +network configuration, you only need to ensure that the physical +server has access to a port (usually TCP port 22) on the conversion +server. (Note that the physical machine may reconnect several times +during the conversion process.) + +The conversion server does not need to be a physical machine. It +could be a virtual machine, as long as it has sufficient memory and +disk space to do the conversion, and as long as the physical machine +can connect directly to its SSH port. + +Because all of the data on the physical server's hard drive(s) has to +be copied over the network, the speed of conversion is largely +determined by the speed of the network between the two machines. + +=head1 GUI INTERACTIVE CONFIGURATION + +When you start virt-p2v, you'll see a graphical configuration dialog +that walks you through connection to the conversion server, asks for +the password, which local hard disks you want to convert, and other +things like the name of the guest to create and the number of virtual +CPUs to give it. + +=head1 KERNEL COMMAND LINE CONFIGURATION + +If you don't want to configure things using the graphical UI, an +alternative is to configure through the kernel command line. This is +especially convenient if you are converting a lot of physical machines +which are booted using PXE. + +Where exactly you set command line arguments depends on your PXE +implementation, but for pxelinux you put them in the C field +in the C file. For example: + + DEFAULT p2v + TIMEOUT 20 + PROMPT 0 + LABEL p2v + KERNEL virt-p2v-vmlinuz + APPEND initrd=virt-p2v-initrd p2v.server=conv.example.com p2v.password=secret + +You have to set some or all of the following command line arguments: + +=over 4 + +=item B + +The name or IP address of the conversion server. + +This is always required if you are using the kernel configuration +method. If virt-p2v does not find this on the kernel command line +then it switches to the GUI (interactive) configuration method. + +=item B + +The SSH port number on the conversion server (default: C<22>). + +=item B + +The SSH username that we log in as on the conversion server +(default: C). + +=item B + +The SSH password that we use to log in to the conversion server. + +The default is to try with no password. If this fails then virt-p2v +will ask the user to type the password (probably several times during +conversion). + +Note that virt-p2v does not support authentication using key +distribution at this time. + +=item B + +Use C to tell virt-p2v to use L to gain root +privileges on the conversion server after logging in as a non-root +user (default: do not use sudo). + +=item B + +The name of the guest that is created. The default is to try to +derive a name from the physical machine's hostname (if possible) else +use a randomly generated name. + +=item B + +The number of virtual CPUs to give to the guest. The default is to +use the same as the number of physical CPUs. + +=item B + +The size of the guest memory. You can specify this in megabytes or +gigabytes by using (eg) C or C. The +default is to use the same amount of RAM as on the physical machine. + +=item B + +Use this to enable full debugging of virt-v2v. + +If asked to diagnose a problem with virt-p2v, you should add +C to the kernel command line, and examine the log file +which is left in C on the conversion server. + +=item B + +A list of physical hard disks to convert, for example: + + p2v.disks=sda,sdc + +The default is to convert all local hard disks that are found. + +=item B + +A list of removable media to convert. The default is to create +virtual removable devices for every physical removable device found. +Note that the content of removable media is never copied over. + +=item B + +A list of network interfaces to convert. The default is to create +virtual network interfaces for every physical network interface found. + +=item B + +Use DHCP for configuring the network interface (this is the default). + +=begin comment + +=item B + +Set up a static IPv4 network configuration. + +=end comment + +=back + +=head1 OPTIONS + +=over 4 + +=item B<--help> + +Display help. + +=item B<--cmdline=CMDLINE> + +This is used for debugging. Instead of parsing the kernel command line +from C, parse the string parameter C. + +=item B<-v> + +=item B<--verbose> + +Enable debugging (on the conversion server). + +=item B<-V> + +=item B<--version> + +Display version number and exit. + +=back + +=head1 SEE ALSO + +L, +L, +L. + +=head1 AUTHORS + +Richard W.M. Jones L + +Matthew Booth + +=head1 COPYRIGHT + +Copyright (C) 2009-2014 Red Hat Inc. diff --git a/po/POTFILES b/po/POTFILES index 3a758f0f3..7d4a0c88d 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -249,6 +249,15 @@ mllib/tty-c.c mllib/uri-c.c ocaml/guestfs-c-actions.c ocaml/guestfs-c.c +p2v/authors.c +p2v/config.c +p2v/conversion.c +p2v/copying.c +p2v/gui.c +p2v/kernel.c +p2v/main.c +p2v/miniexpect.c +p2v/ssh.c perl/Guestfs.c perl/bindtests.pl perl/lib/Sys/Guestfs.pm diff --git a/src/guestfs.pod b/src/guestfs.pod index eb05404fe..55e556a79 100644 --- a/src/guestfs.pod +++ b/src/guestfs.pod @@ -4370,6 +4370,10 @@ L command and documentation. Various libraries and common code used by L and the other tools which are written in OCaml. +=item C + +L command and documentation. + =item C Translations of simple gettext strings. @@ -4770,6 +4774,7 @@ L, L, L, L, +L, L, L, L, diff --git a/src/utils.c b/src/utils.c index 201ca6bd9..be7f64340 100644 --- a/src/utils.c +++ b/src/utils.c @@ -242,6 +242,7 @@ guestfs___random_string (char *ret, size_t len) errno = saved_errno; return -1; } + /* Do not change this! */ ret[i] = "0123456789abcdefghijklmnopqrstuvwxyz"[c % 36]; } ret[len] = '\0'; diff --git a/v2v/virt-v2v.pod b/v2v/virt-v2v.pod index f7e2453ad..810bed512 100644 --- a/v2v/virt-v2v.pod +++ b/v2v/virt-v2v.pod @@ -18,8 +18,8 @@ managed by libvirt or Red Hat Enterprise Virtualisation (RHEV) version 2.2 or later. It can currently convert Red Hat Enterprise Linux and Windows guests running on Xen and VMware ESX. -There is also a companion front-end called "virt-p2v" which comes as an -ISO or CD image that can be booted on physical machines. +There is also a companion front-end called L which comes +as an ISO or CD image that can be booted on physical machines. This manual page documents the rewritten virt-v2v included in libguestfs E 1.28. @@ -286,6 +286,7 @@ For other environment variables, see L. =head1 SEE ALSO +L, L, L, L,