[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]

[Libguestfs] [PATCH v8 42/42] daemon: Reimplement most inspection APIs in the daemon, in OCaml.



Move the following APIs into the daemon, reimplemented in OCaml:

* inspect_os
* inspect_get_roots
* inspect_get_mountpoints
* inspect_get_filesystems
* inspect_get_format [deprecated]
* inspect_get_type
* inspect_get_distro
* inspect_get_package_format
* inspect_get_package_management
* inspect_get_product_name
* inspect_get_product_variant
* inspect_get_major_version
* inspect_get_minor_version
* inspect_get_arch
* inspect_get_hostname
* inspect_get_windows_systemroot
* inspect_get_windows_software_hive
* inspect_get_windows_system_hive
* inspect_get_windows_current_control_set
* inspect_get_drive_mappings
* inspect_is_live [deprecated]
* inspect_is_netinst [deprecated]
* inspect_is_multipart [deprecated]

The following inspection APIs have NOT been reimplemented in this commit:

* inspect_list_applications [deprecated]
* inspect_list_applications2
* inspect_get_icon

This also embeds the ocaml-augeas library (upstream here:
http://git.annexia.org/?p=ocaml-augeas.git;a=summary), but it's
identical to the upstream version and should remain so.
---
 daemon/Makefile.am                          |   17 +
 daemon/augeas-c.c                           |  288 ++++
 daemon/augeas.README                        |    8 +
 daemon/augeas.ml                            |   59 +
 daemon/augeas.mli                           |   95 ++
 daemon/chroot.ml                            |    2 +-
 daemon/daemon_utils_tests.ml                |   15 +
 daemon/guestfsd.c                           |    2 +
 daemon/inspect.ml                           |  398 +++++
 daemon/inspect.mli                          |   41 +
 daemon/inspect_fs.ml                        |  366 +++++
 daemon/inspect_fs.mli                       |   23 +
 daemon/inspect_fs_unix.ml                   |  789 ++++++++++
 daemon/inspect_fs_unix.mli                  |   44 +
 daemon/inspect_fs_unix_fstab.ml             |  537 +++++++
 daemon/inspect_fs_unix_fstab.mli            |   34 +
 daemon/inspect_fs_windows.ml                |  493 ++++++
 daemon/inspect_fs_windows.mli               |   24 +
 daemon/inspect_types.ml                     |  313 ++++
 daemon/inspect_types.mli                    |  180 +++
 daemon/inspect_utils.ml                     |  193 +++
 daemon/inspect_utils.mli                    |   57 +
 daemon/mount.ml                             |   61 +
 daemon/mount.mli                            |    2 +
 daemon/utils.ml                             |  100 ++
 daemon/utils.mli                            |   12 +
 docs/C_SOURCE_FILES                         |    4 +-
 generator/actions.ml                        |    2 +
 generator/actions_inspection.ml             |  394 ++---
 generator/actions_inspection.mli            |    1 +
 generator/actions_inspection_deprecated.ml  |    7 +
 generator/actions_inspection_deprecated.mli |    1 +
 generator/daemon.ml                         |   63 +-
 generator/proc_nr.ml                        |   23 +
 lib/MAX_PROC_NR                             |    2 +-
 lib/Makefile.am                             |    3 -
 lib/guestfs-internal.h                      |  185 +--
 lib/handle.c                                |    1 -
 lib/inspect-apps.c                          |  122 +-
 lib/inspect-fs-unix.c                       | 2158 ---------------------------
 lib/inspect-fs-windows.c                    |  739 ---------
 lib/inspect-fs.c                            |  758 ----------
 lib/inspect-icon.c                          |  261 ++--
 lib/inspect.c                               |  732 +--------
 lib/version.c                               |   28 +
 45 files changed, 4661 insertions(+), 4976 deletions(-)

diff --git a/daemon/Makefile.am b/daemon/Makefile.am
index 8921f6239..1ea046383 100644
--- a/daemon/Makefile.am
+++ b/daemon/Makefile.am
@@ -75,6 +75,7 @@ guestfsd_SOURCES = \
 	actions.h \
 	available.c \
 	augeas.c \
+	augeas-c.c \
 	base64.c \
 	blkdiscard.c \
 	blkid.c \
@@ -253,6 +254,7 @@ guestfsd_CFLAGS = \
 # library and then linked to the daemon.  See
 # https://caml.inria.fr/pub/docs/manual-ocaml/intfc.html
 SOURCES_MLI = \
+	augeas.mli \
 	blkid.mli \
 	btrfs.mli \
 	chroot.mli \
@@ -261,6 +263,13 @@ SOURCES_MLI = \
 	file.mli \
 	filearch.mli \
 	findfs.mli \
+	inspect.mli \
+	inspect_fs.mli \
+	inspect_fs_unix.mli \
+	inspect_fs_unix_fstab.mli \
+	inspect_fs_windows.mli \
+	inspect_types.mli \
+	inspect_utils.mli \
 	is.mli \
 	ldm.mli \
 	link.mli \
@@ -274,6 +283,7 @@ SOURCES_MLI = \
 	utils.mli
 
 SOURCES_ML = \
+	augeas.ml \
 	types.ml \
 	utils.ml \
 	structs.ml \
@@ -295,6 +305,13 @@ SOURCES_ML = \
 	parted.ml \
 	listfs.ml \
 	realpath.ml \
+	inspect_types.ml \
+	inspect_utils.ml \
+	inspect_fs_unix_fstab.ml \
+	inspect_fs_unix.ml \
+	inspect_fs_windows.ml \
+	inspect_fs.ml \
+	inspect.ml \
 	callbacks.ml \
 	daemon.ml
 
diff --git a/daemon/augeas-c.c b/daemon/augeas-c.c
new file mode 100644
index 000000000..c06bf92da
--- /dev/null
+++ b/daemon/augeas-c.c
@@ -0,0 +1,288 @@
+/* Augeas OCaml bindings
+ * Copyright (C) 2008-2012 Red Hat Inc., Richard W.M. Jones
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * $Id: augeas_c.c,v 1.1 2008/05/06 10:48:20 rjones Exp $
+ */
+
+#include "config.h"
+
+#include <augeas.h>
+
+#include <caml/alloc.h>
+#include <caml/memory.h>
+#include <caml/mlvalues.h>
+#include <caml/fail.h>
+#include <caml/callback.h>
+#include <caml/custom.h>
+
+typedef augeas *augeas_t;
+
+/* Raise an Augeas.Error exception. */
+static void
+raise_error (const char *msg)
+{
+  caml_raise_with_string (*caml_named_value ("Augeas.Error"), msg);
+}
+
+/* Map OCaml flags to C flags. */
+static int flag_map[] = {
+  /* AugSaveBackup */  AUG_SAVE_BACKUP,
+  /* AugSaveNewFile */ AUG_SAVE_NEWFILE,
+  /* AugTypeCheck */   AUG_TYPE_CHECK,
+  /* AugNoStdinc */    AUG_NO_STDINC,
+  /* AugSaveNoop */    AUG_SAVE_NOOP,
+  /* AugNoLoad */      AUG_NO_LOAD,
+};
+
+/* Wrap and unwrap augeas_t handles, with a finalizer. */
+#define Augeas_t_val(rv) (*(augeas_t *)Data_custom_val(rv))
+
+static void
+augeas_t_finalize (value tv)
+{
+  augeas_t t = Augeas_t_val (tv);
+  if (t) aug_close (t);
+}
+
+static struct custom_operations custom_operations = {
+  (char *) "augeas_t_custom_operations",
+  augeas_t_finalize,
+  custom_compare_default,
+  custom_hash_default,
+  custom_serialize_default,
+  custom_deserialize_default
+};
+
+static value Val_augeas_t (augeas_t t)
+{
+  CAMLparam0 ();
+  CAMLlocal1 (rv);
+  /* We could choose these so that the GC can make better decisions.
+   * See 18.9.2 of the OCaml manual.
+   */
+  const int used = 0;
+  const int max = 1;
+
+  rv = caml_alloc_custom (&custom_operations,
+			  sizeof (augeas_t), used, max);
+  Augeas_t_val(rv) = t;
+
+  CAMLreturn (rv);
+}
+
+#pragma GCC diagnostic ignored "-Wmissing-prototypes"
+
+/* val create : string -> string option -> flag list -> t */
+CAMLprim value
+ocaml_augeas_create (value rootv, value loadpathv, value flagsv)
+{
+  CAMLparam1 (rootv);
+  char *root = String_val (rootv);
+  char *loadpath;
+  int flags = 0, i;
+  augeas_t t;
+
+  /* Optional loadpath. */
+  loadpath =
+    loadpathv == Val_int (0)
+    ? NULL
+    : String_val (Field (loadpathv, 0));
+
+  /* Convert list of flags to C. */
+  for (; flagsv != Val_int (0); flagsv = Field (flagsv, 1)) {
+    i = Int_val (Field (flagsv, 0));
+    flags |= flag_map[i];
+  }
+
+  t = aug_init (root, loadpath, flags);
+
+  if (t == NULL)
+    raise_error ("Augeas.create");
+
+  CAMLreturn (Val_augeas_t (t));
+}
+
+/* val close : t -> unit */
+CAMLprim value
+ocaml_augeas_close (value tv)
+{
+  CAMLparam1 (tv);
+  augeas_t t = Augeas_t_val (tv);
+
+  if (t) {
+    aug_close (t);
+    Augeas_t_val(tv) = NULL;	/* So the finalizer doesn't double-free. */
+  }
+
+  CAMLreturn (Val_unit);
+}
+
+/* val get : t -> path -> value option */
+CAMLprim value
+ocaml_augeas_get (value tv, value pathv)
+{
+  CAMLparam2 (tv, pathv);
+  CAMLlocal2 (optv, v);
+  augeas_t t = Augeas_t_val (tv);
+  char *path = String_val (pathv);
+  const char *val;
+  int r;
+
+  r = aug_get (t, path, &val);
+  if (r == 1) {			/* Return Some val */
+    v = caml_copy_string (val);
+    optv = caml_alloc (1, 0);
+    Field (optv, 0) = v;
+  } else if (r == 0)		/* Return None */
+    optv = Val_int (0);
+  else if (r == -1)		/* Error or multiple matches */
+    raise_error ("Augeas.get");
+  else
+    failwith ("Augeas.get: bad return value");
+
+  CAMLreturn (optv);
+}
+
+/* val exists : t -> path -> bool */
+CAMLprim value
+ocaml_augeas_exists (value tv, value pathv)
+{
+  CAMLparam2 (tv, pathv);
+  CAMLlocal1 (v);
+  augeas_t t = Augeas_t_val (tv);
+  char *path = String_val (pathv);
+  int r;
+
+  r = aug_get (t, path, NULL);
+  if (r == 1)			/* Return true. */
+    v = Val_int (1);
+  else if (r == 0)		/* Return false */
+    v = Val_int (0);
+  else if (r == -1)		/* Error or multiple matches */
+    raise_error ("Augeas.exists");
+  else
+    failwith ("Augeas.exists: bad return value");
+
+  CAMLreturn (v);
+}
+
+/* val insert : t -> ?before:bool -> path -> string -> unit */
+CAMLprim value
+ocaml_augeas_insert (value tv, value beforev, value pathv, value labelv)
+{
+  CAMLparam4 (tv, beforev, pathv, labelv);
+  augeas_t t = Augeas_t_val (tv);
+  char *path = String_val (pathv);
+  char *label = String_val (labelv);
+  int before;
+
+  before = beforev == Val_int (0) ? 0 : Int_val (Field (beforev, 0));
+
+  if (aug_insert (t, path, label, before) == -1)
+    raise_error ("Augeas.insert");
+
+  CAMLreturn (Val_unit);
+}
+
+/* val rm : t -> path -> int */
+CAMLprim value
+ocaml_augeas_rm (value tv, value pathv)
+{
+  CAMLparam2 (tv, pathv);
+  augeas_t t = Augeas_t_val (tv);
+  char *path = String_val (pathv);
+  int r;
+
+  r = aug_rm (t, path);
+  if (r == -1)
+    raise_error ("Augeas.rm");
+
+  CAMLreturn (Val_int (r));
+}
+
+/* val matches : t -> path -> path list */
+CAMLprim value
+ocaml_augeas_match (value tv, value pathv)
+{
+  CAMLparam2 (tv, pathv);
+  CAMLlocal3 (rv, v, cons);
+  augeas_t t = Augeas_t_val (tv);
+  char *path = String_val (pathv);
+  char **matches;
+  int r, i;
+
+  r = aug_match (t, path, &matches);
+  if (r == -1)
+    raise_error ("Augeas.matches");
+
+  /* Copy the paths to a list. */
+  rv = Val_int (0);
+  for (i = 0; i < r; ++i) {
+    v = caml_copy_string (matches[i]);
+    free (matches[i]);
+    cons = caml_alloc (2, 0);
+    Field (cons, 1) = rv;
+    Field (cons, 0) = v;
+    rv = cons;
+  }
+
+  free (matches);
+
+  CAMLreturn (rv);
+}
+
+/* val count_matches : t -> path -> int */
+CAMLprim value
+ocaml_augeas_count_matches (value tv, value pathv)
+{
+  CAMLparam2 (tv, pathv);
+  augeas_t t = Augeas_t_val (tv);
+  char *path = String_val (pathv);
+  int r;
+
+  r = aug_match (t, path, NULL);
+  if (r == -1)
+    raise_error ("Augeas.count_matches");
+
+  CAMLreturn (Val_int (r));
+}
+
+/* val save : t -> unit */
+CAMLprim value
+ocaml_augeas_save (value tv)
+{
+  CAMLparam1 (tv);
+  augeas_t t = Augeas_t_val (tv);
+
+  if (aug_save (t) == -1)
+    raise_error ("Augeas.save");
+
+  CAMLreturn (Val_unit);
+}
+
+/* val load : t -> unit */
+CAMLprim value
+ocaml_augeas_load (value tv)
+{
+  CAMLparam1 (tv);
+  augeas_t t = Augeas_t_val (tv);
+
+  if (aug_load (t) == -1)
+    raise_error ("Augeas.load");
+
+  CAMLreturn (Val_unit);
+}
diff --git a/daemon/augeas.README b/daemon/augeas.README
new file mode 100644
index 000000000..938dfd255
--- /dev/null
+++ b/daemon/augeas.README
@@ -0,0 +1,8 @@
+The files augeas-c.c, augeas.ml and augeas.mli come from the
+ocaml-augeas library:
+
+  http://git.annexia.org/?p=ocaml-augeas.git
+
+which is released under a compatible license.  We try to keep them
+identical, so if you make changes to these files then you must also
+submit the changes to ocaml-augeas, and vice versa.
\ No newline at end of file
diff --git a/daemon/augeas.ml b/daemon/augeas.ml
new file mode 100644
index 000000000..f556df0f1
--- /dev/null
+++ b/daemon/augeas.ml
@@ -0,0 +1,59 @@
+(* Augeas OCaml bindings
+ * Copyright (C) 2008 Red Hat Inc., Richard W.M. Jones
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * $Id: augeas.ml,v 1.2 2008/05/06 10:48:20 rjones Exp $
+ *)
+
+type t
+
+exception Error of string
+
+type flag =
+  | AugSaveBackup
+  | AugSaveNewFile
+  | AugTypeCheck
+  | AugNoStdinc
+  | AugSaveNoop
+  | AugNoLoad
+
+type path = string
+
+type value = string
+
+external create : string -> string option -> flag list -> t
+  = "ocaml_augeas_create"
+external close : t -> unit
+  = "ocaml_augeas_close"
+external get : t -> path -> value option
+  = "ocaml_augeas_get"
+external exists : t -> path -> bool
+  = "ocaml_augeas_exists"
+external insert : t -> ?before:bool -> path -> string -> unit
+  = "ocaml_augeas_insert"
+external rm : t -> path -> int
+  = "ocaml_augeas_rm"
+external matches : t -> path -> path list
+  = "ocaml_augeas_match"
+external count_matches : t -> path -> int
+  = "ocaml_augeas_count_matches"
+external save : t -> unit
+  = "ocaml_augeas_save"
+external load : t -> unit
+  = "ocaml_augeas_load"
+
+let () =
+  Callback.register_exception "Augeas.Error" (Error "")
diff --git a/daemon/augeas.mli b/daemon/augeas.mli
new file mode 100644
index 000000000..64e824014
--- /dev/null
+++ b/daemon/augeas.mli
@@ -0,0 +1,95 @@
+(** Augeas OCaml bindings *)
+(* Copyright (C) 2008 Red Hat Inc., Richard W.M. Jones
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * $Id: augeas.mli,v 1.2 2008/05/06 10:48:20 rjones Exp $
+ *)
+
+type t
+  (** Augeas library handle. *)
+
+exception Error of string
+  (** This exception is thrown when the underlying Augeas library
+      returns an error. *)
+
+type flag =
+  | AugSaveBackup			(** Rename original with .augsave *)
+  | AugSaveNewFile			(** Save changes to .augnew *)
+  | AugTypeCheck			(** Type-check lenses *)
+  | AugNoStdinc
+  | AugSaveNoop
+  | AugNoLoad
+  (** Flags passed to the {!create} function. *)
+
+type path = string
+  (** A path expression.
+
+      Note in future we may replace this with a type-safe path constructor. *)
+
+type value = string
+  (** A value. *)
+
+val create : string -> string option -> flag list -> t
+  (** [create root loadpath flags] creates an Augeas handle.
+
+      [root] is a file system path describing the location
+      of the configuration files.
+
+      [loadpath] is an optional colon-separated list of directories
+      which are searched for schema definitions.
+
+      [flags] is a list of flags. *)
+
+val close : t -> unit
+  (** [close handle] closes the handle.
+
+      You don't need to close handles explicitly with this function:
+      they will be finalized eventually by the garbage collector.
+      However calling this function frees up any resources used by the
+      underlying Augeas library immediately.
+
+      Do not use the handle after closing it. *)
+
+val get : t -> path -> value option
+  (** [get t path] returns the value at [path], or [None] if there
+      is no value. *)
+
+val exists : t -> path -> bool
+  (** [exists t path] returns true iff there is a value at [path]. *)
+
+val insert : t -> ?before:bool -> path -> string -> unit
+  (** [insert t ?before path label] inserts [label] as a sibling
+      of [path].  By default it is inserted after [path], unless
+      [~before:true] is specified. *)
+
+val rm : t -> path -> int
+  (** [rm t path] removes all nodes matching [path].
+
+      Returns the number of nodes removed (which may be 0). *)
+
+val matches : t -> path -> path list
+  (** [matches t path] returns a list of path expressions
+      of all nodes matching [path]. *)
+
+val count_matches : t -> path -> int
+  (** [count_matches t path] counts the number of nodes matching
+      [path] but does not return them (see {!matches}). *)
+
+val save : t -> unit
+  (** [save t] saves all pending changes to disk. *)
+
+val load : t -> unit
+  (** [load t] loads files into the tree. *)
diff --git a/daemon/chroot.ml b/daemon/chroot.ml
index 6b8b452a5..8dcdf9ab2 100644
--- a/daemon/chroot.ml
+++ b/daemon/chroot.ml
@@ -32,7 +32,7 @@ let create ?(name = "<unnamed>") chroot =
 
 let f t func arg =
   if verbose () then
-    eprintf "chroot: %s: running ‘%s’\n%!" t.chroot t.name;
+    eprintf "chroot: %s: running '%s'\n%!" t.chroot t.name;
 
   let rfd, wfd = pipe () in
 
diff --git a/daemon/daemon_utils_tests.ml b/daemon/daemon_utils_tests.ml
index 892509d89..b1f02de30 100644
--- a/daemon/daemon_utils_tests.ml
+++ b/daemon/daemon_utils_tests.ml
@@ -46,3 +46,18 @@ let () =
 let () =
   assert (proc_unmangle_path "\\040" = " ");
   assert (proc_unmangle_path "\\040\\040" = "  ")
+
+(* Test unix_canonical_path. *)
+let () =
+  assert (unix_canonical_path "/" = "/");
+  assert (unix_canonical_path "/usr" = "/usr");
+  assert (unix_canonical_path "/usr/" = "/usr");
+  assert (unix_canonical_path "/usr/local" = "/usr/local");
+  assert (unix_canonical_path "///" = "/");
+  assert (unix_canonical_path "///usr//local//" = "/usr/local");
+  assert (unix_canonical_path "/usr///" = "/usr")
+
+(* Test utf16le_to_utf8. *)
+let () =
+  assert (utf16le_to_utf8 "\x57\x00\x69\x00\x6e\x00\x64\x00\x6f\x00\x77\x00\x73\x00" = "Windows");
+  assert (utf16le_to_utf8 "\x57\x00\x69\x00\x6e\x00\x64\x00\x6f\x00\x77\x00\x73\x00\xae\x00" = "Windows\xc2\xae")
diff --git a/daemon/guestfsd.c b/daemon/guestfsd.c
index dbc8fef45..e8f797868 100644
--- a/daemon/guestfsd.c
+++ b/daemon/guestfsd.c
@@ -1176,6 +1176,8 @@ ocaml_exn_to_reply_with_error (const char *func, value exn)
     reply_with_error ("%s", String_val (Field (exn, 1)));
   else if (STREQ (exn_name, "Invalid_argument"))
     reply_with_error ("invalid argument: %s", String_val (Field (exn, 1)));
+  else if (STREQ (exn_name, "Augeas.Error"))
+    reply_with_error ("augeas error: %s", String_val (Field (exn, 1)));
   else
     reply_with_error ("internal error: %s: unhandled exception thrown: %s",
                       func, exn_name);
diff --git a/daemon/inspect.ml b/daemon/inspect.ml
new file mode 100644
index 000000000..91c0322c9
--- /dev/null
+++ b/daemon/inspect.ml
@@ -0,0 +1,398 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+open Printf
+
+open Std_utils
+
+open Utils
+open Mountable
+open Inspect_types
+
+let re_primary_partition = Str.regexp "^/dev/(h\\|s\\|v)d.[1234]$"
+
+let rec inspect_os () =
+  Mount.umount_all ();
+
+  (* Iterate over all detected filesystems.  Inspect each one in turn. *)
+  let fses = Listfs.list_filesystems () in
+
+  let fses =
+    filter_map (
+      fun (mountable, vfs_type) ->
+        Inspect_fs.check_for_filesystem_on mountable vfs_type
+  ) fses in
+  if verbose () then (
+    eprintf "inspect_os: fses:\n";
+    List.iter (fun fs -> eprintf "\t%s\n" (string_of_fs fs)) fses;
+    flush stderr
+  );
+
+  (* The OS inspection information for CoreOS are gathered by inspecting
+   * multiple filesystems. Gather all the inspected information in the
+   * inspect_fs struct of the root filesystem.
+   *)
+  let fses = collect_coreos_inspection_info fses in
+
+  (* Check if the same filesystem was listed twice as root in fses.
+   * This may happen for the *BSD root partition where an MBR partition
+   * is a shadow of the real root partition probably /dev/sda5
+   *)
+  let fses = check_for_duplicated_bsd_root fses in
+
+  (* For Linux guests with a separate /usr filesystem, merge some of the
+   * inspected information in that partition to the inspect_fs struct
+   * of the root filesystem.
+   *)
+  let fses = collect_linux_inspection_info fses in
+
+  (* Save what we found in a global variable. *)
+  Inspect_types.inspect_fses := fses;
+
+  (* At this point we have, in the handle, a list of all filesystems
+   * found and data about each one.  Now we assemble the list of
+   * filesystems which are root devices.
+   *
+   * Fall through to inspect_get_roots to do that.
+   *)
+  inspect_get_roots ()
+
+(* Traverse through the filesystem list and find out if it contains
+ * the [/] and [/usr] filesystems of a CoreOS image. If this is the
+ * case, sum up all the collected information on the root fs.
+ *)
+and collect_coreos_inspection_info fses =
+  (* Split the list into CoreOS root(s), CoreOS usr(s), and
+   * everything else.
+   *)
+  let rec loop roots usrs others = function
+    | [] -> roots, usrs, others
+    | ({ role = RoleRoot { distro = Some DISTRO_COREOS } } as r) :: rest ->
+       loop (r::roots) usrs others rest
+    | ({ role = RoleUsr { distro = Some DISTRO_COREOS } } as u) :: rest ->
+       loop roots (u::usrs) others rest
+    | o :: rest ->
+       loop roots usrs (o::others) rest
+  in
+  let roots, usrs, others = loop [] [] [] fses in
+
+  match roots with
+  (* If there are no CoreOS roots, then there's nothing to do. *)
+  | [] -> fses
+  (* If there are more than one CoreOS roots, we cannot inspect the guest. *)
+  | _::_::_ -> failwith "multiple CoreOS root filesystems found"
+  | [root] ->
+     match usrs with
+     (* If there are no CoreOS usr partitions, nothing to do. *)
+     | [] -> fses
+     | usrs ->
+        (* CoreOS is designed to contain 2 /usr partitions (USR-A, USR-B):
+         * https://coreos.com/docs/sdk-distributors/sdk/disk-partitions/
+         * One is active and one passive. During the initial boot, the
+         * passive partition is empty and it gets filled up when an
+         * update is performed.  Then, when the system reboots, the
+         * boot loader is instructed to boot from the passive partition.
+         * If both partitions are valid, we cannot determine which the
+         * active and which the passive is, unless we peep into the
+         * boot loader. As a workaround, we check the OS versions and
+         * pick the one with the higher version as active.
+         *)
+        let compare_versions u1 u2 =
+          let v1 =
+            match u1 with
+            | { role = RoleRoot { version = Some v } } -> v
+            | { role = RoleUsr { version = Some v } } -> v
+            | _ -> (0, 0) in
+          let v2 =
+            match u2 with
+            | { role = RoleRoot { version = Some v } } -> v
+            | { role = RoleUsr { version = Some v } } -> v
+            | _ -> (0, 0) in
+          compare v2 v1 (* reverse order *)
+        in
+        let usrs = List.sort compare_versions usrs in
+        let usr = List.hd usrs in
+
+        merge usr root;
+        root :: others
+
+(* On *BSD systems, sometimes [/dev/sda[1234]] is a shadow of the
+ * real root filesystem that is probably [/dev/sda5] (see:
+ * [http://www.freebsd.org/doc/handbook/disk-organization.html])
+ *)
+and check_for_duplicated_bsd_root fses =
+  try
+    let is_primary_partition = function
+      | { m_type = (MountablePath | MountableBtrfsVol _) } -> false
+      | { m_type = MountableDevice; m_device = d } ->
+         Str.string_match re_primary_partition d 0
+    in
+
+    (* Try to find a "BSD primary", if there is one. *)
+    let bsd_primary =
+      List.find (
+        function
+        | { fs_location = { mountable = mountable };
+            role = RoleRoot { os_type = Some t } } ->
+           (t = OS_TYPE_FREEBSD || t = OS_TYPE_NETBSD || t = OS_TYPE_OPENBSD)
+           && is_primary_partition mountable
+        | _ -> false
+      ) fses in
+
+    let bsd_primary_os_type =
+      match bsd_primary with
+      | { role = RoleRoot { os_type = Some t } } -> t
+      | _ -> assert false in
+
+    (* Try to find a shadow of the primary, and if it is found the
+     * primary is removed.
+     *)
+    let fses_without_bsd_primary = List.filter ((!=) bsd_primary) fses in
+    let shadow_exists =
+      List.exists (
+        function
+        | { role = RoleRoot { os_type = Some t } } ->
+           t = bsd_primary_os_type
+        | _ -> false
+      ) fses_without_bsd_primary in
+    if shadow_exists then fses_without_bsd_primary else fses
+  with
+    Not_found -> fses
+
+(* Traverse through the filesystem list and find out if it contains
+ * the [/] and [/usr] filesystems of a Linux image (but not CoreOS,
+ * for which there is a separate [collect_coreos_inspection_info]).
+ *
+ * If this is the case, sum up all the collected information on each
+ * root fs from the respective [/usr] filesystems.
+ *)
+and collect_linux_inspection_info fses =
+  List.map (
+    function
+    | { role = RoleRoot { distro = Some d } } as root ->
+       if d <> DISTRO_COREOS then
+         collect_linux_inspection_info_for fses root
+       else
+         root
+    | fs -> fs
+  ) fses
+
+(* Traverse through the filesystems and find the /usr filesystem for
+ * the specified C<root>: if found, merge its basic inspection details
+ * to the root when they were set (i.e. because the /usr had os-release
+ * or other ways to identify the OS).
+ *)
+and collect_linux_inspection_info_for fses root =
+  let root_distro, root_fstab =
+    match root with
+    | { role = RoleRoot { distro = Some d; fstab = f } } -> d, f
+    | _ -> assert false in
+
+  try
+    let usr =
+      List.find (
+        function
+        | { role = RoleUsr { distro = d } }
+             when d = Some root_distro || d = None -> true
+        | _ -> false
+      ) fses in
+
+    let usr_mountable = usr.fs_location.mountable in
+
+    (* This checks that [usr] is found in the fstab of the root
+     * filesystem.  If not, [Not_found] is thrown.
+     *)
+    ignore (
+      List.find (fun (mountable, _) -> usr_mountable = mountable) root_fstab
+    );
+
+    merge usr root;
+    root
+  with
+    Not_found -> root
+
+and inspect_get_roots () =
+  let fses = !Inspect_types.inspect_fses in
+
+  let roots =
+    filter_map (
+      fun fs -> try Some (root_of_fs fs) with Invalid_argument _ -> None
+    ) fses in
+  if verbose () then (
+    eprintf "inspect_get_roots: roots:\n";
+    List.iter (fun root -> eprintf "%s" (string_of_root root)) roots;
+    flush stderr
+  );
+
+  (* Only return the list of mountables, since subsequent calls will
+   * be used to retrieve the other information.
+   *)
+  List.map (fun { root_location = { mountable = m } } -> m) roots
+
+and root_of_fs =
+  function
+  | { fs_location = location; role = RoleRoot data } ->
+     { root_location = location; inspection_data = data }
+  | { role = (RoleUsr _ | RoleSwap | RoleOther) } ->
+     invalid_arg "root_of_fs"
+
+and inspect_get_mountpoints root_mountable =
+  let root = search_for_root root_mountable in
+  let fstab = root.inspection_data.fstab in
+
+  (* If no fstab information (Windows) return just the root. *)
+  if fstab = [] then
+    [ "/", root_mountable ]
+  else (
+    filter_map (
+      fun (mountable, mp) ->
+        if String.length mp > 0 && mp.[0] = '/' then
+          Some (mp, mountable)
+        else
+          None
+    ) fstab
+  )
+
+and inspect_get_filesystems root_mountable =
+  let root = search_for_root root_mountable in
+  let fstab = root.inspection_data.fstab in
+
+  (* If no fstab information (Windows) return just the root. *)
+  if fstab = [] then
+    [ root_mountable ]
+  else
+    List.map fst fstab
+
+and inspect_get_format root = "installed"
+
+and inspect_get_type root =
+  let root = search_for_root root in
+  match root.inspection_data.os_type with
+  | Some v -> string_of_os_type v
+  | None -> "unknown"
+
+and inspect_get_distro root =
+  let root = search_for_root root in
+  match root.inspection_data.distro with
+  | Some v -> string_of_distro v
+  | None -> "unknown"
+
+and inspect_get_package_format root =
+  let root = search_for_root root in
+  match root.inspection_data.package_format with
+  | Some v -> string_of_package_format v
+  | None -> "unknown"
+
+and inspect_get_package_management root =
+  let root = search_for_root root in
+  match root.inspection_data.package_management with
+  | Some v -> string_of_package_management v
+  | None -> "unknown"
+
+and inspect_get_product_name root =
+  let root = search_for_root root in
+  match root.inspection_data.product_name with
+  | Some v -> v
+  | None -> "unknown"
+
+and inspect_get_product_variant root =
+  let root = search_for_root root in
+  match root.inspection_data.product_variant with
+  | Some v -> v
+  | None -> "unknown"
+
+and inspect_get_major_version root =
+  let root = search_for_root root in
+  match root.inspection_data.version with
+  | Some (major, _) -> major
+  | None -> 0
+
+and inspect_get_minor_version root =
+  let root = search_for_root root in
+  match root.inspection_data.version with
+  | Some (_, minor) -> minor
+  | None -> 0
+
+and inspect_get_arch root =
+  let root = search_for_root root in
+  match root.inspection_data.arch with
+  | Some v -> v
+  | None -> "unknown"
+
+and inspect_get_hostname root =
+  let root = search_for_root root in
+  match root.inspection_data.hostname with
+  | Some v -> v
+  | None -> "unknown"
+
+and inspect_get_windows_systemroot root =
+  let root = search_for_root root in
+  match root.inspection_data.windows_systemroot with
+  | Some v -> v
+  | None ->
+     failwith "not a Windows guest, or systemroot could not be determined"
+
+and inspect_get_windows_system_hive root =
+  let root = search_for_root root in
+  match root.inspection_data.windows_system_hive with
+  | Some v -> v
+  | None ->
+     failwith "not a Windows guest, or system hive not found"
+
+and inspect_get_windows_software_hive root =
+  let root = search_for_root root in
+  match root.inspection_data.windows_software_hive with
+  | Some v -> v
+  | None ->
+     failwith "not a Windows guest, or software hive not found"
+
+and inspect_get_windows_current_control_set root =
+  let root = search_for_root root in
+  match root.inspection_data.windows_current_control_set with
+  | Some v -> v
+  | None ->
+     failwith "not a Windows guest, or CurrentControlSet could not be determined"
+
+and inspect_is_live root = false
+
+and inspect_is_netinst root = false
+
+and inspect_is_multipart root = false
+
+and inspect_get_drive_mappings root =
+  let root = search_for_root root in
+  root.inspection_data.drive_mappings
+
+and search_for_root root =
+  let fses = !Inspect_types.inspect_fses in
+  if fses = [] then
+    failwith "no inspection data: call guestfs_inspect_os first";
+
+  let root =
+    try
+      List.find (
+        function
+        | { fs_location = { mountable = m }; role = RoleRoot _ } -> root = m
+        | _ -> false
+      ) fses
+    with
+      Not_found ->
+        failwithf "%s: root device not found: only call this function with a root device previously returned by guestfs_inspect_os"
+                  (Mountable.to_string root) in
+
+  root_of_fs root
diff --git a/daemon/inspect.mli b/daemon/inspect.mli
new file mode 100644
index 000000000..29a1c1759
--- /dev/null
+++ b/daemon/inspect.mli
@@ -0,0 +1,41 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+val inspect_os : unit -> Mountable.t list
+val inspect_get_roots : unit -> Mountable.t list
+val inspect_get_mountpoints : Mountable.t -> (string * Mountable.t) list
+val inspect_get_filesystems : Mountable.t -> Mountable.t list
+val inspect_get_format : Mountable.t -> string
+val inspect_get_type : Mountable.t -> string
+val inspect_get_distro : Mountable.t -> string
+val inspect_get_package_format : Mountable.t -> string
+val inspect_get_package_management : Mountable.t -> string
+val inspect_get_product_name : Mountable.t -> string
+val inspect_get_product_variant : Mountable.t -> string
+val inspect_get_major_version : Mountable.t -> int
+val inspect_get_minor_version : Mountable.t -> int
+val inspect_get_arch : Mountable.t -> string
+val inspect_get_hostname : Mountable.t -> string
+val inspect_get_windows_systemroot : Mountable.t -> string
+val inspect_get_windows_software_hive : Mountable.t -> string
+val inspect_get_windows_system_hive : Mountable.t -> string
+val inspect_get_windows_current_control_set : Mountable.t -> string
+val inspect_get_drive_mappings : Mountable.t -> (string * string) list
+val inspect_is_live : Mountable.t -> bool
+val inspect_is_netinst : Mountable.t -> bool
+val inspect_is_multipart : Mountable.t -> bool
diff --git a/daemon/inspect_fs.ml b/daemon/inspect_fs.ml
new file mode 100644
index 000000000..3f57c93b5
--- /dev/null
+++ b/daemon/inspect_fs.ml
@@ -0,0 +1,366 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+open Printf
+
+open Std_utils
+
+open Mountable
+open Inspect_types
+open Inspect_utils
+
+let rec check_for_filesystem_on mountable vfs_type =
+  if verbose () then
+    eprintf "check_for_filesystem_on: %s (%s)\n%!"
+            (Mountable.to_string mountable) vfs_type;
+
+  let role =
+    let is_swap = vfs_type = "swap" in
+    if is_swap then
+      Some RoleSwap
+    else (
+      (* Try mounting the device.  Ignore errors if we can't do this. *)
+      let mounted =
+        if vfs_type = "ufs" then ( (* Hack for the *BSDs. *)
+          (* FreeBSD fs is a variant of ufs called ufs2 ... *)
+          try
+            Mount.mount_vfs (Some "ro,ufstype=ufs2") (Some "ufs")
+                            mountable "/";
+            true
+          with _ ->
+            (* while NetBSD and OpenBSD use another variant labeled 44bsd *)
+            try
+              Mount.mount_vfs (Some "ro,ufstype=44bsd") (Some "ufs")
+                              mountable "/";
+              true
+            with _ -> false
+        ) else (
+          try Mount.mount_ro mountable "/";
+              true
+          with _ -> false
+        ) in
+      if not mounted then None
+      else (
+        let role = check_filesystem mountable in
+        Mount.umount_all ();
+        role
+      )
+    ) in
+
+  match role with
+  | None -> None
+  | Some role ->
+     Some { fs_location = { mountable = mountable; vfs_type = vfs_type };
+            role = role }
+
+(* When this function is called, the filesystem is mounted on sysroot (). *)
+and check_filesystem mountable =
+  let role = ref `Other in
+  (* The following struct is mutated in place by callees: *)
+  let data = null_inspection_data in
+
+  let debug_matching what =
+    if verbose () then
+      eprintf "check_filesystem: %s matched %s\n%!"
+              (Mountable.to_string mountable) what
+  in
+
+  (* Grub /boot? *)
+  if Is.is_file "/grub/menu.lst" ||
+     Is.is_file "/grub/grub.conf" ||
+     Is.is_file "/grub2/grub.cfg" then (
+    debug_matching "Grub /boot";
+    ()
+  )
+  (* FreeBSD root? *)
+  else if Is.is_dir "/etc" &&
+          Is.is_dir "/bin" &&
+          Is.is_file "/etc/freebsd-update.conf" &&
+          Is.is_file "/etc/fstab" then (
+    debug_matching "FreeBSD root";
+    role := `Root;
+    Inspect_fs_unix.check_freebsd_root mountable data
+  )
+  (* NetBSD root? *)
+  else if Is.is_dir "/etc" &&
+          Is.is_dir "/bin" &&
+          Is.is_file "/netbsd" &&
+          Is.is_file "/etc/fstab" &&
+          Is.is_file "/etc/release" then (
+    debug_matching "NetBSD root";
+    role := `Root;
+    Inspect_fs_unix.check_netbsd_root mountable data;
+  )
+  (* OpenBSD root? *)
+  else if Is.is_dir "/etc" &&
+          Is.is_dir "/bin" &&
+          Is.is_file "/bsd" &&
+          Is.is_file "/etc/fstab" &&
+          Is.is_file "/etc/motd" then (
+    debug_matching "OpenBSD root";
+    role := `Root;
+    Inspect_fs_unix.check_openbsd_root mountable data;
+  )
+  (* Hurd root? *)
+  else if Is.is_file "/hurd/console" &&
+          Is.is_file "/hurd/hello" &&
+          Is.is_file "/hurd/null" then (
+    debug_matching "Hurd root";
+    role := `Root;
+    Inspect_fs_unix.check_hurd_root mountable data;
+  )
+  (* Minix root? *)
+  else if Is.is_dir "/etc" &&
+          Is.is_dir "/bin" &&
+          Is.is_file "/service/vm" &&
+          Is.is_file "/etc/fstab" &&
+          Is.is_file "/etc/version" then (
+    debug_matching "Minix root";
+    role := `Root;
+    Inspect_fs_unix.check_minix_root data;
+  )
+  (* Linux root? *)
+  else if Is.is_dir "/etc" &&
+          (Is.is_dir "/bin" ||
+           is_symlink_to "/bin" "usr/bin") &&
+          (Is.is_file "/etc/fstab" ||
+           Is.is_file "/etc/hosts") then (
+    debug_matching "Linux root";
+    role := `Root;
+    Inspect_fs_unix.check_linux_root mountable data;
+  )
+  (* CoreOS root? *)
+  else if Is.is_dir "/etc" &&
+          Is.is_dir "/root" &&
+          Is.is_dir "/home" &&
+          Is.is_dir "/usr" &&
+          Is.is_file "/etc/coreos/update.conf" then (
+    debug_matching "CoreOS root";
+    role := `Root;
+    Inspect_fs_unix.check_coreos_root mountable data;
+  )
+  (* Linux /usr/local? *)
+  else if Is.is_dir "/etc" &&
+          Is.is_dir "/bin" &&
+          Is.is_dir "/share" &&
+          not (Is.is_dir "/local") &&
+          not (Is.is_file "/etc/fstab") then (
+    debug_matching "Linux /usr/local";
+    ()
+  )
+  (* Linux /usr? *)
+  else if Is.is_dir "/etc" &&
+          Is.is_dir "/bin" &&
+          Is.is_dir "/share" &&
+          Is.is_dir "/local" &&
+          not (Is.is_file "/etc/fstab") then (
+    debug_matching "Linux /usr";
+    role := `Usr;
+    Inspect_fs_unix.check_linux_usr data;
+  )
+  (* CoreOS /usr? *)
+  else if Is.is_dir "/bin" &&
+          Is.is_dir "/share" &&
+          Is.is_dir "/local" &&
+          Is.is_dir "/share/coreos" then (
+    debug_matching "CoreOS /usr";
+    role := `Usr;
+    Inspect_fs_unix.check_coreos_usr mountable data;
+  )
+  (* Linux /var? *)
+  else if Is.is_dir "/log" &&
+          Is.is_dir "/run" &&
+          Is.is_dir "/spool" then (
+    debug_matching "Linux /var";
+    ()
+  )
+  (* Windows root? *)
+  else if Inspect_fs_windows.is_windows_systemroot () then (
+    debug_matching "Windows root";
+    role := `Root;
+    Inspect_fs_windows.check_windows_root data;
+  )
+  (* Windows volume with installed applications (but not root)? *)
+  else if is_dir_nocase "/System Volume Information" &&
+          is_dir_nocase "/Program Files" then (
+    debug_matching "Windows volume with installed applications";
+    ()
+  )
+  (* Windows volume (but not root)? *)
+  else if is_dir_nocase "/System Volume Information" then (
+    debug_matching "Windows volume without installed applications";
+    ()
+  )
+  (* FreeDOS? *)
+  else if is_dir_nocase "/FDOS" &&
+          is_file_nocase "/FDOS/FREEDOS.BSS" then (
+    debug_matching "FreeDOS";
+    role := `Root;
+    data.os_type <- Some OS_TYPE_DOS;
+    data.distro <- Some DISTRO_FREEDOS;
+    (* FreeDOS is a mix of 16 and 32 bit, but
+     * assume it requires a 32 bit i386 processor.
+     *)
+    data.arch <- Some "i386"
+  )
+  (* None of the above. *)
+  else (
+    debug_matching "no known OS partition"
+  );
+
+  (* The above code should have set [data.os_type] and [data.distro]
+   * fields, so we can now guess the package management system.
+   *)
+  data.package_format <- check_package_format data;
+  data.package_management <- check_package_management data;
+
+  match !role with
+  | `Root -> Some (RoleRoot data)
+  | `Usr -> Some (RoleUsr data)
+  | `Other -> Some RoleOther
+
+and is_symlink_to file wanted_target =
+  if not (Is.is_symlink file) then false
+  else Link.readlink file = wanted_target
+
+(* At the moment, package format and package management are just a
+ * simple function of the [distro] and [version[0]] fields, so these
+ * can never return an error.  We might be cleverer in future.
+ *)
+and check_package_format { distro = distro } =
+  match distro with
+  | None -> None
+  | Some DISTRO_FEDORA
+  | Some DISTRO_MEEGO
+  | Some DISTRO_REDHAT_BASED
+  | Some DISTRO_RHEL
+  | Some DISTRO_MAGEIA
+  | Some DISTRO_MANDRIVA
+  | Some DISTRO_SUSE_BASED
+  | Some DISTRO_OPENSUSE
+  | Some DISTRO_SLES
+  | Some DISTRO_CENTOS
+  | Some DISTRO_SCIENTIFIC_LINUX
+  | Some DISTRO_ORACLE_LINUX
+  | Some DISTRO_ALTLINUX ->
+     Some PACKAGE_FORMAT_RPM
+  | Some DISTRO_DEBIAN
+  | Some DISTRO_UBUNTU
+  | Some DISTRO_LINUX_MINT ->
+     Some PACKAGE_FORMAT_DEB
+  | Some DISTRO_ARCHLINUX ->
+     Some PACKAGE_FORMAT_PACMAN
+  | Some DISTRO_GENTOO ->
+     Some PACKAGE_FORMAT_EBUILD
+  | Some DISTRO_PARDUS ->
+     Some PACKAGE_FORMAT_PISI
+  | Some DISTRO_ALPINE_LINUX ->
+     Some PACKAGE_FORMAT_APK
+  | Some DISTRO_VOID_LINUX ->
+     Some PACKAGE_FORMAT_XBPS
+  | Some DISTRO_SLACKWARE
+  | Some DISTRO_TTYLINUX
+  | Some DISTRO_COREOS
+  | Some DISTRO_WINDOWS
+  | Some DISTRO_BUILDROOT
+  | Some DISTRO_CIRROS
+  | Some DISTRO_FREEDOS
+  | Some DISTRO_FREEBSD
+  | Some DISTRO_NETBSD
+  | Some DISTRO_OPENBSD
+  | Some DISTRO_FRUGALWARE
+  | Some DISTRO_PLD_LINUX ->
+     None
+
+and check_package_management { distro = distro; version = version } =
+  let major = match version with None -> 0 | Some (major, _) -> major in
+  match distro with
+  | None -> None
+
+  | Some DISTRO_MEEGO ->
+     Some PACKAGE_MANAGEMENT_YUM
+
+  | Some DISTRO_FEDORA ->
+    (* If Fedora >= 22 and dnf is installed, say "dnf". *)
+     if major >= 22 && Is.is_file ~followsymlinks:true "/usr/bin/dnf" then
+       Some PACKAGE_MANAGEMENT_DNF
+     else if major >= 1 then
+       Some PACKAGE_MANAGEMENT_YUM
+     else
+       (* Probably parsing the release file failed, see RHBZ#1332025. *)
+       None
+
+  | Some DISTRO_REDHAT_BASED
+  | Some DISTRO_RHEL
+  | Some DISTRO_CENTOS
+  | Some DISTRO_SCIENTIFIC_LINUX
+  | Some DISTRO_ORACLE_LINUX ->
+     if major >= 8 then
+       Some PACKAGE_MANAGEMENT_DNF
+     else if major >= 5 then
+       Some PACKAGE_MANAGEMENT_YUM
+     else if major >= 2 then
+       Some PACKAGE_MANAGEMENT_UP2DATE
+     else
+       (* Probably parsing the release file failed, see RHBZ#1332025. *)
+       None
+
+  | Some DISTRO_DEBIAN
+  | Some DISTRO_UBUNTU
+  | Some DISTRO_LINUX_MINT
+  | Some DISTRO_ALTLINUX ->
+     Some PACKAGE_MANAGEMENT_APT
+
+  | Some DISTRO_ARCHLINUX ->
+     Some PACKAGE_MANAGEMENT_PACMAN
+
+  | Some DISTRO_GENTOO ->
+     Some PACKAGE_MANAGEMENT_PORTAGE
+
+  | Some DISTRO_PARDUS ->
+     Some PACKAGE_MANAGEMENT_PISI
+
+  | Some DISTRO_MAGEIA
+  | Some DISTRO_MANDRIVA ->
+     Some PACKAGE_MANAGEMENT_URPMI
+
+  | Some DISTRO_SUSE_BASED
+  | Some DISTRO_OPENSUSE
+  | Some DISTRO_SLES ->
+     Some PACKAGE_MANAGEMENT_ZYPPER
+
+  | Some DISTRO_ALPINE_LINUX ->
+     Some PACKAGE_MANAGEMENT_APK
+
+  | Some DISTRO_VOID_LINUX ->
+     Some PACKAGE_MANAGEMENT_XBPS;
+
+  | Some DISTRO_SLACKWARE
+  | Some DISTRO_TTYLINUX
+  | Some DISTRO_COREOS
+  | Some DISTRO_WINDOWS
+  | Some DISTRO_BUILDROOT
+  | Some DISTRO_CIRROS
+  | Some DISTRO_FREEDOS
+  | Some DISTRO_FREEBSD
+  | Some DISTRO_NETBSD
+  | Some DISTRO_OPENBSD
+  | Some DISTRO_FRUGALWARE
+  | Some DISTRO_PLD_LINUX ->
+    None
+
diff --git a/daemon/inspect_fs.mli b/daemon/inspect_fs.mli
new file mode 100644
index 000000000..53ea01587
--- /dev/null
+++ b/daemon/inspect_fs.mli
@@ -0,0 +1,23 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+val check_for_filesystem_on : Mountable.t -> string ->
+                              Inspect_types.fs option
+(** [check_for_filesystem_on cmdline mountable vfs_type] inspects
+    [mountable] looking for a single mountpoint from an operating
+    system. *)
diff --git a/daemon/inspect_fs_unix.ml b/daemon/inspect_fs_unix.ml
new file mode 100644
index 000000000..0c171b76a
--- /dev/null
+++ b/daemon/inspect_fs_unix.ml
@@ -0,0 +1,789 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+open Printf
+
+open C_utils
+open Std_utils
+
+open Utils
+open Inspect_types
+open Inspect_utils
+
+let re_fedora = Str.regexp "Fedora release \\([0-9]+\\)"
+let re_rhel_old = Str.regexp "Red Hat.*release \\([0-9]+\\).*Update \\([0-9]+\\)"
+let re_rhel = Str.regexp "Red Hat.*release \\([0-9]+\\)\\.\\([0-9]+\\)"
+let re_rhel_no_minor = Str.regexp "Red Hat.*release \\([0-9]+\\)"
+let re_centos_old = Str.regexp "CentOS.*release \\([0-9]+\\).*Update \\([0-9]+\\)"
+let re_centos = Str.regexp "CentOS.*release \\([0-9]+\\)\\.\\([0-9]+\\)"
+let re_centos_no_minor = Str.regexp "CentOS.*release \\([0-9]+\\)"
+let re_scientific_linux_old =
+  Str.regexp "Scientific Linux.*release \\([0-9]+\\).*Update \\([0-9]+\\)"
+let re_scientific_linux =
+  Str.regexp "Scientific Linux.*release \\([0-9]+\\)\\.\\([0-9]+\\)"
+let re_scientific_linux_no_minor =
+  Str.regexp "Scientific Linux.*release \\([0-9]+\\)"
+let re_oracle_linux_old =
+  Str.regexp "Oracle Linux.*release \\([0-9]+\\).*Update \\([0-9]+\\)"
+let re_oracle_linux =
+  Str.regexp "Oracle Linux.*release \\([0-9]+\\)\\.\\([0-9]+\\)"
+let re_oracle_linux_no_minor = Str.regexp "Oracle Linux.*release \\([0-9]+\\)"
+let re_netbsd = Str.regexp "^NetBSD \\([0-9]+\\)\\.\\([0-9]+\\)"
+let re_opensuse = Str.regexp "^\\(openSUSE\\|SuSE Linux\\|SUSE LINUX\\) "
+let re_sles = Str.regexp "^SUSE \\(Linux\\|LINUX\\) Enterprise "
+let re_nld = Str.regexp "^Novell Linux Desktop "
+let re_sles_version = Str.regexp "^VERSION = \\([0-9]+\\)"
+let re_sles_patchlevel = Str.regexp "^PATCHLEVEL = \\([0-9]+\\)"
+let re_minix = Str.regexp "^\\([0-9]+\\)\\.\\([0-9]+\\)\\(\\.\\([0-9]+\\)\\)?"
+let re_openbsd = Str.regexp "^OpenBSD \\([0-9]+\\|\\?\\)\\.\\([0-9]+\\|\\?\\)"
+let re_frugalware = Str.regexp "Frugalware \\([0-9]+\\)\\.\\([0-9]+\\)"
+let re_pldlinux = Str.regexp "\\([0-9]+\\)\\.\\([0-9]+\\) PLD Linux"
+
+let arch_binaries =
+  [ "/bin/bash"; "/bin/ls"; "/bin/echo"; "/bin/rm"; "/bin/sh" ]
+
+(* Parse a os-release file.
+ *
+ * Only few fields are parsed, falling back to the usual detection if we
+ * cannot read all of them.
+ *
+ * For the format of os-release, see also:
+ * http://www.freedesktop.org/software/systemd/man/os-release.html
+ *)
+let rec parse_os_release release_file data =
+  let chroot = Chroot.create ~name:"parse_os_release" (Sysroot.sysroot ()) in
+  let lines =
+    Chroot.f chroot (
+      fun () ->
+        if not (is_small_file release_file) then (
+          eprintf "%s: not a regular file or too large\n" release_file;
+          None
+        )
+        else
+          Some (read_whole_file release_file)
+  ) () in
+
+  match lines with
+  | None -> false
+  | Some lines ->
+     let lines = String.nsplit "\n" lines in
+
+     List.iter (
+       fun line ->
+         let line = String.trim line in
+         if line = "" || line.[0] = '#' then
+           ()
+         else (
+           let key, value = String.split "=" line in
+           let value =
+             let n = String.length value in
+             if n >= 2 && value.[0] = '"' && value.[n-1] = '"' then
+               String.sub value 1 (n-2)
+             else
+               value in
+           if key = "ID" then (
+             let distro = distro_of_os_release_id value in
+             match distro with
+             | Some _ as distro -> data.distro <- distro
+             | None -> ()
+           )
+           else if key = "PRETTY_NAME" then
+             data.product_name <- Some value
+           else if key = "VERSION_ID" then
+             parse_version_from_major_minor value data
+         )
+       ) lines;
+
+     (* If we haven't got all the fields, exit right away. *)
+     if data.distro = None || data.product_name = None then
+       false
+     else (
+       (* os-release in Debian and CentOS does not provide the full
+        * version number (VERSION_ID), just the major part of it.  If
+        * we detect that situation then bail out and use the release
+        * files instead.
+        *)
+       match data with
+       | { distro = Some (DISTRO_DEBIAN|DISTRO_CENTOS);
+           version = Some (_, 0) } ->
+          false
+       | _ -> true
+     )
+
+(* ID="fedora" => Some DISTRO_FEDORA *)
+and distro_of_os_release_id = function
+  | "alpine" -> Some DISTRO_ALPINE_LINUX
+  | "altlinux" -> Some DISTRO_ALTLINUX
+  | "arch" -> Some DISTRO_ARCHLINUX
+  | "centos" -> Some DISTRO_CENTOS
+  | "coreos" -> Some DISTRO_COREOS
+  | "debian" -> Some DISTRO_DEBIAN
+  | "fedora" -> Some DISTRO_FEDORA
+  | "frugalware" -> Some DISTRO_FRUGALWARE
+  | "mageia" -> Some DISTRO_MAGEIA
+  | "opensuse" -> Some DISTRO_OPENSUSE
+  | "pld" -> Some DISTRO_PLD_LINUX
+  | "rhel" -> Some DISTRO_RHEL
+  | "sles" | "sled" -> Some DISTRO_SLES
+  | "ubuntu" -> Some DISTRO_UBUNTU
+  | "void" -> Some DISTRO_VOID_LINUX
+  | value ->
+     eprintf "/etc/os-release: unknown ID=%s\n" value;
+     None
+
+(* Ubuntu has /etc/lsb-release containing:
+ *   DISTRIB_ID=Ubuntu                                # Distro
+ *   DISTRIB_RELEASE=10.04                            # Version
+ *   DISTRIB_CODENAME=lucid
+ *   DISTRIB_DESCRIPTION="Ubuntu 10.04.1 LTS"         # Product name
+ *
+ * [Ubuntu-derived ...] Linux Mint was found to have this:
+ *   DISTRIB_ID=LinuxMint
+ *   DISTRIB_RELEASE=10
+ *   DISTRIB_CODENAME=julia
+ *   DISTRIB_DESCRIPTION="Linux Mint 10 Julia"
+ * Linux Mint also has /etc/linuxmint/info with more information,
+ * but we can use the LSB file.
+ *
+ * Mandriva has:
+ *   LSB_VERSION=lsb-4.0-amd64:lsb-4.0-noarch
+ *   DISTRIB_ID=MandrivaLinux
+ *   DISTRIB_RELEASE=2010.1
+ *   DISTRIB_CODENAME=Henry_Farman
+ *   DISTRIB_DESCRIPTION="Mandriva Linux 2010.1"
+ * Mandriva also has a normal release file called /etc/mandriva-release.
+ *
+ * CoreOS has a /etc/lsb-release link to /usr/share/coreos/lsb-release containing:
+ *   DISTRIB_ID=CoreOS
+ *   DISTRIB_RELEASE=647.0.0
+ *   DISTRIB_CODENAME="Red Dog"
+ *   DISTRIB_DESCRIPTION="CoreOS 647.0.0"
+ *)
+and parse_lsb_release release_file data =
+  let chroot = Chroot.create ~name:"parse_lsb_release" (Sysroot.sysroot ()) in
+  let lines =
+    Chroot.f chroot (
+      fun () ->
+        if not (is_small_file release_file) then (
+          eprintf "%s: not a regular file or too large\n" release_file;
+          None
+        )
+        else
+          Some (read_whole_file release_file)
+  ) () in
+
+  match lines with
+  | None -> false
+  | Some lines ->
+     let lines = String.nsplit "\n" lines in
+
+     (* Some distros (eg. RHEL 3) have a bare lsb-release file that might
+      * just contain the LSB_VERSION field and nothing else.  In that case
+      * we must bail out (return false).
+      *)
+     let ok = ref false in
+
+     List.iter (
+       fun line ->
+         if verbose () then
+           eprintf "parse_lsb_release: parsing: %s\n%!" line;
+
+         if data.distro = None && line = "DISTRIB_ID=Ubuntu" then (
+           ok := true;
+           data. distro <- Some DISTRO_UBUNTU
+         )
+         else if data.distro = None && line = "DISTRIB_ID=LinuxMint" then (
+           ok := true;
+           data.distro <- Some DISTRO_LINUX_MINT
+         )
+         else if data.distro = None && line = "DISTRIB_ID=\"Mageia\"" then (
+           ok := true;
+           data.distro <- Some DISTRO_MAGEIA
+         )
+         else if data.distro = None && line = "DISTRIB_ID=CoreOS" then (
+           ok := true;
+           data.distro <- Some DISTRO_COREOS
+         )
+         else if String.is_prefix line "DISTRIB_RELEASE=" then (
+           let line = String.sub line 16 (String.length line - 16) in
+           parse_version_from_major_minor line data
+         )
+         else if String.is_prefix line "DISTRIB_DESCRIPTION=\"" ||
+                 String.is_prefix line "DISTRIB_DESCRIPTION='" then (
+           ok := true;
+           let n = String.length line in
+           let product_name = String.sub line 21 (n-22) in
+           data.product_name <- Some product_name
+         )
+         else if String.is_prefix line "DISTRIB_DESCRIPTION=" then (
+           ok := true;
+           let n = String.length line in
+           let product_name = String.sub line 20 (n-20) in
+           data.product_name <- Some product_name
+         )
+     ) lines;
+
+     !ok
+
+and parse_suse_release release_file data =
+  let chroot = Chroot.create ~name:"parse_suse_release" (Sysroot.sysroot ()) in
+  let lines =
+    Chroot.f chroot (
+      fun () ->
+        if not (is_small_file release_file) then (
+          eprintf "%s: not a regular file or too large\n" release_file;
+          None
+        )
+        else
+          Some (read_whole_file release_file)
+  ) () in
+
+  match lines with
+  | None -> false
+  | Some lines ->
+     let lines = String.nsplit "\n" lines in
+
+     if lines = [] then false
+     else (
+       (* First line is dist release name. *)
+       let product_name = List.hd lines in
+       data.product_name <- Some product_name;
+
+       (* Match SLES first because openSuSE regex overlaps some SLES
+        * release strings.
+        *)
+       if Str.string_match re_sles product_name 0 ||
+          Str.string_match re_nld product_name 0 then (
+         (* Second line contains version string. *)
+         let major =
+           if List.length lines >= 2 then (
+             let line = List.nth lines 1 in
+             if Str.string_match re_sles_version line 0 then
+               Some (int_of_string (Str.matched_group 1 line))
+             else None
+           )
+           else None in
+
+         (* Third line contains service pack string. *)
+         let minor =
+           if List.length lines >= 3 then (
+             let line = List.nth lines 2 in
+             if Str.string_match re_sles_patchlevel line 0 then
+               Some (int_of_string (Str.matched_group 1 line))
+             else None
+           )
+           else None in
+
+         let version =
+           match major, minor with
+           | Some major, Some minor -> Some (major, minor)
+           | Some major, None -> Some (major, 0)
+           | None, Some _ | None, None -> None in
+
+         data.distro <- Some DISTRO_SLES;
+         data.version <- version
+       )
+       else if Str.string_match re_opensuse product_name 0 then (
+         (* Second line contains version string. *)
+         if List.length lines >= 2 then (
+           let line = List.nth lines 1 in
+           parse_version_from_major_minor line data
+         );
+
+         data.distro <- Some DISTRO_OPENSUSE
+       );
+
+       true
+     )
+
+(* Parse any generic /etc/x-release file.
+ *
+ * The optional regular expression which may match 0, 1 or 2
+ * substrings, which are used as the major and minor numbers.
+ *
+ * The fixed distro is always set, and the product name is
+ * set to the first line of the release file.
+ *)
+and parse_generic ?rex distro release_file data =
+  let chroot = Chroot.create ~name:"parse_generic" (Sysroot.sysroot ()) in
+  let product_name =
+    Chroot.f chroot (
+      fun () ->
+        if not (is_small_file release_file) then (
+          eprintf "%s: not a regular file or too large\n" release_file;
+          ""
+        )
+        else
+          read_first_line_from_file release_file
+  ) () in
+  if product_name = "" then
+    false
+  else (
+    if verbose () then
+      eprintf "parse_generic: product_name = %s\n%!" product_name;
+
+    data.product_name <- Some product_name;
+    data.distro <- Some distro;
+
+    (match rex with
+     | Some rex ->
+        (* If ~rex was supplied, then it must match the release file,
+         * else the parsing fails.
+         *)
+        if Str.string_match rex product_name 0 then (
+          (* Although it's not documented, matched_group raises
+           * Invalid_argument if called with an unknown group number.
+           *)
+          let major =
+            try Some (int_of_string (Str.matched_group 1 product_name))
+            with Not_found | Invalid_argument _ | Failure _ -> None in
+          let minor =
+            try Some (int_of_string (Str.matched_group 2 product_name))
+            with Not_found | Invalid_argument _ | Failure _ -> None in
+          (match major, minor with
+           | None, None -> ()
+           | None, Some _ -> ()
+           | Some major, None -> data.version <- Some (major, 0)
+           | Some major, Some minor -> data.version <- Some (major, minor)
+          );
+          true
+        )
+        else
+          false (* ... else the parsing fails. *)
+
+     | None ->
+        (* However if no ~rex was supplied, then we make a best
+         * effort attempt to parse a version number, but don't
+         * fail if one cannot be found.
+         *)
+        parse_version_from_major_minor product_name data;
+        true
+    )
+  )
+
+(* The list of release file tests that we run for Linux root filesystems.
+ * This is processed in order.
+ *
+ * For each test, first we check if the named release file exists.
+ * If so, the parse function is called.  If not, we go on to the next
+ * test.
+ *
+ * Each parse function should return true or false.  If a parse function
+ * returns true, then we finish, else if it returns false then we continue
+ * to the next test.
+ *)
+type parse_function = string -> inspection_data -> bool
+
+let linux_root_tests : (string * parse_function) list = [
+  (* systemd distros include /etc/os-release which is reasonably
+   * standardized.  This entry should be first.
+   *)
+  "/etc/os-release",     parse_os_release;
+  (* LSB is also a reasonable standard.  This entry should be second. *)
+  "/etc/lsb-release",    parse_lsb_release;
+
+  (* Now we enter the Wild West ... *)
+
+  (* RHEL-based distros include a [/etc/redhat-release] file, hence their
+   * checks need to be performed before the Red-Hat one.
+   *)
+  "/etc/oracle-release", parse_generic ~rex:re_oracle_linux_old
+                                       DISTRO_ORACLE_LINUX;
+  "/etc/oracle-release", parse_generic ~rex:re_oracle_linux
+                                       DISTRO_ORACLE_LINUX;
+  "/etc/oracle-release", parse_generic ~rex:re_oracle_linux_no_minor
+                                       DISTRO_ORACLE_LINUX;
+  "/etc/centos-release", parse_generic ~rex:re_centos_old
+                                       DISTRO_CENTOS;
+  "/etc/centos-release", parse_generic ~rex:re_centos
+                                       DISTRO_CENTOS;
+  "/etc/centos-release", parse_generic ~rex:re_centos_no_minor
+                                       DISTRO_CENTOS;
+  "/etc/altlinux-release", parse_generic DISTRO_ALTLINUX;
+  "/etc/redhat-release", parse_generic ~rex:re_fedora
+                                       DISTRO_FEDORA;
+  "/etc/redhat-release", parse_generic ~rex:re_rhel_old
+                                       DISTRO_RHEL;
+  "/etc/redhat-release", parse_generic ~rex:re_rhel
+                                       DISTRO_RHEL;
+  "/etc/redhat-release", parse_generic ~rex:re_rhel_no_minor
+                                       DISTRO_RHEL;
+  "/etc/redhat-release", parse_generic ~rex:re_centos_old
+                                       DISTRO_CENTOS;
+  "/etc/redhat-release", parse_generic ~rex:re_centos
+                                       DISTRO_CENTOS;
+  "/etc/redhat-release", parse_generic ~rex:re_centos_no_minor
+                                       DISTRO_CENTOS;
+  "/etc/redhat-release", parse_generic ~rex:re_scientific_linux_old
+                                       DISTRO_SCIENTIFIC_LINUX;
+  "/etc/redhat-release", parse_generic ~rex:re_scientific_linux
+                                       DISTRO_SCIENTIFIC_LINUX;
+  "/etc/redhat-release", parse_generic ~rex:re_scientific_linux_no_minor
+                                       DISTRO_SCIENTIFIC_LINUX;
+
+  (* If there's an /etc/redhat-release file, but nothing above
+   * matches, then it is a generic Red Hat-based distro.
+   *)
+  "/etc/redhat-release", parse_generic DISTRO_REDHAT_BASED;
+  "/etc/redhat-release",
+  (fun _ data -> data.distro <- Some DISTRO_REDHAT_BASED; true);
+
+  "/etc/debian_version", parse_generic DISTRO_DEBIAN;
+  "/etc/pardus-release", parse_generic DISTRO_PARDUS;
+
+  (* /etc/arch-release file is empty and I can't see a way to
+   * determine the actual release or product string.
+   *)
+  "/etc/arch-release",
+  (fun _ data -> data.distro <- Some DISTRO_ARCHLINUX; true);
+
+  "/etc/gentoo-release", parse_generic DISTRO_GENTOO;
+  "/etc/meego-release", parse_generic DISTRO_MEEGO;
+  "/etc/slackware-version", parse_generic DISTRO_SLACKWARE;
+  "/etc/ttylinux-target", parse_generic DISTRO_TTYLINUX;
+
+  "/etc/SuSE-release", parse_suse_release;
+  "/etc/SuSE-release",
+  (fun _ data -> data.distro <- Some DISTRO_SUSE_BASED; true);
+
+  "/etc/cirros/version", parse_generic DISTRO_CIRROS;
+  "/etc/br-version",
+  (fun release_file data ->
+    let distro =
+      if Is.is_file ~followsymlinks:true "/usr/share/cirros/logo" then
+        DISTRO_CIRROS
+      else
+        DISTRO_BUILDROOT in
+    (* /etc/br-version has the format YYYY.MM[-git/hg/svn release] *)
+    parse_generic distro release_file data);
+
+  "/etc/alpine-release", parse_generic DISTRO_ALPINE_LINUX;
+  "/etc/frugalware-release", parse_generic ~rex:re_frugalware
+                                           DISTRO_FRUGALWARE;
+  "/etc/pld-release", parse_generic ~rex:re_pldlinux
+                                    DISTRO_PLD_LINUX;
+]
+
+let rec check_linux_root mountable data =
+  let os_type = OS_TYPE_LINUX in
+  data.os_type <- Some os_type;
+
+  let rec loop = function
+    | (release_file, parse_fun) :: tests ->
+       if verbose () then
+         eprintf "check_linux_root: checking %s\n%!" release_file;
+       if Is.is_file ~followsymlinks:true release_file then (
+         if parse_fun release_file data then () (* true => finished *)
+         else loop tests
+       ) else loop tests
+    | [] -> ()
+  in
+  loop linux_root_tests;
+
+  data.arch <- check_architecture ();
+  data.fstab <-
+    Inspect_fs_unix_fstab.check_fstab ~mdadm_conf:true mountable os_type;
+  data.hostname <- check_hostname_linux ()
+
+and check_architecture () =
+  let rec loop = function
+    | [] -> None
+    | bin :: bins ->
+       (* Allow symlinks when checking the binaries:,so in case they are
+        * relative ones (which can be resolved within the same partition),
+        * then we can check the architecture of their target.
+        *)
+       if Is.is_file ~followsymlinks:true bin then (
+         try
+           let resolved = Realpath.realpath bin in
+           let arch = Filearch.file_architecture resolved in
+           Some arch
+         with exn ->
+           if verbose () then
+             eprintf "check_architecture: %s: %s\n%!" bin
+                     (Printexc.to_string exn);
+           loop bins
+       )
+       else
+         loop bins
+  in
+  loop arch_binaries
+
+and check_hostname_linux () =
+  (* Red Hat-derived would be in /etc/sysconfig/network or
+   * /etc/hostname (RHEL 7+, F18+).  Debian-derived in the file
+   * /etc/hostname.  Very old Debian and SUSE use /etc/HOSTNAME.
+   * It's best to just look for each of these files in turn, rather
+   * than try anything clever based on distro.
+   *)
+  let rec loop = function
+    | [] -> None
+    | filename :: rest ->
+       match check_hostname_from_file filename with
+       | Some hostname -> Some hostname
+       | None -> loop rest
+  in
+  let hostname = loop [ "/etc/HOSTNAME"; "/etc/hostname" ] in
+  match hostname with
+  | (Some _) as hostname -> hostname
+  | None ->
+     if Is.is_file "/etc/sysconfig/network" then
+       with_augeas ~name:"check_hostname_from_sysconfig_network"
+                   ["/etc/sysconfig/network"]
+                   check_hostname_from_sysconfig_network
+     else
+       None
+
+(* Parse the hostname where it is stored directly in a file. *)
+and check_hostname_from_file filename =
+  let chroot =
+    let name = sprintf "check_hostname_from_file: %s" filename in
+    Chroot.create ~name (Sysroot.sysroot ()) in
+
+  let hostname =
+    Chroot.f chroot (
+      fun () ->
+        if not (is_small_file filename) then (
+          eprintf "%s: not a regular file or too large\n" filename;
+          None
+        )
+        else
+          Some (read_first_line_from_file filename)
+    ) () in
+
+  match hostname with
+  | None | Some "" -> None
+  | (Some _) as hostname -> hostname
+
+(* Parse the hostname from /etc/sysconfig/network.  This must be
+ * called from the 'with_augeas' wrapper.  Note that F18+ and
+ * RHEL7+ use /etc/hostname just like Debian.
+ *)
+and check_hostname_from_sysconfig_network aug =
+  (* Errors here are not fatal (RHBZ#726739), since it could be
+   * just missing HOSTNAME field in the file.
+   *)
+  aug_get_noerrors aug "/files/etc/sysconfig/network/HOSTNAME"
+
+(* The currently mounted device looks like a Linux /usr. *)
+let check_linux_usr data =
+  data.os_type <- Some OS_TYPE_LINUX;
+
+  if Is.is_file "/lib/os-release" ~followsymlinks:true then
+    ignore (parse_os_release "/lib/os-release" data);
+
+  (match check_architecture () with
+   | None -> ()
+   | (Some _) as arch -> data.arch <- arch
+  )
+
+(* The currently mounted device is a CoreOS root. From this partition we can
+ * only determine the hostname. All immutable OS files are under a separate
+ * read-only /usr partition.
+ *)
+let check_coreos_root mountable data =
+  data.os_type <- Some OS_TYPE_LINUX;
+  data.distro <- Some DISTRO_COREOS;
+
+  (* Determine hostname. *)
+  data.hostname <- check_hostname_linux ();
+
+  (* CoreOS does not contain /etc/fstab to determine the mount points.
+   * Associate this filesystem with the "/" mount point.
+   *)
+  data.fstab <- [ mountable, "/" ]
+
+(* The currently mounted device looks like a CoreOS /usr. In CoreOS
+ * the read-only /usr contains the OS version. The /etc/os-release is a
+ * link to /usr/share/coreos/os-release.
+ *)
+let check_coreos_usr mountable data =
+  data.os_type <- Some OS_TYPE_LINUX;
+  data.distro <- Some DISTRO_COREOS;
+
+  if Is.is_file "/lib/os-release" ~followsymlinks:true then
+    ignore (parse_os_release "/lib/os-release" data)
+  else if Is.is_file "/share/coreos/lsb-release" ~followsymlinks:true then
+    ignore (parse_lsb_release "/share/coreos/lsb-release" data);
+
+  (* Determine the architecture. *)
+  (match check_architecture () with
+   | None -> ()
+   | (Some _) as arch -> data.arch <- arch
+  );
+
+  (* CoreOS does not contain /etc/fstab to determine the mount points.
+   * Associate this filesystem with the "/usr" mount point.
+   *)
+  data.fstab <- [ mountable, "/usr" ]
+
+let rec check_freebsd_root mountable data =
+  let os_type = OS_TYPE_FREEBSD and distro = DISTRO_FREEBSD in
+  data.os_type <- Some os_type;
+  data.distro <- Some distro;
+
+  (* FreeBSD has no authoritative version file.  The version number is
+   * in /etc/motd, which the system administrator might edit, but
+   * we'll use that anyway.
+   *)
+  if Is.is_file "/etc/motd" ~followsymlinks:true then
+    ignore (parse_generic distro "/etc/motd" data);
+
+  (* Determine the architecture. *)
+  data.arch <- check_architecture ();
+  (* We already know /etc/fstab exists because it's part of the test
+   * in the caller.
+   *)
+  data.fstab <- Inspect_fs_unix_fstab.check_fstab mountable os_type;
+  data.hostname <- check_hostname_freebsd ()
+
+(* Parse the hostname from /etc/rc.conf.  On FreeBSD and NetBSD
+ * this file contains comments, blank lines and:
+ *   hostname="freebsd8.example.com"
+ *   ifconfig_re0="DHCP"
+ *   keymap="uk.iso"
+ *   sshd_enable="YES"
+ *)
+and check_hostname_freebsd () =
+  let chroot = Chroot.create ~name:"check_hostname_freebsd"
+                             (Sysroot.sysroot ()) in
+  let filename = "/etc/rc.conf" in
+
+  try
+    let lines =
+      Chroot.f chroot (
+        fun () ->
+          if not (is_small_file filename) then (
+            eprintf "%s: not a regular file or too large\n" filename;
+            raise Not_found
+          )
+          else (
+            let lines = read_whole_file filename in
+            String.nsplit "\n" lines
+          )
+      ) () in
+    let rec loop = function
+      | [] ->
+         raise Not_found
+      | line :: _ when String.is_prefix line "hostname=\"" ||
+                       String.is_prefix line "hostname='" ->
+         let len = String.length line - 10 - 1 in
+         String.sub line 10 len
+      | line :: _ when String.is_prefix line "hostname=" ->
+         let len = String.length line - 9 in
+         String.sub line 9 len
+      | _ :: lines ->
+         loop lines
+    in
+    let hostname = loop lines in
+    Some hostname
+  with
+    Not_found -> None
+
+let rec check_netbsd_root mountable data =
+  let os_type = OS_TYPE_NETBSD and distro = DISTRO_NETBSD in
+  data.os_type <- Some os_type;
+  data.distro <- Some distro;
+
+  if Is.is_file "/etc/release" ~followsymlinks:true then
+    ignore (parse_generic ~rex:re_netbsd distro "/etc/release" data);
+
+  (* Determine the architecture. *)
+  data.arch <- check_architecture ();
+  (* We already know /etc/fstab exists because it's part of the test
+   * in the caller.
+   *)
+  data.fstab <- Inspect_fs_unix_fstab.check_fstab mountable os_type;
+  data.hostname <- check_hostname_freebsd ()
+
+and check_hostname_netbsd () = check_hostname_freebsd ()
+
+let rec check_openbsd_root mountable data =
+  let os_type = OS_TYPE_FREEBSD and distro = DISTRO_FREEBSD in
+  data.os_type <- Some os_type;
+  data.distro <- Some distro;
+
+  (* The first line of /etc/motd gets automatically updated at boot. *)
+  if Is.is_file "/etc/motd" ~followsymlinks:true then
+    ignore (parse_generic distro "/etc/motd" data);
+
+  (* Before the first boot, the first line will look like this:
+   *
+   * OpenBSD ?.? (UNKNOWN)
+   *
+   * The previous C code used to check for this case explicitly,
+   * but in this code, parse_generic should be unable to extract
+   * any version and so should return with [data.version = None].
+   *)
+
+  (* Determine the architecture. *)
+  data.arch <- check_architecture ();
+  (* We already know /etc/fstab exists because it's part of the test
+   * in the caller.
+   *)
+  data.fstab <- Inspect_fs_unix_fstab.check_fstab mountable os_type;
+  data.hostname <- check_hostname_freebsd ()
+
+and check_hostname_openbsd () =
+  check_hostname_from_file "/etc/myname"
+
+(* The currently mounted device may be a Hurd root.  Hurd has distros
+ * just like Linux.
+ *)
+let rec check_hurd_root mountable data =
+  let os_type = OS_TYPE_HURD in
+  data.os_type <- Some os_type;
+
+  if Is.is_file "/etc/debian_version" ~followsymlinks:true then (
+    let distro = DISTRO_DEBIAN in
+    ignore (parse_generic distro "/etc/debian_version" data)
+  );
+  (* Arch Hurd also exists, but inconveniently it doesn't have
+   * the normal /etc/arch-release file.  XXX
+   *)
+
+  (* Determine the architecture. *)
+  data.arch <- check_architecture ();
+  (* We already know /etc/fstab exists because it's part of the test
+   * in the caller.
+   *)
+  data.fstab <- Inspect_fs_unix_fstab.check_fstab mountable os_type;
+  data.hostname <- check_hostname_hurd ()
+
+and check_hostname_hurd () = check_hostname_linux ()
+
+let rec check_minix_root data =
+  let os_type = OS_TYPE_MINIX in
+  data.os_type <- Some os_type;
+
+  if Is.is_file "/etc/version" ~followsymlinks:true then (
+    ignore (parse_generic ~rex:re_minix DISTRO_MEEGO (* XXX unset below *)
+                          "/etc/version" data);
+    data.distro <- None
+  );
+
+  (* Determine the architecture. *)
+  data.arch <- check_architecture ();
+  (* TODO: enable fstab inspection once resolve_fstab_device
+   * implements the proper mapping from the Minix device names
+   * to the appliance names.
+   *)
+  data.hostname <- check_hostname_minix ()
+
+and check_hostname_minix () =
+  check_hostname_from_file "/etc/hostname.file"
diff --git a/daemon/inspect_fs_unix.mli b/daemon/inspect_fs_unix.mli
new file mode 100644
index 000000000..af58e5dcc
--- /dev/null
+++ b/daemon/inspect_fs_unix.mli
@@ -0,0 +1,44 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+val check_coreos_usr : Mountable.t -> Inspect_types.inspection_data -> unit
+(** Inspect the CoreOS [/usr] filesystem mounted on sysroot. *)
+
+val check_coreos_root : Mountable.t -> Inspect_types.inspection_data -> unit
+(** Inspect the CoreOS filesystem mounted on sysroot. *)
+
+val check_freebsd_root : Mountable.t -> Inspect_types.inspection_data -> unit
+(** Inspect the FreeBSD filesystem mounted on sysroot. *)
+
+val check_hurd_root : Mountable.t -> Inspect_types.inspection_data -> unit
+(** Inspect the Hurd filesystem mounted on sysroot. *)
+
+val check_linux_usr : Inspect_types.inspection_data -> unit
+(** Inspect the Linux [/usr] filesystem mounted on sysroot. *)
+
+val check_linux_root : Mountable.t -> Inspect_types.inspection_data -> unit
+(** Inspect the Linux filesystem mounted on sysroot. *)
+
+val check_minix_root : Inspect_types.inspection_data -> unit
+(** Inspect the Minix filesystem mounted on sysroot. *)
+
+val check_netbsd_root : Mountable.t -> Inspect_types.inspection_data -> unit
+(** Inspect the NetBSD filesystem mounted on sysroot. *)
+
+val check_openbsd_root : Mountable.t -> Inspect_types.inspection_data -> unit
+(** Inspect the OpenBSD filesystem mounted on sysroot. *)
diff --git a/daemon/inspect_fs_unix_fstab.ml b/daemon/inspect_fs_unix_fstab.ml
new file mode 100644
index 000000000..f103eb304
--- /dev/null
+++ b/daemon/inspect_fs_unix_fstab.ml
@@ -0,0 +1,537 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+open Printf
+
+open C_utils
+open Std_utils
+
+open Utils
+open Inspect_types
+open Inspect_utils
+
+let re_cciss = Str.regexp "^/dev/\\(cciss/c[0-9]+d[0-9]+\\)\\(p\\([0-9]+\\)\\)?$"
+let re_diskbyid = Str.regexp "^/dev/disk/by-id/.*-part\\([0-9]+\\)$"
+let re_freebsd_gpt = Str.regexp "^/dev/\\(ada{0,1}\\|vtbd\\)\\([0-9]+\\)p\\([0-9]+\\)$"
+let re_freebsd_mbr = Str.regexp "^/dev/\\(ada{0,1}\\|vtbd\\)\\([0-9]+\\)s\\([0-9]+\\)\\([a-z]\\)$"
+let re_hurd_dev = Str.regexp "^/dev/\\(h\\)d\\([0-9]+\\)s\\([0-9]+\\)$"
+let re_mdN = Str.regexp "^/dev/md[0-9]+$"
+let re_netbsd_dev = Str.regexp "^/dev/\\(l\\|s\\)d\\([0-9]\\)\\([a-z]\\)$"
+let re_openbsd_dev = Str.regexp "^/dev/\\(s\\|w\\)d\\([0-9]\\)\\([a-z]\\)$"
+let re_openbsd_duid = Str.regexp "^[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]\\.\\([a-z]\\)"
+let re_xdev = Str.regexp "^/dev/\\(h\\|s\\|v\\|xv\\)d\\([a-z]+\\)\\([0-9]*\\)$"
+
+let rec check_fstab ?(mdadm_conf = false) (root_mountable : Mountable.t)
+                    os_type =
+  let configfiles =
+    "/etc/fstab" :: if mdadm_conf then ["/etc/mdadm.conf"] else [] in
+
+  with_augeas ~name:"check_fstab_aug"
+              configfiles (check_fstab_aug mdadm_conf root_mountable os_type)
+
+and check_fstab_aug mdadm_conf root_mountable os_type aug =
+  (* Generate a map of MD device paths listed in /etc/mdadm.conf
+   * to MD device paths in the guestfs appliance.
+   *)
+  let md_map = if mdadm_conf then map_md_devices aug else StringMap.empty in
+
+  let path = "/files/etc/fstab/*[label() != '#comment']" in
+  let entries = aug_matches_noerrors aug path in
+  filter_map (check_fstab_entry md_map root_mountable os_type aug) entries
+
+and check_fstab_entry md_map root_mountable os_type aug entry =
+  if verbose () then
+    eprintf "check_fstab_entry: augeas path: %s\n%!" entry;
+
+  let is_bsd =
+    os_type = OS_TYPE_FREEBSD ||
+    os_type = OS_TYPE_NETBSD ||
+    os_type = OS_TYPE_OPENBSD in
+
+  let spec = aug_get_noerrors aug (entry ^ "/spec") in
+  let mp = aug_get_noerrors aug (entry ^ "/file") in
+  let vfstype = aug_get_noerrors aug (entry ^ "/vfstype") in
+
+  match spec, mp, vfstype with
+  | None, _, _ | Some _, None, _ | Some _, Some _, None -> None
+  | Some spec, Some mp, Some vfstype ->
+     if verbose () then
+       eprintf "check_fstab_entry: spec=%s mp=%s vfstype=%s\n%!"
+               spec mp vfstype;
+
+     (* Ignore /dev/fd (floppy disks) (RHBZ#642929) and CD-ROM drives.
+      *
+      * /dev/iso9660/FREEBSD_INSTALL can be found in FreeBSD's
+      * installation discs.
+      *)
+     if (String.is_prefix spec "/dev/fd" &&
+         String.length spec >= 8 && Char.isdigit spec.[7]) ||
+        (String.is_prefix spec "/dev/cd" &&
+         String.length spec >= 8 && Char.isdigit spec.[7]) ||
+        spec = "/dev/floppy" ||
+        spec = "/dev/cdrom" ||
+        String.is_prefix spec "/dev/iso9660/" then
+       None
+     else (
+       (* Canonicalize the path, so "///usr//local//" -> "/usr/local" *)
+       let mp = unix_canonical_path mp in
+
+       (* Ignore certain mountpoints. *)
+       if String.is_prefix mp "/dev/" ||
+          mp = "/dev" ||
+          String.is_prefix mp "/media/" ||
+          String.is_prefix mp "/proc/" ||
+          mp = "/proc" ||
+          String.is_prefix mp "/selinux/" ||
+          mp = "/selinux" ||
+          String.is_prefix mp "/sys/" ||
+          mp = "/sys" then
+         None
+       else (
+         let mountable =
+           (* Resolve UUID= and LABEL= to the actual device. *)
+           if String.is_prefix spec "UUID=" then (
+             let uuid = String.sub spec 5 (String.length spec - 5) in
+             let uuid = shell_unquote uuid in
+             Some (Mountable.of_device (Findfs.findfs_uuid uuid))
+           )
+           else if String.is_prefix spec "LABEL=" then (
+             let label = String.sub spec 6 (String.length spec - 6) in
+             let label = shell_unquote label in
+             Some (Mountable.of_device (Findfs.findfs_label label))
+           )
+           (* Resolve /dev/root to the current device.
+            * Do the same for the / partition of the *BSD
+            * systems, since the BSD -> Linux device
+            * translation is not straight forward.
+            *)
+           else if spec = "/dev/root" || (is_bsd && mp = "/") then
+             Some root_mountable
+           (* Resolve guest block device names. *)
+           else if String.is_prefix spec "/dev/" then
+             Some (resolve_fstab_device spec md_map os_type)
+           (* In OpenBSD's fstab you can specify partitions
+            * on a disk by appending a period and a partition
+            * letter to a Disklable Unique Identifier. The
+            * DUID is a 16 hex digit field found in the
+            * OpenBSD's altered BSD disklabel. For more info
+            * see here:
+            * http://www.openbsd.org/faq/faq14.html#intro
+            *)
+           else if Str.string_match re_openbsd_duid spec 0 then (
+             let part = Str.matched_group 1 spec in
+             (* We cannot peep into disklabels, we can only
+              * assume that this is the first disk.
+              *)
+             let device = sprintf "/dev/sd0%s" part in
+             Some (resolve_fstab_device device md_map os_type)
+           )
+           (* Ignore "/.swap" (Pardus) and pseudo-devices
+            * like "tmpfs".  If we haven't resolved the device
+            * successfully by this point, just ignore it.
+            *)
+           else
+             None in
+
+         match mountable with
+         | None -> None
+         | Some mountable ->
+            let mountable =
+              if vfstype = "btrfs" then
+                get_btrfs_mountable aug entry mountable
+              else mountable in
+
+            Some (mountable, mp)
+       )
+     )
+
+(* If an fstab entry corresponds to a btrfs filesystem, look for
+ * the 'subvol' option and if it is present then return a btrfs
+ * subvolume (else return the whole device).
+ *)
+and get_btrfs_mountable aug entry mountable =
+  let device =
+    match mountable with
+    | { Mountable.m_type = Mountable.MountableDevice; m_device = device } ->
+       Some device
+    | { Mountable.m_type =
+          (Mountable.MountablePath|Mountable.MountableBtrfsVol _) } ->
+       None in
+
+  match device with
+  | None -> mountable
+  | Some device ->
+     let opts = aug_matches_noerrors aug (entry ^ "/opt") in
+     let rec loop = function
+       | [] -> mountable        (* no subvol, return whole device *)
+       | opt :: opts ->
+          let optname = aug_get_noerrors aug opt in
+          match optname with
+          | None -> loop opts
+          | Some "subvol" ->
+             let subvol = aug_get_noerrors aug (opt ^ "/value") in
+             (match subvol with
+              | None -> loop opts
+              | Some subvol ->
+                 Mountable.of_btrfsvol device subvol
+             )
+          | Some _ ->
+             loop opts
+     in
+     loop opts
+
+(* Get a map of md device names in mdadm.conf to their device names
+ * in the appliance.
+ *)
+and map_md_devices aug =
+  (* Get a map of md device uuids to their device names in the appliance. *)
+  let uuid_map = map_app_md_devices () in
+
+  (* Nothing to do if there are no md devices. *)
+  if StringMap.is_empty uuid_map then StringMap.empty
+  else (
+    (* Get all arrays listed in mdadm.conf. *)
+    let entries = aug_matches_noerrors aug "/files/etc/mdadm.conf/array" in
+
+    (* Log a debug entry if we've got md devices but nothing in mdadm.conf. *)
+    if verbose () && entries = [] then
+      eprintf "warning: appliance has MD devices, but augeas returned no array matches in /etc/mdadm.conf\n%!";
+
+    List.fold_left (
+      fun md_map entry ->
+        try
+          (* Get device name and uuid for each array. *)
+          let dev = aug_get_noerrors aug (entry ^ "/devicename") in
+          let uuid = aug_get_noerrors aug (entry ^ "/uuid") in
+          let dev =
+            match dev with None -> raise Not_found | Some dev -> dev in
+          let uuid =
+            match uuid with None -> raise Not_found | Some uuid -> uuid in
+
+          (* Parse the uuid into an md_uuid structure so we can look
+           * it up in the uuid_map.
+           *)
+          let uuid = parse_md_uuid uuid in
+
+          let md = StringMap.find uuid uuid_map in
+
+          (* If there's a corresponding uuid in the appliance, create
+           * a new entry in the transitive map.
+           *)
+          StringMap.add dev md md_map
+        with
+          (* No Augeas devicename or uuid node found, or could not parse
+           * uuid, or uuid not present in the uuid_map.
+           *
+           * This is not fatal, just ignore the entry.
+           *)
+          Not_found | Invalid_argument _ -> md_map
+    ) StringMap.empty entries
+  )
+
+(* Create a mapping of uuids to appliance md device names. *)
+and map_app_md_devices () =
+  let mds = Md.list_md_devices () in
+  List.fold_left (
+    fun map md ->
+      let detail = Md.md_detail md in
+
+      try
+        (* Find the value of the "uuid" key. *)
+        let uuid = List.assoc "uuid" detail in
+        let uuid = parse_md_uuid uuid in
+        StringMap.add uuid md map
+      with
+        (* uuid not found, or could not be parsed - just ignore the entry *)
+        Not_found | Invalid_argument _ -> map
+  ) StringMap.empty mds
+
+(* Taken from parse_uuid in mdadm.
+ *
+ * Raises Invalid_argument if the input is not an MD UUID.
+ *)
+and parse_md_uuid uuid =
+  let len = String.length uuid in
+  let out = Bytes.create len in
+  let j = ref 0 in
+
+  for i = 0 to len-1 do
+    let c = uuid.[i] in
+    if Char.isxdigit c then (
+      Bytes.set out !j c;
+      incr j
+    )
+    else if c = ':' || c = '.' || c = ' ' || c = '-' then
+      ()
+    else
+      invalid_arg "parse_md_uuid: invalid character"
+  done;
+
+  if !j <> 32 then
+    invalid_arg "parse_md_uuid: invalid length";
+
+  Bytes.sub_string out 0 !j
+
+(* Resolve block device name to the libguestfs device name, eg.
+ * /dev/xvdb1 => /dev/vdb1; and /dev/mapper/VG-LV => /dev/VG/LV.  This
+ * assumes that disks were added in the same order as they appear to
+ * the real VM, which is a reasonable assumption to make.  Return
+ * anything we don't recognize unchanged.
+ *)
+and resolve_fstab_device spec md_map os_type =
+  (* In any case where we didn't match a device pattern or there was
+   * another problem, return this default mountable derived from [spec].
+   *)
+  let default = Mountable.of_device spec in
+
+  let debug_matching what =
+    if verbose () then
+      eprintf "resolve_fstab_device: %s matched %s\n%!" spec what
+  in
+
+  if String.is_prefix spec "/dev/mapper" then (
+    debug_matching "/dev/mapper";
+    (* LVM2 does some strange munging on /dev/mapper paths for VGs and
+     * LVs which contain '-' character:
+     *
+     * ><fs> lvcreate LV--test VG--test 32
+     * ><fs> debug ls /dev/mapper
+     * VG----test-LV----test
+     *
+     * This makes it impossible to reverse those paths directly, so
+     * we have implemented lvm_canonical_lv_name in the daemon.
+     *)
+    try
+      match Lvm.lv_canonical spec with
+      | None -> Mountable.of_device spec
+      | Some device -> Mountable.of_device device
+    with
+    (* Ignore devices that don't exist. (RHBZ#811872) *)
+    | Unix.Unix_error (Unix.ENOENT, _, _) -> default
+  )
+
+  else if Str.string_match re_xdev spec 0 then (
+    debug_matching "xdev";
+    let typ = Str.matched_group 1 spec
+    and disk = Str.matched_group 2 spec
+    and part = int_of_string (Str.matched_group 3 spec) in
+    resolve_xdev typ disk part default
+  )
+
+  else if Str.string_match re_cciss spec 0 then (
+    debug_matching "cciss";
+    let disk = Str.matched_group 1 spec
+    (* group 2 = optional p<NN>, group 3 = <NN> *)
+    and part =
+      try Some (int_of_string (Str.matched_group 3 spec))
+      with Not_found | Invalid_argument _ -> None in
+    resolve_cciss disk part default
+  )
+
+  else if Str.string_match re_mdN spec 0 then (
+    debug_matching "md<N>";
+    try
+      Mountable.of_device (StringMap.find spec md_map)
+    with
+    | Not_found -> default
+  )
+
+  else if Str.string_match re_diskbyid spec 0 then (
+    debug_matching "diskbyid";
+    let part = int_of_string (Str.matched_group 1 spec) in
+    resolve_diskbyid part default
+  )
+
+  else if Str.string_match re_freebsd_gpt spec 0 then (
+    debug_matching "FreeBSD GPT";
+    (* group 1 (type) is not used *)
+    let disk = int_of_string (Str.matched_group 2 spec)
+    and part = int_of_string (Str.matched_group 3 spec) in
+
+    (* If the FreeBSD disk contains GPT partitions, the translation to Linux
+     * device names is straight forward.  Partitions on a virtio disk are
+     * prefixed with [vtbd].  IDE hard drives used to be prefixed with [ad]
+     * and now prefixed with [ada].
+     *)
+    if disk >= 0 && disk <= 26 && part >= 0 && part <= 128 then (
+      let dev = sprintf "/dev/sd%c%d"
+                        (Char.chr (disk + Char.code 'a')) part in
+      Mountable.of_device dev
+    )
+    else default
+  )
+
+  else if Str.string_match re_freebsd_mbr spec 0 then (
+    debug_matching "FreeBSD MBR";
+    (* group 1 (type) is not used *)
+    let disk = int_of_string (Str.matched_group 2 spec)
+    and slice = int_of_string (Str.matched_group 3 spec)
+    (* partition number counting from 0: *)
+    and part = Char.code (Str.matched_group 4 spec).[0] - Char.code 'a' in
+
+    (* FreeBSD MBR disks are organized quite differently.  See:
+     * http://www.freebsd.org/doc/handbook/disk-organization.html
+     * FreeBSD "partitions" are exposed as quasi-extended partitions
+     * numbered from 5 in Linux.  I have no idea what happens when you
+     * have multiple "slices" (the FreeBSD term for MBR partitions).
+     *)
+
+    (* Partition 'c' has the size of the enclosing slice.
+     * Not mapped under Linux.
+     *)
+    let part = if part > 2 then part - 1 else part in
+
+    if disk >= 0 && disk <= 26 &&
+       slice > 0 && slice <= 1 (* > 4 .. see comment above *) &&
+       part >= 0 && part < 25 then (
+      let dev = sprintf "/dev/sd%c%d"
+                        (Char.chr (disk + Char.code 'a')) (part + 5) in
+      Mountable.of_device dev
+    )
+    else default
+  )
+
+  else if os_type = OS_TYPE_NETBSD &&
+            Str.string_match re_netbsd_dev spec 0 then (
+    debug_matching "NetBSD";
+    (* group 1 (type) is not used *)
+    let disk = int_of_string (Str.matched_group 2 spec)
+    (* partition number counting from 0: *)
+    and part = Char.code (Str.matched_group 3 spec).[0] - Char.code 'a' in
+
+    (* Partition 'c' is the disklabel partition and 'd' the hard disk itself.
+     * Not mapped under Linux.
+     *)
+    let part = if part > 3 then part - 2 else part in
+
+    if disk >= 0 && part >= 0 && part < 24 then (
+      let dev = sprintf "/dev/sd%c%d"
+                        (Char.chr (disk + Char.code 'a')) (part + 5) in
+      Mountable.of_device dev
+    )
+    else default
+  )
+
+  else if os_type = OS_TYPE_OPENBSD &&
+            Str.string_match re_openbsd_dev spec 0 then (
+    debug_matching "OpenBSD";
+    (* group 1 (type) is not used *)
+    let disk = int_of_string (Str.matched_group 2 spec)
+    (* partition number counting from 0: *)
+    and part = Char.code (Str.matched_group 3 spec).[0] - Char.code 'a' in
+
+    (* Partition 'c' is the hard disk itself. Not mapped under Linux. *)
+    let part = if part > 2 then part - 1 else part in
+
+    (* In OpenBSD MAXPARTITIONS is defined to 16 for all architectures. *)
+    if disk >= 0 && part >= 0 && part < 15 then (
+      let dev = sprintf "/dev/sd%c%d"
+                        (Char.chr (disk + Char.code 'a')) (part + 5) in
+      Mountable.of_device dev
+    )
+    else default
+  )
+
+  else if Str.string_match re_hurd_dev spec 0 then (
+    debug_matching "Hurd";
+    let typ = Str.matched_group 1 spec
+    and disk = int_of_string (Str.matched_group 2 spec)
+    and part = int_of_string (Str.matched_group 3 spec) in
+
+    (* Hurd disk devices are like /dev/hdNsM, where hdN is the
+     * N-th disk and M is the M-th partition on that disk.
+     * Turn the disk number into a letter-based identifier, so
+     * we can resolve it easily.
+     *)
+    let disk = sprintf "%c" (Char.chr (disk + Char.code 'a')) in
+
+    resolve_xdev typ disk part default
+  )
+
+  else (
+    debug_matching "no known device scheme";
+    default
+  )
+
+(* type: (h|s|v|xv)
+ * disk: [a-z]+
+ * part: \d*
+ *)
+and resolve_xdev typ disk part default =
+  let devices = Devsparts.list_devices () in
+  let devices = Array.of_list devices in
+
+  (* XXX Check any hints we were passed for a non-heuristic mapping.
+   * The C code used hints here to map device names as known by
+   * the library user (eg. from metadata) to libguestfs devices here.
+   * However none of the libguestfs tools ever used this feature.
+   * Nevertheless we should reimplement it at some point because
+   * outside callers might require it, and it's a good idea in general.
+   *)
+
+  (* Guess the appliance device name if we didn't find a matching hint. *)
+  let i = drive_index disk in
+  if i >= 0 && i < Array.length devices then (
+    let dev = Array.get devices i in
+    let dev = dev ^ string_of_int part in
+    if is_partition dev then
+      Mountable.of_device dev
+    else
+      default
+  )
+  else
+    default
+
+(* disk: (cciss/c\d+d\d+)
+ * part: (\d+)?
+ *)
+and resolve_cciss disk part default =
+  (* XXX Check any hints we were passed for a non-heuristic mapping.
+   * The C code used hints here to map device names as known by
+   * the library user (eg. from metadata) to libguestfs devices here.
+   * However none of the libguestfs tools ever used this feature.
+   * Nevertheless we should reimplement it at some point because
+   * outside callers might require it, and it's a good idea in general.
+   *)
+
+  (* We don't try to guess mappings for cciss devices. *)
+  default
+
+(* For /dev/disk/by-id there is a limit to what we can do because
+ * original SCSI ID information has likely been lost.  This
+ * heuristic will only work for guests that have a single block
+ * device.
+ *
+ * So the main task here is to make sure the assumptions above are
+ * true.
+ *
+ * XXX Use hints from virt-p2v if available.
+ * See also: https://bugzilla.redhat.com/show_bug.cgi?id=836573#c3
+ *)
+and resolve_diskbyid part default =
+  let nr_devices = Devsparts.nr_devices () in
+
+  (* If #devices isn't 1, give up trying to translate this fstab entry. *)
+  if nr_devices <> 1 then
+    default
+  else (
+    (* Make the partition name and check it exists. *)
+    let dev = sprintf "/dev/sda%d" part in
+    if is_partition dev then Mountable.of_device dev
+    else default
+  )
diff --git a/daemon/inspect_fs_unix_fstab.mli b/daemon/inspect_fs_unix_fstab.mli
new file mode 100644
index 000000000..3ce3aef05
--- /dev/null
+++ b/daemon/inspect_fs_unix_fstab.mli
@@ -0,0 +1,34 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+val check_fstab : ?mdadm_conf:bool -> Mountable.t -> Inspect_types.os_type ->
+                  (Mountable.t * string) list
+(** [check_fstab] examines the [/etc/fstab] file of a mounted root
+    filesystem, returning the list of devices and their mount points.
+    Various devices (like CD-ROMs) are ignored in the process, and
+    this function also knows how to map (eg) BSD device names into
+    Linux/libguestfs device names.
+
+    [mdadm_conf] is true if you want to check [/etc/mdadm.conf] as well.
+
+    [root_mountable] is the [Mountable.t] of the root filesystem.  (Note
+    that the root filesystem must be mounted on sysroot before this
+    function is called.)
+
+    [os_type] is the presumed operating system type of this root, and
+    is used to make some adjustments to fstab parsing. *)
diff --git a/daemon/inspect_fs_windows.ml b/daemon/inspect_fs_windows.ml
new file mode 100644
index 000000000..313babd0f
--- /dev/null
+++ b/daemon/inspect_fs_windows.ml
@@ -0,0 +1,493 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+open Printf
+
+open Std_utils
+
+open Utils
+open Inspect_types
+open Inspect_utils
+
+(* Check a predefined list of common windows system root locations. *)
+let systemroot_paths =
+  [ "/windows"; "/winnt"; "/win32"; "/win"; "/reactos" ]
+
+let re_boot_ini_os =
+  Str.regexp "^\\(multi\\|scsi\\)(\\([0-9]+\\))disk(\\([0-9]+\\))rdisk(\\([0-9]+\\))partition(\\([0-9]+\\))\\([^=]+\\)="
+
+let rec check_windows_root data =
+  let systemroot =
+    match get_windows_systemroot () with
+    | None -> assert false (* Should never happen - see caller. *)
+    | Some systemroot -> systemroot in
+
+  data.os_type <- Some OS_TYPE_WINDOWS;
+  data.distro <- Some DISTRO_WINDOWS;
+  data.windows_systemroot <- Some systemroot;
+  data.arch <- Some (check_windows_arch systemroot);
+
+  (* Load further fields from the Windows registry. *)
+  check_windows_registry systemroot data
+
+and is_windows_systemroot () =
+  get_windows_systemroot () <> None
+
+and get_windows_systemroot () =
+  let rec loop = function
+    | [] -> None
+    | path :: paths ->
+       let path = case_sensitive_path_silently path in
+       match path with
+       | None -> loop paths
+       | Some path ->
+          if is_systemroot path then Some path
+          else loop paths
+  in
+  let systemroot = loop systemroot_paths in
+
+  let systemroot =
+    match systemroot with
+    | Some systemroot -> Some systemroot
+    | None ->
+       (* If the fs contains boot.ini, check it for non-standard
+        * systemroot locations.
+        *)
+       let boot_ini_path = case_sensitive_path_silently "/boot.ini" in
+       match boot_ini_path with
+       | None -> None
+       | Some boot_ini_path ->
+          get_windows_systemroot_from_boot_ini boot_ini_path in
+
+  match systemroot with
+  | None -> None
+  | Some systemroot ->
+     if verbose () then
+       eprintf "get_windows_systemroot: windows %%SYSTEMROOT%% = %s\n%!"
+               systemroot;
+     Some systemroot
+
+and get_windows_systemroot_from_boot_ini boot_ini_path =
+  let chroot =
+    Chroot.create ~name:"get_windows_systemroot_from_boot_ini"
+                  (Sysroot.sysroot ()) in
+  let lines =
+    Chroot.f chroot (
+      fun () ->
+        if not (is_small_file boot_ini_path) then (
+          eprintf "%s: not a regular file or too large\n" boot_ini_path;
+          None
+        )
+        else
+          Some (read_whole_file boot_ini_path)
+    ) () in
+  match lines with
+  | None -> None
+  | Some lines ->
+     let lines = String.nsplit "\n" lines in
+
+     (* Find:
+      *   [operating systems]
+      * followed by multiple lines starting with "multi" or "scsi".
+      *)
+     let rec loop = function
+       | [] -> None
+       | str :: rest when String.is_prefix str "[operating systems]" ->
+          let rec loop2 = function
+            | [] -> []
+            | str :: rest when String.is_prefix str "multi(" ||
+                               String.is_prefix str "scsi(" ->
+               str :: loop2 rest
+            | _ -> []
+          in
+          Some (loop2 rest)
+       | _ :: rest -> loop rest
+     in
+     match loop lines with
+     | None -> None
+     | Some oses ->
+        (* Rewrite multi|scsi lines, removing any which we cannot parse. *)
+        let oses =
+          filter_map (
+            fun line ->
+              if Str.string_match re_boot_ini_os line 0 then (
+                let ctrlr_type = Str.matched_group 1 line
+                and ctrlr = int_of_string (Str.matched_group 2 line)
+                and disk = int_of_string (Str.matched_group 3 line)
+                and rdisk = int_of_string (Str.matched_group 4 line)
+                and part = int_of_string (Str.matched_group 5 line)
+                and path = Str.matched_group 6 line in
+
+                (* Swap backslashes for forward slashes in the
+                 * system root path.
+                 *)
+                let path = String.replace_char path '\\' '/' in
+
+                Some (ctrlr_type, ctrlr, disk, rdisk, part, path)
+              )
+              else None
+          ) oses in
+
+        (* The Windows system root may be on any disk. However, there
+         * are currently (at least) 2 practical problems preventing us
+         * from locating it on another disk:
+         *
+         * 1. We don't have enough metadata about the disks we were
+         * given to know if what controller they were on and what
+         * index they had.
+         *
+         * 2. The way inspection of filesystems currently works, we
+         * can't mark another filesystem, which we may have already
+         * inspected, to be inspected for a specific Windows system
+         * root.
+         *
+         * Solving 1 properly would require a new API at a minimum. We
+         * might be able to fudge something practical without this,
+         * though, e.g. by looking at the <partition>th partition of
+         * every disk for the specific windows root.
+         *
+         * Solving 2 would probably require a significant refactoring
+         * of the way filesystems are inspected. We should probably do
+         * this some time.
+         *
+         * For the moment, we ignore all partition information and
+         * assume the system root is on the current partition. In
+         * practice, this will normally be correct.
+         *)
+
+        let rec loop = function
+          | [] -> None
+          | (_, _, _, _, _, path) :: rest ->
+             if is_systemroot path then Some path
+             else loop rest
+        in
+        loop oses
+
+(* Try to find Windows systemroot using some common locations.
+ *
+ * Notes:
+ *
+ * (1) We check for some directories inside to see if it is a real
+ * systemroot, and not just a directory that happens to have the same
+ * name.
+ *
+ * (2) If a Windows guest has multiple disks and applications are
+ * installed on those other disks, then those other disks will contain
+ * "/Program Files" and "/System Volume Information".  Those would
+ * *not* be Windows root disks.  (RHBZ#674130)
+ *)
+and is_systemroot systemroot =
+  is_dir_nocase (systemroot ^ "/system32") &&
+  is_dir_nocase (systemroot ^ "/system32/config") &&
+  is_file_nocase (systemroot ^ "/system32/cmd.exe")
+
+(* Return the architecture of the guest from cmd.exe. *)
+and check_windows_arch systemroot =
+  let cmd_exe = sprintf "%s/system32/cmd.exe" systemroot in
+
+  (* Should exist because of previous check above in is_systemroot. *)
+  let cmd_exe = Realpath.case_sensitive_path cmd_exe in
+
+  Filearch.file_architecture cmd_exe
+
+(* Read further fields from the Windows registry. *)
+and check_windows_registry systemroot data =
+  (* We know (from is_systemroot) that the config directory exists. *)
+  let software_hive = sprintf "%s/system32/config/software" systemroot in
+  let software_hive = Realpath.case_sensitive_path software_hive in
+  let software_hive =
+    if Is.is_file software_hive then Some software_hive else None in
+  data.windows_software_hive <- software_hive;
+
+  let system_hive = sprintf "%s/system32/config/system" systemroot in
+  let system_hive = Realpath.case_sensitive_path system_hive in
+  let system_hive =
+    if Is.is_file system_hive then Some system_hive else None in
+  data.windows_system_hive <- system_hive;
+
+  match software_hive, system_hive with
+  | None, _ | Some _, None -> ()
+  | Some software_hive, Some system_hive ->
+     (* Check software hive. *)
+     check_windows_software_registry software_hive data;
+
+     (* Check system hive. *)
+     check_windows_system_registry system_hive data
+
+(* At the moment, pull just the ProductName and version numbers from
+ * the registry.  In future there is a case for making many more
+ * registry fields available to callers.
+ *)
+and check_windows_software_registry software_hive data =
+  with_hive (Sysroot.sysroot () // software_hive) (
+    fun h root ->
+      try
+        let path = [ "Microsoft"; "Windows NT"; "CurrentVersion" ] in
+        let node = get_node h root path in
+        let values = Hivex.node_values h node in
+        let values = Array.to_list values in
+        (* Convert to a list of (key, value) to make the following easier. *)
+        let values = List.map (fun v -> Hivex.value_key h v, v) values in
+
+        (* Look for ProductName key. *)
+        (try
+           let v = List.assoc "ProductName" values in
+           data.product_name <- Some (hivex_value_as_utf8 h v)
+         with
+           Not_found -> ()
+        );
+
+        (* Version is complicated.  Use CurrentMajorVersionNumber and
+         * CurrentMinorVersionNumber if present.  If they are not
+         * found, fall back on CurrentVersion.
+         *)
+        (try
+           let major_v = List.assoc "CurrentMajorVersionNumber" values
+           and minor_v = List.assoc "CurrentMinorVersionNumber" values in
+           let major = Int32.to_int (Hivex.value_dword h major_v)
+           and minor = Int32.to_int (Hivex.value_dword h minor_v) in
+           data.version <- Some (major, minor)
+         with
+           Not_found ->
+           let v = List.assoc "CurrentVersion" values in
+           let v = hivex_value_as_utf8 h v in
+           parse_version_from_major_minor v data
+        );
+
+        (* InstallationType (product_variant). *)
+        (try
+           let v = List.assoc "InstallationType" values in
+           data.product_variant <- Some (hivex_value_as_utf8 h v)
+         with
+           Not_found -> ()
+        );
+      with
+      | Not_found ->
+         if verbose () then
+           eprintf "check_windows_software_registry: cannot locate HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\n%!"
+  ) (* with_hive *)
+
+and check_windows_system_registry system_hive data =
+  with_hive (Sysroot.sysroot () // system_hive) (
+    fun h root ->
+      get_drive_mappings h root data;
+
+      let current_control_set = get_current_control_set h root in
+      data.windows_current_control_set <- current_control_set;
+
+      match current_control_set with
+      | None -> ()
+      | Some current_control_set ->
+         let hostname = get_hostname h root current_control_set in
+         data.hostname <- hostname
+  ) (* with_hive *)
+
+(* Get the CurrentControlSet. *)
+and get_current_control_set h root =
+  try
+    let path = [ "Select" ] in
+    let node = get_node h root path in
+    let current_v = Hivex.node_get_value h node "Current" in
+    let current_control_set =
+      sprintf "ControlSet%03ld" (Hivex.value_dword h current_v) in
+    Some current_control_set
+  with
+  | Not_found ->
+     if verbose () then
+       eprintf "check_windows_system_registry: cannot locate HKLM\\SYSTEM\\Select\n%!";
+     None
+
+(* Get the drive mappings.
+ * This page explains the contents of HKLM\System\MountedDevices:
+ * http://www.goodells.net/multiboot/partsigs.shtml
+ *)
+and get_drive_mappings h root data =
+  let devices = lazy (Devsparts.list_devices ()) in
+  let partitions = lazy (Devsparts.list_partitions ()) in
+  try
+    let path = [ "MountedDevices" ] in
+    let node = get_node h root path in
+    let values = Hivex.node_values h node in
+    let values = Array.to_list values in
+    let values =
+      filter_map (
+        fun value ->
+          let key = Hivex.value_key h value in
+          let keylen = String.length key in
+          if keylen >= 14 &&
+             String.lowercase_ascii (String.sub key 0 12) = "\\dosdevices\\" &&
+             Char.isalpha key.[12] && key.[13] = ':' then (
+            let drive_letter = String.sub key 12 1 in
+
+            (* Get the binary value.  Is it a fixed disk? *)
+            let (typ, blob) = Hivex.value_value h value in
+            let device =
+              if typ = Hivex.REG_BINARY then (
+                if String.length blob >= 24 &&
+                   String.is_prefix blob "DMIO:ID:" (* GPT *) then
+                  map_registry_disk_blob_gpt (Lazy.force partitions) blob
+                else if String.length blob = 12 then
+                  map_registry_disk_blob (Lazy.force devices) blob
+                else
+                  None
+              )
+              else None in
+
+            match device with
+            | None -> None
+            | Some device -> Some (drive_letter, device)
+          )
+          else
+            None
+      ) values in
+
+    data.drive_mappings <- values
+
+  with
+  | Not_found ->
+     if verbose () then
+       eprintf "check_windows_system_registry: cannot find drive mappings\n%!"
+
+(* Windows Registry HKLM\SYSTEM\MountedDevices uses a blob of data
+ * to store partitions.  This blob is described here:
+ * http://www.goodells.net/multiboot/partsigs.shtml
+ * The following function maps this blob to a libguestfs partition
+ * name, if possible.
+ *)
+and map_registry_disk_blob devices blob =
+  try
+    (* First 4 bytes are the disk ID.  Search all devices to find the
+     * disk with this disk ID.
+     *)
+    let diskid = String.sub blob 0 4 in
+    let device = List.find (fun dev -> pread dev 4 0x01b8 = diskid) devices in
+
+    (* Next 8 bytes are the offset of the partition in bytes(!) given as
+     * a 64 bit little endian number.  Luckily it's easy to get the
+     * partition byte offset from Parted.part_list.
+     *)
+    let offset = String.sub blob 4 8 in
+    let offset = int_of_le64 offset in
+    let partitions = Parted.part_list device in
+    let partition =
+      List.find (fun { Parted.part_start = s } -> s = offset) partitions in
+
+    (* Construct the full device name. *)
+    Some (sprintf "%s%ld" device partition.Parted.part_num)
+  with
+  | Not_found -> None
+
+(* Matches Windows registry HKLM\SYSYTEM\MountedDevices\DosDevices blob to
+ * to libguestfs GPT partition device. For GPT disks, the blob is made of
+ * "DMIO:ID:" prefix followed by the GPT partition GUID.
+ *)
+and map_registry_disk_blob_gpt partitions blob =
+  (* The blob_guid is returned as a lowercase hex string. *)
+  let blob_guid = extract_guid_from_registry_blob blob in
+
+  if verbose () then
+    eprintf "map_registry_disk_blob_gpt: searching for GUID %s\n%!"
+            blob_guid;
+
+  try
+    let partition =
+      List.find (
+        fun part ->
+          let partnum = Devsparts.part_to_partnum part in
+          let device = Devsparts.part_to_dev part in
+          let typ = Parted.part_get_parttype device in
+          if typ <> "gpt" then false
+          else (
+            let guid = Parted.part_get_gpt_guid device partnum in
+            String.lowercase_ascii guid = blob_guid
+          )
+      ) partitions in
+    Some partition
+  with
+  | Not_found -> None
+
+(* Extracts the binary GUID stored in blob from Windows registry
+ * HKLM\SYSTYEM\MountedDevices\DosDevices value and converts it to a
+ * GUID string so that it can be matched against libguestfs partition
+ * device GPT GUID.
+ *)
+and extract_guid_from_registry_blob blob =
+  (* Copy relevant sections from blob to respective ints.
+   * Note we have to skip 8 byte "DMIO:ID:" prefix.
+   *)
+  let data1 = int_of_le32 (String.sub blob 8 4)
+  and data2 = int_of_le16 (String.sub blob 12 2)
+  and data3 = int_of_le16 (String.sub blob 14 2)
+  and data4 = int_of_be64 (String.sub blob 16 8) (* really big endian! *) in
+
+  (* Must be lowercase hex. *)
+  sprintf "%08Lx-%04Lx-%04Lx-%04Lx-%012Lx"
+          data1 data2 data3
+          (Int64.shift_right_logical data4 48)
+          (data4 &^ 0xffffffffffff_L)
+
+and pread device size offset =
+  let fd = Unix.openfile device [Unix.O_RDONLY; Unix.O_CLOEXEC] 0 in
+  let ret =
+    protect ~f:(
+      fun () ->
+        ignore (Unix.lseek fd offset Unix.SEEK_SET);
+        let ret = Bytes.create size in
+        if Unix.read fd ret 0 size < size then
+          failwithf "pread: %s: short read" device;
+        ret
+    ) ~finally:(fun () -> Unix.close fd) in
+  Bytes.to_string ret
+
+(* Get the hostname. *)
+and get_hostname h root current_control_set =
+  try
+    let path = [ current_control_set; "Services"; "Tcpip"; "Parameters" ] in
+    let node = get_node h root path in
+    let values = Hivex.node_values h node in
+    let values = Array.to_list values in
+    (* Convert to a list of (key, value) to make the following easier. *)
+    let values = List.map (fun v -> Hivex.value_key h v, v) values in
+    let hostname_v = List.assoc "Hostname" values in
+    Some (hivex_value_as_utf8 h hostname_v)
+  with
+  | Not_found ->
+     if verbose () then
+       eprintf "check_windows_system_registry: cannot locate HKLM\\SYSTEM\\%s\\Services\\Tcpip\\Parameters and/or Hostname key\n%!" current_control_set;
+     None
+
+(* Raises [Not_found] if the node is not found. *)
+and get_node h node = function
+  | [] -> node
+  | x :: xs ->
+     let node = Hivex.node_get_child h node x in
+     get_node h node xs
+
+(* NB: This function DOES NOT test for the existence of the file.  It
+ * will return non-NULL even if the file/directory does not exist.
+ * You have to call guestfs_is_file{,_opts} etc.
+ *)
+and case_sensitive_path_silently path =
+  try
+    Some (Realpath.case_sensitive_path path)
+  with
+  | exn ->
+     if verbose () then
+       eprintf "case_sensitive_path_silently: %s: %s\n%!" path
+               (Printexc.to_string exn);
+     None
diff --git a/daemon/inspect_fs_windows.mli b/daemon/inspect_fs_windows.mli
new file mode 100644
index 000000000..a7dcd76ba
--- /dev/null
+++ b/daemon/inspect_fs_windows.mli
@@ -0,0 +1,24 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+val check_windows_root : Inspect_types.inspection_data -> unit
+(** Inspect the Windows [C:] filesystem mounted on sysroot. *)
+
+val is_windows_systemroot : unit -> bool
+(** Decide if the filesystem mounted on sysroot looks like a
+    Windows [C:] filesystem. *)
diff --git a/daemon/inspect_types.ml b/daemon/inspect_types.ml
new file mode 100644
index 000000000..a774ea272
--- /dev/null
+++ b/daemon/inspect_types.ml
@@ -0,0 +1,313 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+open Printf
+
+open Std_utils
+
+type fs = {
+  fs_location : location;
+  role : role;             (** Special cases: root filesystem or /usr *)
+}
+and root = {
+  root_location : location;
+  inspection_data : inspection_data;
+}
+and location = {
+  mountable : Mountable.t; (** The device name or other mountable object.*)
+  vfs_type : string;       (** Returned from [vfs_type] API. *)
+}
+
+and role =
+  | RoleRoot of inspection_data
+  | RoleUsr of inspection_data
+  | RoleSwap
+  | RoleOther
+and inspection_data = {
+  mutable os_type : os_type option;
+  mutable distro : distro option;
+  mutable package_format : package_format option;
+  mutable package_management : package_management option;
+  mutable product_name : string option;
+  mutable product_variant : string option;
+  mutable version : version option;
+  mutable arch : string option;
+  mutable hostname : string option;
+  mutable fstab : fstab_entry list;
+  mutable windows_systemroot : string option;
+  mutable windows_software_hive : string option;
+  mutable windows_system_hive : string option;
+  mutable windows_current_control_set : string option;
+  mutable drive_mappings : drive_mapping list;
+}
+and os_type =
+  | OS_TYPE_DOS
+  | OS_TYPE_FREEBSD
+  | OS_TYPE_HURD
+  | OS_TYPE_LINUX
+  | OS_TYPE_MINIX
+  | OS_TYPE_NETBSD
+  | OS_TYPE_OPENBSD
+  | OS_TYPE_WINDOWS
+and distro =
+  | DISTRO_ALPINE_LINUX
+  | DISTRO_ALTLINUX
+  | DISTRO_ARCHLINUX
+  | DISTRO_BUILDROOT
+  | DISTRO_CENTOS
+  | DISTRO_CIRROS
+  | DISTRO_COREOS
+  | DISTRO_DEBIAN
+  | DISTRO_FEDORA
+  | DISTRO_FREEBSD
+  | DISTRO_FREEDOS
+  | DISTRO_FRUGALWARE
+  | DISTRO_GENTOO
+  | DISTRO_LINUX_MINT
+  | DISTRO_MAGEIA
+  | DISTRO_MANDRIVA
+  | DISTRO_MEEGO
+  | DISTRO_NETBSD
+  | DISTRO_OPENBSD
+  | DISTRO_OPENSUSE
+  | DISTRO_ORACLE_LINUX
+  | DISTRO_PARDUS
+  | DISTRO_PLD_LINUX
+  | DISTRO_REDHAT_BASED
+  | DISTRO_RHEL
+  | DISTRO_SCIENTIFIC_LINUX
+  | DISTRO_SLACKWARE
+  | DISTRO_SLES
+  | DISTRO_SUSE_BASED
+  | DISTRO_TTYLINUX
+  | DISTRO_UBUNTU
+  | DISTRO_VOID_LINUX
+  | DISTRO_WINDOWS
+and package_format =
+  | PACKAGE_FORMAT_APK
+  | PACKAGE_FORMAT_DEB
+  | PACKAGE_FORMAT_EBUILD
+  | PACKAGE_FORMAT_PACMAN
+  | PACKAGE_FORMAT_PISI
+  | PACKAGE_FORMAT_PKGSRC
+  | PACKAGE_FORMAT_RPM
+  | PACKAGE_FORMAT_XBPS
+and package_management =
+  | PACKAGE_MANAGEMENT_APK
+  | PACKAGE_MANAGEMENT_APT
+  | PACKAGE_MANAGEMENT_DNF
+  | PACKAGE_MANAGEMENT_PACMAN
+  | PACKAGE_MANAGEMENT_PISI
+  | PACKAGE_MANAGEMENT_PORTAGE
+  | PACKAGE_MANAGEMENT_UP2DATE
+  | PACKAGE_MANAGEMENT_URPMI
+  | PACKAGE_MANAGEMENT_XBPS
+  | PACKAGE_MANAGEMENT_YUM
+  | PACKAGE_MANAGEMENT_ZYPPER
+and version = int * int
+and fstab_entry = Mountable.t * string (* mountable, mountpoint *)
+and drive_mapping = string * string (* drive name, device *)
+
+let rec string_of_fs { fs_location = location; role = role } =
+  sprintf "fs: %s role: %s"
+          (string_of_location location)
+          (match role with
+           | RoleRoot _ -> "root"
+           | RoleUsr _ -> "usr"
+           | RoleSwap -> "swap"
+           | RoleOther -> "other")
+
+and string_of_location { mountable = mountable; vfs_type = vfs_type } =
+  sprintf "%s (%s)" (Mountable.to_string mountable) vfs_type
+
+and string_of_root { root_location = location;
+                     inspection_data = inspection_data } =
+  sprintf "%s:\n%s"
+          (string_of_location location)
+          (string_of_inspection_data inspection_data)
+
+and string_of_inspection_data data =
+  let b = Buffer.create 1024 in
+  let bpf fs = bprintf b fs in
+  may (fun v -> bpf "    type: %s\n" (string_of_os_type v))
+      data.os_type;
+  may (fun v -> bpf "    distro: %s\n" (string_of_distro v))
+      data.distro;
+  may (fun v -> bpf "    package_format: %s\n" (string_of_package_format v))
+      data.package_format;
+  may (fun v -> bpf "    package_management: %s\n" (string_of_package_management v))
+      data.package_management;
+  may (fun v -> bpf "    product_name: %s\n" v)
+      data.product_name;
+  may (fun v -> bpf "    product_variant: %s\n" v)
+      data.product_variant;
+  may (fun (major, minor) -> bpf "    version: %d.%d\n" major minor)
+      data.version;
+  may (fun v -> bpf "    arch: %s\n" v)
+      data.arch;
+  may (fun v -> bpf "    hostname: %s\n" v)
+      data.hostname;
+  if data.fstab <> [] then (
+    let v = List.map (
+      fun (a, b) -> sprintf "(%s, %s)" (Mountable.to_string a) b
+    ) data.fstab in
+    bpf "    fstab: [%s]\n" (String.concat ", " v)
+  );
+  may (fun v -> bpf "    windows_systemroot: %s\n" v)
+      data.windows_systemroot;
+  may (fun v -> bpf "    windows_software_hive: %s\n" v)
+      data.windows_software_hive;
+  may (fun v -> bpf "    windows_system_hive: %s\n" v)
+      data.windows_system_hive;
+  may (fun v -> bpf "    windows_current_control_set: %s\n" v)
+      data.windows_current_control_set;
+  if data.drive_mappings <> [] then (
+    let v =
+      List.map (fun (a, b) -> sprintf "(%s, %s)" a b) data.drive_mappings in
+    bpf "    drive_mappings: [%s]\n" (String.concat ", " v)
+  );
+  Buffer.contents b
+
+and string_of_os_type = function
+  | OS_TYPE_DOS -> "dos"
+  | OS_TYPE_FREEBSD -> "freebsd"
+  | OS_TYPE_HURD -> "hurd"
+  | OS_TYPE_LINUX -> "linux"
+  | OS_TYPE_MINIX -> "minix"
+  | OS_TYPE_NETBSD -> "netbsd"
+  | OS_TYPE_OPENBSD -> "openbsd"
+  | OS_TYPE_WINDOWS -> "windows"
+
+and string_of_distro = function
+  | DISTRO_ALPINE_LINUX -> "alpinelinux"
+  | DISTRO_ALTLINUX -> "altlinux"
+  | DISTRO_ARCHLINUX -> "archlinux"
+  | DISTRO_BUILDROOT -> "buildroot"
+  | DISTRO_CENTOS -> "centos"
+  | DISTRO_CIRROS -> "cirros"
+  | DISTRO_COREOS -> "coreos"
+  | DISTRO_DEBIAN -> "debian"
+  | DISTRO_FEDORA -> "fedora"
+  | DISTRO_FREEBSD -> "freebsd"
+  | DISTRO_FREEDOS -> "freedos"
+  | DISTRO_FRUGALWARE -> "frugalware"
+  | DISTRO_GENTOO -> "gentoo"
+  | DISTRO_LINUX_MINT -> "linuxmint"
+  | DISTRO_MAGEIA -> "mageia"
+  | DISTRO_MANDRIVA -> "mandriva"
+  | DISTRO_MEEGO -> "meego"
+  | DISTRO_NETBSD -> "netbsd"
+  | DISTRO_OPENBSD -> "openbsd"
+  | DISTRO_OPENSUSE -> "opensuse"
+  | DISTRO_ORACLE_LINUX -> "oraclelinux"
+  | DISTRO_PARDUS -> "pardus"
+  | DISTRO_PLD_LINUX -> "pldlinux"
+  | DISTRO_REDHAT_BASED -> "redhat-based"
+  | DISTRO_RHEL -> "rhel"
+  | DISTRO_SCIENTIFIC_LINUX -> "scientificlinux"
+  | DISTRO_SLACKWARE -> "slackware"
+  | DISTRO_SLES -> "sles"
+  | DISTRO_SUSE_BASED -> "suse-based"
+  | DISTRO_TTYLINUX -> "ttylinux"
+  | DISTRO_UBUNTU -> "ubuntu"
+  | DISTRO_VOID_LINUX -> "voidlinux"
+  | DISTRO_WINDOWS -> "windows"
+
+and string_of_package_format = function
+  | PACKAGE_FORMAT_APK -> "apk"
+  | PACKAGE_FORMAT_DEB -> "deb"
+  | PACKAGE_FORMAT_EBUILD -> "ebuild"
+  | PACKAGE_FORMAT_PACMAN -> "pacman"
+  | PACKAGE_FORMAT_PISI -> "pisi"
+  | PACKAGE_FORMAT_PKGSRC -> "pkgsrc"
+  | PACKAGE_FORMAT_RPM -> "rpm"
+  | PACKAGE_FORMAT_XBPS -> "xbps"
+
+and string_of_package_management = function
+  | PACKAGE_MANAGEMENT_APK -> "apk"
+  | PACKAGE_MANAGEMENT_APT -> "apt"
+  | PACKAGE_MANAGEMENT_DNF -> "dnf"
+  | PACKAGE_MANAGEMENT_PACMAN -> "pacman"
+  | PACKAGE_MANAGEMENT_PISI -> "pisi"
+  | PACKAGE_MANAGEMENT_PORTAGE -> "portage"
+  | PACKAGE_MANAGEMENT_UP2DATE -> "up2date"
+  | PACKAGE_MANAGEMENT_URPMI -> "urpmi"
+  | PACKAGE_MANAGEMENT_XBPS -> "xbps"
+  | PACKAGE_MANAGEMENT_YUM -> "yum"
+  | PACKAGE_MANAGEMENT_ZYPPER -> "zypper"
+
+let null_inspection_data = {
+  os_type = None;
+  distro = None;
+  package_format = None;
+  package_management = None;
+  product_name = None;
+  product_variant = None;
+  version = None;
+  arch = None;
+  hostname = None;
+  fstab = [];
+  windows_systemroot = None;
+  windows_software_hive = None;
+  windows_system_hive = None;
+  windows_current_control_set = None;
+  drive_mappings = [];
+}
+
+let merge_inspection_data child parent =
+  let merge child parent = if parent = None then child else parent in
+
+  parent.os_type <-         merge child.os_type parent.os_type;
+  parent.distro <-          merge child.distro parent.distro;
+  parent.package_format <-  merge child.package_format parent.package_format;
+  parent.package_management <-
+    merge child.package_management parent.package_management;
+  parent.product_name <-    merge child.product_name parent.product_name;
+  parent.product_variant <- merge child.product_variant parent.product_variant;
+  parent.version <-         merge child.version parent.version;
+  parent.arch <-            merge child.arch parent.arch;
+  parent.hostname <-        merge child.hostname parent.hostname;
+  parent.fstab <-           child.fstab @ parent.fstab;
+  parent.windows_systemroot <-
+    merge child.windows_systemroot parent.windows_systemroot;
+  parent.windows_software_hive <-
+    merge child.windows_software_hive parent.windows_software_hive;
+  parent.windows_system_hive <-
+    merge child.windows_system_hive parent.windows_system_hive;
+  parent.windows_current_control_set <-
+    merge child.windows_current_control_set parent.windows_current_control_set;
+
+  (* This is what the old C code did, but I doubt that it's correct. *)
+  parent.drive_mappings <-  child.drive_mappings @ parent.drive_mappings
+
+let merge child_fs parent_fs =
+  let inspection_data_of_fs = function
+    | { role = RoleRoot data }
+    | { role = RoleUsr data } -> data
+    | { role = (RoleSwap|RoleOther) } -> null_inspection_data
+  in
+
+  match parent_fs with
+  | { role = RoleRoot parent_data } ->
+     merge_inspection_data (inspection_data_of_fs child_fs) parent_data
+  | { role = RoleUsr parent_data } ->
+     merge_inspection_data (inspection_data_of_fs child_fs) parent_data
+  | { role = (RoleSwap|RoleOther) } ->
+     ()
+
+let inspect_fses = ref []
diff --git a/daemon/inspect_types.mli b/daemon/inspect_types.mli
new file mode 100644
index 000000000..aae266845
--- /dev/null
+++ b/daemon/inspect_types.mli
@@ -0,0 +1,180 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+type fs = {
+  fs_location : location;
+  role : role;             (** Special cases: root filesystem or /usr *)
+}
+(** A single filesystem. *)
+
+and root = {
+  root_location : location;
+  inspection_data : inspection_data;
+}
+(** A root (as in "inspect_get_roots"). *)
+
+and location = {
+  mountable : Mountable.t; (** The device name or other mountable object.*)
+  vfs_type : string;       (** Returned from [vfs_type] API. *)
+}
+
+and role =
+  | RoleRoot of inspection_data
+  | RoleUsr of inspection_data
+  | RoleSwap
+  | RoleOther
+(** During inspection, single filesystems are assigned a role which is
+    one of root, /usr, swap or other. *)
+
+and inspection_data = {
+  mutable os_type : os_type option;
+  mutable distro : distro option;
+  mutable package_format : package_format option;
+  mutable package_management : package_management option;
+  mutable product_name : string option;
+  mutable product_variant : string option;
+  mutable version : version option;
+  mutable arch : string option;
+  mutable hostname : string option;
+  mutable fstab : fstab_entry list;
+  mutable windows_systemroot : string option;
+  mutable windows_software_hive : string option;
+  mutable windows_system_hive : string option;
+  mutable windows_current_control_set : string option;
+  mutable drive_mappings : drive_mapping list;
+}
+(** During inspection, this data is collected incrementally for each
+    filesystem.  At the end of inspection, inspection data is merged
+    into the root. *)
+
+and os_type =
+  | OS_TYPE_DOS
+  | OS_TYPE_FREEBSD
+  | OS_TYPE_HURD
+  | OS_TYPE_LINUX
+  | OS_TYPE_MINIX
+  | OS_TYPE_NETBSD
+  | OS_TYPE_OPENBSD
+  | OS_TYPE_WINDOWS
+and distro =
+  | DISTRO_ALPINE_LINUX
+  | DISTRO_ALTLINUX
+  | DISTRO_ARCHLINUX
+  | DISTRO_BUILDROOT
+  | DISTRO_CENTOS
+  | DISTRO_CIRROS
+  | DISTRO_COREOS
+  | DISTRO_DEBIAN
+  | DISTRO_FEDORA
+  | DISTRO_FREEBSD
+  | DISTRO_FREEDOS
+  | DISTRO_FRUGALWARE
+  | DISTRO_GENTOO
+  | DISTRO_LINUX_MINT
+  | DISTRO_MAGEIA
+  | DISTRO_MANDRIVA
+  | DISTRO_MEEGO
+  | DISTRO_NETBSD
+  | DISTRO_OPENBSD
+  | DISTRO_OPENSUSE
+  | DISTRO_ORACLE_LINUX
+  | DISTRO_PARDUS
+  | DISTRO_PLD_LINUX
+  | DISTRO_REDHAT_BASED
+  | DISTRO_RHEL
+  | DISTRO_SCIENTIFIC_LINUX
+  | DISTRO_SLACKWARE
+  | DISTRO_SLES
+  | DISTRO_SUSE_BASED
+  | DISTRO_TTYLINUX
+  | DISTRO_UBUNTU
+  | DISTRO_VOID_LINUX
+  | DISTRO_WINDOWS
+and package_format =
+  | PACKAGE_FORMAT_APK
+  | PACKAGE_FORMAT_DEB
+  | PACKAGE_FORMAT_EBUILD
+  | PACKAGE_FORMAT_PACMAN
+  | PACKAGE_FORMAT_PISI
+  | PACKAGE_FORMAT_PKGSRC
+  | PACKAGE_FORMAT_RPM
+  | PACKAGE_FORMAT_XBPS
+and package_management =
+  | PACKAGE_MANAGEMENT_APK
+  | PACKAGE_MANAGEMENT_APT
+  | PACKAGE_MANAGEMENT_DNF
+  | PACKAGE_MANAGEMENT_PACMAN
+  | PACKAGE_MANAGEMENT_PISI
+  | PACKAGE_MANAGEMENT_PORTAGE
+  | PACKAGE_MANAGEMENT_UP2DATE
+  | PACKAGE_MANAGEMENT_URPMI
+  | PACKAGE_MANAGEMENT_XBPS
+  | PACKAGE_MANAGEMENT_YUM
+  | PACKAGE_MANAGEMENT_ZYPPER
+and version = int * int
+and fstab_entry = Mountable.t * string (* mountable, mountpoint *)
+and drive_mapping = string * string (* drive name, device *)
+
+val merge_inspection_data : inspection_data -> inspection_data -> unit
+(** [merge_inspection_data child parent] merges two sets of inspection
+    data into the parent.  The parent inspection data fields, if
+    present, take precedence over the child inspection data fields.
+
+    It's intended that you merge upwards, ie.
+    [merge_inspection_data usr root] *)
+
+val merge : fs -> fs -> unit
+(** [merge child_fs parent_fs] merges two filesystems,
+    using [merge_inspection_data] to merge the inspection data of
+    the child into the parent.  (Nothing else is merged, only
+    the inspection data). *)
+
+val string_of_fs : fs -> string
+(** Convert [fs] into a single line string, for debugging only. *)
+
+val string_of_root : root -> string
+(** Convert [root] into a multi-line string, for debugging only. *)
+
+val string_of_location : location -> string
+(** Convert [location] into a string, for debugging only. *)
+
+val string_of_inspection_data : inspection_data -> string
+(** Convert [inspection_data] into a multi-line string, for debugging only. *)
+
+val string_of_os_type : os_type -> string
+(** Convert [os_type] to a string.
+    The string is part of the public API. *)
+
+val string_of_distro : distro -> string
+(** Convert [distro] to a string.
+    The string is part of the public API. *)
+
+val string_of_package_format : package_format -> string
+(** Convert [package_format] to a string.
+    The string is part of the public API. *)
+
+val string_of_package_management : package_management -> string
+(** Convert [package_management] to a string.
+    The string is part of the public API. *)
+
+val null_inspection_data : inspection_data
+(** {!inspection_data} structure with all fields set to [None]. *)
+
+val inspect_fses : fs list ref
+(** The global list of filesystems found by the previous call to
+    inspect_os. *)
diff --git a/daemon/inspect_utils.ml b/daemon/inspect_utils.ml
new file mode 100644
index 000000000..9f66bbfea
--- /dev/null
+++ b/daemon/inspect_utils.ml
@@ -0,0 +1,193 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+open Unix
+open Printf
+
+open Std_utils
+
+open Utils
+open Inspect_types
+
+let max_augeas_file_size = 100 * 1000
+
+let rec with_augeas ?name configfiles f =
+  let sysroot = Sysroot.sysroot () in
+  let name =
+    match name with
+    | None -> sprintf "with_augeas: %s" (String.concat " " configfiles)
+    | Some name -> name in
+  let chroot = Chroot.create ~name sysroot in
+
+  (* Security:
+   *
+   * The old C code had a few problems: It ignored non-regular-file
+   * objects (eg. devices), passing them to Augeas, so relying on
+   * Augeas to do the right thing.  Also too-large regular files
+   * caused the whole inspection operation to fail.
+   *
+   * I have tried to improve this so that non-regular files and
+   * too large files are ignored (dropped from the configfiles list),
+   * so that Augeas won't touch them, but they also won't stop
+   * inspection.
+   *)
+  let safe_file file =
+    Is.is_file ~followsymlinks:true file && (
+      let size = (Chroot.f chroot Unix.stat file).Unix.st_size in
+      size <= max_augeas_file_size
+    )
+  in
+  let configfiles = List.filter safe_file configfiles in
+
+  let aug =
+    Augeas.create sysroot None [Augeas.AugSaveNoop; Augeas.AugNoLoad] in
+
+  protect
+    ~f:(fun () ->
+      (* Tell Augeas to only load configfiles and no other files.  This
+       * prevents a rogue guest from performing a denial of service attack
+       * by having large, over-complicated configuration files which are
+       * unrelated to the task at hand.  (Thanks Dominic Cleal).
+       * Note this requires Augeas >= 1.0.0 because of RHBZ#975412.
+       *)
+      let pathexpr = make_augeas_path_expression configfiles in
+      ignore (aug_rm_noerrors aug pathexpr);
+      Augeas.load aug;
+
+      (* Check that augeas did not get a parse error for any of the
+       * configfiles, otherwise we are silently missing information.
+       *)
+      let matches = aug_matches_noerrors aug "/augeas/files//error" in
+      List.iter (
+        fun match_ ->
+          List.iter (
+            fun file ->
+              let errorpath = sprintf "/augeas/files%s/error" file in
+              if match_ = errorpath then (
+                (* There's been an error - get the error details. *)
+                let get path =
+                  match aug_get_noerrors aug (errorpath ^ path) with
+                  | None -> "<missing>"
+                  | Some v -> v
+                in
+                let message = get "message" in
+                let line = get "line" in
+                let charp = get "char" in
+                failwithf "%s:%s:%s: augeas parse failure: %s"
+                          file line charp message
+              )
+          ) configfiles
+      ) matches;
+
+      f aug
+    )
+    ~finally:(
+      fun () -> Augeas.close aug
+    )
+
+(* Explained here: https://bugzilla.redhat.com/show_bug.cgi?id=975412#c0 *)
+and make_augeas_path_expression files =
+  let subexprs =
+    List.map (
+      fun file ->
+        (*           v NB trailing '/' after filename *)
+        sprintf "\"%s/\" !~ regexp('^') + glob(incl) + regexp('/.*')" file
+    ) files in
+  let subexprs = String.concat " and " subexprs in
+
+  let ret = sprintf "/augeas/load/*[ %s ]" subexprs in
+  if verbose () then
+    eprintf "augeas pathexpr = %s\n%!" ret;
+
+  ret
+
+and aug_get_noerrors aug path =
+  try Augeas.get aug path
+  with Augeas.Error _ -> None
+
+and aug_matches_noerrors aug path =
+  try Augeas.matches aug path
+  with Augeas.Error _ -> []
+
+and aug_rm_noerrors aug path =
+  try Augeas.rm aug path
+  with Augeas.Error _ -> 0
+
+let is_file_nocase path =
+  let path =
+    try Some (Realpath.case_sensitive_path path)
+    with _ -> None in
+  match path with
+  | None -> false
+  | Some path -> Is.is_file path
+
+and is_dir_nocase path =
+  let path =
+    try Some (Realpath.case_sensitive_path path)
+    with _ -> None in
+  match path with
+  | None -> false
+  | Some path -> Is.is_dir path
+
+(* Rather hairy test for "is a partition", taken directly from
+ * the old C inspection code.  XXX fix function and callers
+ *)
+let is_partition partition =
+  try
+    let device = Devsparts.part_to_dev partition in
+    ignore (Devsparts.device_index device);
+    true
+  with _ -> false
+
+(* Without non-greedy matching, it's difficult to write these regular
+ * expressions properly.  We should really switch to using PCRE. XXX
+ *)
+let re_major_minor = Str.regexp "[^0-9]*\\([0-9]+\\)\\.\\([0-9]+\\)"
+let re_major_no_minor = Str.regexp "[^0-9]*\\([0-9]+\\)"
+
+let parse_version_from_major_minor str data =
+  if verbose () then
+    eprintf "parse_version_from_major_minor: parsing '%s'\n%!" str;
+
+  if Str.string_match re_major_minor str 0 ||
+       Str.string_match re_major_no_minor str 0 then (
+    let major =
+      try Some (int_of_string (Str.matched_group 1 str))
+      with Not_found | Invalid_argument _ | Failure _ -> None in
+    let minor =
+      try Some (int_of_string (Str.matched_group 2 str))
+      with Not_found | Invalid_argument _ | Failure _ -> None in
+    match major, minor with
+    | None, None
+    | None, Some _ -> ()
+    | Some major, None -> data.version <- Some (major, 0)
+    | Some major, Some minor -> data.version <- Some (major, minor)
+  )
+  else (
+    eprintf "parse_version_from_major_minor: cannot parse version from '%s'\n"
+            str
+  )
+
+let with_hive hive_filename f =
+  let flags = [ Hivex.OPEN_UNSAFE ] in
+  let flags = if verbose () then Hivex.OPEN_VERBOSE :: flags else flags in
+  let h = Hivex.open_file hive_filename flags in
+  protect ~f:(fun () -> f h (Hivex.root h)) ~finally:(fun () -> Hivex.close h)
+
+let hivex_value_as_utf8 h value =
+  utf16le_to_utf8 (snd (Hivex.value_value h value))
diff --git a/daemon/inspect_utils.mli b/daemon/inspect_utils.mli
new file mode 100644
index 000000000..74e196b52
--- /dev/null
+++ b/daemon/inspect_utils.mli
@@ -0,0 +1,57 @@
+(* guestfs-inspection
+ * Copyright (C) 2009-2017 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.
+ *)
+
+val with_augeas : ?name:string -> string list -> (Augeas.t -> 'a) -> 'a
+(** Open an Augeas handle, parse only 'configfiles' (these
+    files must exist), and then call 'f' with the Augeas handle.
+
+    As a security measure, this bails if any file is too large for
+    a reasonable configuration file.  After the call to 'f' the
+    Augeas handle is closed. *)
+
+val aug_get_noerrors : Augeas.t -> string -> string option
+val aug_matches_noerrors : Augeas.t -> string -> string list
+val aug_rm_noerrors : Augeas.t -> string -> int
+(** When inspecting a guest, we don't particularly care if Augeas
+    calls fail.  These functions wrap {!Augeas.get}, {!Augeas.matches}
+    and {!Augeas.rm} returning null content if there is an error. *)
+
+val is_file_nocase : string -> bool
+val is_dir_nocase : string -> bool
+(** With a filesystem mounted under sysroot, check if [path] is
+    a file or directory under that sysroot.  The [path] is
+    checked case-insensitively. *)
+
+val is_partition : string -> bool
+(** Return true if the device is a partition. *)
+
+val parse_version_from_major_minor : string -> Inspect_types.inspection_data -> unit
+(** Make a best effort attempt to parse either X or X.Y from a string,
+    usually the product_name string. *)
+
+val with_hive : string -> (Hivex.t -> Hivex.node -> 'a) -> 'a
+(** Open a Windows registry "hive", and call the function on the
+    handle and root node.
+
+    After the call to the function, the hive is always closed.
+
+    The hive is opened readonly. *)
+
+val hivex_value_as_utf8 : Hivex.t -> Hivex.value -> string
+(** Convert a Hivex value which we interpret as UTF-16LE to UTF-8.
+    The type field stored in the registry is ignored. *)
diff --git a/daemon/mount.ml b/daemon/mount.ml
index 4bb74fb82..40c81be0e 100644
--- a/daemon/mount.ml
+++ b/daemon/mount.ml
@@ -60,3 +60,64 @@ let mount_vfs options vfs mountable mountpoint =
 let mount = mount_vfs None None
 let mount_ro = mount_vfs (Some "ro") None
 let mount_options options = mount_vfs (Some options) None
+
+(* Unmount everything mounted under /sysroot.
+ *
+ * We have to unmount in the correct order, so we sort the paths by
+ * longest first to ensure that child paths are unmounted by parent
+ * paths.
+ *
+ * This call is more important than it appears at first, because it
+ * is widely used by both test and production code in order to
+ * get back to a known state (nothing mounted, everything synchronized).
+ *)
+let rec umount_all () =
+  (* This is called from internal_autosync and generally as a cleanup
+   * function, and since the umount will definitely fail if any
+   * handles are open, we may as well close them.
+   *)
+  (* XXX
+  aug_finalize ();
+  hivex_finalize ();
+  journal_finalize ();
+  *)
+
+  let sysroot = Sysroot.sysroot () in
+  let sysroot_len = String.length sysroot in
+
+  let info = read_whole_file "/proc/self/mountinfo" in
+  let info = String.nsplit "\n" info in
+
+  let mps = ref [] in
+  List.iter (
+    fun line ->
+      let line = String.nsplit " " line in
+      (* The field of interest is the 5th field.  Whitespace is escaped
+       * with octal sequences like \040 (for space).
+       * See fs/seq_file.c:mangle_path.
+       *)
+      if List.length line >= 5 then (
+        let mp = List.nth line 4 in
+        let mp = proc_unmangle_path mp in
+
+        (* Allow a mount directory like "/sysroot" or "/sysroot/..." *)
+        if (sysroot_len > 0 && String.is_prefix mp sysroot) ||
+           (String.is_prefix mp sysroot &&
+            String.length mp > sysroot_len &&
+            mp.[sysroot_len] = '/') then
+          push_front mp mps
+      )
+  ) info;
+
+  let mps = !mps in
+  let mps = List.sort compare_longest_first mps in
+
+  (* Unmount them. *)
+  List.iter (
+    fun mp -> ignore (command "umount" [mp])
+  ) mps
+
+and compare_longest_first s1 s2 =
+  let n1 = String.length s1 in
+  let n2 = String.length s2 in
+  n2 - n1
diff --git a/daemon/mount.mli b/daemon/mount.mli
index e43d97c42..abf538521 100644
--- a/daemon/mount.mli
+++ b/daemon/mount.mli
@@ -20,3 +20,5 @@ val mount : Mountable.t -> string -> unit
 val mount_ro : Mountable.t -> string -> unit
 val mount_options : string -> Mountable.t -> string -> unit
 val mount_vfs : string option -> string option -> Mountable.t -> string -> unit
+
+val umount_all : unit -> unit
diff --git a/daemon/utils.ml b/daemon/utils.ml
index 808e575fd..e53b4bf02 100644
--- a/daemon/utils.ml
+++ b/daemon/utils.ml
@@ -247,3 +247,103 @@ let proc_unmangle_path path =
 let is_small_file path =
   is_regular_file path &&
     (stat path).st_size <= 2 * 1048 * 1024
+
+let unix_canonical_path path =
+  let is_absolute = String.length path > 0 && path.[0] = '/' in
+  let path = String.nsplit "/" path in
+  let path = List.filter ((<>) "") path in
+  (if is_absolute then "/" else "") ^ String.concat "/" path
+
+(* Note that we cannot use iconv here because inside the appliance
+ * all i18n databases are deleted.  For the same reason we cannot
+ * use functions like hivex_value_string, as they also use iconv
+ * internally.
+ *
+ * https://en.wikipedia.org/wiki/UTF-16
+ * Also inspired by functions in glib's glib/gutf8.c
+ *)
+let rec utf16le_to_utf8 instr =
+  (* If the length is odd and the last character is ASCII NUL, just
+   * drop that.  (If it's not ASCII NUL, then there's an error)
+   *)
+  let len = String.length instr in
+  let instr =
+    if len mod 1 = 1 then (
+      if instr.[len-1] = '\000' then String.sub instr 0 (len-1)
+      else invalid_arg "input is not a valid UTF16-LE string: length is odd"
+    ) else instr in
+
+  (* The length should now be even.  If the last two bytes are
+   * '\0\0' then assume it's a NUL-terminated string from the
+   * Windows registry and drop both characters.
+   *)
+  let len = String.length instr in
+  let instr =
+    if len >= 2 && instr.[len-2] = '\000' && instr.[len-1] = '\000' then
+      String.sub instr 0 (len-2)
+    else instr in
+
+  let outbuf = Buffer.create len in
+
+  (* Encode a wide character as UTF-8 and write to outbuf.
+   * Basically this is g_unichar_to_utf8 implemented in OCaml.
+   *)
+  let encode_utf8 c =
+    let first, len =
+      if c < 0x80 then
+        (0, 1)
+      else if c < 0x800 then
+        (0xc0, 2)
+      else if c < 0x10000 then
+        (0xe0, 3)
+      else if c < 0x200000 then
+        (0xf0, 4)
+      else if c < 0x4000000 then
+        (0xf8, 5)
+      else
+        (0xfc, 6) in
+    let rec loop i c =
+      if i = 0 then Buffer.add_char outbuf (Char.chr (c lor first))
+      else if i > 0 then (
+        loop (i-1) (c lsr 6);
+        Buffer.add_char outbuf (Char.chr ((c land 0x3f) lor 0x80))
+      )
+    in
+    loop (len-1) c
+  in
+
+  (* Loop over the input UTF16-LE characters. *)
+  let is_high_surrogate c = c >= 0xd800 && c < 0xdc00
+  and is_low_surrogate c = c >= 0xdc00 && c < 0xe000
+  and surrogate_value highc lowc =
+    0x1_0000 + (highc - 0xd800) * 0x400 + lowc - 0xdc00
+  in
+
+  let len = String.length instr in
+  let rec loop i =
+    if i+1 >= len then ()
+    else (
+      let c = Char.code instr.[i] + (Char.code instr.[i+1] lsl 8) in
+
+      let wc, skip =
+        (* High surrogate - must come first. *)
+        if is_high_surrogate c then (
+          if i+3 >= len then
+            invalid_arg "input is not a valid UTF16-LE string: high surrogate at end of string";
+          let lowc = Char.code instr.[i+2] + (Char.code instr.[i+3] lsl 8) in
+          if not (is_low_surrogate lowc) then
+            invalid_arg "input is not a valid UTF16-LE string: high surrogate not followed by low surrogate";
+          (surrogate_value c lowc, 4)
+        )
+        else if is_low_surrogate c then
+          invalid_arg "input is not a valid UTF16-LE string: unexpected low surrogate"
+        else
+          (c, 2) in
+
+      encode_utf8 wc;
+      loop (i+skip)
+    )
+  in
+  loop 0;
+
+  Buffer.contents outbuf
diff --git a/daemon/utils.mli b/daemon/utils.mli
index d3c8bdf4d..94a77de01 100644
--- a/daemon/utils.mli
+++ b/daemon/utils.mli
@@ -85,3 +85,15 @@ val commandr : ?flags:command_flag list -> string -> string list -> (int * strin
 
 val is_small_file : string -> bool
 (** Return true if the path is a small regular file. *)
+
+val unix_canonical_path : string -> string
+(** Canonicalize a Unix path, so "///usr//local//" -> "/usr/local"
+
+    The path is modified in place because the result is always
+    the same length or shorter than the argument passed. *)
+
+val utf16le_to_utf8 : string -> string
+(** Convert a UTF16-LE string to UTF-8.
+
+    This uses a simple internal implementation since we cannot use
+    iconv inside the daemon. *)
diff --git a/docs/C_SOURCE_FILES b/docs/C_SOURCE_FILES
index 39dcf9035..e90aac0f0 100644
--- a/docs/C_SOURCE_FILES
+++ b/docs/C_SOURCE_FILES
@@ -64,6 +64,7 @@ customize/perl_edit-c.c
 daemon/9p.c
 daemon/acl.c
 daemon/actions.h
+daemon/augeas-c.c
 daemon/augeas.c
 daemon/available.c
 daemon/base64.c
@@ -306,9 +307,6 @@ lib/handle.c
 lib/hivex.c
 lib/info.c
 lib/inspect-apps.c
-lib/inspect-fs-unix.c
-lib/inspect-fs-windows.c
-lib/inspect-fs.c
 lib/inspect-icon.c
 lib/inspect.c
 lib/journal.c
diff --git a/generator/actions.ml b/generator/actions.ml
index 75742397a..515096881 100644
--- a/generator/actions.ml
+++ b/generator/actions.ml
@@ -51,6 +51,8 @@ let daemon_functions =
   Actions_core_deprecated.daemon_functions @
   Actions_debug.daemon_functions @
   Actions_hivex.daemon_functions @
+  Actions_inspection.daemon_functions @
+  Actions_inspection_deprecated.daemon_functions @
   Actions_tsk.daemon_functions @
   Actions_yara.daemon_functions
 
diff --git a/generator/actions_inspection.ml b/generator/actions_inspection.ml
index cd8b9da18..d67d00833 100644
--- a/generator/actions_inspection.ml
+++ b/generator/actions_inspection.ml
@@ -22,10 +22,11 @@ open Types
 
 (* Inspection APIs. *)
 
-let non_daemon_functions = [
+let daemon_functions = [
   { defaults with
     name = "inspect_os"; added = (1, 5, 3);
     style = RStringList (RMountable, "roots"), [], [];
+    impl = OCaml "Inspect.inspect_os";
     shortdesc = "inspect disk and return list of operating systems found";
     longdesc = "\
 This function uses other libguestfs functions and certain
@@ -61,8 +62,24 @@ Please read L<guestfs(3)/INSPECTION> for more details.
 See also C<guestfs_list_filesystems>." };
 
   { defaults with
+    name = "inspect_get_roots"; added = (1, 7, 3);
+    style = RStringList (RMountable, "roots"), [], [];
+    impl = OCaml "Inspect.inspect_get_roots";
+    shortdesc = "return list of operating systems found by last inspection";
+    longdesc = "\
+This function is a convenient way to get the list of root
+devices, as returned from a previous call to C<guestfs_inspect_os>,
+but without redoing the whole inspection process.
+
+This returns an empty list if either no root devices were
+found or the caller has not called C<guestfs_inspect_os>.
+
+Please read L<guestfs(3)/INSPECTION> for more details." };
+
+  { defaults with
     name = "inspect_get_type"; added = (1, 5, 3);
     style = RString (RPlainString, "name"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_type";
     shortdesc = "get type of inspected operating system";
     longdesc = "\
 This returns the type of the inspected operating system.
@@ -116,6 +133,7 @@ Please read L<guestfs(3)/INSPECTION> for more details." };
   { defaults with
     name = "inspect_get_arch"; added = (1, 5, 3);
     style = RString (RPlainString, "arch"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_arch";
     shortdesc = "get architecture of inspected operating system";
     longdesc = "\
 This returns the architecture of the inspected operating system.
@@ -130,6 +148,7 @@ Please read L<guestfs(3)/INSPECTION> for more details." };
   { defaults with
     name = "inspect_get_distro"; added = (1, 5, 3);
     style = RString (RPlainString, "distro"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_distro";
     shortdesc = "get distro of inspected operating system";
     longdesc = "\
 This returns the distro (distribution) of the inspected operating
@@ -286,6 +305,7 @@ Please read L<guestfs(3)/INSPECTION> for more details." };
   { defaults with
     name = "inspect_get_major_version"; added = (1, 5, 3);
     style = RInt "major", [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_major_version";
     shortdesc = "get major version of inspected operating system";
     longdesc = "\
 This returns the major version number of the inspected operating
@@ -305,6 +325,7 @@ Please read L<guestfs(3)/INSPECTION> for more details." };
   { defaults with
     name = "inspect_get_minor_version"; added = (1, 5, 3);
     style = RInt "minor", [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_minor_version";
     shortdesc = "get minor version of inspected operating system";
     longdesc = "\
 This returns the minor version number of the inspected operating
@@ -318,6 +339,7 @@ See also C<guestfs_inspect_get_major_version>." };
   { defaults with
     name = "inspect_get_product_name"; added = (1, 5, 3);
     style = RString (RPlainString, "product"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_product_name";
     shortdesc = "get product name of inspected operating system";
     longdesc = "\
 This returns the product name of the inspected operating
@@ -331,8 +353,164 @@ string C<unknown> is returned.
 Please read L<guestfs(3)/INSPECTION> for more details." };
 
   { defaults with
+    name = "inspect_get_windows_systemroot"; added = (1, 5, 25);
+    style = RString (RPlainString, "systemroot"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_windows_systemroot";
+    shortdesc = "get Windows systemroot of inspected operating system";
+    longdesc = "\
+This returns the Windows systemroot of the inspected guest.
+The systemroot is a directory path such as F</WINDOWS>.
+
+This call assumes that the guest is Windows and that the
+systemroot could be determined by inspection.  If this is not
+the case then an error is returned.
+
+Please read L<guestfs(3)/INSPECTION> for more details." };
+
+  { defaults with
+    name = "inspect_get_package_format"; added = (1, 7, 5);
+    style = RString (RPlainString, "packageformat"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_package_format";
+    shortdesc = "get package format used by the operating system";
+    longdesc = "\
+This function and C<guestfs_inspect_get_package_management> return
+the package format and package management tool used by the
+inspected operating system.  For example for Fedora these
+functions would return C<rpm> (package format), and
+C<yum> or C<dnf> (package management).
+
+This returns the string C<unknown> if we could not determine the
+package format I<or> if the operating system does not have
+a real packaging system (eg. Windows).
+
+Possible strings include:
+C<rpm>, C<deb>, C<ebuild>, C<pisi>, C<pacman>, C<pkgsrc>, C<apk>,
+C<xbps>.
+Future versions of libguestfs may return other strings.
+
+Please read L<guestfs(3)/INSPECTION> for more details." };
+
+  { defaults with
+    name = "inspect_get_package_management"; added = (1, 7, 5);
+    style = RString (RPlainString, "packagemanagement"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_package_management";
+    shortdesc = "get package management tool used by the operating system";
+    longdesc = "\
+C<guestfs_inspect_get_package_format> and this function return
+the package format and package management tool used by the
+inspected operating system.  For example for Fedora these
+functions would return C<rpm> (package format), and
+C<yum> or C<dnf> (package management).
+
+This returns the string C<unknown> if we could not determine the
+package management tool I<or> if the operating system does not have
+a real packaging system (eg. Windows).
+
+Possible strings include: C<yum>, C<dnf>, C<up2date>,
+C<apt> (for all Debian derivatives),
+C<portage>, C<pisi>, C<pacman>, C<urpmi>, C<zypper>, C<apk>, C<xbps>.
+Future versions of libguestfs may return other strings.
+
+Please read L<guestfs(3)/INSPECTION> for more details." };
+
+  { defaults with
+    name = "inspect_get_hostname"; added = (1, 7, 9);
+    style = RString (RPlainString, "hostname"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_hostname";
+    shortdesc = "get hostname of the operating system";
+    longdesc = "\
+This function returns the hostname of the operating system
+as found by inspection of the guest’s configuration files.
+
+If the hostname could not be determined, then the
+string C<unknown> is returned.
+
+Please read L<guestfs(3)/INSPECTION> for more details." };
+
+  { defaults with
+    name = "inspect_get_product_variant"; added = (1, 9, 13);
+    style = RString (RPlainString, "variant"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_product_variant";
+    shortdesc = "get product variant of inspected operating system";
+    longdesc = "\
+This returns the product variant of the inspected operating
+system.
+
+For Windows guests, this returns the contents of the Registry key
+C<HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion>
+C<InstallationType> which is usually a string such as
+C<Client> or C<Server> (other values are possible).  This
+can be used to distinguish consumer and enterprise versions
+of Windows that have the same version number (for example,
+Windows 7 and Windows 2008 Server are both version 6.1,
+but the former is C<Client> and the latter is C<Server>).
+
+For enterprise Linux guests, in future we intend this to return
+the product variant such as C<Desktop>, C<Server> and so on.  But
+this is not implemented at present.
+
+If the product variant could not be determined, then the
+string C<unknown> is returned.
+
+Please read L<guestfs(3)/INSPECTION> for more details.
+See also C<guestfs_inspect_get_product_name>,
+C<guestfs_inspect_get_major_version>." };
+
+  { defaults with
+    name = "inspect_get_windows_current_control_set"; added = (1, 9, 17);
+    style = RString (RPlainString, "controlset"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_windows_current_control_set";
+    shortdesc = "get Windows CurrentControlSet of inspected operating system";
+    longdesc = "\
+This returns the Windows CurrentControlSet of the inspected guest.
+The CurrentControlSet is a registry key name such as C<ControlSet001>.
+
+This call assumes that the guest is Windows and that the
+Registry could be examined by inspection.  If this is not
+the case then an error is returned.
+
+Please read L<guestfs(3)/INSPECTION> for more details." };
+
+  { defaults with
+    name = "inspect_get_windows_software_hive"; added = (1, 35, 26);
+    style = RString (RPlainString, "path"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_windows_software_hive";
+    shortdesc = "get the path of the Windows software hive";
+    longdesc = "\
+This returns the path to the hive (binary Windows Registry file)
+corresponding to HKLM\\SOFTWARE.
+
+This call assumes that the guest is Windows and that the guest
+has a software hive file with the right name.  If this is not the
+case then an error is returned.  This call does not check that the
+hive is a valid Windows Registry hive.
+
+You can use C<guestfs_hivex_open> to read or write to the hive.
+
+Please read L<guestfs(3)/INSPECTION> for more details." };
+
+  { defaults with
+    name = "inspect_get_windows_system_hive"; added = (1, 35, 26);
+    style = RString (RPlainString, "path"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_windows_system_hive";
+    shortdesc = "get the path of the Windows system hive";
+    longdesc = "\
+This returns the path to the hive (binary Windows Registry file)
+corresponding to HKLM\\SYSTEM.
+
+This call assumes that the guest is Windows and that the guest
+has a system hive file with the right name.  If this is not the
+case then an error is returned.  This call does not check that the
+hive is a valid Windows Registry hive.
+
+You can use C<guestfs_hivex_open> to read or write to the hive.
+
+Please read L<guestfs(3)/INSPECTION> for more details." };
+
+  { defaults with
     name = "inspect_get_mountpoints"; added = (1, 5, 3);
     style = RHashtable (RPlainString, RMountable, "mountpoints"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_mountpoints";
     shortdesc = "get mountpoints of inspected operating system";
     longdesc = "\
 This returns a hash of where we think the filesystems
@@ -364,6 +542,7 @@ See also C<guestfs_inspect_get_filesystems>." };
   { defaults with
     name = "inspect_get_filesystems"; added = (1, 5, 3);
     style = RStringList (RMountable, "filesystems"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_filesystems";
     shortdesc = "get filesystems associated with inspected operating system";
     longdesc = "\
 This returns a list of all the filesystems that we think
@@ -378,77 +557,43 @@ Please read L<guestfs(3)/INSPECTION> for more details.
 See also C<guestfs_inspect_get_mountpoints>." };
 
   { defaults with
-    name = "inspect_get_windows_systemroot"; added = (1, 5, 25);
-    style = RString (RPlainString, "systemroot"), [String (Mountable, "root")], [];
-    shortdesc = "get Windows systemroot of inspected operating system";
+    name = "inspect_get_drive_mappings"; added = (1, 9, 17);
+    style = RHashtable (RPlainString, RDevice, "drives"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_drive_mappings";
+    shortdesc = "get drive letter mappings";
     longdesc = "\
-This returns the Windows systemroot of the inspected guest.
-The systemroot is a directory path such as F</WINDOWS>.
+This call is useful for Windows which uses a primitive system
+of assigning drive letters (like F<C:\\>) to partitions.
+This inspection API examines the Windows Registry to find out
+how disks/partitions are mapped to drive letters, and returns
+a hash table as in the example below:
 
-This call assumes that the guest is Windows and that the
-systemroot could be determined by inspection.  If this is not
-the case then an error is returned.
+ C      =>     /dev/vda2
+ E      =>     /dev/vdb1
+ F      =>     /dev/vdc1
 
-Please read L<guestfs(3)/INSPECTION> for more details." };
+Note that keys are drive letters.  For Windows, the key is
+case insensitive and just contains the drive letter, without
+the customary colon separator character.
 
-  { defaults with
-    name = "inspect_get_roots"; added = (1, 7, 3);
-    style = RStringList (RMountable, "roots"), [], [];
-    shortdesc = "return list of operating systems found by last inspection";
-    longdesc = "\
-This function is a convenient way to get the list of root
-devices, as returned from a previous call to C<guestfs_inspect_os>,
-but without redoing the whole inspection process.
-
-This returns an empty list if either no root devices were
-found or the caller has not called C<guestfs_inspect_os>.
-
-Please read L<guestfs(3)/INSPECTION> for more details." };
-
-  { defaults with
-    name = "inspect_get_package_format"; added = (1, 7, 5);
-    style = RString (RPlainString, "packageformat"), [String (Mountable, "root")], [];
-    shortdesc = "get package format used by the operating system";
-    longdesc = "\
-This function and C<guestfs_inspect_get_package_management> return
-the package format and package management tool used by the
-inspected operating system.  For example for Fedora these
-functions would return C<rpm> (package format), and
-C<yum> or C<dnf> (package management).
+In future we may support other operating systems that also used drive
+letters, but the keys for those might not be case insensitive
+and might be longer than 1 character.  For example in OS-9,
+hard drives were named C<h0>, C<h1> etc.
 
-This returns the string C<unknown> if we could not determine the
-package format I<or> if the operating system does not have
-a real packaging system (eg. Windows).
-
-Possible strings include:
-C<rpm>, C<deb>, C<ebuild>, C<pisi>, C<pacman>, C<pkgsrc>, C<apk>,
-C<xbps>.
-Future versions of libguestfs may return other strings.
-
-Please read L<guestfs(3)/INSPECTION> for more details." };
-
-  { defaults with
-    name = "inspect_get_package_management"; added = (1, 7, 5);
-    style = RString (RPlainString, "packagemanagement"), [String (Mountable, "root")], [];
-    shortdesc = "get package management tool used by the operating system";
-    longdesc = "\
-C<guestfs_inspect_get_package_format> and this function return
-the package format and package management tool used by the
-inspected operating system.  For example for Fedora these
-functions would return C<rpm> (package format), and
-C<yum> or C<dnf> (package management).
+For Windows guests, currently only hard drive mappings are
+returned.  Removable disks (eg. DVD-ROMs) are ignored.
 
-This returns the string C<unknown> if we could not determine the
-package management tool I<or> if the operating system does not have
-a real packaging system (eg. Windows).
+For guests that do not use drive mappings, or if the drive mappings
+could not be determined, this returns an empty hash table.
 
-Possible strings include: C<yum>, C<dnf>, C<up2date>,
-C<apt> (for all Debian derivatives),
-C<portage>, C<pisi>, C<pacman>, C<urpmi>, C<zypper>, C<apk>, C<xbps>.
-Future versions of libguestfs may return other strings.
+Please read L<guestfs(3)/INSPECTION> for more details.
+See also C<guestfs_inspect_get_mountpoints>,
+C<guestfs_inspect_get_filesystems>." };
 
-Please read L<guestfs(3)/INSPECTION> for more details." };
+]
 
+let non_daemon_functions = [
   { defaults with
     name = "inspect_list_applications2"; added = (1, 19, 56);
     style = RStructList ("applications2", "application2"), [String (Mountable, "root")], [];
@@ -553,95 +698,6 @@ If unavailable this is returned as an empty string C<\"\">.
 Please read L<guestfs(3)/INSPECTION> for more details." };
 
   { defaults with
-    name = "inspect_get_hostname"; added = (1, 7, 9);
-    style = RString (RPlainString, "hostname"), [String (Mountable, "root")], [];
-    shortdesc = "get hostname of the operating system";
-    longdesc = "\
-This function returns the hostname of the operating system
-as found by inspection of the guest’s configuration files.
-
-If the hostname could not be determined, then the
-string C<unknown> is returned.
-
-Please read L<guestfs(3)/INSPECTION> for more details." };
-
-  { defaults with
-    name = "inspect_get_product_variant"; added = (1, 9, 13);
-    style = RString (RPlainString, "variant"), [String (Mountable, "root")], [];
-    shortdesc = "get product variant of inspected operating system";
-    longdesc = "\
-This returns the product variant of the inspected operating
-system.
-
-For Windows guests, this returns the contents of the Registry key
-C<HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion>
-C<InstallationType> which is usually a string such as
-C<Client> or C<Server> (other values are possible).  This
-can be used to distinguish consumer and enterprise versions
-of Windows that have the same version number (for example,
-Windows 7 and Windows 2008 Server are both version 6.1,
-but the former is C<Client> and the latter is C<Server>).
-
-For enterprise Linux guests, in future we intend this to return
-the product variant such as C<Desktop>, C<Server> and so on.  But
-this is not implemented at present.
-
-If the product variant could not be determined, then the
-string C<unknown> is returned.
-
-Please read L<guestfs(3)/INSPECTION> for more details.
-See also C<guestfs_inspect_get_product_name>,
-C<guestfs_inspect_get_major_version>." };
-
-  { defaults with
-    name = "inspect_get_windows_current_control_set"; added = (1, 9, 17);
-    style = RString (RPlainString, "controlset"), [String (Mountable, "root")], [];
-    shortdesc = "get Windows CurrentControlSet of inspected operating system";
-    longdesc = "\
-This returns the Windows CurrentControlSet of the inspected guest.
-The CurrentControlSet is a registry key name such as C<ControlSet001>.
-
-This call assumes that the guest is Windows and that the
-Registry could be examined by inspection.  If this is not
-the case then an error is returned.
-
-Please read L<guestfs(3)/INSPECTION> for more details." };
-
-  { defaults with
-    name = "inspect_get_drive_mappings"; added = (1, 9, 17);
-    style = RHashtable (RPlainString, RDevice, "drives"), [String (Mountable, "root")], [];
-    shortdesc = "get drive letter mappings";
-    longdesc = "\
-This call is useful for Windows which uses a primitive system
-of assigning drive letters (like F<C:\\>) to partitions.
-This inspection API examines the Windows Registry to find out
-how disks/partitions are mapped to drive letters, and returns
-a hash table as in the example below:
-
- C      =>     /dev/vda2
- E      =>     /dev/vdb1
- F      =>     /dev/vdc1
-
-Note that keys are drive letters.  For Windows, the key is
-case insensitive and just contains the drive letter, without
-the customary colon separator character.
-
-In future we may support other operating systems that also used drive
-letters, but the keys for those might not be case insensitive
-and might be longer than 1 character.  For example in OS-9,
-hard drives were named C<h0>, C<h1> etc.
-
-For Windows guests, currently only hard drive mappings are
-returned.  Removable disks (eg. DVD-ROMs) are ignored.
-
-For guests that do not use drive mappings, or if the drive mappings
-could not be determined, this returns an empty hash table.
-
-Please read L<guestfs(3)/INSPECTION> for more details.
-See also C<guestfs_inspect_get_mountpoints>,
-C<guestfs_inspect_get_filesystems>." };
-
-  { defaults with
     name = "inspect_get_icon"; added = (1, 11, 12);
     style = RBufferOut "icon", [String (Mountable, "root")],  [OBool "favicon"; OBool "highquality"];
     shortdesc = "get the icon corresponding to this operating system";
@@ -706,38 +762,4 @@ advice before using trademarks in applications.
 
 =back" };
 
-  { defaults with
-    name = "inspect_get_windows_software_hive"; added = (1, 35, 26);
-    style = RString (RPlainString, "path"), [String (Mountable, "root")], [];
-    shortdesc = "get the path of the Windows software hive";
-    longdesc = "\
-This returns the path to the hive (binary Windows Registry file)
-corresponding to HKLM\\SOFTWARE.
-
-This call assumes that the guest is Windows and that the guest
-has a software hive file with the right name.  If this is not the
-case then an error is returned.  This call does not check that the
-hive is a valid Windows Registry hive.
-
-You can use C<guestfs_hivex_open> to read or write to the hive.
-
-Please read L<guestfs(3)/INSPECTION> for more details." };
-
-  { defaults with
-    name = "inspect_get_windows_system_hive"; added = (1, 35, 26);
-    style = RString (RPlainString, "path"), [String (Mountable, "root")], [];
-    shortdesc = "get the path of the Windows system hive";
-    longdesc = "\
-This returns the path to the hive (binary Windows Registry file)
-corresponding to HKLM\\SYSTEM.
-
-This call assumes that the guest is Windows and that the guest
-has a system hive file with the right name.  If this is not the
-case then an error is returned.  This call does not check that the
-hive is a valid Windows Registry hive.
-
-You can use C<guestfs_hivex_open> to read or write to the hive.
-
-Please read L<guestfs(3)/INSPECTION> for more details." };
-
 ]
diff --git a/generator/actions_inspection.mli b/generator/actions_inspection.mli
index 327f7aa4f..06b8116c4 100644
--- a/generator/actions_inspection.mli
+++ b/generator/actions_inspection.mli
@@ -19,3 +19,4 @@
 (* Please read generator/README first. *)
 
 val non_daemon_functions : Types.action list
+val daemon_functions : Types.action list
diff --git a/generator/actions_inspection_deprecated.ml b/generator/actions_inspection_deprecated.ml
index 7c34cb1a8..c3e76ff7b 100644
--- a/generator/actions_inspection_deprecated.ml
+++ b/generator/actions_inspection_deprecated.ml
@@ -121,9 +121,13 @@ If unavailable this is returned as an empty string C<\"\">.
 
 Please read L<guestfs(3)/INSPECTION> for more details." };
 
+]
+
+let daemon_functions = [
   { defaults with
     name = "inspect_get_format"; added = (1, 9, 4);
     style = RString (RPlainString, "format"), [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_get_format";
     deprecated_by = Deprecated_no_replacement;
     shortdesc = "get format of inspected operating system";
     longdesc = "\
@@ -155,6 +159,7 @@ Please read L<guestfs(3)/INSPECTION> for more details." };
   { defaults with
     name = "inspect_is_live"; added = (1, 9, 4);
     style = RBool "live", [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_is_live";
     deprecated_by = Deprecated_no_replacement;
     shortdesc = "get live flag for install disk";
     longdesc = "\
@@ -165,6 +170,7 @@ Please read L<guestfs(3)/INSPECTION> for more details." };
   { defaults with
     name = "inspect_is_netinst"; added = (1, 9, 4);
     style = RBool "netinst", [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_is_netinst";
     deprecated_by = Deprecated_no_replacement;
     shortdesc = "get netinst (network installer) flag for install disk";
     longdesc = "\
@@ -175,6 +181,7 @@ Please read L<guestfs(3)/INSPECTION> for more details." };
   { defaults with
     name = "inspect_is_multipart"; added = (1, 9, 4);
     style = RBool "multipart", [String (Mountable, "root")], [];
+    impl = OCaml "Inspect.inspect_is_multipart";
     deprecated_by = Deprecated_no_replacement;
     shortdesc = "get multipart flag for install disk";
     longdesc = "\
diff --git a/generator/actions_inspection_deprecated.mli b/generator/actions_inspection_deprecated.mli
index 327f7aa4f..06b8116c4 100644
--- a/generator/actions_inspection_deprecated.mli
+++ b/generator/actions_inspection_deprecated.mli
@@ -19,3 +19,4 @@
 (* Please read generator/README first. *)
 
 val non_daemon_functions : Types.action list
+val daemon_functions : Types.action list
diff --git a/generator/daemon.ml b/generator/daemon.ml
index f20c87bea..b8d0a3a88 100644
--- a/generator/daemon.ml
+++ b/generator/daemon.ml
@@ -597,6 +597,30 @@ return_string_mountable (value retv)
   }
 }
 
+/* Implement RStringList (RMountable, _). */
+static char **
+return_string_mountable_list (value retv)
+{
+  CLEANUP_FREE_STRINGSBUF DECLARE_STRINGSBUF (ret);
+  value v;
+  char *m;
+
+  while (retv != Val_int (0)) {
+    v = Field (retv, 0);
+    m = return_string_mountable (v);
+    if (m == NULL)
+      return NULL;
+    if (add_string_nodup (&ret, m) == -1)
+      return NULL;
+    retv = Field (retv, 1);
+  }
+
+  if (end_stringsbuf (&ret) == -1)
+    return NULL;
+
+  return take_stringsbuf (&ret); /* caller frees */
+}
+
 /* Implement RHashtable (RPlainString, RPlainString, _). */
 static char **
 return_hashtable_string_string (value retv)
@@ -649,6 +673,34 @@ return_hashtable_mountable_string (value retv)
   return take_stringsbuf (&ret); /* caller frees */
 }
 
+/* Implement RHashtable (RPlainString, RMountable, _). */
+static char **
+return_hashtable_string_mountable (value retv)
+{
+  CLEANUP_FREE_STRINGSBUF DECLARE_STRINGSBUF (ret);
+  value sv, v, mv;
+  char *m;
+
+  while (retv != Val_int (0)) {
+    v = Field (retv, 0);        /* (string, Mountable.t) */
+    sv = Field (v, 0);          /* string */
+    if (add_string (&ret, String_val (sv)) == -1)
+      return NULL;
+    mv = Field (v, 1);          /* Mountable.t */
+    m = return_string_mountable (mv);
+    if (m == NULL)
+      return NULL;
+    if (add_string_nodup (&ret, m) == -1)
+      return NULL;
+    retv = Field (retv, 1);
+  }
+
+  if (end_stringsbuf (&ret) == -1)
+    return NULL;
+
+  return take_stringsbuf (&ret); /* caller frees */
+}
+
 ";
 
   (* Implement code for returning structs and struct lists. *)
@@ -889,9 +941,12 @@ return_hashtable_mountable_string (value retv)
        | RString (RMountable, _) ->
           pr "  char *ret = return_string_mountable (retv);\n";
           pr "  CAMLreturnT (char *, ret); /* caller frees */\n"
-       | RStringList _ ->
+       | RStringList ((RPlainString|RDevice), _) ->
           pr "  char **ret = return_string_list (retv);\n";
           pr "  CAMLreturnT (char **, ret); /* caller frees */\n"
+       | RStringList (RMountable, _) ->
+          pr "  char **ret = return_string_mountable_list (retv);\n";
+          pr "  CAMLreturnT (char **, ret); /* caller frees */\n"
        | RStruct (_, typ) ->
           pr "  guestfs_int_%s *ret =\n" typ;
           pr "    return_%s (retv);\n" typ;
@@ -902,12 +957,16 @@ return_hashtable_mountable_string (value retv)
           pr "    return_%s_list (retv);\n" typ;
           pr "  /* caller frees */\n";
           pr "  CAMLreturnT (guestfs_int_%s_list *, ret);\n" typ
-       | RHashtable (RPlainString, RPlainString, _) ->
+       | RHashtable (RPlainString, RPlainString, _)
+       | RHashtable (RPlainString, RDevice, _) ->
           pr "  char **ret = return_hashtable_string_string (retv);\n";
           pr "  CAMLreturnT (char **, ret); /* caller frees */\n"
        | RHashtable (RMountable, RPlainString, _) ->
           pr "  char **ret = return_hashtable_mountable_string (retv);\n";
           pr "  CAMLreturnT (char **, ret); /* caller frees */\n"
+       | RHashtable (RPlainString, RMountable, _) ->
+          pr "  char **ret = return_hashtable_string_mountable (retv);\n";
+          pr "  CAMLreturnT (char **, ret); /* caller frees */\n"
        | RHashtable _ -> assert false
        | RBufferOut _ -> assert false
       );
diff --git a/generator/proc_nr.ml b/generator/proc_nr.ml
index dec02f5fa..0a41f9c24 100644
--- a/generator/proc_nr.ml
+++ b/generator/proc_nr.ml
@@ -484,6 +484,29 @@ let proc_nr = [
 474, "internal_yara_scan";
 475, "file_architecture";
 476, "list_filesystems";
+477, "inspect_os";
+478, "inspect_get_roots";
+479, "inspect_get_format";
+480, "inspect_get_type";
+481, "inspect_get_distro";
+482, "inspect_get_package_format";
+483, "inspect_get_package_management";
+484, "inspect_get_product_name";
+485, "inspect_get_product_variant";
+486, "inspect_get_major_version";
+487, "inspect_get_minor_version";
+488, "inspect_get_arch";
+489, "inspect_get_hostname";
+490, "inspect_get_windows_systemroot";
+491, "inspect_get_windows_software_hive";
+492, "inspect_get_windows_system_hive";
+493, "inspect_get_windows_current_control_set";
+494, "inspect_is_live";
+495, "inspect_is_netinst";
+496, "inspect_is_multipart";
+497, "inspect_get_mountpoints";
+498, "inspect_get_filesystems";
+499, "inspect_get_drive_mappings";
 ]
 
 (* End of list.  If adding a new entry, add it at the end of the list
diff --git a/lib/MAX_PROC_NR b/lib/MAX_PROC_NR
index b86395733..761fcd3ac 100644
--- a/lib/MAX_PROC_NR
+++ b/lib/MAX_PROC_NR
@@ -1 +1 @@
-476
+499
diff --git a/lib/Makefile.am b/lib/Makefile.am
index 83b33ac8d..185828cc7 100644
--- a/lib/Makefile.am
+++ b/lib/Makefile.am
@@ -96,9 +96,6 @@ libguestfs_la_SOURCES = \
 	info.c \
 	inspect.c \
 	inspect-apps.c \
-	inspect-fs.c \
-	inspect-fs-unix.c \
-	inspect-fs-windows.c \
 	inspect-icon.c \
 	journal.c \
 	launch.c \
diff --git a/lib/guestfs-internal.h b/lib/guestfs-internal.h
index 81755177d..1fa621cf0 100644
--- a/lib/guestfs-internal.h
+++ b/lib/guestfs-internal.h
@@ -116,21 +116,6 @@
 
 /* Some limits on what the inspection code will read, for safety. */
 
-/* Small text configuration files.
- *
- * The upper limit is for general files that we grep or download.  The
- * largest such file is probably "txtsetup.sif" from Windows CDs
- * (~500K).  This number has to be larger than any legitimate file and
- * smaller than the protocol message size.
- *
- * The lower limit is for files parsed by Augeas on the daemon side,
- * where Augeas is running in reduced memory and can potentially
- * create a lot of metadata so we really need to be careful about
- * those.
- */
-#define MAX_SMALL_FILE_SIZE    (2 * 1000 * 1000)
-#define MAX_AUGEAS_FILE_SIZE        (100 * 1000)
-
 /* Maximum RPM or dpkg database we will download to /tmp.  RPM
  * 'Packages' database can get very large: 70 MB is roughly the
  * standard size for a new Fedora install, and after lots of package
@@ -470,12 +455,6 @@ struct guestfs_h {
   struct event *events;
   size_t nr_events;
 
-  /* Information gathered by inspect_os.  Must be freed by calling
-   * guestfs_int_free_inspect_info.
-   */
-  struct inspect_fs *fses;
-  size_t nr_fses;
-
   /* Private data area. */
   struct hash_table *pda;
   struct pda_entry *pda_next;
@@ -536,133 +515,6 @@ struct version {
   int v_micro;
 };
 
-/* Per-filesystem data stored for inspect_os. */
-enum inspect_os_format {
-  OS_FORMAT_UNKNOWN = 0,
-  OS_FORMAT_INSTALLED,
-  OS_FORMAT_INSTALLER,
-  /* in future: supplemental disks */
-};
-
-enum inspect_os_type {
-  OS_TYPE_UNKNOWN = 0,
-  OS_TYPE_LINUX,
-  OS_TYPE_WINDOWS,
-  OS_TYPE_FREEBSD,
-  OS_TYPE_NETBSD,
-  OS_TYPE_HURD,
-  OS_TYPE_DOS,
-  OS_TYPE_OPENBSD,
-  OS_TYPE_MINIX,
-};
-
-enum inspect_os_distro {
-  OS_DISTRO_UNKNOWN = 0,
-  OS_DISTRO_DEBIAN,
-  OS_DISTRO_FEDORA,
-  OS_DISTRO_REDHAT_BASED,
-  OS_DISTRO_RHEL,
-  OS_DISTRO_WINDOWS,
-  OS_DISTRO_PARDUS,
-  OS_DISTRO_ARCHLINUX,
-  OS_DISTRO_GENTOO,
-  OS_DISTRO_UBUNTU,
-  OS_DISTRO_MEEGO,
-  OS_DISTRO_LINUX_MINT,
-  OS_DISTRO_MANDRIVA,
-  OS_DISTRO_SLACKWARE,
-  OS_DISTRO_CENTOS,
-  OS_DISTRO_SCIENTIFIC_LINUX,
-  OS_DISTRO_TTYLINUX,
-  OS_DISTRO_MAGEIA,
-  OS_DISTRO_OPENSUSE,
-  OS_DISTRO_BUILDROOT,
-  OS_DISTRO_CIRROS,
-  OS_DISTRO_FREEDOS,
-  OS_DISTRO_SUSE_BASED,
-  OS_DISTRO_SLES,
-  OS_DISTRO_OPENBSD,
-  OS_DISTRO_ORACLE_LINUX,
-  OS_DISTRO_FREEBSD,
-  OS_DISTRO_NETBSD,
-  OS_DISTRO_COREOS,
-  OS_DISTRO_ALPINE_LINUX,
-  OS_DISTRO_ALTLINUX,
-  OS_DISTRO_FRUGALWARE,
-  OS_DISTRO_PLD_LINUX,
-  OS_DISTRO_VOID_LINUX,
-};
-
-enum inspect_os_package_format {
-  OS_PACKAGE_FORMAT_UNKNOWN = 0,
-  OS_PACKAGE_FORMAT_RPM,
-  OS_PACKAGE_FORMAT_DEB,
-  OS_PACKAGE_FORMAT_PACMAN,
-  OS_PACKAGE_FORMAT_EBUILD,
-  OS_PACKAGE_FORMAT_PISI,
-  OS_PACKAGE_FORMAT_PKGSRC,
-  OS_PACKAGE_FORMAT_APK,
-  OS_PACKAGE_FORMAT_XBPS,
-};
-
-enum inspect_os_package_management {
-  OS_PACKAGE_MANAGEMENT_UNKNOWN = 0,
-  OS_PACKAGE_MANAGEMENT_YUM,
-  OS_PACKAGE_MANAGEMENT_UP2DATE,
-  OS_PACKAGE_MANAGEMENT_APT,
-  OS_PACKAGE_MANAGEMENT_PACMAN,
-  OS_PACKAGE_MANAGEMENT_PORTAGE,
-  OS_PACKAGE_MANAGEMENT_PISI,
-  OS_PACKAGE_MANAGEMENT_URPMI,
-  OS_PACKAGE_MANAGEMENT_ZYPPER,
-  OS_PACKAGE_MANAGEMENT_DNF,
-  OS_PACKAGE_MANAGEMENT_APK,
-  OS_PACKAGE_MANAGEMENT_XBPS,
-};
-
-enum inspect_os_role {
-  OS_ROLE_UNKNOWN = 0,
-  OS_ROLE_ROOT,
-  OS_ROLE_USR,
-};
-
-/**
- * The inspection code maintains one of these structures per mountable
- * filesystem found in the disk image.  The struct (or structs) which
- * have the C<role> attribute set to C<OS_ROLE_ROOT> are inspection roots,
- * each corresponding to a single guest.  Note that a filesystem can be
- * shared between multiple guests.
- */
-struct inspect_fs {
-  enum inspect_os_role role;
-  char *mountable;
-  enum inspect_os_type type;
-  enum inspect_os_distro distro;
-  enum inspect_os_package_format package_format;
-  enum inspect_os_package_management package_management;
-  char *product_name;
-  char *product_variant;
-  struct version version;
-  char *arch;
-  char *hostname;
-  char *windows_systemroot;
-  char *windows_software_hive;
-  char *windows_system_hive;
-  char *windows_current_control_set;
-  char **drive_mappings;
-  enum inspect_os_format format;
-  int is_live_disk;
-  int is_netinst_disk;
-  int is_multipart_disk;
-  struct inspect_fstab_entry *fstab;
-  size_t nr_fstab;
-};
-
-struct inspect_fstab_entry {
-  char *mountable;
-  char *mountpoint;
-};
-
 struct guestfs_message_header;
 struct guestfs_message_error;
 struct guestfs_progress;
@@ -854,40 +706,7 @@ extern int guestfs_int_set_backend (guestfs_h *g, const char *method);
   } while (0)
 
 /* inspect.c */
-extern void guestfs_int_free_inspect_info (guestfs_h *g);
-extern char *guestfs_int_download_to_tmp (guestfs_h *g, struct inspect_fs *fs, const char *filename, const char *basename, uint64_t max_size);
-extern struct inspect_fs *guestfs_int_search_for_root (guestfs_h *g, const char *root);
-extern int guestfs_int_is_partition (guestfs_h *g, const char *partition);
-
-/* inspect-fs.c */
-extern int guestfs_int_is_file_nocase (guestfs_h *g, const char *);
-extern int guestfs_int_is_dir_nocase (guestfs_h *g, const char *);
-extern int guestfs_int_check_for_filesystem_on (guestfs_h *g,
-                                              const char *mountable);
-extern int guestfs_int_parse_unsigned_int (guestfs_h *g, const char *str);
-extern int guestfs_int_parse_unsigned_int_ignore_trailing (guestfs_h *g, const char *str);
-extern int guestfs_int_parse_major_minor (guestfs_h *g, struct inspect_fs *fs);
-extern char *guestfs_int_first_line_of_file (guestfs_h *g, const char *filename);
-extern int guestfs_int_first_egrep_of_file (guestfs_h *g, const char *filename, const char *eregex, int iflag, char **ret);
-extern void guestfs_int_check_package_format (guestfs_h *g, struct inspect_fs *fs);
-extern void guestfs_int_check_package_management (guestfs_h *g, struct inspect_fs *fs);
-extern void guestfs_int_merge_fs_inspections (guestfs_h *g, struct inspect_fs *dst, struct inspect_fs *src);
-
-/* inspect-fs-unix.c */
-extern int guestfs_int_check_linux_root (guestfs_h *g, struct inspect_fs *fs);
-extern int guestfs_int_check_linux_usr (guestfs_h *g, struct inspect_fs *fs);
-extern int guestfs_int_check_freebsd_root (guestfs_h *g, struct inspect_fs *fs);
-extern int guestfs_int_check_netbsd_root (guestfs_h *g, struct inspect_fs *fs);
-extern int guestfs_int_check_openbsd_root (guestfs_h *g, struct inspect_fs *fs);
-extern int guestfs_int_check_hurd_root (guestfs_h *g, struct inspect_fs *fs);
-extern int guestfs_int_check_minix_root (guestfs_h *g, struct inspect_fs *fs);
-extern int guestfs_int_check_coreos_root (guestfs_h *g, struct inspect_fs *fs);
-extern int guestfs_int_check_coreos_usr (guestfs_h *g, struct inspect_fs *fs);
-
-/* inspect-fs-windows.c */
-extern char *guestfs_int_case_sensitive_path_silently (guestfs_h *g, const char *);
-extern char * guestfs_int_get_windows_systemroot (guestfs_h *g);
-extern int guestfs_int_check_windows_root (guestfs_h *g, struct inspect_fs *fs, char *windows_systemroot);
+extern char *guestfs_int_download_to_tmp (guestfs_h *g, const char *filename, const char *basename, uint64_t max_size);
 
 /* dbdump.c */
 typedef int (*guestfs_int_db_dump_callback) (guestfs_h *g, const unsigned char *key, size_t keylen, const unsigned char *value, size_t valuelen, void *opaque);
@@ -969,6 +788,8 @@ struct rusage;
 extern int guestfs_int_wait4 (guestfs_h *g, pid_t pid, int *status, struct rusage *rusage, const char *errmsg);
 
 /* version.c */
+extern int guestfs_int_parse_unsigned_int (guestfs_h *g, const char *str);
+extern int guestfs_int_parse_unsigned_int_ignore_trailing (guestfs_h *g, const char *str);
 extern void guestfs_int_version_from_libvirt (struct version *v, int vernum);
 extern void guestfs_int_version_from_values (struct version *v, int maj, int min, int mic);
 extern int guestfs_int_version_from_x_y (guestfs_h *g, struct version *v, const char *str);
diff --git a/lib/handle.c b/lib/handle.c
index 91f5f755d..57fda24b1 100644
--- a/lib/handle.c
+++ b/lib/handle.c
@@ -369,7 +369,6 @@ guestfs_close (guestfs_h *g)
   guestfs_int_free_fuse (g);
 #endif
 
-  guestfs_int_free_inspect_info (g);
   guestfs_int_free_drives (g);
 
   for (hp = g->hv_params; hp; hp = hp_next) {
diff --git a/lib/inspect-apps.c b/lib/inspect-apps.c
index 25192340c..2a0e2beba 100644
--- a/lib/inspect-apps.c
+++ b/lib/inspect-apps.c
@@ -46,12 +46,12 @@
 #include "structs-cleanups.h"
 
 #ifdef DB_DUMP
-static struct guestfs_application2_list *list_applications_rpm (guestfs_h *g, struct inspect_fs *fs);
+static struct guestfs_application2_list *list_applications_rpm (guestfs_h *g, const char *root);
 #endif
-static struct guestfs_application2_list *list_applications_deb (guestfs_h *g, struct inspect_fs *fs);
-static struct guestfs_application2_list *list_applications_pacman (guestfs_h *g, struct inspect_fs *fs);
-static struct guestfs_application2_list *list_applications_apk (guestfs_h *g, struct inspect_fs *fs);
-static struct guestfs_application2_list *list_applications_windows (guestfs_h *g, struct inspect_fs *fs);
+static struct guestfs_application2_list *list_applications_deb (guestfs_h *g, const char *root);
+static struct guestfs_application2_list *list_applications_pacman (guestfs_h *g, const char *root);
+static struct guestfs_application2_list *list_applications_apk (guestfs_h *g, const char *root);
+static struct guestfs_application2_list *list_applications_windows (guestfs_h *g, const char *root);
 static void add_application (guestfs_h *g, struct guestfs_application2_list *, const char *name, const char *display_name, int32_t epoch, const char *version, const char *release, const char *arch, const char *install_path, const char *publisher, const char *url, const char *source, const char *summary, const char *description);
 static void sort_applications (struct guestfs_application2_list *);
 
@@ -109,68 +109,45 @@ struct guestfs_application2_list *
 guestfs_impl_inspect_list_applications2 (guestfs_h *g, const char *root)
 {
   struct guestfs_application2_list *ret = NULL;
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
+  CLEANUP_FREE char *type = NULL;
+  CLEANUP_FREE char *package_format = NULL;
+
+  type = guestfs_inspect_get_type (g, root);
+  if (!type)
+    return NULL;
+  package_format = guestfs_inspect_get_package_format (g, root);
+  if (!package_format)
     return NULL;
 
-  /* Presently we can only list applications for installed disks.  It
-   * is possible in future to get lists of packages from installers.
-   */
-  if (fs->format == OS_FORMAT_INSTALLED) {
-    switch (fs->type) {
-    case OS_TYPE_LINUX:
-    case OS_TYPE_HURD:
-      switch (fs->package_format) {
-      case OS_PACKAGE_FORMAT_RPM:
+  if (STREQ (type, "linux") || STREQ (type, "hurd")) {
+    if (STREQ (package_format, "rpm")) {
 #ifdef DB_DUMP
-        ret = list_applications_rpm (g, fs);
-        if (ret == NULL)
-          return NULL;
+      ret = list_applications_rpm (g, root);
+      if (ret == NULL)
+        return NULL;
 #endif
-        break;
-
-      case OS_PACKAGE_FORMAT_DEB:
-        ret = list_applications_deb (g, fs);
-        if (ret == NULL)
-          return NULL;
-        break;
-
-      case OS_PACKAGE_FORMAT_PACMAN:
-	ret = list_applications_pacman (g, fs);
-	if (ret == NULL)
-	  return NULL;
-	break;
-
-      case OS_PACKAGE_FORMAT_APK:
-        ret = list_applications_apk (g, fs);
-        if (ret == NULL)
-          return NULL;
-        break;
-
-      case OS_PACKAGE_FORMAT_EBUILD:
-      case OS_PACKAGE_FORMAT_PISI:
-      case OS_PACKAGE_FORMAT_PKGSRC:
-      case OS_PACKAGE_FORMAT_XBPS:
-      case OS_PACKAGE_FORMAT_UNKNOWN:
-        ; /* nothing */
-      }
-      break;
-
-    case OS_TYPE_WINDOWS:
-      ret = list_applications_windows (g, fs);
+    }
+    else if (STREQ (package_format, "deb")) {
+      ret = list_applications_deb (g, root);
+      if (ret == NULL)
+        return NULL;
+    }
+    else if (STREQ (package_format, "pacman")) {
+      ret = list_applications_pacman (g, root);
+      if (ret == NULL)
+        return NULL;
+    }
+    else if (STREQ (package_format, "apk")) {
+      ret = list_applications_apk (g, root);
       if (ret == NULL)
         return NULL;
-      break;
-
-    case OS_TYPE_FREEBSD:
-    case OS_TYPE_MINIX:
-    case OS_TYPE_NETBSD:
-    case OS_TYPE_DOS:
-    case OS_TYPE_OPENBSD:
-    case OS_TYPE_UNKNOWN:
-      ; /* nothing */
     }
   }
+  else if (STREQ (type, "windows")) {
+    ret = list_applications_windows (g, root);
+    if (ret == NULL)
+      return NULL;
+  }
 
   if (ret == NULL) {
     /* Don't know how to do inspection.  Not an error, return an
@@ -386,20 +363,20 @@ read_package (guestfs_h *g,
 #pragma GCC diagnostic pop
 
 static struct guestfs_application2_list *
-list_applications_rpm (guestfs_h *g, struct inspect_fs *fs)
+list_applications_rpm (guestfs_h *g, const char *root)
 {
   CLEANUP_FREE char *Name = NULL, *Packages = NULL;
   struct rpm_names_list list = { .names = NULL, .len = 0 };
   struct guestfs_application2_list *apps = NULL;
   struct read_package_data data;
 
-  Name = guestfs_int_download_to_tmp (g, fs,
+  Name = guestfs_int_download_to_tmp (g,
 				      "/var/lib/rpm/Name", "rpm_Name",
 				      MAX_PKG_DB_SIZE);
   if (Name == NULL)
     goto error;
 
-  Packages = guestfs_int_download_to_tmp (g, fs,
+  Packages = guestfs_int_download_to_tmp (g,
 					  "/var/lib/rpm/Packages", "rpm_Packages",
 					  MAX_PKG_DB_SIZE);
   if (Packages == NULL)
@@ -437,7 +414,7 @@ list_applications_rpm (guestfs_h *g, struct inspect_fs *fs)
 #endif /* defined DB_DUMP */
 
 static struct guestfs_application2_list *
-list_applications_deb (guestfs_h *g, struct inspect_fs *fs)
+list_applications_deb (guestfs_h *g, const char *root)
 {
   CLEANUP_FREE char *status = NULL;
   struct guestfs_application2_list *apps = NULL, *ret = NULL;
@@ -452,7 +429,7 @@ list_applications_deb (guestfs_h *g, struct inspect_fs *fs)
   char **continuation_field = NULL;
   size_t continuation_field_len = 0;
 
-  status = guestfs_int_download_to_tmp (g, fs, "/var/lib/dpkg/status", "status",
+  status = guestfs_int_download_to_tmp (g, "/var/lib/dpkg/status", "status",
 					MAX_PKG_DB_SIZE);
   if (status == NULL)
     return NULL;
@@ -594,7 +571,7 @@ list_applications_deb (guestfs_h *g, struct inspect_fs *fs)
 }
 
 static struct guestfs_application2_list *
-list_applications_pacman (guestfs_h *g, struct inspect_fs *fs)
+list_applications_pacman (guestfs_h *g, const char *root)
 {
   CLEANUP_FREE char *desc_file = NULL, *fname = NULL, *line = NULL;
   CLEANUP_FREE_DIRENT_LIST struct guestfs_dirent_list *local_db = NULL;
@@ -628,7 +605,7 @@ list_applications_pacman (guestfs_h *g, struct inspect_fs *fs)
     fname = safe_malloc (g, strlen (curr->name) + path_len + 1);
     sprintf (fname, "/var/lib/pacman/local/%s/desc", curr->name);
     free (desc_file);
-    desc_file = guestfs_int_download_to_tmp (g, fs, fname, curr->name, 8192);
+    desc_file = guestfs_int_download_to_tmp (g, fname, curr->name, 8192);
 
     /* The desc files are small (4K). If the desc file does not exist or is
      * larger than the 8K limit we've used, the database is probably corrupted,
@@ -725,7 +702,7 @@ list_applications_pacman (guestfs_h *g, struct inspect_fs *fs)
 }
 
 static struct guestfs_application2_list *
-list_applications_apk (guestfs_h *g, struct inspect_fs *fs)
+list_applications_apk (guestfs_h *g, const char *root)
 {
   CLEANUP_FREE char *installed = NULL, *line = NULL;
   struct guestfs_application2_list *apps = NULL, *ret = NULL;
@@ -736,7 +713,7 @@ list_applications_apk (guestfs_h *g, struct inspect_fs *fs)
   CLEANUP_FREE char *name = NULL, *version = NULL, *release = NULL, *arch = NULL,
     *url = NULL, *description = NULL;
 
-  installed = guestfs_int_download_to_tmp (g, fs, "/lib/apk/db/installed",
+  installed = guestfs_int_download_to_tmp (g, "/lib/apk/db/installed",
                                            "installed", MAX_PKG_DB_SIZE);
   if (installed == NULL)
     return NULL;
@@ -843,14 +820,15 @@ list_applications_apk (guestfs_h *g, struct inspect_fs *fs)
 static void list_applications_windows_from_path (guestfs_h *g, struct guestfs_application2_list *apps, const char **path, size_t path_len);
 
 static struct guestfs_application2_list *
-list_applications_windows (guestfs_h *g, struct inspect_fs *fs)
+list_applications_windows (guestfs_h *g, const char *root)
 {
   struct guestfs_application2_list *ret = NULL;
-
-  if (!fs->windows_software_hive)
+  CLEANUP_FREE char *software_hive =
+    guestfs_inspect_get_windows_software_hive (g, root);
+  if (!software_hive)
     return NULL;
 
-  if (guestfs_hivex_open (g, fs->windows_software_hive,
+  if (guestfs_hivex_open (g, software_hive,
                           GUESTFS_HIVEX_OPEN_VERBOSE, g->verbose,
                           GUESTFS_HIVEX_OPEN_UNSAFE, 1,
                           -1) == -1)
diff --git a/lib/inspect-fs-unix.c b/lib/inspect-fs-unix.c
deleted file mode 100644
index 9b6bfbf38..000000000
--- a/lib/inspect-fs-unix.c
+++ /dev/null
@@ -1,2158 +0,0 @@
-/* libguestfs
- * Copyright (C) 2010-2012 Red Hat Inc.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; 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 <inttypes.h>
-#include <unistd.h>
-#include <string.h>
-#include <libintl.h>
-#include <errno.h>
-
-#ifdef HAVE_ENDIAN_H
-#include <endian.h>
-#endif
-
-#include "c-ctype.h"
-#include "ignore-value.h"
-#include "hash-pjw.h"
-
-#include "guestfs.h"
-#include "guestfs-internal.h"
-
-COMPILE_REGEXP (re_fedora, "Fedora release (\\d+)", 0)
-COMPILE_REGEXP (re_rhel_old, "Red Hat.*release (\\d+).*Update (\\d+)", 0)
-COMPILE_REGEXP (re_rhel, "Red Hat.*release (\\d+)\\.(\\d+)", 0)
-COMPILE_REGEXP (re_rhel_no_minor, "Red Hat.*release (\\d+)", 0)
-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)
-COMPILE_REGEXP (re_scientific_linux_old,
-                "Scientific Linux.*release (\\d+).*Update (\\d+)", 0)
-COMPILE_REGEXP (re_scientific_linux,
-                "Scientific Linux.*release (\\d+)\\.(\\d+)", 0)
-COMPILE_REGEXP (re_scientific_linux_no_minor,
-                "Scientific Linux.*release (\\d+)", 0)
-COMPILE_REGEXP (re_oracle_linux_old,
-                "Oracle Linux.*release (\\d+).*Update (\\d+)", 0)
-COMPILE_REGEXP (re_oracle_linux,
-                "Oracle Linux.*release (\\d+)\\.(\\d+)", 0)
-COMPILE_REGEXP (re_oracle_linux_no_minor, "Oracle Linux.*release (\\d+)", 0)
-COMPILE_REGEXP (re_xdev, "^/dev/(h|s|v|xv)d([a-z]+)(\\d*)$", 0)
-COMPILE_REGEXP (re_cciss, "^/dev/(cciss/c\\d+d\\d+)(?:p(\\d+))?$", 0)
-COMPILE_REGEXP (re_mdN, "^(/dev/md\\d+)$", 0)
-COMPILE_REGEXP (re_freebsd_mbr,
-                "^/dev/(ada{0,1}|vtbd)(\\d+)s(\\d+)([a-z])$", 0)
-COMPILE_REGEXP (re_freebsd_gpt, "^/dev/(ada{0,1}|vtbd)(\\d+)p(\\d+)$", 0)
-COMPILE_REGEXP (re_diskbyid, "^/dev/disk/by-id/.*-part(\\d+)$", 0)
-COMPILE_REGEXP (re_netbsd, "^NetBSD (\\d+)\\.(\\d+)", 0)
-COMPILE_REGEXP (re_opensuse, "^(openSUSE|SuSE Linux|SUSE LINUX) ", 0)
-COMPILE_REGEXP (re_sles, "^SUSE (Linux|LINUX) Enterprise ", 0)
-COMPILE_REGEXP (re_nld, "^Novell Linux Desktop ", 0)
-COMPILE_REGEXP (re_opensuse_version, "^VERSION = (\\d+)\\.(\\d+)", 0)
-COMPILE_REGEXP (re_sles_version, "^VERSION = (\\d+)", 0)
-COMPILE_REGEXP (re_sles_patchlevel, "^PATCHLEVEL = (\\d+)", 0)
-COMPILE_REGEXP (re_minix, "^(\\d+)\\.(\\d+)(\\.(\\d+))?", 0)
-COMPILE_REGEXP (re_hurd_dev, "^/dev/(h)d(\\d+)s(\\d+)$", 0)
-COMPILE_REGEXP (re_openbsd, "^OpenBSD (\\d+|\\?)\\.(\\d+|\\?)", 0)
-COMPILE_REGEXP (re_openbsd_duid, "^[0-9a-f]{16}\\.[a-z]", 0)
-COMPILE_REGEXP (re_openbsd_dev, "^/dev/(s|w)d([0-9])([a-z])$", 0)
-COMPILE_REGEXP (re_netbsd_dev, "^/dev/(l|s)d([0-9])([a-z])$", 0)
-COMPILE_REGEXP (re_altlinux, " (?:(\\d+)(?:\\.(\\d+)(?:[\\.\\d]+)?)?)\\s+\\((?:[^)]+)\\)$", 0)
-COMPILE_REGEXP (re_frugalware, "Frugalware (\\d+)\\.(\\d+)", 0)
-COMPILE_REGEXP (re_pldlinux, "(\\d+)\\.(\\d+) PLD Linux", 0)
-
-static void check_architecture (guestfs_h *g, struct inspect_fs *fs);
-static int check_hostname_unix (guestfs_h *g, struct inspect_fs *fs);
-static int check_hostname_redhat (guestfs_h *g, struct inspect_fs *fs);
-static int check_hostname_freebsd (guestfs_h *g, struct inspect_fs *fs);
-static int check_fstab (guestfs_h *g, struct inspect_fs *fs);
-static void add_fstab_entry (guestfs_h *g, struct inspect_fs *fs,
-                             const char *mountable, const char *mp);
-static char *resolve_fstab_device (guestfs_h *g, const char *spec,
-                                   Hash_table *md_map,
-                                   enum inspect_os_type os_type);
-static int inspect_with_augeas (guestfs_h *g, struct inspect_fs *fs, const char **configfiles, int (*f) (guestfs_h *, struct inspect_fs *));
-static void canonical_mountpoint (char *mp);
-
-/* Hash structure for uuid->path lookups */
-typedef struct md_uuid {
-  uint32_t uuid[4];
-  char *path;
-} md_uuid;
-
-static size_t uuid_hash(const void *x, size_t table_size);
-static bool uuid_cmp(const void *x, const void *y);
-static void md_uuid_free(void *x);
-
-static int parse_uuid(const char *str, uint32_t *uuid);
-
-/* Hash structure for path(mdadm)->path(appliance) lookup */
-typedef struct {
-  char *mdadm;
-  char *app;
-} mdadm_app;
-
-static size_t mdadm_app_hash(const void *x, size_t table_size);
-static bool mdadm_app_cmp(const void *x, const void *y);
-static void mdadm_app_free(void *x);
-
-static ssize_t map_app_md_devices (guestfs_h *g, Hash_table **map);
-static int map_md_devices(guestfs_h *g, Hash_table **map);
-
-/* Set fs->product_name to the first line of the release file. */
-static int
-parse_release_file (guestfs_h *g, struct inspect_fs *fs,
-                    const char *release_filename)
-{
-  fs->product_name = guestfs_int_first_line_of_file (g, release_filename);
-  if (fs->product_name == NULL)
-    return -1;
-  if (STREQ (fs->product_name, "")) {
-    free (fs->product_name);
-    fs->product_name = NULL;
-    error (g, _("release file %s is empty or malformed"), release_filename);
-    return -1;
-  }
-  return 0;
-}
-
-/* Parse a os-release file.
- *
- * Only few fields are parsed, falling back to the usual detection if we
- * cannot read all of them.
- *
- * For the format of os-release, see also:
- * http://www.freedesktop.org/software/systemd/man/os-release.html
- */
-static int
-parse_os_release (guestfs_h *g, struct inspect_fs *fs, const char *filename)
-{
-  int64_t size;
-  CLEANUP_FREE_STRING_LIST char **lines = NULL;
-  size_t i;
-  enum inspect_os_distro distro = OS_DISTRO_UNKNOWN;
-  CLEANUP_FREE char *product_name = NULL;
-  struct version version;
-  guestfs_int_version_from_values (&version, -1, -1, 0);
-
-  /* Don't trust guestfs_read_lines not to break with very large files.
-   * Check the file size is something reasonable first.
-   */
-  size = guestfs_filesize (g, filename);
-  if (size == -1)
-    /* guestfs_filesize failed and has already set error in handle */
-    return -1;
-  if (size > MAX_SMALL_FILE_SIZE) {
-    error (g, _("size of %s is unreasonably large (%" PRIi64 " bytes)"),
-           filename, size);
-    return -1;
-  }
-
-  lines = guestfs_read_lines (g, filename);
-  if (lines == NULL)
-    return -1;
-
-  for (i = 0; lines[i] != NULL; ++i) {
-    const char *line = lines[i];
-    const char *value;
-    size_t value_len;
-
-    if (line[0] == '#')
-      continue;
-
-    value = strchr (line, '=');
-    if (value == NULL)
-      continue;
-
-    ++value;
-    value_len = strlen (line) - (value - line);
-    if (value_len > 1 && value[0] == '"' && value[value_len-1] == '"') {
-      ++value;
-      value_len -= 2;
-    }
-
-#define VALUE_IS(a) STREQLEN(value, a, value_len)
-    if (STRPREFIX (line, "ID=")) {
-      if (VALUE_IS ("alpine"))
-        distro = OS_DISTRO_ALPINE_LINUX;
-      else if (VALUE_IS ("altlinux"))
-        distro = OS_DISTRO_ALTLINUX;
-      else if (VALUE_IS ("arch"))
-        distro = OS_DISTRO_ARCHLINUX;
-      else if (VALUE_IS ("centos"))
-        distro = OS_DISTRO_CENTOS;
-      else if (VALUE_IS ("coreos"))
-        distro = OS_DISTRO_COREOS;
-      else if (VALUE_IS ("debian"))
-        distro = OS_DISTRO_DEBIAN;
-      else if (VALUE_IS ("fedora"))
-        distro = OS_DISTRO_FEDORA;
-      else if (VALUE_IS ("frugalware"))
-        distro = OS_DISTRO_FRUGALWARE;
-      else if (VALUE_IS ("mageia"))
-        distro = OS_DISTRO_MAGEIA;
-      else if (VALUE_IS ("opensuse"))
-        distro = OS_DISTRO_OPENSUSE;
-      else if (VALUE_IS ("pld"))
-        distro = OS_DISTRO_PLD_LINUX;
-      else if (VALUE_IS ("rhel"))
-        distro = OS_DISTRO_RHEL;
-      else if (VALUE_IS ("sles") || VALUE_IS ("sled"))
-        distro = OS_DISTRO_SLES;
-      else if (VALUE_IS ("ubuntu"))
-        distro = OS_DISTRO_UBUNTU;
-      else if (VALUE_IS ("void"))
-        distro = OS_DISTRO_VOID_LINUX;
-    } else if (STRPREFIX (line, "PRETTY_NAME=")) {
-      free (product_name);
-      product_name = safe_strndup (g, value, value_len);
-    } else if (STRPREFIX (line, "VERSION_ID=")) {
-      CLEANUP_FREE char *buf =
-        safe_asprintf (g, "%.*s", (int) value_len, value);
-      if (guestfs_int_version_from_x_y_or_x (g, &version, buf) == -1)
-        return -1;
-    }
-#undef VALUE_IS
-  }
-
-  /* If we haven't got all the fields, exit right away. */
-  if (distro == OS_DISTRO_UNKNOWN || product_name == NULL)
-    return 0;
-
-  if (version.v_major == -1 || version.v_minor == -1) {
-    /* Void Linux has no VERSION_ID (yet), but since it's a rolling
-     * distro and has no other version/release-like file. */
-    if (distro == OS_DISTRO_VOID_LINUX)
-      version_init_null (&version);
-    else
-      return 0;
-  }
-
-  /* Apparently, os-release in Debian and CentOS does not provide the full
-   * version number in VERSION_ID, but just the "major" part of it.
-   * Hence, if version.v_minor is 0, act as there was no information in
-   * os-release, which will continue the inspection using the release files
-   * as done previously.
-   */
-  if ((distro == OS_DISTRO_DEBIAN || distro == OS_DISTRO_CENTOS) &&
-      version.v_minor == 0)
-    return 0;
-
-  /* We got everything, so set the fields and report the inspection
-   * was successful.
-   */
-  fs->distro = distro;
-  fs->product_name = product_name;
-  product_name = NULL;
-  fs->version = version;
-
-  return 1;
-}
-
-/* Ubuntu has /etc/lsb-release containing:
- *   DISTRIB_ID=Ubuntu                                # Distro
- *   DISTRIB_RELEASE=10.04                            # Version
- *   DISTRIB_CODENAME=lucid
- *   DISTRIB_DESCRIPTION="Ubuntu 10.04.1 LTS"         # Product name
- *
- * [Ubuntu-derived ...] Linux Mint was found to have this:
- *   DISTRIB_ID=LinuxMint
- *   DISTRIB_RELEASE=10
- *   DISTRIB_CODENAME=julia
- *   DISTRIB_DESCRIPTION="Linux Mint 10 Julia"
- * Linux Mint also has /etc/linuxmint/info with more information,
- * but we can use the LSB file.
- *
- * Mandriva has:
- *   LSB_VERSION=lsb-4.0-amd64:lsb-4.0-noarch
- *   DISTRIB_ID=MandrivaLinux
- *   DISTRIB_RELEASE=2010.1
- *   DISTRIB_CODENAME=Henry_Farman
- *   DISTRIB_DESCRIPTION="Mandriva Linux 2010.1"
- * Mandriva also has a normal release file called /etc/mandriva-release.
- *
- * CoreOS has a /etc/lsb-release link to /usr/share/coreos/lsb-release containing:
- *   DISTRIB_ID=CoreOS
- *   DISTRIB_RELEASE=647.0.0
- *   DISTRIB_CODENAME="Red Dog"
- *   DISTRIB_DESCRIPTION="CoreOS 647.0.0"
- */
-static int
-parse_lsb_release (guestfs_h *g, struct inspect_fs *fs, const char *filename)
-{
-  int64_t size;
-  CLEANUP_FREE_STRING_LIST char **lines = NULL;
-  size_t i;
-  int r = 0;
-
-  /* Don't trust guestfs_head_n not to break with very large files.
-   * Check the file size is something reasonable first.
-   */
-  size = guestfs_filesize (g, filename);
-  if (size == -1)
-    /* guestfs_filesize failed and has already set error in handle */
-    return -1;
-  if (size > MAX_SMALL_FILE_SIZE) {
-    error (g, _("size of %s is unreasonably large (%" PRIi64 " bytes)"),
-           filename, size);
-    return -1;
-  }
-
-  lines = guestfs_head_n (g, 10, filename);
-  if (lines == NULL)
-    return -1;
-
-  for (i = 0; lines[i] != NULL; ++i) {
-    if (fs->distro == 0 &&
-        STREQ (lines[i], "DISTRIB_ID=Ubuntu")) {
-      fs->distro = OS_DISTRO_UBUNTU;
-      r = 1;
-    }
-    else if (fs->distro == 0 &&
-             STREQ (lines[i], "DISTRIB_ID=LinuxMint")) {
-      fs->distro = OS_DISTRO_LINUX_MINT;
-      r = 1;
-    }
-    else if (fs->distro == 0 &&
-             STREQ (lines[i], "DISTRIB_ID=MandrivaLinux")) {
-      fs->distro = OS_DISTRO_MANDRIVA;
-      r = 1;
-    }
-    else if (fs->distro == 0 &&
-             STREQ (lines[i], "DISTRIB_ID=\"Mageia\"")) {
-      fs->distro = OS_DISTRO_MAGEIA;
-      r = 1;
-    }
-    else if (fs->distro == 0 &&
-             STREQ (lines[i], "DISTRIB_ID=CoreOS")) {
-      fs->distro = OS_DISTRO_COREOS;
-      r = 1;
-    }
-    else if (STRPREFIX (lines[i], "DISTRIB_RELEASE=")) {
-      if (guestfs_int_version_from_x_y_or_x (g, &fs->version, &lines[i][16]) == -1)
-        return -1;
-    }
-    else if (fs->product_name == NULL &&
-             (STRPREFIX (lines[i], "DISTRIB_DESCRIPTION=\"") ||
-              STRPREFIX (lines[i], "DISTRIB_DESCRIPTION='"))) {
-      const size_t len = strlen (lines[i]) - 21 - 1;
-      fs->product_name = safe_strndup (g, &lines[i][21], len);
-      r = 1;
-    }
-    else if (fs->product_name == NULL &&
-             STRPREFIX (lines[i], "DISTRIB_DESCRIPTION=")) {
-      const size_t len = strlen (lines[i]) - 20;
-      fs->product_name = safe_strndup (g, &lines[i][20], len);
-      r = 1;
-    }
-  }
-
-  /* The unnecessary construct in the next line is required to
-   * workaround -Wstrict-overflow warning in gcc 4.5.1.
-   */
-  return r ? 1 : 0;
-}
-
-static int
-parse_suse_release (guestfs_h *g, struct inspect_fs *fs, const char *filename)
-{
-  int64_t size;
-  CLEANUP_FREE_STRING_LIST char **lines = NULL;
-  int r = -1;
-
-  /* Don't trust guestfs_head_n not to break with very large files.
-   * Check the file size is something reasonable first.
-   */
-  size = guestfs_filesize (g, filename);
-  if (size == -1)
-    /* guestfs_filesize failed and has already set error in handle */
-    return -1;
-  if (size > MAX_SMALL_FILE_SIZE) {
-    error (g, _("size of %s is unreasonably large (%" PRIi64 " bytes)"),
-           filename, size);
-    return -1;
-  }
-
-  lines = guestfs_head_n (g, 10, filename);
-  if (lines == NULL)
-    return -1;
-
-  if (lines[0] == NULL)
-    goto out;
-
-  /* First line is dist release name */
-  fs->product_name = safe_strdup (g, lines[0]);
-
-  /* Match SLES first because openSuSE regex overlaps some SLES release strings */
-  if (match (g, fs->product_name, re_sles) || match (g, fs->product_name, re_nld)) {
-    char *major, *minor;
-
-    fs->distro = OS_DISTRO_SLES;
-
-    /* Second line contains version string */
-    if (lines[1] == NULL)
-      goto out;
-    major = match1 (g, lines[1], re_sles_version);
-    if (major == NULL)
-      goto out;
-    fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-    free (major);
-    if (fs->version.v_major == -1)
-      goto out;
-
-    /* Third line contains service pack string */
-    if (lines[2] == NULL)
-      goto out;
-    minor = match1 (g, lines[2], re_sles_patchlevel);
-    if (minor == NULL)
-      goto out;
-    fs->version.v_minor = guestfs_int_parse_unsigned_int (g, minor);
-    free (minor);
-    if (fs->version.v_minor == -1)
-      goto out;
-  }
-  else if (match (g, fs->product_name, re_opensuse)) {
-    fs->distro = OS_DISTRO_OPENSUSE;
-
-    /* Second line contains version string */
-    if (lines[1] == NULL)
-      goto out;
-    if (guestfs_int_version_from_x_y_re (g, &fs->version, lines[1],
-                                         re_opensuse_version) == -1)
-      goto out;
-  }
-
-  r = 0;
-
- out:
-  return r;
-}
-
-/* The currently mounted device is known to be a Linux root.  Try to
- * determine from this the distro, version, etc.  Also parse
- * /etc/fstab to determine the arrangement of mountpoints and
- * associated devices.
- */
-int
-guestfs_int_check_linux_root (guestfs_h *g, struct inspect_fs *fs)
-{
-  int r;
-  char *major, *minor;
-
-  fs->type = OS_TYPE_LINUX;
-
-  if (guestfs_is_file_opts (g, "/etc/os-release",
-                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    r = parse_os_release (g, fs, "/etc/os-release");
-    if (r == -1)        /* error */
-      return -1;
-    if (r == 1)         /* ok - detected the release from this file */
-      goto skip_release_checks;
-  }
-
-  if (guestfs_is_file_opts (g, "/etc/lsb-release",
-                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    r = parse_lsb_release (g, fs, "/etc/lsb-release");
-    if (r == -1)        /* error */
-      return -1;
-    if (r == 1)         /* ok - detected the release from this file */
-      goto skip_release_checks;
-  }
-
-  /* RHEL-based distros include a "/etc/redhat-release" file, hence their
-   * checks need to be performed before the Red-Hat one.
-   */
-  if (guestfs_is_file_opts (g, "/etc/oracle-release",
-                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-
-    fs->distro = OS_DISTRO_ORACLE_LINUX;
-
-    if (parse_release_file (g, fs, "/etc/oracle-release") == -1)
-      return -1;
-
-    if (match2 (g, fs->product_name, re_oracle_linux_old, &major, &minor) ||
-        match2 (g, fs->product_name, re_oracle_linux, &major, &minor)) {
-      fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-      free (major);
-      if (fs->version.v_major == -1) {
-        free (minor);
-        return -1;
-      }
-      fs->version.v_minor = guestfs_int_parse_unsigned_int (g, minor);
-      free (minor);
-      if (fs->version.v_minor == -1)
-        return -1;
-    } else if ((major = match1 (g, fs->product_name, re_oracle_linux_no_minor)) != NULL) {
-      fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-      free (major);
-      if (fs->version.v_major == -1)
-        return -1;
-      fs->version.v_minor = 0;
-    }
-  }
-  else if (guestfs_is_file_opts (g, "/etc/centos-release",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_CENTOS;
-
-    if (parse_release_file (g, fs, "/etc/centos-release") == -1)
-      return -1;
-
-    if (match2 (g, fs->product_name, re_centos_old, &major, &minor) ||
-	match2 (g, fs->product_name, re_centos, &major, &minor)) {
-      fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-      free (major);
-      if (fs->version.v_major == -1) {
-        free (minor);
-        return -1;
-      }
-      fs->version.v_minor = guestfs_int_parse_unsigned_int (g, minor);
-      free (minor);
-      if (fs->version.v_minor == -1)
-        return -1;
-    }
-    else if ((major = match1 (g, fs->product_name, re_centos_no_minor)) != NULL) {
-      fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-      free (major);
-      if (fs->version.v_major == -1)
-        return -1;
-      fs->version.v_minor = 0;
-    }
-  }
-  else if (guestfs_is_file_opts (g, "/etc/altlinux-release",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_ALTLINUX;
-
-    if (parse_release_file (g, fs, "/etc/altlinux-release") == -1)
-      return -1;
-
-    if (guestfs_int_version_from_x_y_re (g, &fs->version, fs->product_name,
-                                         re_altlinux) == -1)
-      return -1;
-  }
-  else if (guestfs_is_file_opts (g, "/etc/redhat-release",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_REDHAT_BASED; /* Something generic Red Hat-like. */
-
-    if (parse_release_file (g, fs, "/etc/redhat-release") == -1)
-      return -1;
-
-    if ((major = match1 (g, fs->product_name, re_fedora)) != NULL) {
-      fs->distro = OS_DISTRO_FEDORA;
-      fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-      free (major);
-      if (fs->version.v_major == -1)
-        return -1;
-    }
-    else if (match2 (g, fs->product_name, re_rhel_old, &major, &minor) ||
-             match2 (g, fs->product_name, re_rhel, &major, &minor)) {
-      fs->distro = OS_DISTRO_RHEL;
-      fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-      free (major);
-      if (fs->version.v_major == -1) {
-        free (minor);
-        return -1;
-      }
-      fs->version.v_minor = guestfs_int_parse_unsigned_int (g, minor);
-      free (minor);
-      if (fs->version.v_minor == -1)
-        return -1;
-    }
-    else if ((major = match1 (g, fs->product_name, re_rhel_no_minor)) != NULL) {
-      fs->distro = OS_DISTRO_RHEL;
-      fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-      free (major);
-      if (fs->version.v_major == -1)
-        return -1;
-      fs->version.v_minor = 0;
-    }
-    else if (match2 (g, fs->product_name, re_centos_old, &major, &minor) ||
-             match2 (g, fs->product_name, re_centos, &major, &minor)) {
-      fs->distro = OS_DISTRO_CENTOS;
-      fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-      free (major);
-      if (fs->version.v_major == -1) {
-        free (minor);
-        return -1;
-      }
-      fs->version.v_minor = guestfs_int_parse_unsigned_int (g, minor);
-      free (minor);
-      if (fs->version.v_minor == -1)
-        return -1;
-    }
-    else if ((major = match1 (g, fs->product_name, re_centos_no_minor)) != NULL) {
-      fs->distro = OS_DISTRO_CENTOS;
-      fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-      free (major);
-      if (fs->version.v_major == -1)
-        return -1;
-      fs->version.v_minor = 0;
-    }
-    else if (match2 (g, fs->product_name, re_scientific_linux_old, &major, &minor) ||
-             match2 (g, fs->product_name, re_scientific_linux, &major, &minor)) {
-      fs->distro = OS_DISTRO_SCIENTIFIC_LINUX;
-      fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-      free (major);
-      if (fs->version.v_major == -1) {
-        free (minor);
-        return -1;
-      }
-      fs->version.v_minor = guestfs_int_parse_unsigned_int (g, minor);
-      free (minor);
-      if (fs->version.v_minor == -1)
-        return -1;
-    }
-    else if ((major = match1 (g, fs->product_name, re_scientific_linux_no_minor)) != NULL) {
-      fs->distro = OS_DISTRO_SCIENTIFIC_LINUX;
-      fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-      free (major);
-      if (fs->version.v_major == -1)
-        return -1;
-      fs->version.v_minor = 0;
-    }
-  }
-  else if (guestfs_is_file_opts (g, "/etc/debian_version",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_DEBIAN;
-
-    if (parse_release_file (g, fs, "/etc/debian_version") == -1)
-      return -1;
-
-    if (guestfs_int_parse_major_minor (g, fs) == -1)
-      return -1;
-  }
-  else if (guestfs_is_file_opts (g, "/etc/pardus-release",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_PARDUS;
-
-    if (parse_release_file (g, fs, "/etc/pardus-release") == -1)
-      return -1;
-
-    if (guestfs_int_parse_major_minor (g, fs) == -1)
-      return -1;
-  }
-  else if (guestfs_is_file_opts (g, "/etc/arch-release",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_ARCHLINUX;
-
-    /* /etc/arch-release file is empty and I can't see a way to
-     * determine the actual release or product string.
-     */
-  }
-  else if (guestfs_is_file_opts (g, "/etc/gentoo-release",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_GENTOO;
-
-    if (parse_release_file (g, fs, "/etc/gentoo-release") == -1)
-      return -1;
-
-    if (guestfs_int_parse_major_minor (g, fs) == -1)
-      return -1;
-  }
-  else if (guestfs_is_file_opts (g, "/etc/meego-release",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_MEEGO;
-
-    if (parse_release_file (g, fs, "/etc/meego-release") == -1)
-      return -1;
-
-    if (guestfs_int_parse_major_minor (g, fs) == -1)
-      return -1;
-  }
-  else if (guestfs_is_file_opts (g, "/etc/slackware-version",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_SLACKWARE;
-
-    if (parse_release_file (g, fs, "/etc/slackware-version") == -1)
-      return -1;
-
-    if (guestfs_int_parse_major_minor (g, fs) == -1)
-      return -1;
-  }
-  else if (guestfs_is_file_opts (g, "/etc/ttylinux-target",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_TTYLINUX;
-
-    if (parse_release_file (g, fs, "/etc/ttylinux-target") == -1)
-      return -1;
-
-    if (guestfs_int_parse_major_minor (g, fs) == -1)
-      return -1;
-  }
-  else if (guestfs_is_file_opts (g, "/etc/SuSE-release",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_SUSE_BASED;
-
-    if (parse_suse_release (g, fs, "/etc/SuSE-release") == -1)
-      return -1;
-
-  }
-  /* CirrOS versions providing a own version file.
-   */
-  else if (guestfs_is_file_opts (g, "/etc/cirros/version",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_CIRROS;
-
-    if (parse_release_file (g, fs, "/etc/cirros/version") == -1)
-      return -1;
-
-    if (guestfs_int_parse_major_minor (g, fs) == -1)
-      return -1;
-  }
-  /* Buildroot (http://buildroot.net) is an embedded Linux distro
-   * toolkit.  It is used by specific distros such as Cirros.
-   */
-  else if (guestfs_is_file_opts (g, "/etc/br-version",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    if (guestfs_is_file_opts (g, "/usr/share/cirros/logo",
-                              GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0)
-      fs->distro = OS_DISTRO_CIRROS;
-    else
-      fs->distro = OS_DISTRO_BUILDROOT;
-
-    /* /etc/br-version has the format YYYY.MM[-git/hg/svn release] */
-    if (parse_release_file (g, fs, "/etc/br-version") == -1)
-      return -1;
-
-    if (guestfs_int_parse_major_minor (g, fs) == -1)
-      return -1;
-  }
-  else if (guestfs_is_file_opts (g, "/etc/alpine-release",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_ALPINE_LINUX;
-
-    if (parse_release_file (g, fs, "/etc/alpine-release") == -1)
-      return -1;
-
-    if (guestfs_int_parse_major_minor (g, fs) == -1)
-      return -1;
-  }
-  else if (guestfs_is_file_opts (g, "/etc/frugalware-release",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_FRUGALWARE;
-
-    if (parse_release_file (g, fs, "/etc/frugalware-release") == -1)
-      return -1;
-
-    if (guestfs_int_version_from_x_y_re (g, &fs->version, fs->product_name,
-                                         re_frugalware) == -1)
-      return -1;
-  }
-  else if (guestfs_is_file_opts (g, "/etc/pld-release",
-                                 GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_PLD_LINUX;
-
-    if (parse_release_file (g, fs, "/etc/pld-release") == -1)
-      return -1;
-
-    if (guestfs_int_version_from_x_y_re (g, &fs->version, fs->product_name,
-                                         re_pldlinux) == -1)
-      return -1;
-  }
-
- skip_release_checks:;
-
-  /* Determine the architecture. */
-  check_architecture (g, fs);
-
-  /* We already know /etc/fstab exists because it's part of the test
-   * for Linux root above.  We must now parse this file to determine
-   * which filesystems are used by the operating system and how they
-   * are mounted.
-   */
-  const char *configfiles[] = { "/etc/fstab", "/etc/mdadm.conf", NULL };
-  if (inspect_with_augeas (g, fs, configfiles, check_fstab) == -1)
-    return -1;
-
-  /* Determine hostname. */
-  if (check_hostname_unix (g, fs) == -1)
-    return -1;
-
-  return 0;
-}
-
-/* The currently mounted device looks like a Linux /usr. */
-int
-guestfs_int_check_linux_usr (guestfs_h *g, struct inspect_fs *fs)
-{
-  int r;
-
-  fs->type = OS_TYPE_LINUX;
-  fs->role = OS_ROLE_USR;
-
-  if (guestfs_is_file_opts (g, "/lib/os-release",
-                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    r = parse_os_release (g, fs, "/lib/os-release");
-    if (r == -1)        /* error */
-      return -1;
-    if (r == 1)         /* ok - detected the release from this file */
-      goto skip_release_checks;
-  }
-
- skip_release_checks:;
-
-  /* Determine the architecture. */
-  check_architecture (g, fs);
-
-  return 0;
-}
-
-/* The currently mounted device is known to be a FreeBSD root. */
-int
-guestfs_int_check_freebsd_root (guestfs_h *g, struct inspect_fs *fs)
-{
-  fs->type = OS_TYPE_FREEBSD;
-  fs->distro = OS_DISTRO_FREEBSD;
-
-  /* FreeBSD has no authoritative version file.  The version number is
-   * in /etc/motd, which the system administrator might edit, but
-   * we'll use that anyway.
-   */
-
-  if (guestfs_is_file_opts (g, "/etc/motd",
-                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    if (parse_release_file (g, fs, "/etc/motd") == -1)
-      return -1;
-
-    if (guestfs_int_parse_major_minor (g, fs) == -1)
-      return -1;
-  }
-
-  /* Determine the architecture. */
-  check_architecture (g, fs);
-
-  /* We already know /etc/fstab exists because it's part of the test above. */
-  const char *configfiles[] = { "/etc/fstab", NULL };
-  if (inspect_with_augeas (g, fs, configfiles, check_fstab) == -1)
-    return -1;
-
-  /* Determine hostname. */
-  if (check_hostname_unix (g, fs) == -1)
-    return -1;
-
-  return 0;
-}
-
-/* The currently mounted device is maybe to be a *BSD root. */
-int
-guestfs_int_check_netbsd_root (guestfs_h *g, struct inspect_fs *fs)
-{
-
-  if (guestfs_is_file_opts (g, "/etc/release",
-                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    int result;
-    if (parse_release_file (g, fs, "/etc/release") == -1)
-      return -1;
-
-    result = guestfs_int_version_from_x_y_re (g, &fs->version,
-                                              fs->product_name, re_netbsd);
-    if (result == -1) {
-      return -1;
-    } else if (result == 1) {
-      fs->type = OS_TYPE_NETBSD;
-      fs->distro = OS_DISTRO_NETBSD;
-    }
-  } else {
-    return -1;
-  }
-
-  /* Determine the architecture. */
-  check_architecture (g, fs);
-
-  /* We already know /etc/fstab exists because it's part of the test above. */
-  const char *configfiles[] = { "/etc/fstab", NULL };
-  if (inspect_with_augeas (g, fs, configfiles, check_fstab) == -1)
-    return -1;
-
-  /* Determine hostname. */
-  if (check_hostname_unix (g, fs) == -1)
-    return -1;
-
-  return 0;
-}
-
-/* The currently mounted device may be an OpenBSD root. */
-int
-guestfs_int_check_openbsd_root (guestfs_h *g, struct inspect_fs *fs)
-{
-  if (guestfs_is_file_opts (g, "/etc/motd",
-                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    CLEANUP_FREE char *major = NULL, *minor = NULL;
-
-    /* The first line of this file gets automatically updated at boot. */
-    if (parse_release_file (g, fs, "/etc/motd") == -1)
-      return -1;
-
-    if (match2 (g, fs->product_name, re_openbsd, &major, &minor)) {
-      fs->type = OS_TYPE_OPENBSD;
-      fs->distro = OS_DISTRO_OPENBSD;
-
-      /* Before the first boot, the first line will look like this:
-       *
-       * OpenBSD ?.? (UNKNOWN)
-       */
-      if ((fs->product_name[8] != '?') && (fs->product_name[10] != '?')) {
-        fs->version.v_major = guestfs_int_parse_unsigned_int (g, major);
-        if (fs->version.v_major == -1)
-          return -1;
-
-        fs->version.v_minor = guestfs_int_parse_unsigned_int (g, minor);
-        if (fs->version.v_minor == -1)
-          return -1;
-      }
-    }
-  } else {
-    return -1;
-  }
-
-  /* Determine the architecture. */
-  check_architecture (g, fs);
-
-  /* We already know /etc/fstab exists because it's part of the test above. */
-  const char *configfiles[] = { "/etc/fstab", NULL };
-  if (inspect_with_augeas (g, fs, configfiles, check_fstab) == -1)
-    return -1;
-
-  /* Determine hostname. */
-  if (check_hostname_unix (g, fs) == -1)
-    return -1;
-
-  return 0;
-}
-
-/* The currently mounted device may be a Hurd root.  Hurd has distros
- * just like Linux.
- */
-int
-guestfs_int_check_hurd_root (guestfs_h *g, struct inspect_fs *fs)
-{
-  fs->type = OS_TYPE_HURD;
-
-  if (guestfs_is_file_opts (g, "/etc/debian_version",
-                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    fs->distro = OS_DISTRO_DEBIAN;
-
-    if (parse_release_file (g, fs, "/etc/debian_version") == -1)
-      return -1;
-
-    if (guestfs_int_parse_major_minor (g, fs) == -1)
-      return -1;
-  }
-
-  /* Arch Hurd also exists, but inconveniently it doesn't have
-   * the normal /etc/arch-release file.  XXX
-   */
-
-  /* Determine the architecture. */
-  check_architecture (g, fs);
-
-  if (guestfs_is_file (g, "/etc/fstab") > 0) {
-    const char *configfiles[] = { "/etc/fstab", NULL };
-    if (inspect_with_augeas (g, fs, configfiles, check_fstab) == -1)
-      return -1;
-  }
-
-  /* Determine hostname. */
-  if (check_hostname_unix (g, fs) == -1)
-    return -1;
-
-  return 0;
-}
-
-/* The currently mounted device is maybe to be a Minix root. */
-int
-guestfs_int_check_minix_root (guestfs_h *g, struct inspect_fs *fs)
-{
-  fs->type = OS_TYPE_MINIX;
-
-  if (guestfs_is_file_opts (g, "/etc/version",
-                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    if (parse_release_file (g, fs, "/etc/version") == -1)
-      return -1;
-
-    if (guestfs_int_version_from_x_y_re (g, &fs->version, fs->product_name,
-                                         re_minix) == -1)
-      return -1;
-  } else {
-    return -1;
-  }
-
-  /* Determine the architecture. */
-  check_architecture (g, fs);
-
-  /* TODO: enable fstab inspection once resolve_fstab_device implements
-   * the proper mapping from the Minix device names to the appliance names
-   */
-
-  /* Determine hostname. */
-  if (check_hostname_unix (g, fs) == -1)
-    return -1;
-
-  return 0;
-}
-
-/* The currently mounted device is a CoreOS root. From this partition we can
- * only determine the hostname. All immutable OS files are under a separate
- * read-only /usr partition.
- */
-int
-guestfs_int_check_coreos_root (guestfs_h *g, struct inspect_fs *fs)
-{
-  fs->type = OS_TYPE_LINUX;
-  fs->distro = OS_DISTRO_COREOS;
-
-  /* Determine hostname. */
-  if (check_hostname_unix (g, fs) == -1)
-    return -1;
-
-  /* CoreOS does not contain /etc/fstab to determine the mount points.
-   * Associate this filesystem with the "/" mount point.
-   */
-  add_fstab_entry (g, fs, fs->mountable, "/");
-
-  return 0;
-}
-
-/* The currently mounted device looks like a CoreOS /usr. In CoreOS
- * the read-only /usr contains the OS version. The /etc/os-release is a
- * link to /usr/share/coreos/os-release.
- */
-int
-guestfs_int_check_coreos_usr (guestfs_h *g, struct inspect_fs *fs)
-{
-  int r;
-
-  fs->type = OS_TYPE_LINUX;
-  fs->distro = OS_DISTRO_COREOS;
-  fs->role = OS_ROLE_USR;
-
-  if (guestfs_is_file_opts (g, "/lib/os-release",
-                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    r = parse_os_release (g, fs, "/lib/os-release");
-    if (r == -1)        /* error */
-      return -1;
-    if (r == 1)         /* ok - detected the release from this file */
-      goto skip_release_checks;
-  }
-
-  if (guestfs_is_file_opts (g, "/share/coreos/lsb-release",
-                            GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-    r = parse_lsb_release (g, fs, "/share/coreos/lsb-release");
-    if (r == -1)        /* error */
-      return -1;
-  }
-
- skip_release_checks:;
-
-  /* Determine the architecture. */
-  check_architecture (g, fs);
-
-  /* CoreOS does not contain /etc/fstab to determine the mount points.
-   * Associate this filesystem with the "/usr" mount point.
-   */
-  add_fstab_entry (g, fs, fs->mountable, "/usr");
-
-  return 0;
-}
-
-static void
-check_architecture (guestfs_h *g, struct inspect_fs *fs)
-{
-  const char *binaries[] =
-    { "/bin/bash", "/bin/ls", "/bin/echo", "/bin/rm", "/bin/sh" };
-  size_t i;
-  char *arch = NULL;
-
-  for (i = 0; i < sizeof binaries / sizeof binaries[0]; ++i) {
-    /* Allow symlinks when checking the binaries:,so in case they are
-     * relative ones (which can be resolved within the same partition),
-     * then we can check the architecture of their target.
-     */
-    if (guestfs_is_file_opts (g, binaries[i],
-                              GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0) {
-      CLEANUP_FREE char *resolved = NULL;
-
-      /* Ignore errors from realpath and file_architecture calls. */
-      guestfs_push_error_handler (g, NULL, NULL);
-      resolved = guestfs_realpath (g, binaries[i]);
-      /* If is_file above succeeded realpath should too, but better
-       * be safe than sorry.
-       */
-      if (resolved)
-        arch = guestfs_file_architecture (g, resolved);
-      guestfs_pop_error_handler (g);
-
-      if (arch) {
-        /* String will be owned by handle, freed by
-         * guestfs_int_free_inspect_info.
-         */
-        fs->arch = arch;
-        break;
-      }
-    }
-  }
-}
-
-/* Try several methods to determine the hostname from a Linux or
- * FreeBSD guest.  Note that type and distro have been set, so we can
- * use that information to direct the search.
- */
-static int
-check_hostname_unix (guestfs_h *g, struct inspect_fs *fs)
-{
-  switch (fs->type) {
-  case OS_TYPE_LINUX:
-  case OS_TYPE_HURD:
-    /* Red Hat-derived would be in /etc/sysconfig/network or
-     * /etc/hostname (RHEL 7+, F18+).  Debian-derived in the file
-     * /etc/hostname.  Very old Debian and SUSE use /etc/HOSTNAME.
-     * It's best to just look for each of these files in turn, rather
-     * than try anything clever based on distro.
-     */
-    if (guestfs_is_file (g, "/etc/HOSTNAME")) {
-      fs->hostname = guestfs_int_first_line_of_file (g, "/etc/HOSTNAME");
-      if (fs->hostname == NULL)
-        return -1;
-      if (STREQ (fs->hostname, "")) {
-        free (fs->hostname);
-        fs->hostname = NULL;
-      }
-    }
-
-    if (!fs->hostname && guestfs_is_file (g, "/etc/hostname")) {
-      fs->hostname = guestfs_int_first_line_of_file (g, "/etc/hostname");
-      if (fs->hostname == NULL)
-        return -1;
-      if (STREQ (fs->hostname, "")) {
-        free (fs->hostname);
-        fs->hostname = NULL;
-      }
-    }
-
-    if (!fs->hostname && guestfs_is_file (g, "/etc/sysconfig/network")) {
-      const char *configfiles[] = { "/etc/sysconfig/network", NULL };
-      if (inspect_with_augeas (g, fs, configfiles,
-                               check_hostname_redhat) == -1)
-        return -1;
-    }
-    break;
-
-  case OS_TYPE_FREEBSD:
-  case OS_TYPE_NETBSD:
-    /* /etc/rc.conf contains the hostname, but there is no Augeas lens
-     * for this file.
-     */
-    if (guestfs_is_file (g, "/etc/rc.conf")) {
-      if (check_hostname_freebsd (g, fs) == -1)
-        return -1;
-    }
-    break;
-
-  case OS_TYPE_OPENBSD:
-    if (guestfs_is_file (g, "/etc/myname")) {
-      fs->hostname = guestfs_int_first_line_of_file (g, "/etc/myname");
-      if (fs->hostname == NULL)
-        return -1;
-      if (STREQ (fs->hostname, "")) {
-        free (fs->hostname);
-        fs->hostname = NULL;
-      }
-    }
-    break;
-
-  case OS_TYPE_MINIX:
-    if (guestfs_is_file (g, "/etc/hostname.file")) {
-      fs->hostname = guestfs_int_first_line_of_file (g, "/etc/hostname.file");
-      if (fs->hostname == NULL)
-        return -1;
-      if (STREQ (fs->hostname, "")) {
-        free (fs->hostname);
-        fs->hostname = NULL;
-      }
-    }
-    break;
-
-  case OS_TYPE_WINDOWS: /* not here, see check_windows_system_registry */
-  case OS_TYPE_DOS:
-  case OS_TYPE_UNKNOWN:
-    /* nothing */;
-  }
-
-  return 0;
-}
-
-/* Parse the hostname from /etc/sysconfig/network.  This must be
- * called from the inspect_with_augeas wrapper.  Note that F18+ and
- * RHEL7+ use /etc/hostname just like Debian.
- */
-static int
-check_hostname_redhat (guestfs_h *g, struct inspect_fs *fs)
-{
-  char *hostname;
-
-  /* Errors here are not fatal (RHBZ#726739), since it could be
-   * just missing HOSTNAME field in the file.
-   */
-  guestfs_push_error_handler (g, NULL, NULL);
-  hostname = guestfs_aug_get (g, "/files/etc/sysconfig/network/HOSTNAME");
-  guestfs_pop_error_handler (g);
-
-  /* This is freed by guestfs_int_free_inspect_info.  Note that hostname
-   * could be NULL because we ignored errors above.
-   */
-  fs->hostname = hostname;
-  return 0;
-}
-
-/* Parse the hostname from /etc/rc.conf.  On FreeBSD this file
- * contains comments, blank lines and:
- *   hostname="freebsd8.example.com"
- *   ifconfig_re0="DHCP"
- *   keymap="uk.iso"
- *   sshd_enable="YES"
- */
-static int
-check_hostname_freebsd (guestfs_h *g, struct inspect_fs *fs)
-{
-  const char *filename = "/etc/rc.conf";
-  int64_t size;
-  CLEANUP_FREE_STRING_LIST char **lines = NULL;
-  size_t i;
-
-  /* Don't trust guestfs_read_lines not to break with very large files.
-   * Check the file size is something reasonable first.
-   */
-  size = guestfs_filesize (g, filename);
-  if (size == -1)
-    /* guestfs_filesize failed and has already set error in handle */
-    return -1;
-  if (size > MAX_SMALL_FILE_SIZE) {
-    error (g, _("size of %s is unreasonably large (%" PRIi64 " bytes)"),
-           filename, size);
-    return -1;
-  }
-
-  lines = guestfs_read_lines (g, filename);
-  if (lines == NULL)
-    return -1;
-
-  for (i = 0; lines[i] != NULL; ++i) {
-    if (STRPREFIX (lines[i], "hostname=\"") ||
-        STRPREFIX (lines[i], "hostname='")) {
-      const size_t len = strlen (lines[i]) - 10 - 1;
-      fs->hostname = safe_strndup (g, &lines[i][10], len);
-      break;
-    } else if (STRPREFIX (lines[i], "hostname=")) {
-      const size_t len = strlen (lines[i]) - 9;
-      fs->hostname = safe_strndup (g, &lines[i][9], len);
-      break;
-    }
-  }
-
-  return 0;
-}
-
-static int
-check_fstab (guestfs_h *g, struct inspect_fs *fs)
-{
-  CLEANUP_FREE_STRING_LIST char **entries = NULL;
-  char **entry;
-  char augpath[256];
-  CLEANUP_HASH_FREE Hash_table *md_map = NULL;
-  bool is_bsd = (fs->type == OS_TYPE_FREEBSD ||
-                 fs->type == OS_TYPE_NETBSD ||
-                 fs->type == OS_TYPE_OPENBSD);
-
-  /* Generate a map of MD device paths listed in /etc/mdadm.conf to MD device
-   * paths in the guestfs appliance */
-  if (map_md_devices (g, &md_map) == -1) return -1;
-
-  entries = guestfs_aug_match (g, "/files/etc/fstab/*[label() != '#comment']");
-  if (entries == NULL)
-    return -1;
-
-  for (entry = entries; *entry != NULL; entry++) {
-    CLEANUP_FREE char *spec = NULL;
-    CLEANUP_FREE char *mp = NULL;
-    CLEANUP_FREE char *mountable = NULL;
-    CLEANUP_FREE char *vfstype = NULL;
-
-    snprintf (augpath, sizeof augpath, "%s/spec", *entry);
-    spec = guestfs_aug_get (g, augpath);
-    if (spec == NULL)
-      return -1;
-
-    /* Ignore /dev/fd (floppy disks) (RHBZ#642929) and CD-ROM drives.
-     *
-     * /dev/iso9660/FREEBSD_INSTALL can be found in FreeBSDs installation
-     * discs.
-     */
-    if ((STRPREFIX (spec, "/dev/fd") && c_isdigit (spec[7])) ||
-        (STRPREFIX (spec, "/dev/cd") && c_isdigit (spec[7])) ||
-        STREQ (spec, "/dev/floppy") ||
-        STREQ (spec, "/dev/cdrom") ||
-        STRPREFIX (spec, "/dev/iso9660/"))
-      continue;
-
-    snprintf (augpath, sizeof augpath, "%s/file", *entry);
-    mp = guestfs_aug_get (g, augpath);
-    if (mp == NULL)
-      return -1;
-
-    /* Canonicalize the path, so "///usr//local//" -> "/usr/local" */
-    canonical_mountpoint (mp);
-
-    /* Ignore certain mountpoints. */
-    if (STRPREFIX (mp, "/dev/") ||
-        STREQ (mp, "/dev") ||
-        STRPREFIX (mp, "/media/") ||
-        STRPREFIX (mp, "/proc/") ||
-        STREQ (mp, "/proc") ||
-        STRPREFIX (mp, "/selinux/") ||
-        STREQ (mp, "/selinux") ||
-        STRPREFIX (mp, "/sys/") ||
-        STREQ (mp, "/sys"))
-      continue;
-
-    /* Resolve UUID= and LABEL= to the actual device. */
-    if (STRPREFIX (spec, "UUID=")) {
-      CLEANUP_FREE char *s = guestfs_int_shell_unquote (&spec[5]);
-      if (s == NULL) { perrorf (g, "guestfs_int_shell_unquote"); return -1; }
-      mountable = guestfs_findfs_uuid (g, s);
-    }
-    else if (STRPREFIX (spec, "LABEL=")) {
-      CLEANUP_FREE char *s = guestfs_int_shell_unquote (&spec[6]);
-      if (s == NULL) { perrorf (g, "guestfs_int_shell_unquote"); return -1; }
-      mountable = guestfs_findfs_label (g, s);
-    }
-    /* Ignore "/.swap" (Pardus) and pseudo-devices like "tmpfs". */
-    else if (STREQ (spec, "/dev/root") || (is_bsd && STREQ (mp, "/")))
-      /* Resolve /dev/root to the current device.
-       * Do the same for the / partition of the *BSD systems, since the
-       * BSD -> Linux device translation is not straight forward.
-       */
-      mountable = safe_strdup (g, fs->mountable);
-    else if (STRPREFIX (spec, "/dev/"))
-      /* Resolve guest block device names. */
-      mountable = resolve_fstab_device (g, spec, md_map, fs->type);
-    else if (match (g, spec, re_openbsd_duid)) {
-      /* In OpenBSD's fstab you can specify partitions on a disk by appending a
-       * period and a partition letter to a Disklable Unique Identifier. The
-       * DUID is a 16 hex digit field found in the OpenBSD's altered BSD
-       * disklabel. For more info see here:
-       * http://www.openbsd.org/faq/faq14.html#intro
-       */
-      char device[10]; /* /dev/sd[0-9][a-z] */
-      char part = spec[17];
-
-      /* We cannot peep into disklables, we can only assume that this is the
-       * first disk.
-       */
-      snprintf(device, 10, "%s%c", "/dev/sd0", part);
-      mountable = resolve_fstab_device (g, device, md_map, fs->type);
-    }
-
-    /* If we haven't resolved the device successfully by this point,
-     * we don't care, just ignore it.
-     */
-    if (mountable == NULL)
-      continue;
-
-    snprintf (augpath, sizeof augpath, "%s/vfstype", *entry);
-    vfstype = guestfs_aug_get (g, augpath);
-    if (vfstype == NULL) return -1;
-
-    if (STREQ (vfstype, "btrfs")) {
-      size_t i;
-
-      snprintf (augpath, sizeof augpath, "%s/opt", *entry);
-      CLEANUP_FREE_STRING_LIST char **opts = guestfs_aug_match (g, augpath);
-      if (opts == NULL) return -1;
-
-      for (i = 0; opts[i] != NULL; ++i) {
-        CLEANUP_FREE char *optname = NULL, *optvalue = NULL, *subvol = NULL;
-        char *old_mountable;
-
-        optname = guestfs_aug_get (g, opts[i]);
-        if (optname == NULL) return -1;
-
-        if (STREQ (optname, "subvol")) {
-          optvalue = safe_asprintf (g, "%s/value", opts[i]);
-
-          subvol = guestfs_aug_get (g, optvalue);
-          if (subvol == NULL) return -1;
-
-          old_mountable = mountable;
-          mountable = safe_asprintf (g, "btrfsvol:%s/%s", mountable, subvol);
-          free (old_mountable);
-        }
-      }
-    }
-
-    add_fstab_entry (g, fs, mountable, mp);
-  }
-
-  return 0;
-}
-
-/* Add a filesystem and possibly a mountpoint entry for
- * the root filesystem 'fs'.
- *
- * 'spec' is the fstab spec field, which might be a device name or a
- * pseudodevice or 'UUID=...' or 'LABEL=...'.
- *
- * 'mp' is the mount point, which could also be 'swap' or 'none'.
- */
-static void
-add_fstab_entry (guestfs_h *g, struct inspect_fs *fs,
-                 const char *mountable, const char *mountpoint)
-{
-  /* Add this to the fstab entry in 'fs'.
-   * Note these are further filtered by guestfs_inspect_get_mountpoints
-   * and guestfs_inspect_get_filesystems.
-   */
-  const size_t n = fs->nr_fstab + 1;
-  struct inspect_fstab_entry *p;
-
-  p = safe_realloc (g, fs->fstab, n * sizeof (struct inspect_fstab_entry));
-
-  fs->fstab = p;
-  fs->nr_fstab = n;
-
-  /* These are owned by the handle and freed by guestfs_int_free_inspect_info. */
-  fs->fstab[n-1].mountable = safe_strdup (g, mountable);
-  fs->fstab[n-1].mountpoint = safe_strdup (g, mountpoint);
-
-  debug (g, "fstab: mountable=%s mountpoint=%s", mountable, mountpoint);
-}
-
-/* Compute a uuid hash as a simple xor of of its 4 32bit components */
-static size_t
-uuid_hash(const void *x, size_t table_size)
-{
-  const md_uuid *a = x;
-  size_t h, i;
-
-  h = a->uuid[0];
-  for (i = 1; i < 4; i++) {
-    h ^= a->uuid[i];
-  }
-
-  return h % table_size;
-}
-
-static bool
-uuid_cmp(const void *x, const void *y)
-{
-  const md_uuid *a = x;
-  const md_uuid *b = y;
-  size_t i;
-
-  for (i = 0; i < 1; i++) {
-    if (a->uuid[i] != b->uuid[i]) return 0;
-  }
-
-  return 1;
-}
-
-static void
-md_uuid_free(void *x)
-{
-  md_uuid *a = x;
-  free(a->path);
-  free(a);
-}
-
-/* Taken from parse_uuid in mdadm */
-static int
-parse_uuid (const char *str, uint32_t *uuid)
-{
-  size_t hit = 0; /* number of Hex digIT */
-  char c;
-  size_t i;
-  int n;
-
-  for (i = 0; i < 4; i++)
-    uuid[i] = 0;
-
-  while ((c = *str++)) {
-    if (c >= '0' && c <= '9')
-      n = c - '0';
-    else if (c >= 'a' && c <= 'f')
-      n = 10 + c - 'a';
-    else if (c >= 'A' && c <= 'F')
-      n = 10 + c - 'A';
-    else if (strchr (":. -", c))
-      continue;
-    else
-      return -1;
-
-    if (hit < 32) {
-      uuid[hit / 8] <<= 4;
-      uuid[hit / 8] += n;
-    }
-    hit++;
-  }
-  if (hit == 32) return 0;
-
-  return -1;
-}
-
-/* Create a mapping of uuids to appliance md device names */
-static ssize_t
-map_app_md_devices (guestfs_h *g, Hash_table **map)
-{
-  CLEANUP_FREE_STRING_LIST char **mds = NULL;
-  size_t n = 0;
-  char **md;
-
-  /* A hash mapping uuids to md device names */
-  *map = hash_initialize(16, NULL, uuid_hash, uuid_cmp, md_uuid_free);
-  if (*map == NULL) g->abort_cb();
-
-  mds = guestfs_list_md_devices(g);
-  if (mds == NULL) goto error;
-
-  for (md = mds; *md != NULL; md++) {
-    char **i;
-    CLEANUP_FREE_STRING_LIST char **detail = guestfs_md_detail (g, *md);
-    if (detail == NULL) goto error;
-
-    /* Iterate over keys until we find uuid */
-    for (i = detail; *i != NULL; i += 2) {
-      if (STREQ(*i, "uuid")) break;
-    }
-
-    /* We found it */
-    if (*i) {
-      md_uuid *entry;
-
-      /* Next item is the uuid value */
-      i++;
-
-      entry = safe_malloc(g, sizeof(md_uuid));
-      entry->path = safe_strdup(g, *md);
-
-      if (parse_uuid(*i, entry->uuid) == -1) {
-        /* Invalid UUID is weird, but not fatal. */
-        debug(g, "inspect-os: guestfs_md_detail returned invalid "
-	      "uuid for %s: %s", *md, *i);
-        md_uuid_free(entry);
-        continue;
-      }
-
-      const void *matched = NULL;
-      switch (hash_insert_if_absent(*map, entry, &matched)) {
-      case -1:
-	g->abort_cb();
-
-      case 0:
-	/* Duplicate uuid in for md device is weird, but not fatal. */
-	debug(g, "inspect-os: md devices %s and %s have the same uuid",
-	      ((md_uuid *)matched)->path, entry->path);
-	md_uuid_free(entry);
-	break;
-
-      default:
-	n++;
-      }
-    }
-  }
-
-  return n;
-
- error:
-  hash_free (*map); *map = NULL;
-
-  return -1;
-}
-
-static size_t
-mdadm_app_hash(const void *x, size_t table_size)
-{
-  const mdadm_app *a = x;
-  return hash_pjw(a->mdadm, table_size);
-}
-
-static bool
-mdadm_app_cmp(const void *x, const void *y)
-{
-  const mdadm_app *a = x;
-  const mdadm_app *b = y;
-
-  return STREQ (a->mdadm, b->mdadm);
-}
-
-static void
-mdadm_app_free(void *x)
-{
-  mdadm_app *a = x;
-  free(a->mdadm);
-  free(a->app);
-  free(a);
-}
-
-/* Get a map of md device names in mdadm.conf to their device names in the
- * appliance */
-static int
-map_md_devices(guestfs_h *g, Hash_table **map)
-{
-  CLEANUP_HASH_FREE Hash_table *app_map = NULL;
-  CLEANUP_FREE_STRING_LIST char **matches = NULL;
-  ssize_t n_app_md_devices;
-
-  *map = NULL;
-
-  /* Get a map of md device uuids to their device names in the appliance */
-  n_app_md_devices = map_app_md_devices (g, &app_map);
-  if (n_app_md_devices == -1) goto error;
-
-  /* Nothing to do if there are no md devices */
-  if (n_app_md_devices == 0)
-    return 0;
-
-  /* Get all arrays listed in mdadm.conf */
-  matches = guestfs_aug_match(g, "/files/etc/mdadm.conf/array");
-  if (!matches) goto error;
-
-  /* Log a debug message if we've got md devices, but nothing in mdadm.conf */
-  if (matches[0] == NULL) {
-    debug(g, "Appliance has MD devices, but augeas returned no array matches "
-	  "in mdadm.conf");
-    return 0;
-  }
-
-  *map = hash_initialize(16, NULL, mdadm_app_hash, mdadm_app_cmp,
-			 mdadm_app_free);
-  if (!*map) g->abort_cb();
-
-  for (char **m = matches; *m != NULL; m++) {
-    /* Get device name and uuid for each array */
-    CLEANUP_FREE char *dev_path = safe_asprintf (g, "%s/devicename", *m);
-    char *dev = guestfs_aug_get (g, dev_path);
-    if (!dev) goto error;
-
-    CLEANUP_FREE char *uuid_path = safe_asprintf (g, "%s/uuid", *m);
-    CLEANUP_FREE char *uuid = guestfs_aug_get (g, uuid_path);
-    if (!uuid) {
-      free (dev);
-      continue;
-    }
-
-    /* Parse the uuid into an md_uuid structure so we can look it up in the
-     * uuid->appliance device map */
-    md_uuid mdadm;
-    mdadm.path = dev;
-    if (parse_uuid(uuid, mdadm.uuid) == -1) {
-      /* Invalid uuid. Weird, but not fatal. */
-      debug(g, "inspect-os: mdadm.conf contains invalid uuid for %s: %s",
-            dev, uuid);
-      free (dev);
-      continue;
-    }
-
-    /* If there's a corresponding uuid in the appliance, create a new
-     * entry in the transitive map */
-    md_uuid *app = hash_lookup(app_map, &mdadm);
-    if (app) {
-      mdadm_app *entry = safe_malloc(g, sizeof(mdadm_app));
-      entry->mdadm = dev;
-      entry->app = safe_strdup(g, app->path);
-
-      switch (hash_insert_if_absent(*map, entry, NULL)) {
-      case -1:
-	g->abort_cb();
-
-      case 0:
-	/* Duplicate uuid in for md device is weird, but not fatal. */
-	debug(g, "inspect-os: mdadm.conf contains multiple entries for %s",
-	      app->path);
-	mdadm_app_free(entry);
-	continue;
-      }
-    } else
-      free (dev);
-  }
-
-  return 0;
-
- error:
-  if (*map) hash_free (*map);
-
-  return -1;
-}
-
-static int
-resolve_fstab_device_xdev (guestfs_h *g, const char *type, const char *disk,
-                           const char *part, char **device_ret)
-{
-  CLEANUP_FREE char *name = NULL;
-  char *device;
-  CLEANUP_FREE_STRING_LIST char **devices = NULL;
-  size_t i, count;
-  struct drive *drv;
-  const char *p;
-
-  /* type: (h|s|v|xv)
-   * disk: ([a-z]+)
-   * part: (\d*)
-   */
-
-  devices = guestfs_list_devices (g);
-  if (devices == NULL)
-    return -1;
-
-  /* Check any hints we were passed for a non-heuristic mapping */
-  name = safe_asprintf (g, "%sd%s", type, disk);
-  ITER_DRIVES (g, i, drv) {
-    if (drv->name && STREQ (drv->name, name)) {
-      device = safe_asprintf (g, "%s%s", devices[i], part);
-      if (!guestfs_int_is_partition (g, device)) {
-        free (device);
-        return 0;
-      }
-      *device_ret = device;
-      break;
-    }
-  }
-
-  /* Guess the appliance device name if we didn't find a matching hint */
-  if (!*device_ret) {
-    /* Count how many disks the libguestfs appliance has */
-    for (count = 0; devices[count] != NULL; count++)
-      ;
-
-    /* Calculate the numerical index of the disk */
-    i = disk[0] - 'a';
-    for (p = disk + 1; *p != '\0'; p++) {
-      i += 1; i *= 26;
-      i += *p - 'a';
-    }
-
-    /* Check the index makes sense wrt the number of disks the appliance has.
-     * If it does, map it to an appliance disk.
-     */
-    if (i < count) {
-      device = safe_asprintf (g, "%s%s", devices[i], part);
-      if (!guestfs_int_is_partition (g, device)) {
-        free (device);
-        return 0;
-      }
-      *device_ret = device;
-    }
-  }
-
-  return 0;
-}
-
-static int
-resolve_fstab_device_cciss (guestfs_h *g, const char *disk, const char *part,
-                            char **device_ret)
-{
-  char *device;
-  CLEANUP_FREE_STRING_LIST char **devices = NULL;
-  size_t i;
-  struct drive *drv;
-
-  /* disk: (cciss/c\d+d\d+)
-   * part: (\d+)?
-   */
-
-  devices = guestfs_list_devices (g);
-  if (devices == NULL)
-    return -1;
-
-  /* Check any hints we were passed for a non-heuristic mapping */
-  ITER_DRIVES (g, i, drv) {
-    if (drv->name && STREQ (drv->name, disk)) {
-      if (part) {
-        device = safe_asprintf (g, "%s%s", devices[i], part);
-        if (!guestfs_int_is_partition (g, device)) {
-          free (device);
-          return 0;
-        }
-        *device_ret = device;
-      }
-      else
-        *device_ret = safe_strdup (g, devices[i]);
-      break;
-    }
-  }
-
-  /* We don't try to guess mappings for cciss devices */
-  return 0;
-}
-
-static int
-resolve_fstab_device_diskbyid (guestfs_h *g, const char *part,
-                               char **device_ret)
-{
-  int nr_devices;
-  char *device;
-
-  /* For /dev/disk/by-id there is a limit to what we can do because
-   * original SCSI ID information has likely been lost.  This
-   * heuristic will only work for guests that have a single block
-   * device.
-   *
-   * So the main task here is to make sure the assumptions above are
-   * true.
-   *
-   * XXX Use hints from virt-p2v if available.
-   * See also: https://bugzilla.redhat.com/show_bug.cgi?id=836573#c3
-   */
-
-  nr_devices = guestfs_nr_devices (g);
-  if (nr_devices == -1)
-    return -1;
-
-  /* If #devices isn't 1, give up trying to translate this fstab entry. */
-  if (nr_devices != 1)
-    return 0;
-
-  /* Make the partition name and check it exists. */
-  device = safe_asprintf (g, "/dev/sda%s", part);
-  if (!guestfs_int_is_partition (g, device)) {
-    free (device);
-    return 0;
-  }
-
-  *device_ret = device;
-  return 0;
-}
-
-/* Resolve block device name to the libguestfs device name, eg.
- * /dev/xvdb1 => /dev/vdb1; and /dev/mapper/VG-LV => /dev/VG/LV.  This
- * assumes that disks were added in the same order as they appear to
- * the real VM, which is a reasonable assumption to make.  Return
- * anything we don't recognize unchanged.
- */
-static char *
-resolve_fstab_device (guestfs_h *g, const char *spec, Hash_table *md_map,
-                      enum inspect_os_type os_type)
-{
-  char *device = NULL;
-  char *type, *slice, *disk, *part;
-  int r;
-
-  if (STRPREFIX (spec, "/dev/mapper/")) {
-    /* LVM2 does some strange munging on /dev/mapper paths for VGs and
-     * LVs which contain '-' character:
-     *
-     * ><fs> lvcreate LV--test VG--test 32
-     * ><fs> debug ls /dev/mapper
-     * VG----test-LV----test
-     *
-     * This makes it impossible to reverse those paths directly, so
-     * we have implemented lvm_canonical_lv_name in the daemon.
-     */
-    guestfs_push_error_handler (g, NULL, NULL);
-    device = guestfs_lvm_canonical_lv_name (g, spec);
-    guestfs_pop_error_handler (g);
-    if (device == NULL) {
-      if (guestfs_last_errno (g) == ENOENT) {
-        /* Ignore devices that don't exist. (RHBZ#811872) */
-      } else {
-        guestfs_int_error_errno (g, guestfs_last_errno (g), "%s", guestfs_last_error (g));
-        return NULL;
-      }
-    }
-  }
-  else if (match3 (g, spec, re_xdev, &type, &disk, &part)) {
-    r = resolve_fstab_device_xdev (g, type, disk, part, &device);
-    free (type);
-    free (disk);
-    free (part);
-    if (r == -1)
-      return NULL;
-  }
-  else if (match2 (g, spec, re_cciss, &disk, &part)) {
-    r = resolve_fstab_device_cciss (g, disk, part, &device);
-    free (disk);
-    free (part);
-    if (r == -1)
-      return NULL;
-  }
-  else if (md_map && (disk = match1 (g, spec, re_mdN)) != NULL) {
-    mdadm_app entry;
-    entry.mdadm = disk;
-
-    mdadm_app *app = hash_lookup (md_map, &entry);
-    if (app) device = safe_strdup (g, app->app);
-
-    free (disk);
-  }
-  else if (match3 (g, spec, re_freebsd_gpt, &type, &disk, &part)) {
-    /* If the FreeBSD disk contains GPT partitions, the translation to Linux
-     * device names is straight forward. Partitions on a virtio disk are
-     * prefixed with vtbd. IDE hard drives used to be prefixed with ad and now
-     * are with ada.
-     */
-    const int disk_i = guestfs_int_parse_unsigned_int (g, disk);
-    const int part_i = guestfs_int_parse_unsigned_int (g, part);
-    free (type);
-    free (disk);
-    free (part);
-
-    if (disk_i != -1 && disk_i <= 26 && part_i > 0 && part_i <= 128)
-      device = safe_asprintf (g, "/dev/sd%c%d", disk_i + 'a', part_i);
-  }
-  else if (match4 (g, spec, re_freebsd_mbr, &type, &disk, &slice, &part)) {
-    /* FreeBSD disks are organized quite differently.  See:
-     * http://www.freebsd.org/doc/handbook/disk-organization.html
-     * FreeBSD "partitions" are exposed as quasi-extended partitions
-     * numbered from 5 in Linux.  I have no idea what happens when you
-     * have multiple "slices" (the FreeBSD term for MBR partitions).
-     */
-    const int disk_i = guestfs_int_parse_unsigned_int (g, disk);
-    const int slice_i = guestfs_int_parse_unsigned_int (g, slice);
-    int part_i = part[0] - 'a' /* counting from 0 */;
-    free (type);
-    free (disk);
-    free (slice);
-    free (part);
-
-    if (part_i > 2)
-      /* Partition 'c' has the size of the enclosing slice. Not mapped under Linux. */
-      part_i -= 1;
-
-    if (disk_i != -1 && disk_i <= 26 &&
-        slice_i > 0 && slice_i <= 1 /* > 4 .. see comment above */ &&
-        part_i >= 0 && part_i < 25) {
-      device = safe_asprintf (g, "/dev/sd%c%d", disk_i + 'a', part_i + 5);
-    }
-  }
-  else if ((os_type == OS_TYPE_NETBSD) &&
-           match3 (g, spec, re_netbsd_dev, &type, &disk, &part)) {
-    const int disk_i = guestfs_int_parse_unsigned_int (g, disk);
-    int part_i = part[0] - 'a'; /* counting from 0 */
-    free (type);
-    free (disk);
-    free (part);
-
-    if (part_i > 3)
-      /* Partition 'c' is the disklabel partition and 'd' the hard disk itself.
-       * Not mapped under Linux.
-       */
-      part_i -= 2;
-
-    if (disk_i != -1 && part_i >= 0 && part_i < 24)
-      device = safe_asprintf (g, "/dev/sd%c%d", disk_i + 'a', part_i + 5);
-  }
-  else if ((os_type == OS_TYPE_OPENBSD) &&
-           match3 (g, spec, re_openbsd_dev, &type, &disk, &part)) {
-    const int disk_i = guestfs_int_parse_unsigned_int (g, disk);
-    int part_i = part[0] - 'a'; /* counting from 0 */
-    free (type);
-    free (disk);
-    free (part);
-
-    if (part_i > 2)
-      /* Partition 'c' is the hard disk itself. Not mapped under Linux */
-      part_i -= 1;
-
-    /* In OpenBSD MAXPARTITIONS is defined to 16 for all architectures */
-    if (disk_i != -1 && part_i >= 0 && part_i < 15)
-      device = safe_asprintf (g, "/dev/sd%c%d", disk_i + 'a', part_i + 5);
-  }
-  else if ((part = match1 (g, spec, re_diskbyid)) != NULL) {
-    r = resolve_fstab_device_diskbyid (g, part, &device);
-    free (part);
-    if (r == -1)
-      return NULL;
-  }
-  else if (match3 (g, spec, re_hurd_dev, &type, &disk, &part)) {
-    /* Hurd disk devices are like /dev/hdNsM, where hdN is the
-     * N-th disk and M is the M-th partition on that disk.
-     * Turn the disk number into a letter-based identifier, so
-     * we can resolve it easily.
-     */
-    const int disk_i = guestfs_int_parse_unsigned_int (g, disk);
-    const char disk_as_letter[2] = { disk_i + 'a', 0 };
-    r = resolve_fstab_device_xdev (g, type, disk_as_letter, part, &device);
-    free (type);
-    free (disk);
-    free (part);
-    if (r == -1)
-      return NULL;
-  }
-
-  /* Didn't match device pattern, return original spec unchanged. */
-  if (device == NULL)
-    device = safe_strdup (g, spec);
-
-  return device;
-}
-
-static char *make_augeas_path_expression (guestfs_h *g, const char **configfiles);
-
-/* Call 'f' with Augeas opened and having parsed 'configfiles' (these
- * files must exist).  As a security measure, this bails if any file
- * is too large for a reasonable configuration file.  After the call
- * to 'f' the Augeas handle is closed.
- */
-static int
-inspect_with_augeas (guestfs_h *g, struct inspect_fs *fs,
-                     const char **configfiles,
-                     int (*f) (guestfs_h *, struct inspect_fs *))
-{
-  size_t i;
-  int64_t size;
-  int r;
-  CLEANUP_FREE char *pathexpr = NULL;
-  CLEANUP_FREE_STRING_LIST char **matches = NULL;
-  char **match;
-
-  /* Security: Refuse to do this if a config file is too large. */
-  for (i = 0; configfiles[i] != NULL; ++i) {
-    if (guestfs_is_file_opts (g, configfiles[i],
-                              GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) == 0)
-      continue;
-
-    size = guestfs_filesize (g, configfiles[i]);
-    if (size == -1)
-      /* guestfs_filesize failed and has already set error in handle */
-      return -1;
-    if (size > MAX_AUGEAS_FILE_SIZE) {
-      error (g, _("size of %s is unreasonably large (%" PRIi64 " bytes)"),
-             configfiles[i], size);
-      return -1;
-    }
-  }
-
-  if (guestfs_aug_init (g, "/", 16|32 /* AUG_SAVE_NOOP|AUG_NO_LOAD */) == -1)
-    return -1;
-
-  r = -1;
-
-  /* Tell Augeas to only load configfiles and no other files.  This
-   * prevents a rogue guest from performing a denial of service attack
-   * by having large, over-complicated configuration files which are
-   * unrelated to the task at hand.  (Thanks Dominic Cleal).
-   * Note this requires Augeas >= 1.0.0 because of RHBZ#975412.
-   */
-  pathexpr = make_augeas_path_expression (g, configfiles);
-  if (guestfs_aug_rm (g, pathexpr) == -1)
-    goto out;
-
-  if (guestfs_aug_load (g) == -1)
-    goto out;
-
-  /* Check that augeas did not get a parse error for any of the configfiles,
-   * otherwise we are silently missing information.
-   */
-  matches = guestfs_aug_match (g, "/augeas/files//error");
-  for (match = matches; *match != NULL; ++match) {
-    for (i = 0; configfiles[i] != NULL; ++i) {
-      CLEANUP_FREE char *errorpath =
-        safe_asprintf (g, "/augeas/files%s/error", configfiles[i]);
-
-      if (STREQ (*match, errorpath)) {
-        /* Get the various error details. */
-        guestfs_push_error_handler (g, NULL, NULL);
-        CLEANUP_FREE char *messagepath =
-          safe_asprintf (g, "%s/message", errorpath);
-        CLEANUP_FREE char *message = guestfs_aug_get (g, messagepath);
-        CLEANUP_FREE char *linepath =
-          safe_asprintf (g, "%s/line", errorpath);
-        CLEANUP_FREE char *line = guestfs_aug_get (g, linepath);
-        CLEANUP_FREE char *charpath =
-          safe_asprintf (g, "%s/char", errorpath);
-        CLEANUP_FREE char *charp = guestfs_aug_get (g, charpath);
-        guestfs_pop_error_handler (g);
-
-        error (g, _("%s:%s:%s: augeas parse failure: %s"),
-               configfiles[i],
-               line ? : "<none>",
-               charp ? : "<none>",
-               message ? : "<none>");
-        goto out;
-      }
-    }
-  }
-
-  r = f (g, fs);
-
- out:
-  guestfs_aug_close (g);
-
-  return r;
-}
-
-/* Explained here: https://bugzilla.redhat.com/show_bug.cgi?id=975412#c0 */
-static char *
-make_augeas_path_expression (guestfs_h *g, const char **configfiles)
-{
-  size_t i;
-  size_t nr_files;
-  CLEANUP_FREE_STRING_LIST char **subexprs = NULL;
-  CLEANUP_FREE char *subexpr = NULL;
-  char *ret;
-
-  nr_files = guestfs_int_count_strings ((char **) configfiles);
-  subexprs = safe_malloc (g, sizeof (char *) * (nr_files + 1));
-
-  for (i = 0; i < nr_files; ++i) {
-    subexprs[i] = /*         v NB trailing '/' after filename */
-      safe_asprintf (g, "\"%s/\" !~ regexp('^') + glob(incl) + regexp('/.*')",
-                     configfiles[i]);
-  }
-  subexprs[nr_files] = NULL;
-
-  subexpr = guestfs_int_join_strings (" and ", subexprs);
-  if (subexpr == NULL)
-    g->abort_cb ();
-
-  ret = safe_asprintf (g, "/augeas/load/*[ %s ]", subexpr);
-  debug (g, "augeas pathexpr = %s", ret);
-  return ret;
-}
-
-/* Canonicalize the path, so "///usr//local//" -> "/usr/local"
- *
- * The path is modified in place because the result is always
- * the same length or shorter than the argument passed.
- */
-static void
-canonical_mountpoint (char *s)
-{
-  size_t len = strlen (s);
-  char *orig = s;
-
-  s = strchr (s, '/');
-  while (s != NULL && *s != 0) {
-    char *pos = s + 1;
-    char *p = pos;
-    /* Find how many consecutive slashes are there after the one found,
-     * and shift the characters after them accordingly. */
-    while (*p == '/')
-      ++p;
-    if (p - pos > 0) {
-      memmove (pos, p, len - (p - orig) + 1);
-      len -= p - pos;
-    }
-
-    s = strchr (pos, '/');
-  }
-  /* Ignore the trailing slash, but avoid removing it for "/". */
-  if (len > 1 && orig[len-1] == '/')
-    --len;
-  orig[len] = 0;
-}
diff --git a/lib/inspect-fs-windows.c b/lib/inspect-fs-windows.c
deleted file mode 100644
index 34f33c908..000000000
--- a/lib/inspect-fs-windows.c
+++ /dev/null
@@ -1,739 +0,0 @@
-/* libguestfs
- * Copyright (C) 2010-2012 Red Hat Inc.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; 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 <unistd.h>
-#include <string.h>
-#include <errno.h>
-#include <iconv.h>
-#include <inttypes.h>
-
-#ifdef HAVE_ENDIAN_H
-#include <endian.h>
-#endif
-#ifdef HAVE_SYS_ENDIAN_H
-#include <sys/endian.h>
-#endif
-
-#if defined __APPLE__ && defined __MACH__
-#include <libkern/OSByteOrder.h>
-#define le32toh(x) OSSwapLittleToHostInt32(x)
-#define le64toh(x) OSSwapLittleToHostInt64(x)
-#endif
-
-#include <pcre.h>
-
-#include "c-ctype.h"
-#include "ignore-value.h"
-
-#include "guestfs.h"
-#include "guestfs-internal.h"
-#include "guestfs-internal-actions.h"
-#include "structs-cleanups.h"
-
-COMPILE_REGEXP (re_windows_version, "^(\\d+)\\.(\\d+)", 0)
-COMPILE_REGEXP (re_boot_ini_os_header, "^\\[operating systems\\]\\s*$", 0)
-COMPILE_REGEXP (re_boot_ini_os,
-                "^(multi|scsi)\\((\\d+)\\)disk\\((\\d+)\\)rdisk\\((\\d+)\\)partition\\((\\d+)\\)([^=]+)=", 0)
-
-static int check_windows_arch (guestfs_h *g, struct inspect_fs *fs);
-static int check_windows_registry_paths (guestfs_h *g, struct inspect_fs *fs);
-static int check_windows_software_registry (guestfs_h *g, struct inspect_fs *fs);
-static int check_windows_system_registry (guestfs_h *g, struct inspect_fs *fs);
-static char *map_registry_disk_blob (guestfs_h *g, const void *blob);
-static char *map_registry_disk_blob_gpt (guestfs_h *g, const void *blob);
-static char *extract_guid_from_registry_blob (guestfs_h *g, const void *blob);
-
-/* XXX Handling of boot.ini in the Perl version was pretty broken.  It
- * essentially didn't do anything for modern Windows guests.
- * Therefore I've omitted all that code.
- */
-
-/* Try to find Windows systemroot using some common locations.
- *
- * Notes:
- *
- * (1) We check for some directories inside to see if it is a real
- * systemroot, and not just a directory that happens to have the same
- * name.
- *
- * (2) If a Windows guest has multiple disks and applications are
- * installed on those other disks, then those other disks will contain
- * "/Program Files" and "/System Volume Information".  Those would
- * *not* be Windows root disks.  (RHBZ#674130)
- */
-
-static int
-is_systemroot (guestfs_h *const g, const char *systemroot)
-{
-  CLEANUP_FREE char *path1 = NULL, *path2 = NULL, *path3 = NULL;
-
-  path1 = safe_asprintf (g, "%s/system32", systemroot);
-  if (!guestfs_int_is_dir_nocase (g, path1))
-    return 0;
-
-  path2 = safe_asprintf (g, "%s/system32/config", systemroot);
-  if (!guestfs_int_is_dir_nocase (g, path2))
-    return 0;
-
-  path3 = safe_asprintf (g, "%s/system32/cmd.exe", systemroot);
-  if (!guestfs_int_is_file_nocase (g, path3))
-    return 0;
-
-  return 1;
-}
-
-char *
-guestfs_int_get_windows_systemroot (guestfs_h *g)
-{
-  /* Check a predefined list of common windows system root locations */
-  static const char *systemroots[] =
-    { "/windows", "/winnt", "/win32", "/win", "/reactos", NULL };
-
-  for (size_t i = 0; i < sizeof systemroots / sizeof systemroots[0]; ++i) {
-    char *systemroot =
-      guestfs_int_case_sensitive_path_silently (g, systemroots[i]);
-    if (!systemroot)
-      continue;
-
-    if (is_systemroot (g, systemroot)) {
-      debug (g, "windows %%SYSTEMROOT%% = %s", systemroot);
-
-      return systemroot;
-    } else {
-      free (systemroot);
-    }
-  }
-
-  /* If the fs contains boot.ini, check it for non-standard
-   * systemroot locations */
-  CLEANUP_FREE char *boot_ini_path =
-    guestfs_int_case_sensitive_path_silently (g, "/boot.ini");
-  if (boot_ini_path && guestfs_is_file (g, boot_ini_path) > 0) {
-    CLEANUP_FREE_STRING_LIST char **boot_ini =
-      guestfs_read_lines (g, boot_ini_path);
-    if (!boot_ini) {
-      debug (g, "error reading %s", boot_ini_path);
-      return NULL;
-    }
-
-    int found_os = 0;
-    for (char **i = boot_ini; *i != NULL; i++) {
-      CLEANUP_FREE char *controller_type = NULL;
-      CLEANUP_FREE char *controller = NULL;
-      CLEANUP_FREE char *disk = NULL;
-      CLEANUP_FREE char *rdisk = NULL;
-      CLEANUP_FREE char *partition = NULL;
-      CLEANUP_FREE char *path = NULL;
-
-      char *line = *i;
-
-      if (!found_os) {
-        if (match (g, line, re_boot_ini_os_header)) {
-          found_os = 1;
-          continue;
-        }
-      }
-
-      /* See http://support.microsoft.com/kb/102873 for a discussion
-       * of what this line means */
-      if (match6 (g, line, re_boot_ini_os, &controller_type,
-                  &controller, &disk, &rdisk, &partition, &path))
-	{
-	  /* The Windows system root may be on any disk. However, there
-	   * are currently (at least) 2 practical problems preventing us
-	   * from locating it on another disk:
-	   *
-	   * 1. We don't have enough metadata about the disks we were
-	   * given to know if what controller they were on and what
-	   * index they had.
-	   *
-	   * 2. The way inspection of filesystems currently works, we
-	   * can't mark another filesystem, which we may have already
-	   * inspected, to be inspected for a specific Windows system
-	   * root.
-	   *
-	   * Solving 1 properly would require a new API at a minimum. We
-	   * might be able to fudge something practical without this,
-	   * though, e.g. by looking at the <partition>th partition of
-	   * every disk for the specific windows root.
-	   *
-	   * Solving 2 would probably require a significant refactoring
-	   * of the way filesystems are inspected. We should probably do
-	   * this some time.
-	   *
-	   * For the moment, we ignore all partition information and
-	   * assume the system root is on the current partition. In
-	   * practice, this will normally be correct.
-	   */
-
-	  /* Swap backslashes for forward slashes in the system root
-	   * path */
-	  for (char *j = path; *j != '\0'; j++) {
-	    if (*j == '\\') *j = '/';
-	  }
-
-	  char *systemroot = guestfs_int_case_sensitive_path_silently (g, path);
-	  if (systemroot && is_systemroot (g, systemroot)) {
-	    debug (g, "windows %%SYSTEMROOT%% = %s", systemroot);
-
-	    return systemroot;
-	  } else {
-	    free (systemroot);
-	  }
-	}
-    }
-  }
-
-  return NULL; /* not found */
-}
-
-int
-guestfs_int_check_windows_root (guestfs_h *g, struct inspect_fs *fs,
-				char *const systemroot)
-{
-  fs->type = OS_TYPE_WINDOWS;
-  fs->distro = OS_DISTRO_WINDOWS;
-
-  /* Freed by guestfs_int_free_inspect_info. */
-  fs->windows_systemroot = systemroot;
-
-  if (check_windows_arch (g, fs) == -1)
-    return -1;
-
-  /* Get system and software registry paths. */
-  if (check_windows_registry_paths (g, fs) == -1)
-    return -1;
-
-  /* Product name and version. */
-  if (check_windows_software_registry (g, fs) == -1)
-    return -1;
-
-  /* Hostname. */
-  if (check_windows_system_registry (g, fs) == -1)
-    return -1;
-
-  return 0;
-}
-
-static int
-check_windows_arch (guestfs_h *g, struct inspect_fs *fs)
-{
-  CLEANUP_FREE char *cmd_exe =
-    safe_asprintf (g, "%s/system32/cmd.exe", fs->windows_systemroot);
-
-  /* Should exist because of previous check above in get_windows_systemroot. */
-  CLEANUP_FREE char *cmd_exe_path = guestfs_case_sensitive_path (g, cmd_exe);
-  if (!cmd_exe_path)
-    return -1;
-
-  char *arch = guestfs_file_architecture (g, cmd_exe_path);
-  if (!arch)
-    return -1;
-
-  fs->arch = arch;        /* freed by guestfs_int_free_inspect_info */
-
-  return 0;
-}
-
-static int
-check_windows_registry_paths (guestfs_h *g, struct inspect_fs *fs)
-{
-  int r;
-  CLEANUP_FREE char *software = NULL, *system = NULL;
-
-  if (!fs->windows_systemroot)
-    return 0;
-
-  software = safe_asprintf (g, "%s/system32/config/software",
-                            fs->windows_systemroot);
-
-  fs->windows_software_hive = guestfs_case_sensitive_path (g, software);
-  if (!fs->windows_software_hive)
-    return -1;
-
-  r = guestfs_is_file (g, fs->windows_software_hive);
-  if (r == -1) {
-    free (fs->windows_software_hive);
-    fs->windows_software_hive = NULL;
-    return -1;
-  }
-
-  if (r == 0) {                 /* doesn't exist, or not a file */
-    free (fs->windows_software_hive);
-    fs->windows_software_hive = NULL;
-    /*FALLTHROUGH*/
-  }
-
-  system = safe_asprintf (g, "%s/system32/config/system",
-                          fs->windows_systemroot);
-
-  fs->windows_system_hive = guestfs_case_sensitive_path (g, system);
-  if (!fs->windows_system_hive)
-    return -1;
-
-  r = guestfs_is_file (g, fs->windows_system_hive);
-  if (r == -1) {
-    free (fs->windows_system_hive);
-    fs->windows_system_hive = NULL;
-    return -1;
-  }
-
-  if (r == 0) {                 /* doesn't exist, or not a file */
-    free (fs->windows_system_hive);
-    fs->windows_system_hive = NULL;
-    /*FALLTHROUGH*/
-  }
-
-  return 0;
-}
-
-/* At the moment, pull just the ProductName and version numbers from
- * the registry.  In future there is a case for making many more
- * registry fields available to callers.
- */
-static int
-check_windows_software_registry (guestfs_h *g, struct inspect_fs *fs)
-{
-  int ret = -1;
-  int64_t node;
-  const char *hivepath[] =
-    { "Microsoft", "Windows NT", "CurrentVersion" };
-  size_t i;
-  CLEANUP_FREE_HIVEX_VALUE_LIST struct guestfs_hivex_value_list *values = NULL;
-  bool ignore_currentversion = false;
-
-  /* If the software hive doesn't exist, just accept that we cannot
-   * find product_name etc.
-   */
-  if (!fs->windows_software_hive)
-    return 0;
-
-  if (guestfs_hivex_open (g, fs->windows_software_hive,
-                          GUESTFS_HIVEX_OPEN_VERBOSE, g->verbose,
-                          GUESTFS_HIVEX_OPEN_UNSAFE, 1,
-                          -1) == -1)
-    return -1;
-
-  node = guestfs_hivex_root (g);
-  for (i = 0; node > 0 && i < sizeof hivepath / sizeof hivepath[0]; ++i)
-    node = guestfs_hivex_node_get_child (g, node, hivepath[i]);
-
-  if (node == -1)
-    goto out;
-
-  if (node == 0) {
-    perrorf (g, "hivex: cannot locate HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion");
-    goto out;
-  }
-
-  values = guestfs_hivex_node_values (g, node);
-
-  for (i = 0; i < values->len; ++i) {
-    const int64_t value = values->val[i].hivex_value_h;
-    CLEANUP_FREE char *key = guestfs_hivex_value_key (g, value);
-    if (key == NULL)
-      goto out;
-
-    if (STRCASEEQ (key, "ProductName")) {
-      fs->product_name = guestfs_hivex_value_utf8 (g, value);
-      if (!fs->product_name)
-        goto out;
-    }
-    else if (STRCASEEQ (key, "CurrentMajorVersionNumber")) {
-      size_t vsize;
-      const int64_t vtype = guestfs_hivex_value_type (g, value);
-      CLEANUP_FREE char *vbuf = guestfs_hivex_value_value (g, value, &vsize);
-
-      if (vbuf == NULL)
-        goto out;
-      if (vtype != 4 || vsize != 4) {
-        error (g, "hivex: expected CurrentVersion\\%s to be a DWORD field",
-               "CurrentMajorVersionNumber");
-        goto out;
-      }
-
-      fs->version.v_major = le32toh (*(int32_t *)vbuf);
-
-      /* Ignore CurrentVersion if we see it after this key. */
-      ignore_currentversion = true;
-    }
-    else if (STRCASEEQ (key, "CurrentMinorVersionNumber")) {
-      size_t vsize;
-      const int64_t vtype = guestfs_hivex_value_type (g, value);
-      CLEANUP_FREE char *vbuf = guestfs_hivex_value_value (g, value, &vsize);
-
-      if (vbuf == NULL)
-        goto out;
-      if (vtype != 4 || vsize != 4) {
-        error (g, "hivex: expected CurrentVersion\\%s to be a DWORD field",
-               "CurrentMinorVersionNumber");
-        goto out;
-      }
-
-      fs->version.v_minor = le32toh (*(int32_t *)vbuf);
-
-      /* Ignore CurrentVersion if we see it after this key. */
-      ignore_currentversion = true;
-    }
-    else if (!ignore_currentversion && STRCASEEQ (key, "CurrentVersion")) {
-      CLEANUP_FREE char *version = guestfs_hivex_value_utf8 (g, value);
-      if (!version)
-        goto out;
-      if (guestfs_int_version_from_x_y_re (g, &fs->version, version,
-                                           re_windows_version) == -1)
-        goto out;
-    }
-    else if (STRCASEEQ (key, "InstallationType")) {
-      fs->product_variant = guestfs_hivex_value_utf8 (g, value);
-      if (!fs->product_variant)
-        goto out;
-    }
-  }
-
-  ret = 0;
-
- out:
-  guestfs_hivex_close (g);
-
-  return ret;
-}
-
-static int
-check_windows_system_registry (guestfs_h *g, struct inspect_fs *fs)
-{
-  static const char gpt_prefix[] = "DMIO:ID:";
-  int ret = -1;
-  int64_t root, node, value;
-  CLEANUP_FREE_HIVEX_VALUE_LIST struct guestfs_hivex_value_list *values = NULL;
-  CLEANUP_FREE_HIVEX_VALUE_LIST struct guestfs_hivex_value_list *values2 = NULL;
-  int32_t dword;
-  size_t i, count;
-  CLEANUP_FREE void *buf = NULL;
-  size_t buflen;
-  const char *hivepath[] =
-    { NULL /* current control set */, "Services", "Tcpip", "Parameters" };
-
-  /* If the system hive doesn't exist, just accept that we cannot
-   * find hostname etc.
-   */
-  if (!fs->windows_system_hive)
-    return 0;
-
-  if (guestfs_hivex_open (g, fs->windows_system_hive,
-                          GUESTFS_HIVEX_OPEN_VERBOSE, g->verbose,
-                          GUESTFS_HIVEX_OPEN_UNSAFE, 1,
-                          -1) == -1)
-    goto out;
-
-  root = guestfs_hivex_root (g);
-  if (root == 0)
-    goto out;
-
-  /* Get the CurrentControlSet. */
-  node = guestfs_hivex_node_get_child (g, root, "Select");
-  if (node == -1)
-    goto out;
-
-  if (node == 0) {
-    error (g, "hivex: could not locate HKLM\\SYSTEM\\Select");
-    goto out;
-  }
-
-  value = guestfs_hivex_node_get_value (g, node, "Current");
-  if (value == -1)
-    goto out;
-
-  if (value == 0) {
-    error (g, "hivex: HKLM\\System\\Select Default entry not found");
-    goto out;
-  }
-
-  /* XXX Should check the type. */
-  buf = guestfs_hivex_value_value (g, value, &buflen);
-  if (buflen != 4) {
-    error (g, "hivex: HKLM\\System\\Select\\Current expected to be DWORD");
-    goto out;
-  }
-  dword = le32toh (*(int32_t *)buf);
-  fs->windows_current_control_set = safe_asprintf (g, "ControlSet%03d", dword);
-
-  /* Get the drive mappings.
-   * This page explains the contents of HKLM\System\MountedDevices:
-   * http://www.goodells.net/multiboot/partsigs.shtml
-   */
-  node = guestfs_hivex_node_get_child (g, root, "MountedDevices");
-  if (node == -1)
-    goto out;
-
-  if (node == 0)
-    /* Not found: skip getting drive letter mappings (RHBZ#803664). */
-    goto skip_drive_letter_mappings;
-
-  values = guestfs_hivex_node_values (g, node);
-
-  /* Count how many DOS drive letter mappings there are.  This doesn't
-   * ignore removable devices, so it overestimates, but that doesn't
-   * matter because it just means we'll allocate a few bytes extra.
-   */
-  for (i = count = 0; i < values->len; ++i) {
-    CLEANUP_FREE char *key =
-      guestfs_hivex_value_key (g, values->val[i].hivex_value_h);
-    if (key == NULL)
-      goto out;
-    if (STRCASEEQLEN (key, "\\DosDevices\\", 12) &&
-        c_isalpha (key[12]) && key[13] == ':')
-      count++;
-  }
-
-  fs->drive_mappings = safe_calloc (g, 2*count + 1, sizeof (char *));
-
-  for (i = count = 0; i < values->len; ++i) {
-    const int64_t v = values->val[i].hivex_value_h;
-    CLEANUP_FREE char *key = guestfs_hivex_value_key (g, v);
-    if (key == NULL)
-      goto out;
-    if (STRCASEEQLEN (key, "\\DosDevices\\", 12) &&
-        c_isalpha (key[12]) && key[13] == ':') {
-      /* Get the binary value.  Is it a fixed disk? */
-      CLEANUP_FREE char *blob = NULL;
-      char *device;
-      int64_t type;
-      bool is_gpt;
-      size_t len;
-
-      type = guestfs_hivex_value_type (g, v);
-      blob = guestfs_hivex_value_value (g, v, &len);
-      is_gpt = memcmp (blob, gpt_prefix, 8) == 0;
-      if (blob != NULL && type == 3 && (len == 12 || is_gpt)) {
-        /* Try to map the blob to a known disk and partition. */
-        if (is_gpt)
-          device = map_registry_disk_blob_gpt (g, blob);
-        else
-          device = map_registry_disk_blob (g, blob);
-
-        if (device != NULL) {
-          fs->drive_mappings[count++] = safe_strndup (g, &key[12], 1);
-          fs->drive_mappings[count++] = device;
-        }
-      }
-    }
-  }
-
- skip_drive_letter_mappings:;
-  /* Get the hostname. */
-  hivepath[0] = fs->windows_current_control_set;
-  for (node = root, i = 0;
-       node > 0 && i < sizeof hivepath / sizeof hivepath[0];
-       ++i) {
-    node = guestfs_hivex_node_get_child (g, node, hivepath[i]);
-  }
-
-  if (node == -1)
-    goto out;
-
-  if (node == 0) {
-    perrorf (g, "hivex: cannot locate HKLM\\SYSTEM\\%s\\Services\\Tcpip\\Parameters",
-             fs->windows_current_control_set);
-    goto out;
-  }
-
-  values2 = guestfs_hivex_node_values (g, node);
-  if (values2 == NULL)
-    goto out;
-
-  for (i = 0; i < values2->len; ++i) {
-    const int64_t v = values2->val[i].hivex_value_h;
-    CLEANUP_FREE char *key = guestfs_hivex_value_key (g, v);
-    if (key == NULL)
-      goto out;
-
-    if (STRCASEEQ (key, "Hostname")) {
-      fs->hostname = guestfs_hivex_value_utf8 (g, v);
-      if (!fs->hostname)
-        goto out;
-    }
-    /* many other interesting fields here ... */
-  }
-
-  ret = 0;
-
- out:
-  guestfs_hivex_close (g);
-
-  return ret;
-}
-
-/* Windows Registry HKLM\SYSTEM\MountedDevices uses a blob of data
- * to store partitions.  This blob is described here:
- * http://www.goodells.net/multiboot/partsigs.shtml
- * The following function maps this blob to a libguestfs partition
- * name, if possible.
- */
-static char *
-map_registry_disk_blob (guestfs_h *g, const void *blob)
-{
-  CLEANUP_FREE_STRING_LIST char **devices = NULL;
-  CLEANUP_FREE_PARTITION_LIST struct guestfs_partition_list *partitions = NULL;
-  size_t i, j, len;
-  uint64_t part_offset;
-
-  /* First 4 bytes are the disk ID.  Search all devices to find the
-   * disk with this disk ID.
-   */
-  devices = guestfs_list_devices (g);
-  if (devices == NULL)
-    return NULL;
-
-  for (i = 0; devices[i] != NULL; ++i) {
-    /* Read the disk ID. */
-    CLEANUP_FREE char *diskid =
-      guestfs_pread_device (g, devices[i], 4, 0x01b8, &len);
-    if (diskid == NULL)
-      continue;
-    if (len < 4)
-      continue;
-    if (memcmp (diskid, blob, 4) == 0) /* found it */
-      goto found_disk;
-  }
-  return NULL;
-
- found_disk:
-  /* Next 8 bytes are the offset of the partition in bytes(!) given as
-   * a 64 bit little endian number.  Luckily it's easy to get the
-   * partition byte offset from guestfs_part_list.
-   */
-  memcpy (&part_offset, (char *) blob + 4, sizeof (part_offset));
-  part_offset = le64toh (part_offset);
-
-  partitions = guestfs_part_list (g, devices[i]);
-  if (partitions == NULL)
-    return NULL;
-
-  for (j = 0; j < partitions->len; ++j) {
-    if (partitions->val[j].part_start == part_offset) /* found it */
-      goto found_partition;
-  }
-  return NULL;
-
- found_partition:
-  /* Construct the full device name. */
-  return safe_asprintf (g, "%s%d", devices[i], partitions->val[j].part_num);
-}
-
-/* Matches Windows registry HKLM\SYSYTEM\MountedDevices\DosDevices blob to
- * to libguestfs GPT partition device. For GPT disks, the blob is made of
- * "DMIO:ID:" prefix followed by the GPT partition GUID.
- */
-static char *
-map_registry_disk_blob_gpt (guestfs_h *g, const void *blob)
-{
-  CLEANUP_FREE_STRING_LIST char **parts = NULL;
-  CLEANUP_FREE char *blob_guid = extract_guid_from_registry_blob (g, blob);
-  size_t i;
-
-  parts = guestfs_list_partitions (g);
-  if (parts == NULL)
-    return NULL;
-
-  for (i = 0; parts[i] != NULL; ++i) {
-    CLEANUP_FREE char *fs_guid = NULL;
-    int partnum;
-    CLEANUP_FREE char *device = NULL;
-    CLEANUP_FREE char *type = NULL;
-
-    partnum = guestfs_part_to_partnum (g, parts[i]);
-    if (partnum == -1)
-      continue;
-
-    device = guestfs_part_to_dev (g, parts[i]);
-    if (device == NULL)
-      continue;
-
-    type = guestfs_part_get_parttype (g, device);
-    if (type == NULL)
-      continue;
-
-    if (STRCASENEQ (type, "gpt"))
-      continue;
-
-    /* get the GPT parition GUID from the partition block device */
-    fs_guid = guestfs_part_get_gpt_guid (g, device, partnum);
-    if (fs_guid == NULL)
-      continue;
-
-    /* if both GUIDs match, we have found the mapping for our device */
-    if (STRCASEEQ (fs_guid, blob_guid))
-      return safe_strdup (g, parts[i]);
-  }
-
-  return NULL;
-}
-
-/* Extracts the binary GUID stored in blob from Windows registry
- * HKLM\SYSTYEM\MountedDevices\DosDevices value and converts it to a
- * GUID string so that it can be matched against libguestfs partition
- * device GPT GUID.
- */
-static char *
-extract_guid_from_registry_blob (guestfs_h *g, const void *blob)
-{
-  char guid_bytes[16];
-  uint32_t data1;
-  uint16_t data2, data3;
-  uint64_t data4;
-
-  /* get the GUID bytes from blob (skip 8 byte "DMIO:ID:" prefix) */
-  memcpy (&guid_bytes, (char *) blob + 8, sizeof (guid_bytes));
-
-  /* copy relevant sections from blob to respective ints */
-  memcpy (&data1, guid_bytes, sizeof (data1));
-  memcpy (&data2, guid_bytes + 4, sizeof (data2));
-  memcpy (&data3, guid_bytes + 6, sizeof (data3));
-  memcpy (&data4, guid_bytes + 8, sizeof (data4));
-
-  /* ensure proper endianness */
-  data1 = le32toh (data1);
-  data2 = le16toh (data2);
-  data3 = le16toh (data3);
-  data4 = be64toh (data4);
-
-  return safe_asprintf (g,
-           "%08" PRIX32 "-%04" PRIX16 "-%04" PRIX16 "-%04" PRIX64 "-%012" PRIX64,
-           data1, data2, data3, data4 >> 48, data4 & 0xffffffffffff);
-}
-
-/* NB: This function DOES NOT test for the existence of the file.  It
- * will return non-NULL even if the file/directory does not exist.
- * You have to call guestfs_is_file{,_opts} etc.
- */
-char *
-guestfs_int_case_sensitive_path_silently (guestfs_h *g, const char *path)
-{
-  char *ret;
-
-  guestfs_push_error_handler (g, NULL, NULL);
-  ret = guestfs_case_sensitive_path (g, path);
-  guestfs_pop_error_handler (g);
-
-  return ret;
-}
diff --git a/lib/inspect-fs.c b/lib/inspect-fs.c
deleted file mode 100644
index e320b3e78..000000000
--- a/lib/inspect-fs.c
+++ /dev/null
@@ -1,758 +0,0 @@
-/* libguestfs
- * Copyright (C) 2010-2012 Red Hat Inc.
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2 of the License, or (at your option) any later version.
- *
- * This library 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
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; 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 <unistd.h>
-#include <string.h>
-#include <libintl.h>
-
-#ifdef HAVE_ENDIAN_H
-#include <endian.h>
-#endif
-
-#include <pcre.h>
-
-#include "ignore-value.h"
-#include "xstrtol.h"
-
-#include "guestfs.h"
-#include "guestfs-internal.h"
-#include "structs-cleanups.h"
-
-static int check_filesystem (guestfs_h *g, const char *mountable,
-                             const struct guestfs_internal_mountable *m,
-                             int whole_device);
-static void extend_fses (guestfs_h *g);
-static int get_partition_context (guestfs_h *g, const char *partition, int *partnum_ret, int *nr_partitions_ret);
-static int is_symlink_to (guestfs_h *g, const char *file, const char *wanted_target);
-
-/* Find out if 'device' contains a filesystem.  If it does, add
- * another entry in g->fses.
- */
-int
-guestfs_int_check_for_filesystem_on (guestfs_h *g, const char *mountable)
-{
-  CLEANUP_FREE char *vfs_type = NULL;
-  int is_swap, r;
-  struct inspect_fs *fs;
-  CLEANUP_FREE_INTERNAL_MOUNTABLE struct guestfs_internal_mountable *m = NULL;
-  int whole_device = 0;
-
-  /* Get vfs-type in order to check if it's a Linux(?) swap device.
-   * If there's an error we should ignore it, so to do that we have to
-   * temporarily replace the error handler with a null one.
-   */
-  guestfs_push_error_handler (g, NULL, NULL);
-  vfs_type = guestfs_vfs_type (g, mountable);
-  guestfs_pop_error_handler (g);
-
-  is_swap = vfs_type && STREQ (vfs_type, "swap");
-  debug (g, "check_for_filesystem_on: %s (%s)",
-         mountable, vfs_type ? vfs_type : "failed to get vfs type");
-
-  if (is_swap) {
-    extend_fses (g);
-    fs = &g->fses[g->nr_fses-1];
-    fs->mountable = safe_strdup (g, mountable);
-    return 0;
-  }
-
-  m = guestfs_internal_parse_mountable (g, mountable);
-  if (m == NULL)
-    return -1;
-
-  /* If it's a whole device, see if it is an install ISO. */
-  if (m->im_type == MOUNTABLE_DEVICE) {
-    whole_device = guestfs_is_whole_device (g, m->im_device);
-    if (whole_device == -1) {
-      return -1;
-    }
-  }
-
-  /* Try mounting the device.  As above, ignore errors. */
-  guestfs_push_error_handler (g, NULL, NULL);
-  if (vfs_type && STREQ (vfs_type, "ufs")) { /* Hack for the *BSDs. */
-    /* FreeBSD fs is a variant of ufs called ufs2 ... */
-    r = guestfs_mount_vfs (g, "ro,ufstype=ufs2", "ufs", mountable, "/");
-    if (r == -1)
-      /* while NetBSD and OpenBSD use another variant labeled 44bsd */
-      r = guestfs_mount_vfs (g, "ro,ufstype=44bsd", "ufs", mountable, "/");
-  } else {
-    r = guestfs_mount_ro (g, mountable, "/");
-  }
-  guestfs_pop_error_handler (g);
-  if (r == -1)
-    return 0;
-
-  /* Do the rest of the checks. */
-  r = check_filesystem (g, mountable, m, whole_device);
-
-  /* Unmount the filesystem. */
-  if (guestfs_umount_all (g) == -1)
-    return -1;
-
-  return r;
-}
-
-static int
-check_filesystem (guestfs_h *g, const char *mountable,
-                  const struct guestfs_internal_mountable *m,
-                  int whole_device)
-{
-  int partnum = -1, nr_partitions = -1;
-  /* Not CLEANUP_FREE, as it will be cleaned up with inspection info */
-  char *windows_systemroot = NULL;
-
-  extend_fses (g);
-
-  if (!whole_device && m->im_type == MOUNTABLE_DEVICE &&
-      guestfs_int_is_partition (g, m->im_device)) {
-    if (get_partition_context (g, m->im_device,
-                               &partnum, &nr_partitions) == -1)
-      return -1;
-  }
-
-  struct inspect_fs *fs = &g->fses[g->nr_fses-1];
-
-  fs->mountable = safe_strdup (g, mountable);
-
-  /* Optimize some of the tests by avoiding multiple tests of the same thing. */
-  const int is_dir_etc = guestfs_is_dir (g, "/etc") > 0;
-  const int is_dir_bin = guestfs_is_dir (g, "/bin") > 0;
-  const int is_dir_share = guestfs_is_dir (g, "/share") > 0;
-
-  /* Grub /boot? */
-  if (guestfs_is_file (g, "/grub/menu.lst") > 0 ||
-      guestfs_is_file (g, "/grub/grub.conf") > 0 ||
-      guestfs_is_file (g, "/grub2/grub.cfg") > 0)
-    ;
-  /* FreeBSD root? */
-  else if (is_dir_etc &&
-           is_dir_bin &&
-           guestfs_is_file (g, "/etc/freebsd-update.conf") > 0 &&
-           guestfs_is_file (g, "/etc/fstab") > 0) {
-    fs->role = OS_ROLE_ROOT;
-    fs->format = OS_FORMAT_INSTALLED;
-    if (guestfs_int_check_freebsd_root (g, fs) == -1)
-      return -1;
-  }
-  /* NetBSD root? */
-  else if (is_dir_etc &&
-           is_dir_bin &&
-           guestfs_is_file (g, "/netbsd") > 0 &&
-           guestfs_is_file (g, "/etc/fstab") > 0 &&
-           guestfs_is_file (g, "/etc/release") > 0) {
-    fs->role = OS_ROLE_ROOT;
-    fs->format = OS_FORMAT_INSTALLED;
-    if (guestfs_int_check_netbsd_root (g, fs) == -1)
-      return -1;
-  }
-  /* OpenBSD root? */
-  else if (is_dir_etc &&
-           is_dir_bin &&
-           guestfs_is_file (g, "/bsd") > 0 &&
-           guestfs_is_file (g, "/etc/fstab") > 0 &&
-           guestfs_is_file (g, "/etc/motd") > 0) {
-    fs->role = OS_ROLE_ROOT;
-    fs->format = OS_FORMAT_INSTALLED;
-    if (guestfs_int_check_openbsd_root (g, fs) == -1)
-      return -1;
-  }
-  /* Hurd root? */
-  else if (guestfs_is_file (g, "/hurd/console") > 0 &&
-           guestfs_is_file (g, "/hurd/hello") > 0 &&
-           guestfs_is_file (g, "/hurd/null") > 0) {
-    fs->role = OS_ROLE_ROOT;
-    fs->format = OS_FORMAT_INSTALLED; /* XXX could be more specific */
-    if (guestfs_int_check_hurd_root (g, fs) == -1)
-      return -1;
-  }
-  /* Minix root? */
-  else if (is_dir_etc &&
-           is_dir_bin &&
-           guestfs_is_file (g, "/service/vm") > 0 &&
-           guestfs_is_file (g, "/etc/fstab") > 0 &&
-           guestfs_is_file (g, "/etc/version") > 0) {
-    fs->role = OS_ROLE_ROOT;
-    fs->format = OS_FORMAT_INSTALLED;
-    if (guestfs_int_check_minix_root (g, fs) == -1)
-      return -1;
-  }
-  /* Linux root? */
-  else if (is_dir_etc &&
-           (is_dir_bin ||
-            is_symlink_to (g, "/bin", "usr/bin") > 0) &&
-           (guestfs_is_file (g, "/etc/fstab") > 0 ||
-            guestfs_is_file (g, "/etc/hosts") > 0)) {
-    fs->role = OS_ROLE_ROOT;
-    fs->format = OS_FORMAT_INSTALLED;
-    if (guestfs_int_check_linux_root (g, fs) == -1)
-      return -1;
-  }
-  /* CoreOS root? */
-  else if (is_dir_etc &&
-           guestfs_is_dir (g, "/root") > 0 &&
-           guestfs_is_dir (g, "/home") > 0 &&
-           guestfs_is_dir (g, "/usr") > 0 &&
-           guestfs_is_file (g, "/etc/coreos/update.conf") > 0) {
-    fs->role = OS_ROLE_ROOT;
-    fs->format = OS_FORMAT_INSTALLED;
-    if (guestfs_int_check_coreos_root (g, fs) == -1)
-      return -1;
-  }
-  /* Linux /usr/local? */
-  else if (is_dir_etc &&
-           is_dir_bin &&
-           is_dir_share &&
-           guestfs_is_dir (g, "/local") == 0 &&
-           guestfs_is_file (g, "/etc/fstab") == 0)
-    ;
-  /* Linux /usr? */
-  else if (is_dir_etc &&
-           is_dir_bin &&
-           is_dir_share &&
-           guestfs_is_dir (g, "/local") > 0 &&
-           guestfs_is_file (g, "/etc/fstab") == 0) {
-    if (guestfs_int_check_linux_usr (g, fs) == -1)
-      return -1;
-  }
-  /* CoreOS /usr? */
-  else if (is_dir_bin &&
-           is_dir_share &&
-           guestfs_is_dir (g, "/local") > 0 &&
-           guestfs_is_dir (g, "/share/coreos") > 0) {
-    if (guestfs_int_check_coreos_usr (g, fs) == -1)
-      return -1;
-  }
-  /* Linux /var? */
-  else if (guestfs_is_dir (g, "/log") > 0 &&
-           guestfs_is_dir (g, "/run") > 0 &&
-           guestfs_is_dir (g, "/spool") > 0)
-    ;
-  /* Windows root? */
-  else if ((windows_systemroot = guestfs_int_get_windows_systemroot (g)) != NULL)
-    {
-      fs->role = OS_ROLE_ROOT;
-      fs->format = OS_FORMAT_INSTALLED;
-      if (guestfs_int_check_windows_root (g, fs, windows_systemroot) == -1)
-	return -1;
-    }
-  /* Windows volume with installed applications (but not root)? */
-  else if (guestfs_int_is_dir_nocase (g, "/System Volume Information") > 0 &&
-           guestfs_int_is_dir_nocase (g, "/Program Files") > 0)
-    ;
-  /* Windows volume (but not root)? */
-  else if (guestfs_int_is_dir_nocase (g, "/System Volume Information") > 0)
-    ;
-  /* FreeDOS? */
-  else if (guestfs_int_is_dir_nocase (g, "/FDOS") > 0 &&
-           guestfs_int_is_file_nocase (g, "/FDOS/FREEDOS.BSS") > 0) {
-    fs->role = OS_ROLE_ROOT;
-    fs->format = OS_FORMAT_INSTALLED;
-    fs->type = OS_TYPE_DOS;
-    fs->distro = OS_DISTRO_FREEDOS;
-    /* FreeDOS is a mix of 16 and 32 bit, but assume it requires a
-     * 32 bit i386 processor.
-     */
-    fs->arch = safe_strdup (g, "i386");
-  }
-
-  /* The above code should have set fs->type and fs->distro fields, so
-   * we can now guess the package management system.
-   */
-  guestfs_int_check_package_format (g, fs);
-  guestfs_int_check_package_management (g, fs);
-
-  return 0;
-}
-
-static void
-extend_fses (guestfs_h *g)
-{
-  const size_t n = g->nr_fses + 1;
-  struct inspect_fs *p;
-
-  p = safe_realloc (g, g->fses, n * sizeof (struct inspect_fs));
-
-  g->fses = p;
-  g->nr_fses = n;
-
-  memset (&g->fses[n-1], 0, sizeof (struct inspect_fs));
-}
-
-/* Given a partition (eg. /dev/sda2) then return the partition number
- * (eg. 2) and the total number of other partitions.
- */
-static int
-get_partition_context (guestfs_h *g, const char *partition,
-                       int *partnum_ret, int *nr_partitions_ret)
-{
-  int partnum, nr_partitions;
-  CLEANUP_FREE char *device = NULL;
-  CLEANUP_FREE_PARTITION_LIST struct guestfs_partition_list *partitions = NULL;
-
-  partnum = guestfs_part_to_partnum (g, partition);
-  if (partnum == -1)
-    return -1;
-
-  device = guestfs_part_to_dev (g, partition);
-  if (device == NULL)
-    return -1;
-
-  partitions = guestfs_part_list (g, device);
-  if (partitions == NULL)
-    return -1;
-
-  nr_partitions = partitions->len;
-
-  *partnum_ret = partnum;
-  *nr_partitions_ret = nr_partitions;
-  return 0;
-}
-
-static int
-is_symlink_to (guestfs_h *g, const char *file, const char *wanted_target)
-{
-  CLEANUP_FREE char *target = NULL;
-
-  if (guestfs_is_symlink (g, file) == 0)
-    return 0;
-
-  target = guestfs_readlink (g, file);
-  /* This should not fail, but play safe. */
-  if (target == NULL)
-    return 0;
-
-  return STREQ (target, wanted_target);
-}
-
-int
-guestfs_int_is_file_nocase (guestfs_h *g, const char *path)
-{
-  CLEANUP_FREE char *p = NULL;
-  int r;
-
-  p = guestfs_int_case_sensitive_path_silently (g, path);
-  if (!p)
-    return 0;
-  r = guestfs_is_file (g, p);
-  return r > 0;
-}
-
-int
-guestfs_int_is_dir_nocase (guestfs_h *g, const char *path)
-{
-  CLEANUP_FREE char *p = NULL;
-  int r;
-
-  p = guestfs_int_case_sensitive_path_silently (g, path);
-  if (!p)
-    return 0;
-  r = guestfs_is_dir (g, p);
-  return r > 0;
-}
-
-/* Parse small, unsigned ints, as used in version numbers. */
-int
-guestfs_int_parse_unsigned_int (guestfs_h *g, const char *str)
-{
-  long ret;
-  const int r = xstrtol (str, NULL, 10, &ret, "");
-  if (r != LONGINT_OK) {
-    error (g, _("could not parse integer in version number: %s"), str);
-    return -1;
-  }
-  return ret;
-}
-
-/* Like parse_unsigned_int, but ignore trailing stuff. */
-int
-guestfs_int_parse_unsigned_int_ignore_trailing (guestfs_h *g, const char *str)
-{
-  long ret;
-  const int r = xstrtol (str, NULL, 10, &ret, NULL);
-  if (r != LONGINT_OK) {
-    error (g, _("could not parse integer in version number: %s"), str);
-    return -1;
-  }
-  return ret;
-}
-
-/* Parse generic MAJOR.MINOR from the fs->product_name string. */
-int
-guestfs_int_parse_major_minor (guestfs_h *g, struct inspect_fs *fs)
-{
-  if (guestfs_int_version_from_x_y (g, &fs->version, fs->product_name) == -1)
-    return -1;
-
-  return 0;
-}
-
-/* At the moment, package format and package management is just a
- * simple function of the distro and version.v_major fields, so these
- * can never return an error.  We might be cleverer in future.
- */
-void
-guestfs_int_check_package_format (guestfs_h *g, struct inspect_fs *fs)
-{
-  switch (fs->distro) {
-  case OS_DISTRO_FEDORA:
-  case OS_DISTRO_MEEGO:
-  case OS_DISTRO_REDHAT_BASED:
-  case OS_DISTRO_RHEL:
-  case OS_DISTRO_MAGEIA:
-  case OS_DISTRO_MANDRIVA:
-  case OS_DISTRO_SUSE_BASED:
-  case OS_DISTRO_OPENSUSE:
-  case OS_DISTRO_SLES:
-  case OS_DISTRO_CENTOS:
-  case OS_DISTRO_SCIENTIFIC_LINUX:
-  case OS_DISTRO_ORACLE_LINUX:
-  case OS_DISTRO_ALTLINUX:
-    fs->package_format = OS_PACKAGE_FORMAT_RPM;
-    break;
-
-  case OS_DISTRO_DEBIAN:
-  case OS_DISTRO_UBUNTU:
-  case OS_DISTRO_LINUX_MINT:
-    fs->package_format = OS_PACKAGE_FORMAT_DEB;
-    break;
-
-  case OS_DISTRO_ARCHLINUX:
-    fs->package_format = OS_PACKAGE_FORMAT_PACMAN;
-    break;
-  case OS_DISTRO_GENTOO:
-    fs->package_format = OS_PACKAGE_FORMAT_EBUILD;
-    break;
-  case OS_DISTRO_PARDUS:
-    fs->package_format = OS_PACKAGE_FORMAT_PISI;
-    break;
-
-  case OS_DISTRO_ALPINE_LINUX:
-    fs->package_format = OS_PACKAGE_FORMAT_APK;
-    break;
-
-  case OS_DISTRO_VOID_LINUX:
-    fs->package_format = OS_PACKAGE_FORMAT_XBPS;
-    break;
-
-  case OS_DISTRO_SLACKWARE:
-  case OS_DISTRO_TTYLINUX:
-  case OS_DISTRO_COREOS:
-  case OS_DISTRO_WINDOWS:
-  case OS_DISTRO_BUILDROOT:
-  case OS_DISTRO_CIRROS:
-  case OS_DISTRO_FREEDOS:
-  case OS_DISTRO_FREEBSD:
-  case OS_DISTRO_NETBSD:
-  case OS_DISTRO_OPENBSD:
-  case OS_DISTRO_FRUGALWARE:
-  case OS_DISTRO_PLD_LINUX:
-  case OS_DISTRO_UNKNOWN:
-    fs->package_format = OS_PACKAGE_FORMAT_UNKNOWN;
-    break;
-  }
-}
-
-void
-guestfs_int_check_package_management (guestfs_h *g, struct inspect_fs *fs)
-{
-  switch (fs->distro) {
-  case OS_DISTRO_MEEGO:
-    fs->package_management = OS_PACKAGE_MANAGEMENT_YUM;
-    break;
-
-  case OS_DISTRO_FEDORA:
-    /* If Fedora >= 22 and dnf is installed, say "dnf". */
-    if (guestfs_int_version_ge (&fs->version, 22, 0, 0) &&
-        guestfs_is_file_opts (g, "/usr/bin/dnf",
-                              GUESTFS_IS_FILE_OPTS_FOLLOWSYMLINKS, 1, -1) > 0)
-      fs->package_management = OS_PACKAGE_MANAGEMENT_DNF;
-    else if (guestfs_int_version_ge (&fs->version, 1, 0, 0))
-      fs->package_management = OS_PACKAGE_MANAGEMENT_YUM;
-    else
-      /* Probably parsing the release file failed, see RHBZ#1332025. */
-      fs->package_management = OS_PACKAGE_MANAGEMENT_UNKNOWN;
-    break;
-
-  case OS_DISTRO_REDHAT_BASED:
-  case OS_DISTRO_RHEL:
-  case OS_DISTRO_CENTOS:
-  case OS_DISTRO_SCIENTIFIC_LINUX:
-  case OS_DISTRO_ORACLE_LINUX:
-    if (guestfs_int_version_ge (&fs->version, 5, 0, 0))
-      fs->package_management = OS_PACKAGE_MANAGEMENT_YUM;
-    else if (guestfs_int_version_ge (&fs->version, 2, 0, 0))
-      fs->package_management = OS_PACKAGE_MANAGEMENT_UP2DATE;
-    else
-      /* Probably parsing the release file failed, see RHBZ#1332025. */
-      fs->package_management = OS_PACKAGE_MANAGEMENT_UNKNOWN;
-    break;
-
-  case OS_DISTRO_DEBIAN:
-  case OS_DISTRO_UBUNTU:
-  case OS_DISTRO_LINUX_MINT:
-  case OS_DISTRO_ALTLINUX:
-    fs->package_management = OS_PACKAGE_MANAGEMENT_APT;
-    break;
-
-  case OS_DISTRO_ARCHLINUX:
-    fs->package_management = OS_PACKAGE_MANAGEMENT_PACMAN;
-    break;
-  case OS_DISTRO_GENTOO:
-    fs->package_management = OS_PACKAGE_MANAGEMENT_PORTAGE;
-    break;
-  case OS_DISTRO_PARDUS:
-    fs->package_management = OS_PACKAGE_MANAGEMENT_PISI;
-    break;
-  case OS_DISTRO_MAGEIA:
-  case OS_DISTRO_MANDRIVA:
-    fs->package_management = OS_PACKAGE_MANAGEMENT_URPMI;
-    break;
-
-  case OS_DISTRO_SUSE_BASED:
-  case OS_DISTRO_OPENSUSE:
-  case OS_DISTRO_SLES:
-    fs->package_management = OS_PACKAGE_MANAGEMENT_ZYPPER;
-    break;
-
-  case OS_DISTRO_ALPINE_LINUX:
-    fs->package_management = OS_PACKAGE_MANAGEMENT_APK;
-    break;
-
-  case OS_DISTRO_VOID_LINUX:
-    fs->package_management = OS_PACKAGE_MANAGEMENT_XBPS;
-    break;
-
-  case OS_DISTRO_SLACKWARE:
-  case OS_DISTRO_TTYLINUX:
-  case OS_DISTRO_COREOS:
-  case OS_DISTRO_WINDOWS:
-  case OS_DISTRO_BUILDROOT:
-  case OS_DISTRO_CIRROS:
-  case OS_DISTRO_FREEDOS:
-  case OS_DISTRO_FREEBSD:
-  case OS_DISTRO_NETBSD:
-  case OS_DISTRO_OPENBSD:
-  case OS_DISTRO_FRUGALWARE:
-  case OS_DISTRO_PLD_LINUX:
-  case OS_DISTRO_UNKNOWN:
-    fs->package_management = OS_PACKAGE_MANAGEMENT_UNKNOWN;
-    break;
-  }
-}
-
-/* Get the first line of a small file, without any trailing newline
- * character.
- *
- * NOTE: If the file is completely empty or begins with a '\n'
- * character, this returns an empty string (not NULL).  The caller
- * will usually need to check for this case.
- */
-char *
-guestfs_int_first_line_of_file (guestfs_h *g, const char *filename)
-{
-  char **lines = NULL; /* sic: not CLEANUP_FREE_STRING_LIST */
-  int64_t size;
-  char *ret;
-
-  /* Don't trust guestfs_head_n not to break with very large files.
-   * Check the file size is something reasonable first.
-   */
-  size = guestfs_filesize (g, filename);
-  if (size == -1)
-    /* guestfs_filesize failed and has already set error in handle */
-    return NULL;
-  if (size > MAX_SMALL_FILE_SIZE) {
-    error (g, _("size of %s is unreasonably large (%" PRIi64 " bytes)"),
-           filename, size);
-    return NULL;
-  }
-
-  lines = guestfs_head_n (g, 1, filename);
-  if (lines == NULL)
-    return NULL;
-  if (lines[0] == NULL) {
-    guestfs_int_free_string_list (lines);
-    /* Empty file: Return an empty string as explained above. */
-    return safe_strdup (g, "");
-  }
-  /* lines[1] should be NULL because of '1' argument above ... */
-
-  ret = lines[0];               /* caller frees */
-
-  free (lines);
-
-  return ret;
-}
-
-/* Get the first matching line (using egrep [-i]) of a small file,
- * without any trailing newline character.
- *
- * Returns: 1 = returned a line (in *ret)
- *          0 = no match
- *          -1 = error
- */
-int
-guestfs_int_first_egrep_of_file (guestfs_h *g, const char *filename,
-				 const char *eregex, int iflag, char **ret)
-{
-  char **lines;
-  int64_t size;
-  size_t i;
-  struct guestfs_grep_opts_argv optargs;
-
-  /* Don't trust guestfs_grep not to break with very large files.
-   * Check the file size is something reasonable first.
-   */
-  size = guestfs_filesize (g, filename);
-  if (size == -1)
-    /* guestfs_filesize failed and has already set error in handle */
-    return -1;
-  if (size > MAX_SMALL_FILE_SIZE) {
-    error (g, _("size of %s is unreasonably large (%" PRIi64 " bytes)"),
-           filename, size);
-    return -1;
-  }
-
-  optargs.bitmask = GUESTFS_GREP_OPTS_EXTENDED_BITMASK;
-  optargs.extended = 1;
-  if (iflag) {
-    optargs.bitmask |= GUESTFS_GREP_OPTS_INSENSITIVE_BITMASK;
-    optargs.insensitive = 1;
-  }
-  lines = guestfs_grep_opts_argv (g, eregex, filename, &optargs);
-  if (lines == NULL)
-    return -1;
-  if (lines[0] == NULL) {
-    guestfs_int_free_string_list (lines);
-    return 0;
-  }
-
-  *ret = lines[0];              /* caller frees */
-
-  /* free up any other matches and the array itself */
-  for (i = 1; lines[i] != NULL; ++i)
-    free (lines[i]);
-  free (lines);
-
-  return 1;
-}
-
-/* Merge the missing OS inspection information found on the src inspect_fs into
- * the ones of the dst inspect_fs. This function is useful if the inspection
- * information for an OS are gathered by inspecting multiple filesystems.
- */
-void
-guestfs_int_merge_fs_inspections (guestfs_h *g, struct inspect_fs *dst, struct inspect_fs *src)
-{
-  size_t n, i, old;
-  struct inspect_fstab_entry *fstab = NULL;
-  char ** mappings = NULL;
-
-  if (dst->type == 0)
-    dst->type = src->type;
-
-  if (dst->distro == 0)
-    dst->distro = src->distro;
-
-  if (dst->package_format == 0)
-    dst->package_format = src->package_format;
-
-  if (dst->package_management == 0)
-    dst->package_management = src->package_management;
-
-  if (dst->product_name == NULL) {
-    dst->product_name = src->product_name;
-    src->product_name = NULL;
-  }
-
-  if (dst->product_variant == NULL) {
-    dst->product_variant= src->product_variant;
-    src->product_variant = NULL;
-  }
-
-  if (version_is_null (&dst->version))
-    dst->version = src->version;
-
-  if (dst->arch == NULL) {
-    dst->arch = src->arch;
-    src->arch = NULL;
-  }
-
-  if (dst->hostname == NULL) {
-    dst->hostname = src->hostname;
-    src->hostname = NULL;
-  }
-
-  if (dst->windows_systemroot == NULL) {
-    dst->windows_systemroot = src->windows_systemroot;
-    src->windows_systemroot = NULL;
-  }
-
-  if (dst->windows_current_control_set == NULL) {
-    dst->windows_current_control_set = src->windows_current_control_set;
-    src->windows_current_control_set = NULL;
-  }
-
-  if (src->drive_mappings != NULL) {
-    if (dst->drive_mappings == NULL) {
-      /* Adopt the drive mappings of src */
-      dst->drive_mappings = src->drive_mappings;
-      src->drive_mappings = NULL;
-    } else {
-      n = 0;
-      for (; dst->drive_mappings[n] != NULL; n++)
-        ;
-      old = n;
-      for (; src->drive_mappings[n] != NULL; n++)
-        ;
-
-      /* Merge the src mappings to dst */
-      mappings = safe_realloc (g, dst->drive_mappings,(n + 1) * sizeof (char *));
-
-      for (i = old; i < n; i++)
-        mappings[i] = src->drive_mappings[i - old];
-
-      mappings[n] = NULL;
-      dst->drive_mappings = mappings;
-
-      free(src->drive_mappings);
-      src->drive_mappings = NULL;
-    }
-  }
-
-  if (src->nr_fstab > 0) {
-    n = dst->nr_fstab + src->nr_fstab;
-    fstab = safe_realloc (g, dst->fstab, n * sizeof (struct inspect_fstab_entry));
-
-    for (i = 0; i < src->nr_fstab; i++) {
-      fstab[dst->nr_fstab + i].mountable = src->fstab[i].mountable;
-      fstab[dst->nr_fstab + i].mountpoint = src->fstab[i].mountpoint;
-    }
-    free(src->fstab);
-    src->fstab = NULL;
-    src->nr_fstab = 0;
-
-    dst->fstab = fstab;
-    dst->nr_fstab = n;
-  }
-}
diff --git a/lib/inspect-icon.c b/lib/inspect-icon.c
index 89c232f5b..f4f5f0660 100644
--- a/lib/inspect-icon.c
+++ b/lib/inspect-icon.c
@@ -51,22 +51,24 @@
  *     An icon was found.  'ret' points to the icon buffer, and *size_r
  *     is the size.
  */
-static char *icon_favicon (guestfs_h *g, struct inspect_fs *fs, size_t *size_r);
-static char *icon_fedora (guestfs_h *g, struct inspect_fs *fs, size_t *size_r);
-static char *icon_rhel (guestfs_h *g, struct inspect_fs *fs, size_t *size_r);
-static char *icon_debian (guestfs_h *g, struct inspect_fs *fs, size_t *size_r);
-static char *icon_ubuntu (guestfs_h *g, struct inspect_fs *fs, size_t *size_r);
-static char *icon_mageia (guestfs_h *g, struct inspect_fs *fs, size_t *size_r);
-static char *icon_opensuse (guestfs_h *g, struct inspect_fs *fs, size_t *size_r);
+static char *icon_favicon (guestfs_h *g, const char *type, size_t *size_r);
+static char *icon_fedora (guestfs_h *g, size_t *size_r);
+static char *icon_rhel (guestfs_h *g, int major, size_t *size_r);
+static char *icon_debian (guestfs_h *g, size_t *size_r);
+static char *icon_ubuntu (guestfs_h *g, size_t *size_r);
+static char *icon_mageia (guestfs_h *g, size_t *size_r);
+static char *icon_opensuse (guestfs_h *g, size_t *size_r);
 #if CAN_DO_CIRROS
-static char *icon_cirros (guestfs_h *g, struct inspect_fs *fs, size_t *size_r);
+static char *icon_cirros (guestfs_h *g, size_t *size_r);
 #endif
-static char *icon_voidlinux (guestfs_h *g, struct inspect_fs *fs, size_t *size_r);
-static char *icon_altlinux (guestfs_h *g, struct inspect_fs *fs, size_t *size_r);
+static char *icon_voidlinux (guestfs_h *g, size_t *size_r);
+static char *icon_altlinux (guestfs_h *g, size_t *size_r);
 #if CAN_DO_WINDOWS
-static char *icon_windows (guestfs_h *g, struct inspect_fs *fs, size_t *size_r);
+static char *icon_windows (guestfs_h *g, const char *root, size_t *size_r);
 #endif
 
+static char *case_sensitive_path_silently (guestfs_h *g, const char *path);
+
 /* Dummy static object. */
 static char *NOT_FOUND = (char *) "not_found";
 
@@ -82,13 +84,17 @@ char *
 guestfs_impl_inspect_get_icon (guestfs_h *g, const char *root, size_t *size_r,
 			       const struct guestfs_inspect_get_icon_argv *optargs)
 {
-  struct inspect_fs *fs;
   char *r = NOT_FOUND;
   int favicon, highquality;
   size_t size;
+  CLEANUP_FREE char *type = NULL;
+  CLEANUP_FREE char *distro = NULL;
 
-  fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
+  type = guestfs_inspect_get_type (g, root);
+  if (!type)
+    return NULL;
+  distro = guestfs_inspect_get_distro (g, root);
+  if (!distro)
     return NULL;
 
   /* Get optargs, or defaults. */
@@ -106,7 +112,7 @@ guestfs_impl_inspect_get_icon (guestfs_h *g, const char *root, size_t *size_r,
 
   /* Try looking for a favicon first. */
   if (favicon) {
-    r = icon_favicon (g, fs, &size);
+    r = icon_favicon (g, type, &size);
     if (!r)
       return NULL;
 
@@ -120,96 +126,52 @@ guestfs_impl_inspect_get_icon (guestfs_h *g, const char *root, size_t *size_r,
   /* Favicon failed, so let's try a method based on the detected operating
    * system.
    */
-  switch (fs->type) {
-  case OS_TYPE_LINUX:
-  case OS_TYPE_HURD:
-    switch (fs->distro) {
-    case OS_DISTRO_FEDORA:
-      r = icon_fedora (g, fs, &size);
-      break;
-
-    case OS_DISTRO_RHEL:
-    case OS_DISTRO_REDHAT_BASED:
-    case OS_DISTRO_CENTOS:
-    case OS_DISTRO_SCIENTIFIC_LINUX:
-    case OS_DISTRO_ORACLE_LINUX:
-      r = icon_rhel (g, fs, &size);
-      break;
-
-    case OS_DISTRO_DEBIAN:
-      r = icon_debian (g, fs, &size);
-      break;
-
-    case OS_DISTRO_UBUNTU:
+  if (STREQ (type, "linux") || STREQ (type, "hurd")) {
+    if (STREQ (distro, "fedora")) {
+      r = icon_fedora (g, &size);
+    }
+    else if (STREQ (distro, "rhel") ||
+             STREQ (distro, "redhat-based") ||
+             STREQ (distro, "centos") ||
+             STREQ (distro, "scientificlinux") ||
+             STREQ (distro, "oraclelinux")) {
+      r = icon_rhel (g, guestfs_inspect_get_major_version (g, root), &size);
+    }
+    else if (STREQ (distro, "debian")) {
+      r = icon_debian (g, &size);
+    }
+    else if (STREQ (distro, "ubuntu")) {
       if (!highquality)
-        r = icon_ubuntu (g, fs, &size);
-      break;
-
-    case OS_DISTRO_MAGEIA:
-      r = icon_mageia (g, fs, &size);
-      break;
-
-    case OS_DISTRO_SUSE_BASED:
-    case OS_DISTRO_OPENSUSE:
-    case OS_DISTRO_SLES:
-      r = icon_opensuse (g, fs, &size);
-      break;
-
-    case OS_DISTRO_CIRROS:
+        r = icon_ubuntu (g, &size);
+    }
+    else if (STREQ (distro, "mageia")) {
+      r = icon_mageia (g, &size);
+    }
+    else if (STREQ (distro, "suse-based") ||
+             STREQ (distro, "opensuse") ||
+             STREQ (distro, "sles")) {
+      r = icon_opensuse (g, &size);
+    }
+    else if (STREQ (distro, "cirros")) {
 #if CAN_DO_CIRROS
-      r = icon_cirros (g, fs, &size);
+      r = icon_cirros (g, &size);
 #endif
-      break;
-
-    case OS_DISTRO_VOID_LINUX:
-      r = icon_voidlinux (g, fs, &size);
-      break;
-
-    case OS_DISTRO_ALTLINUX:
-      r = icon_altlinux (g, fs, &size);
-      break;
-
-      /* These are just to keep gcc warnings happy. */
-    case OS_DISTRO_ARCHLINUX:
-    case OS_DISTRO_BUILDROOT:
-    case OS_DISTRO_COREOS:
-    case OS_DISTRO_FREEDOS:
-    case OS_DISTRO_GENTOO:
-    case OS_DISTRO_LINUX_MINT:
-    case OS_DISTRO_MANDRIVA:
-    case OS_DISTRO_MEEGO:
-    case OS_DISTRO_PARDUS:
-    case OS_DISTRO_SLACKWARE:
-    case OS_DISTRO_TTYLINUX:
-    case OS_DISTRO_WINDOWS:
-    case OS_DISTRO_FREEBSD:
-    case OS_DISTRO_NETBSD:
-    case OS_DISTRO_OPENBSD:
-    case OS_DISTRO_ALPINE_LINUX:
-    case OS_DISTRO_FRUGALWARE:
-    case OS_DISTRO_PLD_LINUX:
-    case OS_DISTRO_UNKNOWN:
-      ; /* nothing */
     }
-    break;
-
-  case OS_TYPE_WINDOWS:
+    else if (STREQ (distro, "voidlinux")) {
+      r = icon_voidlinux (g, &size);
+    }
+    else if (STREQ (distro, "altlinux")) {
+      r = icon_altlinux (g, &size);
+    }
+  }
+  else if (STREQ (type, "windows")) {
 #if CAN_DO_WINDOWS
     /* We don't know how to get high quality icons from a Windows guest,
      * so disable this if high quality was specified.
      */
     if (!highquality)
-      r = icon_windows (g, fs, &size);
+      r = icon_windows (g, root, &size);
 #endif
-    break;
-
-  case OS_TYPE_FREEBSD:
-  case OS_TYPE_NETBSD:
-  case OS_TYPE_DOS:
-  case OS_TYPE_OPENBSD:
-  case OS_TYPE_MINIX:
-  case OS_TYPE_UNKNOWN:
-    ; /* nothing */
   }
 
   if (r == NOT_FOUND) {
@@ -229,8 +191,7 @@ guestfs_impl_inspect_get_icon (guestfs_h *g, const char *root, size_t *size_r,
  * If it is, download and return it.
  */
 static char *
-get_png (guestfs_h *g, struct inspect_fs *fs, const char *filename,
-         size_t *size_r, size_t max_size)
+get_png (guestfs_h *g, const char *filename, size_t *size_r, size_t max_size)
 {
   char *ret;
   CLEANUP_FREE char *real = NULL;
@@ -270,7 +231,7 @@ get_png (guestfs_h *g, struct inspect_fs *fs, const char *filename,
   if (max_size == 0)
     max_size = 4 * w * h;
 
-  local = guestfs_int_download_to_tmp (g, fs, real, "icon", max_size);
+  local = guestfs_int_download_to_tmp (g, real, "icon", max_size);
   if (!local)
     return NOT_FOUND;
 
@@ -285,20 +246,20 @@ get_png (guestfs_h *g, struct inspect_fs *fs, const char *filename,
  * it has a reasonable size and format.
  */
 static char *
-icon_favicon (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_favicon (guestfs_h *g, const char *type, size_t *size_r)
 {
   char *ret;
   char *filename = safe_strdup (g, "/etc/favicon.png");
 
-  if (fs->type == OS_TYPE_WINDOWS) {
-    char *f = guestfs_int_case_sensitive_path_silently (g, filename);
+  if (STREQ (type, "windows")) {
+    char *f = case_sensitive_path_silently (g, filename);
     if (f) {
       free (filename);
       filename = f;
     }
   }
 
-  ret = get_png (g, fs, filename, size_r, 0);
+  ret = get_png (g, filename, size_r, 0);
   free (filename);
   return ret;
 }
@@ -309,9 +270,9 @@ icon_favicon (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
 #define FEDORA_ICON "/usr/share/icons/hicolor/96x96/apps/fedora-logo-icon.png"
 
 static char *
-icon_fedora (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_fedora (guestfs_h *g, size_t *size_r)
 {
-  return get_png (g, fs, FEDORA_ICON, size_r, 0);
+  return get_png (g, FEDORA_ICON, size_r, 0);
 }
 
 /* RHEL 3, 4:
@@ -330,28 +291,28 @@ icon_fedora (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
  * RHEL clones have different sizes.
  */
 static char *
-icon_rhel (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_rhel (guestfs_h *g, int major, size_t *size_r)
 {
   const char *shadowman;
 
-  if (!guestfs_int_version_ge (&fs->version, 7, 0, 0))
+  if (major < 7)
     shadowman = "/usr/share/pixmaps/redhat/shadowman-transparent.png";
   else
     shadowman = "/usr/share/pixmaps/fedora-logo-sprite.png";
 
-  return get_png (g, fs, shadowman, size_r, 102400);
+  return get_png (g, shadowman, size_r, 102400);
 }
 
 #define DEBIAN_ICON "/usr/share/pixmaps/debian-logo.png"
 
 static char *
-icon_debian (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_debian (guestfs_h *g, size_t *size_r)
 {
-  return get_png (g, fs, DEBIAN_ICON, size_r, 2048);
+  return get_png (g, DEBIAN_ICON, size_r, 2048);
 }
 
 static char *
-icon_ubuntu (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_ubuntu (guestfs_h *g, size_t *size_r)
 {
   const char *icons[] = {
     "/usr/share/icons/gnome/24x24/places/ubuntu-logo.png",
@@ -366,7 +327,7 @@ icon_ubuntu (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
   char *ret;
 
   for (i = 0; icons[i] != NULL; ++i) {
-    ret = get_png (g, fs, icons[i], size_r, 2048);
+    ret = get_png (g, icons[i], size_r, 2048);
     if (ret == NULL)
       return NULL;
     if (ret != NOT_FOUND)
@@ -378,17 +339,17 @@ icon_ubuntu (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
 #define MAGEIA_ICON "/usr/share/icons/mageia.png"
 
 static char *
-icon_mageia (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_mageia (guestfs_h *g, size_t *size_r)
 {
-  return get_png (g, fs, MAGEIA_ICON, size_r, 2048);
+  return get_png (g, MAGEIA_ICON, size_r, 2048);
 }
 
 #define OPENSUSE_ICON "/usr/share/icons/hicolor/24x24/apps/distributor.png"
 
 static char *
-icon_opensuse (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_opensuse (guestfs_h *g, size_t *size_r)
 {
-  return get_png (g, fs, OPENSUSE_ICON, size_r, 2048);
+  return get_png (g, OPENSUSE_ICON, size_r, 2048);
 }
 
 #if CAN_DO_CIRROS
@@ -397,7 +358,7 @@ icon_opensuse (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
 #define CIRROS_LOGO "/usr/share/cirros/logo"
 
 static char *
-icon_cirros (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_cirros (guestfs_h *g, size_t *size_r)
 {
   char *ret;
   CLEANUP_FREE char *type = NULL;
@@ -421,7 +382,7 @@ icon_cirros (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
   if (!STRPREFIX (type, "ASCII text"))
     return NOT_FOUND;
 
-  local = guestfs_int_download_to_tmp (g, fs, CIRROS_LOGO, "icon", 1024);
+  local = guestfs_int_download_to_tmp (g, CIRROS_LOGO, "icon", 1024);
   if (!local)
     return NOT_FOUND;
 
@@ -450,17 +411,17 @@ icon_cirros (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
 #define VOIDLINUX_ICON "/usr/share/void-artwork/void-logo.png"
 
 static char *
-icon_voidlinux (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_voidlinux (guestfs_h *g, size_t *size_r)
 {
-  return get_png (g, fs, VOIDLINUX_ICON, size_r, 20480);
+  return get_png (g, VOIDLINUX_ICON, size_r, 20480);
 }
 
 #define ALTLINUX_ICON "/usr/share/icons/hicolor/48x48/apps/altlinux.png"
 
 static char *
-icon_altlinux (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_altlinux (guestfs_h *g, size_t *size_r)
 {
-  return get_png (g, fs, ALTLINUX_ICON, size_r, 20480);
+  return get_png (g, ALTLINUX_ICON, size_r, 20480);
 }
 
 #if CAN_DO_WINDOWS
@@ -481,7 +442,7 @@ icon_altlinux (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
  */
 
 static char *
-icon_windows_xp (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_windows_xp (guestfs_h *g, const char *systemroot, size_t *size_r)
 {
   CLEANUP_FREE char *filename = NULL;
   CLEANUP_FREE char *filename_case = NULL;
@@ -492,7 +453,7 @@ icon_windows_xp (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
   char *ret;
 
   /* Download %systemroot%\explorer.exe */
-  filename = safe_asprintf (g, "%s/explorer.exe", fs->windows_systemroot);
+  filename = safe_asprintf (g, "%s/explorer.exe", systemroot);
   filename_case = guestfs_case_sensitive_path (g, filename);
   if (filename_case == NULL)
     return NULL;
@@ -505,7 +466,7 @@ icon_windows_xp (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
   if (r == 0)
     return NOT_FOUND;
 
-  filename_downloaded = guestfs_int_download_to_tmp (g, fs, filename_case,
+  filename_downloaded = guestfs_int_download_to_tmp (g, filename_case,
 						     "explorer.exe",
 						     MAX_WINDOWS_EXPLORER_SIZE);
   if (filename_downloaded == NULL)
@@ -543,7 +504,7 @@ static const char *win7_explorer[] = {
 };
 
 static char *
-icon_windows_7 (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_windows_7 (guestfs_h *g, const char *systemroot, size_t *size_r)
 {
   size_t i;
   CLEANUP_FREE char *filename_case = NULL;
@@ -556,11 +517,10 @@ icon_windows_7 (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
   for (i = 0; win7_explorer[i] != NULL; ++i) {
     CLEANUP_FREE char *filename = NULL;
 
-    filename = safe_asprintf (g, "%s/%s",
-                              fs->windows_systemroot, win7_explorer[i]);
+    filename = safe_asprintf (g, "%s/%s", systemroot, win7_explorer[i]);
 
     free (filename_case);
-    filename_case = guestfs_int_case_sensitive_path_silently (g, filename);
+    filename_case = case_sensitive_path_silently (g, filename);
     if (filename_case == NULL)
       continue;
 
@@ -575,7 +535,7 @@ icon_windows_7 (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
   if (win7_explorer[i] == NULL)
     return NOT_FOUND;
 
-  filename_downloaded = guestfs_int_download_to_tmp (g, fs, filename_case,
+  filename_downloaded = guestfs_int_download_to_tmp (g, filename_case,
 						     "explorer.exe",
 						     MAX_WINDOWS_EXPLORER_SIZE);
   if (filename_downloaded == NULL)
@@ -609,14 +569,14 @@ icon_windows_7 (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
  * - /Windows/System32/slui.exe --type=14 group icon #2
  */
 static char *
-icon_windows_8 (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_windows_8 (guestfs_h *g, size_t *size_r)
 {
   CLEANUP_FREE char *filename_case = NULL;
   CLEANUP_FREE char *filename_downloaded = NULL;
   int r;
   char *ret;
 
-  filename_case = guestfs_int_case_sensitive_path_silently
+  filename_case = case_sensitive_path_silently
     (g, "/ProgramData/Microsoft/Windows Live/WLive48x48.png");
   if (filename_case == NULL)
     return NOT_FOUND; /* Not an error since a parent dir might not exist. */
@@ -629,7 +589,7 @@ icon_windows_8 (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
   if (r == 0)
     return NOT_FOUND;
 
-  filename_downloaded = guestfs_int_download_to_tmp (g, fs, filename_case,
+  filename_downloaded = guestfs_int_download_to_tmp (g, filename_case,
 						     "wlive48x48.png", 8192);
   if (filename_downloaded == NULL)
     return NOT_FOUND;
@@ -641,25 +601,46 @@ icon_windows_8 (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
 }
 
 static char *
-icon_windows (guestfs_h *g, struct inspect_fs *fs, size_t *size_r)
+icon_windows (guestfs_h *g, const char *root, size_t *size_r)
 {
-  if (fs->windows_systemroot == NULL)
+  CLEANUP_FREE char *systemroot =
+    guestfs_inspect_get_windows_systemroot (g, root);
+  int major = guestfs_inspect_get_major_version (g, root);
+  int minor = guestfs_inspect_get_minor_version (g, root);
+
+  if (systemroot == NULL)
     return NOT_FOUND;
 
   /* Windows XP. */
-  if (fs->version.v_major == 5 && fs->version.v_minor == 1)
-    return icon_windows_xp (g, fs, size_r);
+  if (major == 5 && minor == 1)
+    return icon_windows_xp (g, systemroot, size_r);
 
   /* Windows 7. */
-  else if (fs->version.v_major == 6 && fs->version.v_minor == 1)
-    return icon_windows_7 (g, fs, size_r);
+  else if (major == 6 && minor == 1)
+    return icon_windows_7 (g, systemroot, size_r);
 
   /* Windows 8. */
-  else if (fs->version.v_major == 6 && fs->version.v_minor == 2)
-    return icon_windows_8 (g, fs, size_r);
+  else if (major == 6 && minor == 2)
+    return icon_windows_8 (g, size_r);
 
   /* Not (yet) a supported version of Windows. */
   else return NOT_FOUND;
 }
 
 #endif /* CAN_DO_WINDOWS */
+
+/* NB: This function DOES NOT test for the existence of the file.  It
+ * will return non-NULL even if the file/directory does not exist.
+ * You have to call guestfs_is_file{,_opts} etc.
+ */
+static char *
+case_sensitive_path_silently (guestfs_h *g, const char *path)
+{
+  char *ret;
+
+  guestfs_push_error_handler (g, NULL, NULL);
+  ret = guestfs_case_sensitive_path (g, path);
+  guestfs_pop_error_handler (g);
+
+  return ret;
+}
diff --git a/lib/inspect.c b/lib/inspect.c
index 1cc0942f1..f2d64b61e 100644
--- a/lib/inspect.c
+++ b/lib/inspect.c
@@ -43,688 +43,6 @@
 #include "guestfs-internal.h"
 #include "guestfs-internal-actions.h"
 
-COMPILE_REGEXP (re_primary_partition, "^/dev/(?:h|s|v)d.[1234]$", 0)
-
-static void check_for_duplicated_bsd_root (guestfs_h *g);
-static void collect_coreos_inspection_info (guestfs_h *g);
-static void collect_linux_inspection_info (guestfs_h *g);
-static void collect_linux_inspection_info_for (guestfs_h *g, struct inspect_fs *root);
-
-/**
- * The main inspection API.
- */
-char **
-guestfs_impl_inspect_os (guestfs_h *g)
-{
-  CLEANUP_FREE_STRING_LIST char **fses = NULL;
-  char **fs, **ret;
-
-  /* Remove any information previously stored in the handle. */
-  guestfs_int_free_inspect_info (g);
-
-  if (guestfs_umount_all (g) == -1)
-    return NULL;
-
-  /* Iterate over all detected filesystems.  Inspect each one in turn
-   * and add that information to the handle.
-   */
-
-  fses = guestfs_list_filesystems (g);
-  if (fses == NULL) return NULL;
-
-  for (fs = fses; *fs; fs += 2) {
-    if (guestfs_int_check_for_filesystem_on (g, *fs)) {
-      guestfs_int_free_inspect_info (g);
-      return NULL;
-    }
-  }
-
-  /* The OS inspection information for CoreOS are gathered by inspecting
-   * multiple filesystems. Gather all the inspected information in the
-   * inspect_fs struct of the root filesystem.
-   */
-  collect_coreos_inspection_info (g);
-
-  /* Check if the same filesystem was listed twice as root in g->fses.
-   * This may happen for the *BSD root partition where an MBR partition
-   * is a shadow of the real root partition probably /dev/sda5
-   */
-  check_for_duplicated_bsd_root (g);
-
-  /* For Linux guests with a separate /usr filesyste, merge some of the
-   * inspected information in that partition to the inspect_fs struct
-   * of the root filesystem.
-   */
-  collect_linux_inspection_info (g);
-
-  /* At this point we have, in the handle, a list of all filesystems
-   * found and data about each one.  Now we assemble the list of
-   * filesystems which are root devices and return that to the user.
-   * Fall through to guestfs_inspect_get_roots to do that.
-   */
-  ret = guestfs_inspect_get_roots (g);
-  if (ret == NULL)
-    guestfs_int_free_inspect_info (g);
-  return ret;
-}
-
-/**
- * Traverse through the filesystem list and find out if it contains
- * the C</> and C</usr> filesystems of a CoreOS image. If this is the
- * case, sum up all the collected information on the root fs.
- */
-static void
-collect_coreos_inspection_info (guestfs_h *g)
-{
-  size_t i;
-  struct inspect_fs *root = NULL, *usr = NULL;
-
-  for (i = 0; i < g->nr_fses; ++i) {
-    struct inspect_fs *fs = &g->fses[i];
-
-    if (fs->distro == OS_DISTRO_COREOS && fs->role == OS_ROLE_ROOT)
-      root = fs;
-  }
-
-  if (root == NULL)
-    return;
-
-  for (i = 0; i < g->nr_fses; ++i) {
-    struct inspect_fs *fs = &g->fses[i];
-
-    if (fs->distro != OS_DISTRO_COREOS || fs->role != OS_ROLE_USR)
-      continue;
-
-    /* CoreOS is designed to contain 2 /usr partitions (USR-A, USR-B):
-     * https://coreos.com/docs/sdk-distributors/sdk/disk-partitions/
-     * One is active and one passive. During the initial boot, the passive
-     * partition is empty and it gets filled up when an update is performed.
-     * Then, when the system reboots, the boot loader is instructed to boot
-     * from the passive partition. If both partitions are valid, we cannot
-     * determine which the active and which the passive is, unless we peep into
-     * the boot loader. As a workaround, we check the OS versions and pick the
-     * one with the higher version as active.
-     */
-    if (usr && guestfs_int_version_cmp_ge (&usr->version, &fs->version))
-      continue;
-
-    usr = fs;
-  }
-
-  if (usr == NULL)
-    return;
-
-  guestfs_int_merge_fs_inspections (g, root, usr);
-}
-
-/**
- * Traverse through the filesystems and find the /usr filesystem for
- * the specified C<root>: if found, merge its basic inspection details
- * to the root when they were set (i.e. because the /usr had os-release
- * or other ways to identify the OS).
- */
-static void
-collect_linux_inspection_info_for (guestfs_h *g, struct inspect_fs *root)
-{
-  size_t i;
-  struct inspect_fs *usr = NULL;
-
-  for (i = 0; i < g->nr_fses; ++i) {
-    struct inspect_fs *fs = &g->fses[i];
-    size_t j;
-
-    if (!(fs->distro == root->distro || fs->distro == OS_DISTRO_UNKNOWN) ||
-        fs->role != OS_ROLE_USR)
-      continue;
-
-    for (j = 0; j < root->nr_fstab; ++j) {
-      if (STREQ (fs->mountable, root->fstab[j].mountable)) {
-        usr = fs;
-        goto got_usr;
-      }
-    }
-  }
-
-  assert (usr == NULL);
-  return;
-
- got_usr:
-  /* If the version information in /usr is not null, then most probably
-   * there was an os-release file there, so reset what is in root
-   * and pick the results from /usr.
-   */
-  if (!version_is_null (&usr->version)) {
-    root->distro = OS_DISTRO_UNKNOWN;
-    free (root->product_name);
-    root->product_name = NULL;
-  }
-
-  guestfs_int_merge_fs_inspections (g, root, usr);
-}
-
-/**
- * Traverse through the filesystem list and find out if it contains
- * the C</> and C</usr> filesystems of a Linux image (but not CoreOS,
- * for which there is a separate C<collect_coreos_inspection_info>).
- * If this is the case, sum up all the collected information on each
- * root fs from the respective /usr filesystems.
- */
-static void
-collect_linux_inspection_info (guestfs_h *g)
-{
-  size_t i;
-
-  for (i = 0; i < g->nr_fses; ++i) {
-    struct inspect_fs *fs = &g->fses[i];
-
-    if (fs->distro != OS_DISTRO_COREOS && fs->role == OS_ROLE_ROOT)
-      collect_linux_inspection_info_for (g, fs);
-  }
-}
-
-/**
- * On *BSD systems, sometimes F</dev/sda[1234]> is a shadow of the
- * real root filesystem that is probably F</dev/sda5> (see:
- * L<http://www.freebsd.org/doc/handbook/disk-organization.html>)
- */
-static void
-check_for_duplicated_bsd_root (guestfs_h *g)
-{
-  size_t i;
-  struct inspect_fs *bsd_primary = NULL;
-
-  for (i = 0; i < g->nr_fses; ++i) {
-    bool is_bsd;
-    struct inspect_fs *fs = &g->fses[i];
-
-    is_bsd =
-      fs->type == OS_TYPE_FREEBSD ||
-      fs->type == OS_TYPE_NETBSD ||
-      fs->type == OS_TYPE_OPENBSD;
-
-    if (fs->role == OS_ROLE_ROOT && is_bsd &&
-        match (g, fs->mountable, re_primary_partition)) {
-      bsd_primary = fs;
-      continue;
-    }
-
-    if (fs->role == OS_ROLE_ROOT && bsd_primary &&
-        bsd_primary->type == fs->type) {
-      /* remove the root role from the bsd_primary */
-      bsd_primary->role = OS_ROLE_UNKNOWN;
-      bsd_primary->format = OS_FORMAT_UNKNOWN;
-      return;
-    }
-  }
-}
-
-static int
-compare_strings (const void *vp1, const void *vp2)
-{
-  const char *s1 = * (char * const *) vp1;
-  const char *s2 = * (char * const *) vp2;
-
-  return strcmp (s1, s2);
-}
-
-char **
-guestfs_impl_inspect_get_roots (guestfs_h *g)
-{
-  size_t i;
-  DECLARE_STRINGSBUF (ret);
-
-  /* NB. Doesn't matter if g->nr_fses == 0.  We just return an empty
-   * list in this case.
-   */
-  for (i = 0; i < g->nr_fses; ++i) {
-    if (g->fses[i].role == OS_ROLE_ROOT)
-      guestfs_int_add_string (g, &ret, g->fses[i].mountable);
-  }
-  guestfs_int_end_stringsbuf (g, &ret);
-
-  qsort (ret.argv, ret.size-1, sizeof (char *), compare_strings);
-
-  return ret.argv;
-}
-
-char *
-guestfs_impl_inspect_get_type (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  char *ret = NULL;
-
-  if (!fs)
-    return NULL;
-
-  switch (fs->type) {
-  case OS_TYPE_DOS: ret = safe_strdup (g, "dos"); break;
-  case OS_TYPE_FREEBSD: ret = safe_strdup (g, "freebsd"); break;
-  case OS_TYPE_HURD: ret = safe_strdup (g, "hurd"); break;
-  case OS_TYPE_LINUX: ret = safe_strdup (g, "linux"); break;
-  case OS_TYPE_MINIX: ret = safe_strdup (g, "minix"); break;
-  case OS_TYPE_NETBSD: ret = safe_strdup (g, "netbsd"); break;
-  case OS_TYPE_OPENBSD: ret = safe_strdup (g, "openbsd"); break;
-  case OS_TYPE_WINDOWS: ret = safe_strdup (g, "windows"); break;
-  case OS_TYPE_UNKNOWN: ret = safe_strdup (g, "unknown"); break;
-  }
-
-  if (ret == NULL)
-    abort ();
-
-  return ret;
-}
-
-char *
-guestfs_impl_inspect_get_arch (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  return safe_strdup (g, fs->arch ? : "unknown");
-}
-
-char *
-guestfs_impl_inspect_get_distro (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  char *ret = NULL;
-
-  if (!fs)
-    return NULL;
-
-  switch (fs->distro) {
-  case OS_DISTRO_ALPINE_LINUX: ret = safe_strdup (g, "alpinelinux"); break;
-  case OS_DISTRO_ALTLINUX: ret = safe_strdup (g, "altlinux"); break;
-  case OS_DISTRO_ARCHLINUX: ret = safe_strdup (g, "archlinux"); break;
-  case OS_DISTRO_BUILDROOT: ret = safe_strdup (g, "buildroot"); break;
-  case OS_DISTRO_CENTOS: ret = safe_strdup (g, "centos"); break;
-  case OS_DISTRO_CIRROS: ret = safe_strdup (g, "cirros"); break;
-  case OS_DISTRO_COREOS: ret = safe_strdup (g, "coreos"); break;
-  case OS_DISTRO_DEBIAN: ret = safe_strdup (g, "debian"); break;
-  case OS_DISTRO_FEDORA: ret = safe_strdup (g, "fedora"); break;
-  case OS_DISTRO_FREEBSD: ret = safe_strdup (g, "freebsd"); break;
-  case OS_DISTRO_FREEDOS: ret = safe_strdup (g, "freedos"); break;
-  case OS_DISTRO_FRUGALWARE: ret = safe_strdup (g, "frugalware"); break;
-  case OS_DISTRO_GENTOO: ret = safe_strdup (g, "gentoo"); break;
-  case OS_DISTRO_LINUX_MINT: ret = safe_strdup (g, "linuxmint"); break;
-  case OS_DISTRO_MAGEIA: ret = safe_strdup (g, "mageia"); break;
-  case OS_DISTRO_MANDRIVA: ret = safe_strdup (g, "mandriva"); break;
-  case OS_DISTRO_MEEGO: ret = safe_strdup (g, "meego"); break;
-  case OS_DISTRO_NETBSD: ret = safe_strdup (g, "netbsd"); break;
-  case OS_DISTRO_OPENBSD: ret = safe_strdup (g, "openbsd"); break;
-  case OS_DISTRO_OPENSUSE: ret = safe_strdup (g, "opensuse"); break;
-  case OS_DISTRO_ORACLE_LINUX: ret = safe_strdup (g, "oraclelinux"); break;
-  case OS_DISTRO_PARDUS: ret = safe_strdup (g, "pardus"); break;
-  case OS_DISTRO_PLD_LINUX: ret = safe_strdup (g, "pldlinux"); break;
-  case OS_DISTRO_REDHAT_BASED: ret = safe_strdup (g, "redhat-based"); break;
-  case OS_DISTRO_RHEL: ret = safe_strdup (g, "rhel"); break;
-  case OS_DISTRO_SCIENTIFIC_LINUX: ret = safe_strdup (g, "scientificlinux"); break;
-  case OS_DISTRO_SLACKWARE: ret = safe_strdup (g, "slackware"); break;
-  case OS_DISTRO_SLES: ret = safe_strdup (g, "sles"); break;
-  case OS_DISTRO_SUSE_BASED: ret = safe_strdup (g, "suse-based"); break;
-  case OS_DISTRO_TTYLINUX: ret = safe_strdup (g, "ttylinux"); break;
-  case OS_DISTRO_WINDOWS: ret = safe_strdup (g, "windows"); break;
-  case OS_DISTRO_UBUNTU: ret = safe_strdup (g, "ubuntu"); break;
-  case OS_DISTRO_VOID_LINUX: ret = safe_strdup (g, "voidlinux"); break;
-  case OS_DISTRO_UNKNOWN: ret = safe_strdup (g, "unknown"); break;
-  }
-
-  if (ret == NULL)
-    abort ();
-
-  return ret;
-}
-
-int
-guestfs_impl_inspect_get_major_version (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return -1;
-
-  return fs->version.v_major;
-}
-
-int
-guestfs_impl_inspect_get_minor_version (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return -1;
-
-  return fs->version.v_minor;
-}
-
-char *
-guestfs_impl_inspect_get_product_name (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  return safe_strdup (g, fs->product_name ? : "unknown");
-}
-
-char *
-guestfs_impl_inspect_get_product_variant (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  return safe_strdup (g, fs->product_variant ? : "unknown");
-}
-
-char *
-guestfs_impl_inspect_get_windows_systemroot (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  if (!fs->windows_systemroot) {
-    error (g, _("not a Windows guest, or systemroot could not be determined"));
-    return NULL;
-  }
-
-  return safe_strdup (g, fs->windows_systemroot);
-}
-
-char *
-guestfs_impl_inspect_get_windows_software_hive (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  if (!fs->windows_software_hive) {
-    error (g, _("not a Windows guest, or software hive not found"));
-    return NULL;
-  }
-
-  return safe_strdup (g, fs->windows_software_hive);
-}
-
-char *
-guestfs_impl_inspect_get_windows_system_hive (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  if (!fs->windows_system_hive) {
-    error (g, _("not a Windows guest, or system hive not found"));
-    return NULL;
-  }
-
-  return safe_strdup (g, fs->windows_system_hive);
-}
-
-char *
-guestfs_impl_inspect_get_windows_current_control_set (guestfs_h *g,
-						      const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  if (!fs->windows_current_control_set) {
-    error (g, _("not a Windows guest, or CurrentControlSet could not be determined"));
-    return NULL;
-  }
-
-  return safe_strdup (g, fs->windows_current_control_set);
-}
-
-char *
-guestfs_impl_inspect_get_format (guestfs_h *g, const char *root)
-{
-  char *ret = NULL;
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  switch (fs->format) {
-  case OS_FORMAT_INSTALLED: ret = safe_strdup (g, "installed"); break;
-  case OS_FORMAT_INSTALLER: ret = safe_strdup (g, "installer"); break;
-  case OS_FORMAT_UNKNOWN: ret = safe_strdup (g, "unknown"); break;
-  }
-
-  if (ret == NULL)
-    abort ();
-
-  return ret;
-}
-
-int
-guestfs_impl_inspect_is_live (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return -1;
-
-  return fs->is_live_disk;
-}
-
-int
-guestfs_impl_inspect_is_netinst (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return -1;
-
-  return fs->is_netinst_disk;
-}
-
-int
-guestfs_impl_inspect_is_multipart (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return -1;
-
-  return fs->is_multipart_disk;
-}
-
-char **
-guestfs_impl_inspect_get_mountpoints (guestfs_h *g, const char *root)
-{
-  char **ret;
-  size_t i, count, nr;
-  struct inspect_fs *fs;
-
-  fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-#define CRITERION(fs, i) fs->fstab[i].mountpoint[0] == '/'
-
-  nr = fs->nr_fstab;
-
-  if (nr == 0)
-    count = 1;
-  else {
-    count = 0;
-    for (i = 0; i < nr; ++i)
-      if (CRITERION (fs, i))
-        count++;
-  }
-
-  /* Hashtables have 2N+1 entries. */
-  ret = calloc (2*count+1, sizeof (char *));
-  if (ret == NULL) {
-    perrorf (g, "calloc");
-    return NULL;
-  }
-
-  /* If no fstab information (Windows) return just the root. */
-  if (nr == 0) {
-    ret[0] = safe_strdup (g, "/");
-    ret[1] = safe_strdup (g, root);
-    ret[2] = NULL;
-    return ret;
-  }
-
-  count = 0;
-  for (i = 0; i < nr; ++i)
-    if (CRITERION (fs, i)) {
-      ret[2*count] = safe_strdup (g, fs->fstab[i].mountpoint);
-      ret[2*count+1] = safe_strdup (g, fs->fstab[i].mountable);
-      count++;
-    }
-#undef CRITERION
-
-  return ret;
-}
-
-char **
-guestfs_impl_inspect_get_filesystems (guestfs_h *g, const char *root)
-{
-  char **ret;
-  size_t i, nr;
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-
-  if (!fs)
-    return NULL;
-
-  nr = fs->nr_fstab;
-  ret = calloc (nr == 0 ? 2 : nr+1, sizeof (char *));
-  if (ret == NULL) {
-    perrorf (g, "calloc");
-    return NULL;
-  }
-
-  /* If no fstab information (Windows) return just the root. */
-  if (nr == 0) {
-    ret[0] = safe_strdup (g, root);
-    ret[1] = NULL;
-    return ret;
-  }
-
-  for (i = 0; i < nr; ++i)
-    ret[i] = safe_strdup (g, fs->fstab[i].mountable);
-
-  return ret;
-}
-
-char **
-guestfs_impl_inspect_get_drive_mappings (guestfs_h *g, const char *root)
-{
-  DECLARE_STRINGSBUF (ret);
-  size_t i;
-  struct inspect_fs *fs;
-
-  fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  if (fs->drive_mappings) {
-    for (i = 0; fs->drive_mappings[i] != NULL; ++i)
-      guestfs_int_add_string (g, &ret, fs->drive_mappings[i]);
-  }
-
-  guestfs_int_end_stringsbuf (g, &ret);
-  return ret.argv;
-}
-
-char *
-guestfs_impl_inspect_get_package_format (guestfs_h *g, const char *root)
-{
-  char *ret = NULL;
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  switch (fs->package_format) {
-  case OS_PACKAGE_FORMAT_RPM: ret = safe_strdup (g, "rpm"); break;
-  case OS_PACKAGE_FORMAT_DEB: ret = safe_strdup (g, "deb"); break;
-  case OS_PACKAGE_FORMAT_PACMAN: ret = safe_strdup (g, "pacman"); break;
-  case OS_PACKAGE_FORMAT_EBUILD: ret = safe_strdup (g, "ebuild"); break;
-  case OS_PACKAGE_FORMAT_PISI: ret = safe_strdup (g, "pisi"); break;
-  case OS_PACKAGE_FORMAT_PKGSRC: ret = safe_strdup (g, "pkgsrc"); break;
-  case OS_PACKAGE_FORMAT_APK: ret = safe_strdup (g, "apk"); break;
-  case OS_PACKAGE_FORMAT_XBPS: ret = safe_strdup (g, "xbps"); break;
-  case OS_PACKAGE_FORMAT_UNKNOWN:
-    ret = safe_strdup (g, "unknown");
-    break;
-  }
-
-  if (ret == NULL)
-    abort ();
-
-  return ret;
-}
-
-char *
-guestfs_impl_inspect_get_package_management (guestfs_h *g, const char *root)
-{
-  char *ret = NULL;
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  switch (fs->package_management) {
-  case OS_PACKAGE_MANAGEMENT_APK: ret = safe_strdup (g, "apk"); break;
-  case OS_PACKAGE_MANAGEMENT_APT: ret = safe_strdup (g, "apt"); break;
-  case OS_PACKAGE_MANAGEMENT_DNF: ret = safe_strdup (g, "dnf"); break;
-  case OS_PACKAGE_MANAGEMENT_PACMAN: ret = safe_strdup (g, "pacman"); break;
-  case OS_PACKAGE_MANAGEMENT_PISI: ret = safe_strdup (g, "pisi"); break;
-  case OS_PACKAGE_MANAGEMENT_PORTAGE: ret = safe_strdup (g, "portage"); break;
-  case OS_PACKAGE_MANAGEMENT_UP2DATE: ret = safe_strdup (g, "up2date"); break;
-  case OS_PACKAGE_MANAGEMENT_URPMI: ret = safe_strdup (g, "urpmi"); break;
-  case OS_PACKAGE_MANAGEMENT_XBPS: ret = safe_strdup (g, "xbps"); break;
-  case OS_PACKAGE_MANAGEMENT_YUM: ret = safe_strdup (g, "yum"); break;
-  case OS_PACKAGE_MANAGEMENT_ZYPPER: ret = safe_strdup (g, "zypper"); break;
-  case OS_PACKAGE_MANAGEMENT_UNKNOWN:
-    ret = safe_strdup (g, "unknown");
-    break;
-  }
-
-  if (ret == NULL)
-    abort ();
-
-  return ret;
-}
-
-char *
-guestfs_impl_inspect_get_hostname (guestfs_h *g, const char *root)
-{
-  struct inspect_fs *fs = guestfs_int_search_for_root (g, root);
-  if (!fs)
-    return NULL;
-
-  return safe_strdup (g, fs->hostname ? : "unknown");
-}
-
-void
-guestfs_int_free_inspect_info (guestfs_h *g)
-{
-  size_t i, j;
-
-  for (i = 0; i < g->nr_fses; ++i) {
-    free (g->fses[i].mountable);
-    free (g->fses[i].product_name);
-    free (g->fses[i].product_variant);
-    free (g->fses[i].arch);
-    free (g->fses[i].hostname);
-    free (g->fses[i].windows_systemroot);
-    free (g->fses[i].windows_software_hive);
-    free (g->fses[i].windows_system_hive);
-    free (g->fses[i].windows_current_control_set);
-    for (j = 0; j < g->fses[i].nr_fstab; ++j) {
-      free (g->fses[i].fstab[j].mountable);
-      free (g->fses[i].fstab[j].mountpoint);
-    }
-    free (g->fses[i].fstab);
-    if (g->fses[i].drive_mappings)
-      guestfs_int_free_string_list (g->fses[i].drive_mappings);
-  }
-  free (g->fses);
-  g->nr_fses = 0;
-  g->fses = NULL;
-}
-
 /**
  * Download a guest file to a local temporary file.  The file is
  * cached in the temporary directory, and is not downloaded again.
@@ -740,7 +58,7 @@ guestfs_int_free_inspect_info (guestfs_h *g)
  * handle the case of multiple roots.
  */
 char *
-guestfs_int_download_to_tmp (guestfs_h *g, struct inspect_fs *fs,
+guestfs_int_download_to_tmp (guestfs_h *g,
 			     const char *filename,
 			     const char *basename, uint64_t max_size)
 {
@@ -749,10 +67,7 @@ guestfs_int_download_to_tmp (guestfs_h *g, struct inspect_fs *fs,
   char devfd[32];
   int64_t size;
 
-  /* Make the basename unique by prefixing it with the fs number.
-   * This also ensures there is one cache per filesystem.
-   */
-  if (asprintf (&r, "%s/%td-%s", g->tmpdir, fs - g->fses, basename) == -1) {
+  if (asprintf (&r, "%s/%s", g->tmpdir, basename) == -1) {
     perrorf (g, "asprintf");
     return NULL;
   }
@@ -798,46 +113,3 @@ guestfs_int_download_to_tmp (guestfs_h *g, struct inspect_fs *fs,
   free (r);
   return NULL;
 }
-
-struct inspect_fs *
-guestfs_int_search_for_root (guestfs_h *g, const char *root)
-{
-  size_t i;
-
-  if (g->nr_fses == 0) {
-    error (g, _("no inspection data: call guestfs_inspect_os first"));
-    return NULL;
-  }
-
-  for (i = 0; i < g->nr_fses; ++i) {
-    struct inspect_fs *fs = &g->fses[i];
-    if (fs->role == OS_ROLE_ROOT && STREQ (root, fs->mountable))
-      return fs;
-  }
-
-  error (g, _("%s: root device not found: only call this function with a root device previously returned by guestfs_inspect_os"),
-         root);
-  return NULL;
-}
-
-int
-guestfs_int_is_partition (guestfs_h *g, const char *partition)
-{
-  CLEANUP_FREE char *device = NULL;
-
-  guestfs_push_error_handler (g, NULL, NULL);
-
-  if ((device = guestfs_part_to_dev (g, partition)) == NULL) {
-    guestfs_pop_error_handler (g);
-    return 0;
-  }
-
-  if (guestfs_device_index (g, device) == -1) {
-    guestfs_pop_error_handler (g);
-    return 0;
-  }
-
-  guestfs_pop_error_handler (g);
-
-  return 1;
-}
diff --git a/lib/version.c b/lib/version.c
index 60ffe1e89..86bb0b1f0 100644
--- a/lib/version.c
+++ b/lib/version.c
@@ -24,14 +24,42 @@
 
 #include <string.h>
 #include <unistd.h>
+#include <libintl.h>
 
 #include "ignore-value.h"
+#include "xstrtol.h"
 
 #include "guestfs.h"
 #include "guestfs-internal.h"
 
 COMPILE_REGEXP (re_major_minor, "(\\d+)\\.(\\d+)", 0)
 
+/* Parse small, unsigned ints, as used in version numbers. */
+int
+guestfs_int_parse_unsigned_int (guestfs_h *g, const char *str)
+{
+  long ret;
+  const int r = xstrtol (str, NULL, 10, &ret, "");
+  if (r != LONGINT_OK) {
+    error (g, _("could not parse integer in version number: %s"), str);
+    return -1;
+  }
+  return ret;
+}
+
+/* Like parse_unsigned_int, but ignore trailing stuff. */
+int
+guestfs_int_parse_unsigned_int_ignore_trailing (guestfs_h *g, const char *str)
+{
+  long ret;
+  const int r = xstrtol (str, NULL, 10, &ret, NULL);
+  if (r != LONGINT_OK) {
+    error (g, _("could not parse integer in version number: %s"), str);
+    return -1;
+  }
+  return ret;
+}
+
 static int version_from_x_y_or_x (guestfs_h *g, struct version *v, const char *str, const pcre *re, bool allow_only_x);
 
 void
-- 
2.13.0


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]