Since v2: - Fix the things that Pino mentioned, except the recursion. - Implement Windows support. For Windows support to be sane, I had to inline the add_and_mount code. Rich.
This follows (tails) a log file within a guest, rather like the regular 'tail -f' command. For example: virt-tail -d guest /var/log/messages --- .gitignore | 3 + bash/Makefile.am | 4 +- bash/virt-alignment-scan | 6 + cat/Makefile.am | 47 ++++- cat/tail.c | 502 +++++++++++++++++++++++++++++++++++++++++++++++ cat/test-docs.sh | 1 + cat/virt-cat.pod | 3 + cat/virt-log.pod | 8 +- cat/virt-tail.pod | 253 ++++++++++++++++++++++++ docs/guestfs-hacking.pod | 4 +- fish/guestfish.pod | 1 + src/guestfs.pod | 1 + tools/virt-win-reg | 1 + website/index.html.in | 1 + 14 files changed, 824 insertions(+), 11 deletions(-) create mode 100644 cat/tail.c create mode 100644 cat/virt-tail.pod diff --git a/.gitignore b/.gitignore index 3d3bf0d..c4d6eda 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,7 @@ Makefile.in /bash/virt-resize /bash/virt-sysprep /bash/virt-sparsify +/bash/virt-tail /bash/virt-tar-in /bash/virt-tar-out /build-aux/.gitignore @@ -111,6 +112,8 @@ Makefile.in /cat/virt-log.1 /cat/virt-ls /cat/virt-ls.1 +/cat/virt-tail +/cat/virt-tail.1 /ChangeLog /compile /config.cache diff --git a/bash/Makefile.am b/bash/Makefile.am index 9a51847..65505ef 100644 --- a/bash/Makefile.am +++ b/bash/Makefile.am @@ -48,6 +48,7 @@ symlinks = \ virt-resize \ virt-sparsify \ virt-sysprep \ + virt-tail \ virt-tar-in \ virt-tar-out @@ -70,7 +71,8 @@ virt-builder virt-cat virt-customize virt-df virt-dib virt-diff \ virt-edit virt-filesystems virt-format virt-get-kernel virt-inspector \ virt-log virt-ls \ virt-p2v-make-disk virt-p2v-make-kickstart virt-p2v-make-kiwi \ -virt-resize virt-sparsify virt-sysprep: +virt-resize virt-sparsify virt-sysprep \ +virt-tail: rm -f $@ $(LN_S) virt-alignment-scan $@ diff --git a/bash/virt-alignment-scan b/bash/virt-alignment-scan index 055bad1..80f6e51 100644 --- a/bash/virt-alignment-scan +++ b/bash/virt-alignment-scan @@ -204,3 +204,9 @@ _virt_sysprep () _guestfs_virttools "virt-sysprep" 0 } && complete -o default -F _virt_sysprep virt-sysprep + +_virt_tail () +{ + _guestfs_virttools "virt-tail" 1 +} && +complete -o default -F _virt_tail virt-tail diff --git a/cat/Makefile.am b/cat/Makefile.am index 796e808..02a8064 100644 --- a/cat/Makefile.am +++ b/cat/Makefile.am @@ -1,4 +1,4 @@ -# libguestfs virt-cat, virt-filesystems, virt-log and virt-ls. +# libguestfs virt-cat, virt-filesystems, virt-log, virt-ls and virt-tail. # Copyright (C) 2010-2016 Red Hat Inc. # # This program is free software; you can redistribute it and/or modify @@ -26,9 +26,10 @@ EXTRA_DIST = \ test-virt-log.sh \ virt-log.pod \ test-virt-ls.sh \ - virt-ls.pod + virt-ls.pod \ + virt-tail.pod -bin_PROGRAMS = virt-cat virt-filesystems virt-log virt-ls +bin_PROGRAMS = virt-cat virt-filesystems virt-log virt-ls virt-tail SHARED_SOURCE_FILES = \ ../fish/windows.h \ @@ -132,14 +133,39 @@ virt_ls_LDADD = \ $(LTLIBINTL) \ ../gnulib/lib/libgnu.la +virt_tail_SOURCES = \ + $(SHARED_SOURCE_FILES) \ + tail.c + +virt_tail_CPPFLAGS = \ + -DGUESTFS_WARN_DEPRECATED=1 \ + -DLOCALEBASEDIR=\""$(datadir)/locale"\" \ + -I$(top_srcdir)/src -I$(top_builddir)/src \ + -I$(top_srcdir)/fish \ + -I$(srcdir)/../gnulib/lib -I../gnulib/lib + +virt_tail_CFLAGS = \ + $(WARN_CFLAGS) $(WERROR_CFLAGS) \ + $(LIBXML2_CFLAGS) + +virt_tail_LDADD = \ + $(top_builddir)/src/libutils.la \ + $(top_builddir)/src/libguestfs.la \ + $(top_builddir)/fish/libfishcommon.la \ + $(LIBXML2_LIBS) \ + $(LIBVIRT_LIBS) \ + $(LTLIBINTL) \ + ../gnulib/lib/libgnu.la + # Manual pages and HTML files for the website. -man_MANS = virt-cat.1 virt-filesystems.1 virt-log.1 virt-ls.1 +man_MANS = virt-cat.1 virt-filesystems.1 virt-log.1 virt-ls.1 virt-tail.1 noinst_DATA = \ $(top_builddir)/website/virt-cat.1.html \ $(top_builddir)/website/virt-filesystems.1.html \ $(top_builddir)/website/virt-log.1.html \ - $(top_builddir)/website/virt-ls.1.html + $(top_builddir)/website/virt-ls.1.html \ + $(top_builddir)/website/virt-tail.1.html virt-cat.1 $(top_builddir)/website/virt-cat.1.html: stamp-virt-cat.pod @@ -185,6 +211,17 @@ stamp-virt-ls.pod: virt-ls.pod $< touch $@ +virt-tail.1 $(top_builddir)/website/virt-tail.1.html: stamp-virt-tail.pod + +stamp-virt-tail.pod: virt-tail.pod + $(PODWRAPPER) \ + --man virt-tail.1 \ + --html $(top_builddir)/website/virt-tail.1.html \ + --license GPLv2+ \ + --warning safe \ + $< + touch $@ + # Tests. TESTS_ENVIRONMENT = $(top_builddir)/run --test diff --git a/cat/tail.c b/cat/tail.c new file mode 100644 index 0000000..0066be7 --- /dev/null +++ b/cat/tail.c @@ -0,0 +1,502 @@ +/* virt-tail + * Copyright (C) 2016 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 <config.h> + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <inttypes.h> +#include <unistd.h> +#include <getopt.h> +#include <signal.h> +#include <errno.h> +#include <error.h> +#include <locale.h> +#include <assert.h> +#include <libintl.h> +#include <sys/types.h> +#include <sys/stat.h> + +#include "getprogname.h" +#include "ignore-value.h" + +#include "guestfs.h" +#include "options.h" +#include "display-options.h" +#include "windows.h" + +/* Currently open libguestfs handle. */ +guestfs_h *g; + +int read_only = 1; +int live = 0; +int verbose = 0; +int keys_from_stdin = 0; +int echo_keys = 0; +const char *libvirt_uri = NULL; +int inspector = 1; + +static int do_tail (int argc, char *argv[], struct drv *drvs, struct mp *mps); +static time_t disk_mtime (struct drv *drvs); +static int reopen_handle (void); + +static void __attribute__((noreturn)) +usage (int status) +{ + if (status != EXIT_SUCCESS) + fprintf (stderr, _("Try `%s --help' for more information.\n"), + getprogname ()); + else { + printf (_("%s: follow (tail) files in a virtual machine\n" + "Copyright (C) 2016 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" + " -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" + " -f|--follow Ignored for compatibility with tail\n" + " --format[=raw|..] Force disk format for -a option\n" + " --help Display brief help\n" + " --keys-from-stdin Read passphrases from stdin\n" + " -m|--mount dev[:mnt[:opts[:fstype]]]\n" + " Mount dev on mnt (if omitted, /)\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"), + getprogname (), getprogname (), + getprogname (), getprogname ()); + } + exit (status); +} + +int +main (int argc, char *argv[]) +{ + setlocale (LC_ALL, ""); + bindtextdomain (PACKAGE, LOCALEBASEDIR); + textdomain (PACKAGE); + + enum { HELP_OPTION = CHAR_MAX + 1 }; + + static const char options[] = "a:c:d:fm:vVx"; + static const struct option long_options[] = { + { "add", 1, 0, 'a' }, + { "connect", 1, 0, 'c' }, + { "domain", 1, 0, 'd' }, + { "echo-keys", 0, 0, 0 }, + { "follow", 0, 0, 'f' }, + { "format", 2, 0, 0 }, + { "help", 0, 0, HELP_OPTION }, + { "keys-from-stdin", 0, 0, 0 }, + { "long-options", 0, 0, 0 }, + { "mount", 1, 0, 'm' }, + { "short-options", 0, 0, 0 }, + { "verbose", 0, 0, 'v' }, + { "version", 0, 0, 'V' }, + { 0, 0, 0, 0 } + }; + struct drv *drvs = NULL; + struct mp *mps = NULL; + struct mp *mp; + char *p; + const char *format = NULL; + bool format_consumed = true; + int c; + int r; + int option_index; + + g = guestfs_create (); + if (g == NULL) + error (EXIT_FAILURE, errno, "guestfs_create"); + + 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, "short-options")) + display_short_options (options); + else 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")) { + OPTION_format; + } else + error (EXIT_FAILURE, 0, + _("unknown long option: %s (%d)"), + long_options[option_index].name, option_index); + break; + + case 'a': + OPTION_a; + break; + + case 'c': + OPTION_c; + break; + + case 'd': + OPTION_d; + break; + + case 'f': + /* ignored */ + break; + + case 'm': + OPTION_m; + inspector = 0; + break; + + 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); + } + } + + /* 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 == 1); + assert (inspector == 1 || mps != NULL); + assert (live == 0); + + /* User must specify at least one filename on the command line. */ + if (optind >= argc || argc - optind < 1) { + fprintf (stderr, _("%s: error: missing filenames on command line.\n" + "Please specify at least one file to follow.\n"), + getprogname ()); + usage (EXIT_FAILURE); + } + + CHECK_OPTION_format_consumed; + + /* User must have specified some drives. */ + if (drvs == NULL) { + fprintf (stderr, _("%s: error: you must specify at least one -a or -d option.\n"), + getprogname ()); + usage (EXIT_FAILURE); + } + + r = do_tail (argc - optind, &argv[optind], drvs, mps); + + free_drives (drvs); + free_mps (mps); + + guestfs_close (g); + + exit (r == 0 ? EXIT_SUCCESS : EXIT_FAILURE); +} + +struct follow { + int64_t mtime; /* For each file, last mtime. */ + int64_t size; /* For each file, last size. */ +}; + +static sig_atomic_t quit = 0; + +static void +user_cancel (int sig) +{ + quit = 1; + ignore_value (guestfs_user_cancel (g)); +} + +static int +do_tail (int argc, char *argv[], /* list of files in the guest */ + struct drv *drvs, struct mp *mps) +{ + struct sigaction sa; + time_t drvt; + int first_iteration = 1; + int prev_file_displayed = -1; + CLEANUP_FREE struct follow *file = NULL; + + /* Allocate storage to track each file. */ + file = calloc (argc, sizeof (struct follow)); + + /* We loop until the user hits ^C. */ + memset (&sa, 0, sizeof sa); + sa.sa_handler = user_cancel; + sa.sa_flags = SA_RESTART; + sigaction (SIGINT, &sa, NULL); + sigaction (SIGQUIT, &sa, NULL); + + if (guestfs_set_pgroup (g, 1) == -1) + exit (EXIT_FAILURE); + + drvt = disk_mtime (drvs); + if (drvt == (time_t)-1) + return -1; + + while (!quit) { + time_t t; + int i; + int windows = 0; + char *root; + CLEANUP_FREE_STRING_LIST char **roots = NULL; + int processed; + + /* Add drives, inspect and mount. */ + add_drives (drvs, 'a'); + + if (guestfs_launch (g) == -1) + return -1; + + if (mps != NULL) + mount_mps (mps); + else + inspect_mount (); + + if (inspector) { + /* Get root mountpoint. See: fish/inspect.c:inspect_mount */ + roots = guestfs_inspect_get_roots (g); + + assert (roots); + assert (roots[0] != NULL); + assert (roots[1] == NULL); + root = roots[0]; + + /* Windows? Special handling is required. */ + windows = is_windows (g, root); + } + + /* Check files here. */ + processed = 0; + for (i = 0; i < argc; ++i) { + CLEANUP_FREE char *filename = NULL; + CLEANUP_FREE_STATNS struct guestfs_statns *stat = NULL; + + if (windows) { + filename = windows_path (g, root, filename, 1 /* readonly */); + if (filename == NULL) + return -1; /* windows_path printed an error */ + } + else { + filename = strdup (argv[i]); + if (filename == NULL) { + perror ("malloc"); + return -1; + } + } + + guestfs_push_error_handler (g, NULL, NULL); + stat = guestfs_statns (g, filename); + guestfs_pop_error_handler (g); + if (stat == NULL) { + /* There's an error. Treat ENOENT as if the file was empty size. */ + if (guestfs_last_errno (g) == ENOENT) { + time (&t); + file[i].mtime = t; + file[i].size = 0; + } + else { + fprintf (stderr, "%s: %s: %s\n", + getprogname (), filename, guestfs_last_error (g)); + return -1; + } + } + else { + CLEANUP_FREE_STRING_LIST char **lines = NULL; + CLEANUP_FREE char *content = NULL; + + processed++; + + /* We believe the guest mtime to mean the file changed. This + * can include the file changing but the size staying the same, + * so be careful. + */ + if (file[i].mtime != stat->st_mtime_sec || + file[i].size != stat->st_size) { + /* If we get here, the file changed and we're going to display + * something. If there is more than one file, and the file + * displayed is different from previously, then display the + * filename banner. + */ + if (i != prev_file_displayed) + printf ("\n\n--- %s ---\n\n", filename); + prev_file_displayed = i; + + /* If the file grew, display all the new content unless + * it's a lot, in which case display the last few lines. + * If the file shrank, display the last few lines. + * If the file stayed the same size [note that the file + * has changed -- see above], redisplay the last few lines. + */ + if (stat->st_size > file[i].size + 10000) { /* grew a lot */ + goto show_tail; + } + else if (stat->st_size > file[i].size) { /* grew a bit */ + int count = stat->st_size - file[i].size; + size_t r; + guestfs_push_error_handler (g, NULL, NULL); + content = guestfs_pread (g, filename, count, file[i].size, &r); + guestfs_pop_error_handler (g); + if (content) { + size_t j; + for (j = 0; j < r; ++j) + putchar (content[j]); + } + } + else if (stat->st_size <= file[i].size) { /* shrank or same size */ + show_tail: + guestfs_push_error_handler (g, NULL, NULL); + lines = guestfs_tail (g, filename); + guestfs_pop_error_handler (g); + if (lines) { + size_t j; + for (j = 0; lines[j] != NULL; ++j) + puts (lines[j]); + } + } + + fflush (stdout); + + file[i].mtime = stat->st_mtime_sec; + file[i].size = stat->st_size; + } + } + } + + /* If no files were found, exit. If this is the first iteration + * of the loop, then this is an error, otherwise it's an ordinary + * exit when all files get deleted (see man page). + */ + if (processed == 0) { + if (first_iteration) { + fprintf (stderr, + _("%s: error: none of the files were found in the disk image\n"), + getprogname ()); + return -1; + } + else { + printf (_("%s: all files deleted, exiting\n"), getprogname ()); + return 0; + } + } + + /* Do nothing until something happens on the disk image. Even if + * the drive changes, always wait min. 30 seconds. For libvirt + * (-d) and remote sources we cannot check this so we have to use + * a fixed (5 minute) delay instead. Also we recheck every so + * often even if nothing seems to have changed. (XXX Can we do + * better?) + */ + for (i = 0; i < 10 /* 30 seconds * 10 = 5 mins */; ++i) { + time (&t); + sleep (30); + drvt = disk_mtime (drvs); + if (drvt == (time_t)-1) + return -1; + if (drvt-t < 30) break; + } + + if (reopen_handle () == -1) + return -1; + + first_iteration = 0; + } + + return 0; +} + +/* Return the latest (highest) mtime of any local drive in the list of + * drives passed on the command line. If there are no such drives + * (eg. the guest is libvirt or remote) then this returns 0. If there + * is an error it returns (time_t)-1. + */ +static time_t +disk_mtime (struct drv *drvs) +{ + time_t ret; + + if (drvs == NULL) + return 0; + + ret = disk_mtime (drvs->next); + if (ret == (time_t)-1) + return -1; + + if (drvs->type == drv_a) { + struct stat statbuf; + + if (stat (drvs->a.filename, &statbuf) == -1) { + error (0, errno, "stat: %s", drvs->a.filename); + return -1; + } + + if (statbuf.st_mtime > ret) + ret = statbuf.st_mtime; + } + /* XXX "look into" libvirt guests for local drives. */ + + return ret; +} + +/* Reopen the handle. Open the new handle first and copy some + * settings across. We only need to copy settings which are set + * somewhere in the code above, eg by OPTION_v. Settings from + * environment variables will be recreated by guestfs_create. + * + * The global 'g' must never be unset or NULL (visible to code outside + * this function). + */ +static int +reopen_handle (void) +{ + guestfs_h *g2; + + g2 = guestfs_create (); + if (g2 == NULL) { + perror ("guestfs_create"); + return -1; + } + + guestfs_set_verbose (g2, guestfs_get_verbose (g)); + guestfs_set_trace (g2, guestfs_get_trace (g)); + guestfs_set_pgroup (g2, guestfs_get_pgroup (g)); + + guestfs_close (g); + g = g2; + + return 0; +} diff --git a/cat/test-docs.sh b/cat/test-docs.sh index a0ffc61..d8ac358 100755 --- a/cat/test-docs.sh +++ b/cat/test-docs.sh @@ -24,3 +24,4 @@ $srcdir/../podcheck.pl virt-filesystems.pod virt-filesystems $srcdir/../podcheck.pl virt-log.pod virt-log $srcdir/../podcheck.pl virt-ls.pod virt-ls \ --ignore=--checksums,--extra-stat,--time,--uid +$srcdir/../podcheck.pl virt-tail.pod virt-tail diff --git a/cat/virt-cat.pod b/cat/virt-cat.pod index 87b0e13..a81f4f4 100644 --- a/cat/virt-cat.pod +++ b/cat/virt-cat.pod @@ -202,6 +202,8 @@ To list out the log files from guests, see the related tool L<virt-log(1)>. It understands binary log formats such as the systemd journal. +To follow (tail) text log files, use L<virt-tail(1)>. + =head1 WINDOWS PATHS C<virt-cat> has a limited ability to understand Windows drive letters @@ -277,6 +279,7 @@ L<guestfish(1)>, L<virt-copy-out(1)>, L<virt-edit(1)>, L<virt-log(1)>, +L<virt-tail(1)>, L<virt-tar-out(1)>, L<http://libguestfs.org/>. diff --git a/cat/virt-log.pod b/cat/virt-log.pod index a85d0ee..d9a975e 100644 --- a/cat/virt-log.pod +++ b/cat/virt-log.pod @@ -17,9 +17,10 @@ This tool understands and displays both plain text log files (eg. F</var/log/messages>) and binary formats such as the systemd journal. -To display other types of files, use L<virt-cat(1)>. To copy files -out of a virtual machine, use L<virt-copy-out(1)>. To display the -contents of the Windows Registry, use L<virt-win-reg(1)>. +To display other types of files, use L<virt-cat(1)>. To follow (tail) +text log files, use L<virt-tail(1)>. To copy files out of a virtual +machine, use L<virt-copy-out(1)>. To display the contents of the +Windows Registry, use L<virt-win-reg(1)>. =head1 EXAMPLES @@ -138,6 +139,7 @@ L<guestfs(3)>, L<guestfish(1)>, L<virt-cat(1)>, L<virt-copy-out(1)>, +L<virt-tail(1)>, L<virt-tar-out(1)>, L<virt-win-reg(1)>, L<http://libguestfs.org/>. diff --git a/cat/virt-tail.pod b/cat/virt-tail.pod new file mode 100644 index 0000000..4a53553 --- /dev/null +++ b/cat/virt-tail.pod @@ -0,0 +1,253 @@ +=head1 NAME + +virt-tail - Follow (tail) files in a virtual machine + +=head1 SYNOPSIS + + virt-tail [--options] -d domname file [file ...] + + virt-tail [--options] -a disk.img [-a disk.img ...] file [file ...] + +=head1 DESCRIPTION + +C<virt-tail> is a command line tool to follow (tail) the contents of +C<file> where C<file> exists in the named virtual machine (or disk +image). It is similar to the ordinary command S<C<tail -f>>. + +Multiple filenames can be given, in which case each is followed +separately. Each filename must be a full path, starting at the root +directory (starting with '/'). + +The command keeps running until: + +=over 4 + +=item * + +The user presses the ^C or an interrupt signal is received. + +=item * + +None of the listed files was found in the guest, or they +all get deleted. + +=item * + +There is an unrecoverable error. + +=back + +=head1 EXAMPLE + +Follow F</var/log/messages> inside a virtual machine called C<mydomain>: + + virt-tail -d mydomain /etc/fstab + +=head1 OPTIONS + +=over 4 + +=item B<--help> + +Display brief help. + +=item B<-a> file + +=item B<--add> file + +Add I<file> 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<-a URI> + +=item B<--add URI> + +Add a remote disk. See L<guestfish(1)/ADDING REMOTE STORAGE>. + +=item B<-c> URI + +=item B<--connect> URI + +If using libvirt, connect to the given I<URI>. If omitted, then we +connect to the default libvirt hypervisor. + +If you specify guest block devices directly (I<-a>), 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-tail 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<-f> + +=item B<--follow> + +This option is ignored. virt-tail always behaves like +S<L<tail(1)> I<-f>>. You don't need to specify the I<-f> option. + +=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-tail --format=raw -a disk.img file + +forces raw format (no auto-detection) for F<disk.img>. + + virt-tail --format=raw -a disk.img --format -a another.img file + +forces raw format (no auto-detection) for F<disk.img> and reverts to +auto-detection for F<another.img>. + +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<--keys-from-stdin> + +Read key or passphrase parameters from stdin. The default is +to try to read passphrases from the user by opening F</dev/tty>. + +=item B<-m> dev[:mountpoint[:options[:fstype]]] + +=item B<--mount> dev[:mountpoint[:options[:fstype]]] + +Mount the named partition or logical volume on the given mountpoint. + +If the mountpoint is omitted, it defaults to F</>. + +Specifying any mountpoint disables the inspection of the guest and +the mount of its root and all of its mountpoints, so make sure +to mount all the mountpoints needed to work with the filenames +given as arguments. + +If you don't know what filesystems a disk image contains, you can +either run guestfish without this option, then list the partitions, +filesystems and LVs available (see L</list-partitions>, +L</list-filesystems> and L</lvs> commands), or you can use the +L<virt-filesystems(1)> program. + +The third (and rarely used) part of the mount parameter is the list of +mount options used to mount the underlying filesystem. If this is not +given, then the mount options are either the empty string or C<ro> +(the latter if the I<--ro> flag is used). By specifying the mount +options, you override this default choice. Probably the only time you +would use this is to enable ACLs and/or extended attributes if the +filesystem can support them: + + -m /dev/sda1:/:acl,user_xattr + +Using this flag is equivalent to using the C<mount-options> command. + +The fourth part of the parameter is the filesystem driver to use, such +as C<ext3> or C<ntfs>. This is rarely needed, but can be useful if +multiple drivers are valid for a filesystem (eg: C<ext2> and C<ext3>), +or if libguestfs misidentifies a filesystem. + +=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 LOG FILES + +To list out the log files from guests, see the related tool +L<virt-log(1)>. It understands binary log formats such as the systemd +journal. + +=head1 WINDOWS PATHS + +C<virt-tail> has a limited ability to understand Windows drive letters +and paths (eg. F<E:\foo\bar.txt>). + +If and only if the guest is running Windows then: + +=over 4 + +=item * + +Drive letter prefixes like C<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 displayed. + +=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 EXIT STATUS + +This program returns 0 if successful, or non-zero if there was an +error. + +=head1 SEE ALSO + +L<guestfs(3)>, +L<guestfish(1)>, +L<virt-copy-out(1)>, +L<virt-cat(1)>, +L<virt-log(1)>, +L<virt-tar-out(1)>, +L<tail(1)>, +L<http://libguestfs.org/>. + +=head1 AUTHOR + +Richard W.M. Jones L<http://people.redhat.com/~rjones/> + +=head1 COPYRIGHT + +Copyright (C) 2016 Red Hat Inc. diff --git a/docs/guestfs-hacking.pod b/docs/guestfs-hacking.pod index 6b7ac1c..46df37f 100644 --- a/docs/guestfs-hacking.pod +++ b/docs/guestfs-hacking.pod @@ -73,8 +73,8 @@ L<virt-builder(1)> command and documentation. =item F<cat> -The L<virt-cat(1)>, L<virt-filesystems(1)>, L<virt-log(1)> -and L<virt-ls(1)> commands and documentation. +The L<virt-cat(1)>, L<virt-filesystems(1)>, L<virt-log(1)>, +L<virt-ls(1)> and L<virt-tail(1)> commands and documentation. =item F<contrib> diff --git a/fish/guestfish.pod b/fish/guestfish.pod index b914449..b08f172 100644 --- a/fish/guestfish.pod +++ b/fish/guestfish.pod @@ -1623,6 +1623,7 @@ L<virt-rescue(1)>, L<virt-resize(1)>, L<virt-sparsify(1)>, L<virt-sysprep(1)>, +L<virt-tail(1)>, L<virt-tar(1)>, L<virt-tar-in(1)>, L<virt-tar-out(1)>, diff --git a/src/guestfs.pod b/src/guestfs.pod index 864b9db..bdc470b 100644 --- a/src/guestfs.pod +++ b/src/guestfs.pod @@ -3519,6 +3519,7 @@ L<virt-rescue(1)>, L<virt-resize(1)>, L<virt-sparsify(1)>, L<virt-sysprep(1)>, +L<virt-tail(1)>, L<virt-tar(1)>, L<virt-tar-in(1)>, L<virt-tar-out(1)>, diff --git a/tools/virt-win-reg b/tools/virt-win-reg index 57188c8..18100e7 100755 --- a/tools/virt-win-reg +++ b/tools/virt-win-reg @@ -790,6 +790,7 @@ L<hivexregedit(1)>, L<guestfs(3)>, L<guestfish(1)>, L<virt-cat(1)>, +L<virt-tail(1)>, L<Sys::Guestfs(3)>, L<Win::Hivex(3)>, L<Win::Hivex::Regedit(3)>, diff --git a/website/index.html.in b/website/index.html.in index 05b5112..6d43941 100644 --- a/website/index.html.in +++ b/website/index.html.in @@ -101,6 +101,7 @@ on <a href="http://freenode.net/">FreeNode</a>. <a href="virt-resize.1.html">virt-resize(1)</a> — resize virtual machines <br/> <a href="virt-sparsify.1.html">virt-sparsify(1)</a> — make virtual machines sparse (thin-provisioned) <br/> <a href="virt-sysprep.1.html">virt-sysprep(1)</a> — unconfigure a virtual machine before cloning <br/> +<a href="virt-tail.1.html">virt-tail(1)</a> — follow log file <br/> <a href="virt-tar.1.html">virt-tar(1)</a> — archive and upload files <br/> <a href="virt-tar-in.1.html">virt-tar-in(1)</a> — archive and upload files <br/> <a href="virt-tar-out.1.html">virt-tar-out(1)</a> — archive and download files <br/> -- 2.9.3
Richard W.M. Jones
2016-Oct-03 14:07 UTC
[Libguestfs] [PATCH v3 2/2] Add a test for virt-tail.
--- cat/Makefile.am | 4 +- cat/test-virt-tail.sh | 116 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 1 deletion(-) create mode 100755 cat/test-virt-tail.sh diff --git a/cat/Makefile.am b/cat/Makefile.am index 02a8064..38bfd01 100644 --- a/cat/Makefile.am +++ b/cat/Makefile.am @@ -27,6 +27,7 @@ EXTRA_DIST = \ virt-log.pod \ test-virt-ls.sh \ virt-ls.pod \ + test-virt-tail.sh \ virt-tail.pod bin_PROGRAMS = virt-cat virt-filesystems virt-log virt-ls virt-tail @@ -234,7 +235,8 @@ TESTS += \ test-virt-cat.sh \ test-virt-filesystems.sh \ test-virt-log.sh \ - test-virt-ls.sh + test-virt-ls.sh \ + test-virt-tail.sh endif ENABLE_APPLIANCE check-valgrind: diff --git a/cat/test-virt-tail.sh b/cat/test-virt-tail.sh new file mode 100755 index 0000000..518bbf7 --- /dev/null +++ b/cat/test-virt-tail.sh @@ -0,0 +1,116 @@ +#!/bin/bash - +# libguestfs +# Copyright (C) 2016 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. + +# To test virt-tail, we run a guestfish instance which creates a disk +# and a file in that disk. We then run virt-tail in parallel. Back +# in the guestfish instance we append to the file, and we check that +# the addenda are displayed by virt-tail. + +export LANG=C +set -e +set -x + +# Libvirt screws with the SELinux labels, preventing guestfish from +# continuing to write to the original disk. Therefore only run this +# test when using direct access. +if [ "$(guestfish get-backend)" != "direct" ]; then + echo "$0: test skipped because default backend is not 'direct'" + exit 77 +fi + +out=test-virt-tail.out +disk=test-virt-tail.disk + +rm -f $out $disk + +tailpid=0 + +eval `guestfish --listen` + +# Clean up if the script is killed or exits early. +cleanup () +{ + status=$? + set +e + guestfish --remote exit + if [ "$tailpid" -gt 0 ]; then kill "$tailpid"; fi + + # Don't delete the output files if non-zero exit. + if [ "$status" -eq 0 ]; then rm -f $disk $out; fi + + exit $status +} +trap cleanup INT QUIT TERM EXIT ERR + +# Create the output disk. +guestfish --remote sparse $disk 10M +guestfish --remote run +guestfish --remote part-disk /dev/sda mbr +guestfish --remote mkfs ext2 /dev/sda1 +guestfish --remote mount /dev/sda1 / + +# Create the file to be tailed with a single full line of content. +guestfish --remote write /tail 'line 1 +' +guestfish --remote sync + +# Run virt-tail in the background +$VG virt-tail -a $disk -m /dev/sda1 /tail > $out & +tailpid=$! + +# Wait for the first line of the tailed file to appear. +# Note we can wait up to 10 minutes here to deal with slow machines. +for retry in `seq 0 60`; do + if grep -sq "line 1" $out; then break; fi + sleep 10; +done +if [ "$retry" -ge 60 ]; then + echo "$0: error: initial line of output did not appear" + exit 1 +fi + +# Write some more lines to the file. +guestfish --remote write-append /tail 'line 2 +line 3 +' +guestfish --remote sync + +# Wait for new content to appear. +for retry in `seq 0 60`; do + if grep -sq "line 3" $out; then break; fi + sleep 10; +done +if [ "$retry" -ge 60 ]; then + echo "$0: error: continued output did not appear" + exit 1 +fi + +# Delete the file. This should cause virt-tail to exit gracefully. +guestfish --remote rm /tail + +# Wait for virt-tail to finish and check the status. +wait "$tailpid" +tailstatus=$? +tailpid=0 +if [ "$tailstatus" -ne 0 ]; then + echo "$0: error: non-zero exit status from virt-tail: $tailstatus" + exit 1 +fi + +# cleanup() is called implicitly which cleans up everything. +exit 0 -- 2.9.3
On Monday, 3 October 2016 15:07:10 CEST Richard W.M. Jones wrote:> This follows (tails) a log file within a guest, rather like > the regular 'tail -f' command. For example: > > virt-tail -d guest /var/log/messages > ---Mostly LGTM, just a few notes.> + guestfs_push_error_handler (g, NULL, NULL); > + stat = guestfs_statns (g, filename); > + guestfs_pop_error_handler (g); > + if (stat == NULL) { > + /* There's an error. Treat ENOENT as if the file was empty size. */ > + if (guestfs_last_errno (g) == ENOENT) { > + time (&t); > + file[i].mtime = t; > + file[i].size = 0;I'd set size as -1, otherwise this is considering the file as existing. If virt-tail (wants to) behaves like `tail -f`, I'd: - print an error/message line (not fatal) to warn that a file does not exist - when it appears, print that and start following it> + /* If we get here, the file changed and we're going to display > + * something. If there is more than one file, and the file > + * displayed is different from previously, then display the > + * filename banner. > + */ > + if (i != prev_file_displayed) > + printf ("\n\n--- %s ---\n\n", filename); > + prev_file_displayed = i;I'd simplify the check as "if (argc > 1)", and remove the "prev_file_displayed" variable (not used otherwise).> + /* Do nothing until something happens on the disk image. Even if > + * the drive changes, always wait min. 30 seconds. For libvirt > + * (-d) and remote sources we cannot check this so we have to use > + * a fixed (5 minute) delay instead. Also we recheck every so > + * often even if nothing seems to have changed. (XXX Can we do > + * better?)Should -s/--sleep-interval be available too, then? Thanks, -- Pino Toscano