diff options
Diffstat (limited to 'edit/virt-edit.c')
-rw-r--r-- | edit/virt-edit.c | 666 |
1 files changed, 666 insertions, 0 deletions
diff --git a/edit/virt-edit.c b/edit/virt-edit.c new file mode 100644 index 00000000..dc2e1302 --- /dev/null +++ b/edit/virt-edit.c @@ -0,0 +1,666 @@ +/* virt-edit + * Copyright (C) 2009-2011 Red Hat Inc. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + +#include <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <inttypes.h> +#include <unistd.h> +#include <locale.h> +#include <getopt.h> +#include <assert.h> +#include <libintl.h> +#include <sys/time.h> +#include <sys/stat.h> +#include <utime.h> + +#include "progname.h" +#include "xvasprintf.h" +#include "c-ctype.h" + +#include "guestfs.h" +#include "options.h" + +/* Currently open libguestfs handle. */ +guestfs_h *g; + +int read_only = 0; +int live = 0; +int verbose = 0; +int keys_from_stdin = 0; +int echo_keys = 0; +const char *libvirt_uri = NULL; +int inspector = 1; + +static const char *backup_extension = NULL; +static const char *perl_expr = NULL; + +static void edit (const char *filename, const char *root); +static char *edit_interactively (const char *tmpfile); +static char *edit_non_interactively (const char *tmpfile); +static int is_windows (guestfs_h *g, const char *root); +static char *windows_path (guestfs_h *g, const char *root, const char *filename); +static char *generate_random_name (const char *filename); +static char *generate_backup_name (const char *filename); + +static inline char * +bad_cast (char const *s) +{ + return (char *) s; +} + +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: Edit a file in a virtual machine\n" + "Copyright (C) 2009-2011 Red Hat Inc.\n" + "Usage:\n" + " %s [--options] -d domname file [file ...]\n" + " %s [--options] -a disk.img [-a disk.img ...] file [file ...]\n" + "Options:\n" + " -a|--add image Add image\n" + " -b|--backup .ext Backup original as original.ext\n" + " -c|--connect uri Specify libvirt URI for -d option\n" + " -d|--domain guest Add disks from libvirt guest\n" + " --echo-keys Don't turn off echo for passphrases\n" + " -e|--expr expr Non-interactive editing using Perl expr\n" + " --format[=raw|..] Force disk format for -a option\n" + " --help Display brief help\n" + " --keys-from-stdin Read passphrases from stdin\n" + " -v|--verbose Verbose messages\n" + " -V|--version Display version and exit\n" + " -x Trace libguestfs API calls\n" + "For more information, see the manpage %s(1).\n"), + program_name, program_name, program_name, + program_name); + } + exit (status); +} + +int +main (int argc, char *argv[]) +{ + /* Set global program name that is not polluted with libtool artifacts. */ + set_program_name (argv[0]); + + setlocale (LC_ALL, ""); + bindtextdomain (PACKAGE, LOCALEBASEDIR); + textdomain (PACKAGE); + + /* We use random(3) below. */ + srandom (time (NULL)); + + enum { HELP_OPTION = CHAR_MAX + 1 }; + + static const char *options = "a:b:c:d:e:vVx"; + static const struct option long_options[] = { + { "add", 1, 0, 'a' }, + { "backup", 1, 0, 'b' }, + { "connect", 1, 0, 'c' }, + { "domain", 1, 0, 'd' }, + { "echo-keys", 0, 0, 0 }, + { "expr", 1, 0, 'e' }, + { "format", 2, 0, 0 }, + { "help", 0, 0, HELP_OPTION }, + { "keys-from-stdin", 0, 0, 0 }, + { "verbose", 0, 0, 'v' }, + { "version", 0, 0, 'V' }, + { 0, 0, 0, 0 } + }; + struct drv *drvs = NULL; + struct drv *drv; + const char *format = NULL; + int c; + int option_index; + char *root, **roots; + + g = guestfs_create (); + if (g == NULL) { + fprintf (stderr, _("guestfs_create: failed to create handle\n")); + exit (EXIT_FAILURE); + } + + argv[0] = bad_cast (program_name); + + 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, "keys-from-stdin")) { + keys_from_stdin = 1; + } else if (STREQ (long_options[option_index].name, "echo-keys")) { + echo_keys = 1; + } else if (STREQ (long_options[option_index].name, "format")) { + if (!optarg || STREQ (optarg, "")) + format = NULL; + else + format = 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 'a': + OPTION_a; + break; + + case 'b': + if (backup_extension) { + fprintf (stderr, _("%s: -b option given multiple times\n"), + program_name); + exit (EXIT_FAILURE); + } + backup_extension = optarg; + break; + + case 'c': + OPTION_c; + break; + + case 'd': + OPTION_d; + break; + + case 'e': + if (perl_expr) { + fprintf (stderr, _("%s: -e option given multiple times\n"), + program_name); + exit (EXIT_FAILURE); + } + perl_expr = optarg; + break; + + case 'h': + usage (EXIT_SUCCESS); + + case 'v': + OPTION_v; + break; + + case 'V': + OPTION_V; + break; + + case 'x': + OPTION_x; + break; + + case HELP_OPTION: + usage (EXIT_SUCCESS); + + default: + usage (EXIT_FAILURE); + } + } + + /* Old-style syntax? There were no -a or -d options in the old + * virt-edit which is how we detect this. + */ + if (drvs == NULL) { + /* argc - 1 because last parameter is the single filename. */ + while (optind < argc - 1) { + if (strchr (argv[optind], '/') || + access (argv[optind], F_OK) == 0) { /* simulate -a option */ + drv = malloc (sizeof (struct drv)); + if (!drv) { + perror ("malloc"); + exit (EXIT_FAILURE); + } + drv->type = drv_a; + drv->a.filename = argv[optind]; + drv->a.format = NULL; + drv->next = drvs; + drvs = drv; + } else { /* simulate -d option */ + drv = malloc (sizeof (struct drv)); + if (!drv) { + perror ("malloc"); + exit (EXIT_FAILURE); + } + drv->type = drv_d; + drv->d.guest = argv[optind]; + drv->next = drvs; + drvs = drv; + } + + optind++; + } + } + + /* These are really constants, but they have to be variables for the + * options parsing code. Assert here that they have known-good + * values. + */ + assert (read_only == 0); + assert (inspector == 1); + assert (live == 0); + + /* User must specify at least one filename on the command line. */ + if (optind >= argc || argc - optind < 1) + usage (EXIT_FAILURE); + + /* User must have specified some drives. */ + if (drvs == NULL) + usage (EXIT_FAILURE); + + /* Add drives. */ + add_drives (drvs, 'a'); + + if (guestfs_launch (g) == -1) + exit (EXIT_FAILURE); + + inspect_mount (); + + /* Free up data structures, no longer needed after this point. */ + free_drives (drvs); + + /* Get root mountpoint. */ + roots = guestfs_inspect_get_roots (g); + if (!roots) + exit (EXIT_FAILURE); + /* see fish/inspect.c:inspect_mount */ + assert (roots[0] != NULL && roots[1] == NULL); + root = roots[0]; + free (roots); + + while (optind < argc) { + edit (argv[optind], root); + optind++; + } + + free (root); + + /* Cleanly unmount the disks after editing. */ + if (guestfs_umount_all (g) == -1 || guestfs_sync (g) == -1) + exit (EXIT_FAILURE); + + guestfs_close (g); + + exit (EXIT_SUCCESS); +} + +static void +edit (const char *filename, const char *root) +{ + char *filename_to_free = NULL; + const char *tmpdir = guestfs_tmpdir (); + char tmpfile[strlen (tmpdir) + 32]; + sprintf (tmpfile, "%s/virteditXXXXXX", tmpdir); + int fd; + char fdbuf[32]; + char *upload_from = NULL; + char *newname = NULL; + char *backupname = NULL; + + /* Windows? Special handling is required. */ + if (is_windows (g, root)) + filename = filename_to_free = windows_path (g, root, filename); + + /* Download the file to a temporary. */ + fd = mkstemp (tmpfile); + if (fd == -1) { + perror ("mkstemp"); + exit (EXIT_FAILURE); + } + + snprintf (fdbuf, sizeof fdbuf, "/dev/fd/%d", fd); + + if (guestfs_download (g, filename, fdbuf) == -1) + goto error; + + if (close (fd) == -1) { + perror (tmpfile); + goto error; + } + + if (!perl_expr) + upload_from = edit_interactively (tmpfile); + else + upload_from = edit_non_interactively (tmpfile); + + /* We don't always need to upload: upload_from could be NULL because + * the user closed the editor without changing the file. + */ + if (upload_from) { + /* Upload to a new file in the same directory, so if it fails we + * don't end up with a partially written file. Give the new file + * a completely random name so we have only a tiny chance of + * overwriting some existing file. + */ + newname = generate_random_name (filename); + + if (guestfs_upload (g, upload_from, newname) == -1) + goto error; + + /* Backup or overwrite the file. */ + if (backup_extension) { + backupname = generate_backup_name (filename); + if (guestfs_mv (g, filename, backupname) == -1) + goto error; + } + if (guestfs_mv (g, newname, filename) == -1) + goto error; + } + + unlink (tmpfile); + free (filename_to_free); + free (upload_from); + free (newname); + free (backupname); + return; + + error: + unlink (tmpfile); + exit (EXIT_FAILURE); +} + +static char * +edit_interactively (const char *tmpfile) +{ + struct utimbuf times; + struct stat oldstat, newstat; + const char *editor; + char *cmd; + int r; + char *ret; + + /* Set the time back a few seconds on the original file. This is so + * that if the user is very fast at editing, or if EDITOR is an + * automatic editor, then the edit might happen within the 1 second + * granularity of mtime, and we would think the file hasn't changed. + */ + if (stat (tmpfile, &oldstat) == -1) { + perror (tmpfile); + exit (EXIT_FAILURE); + } + + times.actime = oldstat.st_atime - 5; + times.modtime = oldstat.st_mtime - 5; + if (utime (tmpfile, ×) == -1) { + perror ("utimes"); + exit (EXIT_FAILURE); + } + + if (stat (tmpfile, &oldstat) == -1) { + perror (tmpfile); + exit (EXIT_FAILURE); + } + + editor = getenv ("EDITOR"); + if (editor == NULL) + editor = "vi"; + + if (asprintf (&cmd, "%s %s", editor, tmpfile) == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + + if (verbose) + fprintf (stderr, "%s\n", cmd); + + r = system (cmd); + if (r == -1 || WEXITSTATUS (r) != 0) + exit (EXIT_FAILURE); + + free (cmd); + + if (stat (tmpfile, &newstat) == -1) { + perror (tmpfile); + exit (EXIT_FAILURE); + } + + if (oldstat.st_ctime == newstat.st_ctime && + oldstat.st_mtime == newstat.st_mtime) { + printf ("File not changed.\n"); + return NULL; + } + + ret = strdup (tmpfile); + if (!ret) { + perror ("strdup"); + exit (EXIT_FAILURE); + } + + return ret; +} + +static char * +edit_non_interactively (const char *tmpfile) +{ + char *cmd, *outfile, *ret; + int r; + + assert (perl_expr != NULL); + + /* Pass the expression to Perl via the environment. This sidesteps + * any quoting problems with the already complex Perl command line. + */ + setenv ("virt_edit_expr", perl_expr, 1); + + /* Call out to a canned Perl script. */ + if (asprintf (&cmd, + "perl -e '" + "$lineno = 0; " + "$expr = $ENV{virt_edit_expr}; " + "while (<STDIN>) { " + " $lineno++; " + " eval $expr; " + " die if $@; " + " print STDOUT $_ or die \"print: $!\"; " + "} " + "close STDOUT or die \"close: $!\"; " + "' < %s > %s.out", + tmpfile, tmpfile) == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + + if (verbose) + fprintf (stderr, "%s\n", cmd); + + r = system (cmd); + if (r == -1 || WEXITSTATUS (r) != 0) + exit (EXIT_FAILURE); + + free (cmd); + + if (asprintf (&outfile, "%s.out", tmpfile) == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + + if (rename (outfile, tmpfile) == -1) { + perror ("rename"); + exit (EXIT_FAILURE); + } + + free (outfile); + + ret = strdup (tmpfile); + if (!ret) { + perror ("strdup"); + exit (EXIT_FAILURE); + } + + return ret; /* caller will free */ +} + +static int +is_windows (guestfs_h *g, const char *root) +{ + char *type; + int w; + + type = guestfs_inspect_get_type (g, root); + if (!type) + return 0; + + w = STREQ (type, "windows"); + free (type); + return w; +} + +static void mount_drive_letter (char drive_letter, const char *root); + +static char * +windows_path (guestfs_h *g, const char *root, const char *path) +{ + char *ret; + size_t i; + + /* If there is a drive letter, rewrite the path. */ + if (c_isalpha (path[0]) && path[1] == ':') { + char drive_letter = c_tolower (path[0]); + /* This returns the newly allocated string. */ + mount_drive_letter (drive_letter, root); + ret = strdup (path + 2); + if (ret == NULL) { + perror ("strdup"); + exit (EXIT_FAILURE); + } + } + else if (!*path) { + ret = strdup ("/"); + if (ret == NULL) { + perror ("strdup"); + exit (EXIT_FAILURE); + } + } + else { + ret = strdup (path); + if (ret == NULL) { + perror ("strdup"); + exit (EXIT_FAILURE); + } + } + + /* Blindly convert any backslashes into forward slashes. Is this good? */ + for (i = 0; i < strlen (ret); ++i) + if (ret[i] == '\\') + ret[i] = '/'; + + char *t = guestfs_case_sensitive_path (g, ret); + free (ret); + ret = t; + + return ret; +} + +static void +mount_drive_letter (char drive_letter, const char *root) +{ + char **drives; + char *device; + size_t i; + + /* Resolve the drive letter using the drive mappings table. */ + drives = guestfs_inspect_get_drive_mappings (g, root); + if (drives == NULL || drives[0] == NULL) { + fprintf (stderr, _("%s: to use Windows drive letters, this must be a Windows guest\n"), + program_name); + exit (EXIT_FAILURE); + } + + device = NULL; + for (i = 0; drives[i] != NULL; i += 2) { + if (c_tolower (drives[i][0]) == drive_letter && drives[i][1] == '\0') { + device = drives[i+1]; + break; + } + } + + if (device == NULL) { + fprintf (stderr, _("%s: drive '%c:' not found.\n"), + program_name, drive_letter); + exit (EXIT_FAILURE); + } + + /* Unmount current disk and remount device. */ + if (guestfs_umount_all (g) == -1) + exit (EXIT_FAILURE); + + if (guestfs_mount_options (g, "", device, "/") == -1) + exit (EXIT_FAILURE); + + for (i = 0; drives[i] != NULL; ++i) + free (drives[i]); + free (drives); + /* Don't need to free (device) because that string was in the + * drives array. + */ +} + +static char +random_char (void) +{ + char c[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + return c[random () % (sizeof c - 1)]; +} + +static char * +generate_random_name (const char *filename) +{ + char *ret, *p; + size_t i; + + ret = malloc (strlen (filename) + 16); + if (!ret) { + perror ("malloc"); + exit (EXIT_FAILURE); + } + strcpy (ret, filename); + + p = strrchr (ret, '/'); + assert (p); + p++; + + /* Because of "+ 16" above, there should be enough space in the + * output buffer to write 8 random characters here. + */ + for (i = 0; i < 8; ++i) + *p++ = random_char (); + *p++ = '\0'; + + return ret; /* caller will free */ +} + +static char * +generate_backup_name (const char *filename) +{ + char *ret; + + assert (backup_extension != NULL); + + if (asprintf (&ret, "%s%s", filename, backup_extension) == -1) { + perror ("asprintf"); + exit (EXIT_FAILURE); + } + + return ret; /* caller will free */ +} |