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 _______________________________________________ Libguestfs mailing list Libguestfs@redhat.com https://www.redhat.com/mailman/listinfo/libguestfs