[Libguestfs] [PATCH v3 2/4] inspection: Add inspection rules and supporting code.

Richard W.M. Jones rjones at redhat.com
Tue Feb 23 12:50:45 UTC 2016


Using the rules compiler added in the previous commit, create an
inspection program ("guestfs-inspection").  This will run inside the
appliance.  It is partly written in the Prolog-inspired language, with
C supporting functions.

For more details, read the guestfs-inspection(8) man page added in
this commit.
---
 .gitignore                        |   4 +
 Makefile.am                       |   2 +
 appliance/Makefile.am             |   3 +-
 configure.ac                      |   1 +
 docs/guestfs-hacking.pod          |   6 +
 generator/Makefile.am             |   2 +-
 generator/main.ml                 |   4 +
 inspection/Makefile.am            |  92 ++++++
 inspection/detect.c               | 152 ++++++++++
 inspection/facts.c                | 310 ++++++++++++++++++++
 inspection/guestfs-inspection.pod | 450 ++++++++++++++++++++++++++++
 inspection/inspection.c           | 106 +++++++
 inspection/inspection.h           |  60 ++++
 inspection/inspection.rules       | 476 ++++++++++++++++++++++++++++++
 inspection/match.c                | 173 +++++++++++
 inspection/mount.c                | 597 ++++++++++++++++++++++++++++++++++++++
 inspection/rules.h                |  69 +++++
 inspection/stringsbuf.c           | 254 ++++++++++++++++
 inspection/stringsbuf.h           |  56 ++++
 inspection/utils.c                |  66 +++++
 po/POTFILES                       |   7 +
 src/guestfs.pod                   |   1 +
 22 files changed, 2889 insertions(+), 2 deletions(-)
 create mode 100644 inspection/Makefile.am
 create mode 100644 inspection/detect.c
 create mode 100644 inspection/facts.c
 create mode 100644 inspection/guestfs-inspection.pod
 create mode 100644 inspection/inspection.c
 create mode 100644 inspection/inspection.h
 create mode 100644 inspection/inspection.rules
 create mode 100644 inspection/match.c
 create mode 100644 inspection/mount.c
 create mode 100644 inspection/rules.h
 create mode 100644 inspection/stringsbuf.c
 create mode 100644 inspection/stringsbuf.h
 create mode 100644 inspection/utils.c

diff --git a/.gitignore b/.gitignore
index 67d8a2e..8da932c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -252,6 +252,10 @@ Makefile.in
 /haskell/Guestfs030Config
 /haskell/Guestfs050LVCreate
 /haskell/Guestfs.hs
+/inspection/guestfs-inspection
+/inspection/guestfs-inspection.8
+/inspection/rules.c
+/inspection/stamp-guestfs-inspection.pod
 /inspector/actual-*.xml
 /inspector/stamp-virt-inspector.pod
 /inspector/test-xmllint.sh
diff --git a/Makefile.am b/Makefile.am
index ba99feb..11f5e78 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -42,6 +42,7 @@ SUBDIRS += src docs examples po
 
 # The daemon and the appliance.
 if ENABLE_DAEMON
+SUBDIRS += inspection
 SUBDIRS += daemon
 SUBDIRS += tests/daemon
 endif
@@ -290,6 +291,7 @@ all-local:
 	find $(DIST_SUBDIRS) -name '*.c' -o -name '*.pl' -o -name '*.pm' | \
 	grep -v -E '^(examples|gnulib|gobject/docs|perl/(blib|examples)|po-docs|tests|test-data)/' | \
 	grep -v -E '/((guestfs|rc)_protocol\.c)$$' | \
+	grep -v -E '^inspection/rules\.c$$' | \
 	grep -v -E '^python/utils\.c$$' | \
 	LC_ALL=C sort > po/POTFILES
 	cd $(srcdir); \
diff --git a/appliance/Makefile.am b/appliance/Makefile.am
index d8fb15b..eb1f4d3 100644
--- a/appliance/Makefile.am
+++ b/appliance/Makefile.am
@@ -72,11 +72,12 @@ packagelist: packagelist.in Makefile
 	cmp -s $@ $@-t || mv $@-t $@
 	rm -f $@-t
 
-supermin.d/daemon.tar.gz: ../daemon/guestfsd guestfs_lvm_conf.aug guestfs_shadow.aug
+supermin.d/daemon.tar.gz: ../daemon/guestfsd ../inspection/guestfs-inspection guestfs_lvm_conf.aug guestfs_shadow.aug
 	rm -f $@ $@-t
 	rm -rf tmp-d
 	mkdir -p tmp-d$(DAEMON_SUPERMIN_DIR) tmp-d/etc tmp-d/usr/share/guestfs
 	ln ../daemon/guestfsd tmp-d$(DAEMON_SUPERMIN_DIR)/guestfsd
+	ln ../inspection/guestfs-inspection tmp-d$(DAEMON_SUPERMIN_DIR)/guestfs-inspection
 	ln $(srcdir)/guestfs_lvm_conf.aug tmp-d/usr/share/guestfs/guestfs_lvm_conf.aug
 	ln $(srcdir)/guestfs_shadow.aug tmp-d/usr/share/guestfs/guestfs_shadow.aug
 	( cd tmp-d && tar zcf - * ) > $@-t
