Nothing new in the virt-tail command itself, but the second commit includes a simple test. 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 | 498 +++++++++++++++++++++++++++++++++++++++++++++++
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, 820 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..85aa471
--- /dev/null
+++ b/cat/tail.c
@@ -0,0 +1,498 @@
+/* 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 add_and_mount (struct drv *drvs, struct mp *mps, int *windows_ret);
+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)
+ 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 int 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;
+ int processed;
+
+ if (add_and_mount (drvs, mps, &windows) == -1)
+ return -1;
+
+ /* Check files here. */
+ processed = 0;
+ for (i = 0; i < argc; ++i) {
+ CLEANUP_FREE_STATNS struct guestfs_statns *stat = NULL;
+
+ guestfs_push_error_handler (g, NULL, NULL);
+ stat = guestfs_statns (g, argv[i]);
+ 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 (), argv[i], 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", argv[i]);
+ 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, argv[i], 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, argv[i]);
+ 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;
+}
+
+/* Add drives, inspect and mount. */
+static int
+add_and_mount (struct drv *drvs, struct mp *mps, int *windows_ret)
+{
+ int windows = 0;
+ char *root;
+ CLEANUP_FREE_STRING_LIST char **roots = NULL;
+
+ 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);
+ }
+
+ *windows_ret = windows;
+
+ 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 12:27 UTC
[Libguestfs] [PATCH v2 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 13:27:13 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 > --- > .gitignore | 3 + > bash/Makefile.am | 4 +- > bash/virt-alignment-scan | 6 + > cat/Makefile.am | 47 ++++- > cat/tail.c | 498 +++++++++++++++++++++++++++++++++++++++++++++++ > 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, 820 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..85aa471 > --- /dev/null > +++ b/cat/tail.c > @@ -0,0 +1,498 @@ > +/* 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 add_and_mount (struct drv *drvs, struct mp *mps, int *windows_ret); > +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) > + usage (EXIT_FAILURE);I'd add: fprintf (stderr, _("%s: error: missing filenames on command line.\n" "Please specify at least one file to follow.\n"), getprogname ()); so there is a better error message.> + > + 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 int quit = 0;I guess this could be better as sig_atomic_t.> +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; > + int processed; > + > + if (add_and_mount (drvs, mps, &windows) == -1) > + return -1; > + > + /* Check files here. */ > + processed = 0; > + for (i = 0; i < argc; ++i) { > + CLEANUP_FREE_STATNS struct guestfs_statns *stat = NULL; > + > + guestfs_push_error_handler (g, NULL, NULL); > + stat = guestfs_statns (g, argv[i]); > + 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 (), argv[i], 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", argv[i]); > + 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, argv[i], 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, argv[i]); > + 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; > +} > + > +/* Add drives, inspect and mount. */ > +static int > +add_and_mount (struct drv *drvs, struct mp *mps, int *windows_ret) > +{The 'windows_ret' out parameter does not seem used outside, so could be better to drop it for now/> + int windows = 0; > + char *root; > + CLEANUP_FREE_STRING_LIST char **roots = NULL; > + > + 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); > + } > + > + *windows_ret = windows; > + > + 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);This may be better written as simple non-recursive loop, to avoid neverending loop, and make the code slightly more understandable to follow (IMHO).> + 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/> >-- Pino Toscano