398 lines
13 KiB
Python
398 lines
13 KiB
Python
|
#
|
||
|
# 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
|