calamares/src/modules/unpackfs/main.py

415 lines
14 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
2015-02-18 15:06:10 +01:00
# -*- coding: utf-8 -*-
#
# === This file is part of Calamares - <https://github.com/calamares> ===
#
# Copyright 2014, Teo Mrnjavac <teo@kde.org>
# Copyright 2014, Daniel Hillenbrand <codeworkx@bbqlinux.org>
# Copyright 2014, Philip Müller <philm@manjaro.org>
# Copyright 2017, Alf Gaida <agaida@siduction.org>
# Copyright 2019, Kevin Kofler <kevin.kofler@chello.at>
#
# Calamares 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 3 of the License, or
# (at your option) any later version.
#
# Calamares 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 Calamares. If not, see <http://www.gnu.org/licenses/>.
import os
import re
2014-07-28 17:46:56 +02:00
import shutil
import subprocess
import sys
import tempfile
from libcalamares import *
import gettext
_ = gettext.translation("calamares-python",
localedir=utils.gettext_path(),
languages=utils.gettext_languages(),
fallback=True).gettext
2015-02-18 15:47:24 +01:00
def pretty_name():
2019-03-29 22:32:52 +01:00
return _("Filling up filesystems.")
class UnpackEntry:
"""
Extraction routine using rsync.
2015-02-20 20:54:25 +01:00
:param source:
:param sourcefs:
:param destination:
"""
__slots__ = ['source', 'sourcefs', 'destination', 'copied', 'total', 'exclude', 'excludeFile']
def __init__(self, source, sourcefs, destination):
"""
@p source is the source file name (might be an image file, or
a directory, too)
@p sourcefs is a type indication; "file" is special, as is
"squashfs".
@p destination is where the files from the source go. This is
**already** prefixed by rootMountPoint, so should be a
valid absolute path within the host system.
The members copied and total are filled in by the copying process.
"""
self.source = source
self.sourcefs = sourcefs
self.destination = destination
self.exclude = None
self.excludeFile = None
self.copied = 0
self.total = 0
2019-09-30 16:47:39 +02:00
def is_file(self):
return self.sourcefs == "file"
2015-02-18 15:47:24 +01:00
ON_POSIX = 'posix' in sys.builtin_module_names
def global_excludes():
"""
List excludes for rsync.
2015-02-20 20:54:25 +01:00
"""
lst = []
extra_mounts = globalstorage.value("extraMounts")
if extra_mounts is None:
extra_mounts = []
2015-06-14 13:25:37 +02:00
for extra_mount in extra_mounts:
mount_point = extra_mount["mountPoint"]
2015-06-14 13:25:37 +02:00
if mount_point:
lst.extend(['--exclude', mount_point + '/'])
2015-06-14 13:25:37 +02:00
return lst
def file_copy(source, entry, progress_cb):
"""
Extract given image using rsync.
2015-02-20 20:54:25 +01:00
:param source: Source file. This may be the place the entry's
image is mounted, or if it's a single file, the entry's source value.
:param entry: The UnpackEntry being copied.
:param progress_cb: A callback function for progress reporting.
Takes a number and a total-number.
2015-02-20 20:54:25 +01:00
"""
dest = entry.destination
# Environment used for executing rsync properly
# Setting locale to C (fix issue with tr_TR locale)
at_env = os.environ
at_env["LC_ALL"] = "C"
2014-08-01 11:46:29 +02:00
# `source` *must* end with '/' otherwise a directory named after the source
# will be created in `dest`: ie if `source` is "/foo/bar" and `dest` is
# "/dest", then files will be copied in "/dest/bar".
if not source.endswith("/") and not os.path.isfile(source):
2019-03-29 21:09:11 +01:00
source += "/"
2014-08-01 11:46:29 +02:00
num_files_total_local = 0
num_files_copied = 0 # Gets updated through rsync output
args = ['rsync', '-aHAXr']
args.extend(global_excludes())
args.extend(['--progress', source, dest])
process = subprocess.Popen(
args, env=at_env, bufsize=1, stdout=subprocess.PIPE, close_fds=ON_POSIX
)
2014-07-29 15:10:18 +02:00
for line in iter(process.stdout.readline, b''):
# rsync outputs progress in parentheses. Each line will have an
# xfer and a chk item (either ir-chk or to-chk) as follows:
#
# - xfer#x => Interpret it as 'file copy try no. x'
# - ir-chk=x/y, where:
# - x = number of files yet to be checked
# - y = currently calculated total number of files.
# - to-chk=x/y, which is similar and happens once the ir-chk
# phase (collecting total files) is over.
#
# If you're copying directory with some links in it, the xfer#
2014-07-29 15:10:18 +02:00
# might not be a reliable counter (for one increase of xfer, many
# files may be created).
m = re.findall(r'xfr#(\d+), ..-chk=(\d+)/(\d+)', line.decode())
2015-06-14 13:25:37 +02:00
if m:
# we've got a percentage update
num_files_remaining = int(m[0][1])
num_files_total_local = int(m[0][2])
# adjusting the offset so that progressbar can be continuesly drawn
num_files_copied = num_files_total_local - num_files_remaining
# I guess we're updating every 100 files...
if num_files_copied % 100 == 0:
progress_cb(num_files_copied, num_files_total_local)
2015-06-14 13:25:37 +02:00
process.wait()
progress_cb(num_files_copied, num_files_total_local) # Push towards 100%
2015-06-14 13:25:37 +02:00
# 23 is the return code rsync returns if it cannot write extended
# attributes (with -X) because the target file system does not support it,
# e.g., the FAT EFI system partition. We need -X because distributions
# using file system capabilities and/or SELinux require the extended
# attributes. But distributions using SELinux may also have SELinux labels
# set on files under /boot/efi, and rsync complains about those. The only
# clean way would be to split the rsync into one with -X and
# --exclude /boot/efi and a separate one without -X for /boot/efi, but only
# if /boot/efi is actually an EFI system partition. For now, this hack will
# have to do. See also:
# https://bugzilla.redhat.com/show_bug.cgi?id=868755#c50
# for the same issue in Anaconda, which uses a similar workaround.
if process.returncode != 0 and process.returncode != 23:
utils.warning("rsync failed with error code {}.".format(process.returncode))
return _("rsync failed with error code {}.").format(process.returncode)
2015-06-14 13:25:37 +02:00
return None
class UnpackOperation:
"""
Extraction routine using unsquashfs.
2015-02-20 20:54:25 +01:00
:param entries:
"""
2015-06-14 13:25:37 +02:00
def __init__(self, entries):
self.entries = entries
self.entry_for_source = dict((x.source, x) for x in self.entries)
2014-07-29 15:10:18 +02:00
def report_progress(self):
"""
Pass progress to user interface
"""
2014-07-29 15:10:18 +02:00
progress = float(0)
2015-06-14 13:25:37 +02:00
done = 0
total = 0
complete = 0
for entry in self.entries:
if entry.total == 0:
continue
total += entry.total
done += entry.copied
if entry.total == entry.copied:
complete += 1
if done > 0 and total > 0:
progress = 0.05 + (0.90 * done / total) + (0.05 * complete / len(self.entries))
2014-07-29 15:10:18 +02:00
job.setprogress(progress)
2014-07-29 15:10:18 +02:00
def run(self):
"""
Extract given image using unsquashfs.
2015-02-20 20:54:25 +01:00
:return:
"""
2014-07-29 15:10:18 +02:00
source_mount_path = tempfile.mkdtemp()
2015-06-14 13:25:37 +02:00
try:
for entry in self.entries:
imgbasename = os.path.splitext(
os.path.basename(entry.source))[0]
2014-07-30 15:06:59 +02:00
imgmountdir = os.path.join(source_mount_path, imgbasename)
os.makedirs(imgmountdir, exist_ok=True)
self.mount_image(entry, imgmountdir)
2014-08-19 19:12:48 +02:00
fslist = ""
if entry.sourcefs == "squashfs":
2015-02-18 15:20:02 +01:00
if shutil.which("unsquashfs") is None:
utils.warning("Failed to find unsquashfs")
return (_("Failed to unpack image \"{}\"").format(entry.source),
_("Failed to find unsquashfs, make sure you have the squashfs-tools package installed"))
fslist = subprocess.check_output(
["unsquashfs", "-l", entry.source]
)
2015-06-14 13:25:37 +02:00
elif entry.sourcefs == "ext4":
fslist = subprocess.check_output(
["find", imgmountdir, "-type", "f"]
)
2015-06-14 13:25:37 +02:00
elif entry.is_file():
# Hasn't been mounted, copy directly; find handles both
# files and directories.
fslist = subprocess.check_output(["find", entry.source, "-type", "f"])
2014-08-19 19:12:48 +02:00
entry.total = len(fslist.splitlines())
2014-07-29 15:10:18 +02:00
self.report_progress()
error_msg = self.unpack_image(entry, imgmountdir)
2015-06-14 13:25:37 +02:00
if error_msg:
return (_("Failed to unpack image \"{}\"").format(entry.source),
error_msg)
2015-06-14 13:25:37 +02:00
return None
finally:
shutil.rmtree(source_mount_path, ignore_errors=True, onerror=None)
2014-07-28 12:17:06 +02:00
def mount_image(self, entry, imgmountdir):
"""
Mount given image as loop device.
2015-02-20 20:54:25 +01:00
2019-09-30 16:47:39 +02:00
A *file* entry (e.g. one with *sourcefs* set to *file*)
is not mounted and just ignored.
2015-02-20 20:54:25 +01:00
:param entry:
:param imgmountdir:
"""
2019-09-30 16:47:39 +02:00
if entry.is_file():
return
if os.path.isdir(entry.source):
subprocess.check_call(["mount",
"--bind", entry.source,
imgmountdir])
elif os.path.isfile(entry.source):
subprocess.check_call(["mount",
entry.source,
imgmountdir,
"-t", entry.sourcefs,
"-o", "loop"
])
else: # entry.source is a device
subprocess.check_call(["mount",
entry.source,
imgmountdir,
"-t", entry.sourcefs
])
def unpack_image(self, entry, imgmountdir):
"""
Unpacks image.
2015-02-20 20:54:25 +01:00
:param entry:
:param imgmountdir:
:return:
"""
def progress_cb(copied, total):
2015-02-21 11:02:25 +01:00
""" Copies file to given destination target.
2015-02-20 20:54:25 +01:00
:param copied:
"""
entry.copied = copied
if total > entry.total:
entry.total = total
2014-07-30 15:35:51 +02:00
self.report_progress()
try:
if entry.is_file():
source = entry.source
else:
source = imgmountdir
return file_copy(source, entry, progress_cb)
finally:
2019-09-30 16:47:39 +02:00
if not entry.is_file():
subprocess.check_call(["umount", "-l", imgmountdir])
2019-09-30 16:47:39 +02:00
def get_supported_filesystems_kernel():
"""
Reads /proc/filesystems (the list of supported filesystems
for the current kernel) and returns a list of (names of)
those filesystems.
2015-02-20 20:54:25 +01:00
"""
PATH_PROCFS = '/proc/filesystems'
if os.path.isfile(PATH_PROCFS) and os.access(PATH_PROCFS, os.R_OK):
with open(PATH_PROCFS, 'r') as procfile:
filesystems = procfile.read()
filesystems = filesystems.replace(
"nodev", "").replace("\t", "").splitlines()
return filesystems
return []
2019-09-30 16:47:39 +02:00
def get_supported_filesystems():
"""
Returns a list of all the supported filesystems
(valid values for the *sourcefs* key in an item.
"""
return ["file"] + get_supported_filesystems_kernel()
def run():
"""
Unsquash filesystem.
"""
2014-07-29 15:10:18 +02:00
root_mount_point = globalstorage.value("rootMountPoint")
2015-06-14 13:25:37 +02:00
2014-07-29 15:10:18 +02:00
if not root_mount_point:
utils.warning("No mount point for root partition")
return (_("No mount point for root partition"),
_("globalstorage does not contain a \"rootMountPoint\" key, "
"doing nothing"))
2015-06-14 13:25:37 +02:00
2014-07-29 15:10:18 +02:00
if not os.path.exists(root_mount_point):
utils.warning("Bad root mount point \"{}\"".format(root_mount_point))
return (_("Bad mount point for root partition"),
_("rootMountPoint is \"{}\", which does not "
"exist, doing nothing").format(root_mount_point))
2015-06-14 13:25:37 +02:00
supported_filesystems = get_supported_filesystems()
# Bail out before we start when there are obvious problems
2014-07-29 15:10:18 +02:00
for entry in job.configuration["unpack"]:
source = os.path.abspath(entry["source"])
sourcefs = entry["sourcefs"]
if sourcefs not in supported_filesystems:
utils.warning("The filesystem for \"{}\" ({}) is not supported".format(source, sourcefs))
return (_("Bad unsquash configuration"),
_("The filesystem for \"{}\" ({}) is not supported").format(source, sourcefs))
if not os.path.exists(source):
utils.warning("The source filesystem \"{}\" does not exist".format(source))
return (_("Bad unsquash configuration"),
_("The source filesystem \"{}\" does not exist").format(source))
2015-06-14 13:25:37 +02:00
unpack = list()
is_first = True
for entry in job.configuration["unpack"]:
source = os.path.abspath(entry["source"])
sourcefs = entry["sourcefs"]
destination = os.path.abspath(root_mount_point + entry["destination"])
if not os.path.isdir(destination) and sourcefs != "file":
utils.warning(("The destination \"{}\" in the target system is not a directory").format(destination))
if is_first:
return (_("Bad unsquash configuration"),
_("The destination \"{}\" in the target system is not a directory").format(destination))
else:
utils.debug(".. assuming that the previous targets will create that directory.")
unpack.append(UnpackEntry(source, sourcefs, destination))
# Optional settings
if entry.get("exclude", None):
unpack[-1].exclude = entry["exclude"]
if entry.get("excludeFile", None):
unpack[-1].excludeFile = entry["excludeFile"]
is_first = False
unpackop = UnpackOperation(unpack)
2015-06-14 13:25:37 +02:00
return unpackop.run()