diff --git a/configure.ac b/configure.ac
index 29b5092..00fccbb 100644
--- a/configure.ac
+++ b/configure.ac
@@ -205,6 +205,7 @@ AC_CONFIG_FILES([Makefile
                  golang/Makefile
                  golang/examples/Makefile
                  haskell/Makefile
+                 inspection/Makefile
                  inspector/Makefile
                  java/Makefile
                  java/examples/Makefile
diff --git a/docs/guestfs-hacking.pod b/docs/guestfs-hacking.pod
index 419a4c2..4cd76bf 100644
--- a/docs/guestfs-hacking.pod
+++ b/docs/guestfs-hacking.pod
@@ -558,6 +558,11 @@ L<virt-get-kernel(1)> command and documentation.
 Gnulib is used as a portability library.  A copy of gnulib is included
 under here.
 
+=item F<inspection>
+
+Inspection.  See L<guestfs(3)/INSPECTION> and
+L<guestfs-inspection(8)>.
+
 =item F<inspector>
 
 L<virt-inspector(1)>, the virtual machine image inspector.
@@ -837,6 +842,7 @@ Optionally do a full release of the development branch.
 L<guestfs(3)>,
 L<guestfs-building(1)>,
 L<guestfs-examples(3)>,
+L<guestfs-inspection(8)>,
 L<guestfs-internals(3)>,
 L<guestfs-performance(1)>,
 L<guestfs-release-notes(1)>,
diff --git a/generator/Makefile.am b/generator/Makefile.am
index c53d3b9..210113a 100644
--- a/generator/Makefile.am
+++ b/generator/Makefile.am
@@ -194,7 +194,7 @@ noinst_DATA = stamp-generator
 # Git removes empty directories, so in cases where the
 # generator is creating the sole file in a directory, we
 # have to create the directory first.
-stamp-generator: generator
+stamp-generator: generator $(wildcard $(top_srcdir)/inspection/*.rules)
 	mkdir -p $(top_srcdir)/perl/lib/Sys
 	mkdir -p $(top_srcdir)/ruby/ext/guestfs
 	mkdir -p $(top_srcdir)/java/com/redhat/et/libguestfs
diff --git a/generator/main.ml b/generator/main.ml
index 63a5d25..4137f85 100644
--- a/generator/main.ml
+++ b/generator/main.ml
@@ -213,6 +213,10 @@ Run it from the top source directory using the command
   output_to "customize/customize-synopsis.pod" generate_customize_synopsis_pod;
   output_to "customize/customize-options.pod" generate_customize_options_pod;
 
+  (* Run the rules compiler to generate inspection rules. *)
+  output_to "inspection/rules.c"
+            (Rules_compiler.compile "inspection/inspection.rules");
+
   (* Generate the list of files generated -- last. *)
   printf "generated %d lines of code\n" (get_lines_generated ());
   let files = List.sort compare (get_files_generated ()) in
diff --git a/inspection/Makefile.am b/inspection/Makefile.am
new file mode 100644
index 0000000..5232bc3
--- /dev/null
+++ b/inspection/Makefile.am
@@ -0,0 +1,92 @@
+# libguestfs
+# Copyright (C) 2015-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 $(top_srcdir)/subdir-rules.mk
+
+generator_built = \
+	rules.c
+
+BUILT_SOURCES = \
+	$(generator_built)
+
+CLEANFILES = \
+	stamp-guestfs-inspection.pod \
+	guestfs-inspection.8
+
+# Build the inspection program.
+
+if INSTALL_DAEMON
+sbin_PROGRAMS = guestfs-inspection
+else
+noinst_PROGRAMS = guestfs-inspection
+endif
+
+guestfs_inspection_SOURCES = \
+	../daemon/cleanups.c \
+	../daemon/cleanups.h \
+	../daemon/command.c \
+	../daemon/command.h \
+	detect.c \
+	facts.c \
+	inspection.c \
+	inspection.h \
+	match.c \
+	mount.c \
+	rules.c \
+	rules.h \
+	stringsbuf.c \
+	stringsbuf.h \
+	utils.c
+
+guestfs_inspection_LDADD = \
+	$(AUGEAS_LIBS) \
+	$(HIVEX_LIBS) \
+	$(PCRE_LIBS) \
+	$(top_builddir)/gnulib/lib/.libs/libgnu.a
+
+guestfs_inspection_CPPFLAGS = \
+	-I$(top_srcdir)/gnulib/lib \
+	-I$(top_builddir)/gnulib/lib \
+	-I$(top_srcdir)/daemon \
+	-I$(top_builddir)/daemon \
+	-I$(top_srcdir)/src \
+	-I$(top_builddir)/src
+
+guestfs_inspection_CFLAGS = \
+	$(WARN_CFLAGS) $(WERROR_CFLAGS) \
+	$(AUGEAS_CFLAGS) \
+	$(HIVEX_CFLAGS) \
+	$(PCRE_CFLAGS)
+
+# Manual pages and HTML files for the website.
+if INSTALL_DAEMON
+man_MANS = guestfs-inspection.8
+else
+noinst_MANS = guestfs-inspection.8
+endif
+noinst_DATA = $(top_builddir)/website/guestfs-inspection.8.html
+
+guestfs-inspection.8 $(top_builddir)/website/guestfs-inspection.8.html: stamp-guestfs-inspection.pod
+
+stamp-guestfs-inspection.pod: guestfs-inspection.pod
+	$(PODWRAPPER) \
+	  --section 8 \
+	  --man guestfs-inspection.8 \
+	  --html $(top_builddir)/website/guestfs-inspection.8.html \
+	  --license GPLv2+ \
+	  $<
+	touch $@
diff --git a/inspection/detect.c b/inspection/detect.c
new file mode 100644
index 0000000..1ebabf0
--- /dev/null
+++ b/inspection/detect.c
@@ -0,0 +1,152 @@
+/* guestfs-inspection
+ * Copyright (C) 2009-2015 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 <unistd.h>
+#include <error.h>
+#include <errno.h>
+
+#include "ignore-value.h"
+
+#include <pcre.h>
+
+#include "guestfs-internal-all.h"
+#include "cleanups.h"
+#include "inspection.h"
+
+int
+get_distro_from_os_release (const char *fs, char **distro)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+int
+get_version_from_os_release (const char *fs, char **major, char **minor)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+int
+get_product_name_from_os_release (const char *fs, char **product_name)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+int
+get_distro_from_lsb_release (const char *fs, char **distro)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+int
+get_version_from_lsb_release (const char *fs, char **major, char **minor)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+int
+get_product_name_from_lsb_release (const char *fs, char **product_name)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+int
+get_version_from_oracle_release (const char *fs, char **major, char **minor)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+COMPILE_REGEXP (re_centos_old, "CentOS.*release (\\d+).*Update (\\d+)", 0)
+COMPILE_REGEXP (re_centos, "CentOS.*release (\\d+)\\.(\\d+)", 0)
+COMPILE_REGEXP (re_centos_no_minor, "CentOS.*release (\\d+)", 0)
+
+#define CENTOS_RELEASE_FILE "/etc/centos-release"
+
+int
+get_version_from_centos_release (const char *fs, char **major, char **minor)
+{
+  CLEANUP_FREE char *line = first_line_of_file (fs, CENTOS_RELEASE_FILE);
+
+  if (match2 (line, re_centos_old, major, minor) ||
+      match2 (line, re_centos, major, minor))
+    return 0;
+
+  if ((*major = match1 (line, re_centos_no_minor)) != NULL) {
+    *minor = strdup ("0");
+    if (*minor == NULL)
+      error (EXIT_FAILURE, errno, "strdup");
+    return 0;
+  }
+
+  fprintf (stderr, "%s: cannot parse major/minor version from file",
+           CENTOS_RELEASE_FILE);
+  return -1;
+}
+
+int
+get_version_from_altlinux_release (const char *fs, char **major, char **minor)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+int
+match_redhat_release_fedora (const char *fs)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+int
+match_redhat_release_rhel (const char *fs)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+int
+match_redhat_release_centos (const char *fs)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+int
+match_redhat_release_scientific_linux (const char *fs)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
+
+int
+get_version_from_redhat_release (const char *fs, char **major, char **minor)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return -1;
+}
diff --git a/inspection/facts.c b/inspection/facts.c
new file mode 100644
index 0000000..d4ba7d7
--- /dev/null
+++ b/inspection/facts.c
@@ -0,0 +1,310 @@
+/* guestfs-inspection
+ * Copyright (C) 2009-2015 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.
+ */
+
+/* Handle the true and false facts sets, and other helper functions. */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <string.h>
+#include <inttypes.h>
+#include <assert.h>
+#include <error.h>
+#include <errno.h>
+#include <alloca.h>
+
+#include "gl_oset.h"
+#include "gl_xoset.h"
+#include "gl_array_oset.h"
+
+#include "rules.h"
+
+/* True and false facts sets.  See guestfs-inspection(8)/WRITING RULES
+ * to understand why these are used.  The fact structs themselves are
+ * identical, we only know that a fact is false or true based on which
+ * list it appears on.
+ */
+static gl_oset_t true_facts = NULL;
+static gl_oset_t false_facts = NULL;
+
+static int
+compare_facts (const void *vf1, const void *vf2)
+{
+  size_t i;
+  int r;
+  const fact *f1 = vf1;
+  const fact *f2 = vf2;
+
+  r = strcmp (f1->term_name, f2->term_name);
+  if (r != 0) return r;
+
+  /* If term names are equal, they are supposed to have the same
+   * number of arguments.  We type-checked that when reading the
+   * source.  However, better check.
+   */
+  assert (f1->nr_term_args == f2->nr_term_args);
+
+  for (i = 0; i < f1->nr_term_args; ++i) {
+    r = strcmp (f1->term_arg[i], f2->term_arg[i]);
+    if (r != 0) return r;
+  }
+
+  return 0;
+}
+
+static void
+free_fact (const void *vf)
+{
+  /* Why is the parameter const?
+   * See Bruno Haible's explanation here:
+   * https://www.mail-archive.com/bug-gnulib@gnu.org/msg08619.html
+   */
+  fact *f = (fact *) vf;
+  size_t i;
+
+  free (f->term_name);
+  for (i = 0; i < f->nr_term_args; ++i)
+    free (f->term_arg[i]);
+  free (f);
+}
+
+static void init_facts (void) __attribute__((constructor));
+
+static void
+init_facts (void)
+{
+  true_facts = gl_oset_create_empty (GL_ARRAY_OSET, compare_facts, free_fact);
+  false_facts = gl_oset_create_empty (GL_ARRAY_OSET, compare_facts, free_fact);
+}
+
+static void free_facts (void) __attribute__((destructor));
+
+static void
+free_facts (void)
+{
+  gl_oset_free (true_facts);
+  gl_oset_free (false_facts);
+}
+
+static void
+clear_set (gl_oset_t set)
+{
+  const void *f;
+  gl_oset_iterator_t iter = gl_oset_iterator (set);
+
+  while (gl_oset_iterator_next (&iter, &f)) {
+    gl_oset_remove (set, (void *) f);
+  }
+  gl_oset_iterator_free (&iter);
+}
+
+void
+clear_true_facts (void)
+{
+  clear_set (true_facts);
+}
+
+void
+clear_false_facts (void)
+{
+  clear_set (false_facts);
+}
+
+size_t
+count_true_facts (void)
+{
+  return gl_oset_size (true_facts);
+}
+
+/* This is just for debugging facts. */
+void
+print_fact (bool is_true, const fact *f, FILE *fp)
+{
+  size_t i;
+
+  if (!is_true)
+    fputc ('!', fp);
+  fputs (f->term_name, fp);
+  if (f->nr_term_args > 0)
+    fputc ('(', fp);
+  for (i = 0; i < f->nr_term_args; ++i) {
+    fputc ('"', fp);
+    fputs (f->term_arg[i], fp);
+    fputc ('"', fp);
+    if (i+1 != f->nr_term_args)
+      fputs (", ", fp);
+  }
+  if (f->nr_term_args > 0)
+    fputc (')', fp);
+}
+
+static void
+print_set (bool is_true, gl_oset_t set)
+{
+  const void *vf;
+  gl_oset_iterator_t iter = gl_oset_iterator (set);
+
+  while (gl_oset_iterator_next (&iter, &vf)) {
+    fact *f = (fact *) vf;
+    print_fact (is_true, f, stdout);
+    printf ("\n");
+  }
+  gl_oset_iterator_free (&iter);
+}
+
+void
+print_true_facts (void)
+{
+  print_set (true, true_facts);
+}
+
+void
+print_false_facts (void)
+{
+  print_set (false, false_facts);
+}
+
+/* Look for every string parameter of every fact we know about, and
+ * add all those strings to the set.
+ */
+void
+add_all_fact_strings (gl_oset_t set)
+{
+  const void *vf;
+  fact *f;
+  gl_oset_iterator_t iter;
+  size_t i;
+
+  iter = gl_oset_iterator (true_facts);
+  while (gl_oset_iterator_next (&iter, &vf)) {
+    f = (fact *) vf;
+    for (i = 0; i < f->nr_term_args; ++i)
+      gl_oset_add (set, f->term_arg[i]);
+  }
+  gl_oset_iterator_free (&iter);
+
+  iter = gl_oset_iterator (false_facts);
+  while (gl_oset_iterator_next (&iter, &vf)) {
+    f = (fact *) vf;
+    for (i = 0; i < f->nr_term_args; ++i)
+      gl_oset_add (set, f->term_arg[i]);
+  }
+  gl_oset_iterator_free (&iter);
+}
+
+/* Look for every string parameter in the specific argument position
+ * of the specific term name (both true and false), and add those
+ * strings only to the set.
+ */
+void
+add_strings_from_facts (gl_oset_t set, const char *term_name, size_t arg_i)
+{
+  const void *vf;
+  fact *f;
+  gl_oset_iterator_t iter;
+
+  iter = gl_oset_iterator (true_facts);
+  while (gl_oset_iterator_next (&iter, &vf)) {
+    f = (fact *) vf;
+    if (strcmp (f->term_name, term_name) == 0)
+      gl_oset_add (set, f->term_arg[arg_i]);
+  }
+  gl_oset_iterator_free (&iter);
+
+  iter = gl_oset_iterator (false_facts);
+  while (gl_oset_iterator_next (&iter, &vf)) {
+    f = (fact *) vf;
+    if (strcmp (f->term_name, term_name) == 0)
+      gl_oset_add (set, f->term_arg[arg_i]);
+  }
+  gl_oset_iterator_free (&iter);
+}
+
+/* NB: This does not make a deep copy of the strings.  However before
+ * we add the fact to the true_facts or false_facts arrays, we do call
+ * deep_copy to copy the strings.
+ */
+fact *
+create_fact (const char *term_name, size_t n, ...)
+{
+  fact *f;
+  size_t i;
+  va_list args;
+
+  f = malloc (sizeof (*f) + n * sizeof (char *));
+  if (f == NULL)
+    error (EXIT_FAILURE, errno, "malloc");
+  va_start (args, term_name);
+  for (i = 0; i < n; ++i) {
+    const char *p = va_arg (args, const char *);
+    f->term_arg[i] = (char *) p;
+  }
+  va_end (args);
+  f->term_name = (char *) term_name;
+  f->nr_term_args = n;
+  return f;                     /* caller must free only the struct */
+}
+
+static fact *
+deep_copy_fact (const fact *f)
+{
+  fact *ret;
+  size_t i;
+
+  ret = malloc (sizeof (*ret) + f->nr_term_args * sizeof (char *));
+  if (ret == NULL)
+    error (EXIT_FAILURE, errno, "malloc");
+
+  ret->term_name = strdup (f->term_name);
+  if (ret->term_name == NULL)
+    error (EXIT_FAILURE, errno, "strdup");
+
+  ret->nr_term_args = f->nr_term_args;
+
+  for (i = 0; i < f->nr_term_args; ++i) {
+    ret->term_arg[i] = strdup (f->term_arg[i]);
+    if (ret->term_arg[i] == NULL)
+      error (EXIT_FAILURE, errno, "strdup");
+  }
+
+  return ret;
+}
+
+bool
+is_fact (bool is_true, const fact *f)
+{
+  return gl_oset_search (is_true ? true_facts : false_facts, f);
+}
+
+bool
+add_fact (bool is_true, const fact *f)
+{
+  fact *f2 = deep_copy_fact (f);
+  bool ret;
+
+  ret = gl_oset_add (is_true ? true_facts : false_facts, f2);
+
+  /* Didn't add it, so we must free the deep copy. */
+  if (!ret)
+    free_fact (f2);
+
+  return ret;
+}
diff --git a/inspection/guestfs-inspection.pod b/inspection/guestfs-inspection.pod
new file mode 100644
index 0000000..2848308
--- /dev/null
+++ b/inspection/guestfs-inspection.pod
@@ -0,0 +1,450 @@
+=head1 NAME
+
+guestfs-inspection - guestfs inspection program
+
+=head1 SYNOPSIS
+
+ guestfs-inspection
+
+=head1 NOTE
+
+This man page documents the guestfs inspection program.  If you want
+to read about guestfs inspection then this is the wrong place.  See
+L<guestfs(3)/INSPECTION> instead.
+
+=head1 DESCRIPTION
+
+C<guestfs-inspection> is a standalone program that performs inspection
+on the local disks, to find out what operating system(s) are
+installed.  It normally runs inside the libguestfs appliance, started
+by L<guestfsd(8)>, when the caller uses the C<guestfs_inspect_os> API
+(see L<guestfs-internals(1)> and L<guestfs(3)/guestfs_inspect_os>).
+You should never need to run this program by hand.
+
+The program looks at all disks attached to the appliance, looking for
+filesystems that might belong to operating systems.  It may mount
+these temporarily to examine them for Linux configuration files,
+Windows Registries, and so on.  It then tries to determine what
+operating system(s) are installed on the disks.  It is able to detect
+many different Linux distributions, Windows, BSD, and others.  The
+currently mounted root filesystem is ignored, since when running under
+libguestfs, that filesystem is part of the libguestfs appliance (this
+is the main difference compared to programs like C<facter>).
+
+Guestfs-inpection is written in C, but most of the C is generated by a
+rules compiler from a set of inspection rules written in a more
+compact, declarative, Prolog-inspired language.  If you want to write
+or modify the rules, see L</WRITING RULES> below.
+
+=head1 OPTIONS
+
+=over 4
+
+=item B<-?>
+
+=item B<--help>
+
+Display brief help.
+
+=item B<-v>
+
+=item B<--verbose>
+
+Enable verbose messages for debugging.
+
+=back
+
+=head1 WRITING RULES
+
+Inspection is performed according to a set of rules written in a
+compact, declarative, Prolog-inspired language.  This section explains
+how this language works, so you can write your own rules to detect
+other operating systems.
+
+The rules can be found starting in C<inspection/inspection.rules> (in
+the libguestfs sources).  The rules are compiled down to C and linked
+into the guestfs-inspection program, together with a bit of extra C
+code to provide runtime support.
+
+=head2 Facts
+
+Facts are what we try to determine about the operating system(s) we
+are inspecting.  They look like this:
+
+ Filesystem("/dev/sda1")
+
+which means "F</dev/sda1> is a filesystem".
+
+ File("/dev/sda1", "/etc/fstab")
+
+which means "there exists a file called F</etc/fstab> on the
+F</dev/sda1> filesystem".
+
+Facts come in three flavours: true facts, false facts, and unknown
+facts.  False facts are written like this:
+
+ ! File("/dev/sda1", "/etc/fstab")
+
+which means "either F</dev/sda1> is not a filesystem or there does not
+exist a file called F</etc/fstab> on this filesystem".
+
+Unknown facts are facts that we don't know if they are true or false
+yet.
+
+=head2 Rules
+
+Rules are used to generate more facts.  A simple rule for generating
+C<File> facts might look like this:
+
+ File(fs, filename) :-
+     Filesystem(fs),
+     {{
+       // some C code to mount 'fs' and check for 'filename'
+     }}.
+
+You can read this as: "For all C<fs> & C<filename>, if C<fs> is a
+filesystem, and running the C code with parameters C<fs> and
+C<filename> returns true, then C<File(fs, filename)> is a true fact".
+
+In the Prolog-inspired language, a comma (C<,>) is the AND operator.
+A semicolon (C<;>) is the OR operator.  C<:-> is a backwards
+if-statement (the condition is on the right, the conclusion is on the
+left).  Also notice the dot (C<.>) which must follow each rule.
+
+Uppercase identifiers are facts.  Lowercase identifiers are variables.
+All identifiers are case-sensitive.
+
+Everything in C<{{ ... }}> is embedded C code.  In this case the C
+code returns a true/false/error indication, but embedded C code can
+also do more complicated things and return strings and lists as we'll
+see later.
+
+You can use parentheses C<(...)> for grouping expressions on the right
+hand side of the C<:-> operator.
+
+=head2 Program evaluation
+
+Let's take a simple set of rules which you might use to detect a
+Fedora root filesystem:
+
+ File(fs, filename) :-
+     Filesystem(fs),
+     {{
+       // some C code to mount 'fs' and check for 'filename'
+     }}.
+ 
+ Fedora(rootfs) :-
+     Filesystem(rootfs),
+     File(rootfs, "/etc/fedora-release").
+
+When evaluating this program, there are two sets of facts, the true
+facts and the false facts.  Let's start with the false facts set being
+empty, and let's seed the true facts set with some C<Filesystem>
+facts:
+
+ true_facts = { Filesystem("/dev/sda1"), Filesystem("/dev/sda3") }
+ false_facts = {  } // empty set
+
+Unknown facts are facts which don't appear in either set.
+
+Evaluating the program works like this: We consider each rule in turn,
+and see if we can find new true or false facts from it.  These new
+facts are added to the true or false facts sets.  After looking at
+each rule in the program, as long as at least one new fact was added
+to the true facts set, we go back to the start of the rules and repeat
+over.  We do this until we can no longer add any new true facts, and
+then we're done.
+
+In the case of this program, we start with the C<File> rule, and we
+substitute (theoretically) every possible string for C<fs> and
+C<filename>.
+
+For example, this substitution:
+
+ File("/dev/sda1", "/etc/fedora-release") :-
+     Filesystem("/dev/sda1"),
+     {{ // checks for file and returns false }}.
+
+turns out to be false (because the C code doesn't find F</etc/fstab>
+in F</dev/sda1>), so that yields a new false fact:
+
+ ! File("/dev/sda1", "/etc/fedora-release")
+
+But this substitution turns out to be true:
+
+ File("/dev/sda3", "/etc/fedora-release") :-
+     Filesystem("/dev/sda3"),
+     {{ // checks for file and returns true }}.
+
+so that yields a new true fact:
+
+ File("/dev/sda3", "/etc/fedora-release")
+
+In theory every possible string is tried, eg C<File("ardvark", "foo123654")>.
+That would take literally forever to run, but luckily the rules
+compiler is smarter.
+
+Looking now at the second rule, we try this substitution:
+
+ Fedora("/dev/sda3") :-
+     Filesystem("/dev/sda3"),
+     File("/dev/sda3", "/etc/fedora-release").
+
+which yields another new true fact:
+
+ Fedora("/dev/sda3")
+
+Because we added several new true facts to the set, we go back and
+repeat the whole process.  But after trying all the rules for a second
+time, no more true facts can be added, so now we're done.
+
+At the end, the set of true facts is:
+
+ true_facts = { Filesystem("/dev/sda1"), Filesystem("/dev/sda3"),
+                File("/dev/sda3", "/etc/fedora-release"),
+                Fedora("/dev/sda3") }
+
+We don't care about the false facts -- they are discarded at the end
+of the program.
+
+The summary of inspection is that F</dev/sda3> contains a Fedora root
+filesystem.
+
+Of course real inspection is much more complicated than this, but the
+same program evaluation order is followed.
+
+=head2 Some caveats with the language
+
+It's easy to look at an expression like:
+
+ Fedora(rootfs) :-
+     Filesystem(rootfs),
+     File(rootfs, "/etc/fedora-release"). /* line 3 */
+
+and think that line 3 is "calling" the "File function".  This is
+B<not> what is happening!  Rules are not functions.  Rules are
+considered in isolation.  Rules don't "call" other rules.  Instead
+when trying to find possible values that can be substituted into a
+rule, we only look at the rule and the current sets of true and false
+facts.
+
+When searching for values to subsitute, in theory the compiler would
+have to look at every possible string.  In practice of course it can't
+and doesn't do that.  Instead it looks at the current sets of true and
+false facts to find strings to substitute.  In the following rule:
+
+ File(fs, filename) :-
+     Filesystem(fs),
+     {{ // C code }}.
+
+suitable choices for C<fs> are found by looking at any C<Filesystem>
+facts in either the true or false sets.
+
+In some cases, this doesn't work, as in the example above where we
+have no clues for the C<filename> variable.  In that case the compiler
+tries every string literal from every rule in the program.  This can
+be inefficient, but by modifying the rule slightly you can avoid this.
+In the following program, only the strings F</etc/fstab> and
+F</etc/fedora-release> would be tried:
+
+ Filename("/etc/fstab").
+ Filename("/etc/fedora-release").
+ File(fs, filename) :-
+     Filesystem(fs),
+     Filename(filename),
+     {{ // C code }}.
+
+=head2 C expressions returning boolean
+
+Simple C code enclosed in C<{{ ... }}> as shown above should return a
+true, false or error status only.  It returns true by returning any
+integer E<ge> 1.  It should return C<0> to indicate false, and it
+should return C<-1> to indicate an error (which stops the program and
+causes inspection to fail with a user-visible error).
+
+Here is an example of a simple C expression returning a boolean:
+
+ File(fs, filename) :-
+     Filesystem(fs),
+     {{
+       int r;
+       char *relative_filename;
+       r = get_mount (fs, filename, &relative_filename);
+       if (r != 1) return r;
+       r = access (relative_filename, F_OK);
+       free (relative_filename);
+       if (r == -1) {
+         if (errno == ENOENT || errno == ENOTDIR)
+           return 0;
+         perror ("access");
+         return -1;
+       }
+       return 1;
+     }}.
+
+Notice that C<fs> and C<filename> are passed into the C code as local
+variables.
+
+You can see that dealing with errors is a bit involved, because we
+want to fail hard if some error like C<EIO> is thrown.
+
+=head2 C expressions returning strings
+
+C expressions can also return strings or tuples of strings.  This is
+useful where you want to parse the content of external files.
+
+The syntax for this sort of C expression is:
+
+ (var1, var2, ...)={{ ... }}
+
+where C<var1>, C<var2>, etc. are outputs from the C code.
+
+In the following example, a lot of error checking has been omitted
+for clarity:
+
+ ProductName(fs, product_name) :-
+     Unix_root(fs),
+     Distro(fs, "RHEL"),
+     (product_name)={{
+       int r;
+       char *line = NULL;
+       size_t n;
+       char *relative_filename;
+       r = get_mount (fs, "/etc/redhat-release", &relative_filename);
+       FILE *fp = fopen (relative_filename, "r");
+       free (relative_filename);
+       getline (&line, &n, fp);
+       fclose (fp);
+       set_product_name (line);
+       free (line);
+       return 0;
+     }}.
+
+The C code calls a function C<set_product_name> (that the compiler
+generates).
+
+The return value from the C code should be C<0> if everything was OK,
+or C<-1> if there is a error (which stops the whole program).
+
+=head2 C expressions returning multiple results
+
+Finally it is possible for C code to return multiple results.
+
+The syntax is:
+
+ (var1, var2, ...)*={{ ... }}
+
+ (var1, var2, ...)?={{ ... }}
+
+ (var1, var2, ...)+={{ ... }}
+
+where C<var1>, C<var2>, etc. are outputs.  Unlike the previous rules,
+these rules may generate zero or multiple facts from a single string
+substitution.
+
+For example, here is how we could populate a list of C<Filesystem>
+facts:
+
+ Filesystem(fs) :-
+     (fs)*={{
+       int i;
+       extern char **fs;
+       for (i = 0; fs[i] != NULL; ++i) {
+         set_fs (fs[i]);
+       }
+       return 0;
+     }}.
+
+In this case, the C code repeatedly calls a function C<set_fs> (that
+the compiler generates) for each new filesystem discovered.  Multiple
+C<Filesystem> facts can be generated as a result of one application of
+this rule.
+
+As with regular expressions, C<*> means we expect zero or more rows of
+results, C<?> means zero or one, and C<+> means one or more.  The
+generated target code checks that you call the C<set_*> function the
+correct number of times.
+
+The return value from the C code should be C<0> if everything was OK,
+or C<-1> if there is a error (which stops the whole program).
+
+=head2 Prologue and epilogue
+
+If you want to insert arbitrary chunks of C code into the output, for
+example to C<#include> headers, then you can insert literal code
+between C<{{ ... }}> at the top and bottom of the file (ie. before
+all rules and after all rules).  You cannot insert these chunks
+between rules.
+
+=head2 C code memoization
+
+Although currently B<only partially implemented>, in future we will
+implement memoization of C code.  This means that for every input, the
+C code will be called only once, making it less important that C code
+is efficient: no matter how many times we need to evaluate if
+F</etc/fstab> exists on F</dev/sda1>, the C code would only be called
+once.
+
+This means that impure C functions returning different results for the
+same input won't work in future, so don't do that.
+
+=head2 Type checking
+
+The current language treats every value as a string.  Every expression
+is a boolean.  One possible future enhancement is to handle other
+types.  There is still some minimal type checking applied:
+
+=over 4
+
+=item *
+
+A fact name which appears on a right hand side of any rule must also
+appear on the left hand side of a rule.  This is mainly for catching
+typos.
+
+=item *
+
+A fact must have the same number of arguments ("arity") each time it
+appears in the source.
+
+=back
+
+=head2 Debugging
+
+You can debug the evaluation of inspection programs by calling
+C<guestfs_set_verbose> (or setting C<$LIBGUESTFS_DEBUG=1>) before
+launching the handle.
+
+This causes L<guestfsd(8)> to pass the I<--verbose> parameter to this
+inspection program, which in turn causes the inspection program to
+print information about what rules it is trying and what true/false
+facts it has found.  These are passed back to libguestfs and printed
+on C<stderr> (or sent to the event system if you are using that).
+
+You can also print debug messages from C code embedded in C<{{...}}>
+expressions.  These are similarly sent upwards through to libguestfs
+and will appear on C<stderr>.
+
+Remember that C memoization [to be implemented in a future version]
+can cause C code to run fewer times than expected.
+
+=head1 EXIT STATUS
+
+This program returns 0 if successful, or non-zero if there was an
+error.
+
+=head1 SEE ALSO
+
+L<guestfsd(8)>,
+L<guestfs-hacking(1)>,
+L<guestfs-internals(1)>,
+L<guestfs(3)/INSPECTION>,
+L<http://libguestfs.org/>.
+
+=head1 AUTHOR
+
+Richard W.M. Jones L<http://people.redhat.com/~rjones/>
+
+=head1 COPYRIGHT
+
+Copyright (C) 2009-2016 Red Hat Inc.
diff --git a/inspection/inspection.c b/inspection/inspection.c
new file mode 100644
index 0000000..d84b9fe
--- /dev/null
+++ b/inspection/inspection.c
@@ -0,0 +1,106 @@
+/* guestfs-inspection
+ * Copyright (C) 2009-2015 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 <getopt.h>
+#include <unistd.h>
+
+#include "rules.h"
+
+int verbose = 0;
+const char *sysroot = NULL;
+const size_t sysroot_len = 0;
+
+/* Required by the gnulib 'error' module. */
+const char *program_name = "guestfs-inspection";
+
+static void __attribute__((noreturn))
+usage (int status)
+{
+  if (status != EXIT_SUCCESS)
+    fprintf (stderr, "Try `%s --help' for more information.\n",
+             program_name);
+  else {
+    printf ("%s: guestfs inspection\n"
+            "Copyright (C) 2009-2015 Red Hat Inc.\n"
+            "Usage:\n"
+            "Options:\n"
+            "  --help               Display brief help\n"
+            "  -v|--verbose         Verbose messages\n"
+            "  -V|--version         Display version and exit\n"
+            "For more information, see the manpage %s(8).\n",
+            program_name, program_name);
+  }
+  exit (status);
+}
+
+int
+main (int argc, char *argv[])
+{
+  enum { HELP_OPTION = CHAR_MAX + 1 };
+
+  static const char *options = "vV";
+  static const struct option long_options[] = {
+    { "help", 0, 0, HELP_OPTION },
+    { "verbose", 0, 0, 'v' },
+    { "version", 0, 0, 'V' },
+    { 0, 0, 0, 0 }
+  };
+  int c;
+  int option_index;
+
+  for (;;) {
+    c = getopt_long (argc, argv, options, long_options, &option_index);
+    if (c == -1) break;
+
+    switch (c) {
+    case 0:			/* options which are long only */
+      fprintf (stderr, "%s: unknown long option: %s (%d)\n",
+               program_name,
+               long_options[option_index].name, option_index);
+      exit (EXIT_FAILURE);
+
+    case 'v':
+      verbose++;
+      break;
+
+    case 'V':
+      printf ("%s %s\n", program_name, PACKAGE_VERSION_FULL);
+      exit (EXIT_SUCCESS);
+
+    case HELP_OPTION:
+      usage (EXIT_SUCCESS);
+
+    default:
+      usage (EXIT_FAILURE);
+    }
+  }
+
+  /* Run the rules. */
+  rules ();
+
+  /* Print the true facts.  XXX Output XXX */
+  print_true_facts ();
+
+  exit (EXIT_SUCCESS);
+}
diff --git a/inspection/inspection.h b/inspection/inspection.h
new file mode 100644
index 0000000..f3359b1
--- /dev/null
+++ b/inspection/inspection.h
@@ -0,0 +1,60 @@
+/* guestfs-inspection
+ * Copyright (C) 2009-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.
+ */
+
+#ifndef GUESTFS_INSPECTION_H
+#define GUESTFS_INSPECTION_H
+
+#include <pcre.h>
+
+/* detect.c - used by rules */
+extern int get_distro_from_os_release (const char *fs, char **distro);
+extern int get_version_from_os_release (const char *fs, char **major, char **minor);
+extern int get_product_name_from_os_release (const char *fs, char **product_name);
+extern int get_distro_from_lsb_release (const char *fs, char **distro);
+extern int get_version_from_lsb_release (const char *fs, char **major, char **minor);
+extern int get_product_name_from_lsb_release (const char *fs, char **product_name);
+extern int get_version_from_oracle_release (const char *fs, char **major, char **minor);
+extern int get_version_from_centos_release (const char *fs, char **major, char **minor);
+extern int get_version_from_altlinux_release (const char *fs, char **major, char **minor);
+extern int match_redhat_release_fedora (const char *fs);
+extern int match_redhat_release_rhel (const char *fs);
+extern int match_redhat_release_centos (const char *fs);
+extern int match_redhat_release_scientific_linux (const char *fs);
+extern int get_version_from_redhat_release (const char *fs, char **major, char **minor);
+
+/* match.c */
+extern char *match1 (const char *str, const pcre *re);
+extern int match2 (const char *str, const pcre *re, char **ret1, char **ret2);
+
+/* mount.c - used by rules */
+extern char **get_all_block_devices (void);
+extern char **get_all_partitions (void);
+extern char **get_all_mddevs (void);
+extern char **get_all_lvs (void);
+extern char **get_all_ldmvols (void);
+extern char **get_all_ldmparts (void);
+extern char **get_all_btrfs_subvolumes (const char *fs);
+extern char *get_vfs_type (const char *fs);
+extern int get_partition_mbr_id (const char *fs);
+extern int is_mountable (const char *fs);
+extern int get_mount (const char *fs, const char *filename, char **relative_filename);
+
+/* utils.c */
+extern char *first_line_of_file (const char *fs, const char *filename);
+
+#endif /* GUESTFS_INSPECTION_H */
diff --git a/inspection/inspection.rules b/inspection/inspection.rules
new file mode 100644
index 0000000..0bdad19
--- /dev/null
+++ b/inspection/inspection.rules
@@ -0,0 +1,476 @@
+/* Libguestfs inspection rules -*- prolog -*-
+ * Copyright (C) 2009-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 understand what's going on here, it's recommended that you read
+ * guestfs-inspection(8) (inspection/guestfs-inspection.pod) first.
+ */
+
+{{
+#include "inspection.h"
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <errno.h>
+}}
+
+/* Whole block devices. */
+BlockDevice(dev) :-
+    (dev)*={{
+      CLEANUP_FREE_STRING_LIST char **devs = get_all_block_devices ();
+      if (devs == NULL) return -1;
+      for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]);
+      return 0;
+    }}.
+
+/* Partitions. */
+Partition(dev) :-
+    (dev)*={{
+      CLEANUP_FREE_STRING_LIST char **devs = get_all_partitions ();
+      if (devs == NULL) return -1;
+      /* Ignore partitions with type byte 0x42 (RHBZ#887520). */
+      for (size_t i = 0; devs[i] != NULL; ++i) {
+        int r = get_partition_mbr_id (devs[i]);
+        if (r == -1) return -1;
+        if (r != 0x42) set_dev (devs[i]);
+      }
+      return 0;
+    }}.
+
+/* LVM2 logical volumes. */
+LV(dev) :-
+    (dev)*={{
+      CLEANUP_FREE_STRING_LIST char **devs = get_all_lvs ();
+      if (devs == NULL) return -1;
+      for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]);
+      return 0;
+    }}.
+
+/* /dev/md* devices. */
+MDDev(dev) :-
+    (dev)*={{
+      CLEANUP_FREE_STRING_LIST char **devs = get_all_mddevs ();
+      if (devs == NULL) return -1;
+      for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]);
+      return 0;
+    }}.
+
+/* Windows LDM voumes. */
+LDMVol(dev) :-
+    (dev)*={{
+      CLEANUP_FREE_STRING_LIST char **devs = get_all_ldmvols ();
+      if (devs == NULL) return -1;
+      for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]);
+      return 0;
+    }}.
+
+/* Windows LDM partitions. */
+LDMPart(dev) :-
+    (dev)*={{
+      CLEANUP_FREE_STRING_LIST char **devs = get_all_ldmparts ();
+      if (devs == NULL) return -1;
+      for (size_t i = 0; devs[i] != NULL; ++i) set_dev (devs[i]);
+      return 0;
+    }}.
+
+/* Device(dev) is just a group name for block devices, partitions etc. */
+Device(dev) :-
+    BlockDevice(dev); Partition(dev); LV(dev);
+    MDDev(dev); LDMVol(dev); LDMPart(dev).
+
+/* Map a filesystem to its VFS type (from blkid). */
+VFSType(fs, vfs_type) :-
+    Device(fs),
+    (vfs_type)?={{
+      CLEANUP_FREE char *vfs_type = get_vfs_type (fs);
+      if (vfs_type != NULL) set_vfs_type (vfs_type);
+      return 0;
+    }}.
+
+/* A device contains a mountable filesystem (not swap, empty, etc). */
+Mountable(fs) :-
+    Device(fs),
+    {{ return is_mountable (fs); }}.
+
+/* Where a filesystem is btrfs and mountable, get the subvolumes. */
+BtrfsSubvolume(subvol) :-
+    Device(fs),
+    Mountable(fs),
+    VFSType(fs, "btrfs"),
+    (subvol)*={{
+      size_t i;
+      CLEANUP_FREE_STRING_LIST char **paths = get_all_btrfs_subvolumes (fs);
+      if (paths == NULL) return -1;
+      for (i = 0; paths[i] != NULL; ++i) {
+        CLEANUP_FREE char *subvol;
+        if (asprintf (&subvol, "btrfsvol:%s/%s", fs, paths[i]) == -1) {
+          perror ("asprintf");
+          exit (EXIT_FAILURE);
+        }
+        set_subvol (subvol);
+      }
+      return 0;
+    }}.
+VFSType(subvol, "btrfs") :- BtrfsSubvolume(subvol).
+
+/* Ignore all *_member types.  In libblkid these are returned
+ * for things which are members of some RAID or LVM set, most
+ * importantly "LVM2_member" which is a PV.  Also ignore
+ * crypto_LUKS (LUKS encrypted partition).
+ */
+ContainerDevice(dev) :-
+    VFSType(dev, vfs_type),
+    {{ return STRSUFFIX (vfs_type, "_member"); }}.
+ContainerDevice(dev) :-
+    VFSType(dev, "crypto_LUKS").
+
+/* Ignore all swap devices. */
+SwapDevice(dev) :- VFSType(dev, "swap").
+
+/* This rule generates one Filesystem(fs) fact per mountable
+ * filesystem found in the appliance.  A filesystem could be
+ * a device, partition, LV, btrfs subvolume, etc.
+ */
+Filesystem(fs) :-
+    !ContainerDevice(fs),
+    !SwapDevice(fs),
+    (Device(fs), Mountable(fs); BtrfsSubvolume(fs)).
+
+/* File(fs, filename) is true if filename is a regular file in fs.
+ * It also follows symlinks.
+ */
+File(fs, filename) :-
+    Filesystem(fs),
+    {{
+        int r;
+        CLEANUP_FREE char *relative_filename = NULL;
+        struct stat statbuf;
+
+        if (filename[0] != '/') return 0;
+        if (get_mount (fs, filename, &relative_filename) == -1)
+          return -1;
+        r = stat (relative_filename, &statbuf);
+        if (r == -1) {
+          if (errno == ENOENT || errno == ENOTDIR || errno == ELOOP)
+            return 0;
+          perror (relative_filename);
+          return -1;
+        }
+        return S_ISREG (statbuf.st_mode);
+    }}.
+
+/* Directory(fs, dirname) is true if dirname is a directory in fs.
+ * It also follows symlinks.
+ */
+Directory(fs, dirname) :-
+    Filesystem(fs),
+    {{
+        int r;
+        CLEANUP_FREE char *relative_dirname = NULL;
+        struct stat statbuf;
+
+        if (dirname[0] != '/') return 0;
+        if (get_mount (fs, dirname, &relative_dirname) == -1)
+          return -1;
+        r = stat (relative_dirname, &statbuf);
+        if (r == -1) {
+          if (errno == ENOENT || errno == ENOTDIR || errno == ELOOP)
+            return 0;
+          perror (relative_dirname);
+          return -1;
+        }
+        return S_ISDIR (statbuf.st_mode);
+    }}.
+
+/* Symlink(fs, filename) is true if filename is a symlink. */
+Symlink(fs, filename) :-
+    Filesystem(fs),
+    {{
+        int r;
+        CLEANUP_FREE char *relative_filename = NULL;
+        struct stat statbuf;
+
+        if (filename[0] != '/') return 0;
+        if (get_mount (fs, filename, &relative_filename) == -1)
+          return -1;
+        r = stat (relative_filename, &statbuf);
+        if (r == -1) {
+          if (errno == ENOENT || errno == ENOTDIR || errno == ELOOP)
+            return 0;
+          perror (relative_filename);
+          return -1;
+        }
+        return S_ISLNK (statbuf.st_mode);
+    }}.
+
+/* grub or grub2 /boot */
+GrubBoot(fs) :-
+    Filesystem(fs),
+    File(fs, "/grub/menu.lst"),
+    File(fs, "/grub/grub.conf"),
+    File(fs, "/grub2/grub.cfg").
+
+/* FreeBSD root. */
+FreeBSDRoot(fs) :-
+    Filesystem(fs),
+    Directory(fs, "/bin"),
+    Directory(fs, "/etc"),
+    File(fs, "/etc/freebsd-update.conf"),
+    File(fs, "/etc/fstab").
+
+/* NetBSD root. */
+NetBSDRoot(fs) :-
+    Filesystem(fs),
+    Directory(fs, "/bin"),
+    Directory(fs, "/etc"),
+    File(fs, "/netbsd"),
+    File(fs, "/etc/fstab"),
+    File(fs, "/etc/release").
+
+/* OpenBSD root. */
+OpenBSDRoot(fs) :-
+    Filesystem(fs),
+    Directory(fs, "/bin"),
+    Directory(fs, "/etc"),
+    File(fs, "/bsd"),
+    File(fs, "/etc/fstab"),
+    File(fs, "/etc/motd").
+
+/* Hurd root. */
+HurdRoot(fs) :-
+    Filesystem(fs),
+    File(fs, "/hurd/console"),
+    File(fs, "/hurd/hello"),
+    File(fs, "/hurd/null").
+
+/* Minix root. */
+MinixRoot(fs) :-
+    Filesystem(fs),
+    File(fs, "/service/vm"),
+    File(fs, "/etc/fstab"),
+    File(fs, "/etc/version").
+
+/* Linux root (any distro). */
+LinuxRoot(fs) :-
+    Filesystem(fs),
+    Directory(fs, "/etc"),
+    File(fs, "/etc/fstab"),
+    (Directory(fs, "/bin"); Symlink(fs, "/bin")),
+    !FreeBSDRoot(fs),
+    !NetBSDRoot(fs),
+    !OpenBSDRoot(fs),
+    !HurdRoot(fs),
+    !MinixRoot(fs).
+
+/* Is it Linux using /etc/os-release?  (Linux systemd). */
+LinuxRootWithOSRelease(fs) :-
+    LinuxRoot(fs),
+    File(fs, "/etc/os-release").
+
+Distro(fs, distro) :-
+    LinuxRootWithOSRelease(fs),
+    (distro)?={{
+      int r;
+      CLEANUP_FREE char *distro = NULL;
+      if ((r = get_distro_from_os_release (fs, &distro)) <= 0)
+        return r;
+      set_distro (distro);
+      return 0;
+    }}.
+
+Version(fs, major, minor) :-
+    LinuxRootWithOSRelease(fs),
+    (major, minor)?={{
+      int r;
+      CLEANUP_FREE char *major = NULL, *minor = NULL;
+      if ((r = get_version_from_os_release (fs, &major, &minor)) <= 0)
+        return r;
+      set_major_minor (major, minor);
+      return 0;
+    }}.
+
+ProductName(fs, product_name) :-
+    LinuxRootWithOSRelease(fs),
+    (product_name)?={{
+      int r;
+      CLEANUP_FREE char *product_name = NULL;
+      if ((r = get_product_name_from_os_release (fs, &product_name)) <= 0)
+        return r;
+      set_product_name (product_name);
+      return 0;
+    }}.
+
+/* Is it Linux using /etc/lsb-release?  (Linux Standards Base). */
+LinuxRootWithLSBRelease(fs) :-
+    LinuxRoot(fs),
+    File(fs, "/etc/lsb-release"),
+    !LinuxRootWithOSRelease(fs). /* prefer /etc/os-release */
+
+Distro(fs, distro) :-
+    LinuxRootWithLSBRelease(fs),
+    (distro)?={{
+      int r;
+      CLEANUP_FREE char *distro = NULL;
+      if ((r = get_distro_from_lsb_release (fs, &distro)) <= 0)
+        return r;
+      set_distro (distro);
+      return 0;
+    }}.
+
+Version(fs, major, minor) :-
+    LinuxRootWithLSBRelease(fs),
+    (major, minor)?={{
+      int r;
+      CLEANUP_FREE char *major = NULL, *minor = NULL;
+      if ((r = get_version_from_lsb_release (fs, &major, &minor)) <= 0)
+        return r;
+      set_major_minor (major, minor);
+      return 0;
+    }}.
+
+ProductName(fs, product_name) :-
+    LinuxRootWithLSBRelease(fs),
+    (product_name)?={{
+      int r;
+      CLEANUP_FREE char *product_name = NULL;
+      if ((r = get_product_name_from_lsb_release (fs, &product_name)) <= 0)
+        return r;
+      set_product_name (product_name);
+      return 0;
+    }}.
+
+/* Fall back on /etc/*-release files to determine the distro, version and
+ * product name.
+ */
+LinuxRootWithReleaseFile(fs, "/etc/oracle-release") :-
+    LinuxRoot(fs),
+    !LinuxRootWithOSRelease(fs), !LinuxRootWithLSBRelease(fs),
+    File(fs, "/etc/oracle-release").
+Distro(fs, "oracle-linux") :-
+    LinuxRootWithReleaseFile(fs, "/etc/oracle-release").
+Version(fs, major, minor) :-
+    LinuxRootWithReleaseFile(fs, "/etc/oracle-release"),
+    (major, minor)={{
+      CLEANUP_FREE char *major = NULL, *minor = NULL;
+      if (get_version_from_oracle_release (fs, &major, &minor) == -1)
+        return -1;
+      set_major_minor (major, minor);
+      return 0;
+    }}.
+
+LinuxRootWithReleaseFile(fs, "/etc/centos-release") :-
+    LinuxRoot(fs),
+    !LinuxRootWithOSRelease(fs), !LinuxRootWithLSBRelease(fs),
+    File(fs, "/etc/centos-release").
+Distro(fs, "centos") :-
+    LinuxRootWithReleaseFile(fs, "/etc/centos-release").
+Version(fs, major, minor) :-
+    LinuxRootWithReleaseFile(fs, "/etc/centos-release"),
+    (major, minor)={{
+      CLEANUP_FREE char *major = NULL, *minor = NULL;
+      if (get_version_from_centos_release (fs, &major, &minor) == -1)
+        return -1;
+      set_major_minor (major, minor);
+      return 0;
+    }}.
+
+LinuxRootWithReleaseFile(fs, "/etc/altlinux-release") :-
+    LinuxRoot(fs),
+    !LinuxRootWithOSRelease(fs), !LinuxRootWithLSBRelease(fs),
+    File(fs, "/etc/altlinux-release").
+Distro(fs, "altlinux") :-
+    LinuxRootWithReleaseFile(fs, "/etc/altlinux-release").
+Version(fs, major, minor) :-
+    LinuxRootWithReleaseFile(fs, "/etc/altlinux-release"),
+    (major, minor)={{
+      CLEANUP_FREE char *major = NULL, *minor = NULL;
+      if (get_version_from_altlinux_release (fs, &major, &minor) == -1)
+        return -1;
+      set_major_minor (major, minor);
+      return 0;
+    }}.
+
+/* Handle the fallback case of /etc/redhat-release. */
+LinuxRootWithReleaseFile(fs, "/etc/redhat-release") :-
+    LinuxRoot(fs),
+    !LinuxRootWithOSRelease(fs), !LinuxRootWithLSBRelease(fs),
+    !LinuxRootWithReleaseFile(fs, "/etc/oracle-release"),
+    !LinuxRootWithReleaseFile(fs, "/etc/centos-release"),
+    !LinuxRootWithReleaseFile(fs, "/etc/altlinux-release"),
+    File(fs, "/etc/redhat-release").
+Distro(fs, "fedora") :-
+    LinuxRootWithReleaseFile(fs, "/etc/redhat-release"),
+    {{ return match_redhat_release_fedora (fs); }}.
+Distro(fs, "rhel") :-
+    LinuxRootWithReleaseFile(fs, "/etc/redhat-release"),
+    {{ return match_redhat_release_rhel (fs); }}.
+Distro(fs, "centos") :-
+    LinuxRootWithReleaseFile(fs, "/etc/redhat-release"),
+    {{ return match_redhat_release_centos (fs); }}.
+Distro(fs, "scientificlinux") :-
+    LinuxRootWithReleaseFile(fs, "/etc/redhat-release"),
+    {{ return match_redhat_release_scientific_linux (fs); }}.
+Distro(fs, "redhat-based") :-
+    LinuxRootWithReleaseFile(fs, "/etc/redhat-release"),
+    !Distro(fs, "fedora"),
+    !Distro(fs, "rhel"),
+    !Distro(fs, "centos"),
+    !Distro(fs, "scientificlinux").
+
+Version(fs, major, minor) :-
+    LinuxRootWithReleaseFile(fs, "/etc/redhat-release"),
+    (major, minor)={{
+      CLEANUP_FREE char *major = NULL, *minor = NULL;
+      if (get_version_from_redhat_release (fs, &major, &minor) == -1)
+        return -1;
+      set_major_minor (major, minor);
+      return 0;
+    }}.
+
+/* Get the product name from a generic Linux release file. */
+ProductName(fs, product_name) :-
+    LinuxRootWithReleaseFile(fs, release_file),
+    (product_name)={{
+      CLEANUP_FREE char *product_name = first_line_of_file (fs, release_file);
+      set_product_name (product_name);
+      return 0;
+    }}.
+
+/* XXX debian, arch-linux etc release files */
+
+/* XXX Linux architecture, fstab, hostname */
+
+
+
+/* XXX CoreOS etc. */
+
+
+
+
+
+
+
+
+/*
+Has_fstab(rootfs) :-
+    Unix_root(fs),
+    File(rootfs, "/etc/fstab").
+Mount(rootfs, dev, mountpoint) :-
+    Has_fstab(rootfs),
+    (dev, mountpoint)*={{
+        // code to return (dev, mountpoint) pairs from /etc/fstab
+    }}.
+*/
diff --git a/inspection/match.c b/inspection/match.c
new file mode 100644
index 0000000..fe12b12
--- /dev/null
+++ b/inspection/match.c
@@ -0,0 +1,173 @@
+/* libguestfs
+ * Copyright (C) 2010-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 <error.h>
+#include <errno.h>
+
+#include <pcre.h>
+
+#include "inspection.h"
+
+static char *
+safe_strndup (const char *str, size_t len)
+{
+  char *ret = strndup (str, len);
+  if (ret == NULL)
+    error (EXIT_FAILURE, errno, "strndup");
+  return ret;
+}
+
+#if 0
+/* Match a regular expression which contains no captures.  Returns
+ * true if it matches or false if it doesn't.
+ */
+int
+match (const char *str, const pcre *re)
+{
+  size_t len = strlen (str);
+  int vec[30], r;
+
+  r = pcre_exec (re, NULL, str, len, 0, 0, vec, sizeof vec / sizeof vec[0]);
+  if (r == PCRE_ERROR_NOMATCH)
+    return 0;
+
+  return 1;
+}
+#endif
+
+/* Match a regular expression which contains exactly one capture.  If
+ * the string matches, return the capture, otherwise return NULL.  The
+ * caller must free the result.
+ */
+char *
+match1 (const char *str, const pcre *re)
+{
+  size_t len = strlen (str);
+  int vec[30], r;
+
+  r = pcre_exec (re, NULL, str, len, 0, 0, vec, sizeof vec / sizeof vec[0]);
+  if (r == PCRE_ERROR_NOMATCH)
+    return NULL;
+
+  return r == 2 ? safe_strndup (&str[vec[2]], vec[3]-vec[2]) : NULL;
+}
+
+/* Match a regular expression which contains exactly two captures. */
+int
+match2 (const char *str, const pcre *re, char **ret1, char **ret2)
+{
+  size_t len = strlen (str);
+  int vec[30], r;
+
+  r = pcre_exec (re, NULL, str, len, 0, 0, vec, 30);
+  if (r == PCRE_ERROR_NOMATCH)
+    return 0;
+
+  *ret1 = NULL;
+  *ret2 = NULL;
+
+  if (r > 1) *ret1 = safe_strndup (&str[vec[2]], vec[3]-vec[2]);
+  if (r > 2) *ret2 = safe_strndup (&str[vec[4]], vec[5]-vec[4]);
+
+  return 1;
+}
+
+#if 0
+/* Match a regular expression which contains exactly three captures. */
+int
+match3 (const char *str, const pcre *re,
+        char **ret1, char **ret2, char **ret3)
+{
+  size_t len = strlen (str);
+  int vec[30], r;
+
+  r = pcre_exec (re, NULL, str, len, 0, 0, vec, 30);
+  if (r == PCRE_ERROR_NOMATCH)
+    return 0;
+
+  *ret1 = NULL;
+  *ret2 = NULL;
+  *ret3 = NULL;
+
+  if (r > 1) *ret1 = safe_strndup (&str[vec[2]], vec[3]-vec[2]);
+  if (r > 2) *ret2 = safe_strndup (&str[vec[4]], vec[5]-vec[4]);
+  if (r > 3) *ret3 = safe_strndup (&str[vec[6]], vec[7]-vec[6]);
+
+  return 1;
+}
+
+/* Match a regular expression which contains exactly four captures. */
+int
+match4 (const char *str, const pcre *re,
+        char **ret1, char **ret2, char **ret3, char **ret4)
+{
+  size_t len = strlen (str);
+  int vec[30], r;
+
+  r = pcre_exec (re, NULL, str, len, 0, 0, vec, 30);
+  if (r == PCRE_ERROR_NOMATCH)
+    return 0;
+
+  *ret1 = NULL;
+  *ret2 = NULL;
+  *ret3 = NULL;
+  *ret4 = NULL;
+
+  if (r > 1) *ret1 = safe_strndup (&str[vec[2]], vec[3]-vec[2]);
+  if (r > 2) *ret2 = safe_strndup (&str[vec[4]], vec[5]-vec[4]);
+  if (r > 3) *ret3 = safe_strndup (&str[vec[6]], vec[7]-vec[6]);
+  if (r > 4) *ret4 = safe_strndup (&str[vec[8]], vec[9]-vec[8]);
+
+  return 1;
+}
+
+/* Match a regular expression which contains exactly six captures. */
+int
+match6 (const char *str, const pcre *re,
+        char **ret1, char **ret2, char **ret3, char **ret4,
+        char **ret5, char **ret6)
+{
+  size_t len = strlen (str);
+  int vec[30], r;
+
+  r = pcre_exec (re, NULL, str, len, 0, 0, vec, 30);
+  if (r == PCRE_ERROR_NOMATCH)
+    return 0;
+
+  *ret1 = NULL;
+  *ret2 = NULL;
+  *ret3 = NULL;
+  *ret4 = NULL;
+  *ret5 = NULL;
+  *ret6 = NULL;
+
+  if (r > 1) *ret1 = safe_strndup (&str[vec[2]], vec[3]-vec[2]);
+  if (r > 2) *ret2 = safe_strndup (&str[vec[4]], vec[5]-vec[4]);
+  if (r > 3) *ret3 = safe_strndup (&str[vec[6]], vec[7]-vec[6]);
+  if (r > 4) *ret4 = safe_strndup (&str[vec[8]], vec[9]-vec[8]);
+  if (r > 5) *ret5 = safe_strndup (&str[vec[10]], vec[11]-vec[10]);
+  if (r > 6) *ret6 = safe_strndup (&str[vec[12]], vec[13]-vec[12]);
+
+  return 1;
+}
+#endif
diff --git a/inspection/mount.c b/inspection/mount.c
new file mode 100644
index 0000000..e10dbdd
--- /dev/null
+++ b/inspection/mount.c
@@ -0,0 +1,597 @@
+/* guestfs-inspection
+ * Copyright (C) 2009-2015 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 <stdbool.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mount.h>
+#include <errno.h>
+#include <error.h>
+
+#include "c-ctype.h"
+
+#include "guestfs-internal-all.h"
+
+#include "inspection.h"
+#include "cleanups.h"
+#include "command.h"
+#include "stringsbuf.h"
+
+/* If root device is an ext2 filesystem, this is the major and minor.
+ * This is so we can ignore this device from the point of view of the
+ * user, eg. in guestfs_list_devices and many other places.
+ */
+static dev_t root_device = 0;
+
+/* A temporary directory where we place all the mountpoints. */
+static char mountpoints[] = "/tmp/mp.XXXXXX";
+
+static void init_mount (void) __attribute__((constructor));
+
+static void
+init_mount (void)
+{
+  struct stat statbuf;
+
+  if (stat ("/", &statbuf) == 0)
+    root_device = statbuf.st_dev;
+
+  if (mkdtemp (mountpoints) == NULL)
+    perror ("mkdtemp");
+}
+
+static void free_mount (void) __attribute__((destructor));
+
+static void
+free_mount (void)
+{
+  DIR *dir;
+  struct dirent *d;
+
+  /* Unmount all mountpoints in the temporary directory, then
+   * delete those directories and the parent.
+   */
+  dir = opendir (mountpoints);
+  if (!dir) {
+    perror (mountpoints);
+    return;
+  }
+
+  for (;;) {
+    errno = 0;
+    d = readdir (dir);
+    if (!d) break;
+
+    if (d->d_name[0] != '.') {
+      CLEANUP_FREE char *mp;
+
+      if (asprintf (&mp, "%s/%s", mountpoints, d->d_name) == -1) {
+        perror ("asprintf");
+        continue;
+      }
+
+      if (umount2 (mp, MNT_DETACH) == -1) /* lazy umount */
+        perror (mp);
+
+      if (rmdir (mp) == -1)
+        perror (mp);
+    }
+  }
+
+  /* Check readdir didn't fail */
+  if (errno != 0) {
+    perror ("readdir");
+    return;
+  }
+
+  /* Close the directory handle */
+  if (closedir (dir) == -1) {
+    perror ("closedir");
+    return;
+  }
+
+  if (rmdir (mountpoints) == -1)
+    perror (mountpoints);
+}
+
+/* Return true iff device is the root device (and therefore should be
+ * ignored from the point of view of user calls).
+ */
+static int
+is_root_device_stat (struct stat *statbuf)
+{
+  if (statbuf->st_rdev == root_device) return 1;
+  return 0;
+}
+
+static int
+is_root_device (const char *device)
+{
+  struct stat statbuf;
+
+  if (stat (device, &statbuf) == -1) {
+    perror (device);
+    return 0;
+  }
+
+  return is_root_device_stat (&statbuf);
+}
+
+typedef int (*block_dev_func_t) (const char *dev, struct stringsbuf *r);
+
+/* Execute a given function for each discovered block device */
+static char **
+foreach_block_device (block_dev_func_t func)
+{
+  DECLARE_STRINGSBUF (r);
+  DIR *dir;
+  struct dirent *d;
+  char dev_path[256];
+  int fd;
+  bool err = false;
+
+  dir = opendir ("/sys/block");
+  if (!dir) {
+    perror ("opendir: /sys/block");
+    return NULL;
+  }
+
+  for (;;) {
+    errno = 0;
+    d = readdir (dir);
+    if (!d) break;
+
+    if (STREQLEN (d->d_name, "sd", 2) ||
+        STREQLEN (d->d_name, "hd", 2) ||
+        STREQLEN (d->d_name, "ubd", 3) ||
+        STREQLEN (d->d_name, "vd", 2) ||
+        STREQLEN (d->d_name, "sr", 2)) {
+      snprintf (dev_path, sizeof dev_path, "/dev/%s", d->d_name);
+
+      /* Ignore the root device. */
+      if (is_root_device (dev_path))
+        continue;
+
+      /* RHBZ#514505: Some versions of qemu <= 0.10 add a
+       * CD-ROM device even though we didn't request it.  Try to
+       * detect this by seeing if the device contains media.
+       */
+      fd = open (dev_path, O_RDONLY|O_CLOEXEC);
+      if (fd == -1) {
+        perror (dev_path);
+        continue;
+      }
+      close (fd);
+
+      /* Call the map function for this device */
+      if ((*func)(d->d_name, &r) != 0) {
+        err = true;
+        break;
+      }
+    }
+  }
+
+  /* Check readdir didn't fail */
+  if (errno != 0) {
+    perror ("readdir: /sys/block");
+    free_stringslen (r.argv, r.size);
+    closedir (dir);
+    return NULL;
+  }
+
+  /* Close the directory handle */
+  if (closedir (dir) == -1) {
+    perror ("closedir: /sys/block");
+    free_stringslen (r.argv, r.size);
+    return NULL;
+  }
+
+  /* Free the result list on error */
+  if (err) {
+    free_stringslen (r.argv, r.size);
+    return NULL;
+  }
+
+  /* Sort the devices. */
+  if (r.size > 0)
+    sort_device_names (r.argv, r.size);
+
+  /* NULL terminate the list */
+  end_stringsbuf (&r);
+
+  return r.argv;
+}
+
+/* Add a device to the list of devices */
+static int
+add_device (const char *device, struct stringsbuf *r)
+{
+  char dev_path[256];
+  snprintf (dev_path, sizeof dev_path, "/dev/%s", device);
+
+  add_string (r, dev_path);
+
+  return 0;
+}
+
+char **
+get_all_block_devices (void)
+{
+  return foreach_block_device (add_device);
+}
+
+static int
+add_partitions (const char *device, struct stringsbuf *r)
+{
+  char devdir[256];
+
+  /* Open the device's directory under /sys/block */
+  snprintf (devdir, sizeof devdir, "/sys/block/%s", device);
+
+  DIR *dir = opendir (devdir);
+  if (!dir) {
+    perror (devdir);
+    free_stringslen (r->argv, r->size);
+    return -1;
+  }
+
+  /* Look in /sys/block/<device>/ for entries starting with <device>
+   * e.g. /sys/block/sda/sda1
+   */
+  errno = 0;
+  struct dirent *d;
+  while ((d = readdir (dir)) != NULL) {
+    if (STREQLEN (d->d_name, device, strlen (device))) {
+      char part[256];
+      snprintf (part, sizeof part, "/dev/%s", d->d_name);
+
+      add_string (r, part);
+    }
+  }
+
+  /* Check if readdir failed */
+  if (0 != errno) {
+    perror (devdir);
+    free_stringslen (r->argv, r->size);
+    closedir (dir);
+    return -1;
+  }
+
+  /* Close the directory handle */
+  if (closedir (dir) == -1) {
+    perror (device);
+    free_stringslen (r->argv, r->size);
+    return -1;
+  }
+
+  return 0;
+}
+
+char **
+get_all_partitions (void)
+{
+  return foreach_block_device (add_partitions);
+}
+
+char **
+get_all_mddevs (void)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return NULL;
+}
+
+char **
+get_all_lvs (void)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return NULL;
+}
+
+char **
+get_all_ldmvols (void)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return NULL;
+}
+
+char **
+get_all_ldmparts (void)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return NULL;
+}
+
+char **
+get_all_btrfs_subvolumes (const char *fs)
+{
+  error (EXIT_FAILURE, 0, "%s: not implemented", __func__);
+  return NULL;
+}
+
+static char *
+get_blkid_tag (const char *device, const char *tag)
+{
+  char *out;
+  CLEANUP_FREE char *err = NULL;
+  int r;
+  size_t len;
+
+  r = commandr (&out, &err,
+                "blkid",
+                /* Adding -c option kills all caching, even on RHEL 5. */
+                "-c", "/dev/null",
+                "-o", "value", "-s", tag, device, NULL);
+  if (r != 0 && r != 2) {
+    if (r >= 0)
+      fprintf (stderr, "%s: %s (blkid returned %d)\n", device, err, r);
+    else
+      fprintf (stderr, "%s: %s\n", device, err);
+    free (out);
+    return NULL;
+  }
+
+  if (r == 2) {                 /* means UUID etc not found */
+    free (out);
+    out = strdup ("");
+    if (out == NULL)
+      perror ("strdup");
+    return out;
+  }
+
+  /* Trim trailing \n if present. */
+  len = strlen (out);
+  if (len > 0 && out[len-1] == '\n')
+    out[len-1] = '\0';
+
+  return out;                   /* caller frees */
+}
+
+char *
+get_vfs_type (const char *fs)
+{
+  return get_blkid_tag (fs, "TYPE");
+}
+
+/* Test if sfdisk is recent enough to have --part-type, to be used instead
+ * of --print-id and --change-id.
+ */
+static int
+test_sfdisk_has_part_type (void)
+{
+  static int tested = -1;
+  int r;
+  CLEANUP_FREE char *out = NULL, *err = NULL;
+
+  if (tested != -1)
+    return tested;
+
+  r = command (&out, &err, "sfdisk", "--help", NULL);
+  if (r == -1) {
+    fprintf (stderr, "%s: %s\n", "sfdisk --help", err);
+    return -1;
+  }
+
+  tested = strstr (out, "--part-type") != NULL;
+  return tested;
+}
+
+static char *
+part_to_dev (const char *part)
+{
+  int err = 1;
+  size_t n = strlen (part);
+  char *r;
+
+  while (n >= 1 && c_isdigit (part[n-1])) {
+    err = 0;
+    n--;
+  }
+
+  if (err) {
+    fprintf (stderr, "device name is not a partition\n");
+    return NULL;
+  }
+
+  r = strndup (part, n);
+  if (r == NULL) {
+    perror ("strdup");
+    return NULL;
+  }
+
+  return r;
+}
+
+static int
+part_to_partnum (const char *part)
+{
+  int err = 1;
+  size_t n = strlen (part);
+  int r;
+
+  while (n >= 1 && c_isdigit (part[n-1])) {
+    err = 0;
+    n--;
+  }
+
+  if (err) {
+    fprintf (stderr, "device name is not a partition\n");
+    return -1;
+  }
+
+  if (sscanf (&part[n], "%d", &r) != 1) {
+    fprintf (stderr, "could not parse number\n");
+    return -1;
+  }
+
+  return r;
+}
+
+int
+get_partition_mbr_id (const char *fs)
+{
+  CLEANUP_FREE char *device;
+  int partnum;
+  char partnum_str[16];
+  const char *param =
+    test_sfdisk_has_part_type () ? "--part-type" : "--print-id";
+  CLEANUP_FREE char *out = NULL, *err = NULL;
+  int r;
+  unsigned id;
+
+  /* Get the block device and partition number from the filesystem
+   * string.
+   */
+  device = part_to_dev (fs);
+  if (device == NULL)
+    return -1;
+  partnum = part_to_partnum (fs);
+  if (partnum == -1)
+    return -1;
+  snprintf (partnum_str, sizeof partnum_str, "%d", partnum);
+
+  r = command (&out, &err, "sfdisk", param, device, partnum_str, NULL);
+  if (r == -1) {
+    fprintf (stderr, "sfdisk %s: %s\n", param, err);
+    return -1;
+  }
+
+  /* It's printed in hex ... */
+  if (sscanf (out, "%x", &id) != 1) {
+    fprintf (stderr, "sfdisk --print-id: cannot parse output: %s\n", out);
+    return -1;
+  }
+
+  return id;
+}
+
+/* When mounting filesystems, we place them in temporary directories
+ * under 'mountpoints'.  We name the temporary directory after the
+ * device name, but since device names contain '/' characters, we have
+ * to mangle the name.
+ */
+static char *
+get_mount_name (const char *fs)
+{
+  char *ret;
+  size_t i;
+
+  if (asprintf (&ret, "%s/%s", mountpoints, fs) == -1) {
+    perror ("asprintf");
+    return NULL;
+  }
+
+  for (i = strlen (mountpoints) + 1; i < strlen (ret); ++i) {
+    if (ret[i] == '/')
+      ret[i] = '_';
+  }
+
+  return ret;                   /* caller frees */
+}
+
+int
+is_mountable (const char *fs)
+{
+  CLEANUP_FREE char *mp = NULL;
+  struct stat statbuf;
+  int r;
+  CLEANUP_FREE char *err = NULL;
+
+  mp = get_mount_name (fs);
+  if (mp == NULL)
+    return -1;
+
+  if (stat (mp, &statbuf) == 0 && S_ISDIR (statbuf.st_mode))
+    return 1;                   /* mountable, and mounted already */
+
+  /* Try to create the mountpoint. */
+  if (mkdir (mp, 0700) == -1) {
+    perror (mp);
+    return -1;
+  }
+
+  /* Try to mount the filesystem. */
+  r = command (NULL, &err,
+               "mount", "-o", "ro", fs, mp, NULL);
+  if (r == -1) {
+    fprintf (stderr, "mount: %s: %s\n", fs, err);
+
+    /* Now hack things for the *BSDs. */
+    /* FreeBSD fs is a variant of ufs called ufs2 ... */
+    free (err); err = NULL;
+    r = command (NULL, &err,
+                 "mount", "-o", "ro,ufstype=ufs2", fs, mp, NULL);
+    if (r == -1) {
+      fprintf (stderr, "mount [ufs2]: %s: %s\n", fs, err);
+
+      /* while NetBSD and OpenBSD use another variant labeled 44bsd */
+      free (err); err = NULL;
+      r = command (NULL, &err,
+                   "mount", "-o", "ro,ufstype=44bsd", fs, mp, NULL);
+      if (r == -1) {
+        fprintf (stderr, "mount [44bsd]: %s: %s\n", fs, err);
+
+        /* Mount failed, so remove the mountpoint. */
+        rmdir (mp);
+        return 0;
+      }
+    }
+  }
+
+  /* Mount succeeded. */
+  return 1;
+}
+
+int
+get_mount (const char *fs, const char *filename, char **relative_filename)
+{
+  CLEANUP_FREE char *mp = NULL;
+  int r;
+
+  if (filename[0] != '/') {
+    fprintf (stderr, "get_mount: filename is not an absolute path: %s\n",
+             filename);
+    return -1;
+  }
+
+  r = is_mountable (fs);
+  if (r == -1)
+    return -1;
+  if (r == 0) {
+    fprintf (stderr, "get_mount: called on non-mountable filesystem: %s\n",
+             fs);
+    return -1;
+  }
+
+  mp = get_mount_name (fs);
+
+  /* Construct the filename relative to the mountpoint. */
+  if (asprintf (relative_filename, "%s%s", mp, filename) == -1) {
+    perror ("asprintf");
+    return -1;
+  }
+
+  return 0;
+}
diff --git a/inspection/rules.h b/inspection/rules.h
new file mode 100644
index 0000000..cf5b646
--- /dev/null
+++ b/inspection/rules.h
@@ -0,0 +1,69 @@
+/* guestfs-inspection
+ * Copyright (C) 2009-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.
+ */
+
+#ifndef GUESTFS_RULES_H
+#define GUESTFS_RULES_H
+
+#include <stdbool.h>
+
+#include "gl_oset.h"
+
+extern int verbose;
+
+/* facts.c */
+struct fact {
+  char *term_name;
+  size_t nr_term_args;
+  char *term_arg[];
+};
+typedef struct fact fact;
+
+/* Create a fact on the heap.  This doesn't copy the strings, but they
+ * are deep copied when we call add_fact.
+ */
+extern fact *create_fact (const char *term_name, size_t n, ...);
+
+/* Create a fact on the stack and set 'var' to be a const pointer to
+ * it.  This doesn't copy the strings, but they are deep copied when
+ * we call add_fact.  Because of stupidity in C99, this is way more
+ * complex than it needs to be.
+ */
+#define CREATE_FACT(var,name,n,...)                     \
+  struct {                                              \
+    const char *term_name;                              \
+    size_t nr_term_args;                                \
+    const char *term_arg[n];                            \
+  } var##_tmp_fact = { (name), (n), { __VA_ARGS__ } };  \
+  const fact *var = (struct fact *) &var##_tmp_fact
+
+extern void clear_true_facts (void);
+extern void clear_false_facts (void);
+extern size_t count_true_facts (void);
+extern void print_fact (bool is_true, const fact *f, FILE *fp);
+extern void print_true_facts (void);
+extern void print_false_facts (void);
+extern void add_all_fact_strings (gl_oset_t set);
+extern void add_strings_from_facts (gl_oset_t set, const char *term_name, size_t arg_i);
+extern bool is_fact (bool is_true, const fact *);
+extern bool add_fact (bool is_true, const fact *);
+
+/* rules.c - generated code */
+extern const char *all_strings[];
+extern void rules (void);
+
+#endif /* GUESTFS_RULES_H */
diff --git a/inspection/stringsbuf.c b/inspection/stringsbuf.c
new file mode 100644
index 0000000..338134e
--- /dev/null
+++ b/inspection/stringsbuf.c
@@ -0,0 +1,254 @@
+/* libguestfs - the guestfsd daemon
+ * Copyright (C) 2009-2015 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 <errno.h>
+#include <error.h>
+#include <assert.h>
+
+#include "guestfs-internal-all.h"
+
+#include "stringsbuf.h"
+
+void
+add_string_nodup (struct stringsbuf *sb, char *str)
+{
+  char **new_argv;
+
+  if (sb->size >= sb->alloc) {
+    sb->alloc += 64;
+    new_argv = realloc (sb->argv, sb->alloc * sizeof (char *));
+    if (new_argv == NULL)
+      error (EXIT_FAILURE, errno, "realloc");
+    sb->argv = new_argv;
+  }
+
+  sb->argv[sb->size] = str;
+  sb->size++;
+}
+
+void
+add_string (struct stringsbuf *sb, const char *str)
+{
+  char *new_str = NULL;
+
+  if (str) {
+    new_str = strdup (str);
+    if (new_str == NULL)
+      error (EXIT_FAILURE, errno, "strdup");
+  }
+
+  add_string_nodup (sb, new_str);
+}
+
+void
+add_sprintf (struct stringsbuf *sb, const char *fs, ...)
+{
+  va_list args;
+  char *str;
+  int r;
+
+  va_start (args, fs);
+  r = vasprintf (&str, fs, args);
+  va_end (args);
+  if (r == -1)
+    error (EXIT_FAILURE, errno, "vasprintf");
+
+  add_string_nodup (sb, str);
+}
+
+void
+end_stringsbuf (struct stringsbuf *sb)
+{
+  add_string_nodup (sb, NULL);
+}
+
+void
+free_stringsbuf (struct stringsbuf *sb)
+{
+  if (sb->argv != NULL)
+    free_stringslen (sb->argv, sb->size);
+}
+
+size_t
+count_strings (char *const *argv)
+{
+  size_t argc;
+
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    ;
+  return argc;
+}
+
+static int
+compare (const void *vp1, const void *vp2)
+{
+  char * const *p1 = (char * const *) vp1;
+  char * const *p2 = (char * const *) vp2;
+  return strcmp (*p1, *p2);
+}
+
+void
+sort_strings (char **argv, size_t len)
+{
+  qsort (argv, len, sizeof (char *), compare);
+}
+
+void
+free_strings (char **argv)
+{
+  size_t argc;
+
+  if (!argv)
+    return;
+
+  for (argc = 0; argv[argc] != NULL; ++argc)
+    free (argv[argc]);
+  free (argv);
+}
+
+void
+free_stringslen (char **argv, size_t len)
+{
+  size_t i;
+
+  if (!argv)
+    return;
+
+  for (i = 0; i < len; ++i)
+    free (argv[i]);
+  free (argv);
+}
+
+/* Compare device names (including partition numbers if present).
+ * https://rwmj.wordpress.com/2011/01/09/how-are-linux-drives-named-beyond-drive-26-devsdz/
+ */
+int
+compare_device_names (const char *a, const char *b)
+{
+  size_t alen, blen;
+  int r;
+  int a_partnum, b_partnum;
+
+  /* Skip /dev/ prefix if present. */
+  if (STRPREFIX (a, "/dev/"))
+    a += 5;
+  if (STRPREFIX (b, "/dev/"))
+    b += 5;
+
+  /* Skip sd/hd/ubd/vd. */
+  alen = strcspn (a, "d");
+  blen = strcspn (b, "d");
+  assert (alen > 0 && alen <= 2);
+  assert (blen > 0 && blen <= 2);
+  a += alen + 1;
+  b += blen + 1;
+
+  /* Get device name part, that is, just 'a', 'ab' etc. */
+  alen = strcspn (a, "0123456789");
+  blen = strcspn (b, "0123456789");
+
+  /* If device name part is longer, it is always greater, eg.
+   * "/dev/sdz" < "/dev/sdaa".
+   */
+  if (alen != blen)
+    return alen - blen;
+
+  /* Device name parts are the same length, so do a regular compare. */
+  r = strncmp (a, b, alen);
+  if (r != 0)
+    return r;
+
+  /* Compare partitions numbers. */
+  a += alen;
+  b += alen;
+
+  /* If no partition numbers, bail -- the devices are the same.  This
+   * can happen in one peculiar case: where you have a mix of devices
+   * with different interfaces (eg. /dev/sda and /dev/vda).
+   * (RHBZ#858128).
+   */
+  if (!*a && !*b)
+    return 0;
+
+  r = sscanf (a, "%d", &a_partnum);
+  assert (r == 1);
+  r = sscanf (b, "%d", &b_partnum);
+  assert (r == 1);
+
+  return a_partnum - b_partnum;
+}
+
+static int
+compare_device_names_vp (const void *vp1, const void *vp2)
+{
+  char * const *p1 = (char * const *) vp1;
+  char * const *p2 = (char * const *) vp2;
+  return compare_device_names (*p1, *p2);
+}
+
+void
+sort_device_names (char **argv, size_t len)
+{
+  qsort (argv, len, sizeof (char *), compare_device_names_vp);
+}
+
+char *
+concat_strings (char *const *argv)
+{
+  return join_strings ("", argv);
+}
+
+char *
+join_strings (const char *separator, char *const *argv)
+{
+  size_t i, len, seplen, rlen;
+  char *r;
+
+  seplen = strlen (separator);
+
+  len = 0;
+  for (i = 0; argv[i] != NULL; ++i) {
+    if (i > 0)
+      len += seplen;
+    len += strlen (argv[i]);
+  }
+  len++; /* for final \0 */
+
+  r = malloc (len);
+  if (r == NULL)
+    error (EXIT_FAILURE, errno, "malloc");
+
+  rlen = 0;
+  for (i = 0; argv[i] != NULL; ++i) {
+    if (i > 0) {
+      memcpy (&r[rlen], separator, seplen);
+      rlen += seplen;
+    }
+    len = strlen (argv[i]);
+    memcpy (&r[rlen], argv[i], len);
+    rlen += len;
+  }
+  r[rlen] = '\0';
+
+  return r;
+}
diff --git a/inspection/stringsbuf.h b/inspection/stringsbuf.h
new file mode 100644
index 0000000..4a26ffb
--- /dev/null
+++ b/inspection/stringsbuf.h
@@ -0,0 +1,56 @@
+/* libguestfs - the guestfsd daemon
+ * Copyright (C) 2009-2015 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.
+ */
+
+#ifndef GUESTFS_INSPECTION_STRINGSBUF_H
+#define GUESTFS_INSPECTION_STRINGSBUF_H
+
+/* Growable strings buffer. */
+struct stringsbuf {
+  char **argv;
+  size_t size;
+  size_t alloc;
+};
+#define DECLARE_STRINGSBUF(v) \
+  struct stringsbuf (v) = { .argv = NULL, .size = 0, .alloc = 0 }
+
+/* Append a string to the strings buffer.
+ *
+ * add_string_nodup: don't copy the string.
+ * add_string: copy the string.
+ * end_stringsbuf: NULL-terminate the buffer.
+ */
+extern void add_string_nodup (struct stringsbuf *sb, char *str);
+extern void add_string (struct stringsbuf *sb, const char *str);
+extern void add_sprintf (struct stringsbuf *sb, const char *fs, ...)
+  __attribute__((format (printf,2,3)));
+extern void end_stringsbuf (struct stringsbuf *sb);
+extern void free_stringsbuf (struct stringsbuf *sb);
+
+extern size_t count_strings (char *const *argv);
+extern void sort_strings (char **argv, size_t len);
+extern void free_strings (char **argv);
+extern void free_stringslen (char **argv, size_t len);
+
+extern void sort_device_names (char **argv, size_t len);
+extern int compare_device_names (const char *a, const char *b);
+
+/* Concatenate strings, optionally with a separator string between each. */
+extern char *concat_strings (char *const *argv);
+extern char *join_strings (const char *separator, char *const *argv);
+
+#endif /* GUESTFS_INSPECTION_STRINGSBUF_H */
diff --git a/inspection/utils.c b/inspection/utils.c
new file mode 100644
index 0000000..9e44ab3
--- /dev/null
+++ b/inspection/utils.c
@@ -0,0 +1,66 @@
+/* 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.
+ */
+
+#include <config.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <error.h>
+#include <errno.h>
+
+#include <pcre.h>
+
+#include "cleanups.h"
+#include "inspection.h"
+
+/* Get the first line of the file, without any trailing newline
+ * character.  The caller must free the returned string.
+ *
+ * If the file is completely empty or begins with '\n' this returns an
+ * empty string.
+ *
+ * This function never returns NULL.  If the file does not exist or
+ * there is some other error, it exits with an error.
+ */
+char *
+first_line_of_file (const char *fs, const char *filename)
+{
+  CLEANUP_FREE char *relative_filename = NULL;
+  FILE *fp;
+  ssize_t r;
+  size_t n = 0;
+  char *line = NULL;
+
+  if (get_mount (fs, filename, &relative_filename) == -1)
+    error (EXIT_FAILURE, 0, "%s: failed to get mountpoint: %s %s",
+           __func__, fs, filename);
+
+  fp = fopen (relative_filename, "r");
+  if (fp == NULL)
+    error (EXIT_FAILURE, errno, "%s: open: %s", __func__, relative_filename);
+  r = getline (&line, &n, fp);
+  if (r == -1)
+    error (EXIT_FAILURE, errno, "%s: getline: %s", __func__, relative_filename);
+  fclose (fp);
+
+  if (r > 0 && line[r-1] == '\n')
+    line[r-1] = '\0';
+
+  return line;                  /* caller frees */
+}
diff --git a/po/POTFILES b/po/POTFILES
index 2a1e313..c89ca14 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -259,6 +259,13 @@ gobject/src/struct-version.c
 gobject/src/struct-xattr.c
 gobject/src/struct-xfsinfo.c
 gobject/src/tristate.c
+inspection/detect.c
+inspection/facts.c
+inspection/inspection.c
+inspection/match.c
+inspection/mount.c
+inspection/stringsbuf.c
+inspection/utils.c
 inspector/inspector.c
 java/com_redhat_et_libguestfs_GuestFS.c
 lua/lua-guestfs.c
diff --git a/src/guestfs.pod b/src/guestfs.pod
index 2a199c0..4fb58e6 100644
--- a/src/guestfs.pod
+++ b/src/guestfs.pod
@@ -3545,6 +3545,7 @@ Other libguestfs topics:
 L<guestfs-building(1)>,
 L<guestfs-faq(1)>,
 L<guestfs-hacking(1)>,
+L<guestfs-inspection(8)>,
 L<guestfs-internals(1)>,
 L<guestfs-performance(1)>,
 L<guestfs-release-notes(1)>,
-- 
2.5.0




More information about the Libguestfs mailing list