This is essentially a line-for-line translation of the C inspection
code.
---
 daemon/Makefile.am               |   8 +
 daemon/inspect.ml                | 396 +++++++++++++++++++++
 daemon/inspect.mli               |  41 +++
 daemon/inspect_fs.ml             | 363 +++++++++++++++++++
 daemon/inspect_fs.mli            |  23 ++
 daemon/inspect_fs_unix.ml        | 745 +++++++++++++++++++++++++++++++++++++++
 daemon/inspect_fs_unix.mli       |  44 +++
 daemon/inspect_fs_unix_fstab.ml  | 533 ++++++++++++++++++++++++++++
 daemon/inspect_fs_unix_fstab.mli |  34 ++
 9 files changed, 2187 insertions(+)

diff --git a/daemon/Makefile.am b/daemon/Makefile.am
index 51737e511..f035add2b 100644
--- a/daemon/Makefile.am
+++ b/daemon/Makefile.am
@@ -252,6 +252,10 @@ SOURCES_MLI = \
        file.mli \
        filearch.mli \
        findfs.mli \
+       inspect.mli \
+       inspect_fs.mli \
+       inspect_fs_unix.mli \
+       inspect_fs_unix_fstab.mli \
        inspect_types.mli \
        inspect_utils.mli \
        is.mli \
@@ -291,6 +295,10 @@ SOURCES_ML = \
        realpath.ml \
        inspect_types.ml \
        inspect_utils.ml \
+       inspect_fs_unix_fstab.ml \
+       inspect_fs_unix.ml \
+       inspect_fs.ml \
+       inspect.ml \
        callbacks.ml \
        daemon.ml
 
