From 2b5fbc882a0a79646b668c7b95442f0b3673d63a Mon Sep 17 00:00:00 2001 From: "Richard W.M. Jones" Date: Fri, 6 May 2011 17:42:07 -0400 Subject: [PATCH 1/1] Rewrite virt-edit in C. --- .gitignore | 3 + Makefile.am | 2 +- configure.ac | 1 + edit/Makefile.am | 75 ++++ edit/test-virt-edit.sh | 42 +++ edit/virt-edit.c | 666 ++++++++++++++++++++++++++++++++++++ edit/virt-edit.pod | 381 +++++++++++++++++++++ images/guest-aux/make-fedora-img.sh | 8 + po/POTFILES.in | 2 +- src/guestfs.pod | 4 + tools/Makefile.am | 5 +- tools/virt-edit | 530 ---------------------------- 12 files changed, 1184 insertions(+), 535 deletions(-) create mode 100644 edit/Makefile.am create mode 100755 edit/test-virt-edit.sh create mode 100644 edit/virt-edit.c create mode 100644 edit/virt-edit.pod delete mode 100755 tools/virt-edit diff --git a/.gitignore b/.gitignore index 06903d4..969aa08 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,9 @@ depcomp df/stamp-virt-df.pod df/virt-df df/virt-df.1 +edit/stamp-virt-*.pod +edit/virt-edit +edit/virt-edit.1 emptydisk examples/create_disk examples/guestfs-examples.3 diff --git a/Makefile.am b/Makefile.am index fcd190a..b475659 100644 --- a/Makefile.am +++ b/Makefile.am @@ -36,7 +36,7 @@ SUBDIRS += gnulib/tests capitests regressions test-tool SUBDIRS += fish # virt-tools in C. -SUBDIRS += cat df inspector rescue +SUBDIRS += cat df edit inspector rescue # Language bindings. if HAVE_PERL diff --git a/configure.ac b/configure.ac index eccbf3c..e24c46d 100644 --- a/configure.ac +++ b/configure.ac @@ -826,6 +826,7 @@ AC_CONFIG_FILES([Makefile csharp/Makefile debian/changelog df/Makefile + edit/Makefile examples/Makefile fish/Makefile fuse/Makefile diff --git a/edit/Makefile.am b/edit/Makefile.am new file mode 100644 index 0000000..5195b51 --- /dev/null +++ b/edit/Makefile.am @@ -0,0 +1,75 @@ +# libguestfs 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 $(top_srcdir)/subdir-rules.mk + +EXTRA_DIST = \ + test-virt-edit.sh \ + virt-edit.pod + +CLEANFILES = stamp-virt-edit.pod + +bin_PROGRAMS = virt-edit + +SHARED_SOURCE_FILES = \ + ../fish/config.c \ + ../fish/inspect.c \ + ../fish/keys.c \ + ../fish/options.h \ + ../fish/options.c \ + ../fish/virt.c + +virt_edit_SOURCES = \ + $(SHARED_SOURCE_FILES) \ + virt-edit.c + +virt_edit_CFLAGS = \ + -I$(top_srcdir)/src -I$(top_builddir)/src \ + -I$(top_srcdir)/fish \ + -I$(srcdir)/../gnulib/lib -I../gnulib/lib \ + -DLOCALEBASEDIR=\""$(datadir)/locale"\" \ + $(LIBCONFIG_CFLAGS) \ + $(WARN_CFLAGS) $(WERROR_CFLAGS) + +virt_edit_LDADD = \ + $(LIBCONFIG_LIBS) \ + $(top_builddir)/src/libguestfs.la \ + ../gnulib/lib/libgnu.la + +# Manual pages and HTML files for the website. +man_MANS = virt-edit.1 +noinst_DATA = $(top_builddir)/html/virt-edit.1.html + +virt-edit.1 $(top_builddir)/html/virt-edit.1.html: stamp-virt-edit.pod + +stamp-virt-edit.pod: virt-edit.pod + $(top_srcdir)/podwrapper.sh \ + --man virt-edit.1 \ + --html $(top_builddir)/html/virt-edit.1.html \ + $< + touch $@ + +# Tests. + +random_val := $(shell awk 'BEGIN{srand(); print 1+int(255*rand())}' < /dev/null) + +TESTS_ENVIRONMENT = \ + MALLOC_PERTURB_=$(random_val) \ + LD_LIBRARY_PATH=$(top_builddir)/src/.libs \ + LIBGUESTFS_PATH=$(top_builddir)/appliance + +TESTS = test-virt-edit.sh diff --git a/edit/test-virt-edit.sh b/edit/test-virt-edit.sh new file mode 100755 index 0000000..d43851b --- /dev/null +++ b/edit/test-virt-edit.sh @@ -0,0 +1,42 @@ +#!/bin/bash - + +export LANG=C +set -e + +# Make a copy of the Fedora image so we can write to it then +# discard it. +cp ../images/fedora.img test.img + +# Edit interactively. We have to simulate this by setting $EDITOR. +# The command will be: echo newline >> /tmp/file +export EDITOR='echo newline >>' +./virt-edit -a test.img /etc/test3 +if [ "$(../cat/virt-cat -a test.img /etc/test3)" != "a +b +c +d +e +f +newline" ]; then + echo "$0: error: mismatch in interactive editing of file /etc/test3" + exit 1 +fi +unset EDITOR + +# Edit non-interactively, only if we have 'perl' binary. +if perl --version >/dev/null 2>&1; then + ./virt-edit -a test.img /etc/test3 -e 's/^[a-f]/$lineno/' + if [ "$(../cat/virt-cat -a test.img /etc/test3)" != "1 +2 +3 +4 +5 +6 +newline" ]; then + echo "$0: error: mismatch in non-interactive editing of file /etc/test3" + exit 1 + fi +fi + +# Discard test image. +rm test.img diff --git a/edit/virt-edit.c b/edit/virt-edit.c new file mode 100644 index 0000000..dc2e130 --- /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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 () { " + " $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 */ +} diff --git a/edit/virt-edit.pod b/edit/virt-edit.pod new file mode 100644 index 0000000..ba4e3a6 --- /dev/null +++ b/edit/virt-edit.pod @@ -0,0 +1,381 @@ +=encoding utf8 + +=head1 NAME + +virt-edit - Edit a file in a virtual machine + +=head1 SYNOPSIS + + virt-edit [--options] -d domname file [file ...] + + virt-edit [--options] -a disk.img [-a disk.img ...] file [file ...] + + virt-edit [-d domname|-a disk.img] file -e 'expr' + +Old-style: + + virt-edit domname file + + virt-edit disk.img [disk.img ...] file + +=head1 WARNING + +You must I use C on live virtual machines. If you do +this, you risk disk corruption in the VM. C tries to stop +you from doing this, but doesn't catch all cases. + +=head1 DESCRIPTION + +C is a command line tool to edit C where each C +exists in the named virtual machine (or disk image). + +Multiple filenames can be given, in which case they are each edited in +turn. Each filename must be a full path, starting at the root +directory (starting with '/'). + +If you want to just view a file, use L. + +For more complex cases you should look at the L tool +(see L below). + +C cannot be used to create a new file. L can +do that and much more. + +=head1 EXAMPLES + +Edit the named files interactively: + + virt-edit -d mydomain /boot/grub/grub.conf + + virt-edit -d mydomain /etc/passwd + +For Windows guests, some Windows paths are understood: + + virt-edit -d mywindomain 'c:\autoexec.bat' + +If Perl is installed, you can also edit files non-interactively (see +L below). +To change the init default level to 5: + + virt-edit -d mydomain /etc/inittab -e 's/^id:.*/id:5:initdefault:/' + +=head1 OPTIONS + +=over 4 + +=item B<--help> + +Display brief help. + +=item B<-a> file + +=item B<--add> file + +Add I which should be a disk image from a virtual machine. If +the virtual machine has multiple block devices, you must supply all of +them with separate I<-a> options. + +The format of the disk image is auto-detected. To override this and +force a particular format use the I<--format=..> option. + +=item B<-b> extension + +=item B<--backup> extension + +Create a backup of the original file I. +The backup has the original filename with C added. + +Usually the first character of C would be a dot C<.> +so you would write: + + virt-edit -b .orig [etc] + +By default, no backup file is made. + +=item B<--connect URI> | B<-c URI> + +If using libvirt, connect to the given I. If omitted, then we +connect to the default libvirt hypervisor. + +If you specify guest block devices directly, then libvirt is not used +at all. + +=item B<-d> guest + +=item B<--domain> guest + +Add all the disks from the named libvirt guest. Domain UUIDs can be +used instead of names. + +=item B<--echo-keys> + +When prompting for keys and passphrases, virt-cat normally turns +echoing off so you cannot see what you are typing. If you are not +worried about Tempest attacks and there is no one else in the room you +can specify this flag to see what you are typing. + +=item B<--format> raw|qcow2|... + +=item B<--format> + +The default for the I<-a> option is to auto-detect the format of the +disk image. Using this forces the disk format for I<-a> options which +follow on the command line. Using I<--format> with no argument +switches back to auto-detection for subsequent I<-a> options. + +For example: + + virt-edit --format=raw -a disk.img file + +forces raw format (no auto-detection) for C. + + virt-edit --format=raw -a disk.img --format -a another.img file + +forces raw format (no auto-detection) for C and reverts to +auto-detection for C. + +If you have untrusted raw-format guest disk images, you should use +this option to specify the disk format. This avoids a possible +security problem with malicious guests (CVE-2010-3851). + +=item B<-e> EXPR + +=item B<--expr> EXPR + +Instead of launching the external editor, non-interactively +apply the Perl expression C to each line in the file. +See L below. + +Be careful to properly quote the expression to prevent it from +being altered by the shell. + +Note that this option is only available when Perl 5 is installed. + +=item B<--keys-from-stdin> + +Read key or passphrase parameters from stdin. The default is +to try to read passphrases from the user by opening C. + +=item B<-v> + +=item B<--verbose> + +Enable verbose messages for debugging. + +=item B<-V> + +=item B<--version> + +Display version number and exit. + +=item B<-x> + +Enable tracing of libguestfs API calls. + +=back + +=head1 OLD-STYLE COMMAND LINE ARGUMENTS + +Previous versions of virt-edit allowed you to write either: + + virt-edit disk.img [disk.img ...] file + +or + + virt-edit guestname file + +whereas in this version you should use I<-a> or I<-d> respectively +to avoid the confusing case where a disk image might have the same +name as a guest. + +For compatibility the old style is still supported. + +=head1 NON-INTERACTIVE EDITING + +C normally calls out to C<$EDITOR> (or vi) so +the system administrator can interactively edit the file. + +There are two ways also to use C from scripts in order to +make automated edits to files. (Note that although you I use +C like this, it's less error-prone to write scripts +directly using the libguestfs API and Augeas for configuration file +editing.) + +The first method is to temporarily set C<$EDITOR> to any script or +program you want to run. The script is invoked as C<$EDITOR tmpfile> +and it should update C in place however it likes. + +The second method is to use the I<-e> parameter of C to run +a short Perl snippet in the style of L. For example to +replace all instances of C with C in a file: + + virt-edit -d domname filename -e 's/foo/bar/' + +The full power of Perl regular expressions can be used (see +L). For example to delete root's password you could do: + + virt-edit -d domname /etc/passwd -e 's/^root:.*?:/root::/' + +What really happens is that the snippet is evaluated as a Perl +expression for each line of the file. The line, including the final +C<\n>, is passed in C<$_> and the expression should update C<$_> or +leave it unchanged. + +To delete a line, set C<$_> to the empty string. For example, to +delete the C user account from the password file you can do: + + virt-edit -d mydomain /etc/passwd -e '$_ = "" if /^apache:/' + +To insert a line, prepend or append it to C<$_>. However appending +lines to the end of the file is rather difficult this way since there +is no concept of "last line of the file" - your expression just +doesn't get called again. You might want to use the first method +(setting C<$EDITOR>) if you want to do this. + +The variable C<$lineno> contains the current line number. +As is traditional, the first line in the file is number C<1>. + +The return value from the expression is ignored, but the expression +may call C in order to abort the whole program, leaving the +original file untouched. + +Remember when matching the end of a line that C<$_> may contain the +final C<\n>, or (for DOS files) C<\r\n>, or if the file does not end +with a newline then neither of these. Thus to match or substitute +some text at the end of a line, use this regular expression: + + /some text(\r?\n)?$/ + +Alternately, use the perl C function, being careful not to +chomp C<$_> itself (since that would remove all newlines from the +file): + + my $m = $_; chomp $m; $m =~ /some text$/ + +=head1 WINDOWS PATHS + +C has a limited ability to understand Windows drive letters +and paths (eg. C). + +If and only if the guest is running Windows then: + +=over 4 + +=item * + +Drive letter prefixes like C are resolved against the +Windows Registry to the correct filesystem. + +=item * + +Any backslash (C<\>) characters in the path are replaced +with forward slashes so that libguestfs can process it. + +=item * + +The path is resolved case insensitively to locate the file +that should be edited. + +=back + +There are some known shortcomings: + +=over 4 + +=item * + +Some NTFS symbolic links may not be followed correctly. + +=item * + +NTFS junction points that cross filesystems are not followed. + +=back + +=head1 USING GUESTFISH + +L is a more powerful, lower level tool which you can use +when C doesn't work. + +Using C is approximately equivalent to doing: + + guestfish --rw -i -d domname edit /file + +where C is the name of the libvirt guest, and C is the +full path to the file. + +The command above uses libguestfs's guest inspection feature and so +does not work on guests that libguestfs cannot inspect, or on things +like arbitrary disk images that don't contain guests. To edit a file +on a disk image directly, use: + + guestfish --rw -a disk.img -m /dev/sda1 edit /file + +where C is the disk image, C is the filesystem +within the disk image to edit, and C is the full path to the +file. + +C cannot create new files. Use the guestfish commands +C, C or C instead: + + guestfish --rw -i -d domname touch /newfile + + guestfish --rw -i -d domname write /newfile "new content" + + guestfish --rw -i -d domname upload localfile /newfile + +=head1 ENVIRONMENT VARIABLES + +=over 4 + +=item C + +If set, this string is used as the editor. It may contain arguments, +eg. C<"emacs -nw"> + +If not set, C is used. + +=back + +=head1 SHELL QUOTING + +Libvirt guest names can contain arbitrary characters, some of which +have meaning to the shell such as C<#> and space. You may need to +quote or escape these characters on the command line. See the shell +manual page L for details. + +=head1 SEE ALSO + +L, +L, +L, +L, +L, +L, +L, +L, +L, +L, +L. + +=head1 AUTHOR + +Richard W.M. Jones L + +=head1 COPYRIGHT + +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. diff --git a/images/guest-aux/make-fedora-img.sh b/images/guest-aux/make-fedora-img.sh index 0e064b4..a038432 100755 --- a/images/guest-aux/make-fedora-img.sh +++ b/images/guest-aux/make-fedora-img.sh @@ -83,6 +83,14 @@ touch /boot/grub/grub.conf # Test files. write /etc/test1 "abcdefg" write /etc/test2 "" +upload -<<__end /etc/test3 +a +b +c +d +e +f +__end write /bin/test1 "abcdefg" write /bin/test2 "zxcvbnm" write /bin/test3 "1234567" diff --git a/po/POTFILES.in b/po/POTFILES.in index c023ab9..7c0df52 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -78,6 +78,7 @@ df/df.c df/domains.c df/main.c df/output.c +edit/virt-edit.c fish/alloc.c fish/cmds.c fish/cmds_gperf.c @@ -151,7 +152,6 @@ src/match.c src/proto.c src/virt.c test-tool/test-tool.c -tools/virt-edit.pl tools/virt-list-filesystems.pl tools/virt-list-partitions.pl tools/virt-make-fs.pl diff --git a/src/guestfs.pod b/src/guestfs.pod index 5d9d804..915d860 100644 --- a/src/guestfs.pod +++ b/src/guestfs.pod @@ -2671,6 +2671,10 @@ actions. L command and documentation. +=item C + +L command and documentation. + =item C C API example code. diff --git a/tools/Makefile.am b/tools/Makefile.am index 6441333..7904f1c 100644 --- a/tools/Makefile.am +++ b/tools/Makefile.am @@ -18,7 +18,6 @@ include $(top_srcdir)/subdir-rules.mk tools = \ - edit \ list-filesystems \ list-partitions \ make-fs \ @@ -37,10 +36,10 @@ bin_SCRIPTS = $(tools:%=virt-%) # Manual pages and HTML files for the website. -# XXX Bug in automake? If you list virt-edit.1 explicitly, then it +# XXX Bug in automake? If you list virt-tar.1 explicitly, then it # builds and installs the man pages. However if this is removed, # then the man pages are neither built nor installed. -man_MANS = virt-edit.1 $(patsubst %,virt-%.1,$(filter-out edit,$(tools))) +man_MANS = virt-tar.1 $(patsubst %,virt-%.1,$(filter-out tar,$(tools))) noinst_DATA = $(tools:%=$(top_builddir)/html/virt-%.1.html) diff --git a/tools/virt-edit b/tools/virt-edit deleted file mode 100755 index a004291..0000000 --- a/tools/virt-edit +++ /dev/null @@ -1,530 +0,0 @@ -#!/usr/bin/perl -w -# 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. - -use warnings; -use strict; - -use Sys::Guestfs; -use Sys::Guestfs::Lib qw(open_guest); -use Pod::Usage; -use Getopt::Long; -use File::Temp qw/tempfile/; -use File::Basename; -use Locale::TextDomain 'libguestfs'; - -=encoding utf8 - -=head1 NAME - -virt-edit - Edit a file in a virtual machine - -=head1 SYNOPSIS - - virt-edit [--options] domname file - - virt-edit [--options] disk.img [disk.img ...] file - - virt-edit [domname|disk.img] file -e 'expr' - -=head1 WARNING - -You must I use C on live virtual machines. If you do -this, you risk disk corruption in the VM. C tries to stop -you from doing this, but doesn't catch all cases. - -=head1 DESCRIPTION - -C is a command line tool to edit C where C -exists in the named virtual machine (or disk image). - -If you want to just view a file, use L. - -For more complex cases you should look at the L tool -(see L below). - -C cannot be used to create a new file, nor to edit -multiple files. L can do that and much more. - -=head1 EXAMPLES - -Edit the named files interactively: - - virt-edit mydomain /boot/grub/grub.conf - - virt-edit mydomain /etc/passwd - -For Windows guests, some Windows paths are understood: - - virt-edit mywindomain 'c:\autoexec.bat' - -You can also edit files non-interactively (see -L below). -To change the init default level to 5: - - virt-edit mydomain /etc/inittab -e 's/^id:.*/id:5:initdefault:/' - -=head1 OPTIONS - -=over 4 - -=cut - -my $help; - -=item B<--help> - -Display brief help. - -=cut - -my $version; - -=item B<--version> - -Display version number and exit. - -=cut - -my $backup; - -=item B<--backup extension> | B<-b extension> - -Create a backup of the original file I. -The backup has the original filename with C added. - -Usually the first character of C would be a dot C<.> -so you would write: - - virt-edit -b .orig [etc] - -By default, no backup file is made. - -=cut - -my $uri; - -=item B<--connect URI> | B<-c URI> - -If using libvirt, connect to the given I. If omitted, then we -connect to the default libvirt hypervisor. - -If you specify guest block devices directly, then libvirt is not used -at all. - -=cut - -my $format; - -=item B<--format> raw - -Specify the format of disk images given on the command line. If this -is omitted then the format is autodetected from the content of the -disk image. - -If disk images are requested from libvirt, then this program asks -libvirt for this information. In this case, the value of the format -parameter is ignored. - -If working with untrusted raw-format guest disk images, you should -ensure the format is always specified. - -=cut - -my $expr; - -=item B<--expr EXPR> | B<-e EXPR> - -Instead of launching the external editor, non-interactively -apply the Perl expression C to each line in the file. -See L below. - -Be careful to properly quote the expression to prevent it from -being altered by the shell. - -=back - -=cut - -GetOptions ("help|?" => \$help, - "version" => \$version, - "connect|c=s" => \$uri, - "format=s" => \$format, - "expr|e=s" => \$expr, - "backup|b=s" => \$backup, - ) or pod2usage (2); -pod2usage (1) if $help; -if ($version) { - my $g = Sys::Guestfs->new (); - my %h = $g->version (); - print "$h{major}.$h{minor}.$h{release}$h{extra}\n"; - exit -} - -pod2usage (__"virt-edit: no image, VM names or filenames to edit given") - if @ARGV <= 1; - -my $filename = pop @ARGV; - -my $g; -if ($uri) { - $g = open_guest (\@ARGV, address => $uri, rw => 1, format => $format); -} else { - $g = open_guest (\@ARGV, rw => 1, format => $format); -} - -$g->launch (); - -my @roots = $g->inspect_os (); -if (@roots == 0) { - die __x("{prog}: No operating system could be detected inside this disk image.\n\nThis may be because the file is not a disk image, or is not a virtual machine\nimage, or because the OS type is not understood by libguestfs.\n\nIf you feel this is an error, please file a bug report including as much\ninformation about the disk image as possible.\n", - prog => basename ($0)); -} -if (@roots > 1) { - die __x("{prog}: multiboot operating systems are not supported.\n", - prog => basename ($0)) -} -my $root = $roots[0]; -my %fses = $g->inspect_get_mountpoints ($root); -my @fses = sort { length $a <=> length $b } keys %fses; -foreach (@fses) { - $g->mount_options ("", $fses{$_}, $_); -} - -# Special handling for Windows filenames. -$filename = windows_path ($g, $root, $filename) - if $g->inspect_get_type ($root) eq "windows"; - -my ($fh, $tempname) = tempfile (UNLINK => 1); -my $fddev = "/dev/fd/" . fileno ($fh); - -# Allow this to fail in case eg. the file does not exist. -$g->download ($filename, $fddev); - -close $fh or die "close: $!"; - -my $do_upload = $tempname; - -if (!defined $expr) { - # Interactively edit the file. - my $oldctime = (stat ($tempname))[10]; - - my $editor = $ENV{EDITOR}; - $editor ||= "vi"; - system ("$editor $tempname") == 0 - or die "edit failed: $editor: $?"; - - my $newctime = (stat ($tempname))[10]; - - if ($oldctime == $newctime) { - $do_upload = undef; - print __"File not changed.\n"; - } -} else { - my ($fh, $tempout) = tempfile (UNLINK => 1); - - # Apply a Perl expression to the lines of the file. - open IFILE, $tempname or die "$tempname: $!"; - my $lineno = 0; - while () { - $lineno++; - eval $expr; - die if $@; - print $fh $_ or die "print: $!"; - } - close $fh or die "close: $!"; - - $do_upload = $tempout; -} - -if (defined $do_upload) { - # Upload to a new file, 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. - my $dirname = $filename; - $dirname =~ s{/[^/]+$}{/}; - - my @chars = ('a'..'z', 'A'..'Z', '0'..'9'); - my $newname = $dirname; - foreach (0..7) { - $newname .= $chars[rand @chars]; - } - - $g->upload ($do_upload, $newname); - - # Backup or overwrite? - $g->mv ($filename, "$filename$backup") if defined $backup; - $g->mv ($newname, $filename); - - $g->umount_all (); - $g->sync (); -} - -undef $g; - -=head1 NON-INTERACTIVE EDITING - -C normally calls out to C<$EDITOR> (or vi) so -the system administrator can interactively edit the file. - -There are two ways also to use C from scripts in order to -make automated edits to files. (Note that although you I use -C like this, it's less error-prone to write scripts -directly using the libguestfs API and Augeas for configuration file -editing.) - -The first method is to temporarily set C<$EDITOR> to any script or -program you want to run. The script is invoked as C<$EDITOR tmpfile> -and it should update C in place however it likes. - -The second method is to use the I<-e> parameter of C to run -a short Perl snippet in the style of L. For example to -replace all instances of C with C in a file: - - virt-edit domname filename -e 's/foo/bar/' - -The full power of Perl regular expressions can be used (see -L). For example to delete root's password you could do: - - virt-edit domname /etc/passwd -e 's/^root:.*?:/root::/' - -What really happens is that the snippet is evaluated as a Perl -expression for each line of the file. The line, including the final -C<\n>, is passed in C<$_> and the expression should update C<$_> or -leave it unchanged. - -To delete a line, set C<$_> to the empty string. For example, to -delete the C user account from the password file you can do: - - virt-edit mydomain /etc/passwd -e '$_ = "" if /^apache:/' - -To insert a line, prepend or append it to C<$_>. However appending -lines to the end of the file is rather difficult this way since there -is no concept of "last line of the file" - your expression just -doesn't get called again. You might want to use the first method -(setting C<$EDITOR>) if you want to do this. - -The variable C<$lineno> contains the current line number. -As is traditional, the first line in the file is number C<1>. - -The return value from the expression is ignored, but the expression -may call C in order to abort the whole program, leaving the -original file untouched. - -Remember when matching the end of a line that C<$_> may contain the -final C<\n>, or (for DOS files) C<\r\n>, or if the file does not end -with a newline then neither of these. Thus to match or substitute -some text at the end of a line, use this regular expression: - - /some text(\r?\n)?$/ - -Alternately, use the perl C function, being careful not to -chomp C<$_> itself (since that would remove all newlines from the -file): - - my $m = $_; chomp $m; $m =~ /some text$/ - -=head1 WINDOWS PATHS - -C has a limited ability to understand Windows drive letters -and paths (eg. C). - -If and only if the guest is running Windows then: - -=over 4 - -=item * - -Drive letter prefixes like C are resolved against the -Windows Registry to the correct filesystem. - -=item * - -Any backslash (C<\>) characters in the path are replaced -with forward slashes so that libguestfs can process it. - -=item * - -The path is resolved case insensitively to locate the file -that should be edited. - -=back - -There are some known shortcomings: - -=over 4 - -=item * - -Some NTFS symbolic links may not be followed correctly. - -=item * - -NTFS junction points that cross filesystems are not followed. - -=back - -=cut - -sub windows_path -{ - my $g = shift; - my $root = shift; - my $filename = shift; - - # Deal with drive letters. - if ($filename =~ /^([a-z]):(.*)/i) { - $filename = $2; - my $drive_letter = $1; - - # Look up the drive letter in the drive mapping table. We - # have to do a case insensitive comparison, the slow way. - my $device; - my %drives = $g->inspect_get_drive_mappings ($root); - foreach (keys %drives) { - if (lc $_ eq lc $drive_letter) { - $device = $drives{$_}; - last; - } - } - - die __x("virt-edit: drive '{x}:' not found\n", x => $drive_letter) - unless defined $device; - - # Unmount current disk and remount $device. - $g->umount_all (); - $g->mount_options ("", $device, "/"); - } - - # Replace any backslashes in the rest of the path with - # forward slashes. - $filename =~ s{\\}{/}g; - - # If the user put \foo on the command line without quoting it - # properly, then we'll see that here as a bare path. Add a more - # descriptive error message here. - if (substr ($filename, 0, 1) ne "/") { - die __x("virt-edit: '{f}' does not start with a / or \\ character. -If you are using Windows style paths with backslashes like C:\\foo.txt -then don't forget that you must quote them with single quotes to -prevent the shell from munging the backslashes.\n", - f => $filename) - } - - # Case sensitivity. - $filename = $g->case_sensitive_path ($filename); - - return $filename; -} - -=head1 USING GUESTFISH - -L is a more powerful, lower level tool which you can use -when C doesn't work. - -Using C is approximately equivalent to doing: - - guestfish --rw -i -d domname edit /file - -where C is the name of the libvirt guest, and C is the -full path to the file. - -The command above uses libguestfs's guest inspection feature and so -does not work on guests that libguestfs cannot inspect, or on things -like arbitrary disk images that don't contain guests. To edit a file -on a disk image directly, use: - - guestfish --rw -a disk.img -m /dev/sda1 edit /file - -where C is the disk image, C is the filesystem -within the disk image to edit, and C is the full path to the -file. - -C cannot create new files. Use the guestfish commands -C, C or C instead: - - guestfish --rw -i -d domname touch /newfile - - guestfish --rw -i -d domname write /newfile "new content" - - guestfish --rw -i -d domname upload localfile /newfile - -C cannot edit multiple files, but guestfish can -do it like this: - - guestfish --rw -i -d domname edit /file1 : edit /file2 - -=cut - -exit 0; - -=head1 ENVIRONMENT VARIABLES - -=over 4 - -=item C - -If set, this string is used as the editor. It may contain arguments, -eg. C<"emacs -nw"> - -If not set, C is used. - -=back - -=head1 SHELL QUOTING - -Libvirt guest names can contain arbitrary characters, some of which -have meaning to the shell such as C<#> and space. You may need to -quote or escape these characters on the command line. See the shell -manual page L for details. - -=head1 SEE ALSO - -L, -L, -L, -L, -L, -L, -L, -L, -L, -L, -L. - -=head1 AUTHOR - -Richard W.M. Jones L - -=head1 COPYRIGHT - -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. -- 1.8.3.1