[Libguestfs] [PATCH v2 1/2] New tool: virt-tail.

Richard W.M. Jones
2016-10-03T12:27:13Z

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.
virt-resize(1)
&mdash; resize virtual machines
virt-sparsify(1)
&mdash; make virtual machines sparse (thin-provisioned)
virt-sysprep(1)
&mdash; unconfigure a virtual machine before cloning
+virt-tail(1) &mdash;
follow log file
virt-tar(1) &mdash;
archive and upload files
virt-tar-in(1)
&mdash; archive and upload files
virt-tar-out(1)
&mdash; archive and download files
--
2.9.3