# # Copyright (C) 2019 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of # the GNU General Public License v.2, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY expressed or implied, including the implied warranties 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. Any Red Hat trademarks that are incorporated in the # source code or documentation are not subject to the GNU General Public # License and may only be used or replicated with the express permission of # Red Hat, Inc. # import copy import os import shlex from blivet import util as blivet_util from blivet.errors import StorageError from blivet.storage_log import log_exception_info from pyanaconda.core.configuration.anaconda import conf from pyanaconda.core.i18n import _ from pyanaconda.core.path import set_system_root from pyanaconda.modules.storage.devicetree.fsset import BlkidTab, CryptTab from pyanaconda.anaconda_loggers import get_module_logger log = get_module_logger(__name__) __all__ = ["mount_existing_system", "find_existing_installations", "Root"] def mount_existing_system(storage, root_device, read_only=None): """Mount filesystems specified in root_device's /etc/fstab file.""" root_path = conf.target.physical_root read_only = "ro" if read_only else "" # Mount the root device. if root_device.protected and os.path.ismount("/mnt/install/isodir"): blivet_util.mount("/mnt/install/isodir", root_path, fstype=root_device.format.type, options="bind") else: root_device.setup() root_device.format.mount(chroot=root_path, mountpoint="/", options="%s,%s" % (root_device.format.options, read_only)) # Set up the sysroot. set_system_root(root_path) # Mount the filesystems. storage.fsset.parse_fstab(chroot=root_path) storage.fsset.mount_filesystems(root_path=root_path, read_only=read_only, skip_root=True) # Turn on swap. if not conf.target.is_image or not read_only: try: storage.fsset.turn_on_swap(root_path=root_path) except StorageError as e: log.error("Error enabling swap: %s", str(e)) # Generate mtab. if not read_only: storage.make_mtab(chroot=root_path) def find_existing_installations(devicetree): """Find existing GNU/Linux installations on devices from the device tree. :param devicetree: a device tree to find existing installations in :return: roots of all found installations """ try: roots = _find_existing_installations(devicetree) return roots except Exception: # pylint: disable=broad-except log_exception_info(log.info, "failure detecting existing installations") finally: devicetree.teardown_all() return [] def _find_existing_installations(devicetree): """Find existing GNU/Linux installations on devices from the device tree. :param devicetree: a device tree to find existing installations in :return: roots of all found installations """ if not os.path.exists(conf.target.physical_root): blivet_util.makedirs(conf.target.physical_root) sysroot = conf.target.physical_root roots = [] direct_devices = (dev for dev in devicetree.devices if dev.direct) for device in direct_devices: if not device.format.linux_native or not device.format.mountable or \ not device.controllable or not device.format.exists: continue try: device.setup() except Exception: # pylint: disable=broad-except log_exception_info(log.warning, "setup of %s failed", [device.name]) continue options = device.format.options + ",ro" try: device.format.mount(options=options, mountpoint=sysroot) except Exception: # pylint: disable=broad-except log_exception_info(log.warning, "mount of %s as %s failed", [device.name, device.format.type]) blivet_util.umount(mountpoint=sysroot) continue if not os.access(sysroot + "/etc/fstab", os.R_OK): blivet_util.umount(mountpoint=sysroot) device.teardown() continue architecture, product, version = get_release_string(chroot=sysroot) (mounts, devices) = _parse_fstab(devicetree, chroot=sysroot) blivet_util.umount(mountpoint=sysroot) if not mounts and not devices: # empty /etc/fstab. weird, but I've seen it happen. continue roots.append(Root( product=product, version=version, arch=architecture, devices=devices, mounts=mounts, )) return roots def get_release_string(chroot): """Identify the installation of a Linux distribution. Attempt to identify the installation of a Linux distribution by checking a previously mounted filesystem for several files. The filesystem must be mounted under the target physical root. :returns: The machine's arch, distribution name, and distribution version or None for any parts that cannot be determined :rtype: (string, string, string) """ rel_name = None rel_ver = None sysroot = chroot try: rel_arch = blivet_util.capture_output(["arch"], root=sysroot).strip() except OSError: rel_arch = None try: filename = "%s/etc/redhat-release" % sysroot if os.access(filename, os.R_OK): (rel_name, rel_ver) = _release_from_redhat_release(filename) else: filename = "%s/etc/os-release" % sysroot if os.access(filename, os.R_OK): (rel_name, rel_ver) = _release_from_os_release(filename) except ValueError: pass return rel_arch, rel_name, rel_ver def _release_from_redhat_release(fn): """Identify the installation of a Linux distribution via /etc/redhat-release. Attempt to identify the installation of a Linux distribution via /etc/redhat-release. This file must already have been verified to exist and be readable. :param fn: an open filehandle on /etc/redhat-release :type fn: filehandle :returns: The distribution's name and version, or None for either or both if they cannot be determined :rtype: (string, string) """ rel_name = None rel_ver = None with open(fn) as f: try: relstr = f.readline().strip() except (OSError, AttributeError): relstr = "" # get the release name and version # assumes that form is something # like "Red Hat Linux release 6.2 (Zoot)" (product, sep, version) = relstr.partition(" release ") if sep: rel_name = product rel_ver = version.split()[0] return rel_name, rel_ver def _release_from_os_release(fn): """Identify the installation of a Linux distribution via /etc/os-release. Attempt to identify the installation of a Linux distribution via /etc/os-release. This file must already have been verified to exist and be readable. :param fn: an open filehandle on /etc/os-release :type fn: filehandle :returns: The distribution's name and version, or None for either or both if they cannot be determined :rtype: (string, string) """ rel_name = None rel_ver = None with open(fn, "r") as f: parser = shlex.shlex(f) while True: key = parser.get_token() if key == parser.eof: break elif key == "NAME": # Throw away the "=". parser.get_token() rel_name = parser.get_token().strip("'\"") elif key == "VERSION_ID": # Throw away the "=". parser.get_token() rel_ver = parser.get_token().strip("'\"") return rel_name, rel_ver def _parse_fstab(devicetree, chroot): """Parse /etc/fstab. :param devicetree: a device tree :param chroot: a path to the target OS installation :return: a tuple of a mount dict and a device list """ mounts = {} devices = [] path = "%s/etc/fstab" % chroot if not os.access(path, os.R_OK): # XXX should we raise an exception instead? log.info("cannot open %s for read", path) return mounts, devices blkid_tab = BlkidTab(chroot=chroot) try: blkid_tab.parse() log.debug("blkid.tab devs: %s", list(blkid_tab.devices.keys())) except Exception: # pylint: disable=broad-except log_exception_info(log.info, "error parsing blkid.tab") blkid_tab = None crypt_tab = CryptTab(devicetree, blkid_tab=blkid_tab, chroot=chroot) try: crypt_tab.parse(chroot=chroot) log.debug("crypttab maps: %s", list(crypt_tab.mappings.keys())) except Exception: # pylint: disable=broad-except log_exception_info(log.info, "error parsing crypttab") crypt_tab = None with open(path) as f: log.debug("parsing %s", path) for line in f.readlines(): (line, _pound, _comment) = line.partition("#") fields = line.split(None, 4) if len(fields) < 5: continue (devspec, mountpoint, fstype, options, _rest) = fields # find device in the tree device = devicetree.resolve_device( devspec, crypt_tab=crypt_tab, blkid_tab=blkid_tab, options=options ) if device is None: continue # If a btrfs volume is found but a subvolume is expected, ignore the volume. if device.type == "btrfs volume" and "subvol=" in options: log.debug("subvolume from %s for %s not found", options, devspec) continue if fstype != "swap": mounts[mountpoint] = device devices.append(device) return mounts, devices class Root(object): """A root represents an existing OS installation.""" def __init__(self, name=None, product=None, version=None, arch=None, devices=None, mounts=None): """Create a new OS representation. :param name: a name of the OS or None :param product: a distribution name or None :param version: a distribution version or None :param arch: a machine's architecture or None :param devices: a list of all devices :param mounts: a dictionary of mount points and devices """ self._name = name self._product = product self._version = version self._arch = arch self._devices = devices or [] self._mounts = mounts or {} @property def name(self): """The name of the OS.""" # Use the specified name. if self._name: return self._name # Or generate a translated name. if not self._product or not self._version or not self._arch: return _("Unknown Linux") if "linux" in self._product.lower(): template = _("{product} {version} for {arch}") else: template = _("{product} Linux {version} for {arch}") return template.format( product=self._product, version=self._version, arch=self._arch ) @property def devices(self): """Devices used by the OS. For example: * bootloader devices * mount point sources * swap devices :return: a list of all devices """ return self._devices @property def mounts(self): """Mount points defined by the OS. :return: a dictionary of mount points and devices """ return self._mounts def copy(self, storage): """Create a copy with devices of the given storage model. :param InstallerStorage storage: a storage model :return Root: a copy of this root object """ new_root = copy.deepcopy(self) def _get_device(d): return storage.devicetree.get_device_by_id(d.id, hidden=True) def _get_mount(i): m, d = i[0], _get_device(i[1]) return (m, d) if m and d else None new_root._devices = list(filter(None, map(_get_device, new_root._devices))) new_root._mounts = dict(filter(None, map(_get_mount, new_root._mounts.items()))) return new_root