Files
libguestfs/p2v/conversion.c
Richard W.M. Jones 05d4fcb64d Update copyright dates for 2019.
This command run over the source:

perl -pi.bak -e 's/(20[01][0-9])-2018/$1-2019/g' `git ls-files`
2019-01-08 11:58:30 +00:00

669 lines
19 KiB
C

/* virt-p2v
* Copyright (C) 2009-2019 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 manages the p2v conversion.
*
* The conversion is actually done by L<virt-v2v(1)> running on the
* remote conversion server. This file manages running the remote
* command and provides callbacks for displaying the output.
*
* When virt-p2v operates in GUI mode, this code runs in a separate
* thread. When virt-p2v operates in kernel mode, this runs
* synchronously in the main thread.
*/
#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <inttypes.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <error.h>
#include <locale.h>
#include <libintl.h>
#include <netdb.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include "ignore-value.h"
#include "getprogname.h"
#include "miniexpect.h"
#include "p2v.h"
static void cleanup_data_conns (struct data_conn *data_conns, size_t nr);
static void generate_name (struct config *, const char *filename);
static void generate_wrapper_script (struct config *, const char *remote_dir, const char *filename);
static void generate_system_data (const char *dmesg_file, const char *lscpu_file, const char *lspci_file, const char *lsscsi_file, const char *lsusb_file);
static void generate_p2v_version_file (const char *p2v_version_file);
static void print_quoted (FILE *fp, const char *s);
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)
error (EXIT_FAILURE, errno,
"vasprintf (original error format string: %s)", fs);
free (conversion_error);
conversion_error = msg;
}
const char *
get_conversion_error (void)
{
return conversion_error;
}
static pthread_mutex_t running_mutex = PTHREAD_MUTEX_INITIALIZER;
static int running = 0;
static pthread_mutex_t cancel_requested_mutex = PTHREAD_MUTEX_INITIALIZER;
static int cancel_requested = 0;
static mexp_h *control_h = NULL;
static int
is_running (void)
{
int r;
pthread_mutex_lock (&running_mutex);
r = running;
pthread_mutex_unlock (&running_mutex);
return r;
}
static void
set_running (int r)
{
pthread_mutex_lock (&running_mutex);
running = r;
pthread_mutex_unlock (&running_mutex);
}
static int
is_cancel_requested (void)
{
int r;
pthread_mutex_lock (&cancel_requested_mutex);
r = cancel_requested;
pthread_mutex_unlock (&cancel_requested_mutex);
return r;
}
static void
set_cancel_requested (int r)
{
pthread_mutex_lock (&cancel_requested_mutex);
cancel_requested = r;
/* Send ^C to the remote so that virt-v2v "knows" the connection has
* been cancelled. mexp_send_interrupt is a single write(2) call.
*/
if (r && control_h)
ignore_value (mexp_send_interrupt (control_h));
pthread_mutex_unlock (&cancel_requested_mutex);
}
static void
set_control_h (mexp_h *new_h)
{
pthread_mutex_lock (&cancel_requested_mutex);
control_h = new_h;
pthread_mutex_unlock (&cancel_requested_mutex);
}
#pragma GCC diagnostic ignored "-Wsuggest-attribute=noreturn"
int
start_conversion (struct config *config,
void (*notify_ui) (int type, const char *data))
{
int ret = -1;
int status;
size_t i, len;
const size_t nr_disks = guestfs_int_count_strings (config->disks);
time_t now;
struct tm tm;
CLEANUP_FREE struct data_conn *data_conns = NULL;
CLEANUP_FREE char *remote_dir = NULL;
char tmpdir[] = "/tmp/p2v.XXXXXX";
char name_file[] = "/tmp/p2v.XXXXXX/name";
char physical_xml_file[] = "/tmp/p2v.XXXXXX/physical.xml";
char wrapper_script[] = "/tmp/p2v.XXXXXX/virt-v2v-wrapper.sh";
char dmesg_file[] = "/tmp/p2v.XXXXXX/dmesg";
char lscpu_file[] = "/tmp/p2v.XXXXXX/lscpu";
char lspci_file[] = "/tmp/p2v.XXXXXX/lspci";
char lsscsi_file[] = "/tmp/p2v.XXXXXX/lsscsi";
char lsusb_file[] = "/tmp/p2v.XXXXXX/lsusb";
char p2v_version_file[] = "/tmp/p2v.XXXXXX/p2v-version";
int inhibit_fd = -1;
#if DEBUG_STDERR
print_config (config, stderr);
fprintf (stderr, "\n");
#endif
set_control_h (NULL);
set_running (1);
set_cancel_requested (0);
inhibit_fd = inhibit_power_saving ();
#ifdef DEBUG_STDERR
if (inhibit_fd == -1)
fprintf (stderr, "warning: virt-p2v cannot inhibit power saving during conversion.\n");
#endif
data_conns = malloc (sizeof (struct data_conn) * nr_disks);
if (data_conns == NULL)
error (EXIT_FAILURE, errno, "malloc");
for (i = 0; config->disks[i] != NULL; ++i) {
data_conns[i].h = NULL;
data_conns[i].nbd_pid = 0;
data_conns[i].nbd_remote_port = -1;
}
/* Start the data connections and NBD server processes, one per disk. */
for (i = 0; config->disks[i] != NULL; ++i) {
const char *nbd_local_ipaddr;
int nbd_local_port;
CLEANUP_FREE char *device = NULL;
if (config->disks[i][0] == '/') {
device = strdup (config->disks[i]);
if (!device) {
perror ("strdup");
cleanup_data_conns (data_conns, nr_disks);
exit (EXIT_FAILURE);
}
}
else if (asprintf (&device, "/dev/%s", config->disks[i]) == -1) {
perror ("asprintf");
cleanup_data_conns (data_conns, nr_disks);
exit (EXIT_FAILURE);
}
if (notify_ui) {
CLEANUP_FREE char *msg;
if (asprintf (&msg,
_("Starting local NBD server for %s ..."),
config->disks[i]) == -1)
error (EXIT_FAILURE, errno, "asprintf");
notify_ui (NOTIFY_STATUS, msg);
}
/* Start NBD server listening on the given port number. */
data_conns[i].nbd_pid =
start_nbd_server (&nbd_local_ipaddr, &nbd_local_port, device);
if (data_conns[i].nbd_pid == 0) {
set_conversion_error ("NBD server error: %s", get_nbd_error ());
goto out;
}
/* Wait for NBD server to start up and listen. */
if (wait_for_nbd_server_to_start (nbd_local_ipaddr, nbd_local_port) == -1) {
set_conversion_error ("NBD server error: %s", get_nbd_error ());
goto out;
}
if (notify_ui) {
CLEANUP_FREE char *msg;
if (asprintf (&msg,
_("Opening data connection for %s ..."),
config->disks[i]) == -1)
error (EXIT_FAILURE, errno, "asprintf");
notify_ui (NOTIFY_STATUS, msg);
}
/* Open the SSH data connection, with reverse port forwarding
* back to the NBD server.
*/
data_conns[i].h = open_data_connection (config,
nbd_local_ipaddr, 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 DEBUG_STDERR
fprintf (stderr,
"%s: data connection for %s: SSH remote port %d, local port %s:%d\n",
getprogname (), device,
data_conns[i].nbd_remote_port,
nbd_local_ipaddr, 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_int_random_string (&remote_dir[len-8], 8);
if (notify_ui)
notify_ui (NOTIFY_LOG_DIR, remote_dir);
/* Generate the local temporary directory. */
if (mkdtemp (tmpdir) == NULL) {
perror ("mkdtemp");
cleanup_data_conns (data_conns, nr_disks);
exit (EXIT_FAILURE);
}
memcpy (name_file, tmpdir, strlen (tmpdir));
memcpy (physical_xml_file, tmpdir, strlen (tmpdir));
memcpy (wrapper_script, tmpdir, strlen (tmpdir));
memcpy (dmesg_file, tmpdir, strlen (tmpdir));
memcpy (lscpu_file, tmpdir, strlen (tmpdir));
memcpy (lspci_file, tmpdir, strlen (tmpdir));
memcpy (lsscsi_file, tmpdir, strlen (tmpdir));
memcpy (lsusb_file, tmpdir, strlen (tmpdir));
memcpy (p2v_version_file, tmpdir, strlen (tmpdir));
/* Generate the static files. */
generate_name (config, name_file);
generate_physical_xml (config, data_conns, physical_xml_file);
generate_wrapper_script (config, remote_dir, wrapper_script);
generate_system_data (dmesg_file,
lscpu_file, lspci_file, lsscsi_file, lsusb_file);
generate_p2v_version_file (p2v_version_file);
/* Open the control connection. This also creates remote_dir. */
if (notify_ui)
notify_ui (NOTIFY_STATUS, _("Setting up the control connection ..."));
set_control_h (start_remote_connection (config, remote_dir));
if (control_h == NULL) {
set_conversion_error ("could not open control connection over SSH to the conversion server: %s",
get_ssh_error ());
goto out;
}
/* Copy the static files to the remote dir. */
/* These three files must not fail, so check for errors here. */
if (scp_file (config, remote_dir,
name_file, physical_xml_file, wrapper_script, NULL) == -1) {
set_conversion_error ("scp: %s: %s",
remote_dir, get_ssh_error ());
goto out;
}
/* It's not essential that these files are copied, so ignore errors. */
ignore_value (scp_file (config, remote_dir,
dmesg_file, lscpu_file, lspci_file, lsscsi_file,
lsusb_file, p2v_version_file, NULL));
/* Do the conversion. This runs until virt-v2v exits. */
if (notify_ui)
notify_ui (NOTIFY_STATUS, _("Doing conversion ..."));
if (mexp_printf (control_h,
/* To simplify things in the wrapper script, it
* writes virt-v2v's exit status to
* /remote_dir/status, and here we read that and
* exit the ssh shell with the same status.
*/
"%s/virt-v2v-wrapper.sh; "
"exit $(< %s/status)\n",
remote_dir, remote_dir) == -1) {
set_conversion_error ("mexp_printf: virt-v2v: %m");
goto out;
}
/* Read output from the virt-v2v process and echo it through the
* notify function, until virt-v2v closes the connection.
*/
while (!is_cancel_requested ()) {
char buf[257];
ssize_t r;
r = read (mexp_get_fd (control_h), buf, sizeof buf - 1);
if (r == -1) {
/* See comment about this in miniexpect.c. */
if (errno == EIO)
break; /* EOF */
set_conversion_error ("read: %m");
goto out;
}
if (r == 0)
break; /* EOF */
buf[r] = '\0';
if (notify_ui)
notify_ui (NOTIFY_REMOTE_MESSAGE, buf);
}
if (is_cancel_requested ()) {
set_conversion_error ("cancelled by user");
if (notify_ui)
notify_ui (NOTIFY_STATUS, _("Conversion cancelled by user."));
goto out;
}
if (notify_ui)
notify_ui (NOTIFY_STATUS, _("Control connection closed by remote."));
ret = 0;
out:
if (control_h) {
mexp_h *h = control_h;
set_control_h (NULL);
status = mexp_close (h);
if (status == -1) {
set_conversion_error ("mexp_close: %m");
ret = -1;
}
else if (ret == 0 &&
WIFEXITED (status) &&
WEXITSTATUS (status) != 0) {
set_conversion_error ("virt-v2v exited with status %d",
WEXITSTATUS (status));
ret = -1;
}
}
cleanup_data_conns (data_conns, nr_disks);
if (inhibit_fd >= 0)
close (inhibit_fd);
set_running (0);
return ret;
}
int
conversion_is_running (void)
{
return is_running ();
}
void
cancel_conversion (void)
{
set_cancel_requested (1);
}
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 (mexp_get_pid (data_conns[i].h), SIGHUP);
mexp_close (data_conns[i].h);
}
if (data_conns[i].nbd_pid > 0) {
/* Kill NBD process and clean up. */
kill (data_conns[i].nbd_pid, SIGTERM);
waitpid (data_conns[i].nbd_pid, NULL, 0);
}
}
}
/**
* Write the guest name into C<filename>.
*/
static void
generate_name (struct config *config, const char *filename)
{
FILE *fp;
fp = fopen (filename, "w");
if (fp == NULL)
error (EXIT_FAILURE, errno, "fopen: %s", filename);
fprintf (fp, "%s\n", config->guestname);
fclose (fp);
}
/**
* Construct the virt-v2v wrapper script.
*
* This will be sent to the remote server, and is easier than trying
* to "type" a long and complex single command line into the ssh
* connection when we start the conversion.
*/
static void
generate_wrapper_script (struct config *config, const char *remote_dir,
const char *filename)
{
FILE *fp;
fp = fopen (filename, "w");
if (fp == NULL)
error (EXIT_FAILURE, errno, "fopen: %s", filename);
fprintf (fp, "#!/bin/bash -\n");
fprintf (fp, "\n");
fprintf (fp, "cd %s\n", remote_dir);
fprintf (fp, "\n");
/* The virt-v2v command, as a shell function called "v2v". */
fprintf (fp, "v2v ()\n");
fprintf (fp, "{\n");
if (config->auth.sudo)
fprintf (fp, "sudo -n ");
fprintf (fp, "virt-v2v -v -x");
if (feature_colours_option)
fprintf (fp, " --colours");
fprintf (fp, " -i libvirtxml");
if (config->output.type) { /* -o */
fprintf (fp, " -o ");
print_quoted (fp, config->output.type);
}
switch (config->output.allocation) { /* -oa */
case OUTPUT_ALLOCATION_NONE:
/* nothing */
break;
case OUTPUT_ALLOCATION_SPARSE:
fprintf (fp, " -oa sparse");
break;
case OUTPUT_ALLOCATION_PREALLOCATED:
fprintf (fp, " -oa preallocated");
break;
default:
abort ();
}
if (config->output.format) { /* -of */
fprintf (fp, " -of ");
print_quoted (fp, config->output.format);
}
if (config->output.storage) { /* -os */
fprintf (fp, " -os ");
print_quoted (fp, config->output.storage);
}
fprintf (fp, " --root first");
fprintf (fp, " physical.xml");
fprintf (fp, " </dev/null"); /* no stdin */
fprintf (fp, "\n");
fprintf (fp,
"# Save the exit code of virt-v2v into the 'status' file.\n");
fprintf (fp, "echo $? > status\n");
fprintf (fp, "}\n");
fprintf (fp, "\n");
fprintf (fp,
"# Write a pre-emptive error status, in case the virt-v2v\n"
"# command doesn't get to run at all. This will be\n"
"# overwritten with the true exit code when virt-v2v runs.\n");
fprintf (fp, "echo 99 > status\n");
fprintf (fp, "\n");
fprintf (fp, "log=virt-v2v-conversion-log.txt\n");
fprintf (fp, "rm -f $log\n");
fprintf (fp, "\n");
fprintf (fp,
"# Log the environment where virt-v2v will run.\n");
fprintf (fp, "printenv > environment\n");
fprintf (fp, "\n");
fprintf (fp,
"# Log the version of virt-v2v (for information only).\n");
if (config->auth.sudo)
fprintf (fp, "sudo -n ");
fprintf (fp, "virt-v2v --version > v2v-version\n");
fprintf (fp, "\n");
fprintf (fp,
"# Run virt-v2v. Send stdout back to virt-p2v. Send stdout\n"
"# and stderr (debugging info) to the log file.\n");
fprintf (fp, "v2v 2>> $log | tee -a $log\n");
fprintf (fp, "\n");
fprintf (fp,
"# If virt-v2v failed then the error message (sent to stderr)\n"
"# will not be seen in virt-p2v. Send the last few lines of\n"
"# the log back to virt-p2v in this case.\n");
fprintf (fp,
"if [ \"$(< status)\" -ne 0 ]; then\n"
" echo\n"
" echo\n"
" echo\n"
" echo -ne '\\e[1;31m'\n"
" echo '***' virt-v2v command failed '***'\n"
" echo\n"
" echo The full log is available on the conversion server in:\n"
" echo ' ' %s/$log\n"
" echo Only the last 50 lines are shown below.\n"
" echo -ne '\\e[0m'\n"
" echo\n"
" echo\n"
" echo\n"
" tail -50 $log\n"
"fi\n",
remote_dir);
fprintf (fp, "\n");
fprintf (fp, "# EOF\n");
fclose (fp);
if (chmod (filename, 0755) == -1)
error (EXIT_FAILURE, errno, "chmod: %s", filename);
}
/**
* Print a shell-quoted string on C<fp>.
*/
static void
print_quoted (FILE *fp, const char *s)
{
fprintf (fp, "\"");
while (*s) {
if (*s == '$' || *s == '`' || *s == '\\' || *s == '"')
fprintf (fp, "\\");
fprintf (fp, "%c", *s);
++s;
}
fprintf (fp, "\"");
}
/**
* Collect data about the system running virt-p2v such as the dmesg
* output and lists of PCI devices. This is useful for diagnosis when
* things go wrong.
*
* If any command fails, this is non-fatal.
*/
static void
generate_system_data (const char *dmesg_file,
const char *lscpu_file,
const char *lspci_file,
const char *lsscsi_file,
const char *lsusb_file)
{
CLEANUP_FREE char *cmd = NULL;
if (asprintf (&cmd,
"dmesg >%s 2>&1; "
"lscpu >%s 2>&1; "
"lspci -vvv >%s 2>&1; "
"lsscsi -v >%s 2>&1; "
"lsusb -v >%s 2>&1",
dmesg_file, lscpu_file, lspci_file, lsscsi_file, lsusb_file)
== -1)
error (EXIT_FAILURE, errno, "asprintf");
ignore_value (system (cmd));
}
/**
* Generate a file containing the version of virt-p2v.
*
* The version of virt-v2v is contained in the conversion log.
*/
static void
generate_p2v_version_file (const char *p2v_version_file)
{
FILE *fp = fopen (p2v_version_file, "w");
if (fp == NULL) {
perror (p2v_version_file);
return; /* non-fatal */
}
fprintf (fp, "%s %s\n",
getprogname (), PACKAGE_VERSION_FULL);
fclose (fp);
}