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 | 788 +++++++++++++++++++++++++++++++++++++++
daemon/inspect_fs_unix.mli | 44 +++
daemon/inspect_fs_unix_fstab.ml | 537 ++++++++++++++++++++++++++
daemon/inspect_fs_unix_fstab.mli | 34 ++
9 files changed, 2234 insertions(+)
diff --git a/daemon/Makefile.am b/daemon/Makefile.am
index cab95f34f..a4657ed86 100644
--- a/daemon/Makefile.am
+++ b/daemon/Makefile.am
@@ -250,6 +250,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 \
@@ -290,6 +294,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..88a3d93d6
--- /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 = 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 "%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 } ->
+ 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..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..f09cdf51a
--- /dev/null
+++ b/daemon/inspect_fs_unix.ml
@@ -0,0 +1,788 @@
+(* 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" () 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" () 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" () 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" () 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 () 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" () 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. *)
--
2.13.2