qubes-installer-qubes-os/anaconda/pyanaconda/packaging/livepayload.py
Marek Marczykowski-Górecki 6bc5671491
anaconda: update to 25.20.9-1
Apply:
  git diff --full-index --binary anaconda-23.19.10-1..anaconda-25.20.9-1

And resolve conflicts.

QubesOS/qubes-issues#2574
2017-02-14 02:36:20 +01:00

543 lines
20 KiB
Python

# livepayload.py
# Live media software payload management.
#
# Copyright (C) 2012 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.
#
"""
TODO
- error handling!!!
- document all methods
- LiveImagePayload
- register the live image, either via self.data.method or in setup
using storage
"""
import os
import stat
from time import sleep
from threading import Lock
import requests
from pyanaconda.iutil import ProxyString, ProxyStringError, lowerASCII
import hashlib
import glob
import functools
from pyanaconda.packaging import ImagePayload, PayloadSetupError, PayloadInstallError
from pyanaconda.constants import INSTALL_TREE, THREAD_LIVE_PROGRESS
from pyanaconda.constants import IMAGE_DIR, TAR_SUFFIX
from pyanaconda import iutil
import logging
log = logging.getLogger("packaging")
from pyanaconda.errors import errorHandler, ERROR_RAISE
from pyanaconda.progress import progressQ
from blivet.size import Size
import blivet.util
from pyanaconda.threads import threadMgr, AnacondaThread
from pyanaconda.i18n import _
from pyanaconda.packaging import versionCmp
class LiveImagePayload(ImagePayload):
""" A LivePayload copies the source image onto the target system. """
def __init__(self, *args, **kwargs):
super(LiveImagePayload, self).__init__(*args, **kwargs)
# Used to adjust size of sysroot when files are already present
self._adj_size = 0
self.pct = 0
self.pct_lock = None
self.source_size = 1
self._kernelVersionList = []
def setup(self, storage, instClass):
super(LiveImagePayload, self).setup(storage, instClass)
# Mount the live device and copy from it instead of the overlay at /
osimg = storage.devicetree.get_device_by_path(self.data.method.partition)
if not osimg:
raise PayloadInstallError("Unable to find osimg for %s" % self.data.method.partition)
if not stat.S_ISBLK(os.stat(osimg.path)[stat.ST_MODE]):
exn = PayloadSetupError("%s is not a valid block device" % (self.data.method.partition,))
if errorHandler.cb(exn) == ERROR_RAISE:
raise exn
rc = blivet.util.mount(osimg.path, INSTALL_TREE, fstype="auto", options="ro")
if rc != 0:
raise PayloadInstallError("Failed to mount the install tree")
# Grab the kernel version list now so it's available after umount
self._updateKernelVersionList()
source = os.statvfs(INSTALL_TREE)
self.source_size = source.f_frsize * (source.f_blocks - source.f_bfree)
def unsetup(self):
super(LiveImagePayload, self).unsetup()
# Unmount a previously mounted live tree
try:
blivet.util.umount(INSTALL_TREE)
except OSError:
pass
def preInstall(self, packages=None, groups=None):
""" Perform pre-installation tasks. """
super(LiveImagePayload, self).preInstall(packages=packages, groups=groups)
progressQ.send_message(_("Installing software") + (" %d%%") % (0,))
def progress(self):
"""Monitor the amount of disk space used on the target and source and
update the hub's progress bar.
"""
mountpoints = self.storage.mountpoints.copy()
last_pct = -1
while self.pct < 100:
dest_size = 0
for mnt in mountpoints:
mnt_stat = os.statvfs(iutil.getSysroot()+mnt)
dest_size += mnt_stat.f_frsize * (mnt_stat.f_blocks - mnt_stat.f_bfree)
if dest_size >= self._adj_size:
dest_size -= self._adj_size
pct = int(100 * dest_size / self.source_size)
if pct != last_pct:
with self.pct_lock:
self.pct = pct
last_pct = pct
progressQ.send_message(_("Installing software") + (" %d%%") % (min(100, self.pct),))
sleep(0.777)
def install(self):
""" Install the payload. """
if self.source_size <= 0:
raise PayloadInstallError("Nothing to install")
self.pct_lock = Lock()
self.pct = 0
threadMgr.add(AnacondaThread(name=THREAD_LIVE_PROGRESS,
target=self.progress))
cmd = "rsync"
# preserve: permissions, owners, groups, ACL's, xattrs, times,
# symlinks, hardlinks
# go recursively, include devices and special files, don't cross
# file system boundaries
args = ["-pogAXtlHrDx", "--exclude", "/dev/", "--exclude", "/proc/",
"--exclude", "/sys/", "--exclude", "/run/", "--exclude", "/boot/*rescue*",
"--exclude", "/etc/machine-id", INSTALL_TREE+"/", iutil.getSysroot()]
try:
rc = iutil.execWithRedirect(cmd, args)
except (OSError, RuntimeError) as e:
msg = None
err = str(e)
log.error(err)
else:
err = None
msg = "%s exited with code %d" % (cmd, rc)
log.info(msg)
if err or rc == 12:
exn = PayloadInstallError(err or msg)
if errorHandler.cb(exn) == ERROR_RAISE:
raise exn
# Wait for progress thread to finish
with self.pct_lock:
self.pct = 100
threadMgr.wait(THREAD_LIVE_PROGRESS)
# Live needs to create the rescue image before bootloader is written
if not os.path.exists(iutil.getSysroot() + "/usr/sbin/new-kernel-pkg"):
log.error("new-kernel-pkg does not exist - grubby wasn't installed? skipping")
return
for kernel in self.kernelVersionList:
log.info("Generating rescue image for %s", kernel)
iutil.execInSysroot("new-kernel-pkg",
["--rpmposttrans", kernel])
def postInstall(self):
""" Perform post-installation tasks. """
progressQ.send_message(_("Performing post-installation setup tasks"))
blivet.util.umount(INSTALL_TREE)
super(LiveImagePayload, self).postInstall()
# Make sure the new system has a machine-id, it won't boot without it
if not os.path.exists(iutil.getSysroot()+"/etc/machine-id"):
iutil.execInSysroot("systemd-machine-id-setup", [])
@property
def spaceRequired(self):
return Size(iutil.getDirSize("/")*1024)
def _updateKernelVersionList(self):
files = glob.glob(INSTALL_TREE + "/boot/vmlinuz-*")
files.extend(glob.glob(INSTALL_TREE + "/boot/efi/EFI/%s/vmlinuz-*" % self.instclass.efi_dir))
self._kernelVersionList = sorted((f.split("/")[-1][8:] for f in files
if os.path.isfile(f) and "-rescue-" not in f), key=functools.cmp_to_key(versionCmp))
@property
def kernelVersionList(self):
return self._kernelVersionList
def writeStorageEarly(self):
pass
class DownloadProgress(object):
""" Provide methods for download progress reporting."""
def start(self, url, size):
""" Start of download
:param url: url of the download
:type url: str
:param size: length of the file
:type size: int
"""
self.url = url
self.size = size
self._pct = -1
def update(self, bytes_read):
""" Download update
:param bytes_read: Bytes read so far
:type bytes_read: int
"""
if not bytes_read:
return
pct = min(100, int(100 * bytes_read / self.size))
if pct == self._pct:
return
self._pct = pct
progressQ.send_message(_("Downloading %(url)s (%(pct)d%%)") % \
{"url" : self.url, "pct" : pct})
def end(self, bytes_read):
""" Download complete
:param bytes_read: Bytes read so far
:type bytes_read: int
"""
progressQ.send_message(_("Downloading %(url)s (%(pct)d%%)") % \
{"url" : self.url, "pct" : 100})
class LiveImageKSPayload(LiveImagePayload):
""" Install using a live filesystem image from the network """
def __init__(self, *args, **kwargs):
super(LiveImageKSPayload, self).__init__(*args, **kwargs)
self._min_size = 0
self._proxies = {}
self.image_path = iutil.getSysroot()+"/disk.img"
@property
def is_tarfile(self):
""" Return True if the url ends with a tar suffix """
return any(self.data.method.url.endswith(suffix) for suffix in TAR_SUFFIX)
def _setup_url_image(self):
""" Check to make sure the url is available and estimate the space
needed to download and install it.
"""
self._proxies = {}
if self.data.method.proxy:
try:
proxy = ProxyString(self.data.method.proxy)
self._proxies = {"http": proxy.url,
"https": proxy.url}
except ProxyStringError as e:
log.info("Failed to parse proxy for liveimg --proxy=\"%s\": %s",
self.data.method.proxy, e)
error = None
try:
response = self._session.get(self.data.method.url, proxies=self._proxies, verify=True)
# At this point we know we can get the image and what its size is
# Make a guess as to minimum size needed:
# Enough space for image and image * 3
if response.headers.get('content-length'):
self._min_size = int(response.headers.get('content-length')) * 4
except IOError as e:
log.error("Error opening liveimg: %s", e)
error = e
else:
if response.status_code != 200:
error = "http request returned %s" % response.status_code
return error
def _setup_file_image(self):
""" Check to make sure the file is available and estimate the space
needed to install it.
"""
if not os.path.exists(self.data.method.url[7:]):
return "file does not exist: %s" % self.data.method.url
self._min_size = os.stat(self.data.method.url[7:])[stat.ST_SIZE] * 3
return None
def setup(self, storage, instClass):
""" Check the availability and size of the image.
"""
# This is on purpose, we don't want to call LiveImagePayload's setup method.
ImagePayload.setup(self, storage, instClass)
if self.data.method.url.startswith("file://"):
error = self._setup_file_image()
else:
error = self._setup_url_image()
if error:
exn = PayloadInstallError(str(error))
if errorHandler.cb(exn) == ERROR_RAISE:
raise exn
log.debug("liveimg size is %s", self._min_size)
def unsetup(self):
# Skip LiveImagePayload's unsetup method
ImagePayload.unsetup(self)
def _preInstall_url_image(self):
""" Download the image using Requests with progress reporting"""
error = None
progress = DownloadProgress()
try:
log.info("Starting image download")
with open(self.image_path, "wb") as f:
ssl_verify = not self.data.method.noverifyssl
response = self._session.get(self.data.method.url, proxies=self._proxies, verify=ssl_verify, stream=True)
total_length = response.headers.get('content-length')
if total_length is None: # no content length header
# just download the file in one go and fake the progress reporting once done
log.warning("content-length header is missing for the installation image, "
"download progress reporting will not be available")
f.write(response.content)
size = f.tell()
progress.start(self.data.method.url, size)
progress.end(size)
else:
# requests return headers as strings, so convert total_length to int
progress.start(self.data.method.url, int(total_length))
bytes_read = 0
for buf in response.iter_content(1024 * 1024): # 1 MB chunks
if buf:
f.write(buf)
f.flush()
bytes_read += len(buf)
progress.update(bytes_read)
progress.end(bytes_read)
log.info("Image download finished")
except requests.exceptions.RequestException as e:
log.error("Error downloading liveimg: %s", e)
error = e
else:
if not os.path.exists(self.image_path):
error = "Failed to download %s, file doesn't exist" % self.data.method.url
log.error(error)
def preInstall(self, *args, **kwargs):
""" Get image and loopback mount it.
This is called after partitioning is setup, we now have space to
grab the image. If it is a network source Download it to sysroot
and provide feedback during the download (using urlgrabber
callback).
If it is a file:// source then use the file directly.
"""
error = None
if self.data.method.url.startswith("file://"):
self.image_path = self.data.method.url[7:]
else:
error = self._preInstall_url_image()
if error:
exn = PayloadInstallError(str(error))
if errorHandler.cb(exn) == ERROR_RAISE:
raise exn
# Used to make install progress % look correct
self._adj_size = os.stat(self.image_path)[stat.ST_SIZE]
if self.data.method.checksum:
progressQ.send_message(_("Checking image checksum"))
sha256 = hashlib.sha256()
with open(self.image_path, "rb") as f:
while True:
data = f.read(1024*1024)
if not data:
break
sha256.update(data)
filesum = sha256.hexdigest()
log.debug("sha256 of %s is %s", self.data.method.url, filesum)
if lowerASCII(self.data.method.checksum) != filesum:
log.error("%s does not match checksum.", self.data.method.checksum)
exn = PayloadInstallError("Checksum of image does not match")
if errorHandler.cb(exn) == ERROR_RAISE:
raise exn
# If this looks like a tarfile, skip trying to mount it
if self.is_tarfile:
return
# Mount the image and check to see if it is a LiveOS/*.img
# style squashfs image. If so, move it to IMAGE_DIR and mount the real
# root image on INSTALL_TREE
rc = blivet.util.mount(self.image_path, INSTALL_TREE, fstype="auto", options="ro")
if rc != 0:
log.error("mount error (%s) with %s", rc, self.image_path)
exn = PayloadInstallError("mount error %s" % rc)
if errorHandler.cb(exn) == ERROR_RAISE:
raise exn
# Nothing more to mount
if not os.path.exists(INSTALL_TREE+"/LiveOS"):
self._updateKernelVersionList()
return
# Mount the first .img in the directory on INSTALL_TREE
img_files = glob.glob(INSTALL_TREE+"/LiveOS/*.img")
if img_files:
# move the mount to IMAGE_DIR
os.makedirs(IMAGE_DIR, 0o755)
# work around inability to move shared filesystems
rc = iutil.execWithRedirect("mount",
["--make-rprivate", "/"])
if rc == 0:
rc = iutil.execWithRedirect("mount",
["--move", INSTALL_TREE, IMAGE_DIR])
if rc != 0:
log.error("error %s moving mount", rc)
exn = PayloadInstallError("mount error %s" % rc)
if errorHandler.cb(exn) == ERROR_RAISE:
raise exn
img_file = IMAGE_DIR+"/LiveOS/"+os.path.basename(sorted(img_files)[0])
rc = blivet.util.mount(img_file, INSTALL_TREE, fstype="auto", options="ro")
if rc != 0:
log.error("mount error (%s) with %s", rc, img_file)
exn = PayloadInstallError("mount error %s with %s" % (rc, img_file))
if errorHandler.cb(exn) == ERROR_RAISE:
raise exn
self._updateKernelVersionList()
source = os.statvfs(INSTALL_TREE)
self.source_size = source.f_frsize * (source.f_blocks - source.f_bfree)
def install(self):
""" Install the payload if it is a tar.
Otherwise fall back to rsync of INSTALL_TREE
"""
# If it doesn't look like a tarfile use the super's install()
if not self.is_tarfile:
super(LiveImageKSPayload, self).install()
return
# Use 2x the archive's size to estimate the size of the install
# This is used to drive the progress display
self.source_size = os.stat(self.image_path)[stat.ST_SIZE] * 2
self.pct_lock = Lock()
self.pct = 0
threadMgr.add(AnacondaThread(name=THREAD_LIVE_PROGRESS,
target=self.progress))
cmd = "tar"
# preserve: ACL's, xattrs, and SELinux context
args = ["--selinux", "--acls", "--xattrs", "--xattrs-include", "*",
"--exclude", "/dev/", "--exclude", "/proc/",
"--exclude", "/sys/", "--exclude", "/run/", "--exclude", "/boot/*rescue*",
"--exclude", "/etc/machine-id", "-xaf", self.image_path, "-C", iutil.getSysroot()]
try:
rc = iutil.execWithRedirect(cmd, args)
except (OSError, RuntimeError) as e:
msg = None
err = str(e)
log.error(err)
else:
err = None
msg = "%s exited with code %d" % (cmd, rc)
log.info(msg)
if err:
exn = PayloadInstallError(err or msg)
if errorHandler.cb(exn) == ERROR_RAISE:
raise exn
# Wait for progress thread to finish
with self.pct_lock:
self.pct = 100
threadMgr.wait(THREAD_LIVE_PROGRESS)
# Live needs to create the rescue image before bootloader is written
for kernel in self.kernelVersionList:
log.info("Generating rescue image for %s", kernel)
iutil.execInSysroot("new-kernel-pkg",
["--rpmposttrans", kernel])
def postInstall(self):
""" Unmount and remove image
If file:// was used, just unmount it.
"""
super(LiveImageKSPayload, self).postInstall()
if os.path.exists(IMAGE_DIR+"/LiveOS"):
blivet.util.umount(IMAGE_DIR)
if os.path.exists(self.image_path) and not self.data.method.url.startswith("file://"):
os.unlink(self.image_path)
@property
def spaceRequired(self):
""" We don't know the filesystem size until it is downloaded.
Default to 1G which should be enough for a minimal image download
and install.
"""
if self._min_size:
return Size(self._min_size)
else:
return Size(1024*1024*1024)
@property
def kernelVersionList(self):
# If it doesn't look like a tarfile use the super's kernelVersionList
if not self.is_tarfile:
return super(LiveImageKSPayload, self).kernelVersionList
import tarfile
with tarfile.open(self.image_path) as archive:
names = archive.getnames()
# Strip out vmlinuz- from the names
return sorted((n.split("/")[-1][8:] for n in names if "boot/vmlinuz-" in n),
key=functools.cmp_to_key(versionCmp))