diff --git a/daemon/inspect.ml b/daemon/inspect.ml
new file mode 100644
index 000000000..0f5bcfc10
--- /dev/null
+++ b/daemon/inspect.ml
@@ -0,0 +1,396 @@
+(* 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 = PCRE.compile "^/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 "%s" (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 = RoleUsr { version = Some v } } -> v
+            | _ -> (0, 0) in
+          let v2 =
+            match u2 with
+            | { 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 } ->
+         PCRE.matches re_primary_partition d
+    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..9153e68a5
--- /dev/null
+++ b/daemon/inspect_fs.ml
@@ -0,0 +1,363 @@
+(* 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.  However we
+   * need to make a copy of the object here so we don't mutate the
+   * null_inspection_data struct!
+   *)
+  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 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..ce48942bd
--- /dev/null
+++ b/daemon/inspect_fs_unix.ml
@@ -0,0 +1,745 @@
+(* 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 = PCRE.compile "Fedora release (\\d+)"
+let re_rhel_old = PCRE.compile "Red Hat.*release (\\d+).*Update (\\d+)"
+let re_rhel = PCRE.compile "Red Hat.*release (\\d+)\\.(\\d+)"
+let re_rhel_no_minor = PCRE.compile "Red Hat.*release (\\d+)"
+let re_centos_old = PCRE.compile "CentOS.*release (\\d+).*Update (\\d+)"
+let re_centos = PCRE.compile "CentOS.*release (\\d+)\\.(\\d+)"
+let re_centos_no_minor = PCRE.compile "CentOS.*release (\\d+)"
+let re_scientific_linux_old =
+  PCRE.compile "Scientific Linux.*release (\\d+).*Update (\\d+)"
+let re_scientific_linux =
+  PCRE.compile "Scientific Linux.*release (\\d+)\\.(\\d+)"
+let re_scientific_linux_no_minor =
+  PCRE.compile "Scientific Linux.*release (\\d+)"
+let re_oracle_linux_old =
+  PCRE.compile "Oracle Linux.*release (\\d+).*Update (\\d+)"
+let re_oracle_linux =
+  PCRE.compile "Oracle Linux.*release (\\d+)\\.(\\d+)"
+let re_oracle_linux_no_minor = PCRE.compile "Oracle Linux.*release (\\d+)"
+let re_netbsd = PCRE.compile "^NetBSD (\\d+)\\.(\\d+)"
+let re_opensuse = PCRE.compile "^(openSUSE|SuSE Linux|SUSE LINUX) "
+let re_sles = PCRE.compile "^SUSE (Linux|LINUX) Enterprise "
+let re_nld = PCRE.compile "^Novell Linux Desktop "
+let re_sles_version = PCRE.compile "^VERSION = (\\d+)"
+let re_sles_patchlevel = PCRE.compile "^PATCHLEVEL = (\\d+)"
+let re_minix = PCRE.compile "^(\\d+)\\.(\\d+)(\\.(\\d+))?"
+let re_openbsd = PCRE.compile "^OpenBSD (\\d+|\\?)\\.(\\d+|\\?)"
+let re_frugalware = PCRE.compile "Frugalware (\\d+)\\.(\\d+)"
+let re_pldlinux = PCRE.compile "(\\d+)\\.(\\d+) 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" () in
+  let lines = Chroot.f chroot (fun () -> read_small_file release_file) () in
+
+  match lines with
+  | None -> false
+  | Some lines ->
+     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 (
+       match data with
+       (* 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.
+        *)
+       | { distro = Some (DISTRO_DEBIAN|DISTRO_CENTOS);
+           version = Some (_, 0) } ->
+          false
+
+       (* Rolling releases:
+        * Void Linux has no VERSION_ID and no other version/release-
+        * like file.
+        *)
+       | { distro = Some DISTRO_VOID_LINUX;
+           version = None } ->
+          data.version <- Some (0, 0);
+          true
+
+       | _ -> 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" () in
+  let lines = Chroot.f chroot (fun () -> read_small_file release_file) () in
+
+  match lines with
+  | None -> false
+  | Some lines ->
+     (* 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" () in
+  let lines = Chroot.f chroot (fun () -> read_small_file release_file) () in
+
+  match lines with
+  | None
+  | Some [] -> false
+  | Some lines ->
+     (* 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 PCRE.matches re_sles product_name ||
+        PCRE.matches re_nld product_name then (
+       (* Second line contains version string. *)
+       let major =
+         if List.length lines >= 2 then (
+           let line = List.nth lines 1 in
+           if PCRE.matches re_sles_version line then
+             Some (int_of_string (PCRE.sub 1))
+           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 PCRE.matches re_sles_patchlevel line then
+             Some (int_of_string (PCRE.sub 1))
+           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 PCRE.matches re_opensuse product_name 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" () 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 PCRE.matches rex product_name 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 (PCRE.sub 1))
+            with Not_found | Invalid_argument _ | Failure _ -> None in
+          let minor =
+            try Some (int_of_string (PCRE.sub 2))
+            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
+type tests = (string * parse_function) list
+
+let linux_root_tests : tests = [
+  (* 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_tests data = function
+  | (release_file, parse_fun) :: tests ->
+     if verbose () then
+       eprintf "check_tests: checking %s\n%!" release_file;
+     if Is.is_file ~followsymlinks:true release_file then (
+       if parse_fun release_file data then () (* true => finished *)
+       else check_tests data tests
+     ) else check_tests data tests
+  | [] -> ()
+
+let rec check_linux_root mountable data =
+  let os_type = OS_TYPE_LINUX in
+  data.os_type <- Some os_type;
+
+  check_tests data 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 () in
+
+  let hostname = Chroot.f chroot (fun () -> read_small_file filename) () in
+  match hostname with
+  | None | Some [] | Some [""] -> None
+  | Some (hostname :: _) -> Some 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" () in
+  let filename = "/etc/rc.conf" in
+
+  try
+    let lines = Chroot.f chroot (fun () -> read_small_file filename) () in
+    let lines =
+      match lines with None -> raise Not_found | Some lines -> 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..e3c7fd1cd
--- /dev/null
+++ b/daemon/inspect_fs_unix_fstab.ml
@@ -0,0 +1,533 @@
+(* 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 = PCRE.compile "^/dev/(cciss/c\\d+d\\d+)(?:p(\\d+))?$"
+let re_diskbyid = PCRE.compile "^/dev/disk/by-id/.*-part(\\d+)$"
+let re_freebsd_gpt = PCRE.compile "^/dev/(ada{0,1}|vtbd)(\\d+)p(\\d+)$"
+let re_freebsd_mbr = PCRE.compile "^/dev/(ada{0,1}|vtbd)(\\d+)s(\\d+)([a-z])$"
+let re_hurd_dev = PCRE.compile "^/dev/(h)d(\\d+)s(\\d+)$"
+let re_mdN = PCRE.compile "^/dev/md\\d+$"
+let re_netbsd_dev = PCRE.compile "^/dev/(l|s)d([0-9])([a-z])$"
+let re_openbsd_dev = PCRE.compile "^/dev/(s|w)d([0-9])([a-z])$"
+let re_openbsd_duid = PCRE.compile "^[0-9a-f]{16}\\.[a-z]"
+let re_xdev = PCRE.compile "^/dev/(h|s|v|xv)d([a-z]+)(\\d*)$"
+
+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 =
+    match os_type with
+    | OS_TYPE_FREEBSD | OS_TYPE_NETBSD | OS_TYPE_OPENBSD -> true
+    | OS_TYPE_DOS | OS_TYPE_HURD | OS_TYPE_LINUX | OS_TYPE_MINIX
+    | OS_TYPE_WINDOWS -> false 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 PCRE.matches re_openbsd_duid spec then (
+             let part = spec.[17] in
+             (* We cannot peep into disklabels, we can only
+              * assume that this is the first disk.
+              *)
+             let device = sprintf "/dev/sd0%c" 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 PCRE.matches re_xdev spec then (
+    debug_matching "xdev";
+    let typ = PCRE.sub 1
+    and disk = PCRE.sub 2
+    and part = int_of_string (PCRE.sub 3) in
+    resolve_xdev typ disk part default
+  )
+
+  else if PCRE.matches re_cciss spec then (
+    debug_matching "cciss";
+    let disk = PCRE.sub 1
+    and part = try Some (int_of_string (PCRE.sub 2)) with Not_found -> None in
+    resolve_cciss disk part default
+  )
+
+  else if PCRE.matches re_mdN spec then (
+    debug_matching "md<N>";
+    try
+      Mountable.of_device (StringMap.find spec md_map)
+    with
+    | Not_found -> default
+  )
+
+  else if PCRE.matches re_diskbyid spec then (
+    debug_matching "diskbyid";
+    let part = int_of_string (PCRE.sub 1) in
+    resolve_diskbyid part default
+  )
+
+  else if PCRE.matches re_freebsd_gpt spec then (
+    debug_matching "FreeBSD GPT";
+    (* group 1 (type) is not used *)
+    let disk = int_of_string (PCRE.sub 2)
+    and part = int_of_string (PCRE.sub 3) 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 PCRE.matches re_freebsd_mbr spec then (
+    debug_matching "FreeBSD MBR";
+    (* group 1 (type) is not used *)
+    let disk = int_of_string (PCRE.sub 2)
+    and slice = int_of_string (PCRE.sub 3)
+    (* partition number counting from 0: *)
+    and part = Char.code (PCRE.sub 4).[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 && PCRE.matches re_netbsd_dev spec then (
+    debug_matching "NetBSD";
+    (* group 1 (type) is not used *)
+    let disk = int_of_string (PCRE.sub 2)
+    (* partition number counting from 0: *)
+    and part = Char.code (PCRE.sub 3).[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 && PCRE.matches re_openbsd_dev spec then (
+    debug_matching "OpenBSD";
+    (* group 1 (type) is not used *)
+    let disk = int_of_string (PCRE.sub 2)
+    (* partition number counting from 0: *)
+    and part = Char.code (PCRE.sub 3).[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 PCRE.matches re_hurd_dev spec then (
+    debug_matching "Hurd";
+    let typ = PCRE.sub 1
+    and disk = int_of_string (PCRE.sub 2)
+    and part = int_of_string (PCRE.sub 3) 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. *)
-- 
2.13.2

_______________________________________________
Libguestfs mailing list
Libguestfs@redhat.com
https://www.redhat.com/mailman/listinfo/libguestfs

Reply via email to