1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720
|
# -*- coding: utf-8 -*-
#
# Copyright (c) 2015-2020 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, see <http://www.gnu.org/licenses/>.
#
# Author(s): Will Woods <wwoods@redhat.com>
"""system_upgrade.py - DNF plugin to handle major-version system upgrades."""
from subprocess import call, Popen, check_output, CalledProcessError
import json
import os
import os.path
import re
import sys
import uuid
from systemd import journal
from dnfpluginscore import _, logger
import dnf
import dnf.cli
from dnf.cli import CliError
from dnf.i18n import ucd
import dnf.transaction
from dnf.transaction_sr import serialize_transaction, TransactionReplay
import libdnf.conf
# Translators: This string is only used in unit tests.
_("the color of the sky")
DOWNLOAD_FINISHED_ID = uuid.UUID('9348174c5cc74001a71ef26bd79d302e')
REBOOT_REQUESTED_ID = uuid.UUID('fef1cc509d5047268b83a3a553f54b43')
UPGRADE_STARTED_ID = uuid.UUID('3e0a5636d16b4ca4bbe5321d06c6aa62')
UPGRADE_FINISHED_ID = uuid.UUID('8cec00a1566f4d3594f116450395f06c')
ID_TO_IDENTIFY_BOOTS = UPGRADE_STARTED_ID
PLYMOUTH = '/usr/bin/plymouth'
RELEASEVER_MSG = _(
"Need a --releasever greater than the current system version.")
DOWNLOAD_FINISHED_MSG = _( # Translators: do not change "reboot" here
"Download complete! Use 'dnf {command} reboot' to start the upgrade.\n"
"To remove cached metadata and transaction use 'dnf {command} clean'")
CANT_RESET_RELEASEVER = _(
"Sorry, you need to use 'download --releasever' instead of '--network'")
STATE_VERSION = 3
# --- Miscellaneous helper functions ------------------------------------------
def reboot(poweroff = False):
if os.getenv("DNF_SYSTEM_UPGRADE_NO_REBOOT", default=False):
logger.info(_("Reboot turned off, not rebooting."))
else:
if poweroff:
Popen(["systemctl", "poweroff"])
else:
Popen(["systemctl", "reboot"])
def get_url_from_os_release():
key = "UPGRADE_GUIDE_URL="
for path in ["/etc/os-release", "/usr/lib/os-release"]:
try:
with open(path) as release_file:
for line in release_file:
line = line.strip()
if line.startswith(key):
return line[len(key):].strip('"')
except IOError:
continue
return None
# DNF-FIXME: dnf.util.clear_dir() doesn't delete regular files :/
def clear_dir(path, ignore=[]):
if not os.path.isdir(path):
return
for entry in os.listdir(path):
fullpath = os.path.join(path, entry)
if fullpath in ignore:
continue
try:
if os.path.isdir(fullpath):
dnf.util.rm_rf(fullpath)
else:
os.unlink(fullpath)
except OSError:
pass
def check_release_ver(conf, target=None):
if dnf.rpm.detect_releasever(conf.installroot) == conf.releasever:
raise CliError(RELEASEVER_MSG)
if target and target != conf.releasever:
# it's too late to set releasever here, so this can't work.
# (see https://bugzilla.redhat.com/show_bug.cgi?id=1212341)
raise CliError(CANT_RESET_RELEASEVER)
def disable_blanking():
try:
tty = open('/dev/tty0', 'wb')
tty.write(b'\33[9;0]')
except Exception as e:
print(_("Screen blanking can't be disabled: %s") % e)
# --- State object - for tracking upgrade state between runs ------------------
# DNF-INTEGRATION-NOTE: basically the same thing as dnf.persistor.JSONDB
class State(object):
def __init__(self, statefile):
self.statefile = statefile
self._data = {}
self._read()
def _read(self):
try:
with open(self.statefile) as fp:
self._data = json.load(fp)
except IOError:
self._data = {}
except ValueError:
self._data = {}
logger.warning(_("Failed loading state file: %s, continuing with "
"empty state."), self.statefile)
def write(self):
dnf.util.ensure_dir(os.path.dirname(self.statefile))
with open(self.statefile, 'w') as outf:
json.dump(self._data, outf, indent=4, sort_keys=True)
def clear(self):
if os.path.exists(self.statefile):
os.unlink(self.statefile)
self._read()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
if exc_type is None:
self.write()
# helper function for creating properties. pylint: disable=protected-access
def _prop(option): # pylint: disable=no-self-argument
def setprop(self, value):
self._data[option] = value
def getprop(self):
return self._data.get(option)
return property(getprop, setprop)
# !!! Increase STATE_VERSION for any changes in data structure like a new property or a new
# data structure !!!
state_version = _prop("state_version")
download_status = _prop("download_status")
destdir = _prop("destdir")
target_releasever = _prop("target_releasever")
system_releasever = _prop("system_releasever")
gpgcheck = _prop("gpgcheck")
# list of repos with gpgcheck=True
gpgcheck_repos = _prop("gpgcheck_repos")
# list of repos with repo_gpgcheck=True
repo_gpgcheck_repos = _prop("repo_gpgcheck_repos")
upgrade_status = _prop("upgrade_status")
upgrade_command = _prop("upgrade_command")
distro_sync = _prop("distro_sync")
poweroff_after = _prop("poweroff_after")
enable_disable_repos = _prop("enable_disable_repos")
module_platform_id = _prop("module_platform_id")
# --- Plymouth output helpers -------------------------------------------------
class PlymouthOutput(object):
"""A plymouth output helper class.
Filters duplicate calls, and stops calling the plymouth binary if we
fail to contact it.
"""
def __init__(self):
self.alive = True
self._last_args = dict()
self._last_msg = None
def _plymouth(self, cmd, *args):
dupe_cmd = (args == self._last_args.get(cmd))
if (self.alive and not dupe_cmd) or cmd == '--ping':
try:
self.alive = (call((PLYMOUTH, cmd) + args) == 0)
except OSError:
self.alive = False
self._last_args[cmd] = args
return self.alive
def ping(self):
return self._plymouth("--ping")
def message(self, msg):
if self._last_msg and self._last_msg != msg:
self._plymouth("hide-message", "--text", self._last_msg)
self._last_msg = msg
return self._plymouth("display-message", "--text", msg)
def set_mode(self):
mode = 'updates'
try:
s = check_output([PLYMOUTH, '--help'])
if re.search('--system-upgrade', ucd(s)):
mode = 'system-upgrade'
except (CalledProcessError, OSError):
pass
return self._plymouth("change-mode", "--" + mode)
def progress(self, percent):
return self._plymouth("system-update", "--progress", str(percent))
# A single PlymouthOutput instance for us to use within this module
Plymouth = PlymouthOutput()
# A TransactionProgress class that updates plymouth for us.
class PlymouthTransactionProgress(dnf.callback.TransactionProgress):
# pylint: disable=too-many-arguments
def progress(self, package, action, ti_done, ti_total, ts_done, ts_total):
self._update_plymouth(package, action, ts_done, ts_total)
def _update_plymouth(self, package, action, current, total):
# Prevents quick jumps of progressbar when pretrans scriptlets
# and TRANS_PREPARATION are reported as 1/1
if total == 1:
return
# Verification goes through all the packages again,
# which resets the "current" param value, this prevents
# resetting of the progress bar as well. (Rhbug:1809096)
if action != dnf.callback.PKG_VERIFY:
Plymouth.progress(int(90.0 * current / total))
else:
Plymouth.progress(90 + int(10.0 * current / total))
Plymouth.message(self._fmt_event(package, action, current, total))
def _fmt_event(self, package, action, current, total):
action = dnf.transaction.ACTIONS.get(action, action)
return "[%d/%d] %s %s..." % (current, total, action, package)
# --- journal helpers -------------------------------------------------
def find_boots(message_id):
"""Find all boots with this message id.
Returns the entries of all found boots.
"""
j = journal.Reader()
j.add_match(MESSAGE_ID=message_id.hex, # identify the message
_UID=0) # prevent spoofing of logs
oldboot = None
for entry in j:
boot = entry['_BOOT_ID']
if boot == oldboot:
continue
oldboot = boot
yield entry
def list_logs():
print(_('The following boots appear to contain upgrade logs:'))
n = -1
for n, entry in enumerate(find_boots(ID_TO_IDENTIFY_BOOTS)):
print('{} / {.hex}: {:%Y-%m-%d %H:%M:%S} {}→{}'.format(
n + 1,
entry['_BOOT_ID'],
entry['__REALTIME_TIMESTAMP'],
entry.get('SYSTEM_RELEASEVER', '??'),
entry.get('TARGET_RELEASEVER', '??')))
if n == -1:
print(_('-- no logs were found --'))
def pick_boot(message_id, n):
boots = list(find_boots(message_id))
# Positive indices index all found boots starting with 1 and going forward,
# zero is the current boot, and -1, -2, -3 are previous going backwards.
# This is the same as journalctl.
try:
if n == 0:
raise IndexError
if n > 0:
n -= 1
return boots[n]['_BOOT_ID']
except IndexError:
raise CliError(_("Cannot find logs with this index."))
def show_log(n):
boot_id = pick_boot(ID_TO_IDENTIFY_BOOTS, n)
process = Popen(['journalctl', '--boot', boot_id.hex])
process.wait()
rc = process.returncode
if rc == 1:
raise dnf.exceptions.Error(_("Unable to match systemd journal entry"))
CMDS = ['download', 'clean', 'reboot', 'upgrade', 'log']
# --- The actual Plugin and Command objects! ----------------------------------
class SystemUpgradePlugin(dnf.Plugin):
name = 'system-upgrade'
def __init__(self, base, cli):
super(SystemUpgradePlugin, self).__init__(base, cli)
if cli:
cli.register_command(SystemUpgradeCommand)
cli.register_command(OfflineUpgradeCommand)
cli.register_command(OfflineDistrosyncCommand)
class SystemUpgradeCommand(dnf.cli.Command):
aliases = ('system-upgrade', 'fedup',)
summary = _("Prepare system for upgrade to a new release")
DATADIR = 'var/lib/dnf/system-upgrade'
def __init__(self, cli):
super(SystemUpgradeCommand, self).__init__(cli)
self.datadir = os.path.join(cli.base.conf.installroot, self.DATADIR)
self.transaction_file = os.path.join(self.datadir, 'system-upgrade-transaction.json')
self.magic_symlink = os.path.join(cli.base.conf.installroot, 'system-update')
self.state = State(os.path.join(self.datadir, 'system-upgrade-state.json'))
@staticmethod
def set_argparser(parser):
parser.add_argument("--no-downgrade", dest='distro_sync',
action='store_false',
help=_("keep installed packages if the new "
"release's version is older"))
parser.add_argument('--poweroff', dest='poweroff_after',
action='store_true',
help=_("power off system after the operation "
"is completed"))
parser.add_argument('tid', nargs=1, choices=CMDS,
metavar="[%s]" % "|".join(CMDS))
parser.add_argument('--number', type=int, help=_('which logs to show'))
def log_status(self, message, message_id):
"""Log directly to the journal."""
journal.send(message,
MESSAGE_ID=message_id,
PRIORITY=journal.LOG_NOTICE,
SYSTEM_RELEASEVER=self.state.system_releasever,
TARGET_RELEASEVER=self.state.target_releasever,
DNF_VERSION=dnf.const.VERSION)
def pre_configure(self):
self._call_sub("check")
self._call_sub("pre_configure")
def configure(self):
self._call_sub("configure")
def run(self):
self._call_sub("run")
def run_transaction(self):
self._call_sub("transaction")
def run_resolved(self):
self._call_sub("resolved")
def _call_sub(self, name):
subfunc = getattr(self, name + '_' + self.opts.tid[0], None)
if callable(subfunc):
subfunc()
def _check_state_version(self, command):
if self.state.state_version != STATE_VERSION:
msg = _("Incompatible version of data. Rerun 'dnf {command} download [OPTIONS]'"
"").format(command=command)
raise CliError(msg)
def _set_cachedir(self):
# set download directories from json state file
self.base.conf.cachedir = self.datadir
self.base.conf.destdir = self.state.destdir if self.state.destdir else None
def _get_forward_reverse_pkg_reason_pairs(self):
"""
forward = {repoid:{pkg_nevra: {tsi.action: tsi.reason}}
reverse = {pkg_nevra: {tsi.action: tsi.reason}}
:return: forward, reverse
"""
backward_action = set(dnf.transaction.BACKWARD_ACTIONS + [libdnf.transaction.TransactionItemAction_REINSTALLED])
forward_actions = set(dnf.transaction.FORWARD_ACTIONS)
forward = {}
reverse = {}
for tsi in self.cli.base.transaction:
if tsi.action in forward_actions:
pkg = tsi.pkg
forward.setdefault(pkg.repo.id, {}).setdefault(
str(pkg), {})[tsi.action] = tsi.reason
elif tsi.action in backward_action:
reverse.setdefault(str(tsi.pkg), {})[tsi.action] = tsi.reason
return forward, reverse
# == pre_configure_*: set up action-specific demands ==========================
def pre_configure_download(self):
# only download subcommand accepts --destdir command line option
self.base.conf.cachedir = self.datadir
self.base.conf.destdir = self.opts.destdir if self.opts.destdir else None
if 'offline-distrosync' == self.opts.command and not self.opts.distro_sync:
raise CliError(
_("Command 'offline-distrosync' cannot be used with --no-downgrade option"))
elif 'offline-upgrade' == self.opts.command:
self.opts.distro_sync = False
def pre_configure_reboot(self):
self._set_cachedir()
def pre_configure_upgrade(self):
self._set_cachedir()
if self.state.enable_disable_repos:
self.opts.repos_ed = self.state.enable_disable_repos
self.base.conf.releasever = self.state.target_releasever
def pre_configure_clean(self):
self._set_cachedir()
# == configure_*: set up action-specific demands ==========================
def configure_download(self):
if 'system-upgrade' == self.opts.command or 'fedup' == self.opts.command:
help_url = get_url_from_os_release()
if help_url:
msg = _('Additional information for System Upgrade: {}')
logger.info(msg.format(ucd(help_url)))
if self.base._promptWanted():
msg = _('Before you continue ensure that your system is fully upgraded by running '
'"dnf --refresh upgrade". Do you want to continue')
if self.base.conf.assumeno or not self.base.output.userconfirm(
msg='{} [y/N]: '.format(msg), defaultyes_msg='{} [Y/n]: '.format(msg)):
logger.error(_("Operation aborted."))
sys.exit(1)
check_release_ver(self.base.conf, target=self.opts.releasever)
elif 'offline-upgrade' == self.opts.command:
self.cli._populate_update_security_filter(self.opts)
self.cli.demands.root_user = True
self.cli.demands.resolving = True
self.cli.demands.available_repos = True
self.cli.demands.sack_activation = True
self.cli.demands.freshest_metadata = True
# We want to do the depsolve / download / transaction-test, but *not*
# run the actual RPM transaction to install the downloaded packages.
# Setting the "test" flag makes the RPM transaction a test transaction,
# so nothing actually gets installed.
# (It also means that we run two test transactions in a row, which is
# kind of silly, but that's something for DNF to fix...)
self.base.conf.tsflags += ["test"]
def configure_reboot(self):
# FUTURE: add a --debug-shell option to enable debug shell:
# systemctl add-wants system-update.target debug-shell.service
self.cli.demands.root_user = True
def configure_upgrade(self):
# same as the download, but offline and non-interactive. so...
self.cli.demands.root_user = True
self.cli.demands.resolving = True
self.cli.demands.available_repos = True
self.cli.demands.sack_activation = True
# use the saved value for --allowerasing, etc.
self.opts.distro_sync = self.state.distro_sync
if self.state.gpgcheck is not None:
self.base.conf.gpgcheck = self.state.gpgcheck
if self.state.gpgcheck_repos is not None:
for repo in self.base.repos.values():
repo.gpgcheck = repo.id in self.state.gpgcheck_repos
if self.state.repo_gpgcheck_repos is not None:
for repo in self.base.repos.values():
repo.repo_gpgcheck = repo.id in self.state.repo_gpgcheck_repos
self.base.conf.module_platform_id = self.state.module_platform_id
# don't try to get new metadata, 'cuz we're offline
self.cli.demands.cacheonly = True
# and don't ask any questions (we confirmed all this beforehand)
self.base.conf.assumeyes = True
self.cli.demands.transaction_display = PlymouthTransactionProgress()
# upgrade operation already removes all element that must be removed. Additional removal
# could trigger unwanted changes in transaction.
self.base.conf.clean_requirements_on_remove = False
self.base.conf.install_weak_deps = False
def configure_clean(self):
self.cli.demands.root_user = True
def configure_log(self):
pass
# == check_*: do any action-specific checks ===============================
def check_reboot(self):
if not self.state.download_status == 'complete':
raise CliError(_("system is not ready for upgrade"))
self._check_state_version(self.opts.command)
if self.state.upgrade_command != self.opts.command:
msg = _("the transaction was not prepared for '{command}'. "
"Rerun 'dnf {command} download [OPTIONS]'").format(command=self.opts.command)
raise CliError(msg)
if os.path.lexists(self.magic_symlink):
raise CliError(_("upgrade is already scheduled"))
dnf.util.ensure_dir(self.datadir)
# FUTURE: checkRPMDBStatus(self.state.download_transaction_id)
def check_upgrade(self):
if not os.path.lexists(self.magic_symlink):
logger.info(_("trigger file does not exist. exiting quietly."))
raise SystemExit(0)
if os.readlink(self.magic_symlink) != self.datadir:
logger.info(_("another upgrade tool is running. exiting quietly."))
raise SystemExit(0)
# Delete symlink ASAP to avoid reboot loops
dnf.yum.misc.unlink_f(self.magic_symlink)
command = self.state.upgrade_command
if not command:
command = self.opts.command
self._check_state_version(command)
if not self.state.upgrade_status == 'ready':
msg = _("use 'dnf {command} reboot' to begin the upgrade").format(command=command)
raise CliError(msg)
# == run_*: run the action/prep the transaction ===========================
def run_prepare(self):
# make the magic symlink
os.symlink(self.datadir, self.magic_symlink)
# set upgrade_status so that the upgrade can run
with self.state as state:
state.upgrade_status = 'ready'
def run_reboot(self):
self.run_prepare()
if not self.opts.tid[0] == "reboot":
return
self.state.poweroff_after = self.opts.poweroff_after
self.log_status(_("Rebooting to perform upgrade."),
REBOOT_REQUESTED_ID)
# Explicit write since __exit__ doesn't seem to get called when rebooting
self.state.write()
reboot()
def run_download(self):
# Mark everything in the world for upgrade/sync
if self.opts.distro_sync:
self.base.distro_sync()
else:
self.base.upgrade_all()
if self.opts.command not in ['offline-upgrade', 'offline-distrosync']:
# Mark all installed groups and environments for upgrade
self.base.read_comps()
installed_groups = [g.id for g in self.base.comps.groups if self.base.history.group.get(g.id)]
if installed_groups:
self.base.env_group_upgrade(installed_groups)
installed_environments = [g.id for g in self.base.comps.environments if self.base.history.env.get(g.id)]
if installed_environments:
self.base.env_group_upgrade(installed_environments)
with self.state as state:
state.download_status = 'downloading'
state.target_releasever = self.base.conf.releasever
state.destdir = self.base.conf.destdir
def run_upgrade(self):
# change the upgrade status (so we can detect crashed upgrades later)
command = ''
with self.state as state:
state.upgrade_status = 'incomplete'
command = state.upgrade_command
if command == 'offline-upgrade':
msg = _("Starting offline upgrade. This will take a while.")
elif command == 'offline-distrosync':
msg = _("Starting offline distrosync. This will take a while.")
else:
msg = _("Starting system upgrade. This will take a while.")
self.log_status(msg, UPGRADE_STARTED_ID)
# reset the splash mode and let the user know we're running
Plymouth.set_mode()
Plymouth.progress(0)
Plymouth.message(msg)
# disable screen blanking
disable_blanking()
self.replay = TransactionReplay(self.base, self.transaction_file)
self.replay.run()
def run_clean(self):
logger.info(_("Cleaning up downloaded data..."))
# Don't delete persistor, it contains paths for downloaded packages
# that are used by dnf during finalizing base to clean them up
clear_dir(self.base.conf.cachedir,
[dnf.persistor.TempfilePersistor(self.base.conf.cachedir).db_path])
with self.state as state:
state.download_status = None
state.state_version = None
state.upgrade_status = None
state.upgrade_command = None
state.destdir = None
def run_log(self):
if self.opts.number:
show_log(self.opts.number)
else:
list_logs()
# == resolved_*: do staff after succesful resolvement =====================
def resolved_upgrade(self):
"""Adjust transaction reasons according to stored values"""
self.replay.post_transaction()
# == transaction_*: do stuff after a successful transaction ===============
def transaction_download(self):
transaction = self.base.history.get_current()
if not transaction.packages():
logger.info(_("The system-upgrade transaction is empty, your system is already up-to-date."))
return
data = serialize_transaction(transaction)
try:
with open(self.transaction_file, "w") as f:
json.dump(data, f, indent=4, sort_keys=True)
f.write("\n")
print(_("Transaction saved to {}.").format(self.transaction_file))
except OSError as e:
raise dnf.cli.CliError(_('Error storing transaction: {}').format(str(e)))
# Okay! Write out the state so the upgrade can use it.
system_ver = dnf.rpm.detect_releasever(self.base.conf.installroot)
with self.state as state:
state.download_status = 'complete'
state.state_version = STATE_VERSION
state.distro_sync = self.opts.distro_sync
state.gpgcheck = self.base.conf.gpgcheck
state.gpgcheck_repos = [
repo.id for repo in self.base.repos.values() if repo.gpgcheck]
state.repo_gpgcheck_repos = [
repo.id for repo in self.base.repos.values() if repo.repo_gpgcheck]
state.system_releasever = system_ver
state.target_releasever = self.base.conf.releasever
state.module_platform_id = self.base.conf.module_platform_id
state.enable_disable_repos = self.opts.repos_ed
state.destdir = self.base.conf.destdir
state.upgrade_command = self.opts.command
msg = DOWNLOAD_FINISHED_MSG.format(command=self.opts.command)
logger.info(msg)
self.log_status(_("Download finished."), DOWNLOAD_FINISHED_ID)
def transaction_upgrade(self):
if self.state.poweroff_after:
upgrade_complete_msg = _("Upgrade complete! Cleaning up and powering off...")
else:
upgrade_complete_msg = _("Upgrade complete! Cleaning up and rebooting...")
Plymouth.message(upgrade_complete_msg)
self.log_status(upgrade_complete_msg, UPGRADE_FINISHED_ID)
self.run_clean()
if self.opts.tid[0] == "upgrade":
reboot(self.state.poweroff_after)
class OfflineUpgradeCommand(SystemUpgradeCommand):
aliases = ('offline-upgrade',)
summary = _("Prepare offline upgrade of the system")
class OfflineDistrosyncCommand(SystemUpgradeCommand):
aliases = ('offline-distrosync',)
summary = _("Prepare offline distrosync of the system")
|