From a636a2cf15b70e3bde5fa2ec8102fc5e14803348 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Thu, 7 Nov 2024 02:31:25 +0000 Subject: [PATCH] Add support for --transient Adds support for the --transient option on all transactions. Passing --transient on a bootc system will call `bootc usr-overlay` to create a transient writeable /usr and continue the transaction. Specifying --transient on a non-bootc system will throw an error; we don't want to mislead users to thinking this feature works on non-bootc systems. If --transient is not specified and the bootc system is in a locked state, the operation will be aborted and a message will be printed suggesting to try again with --transient. --- dnf/cli/cli.py | 25 +++++++++++++++++++------ dnf/cli/option_parser.py | 4 ++++ dnf/conf/config.py | 2 +- dnf/util.py | 34 +++++++++++++++++++++++----------- 4 files changed, 47 insertions(+), 18 deletions(-) diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py index d3844df344..3ebe56f9bb 100644 --- a/dnf/cli/cli.py +++ b/dnf/cli/cli.py @@ -205,28 +205,41 @@ def do_transaction(self, display=()): else: self.output.reportDownloadSize(install_pkgs, install_only) + bootc_unlock_requested = False + if trans or self._moduleContainer.isChanged() or \ (self._history and (self._history.group or self._history.env)): # confirm with user if self.conf.downloadonly: logger.info(_("{prog} will only download packages for the transaction.").format( prog=dnf.util.MAIN_PROG_UPPER)) + elif 'test' in self.conf.tsflags: logger.info(_("{prog} will only download packages, install gpg keys, and check the " "transaction.").format(prog=dnf.util.MAIN_PROG_UPPER)) + if dnf.util._is_bootc_host() and \ os.path.realpath(self.conf.installroot) == "/" and \ not self.conf.downloadonly: - _bootc_host_msg = _(""" -*** Error: system is configured to be read-only; for more -*** information run `bootc --help`. -""") - logger.info(_bootc_host_msg) - raise CliError(_("Operation aborted.")) + if not dnf.util._is_bootc_unlocked(): + if not self.conf.transient: + bootc_host_msg = _(""" + *** This bootc system is configured to be read-only. Pass --transient to perform this and subsequent transactions in a transient overlay which will reset when the system reboots. + """) + + logger.info(bootc_host_msg) + raise CliError(_("Operation aborted.")) + logger.info(_("A transient writeable overlayfs will be created on /usr that will be discarded on reboot. Keep in mind that changes to /etc and /var will still persist, and packages commonly modify these directories.")) + bootc_unlock_requested = True + elif self.conf.transient: + raise CliError(_("Transient transactions are only supported on bootc systems.")) if self._promptWanted(): if self.conf.assumeno or not self.output.userconfirm(): raise CliError(_("Operation aborted.")) + + if bootc_unlock_requested: + dnf.util._bootc_unlock() else: logger.info(_('Nothing to do.')) return diff --git a/dnf/cli/option_parser.py b/dnf/cli/option_parser.py index 042d5fbbee..692a61f164 100644 --- a/dnf/cli/option_parser.py +++ b/dnf/cli/option_parser.py @@ -317,6 +317,10 @@ def _add_general_options(self): general_grp.add_argument("--downloadonly", dest="downloadonly", action="store_true", default=False, help=_("only download packages")) + general_grp.add_argument("--transient", dest="transient", + action="store_true", default=False, + help=_("On a bootc system, use a transient" + "overlay which will reset on reboot")) general_grp.add_argument("--comment", dest="comment", default=None, help=_("add a comment to transaction")) # Updateinfo options... diff --git a/dnf/conf/config.py b/dnf/conf/config.py index f9c8d932a5..4e2d0f2009 100644 --- a/dnf/conf/config.py +++ b/dnf/conf/config.py @@ -343,7 +343,7 @@ def _configure_from_options(self, opts): 'best', 'assumeyes', 'assumeno', 'clean_requirements_on_remove', 'gpgcheck', 'showdupesfromrepos', 'plugins', 'ip_resolve', 'rpmverbosity', 'disable_excludes', 'color', - 'downloadonly', 'exclude', 'excludepkgs', 'skip_broken', + 'downloadonly', 'transient', 'exclude', 'excludepkgs', 'skip_broken', 'tsflags', 'arch', 'basearch', 'ignorearch', 'cacheonly', 'comment'] for name in config_args: diff --git a/dnf/util.py b/dnf/util.py index 0327321caf..889862e922 100644 --- a/dnf/util.py +++ b/dnf/util.py @@ -38,6 +38,7 @@ import os import pwd import shutil +import subprocess import sys import tempfile import time @@ -642,15 +643,26 @@ def _is_file_pattern_present(specs): def _is_bootc_host(): - """Returns true is the system is managed as an immutable container, - false otherwise. If msg is True, a warning message is displayed - for the user. - """ - ostree_booted = '/run/ostree-booted' - usr = '/usr/' - # Check if usr is writtable and we are in a running ostree system. - # We want this code to return true only when the system is in locked state. If someone ran - # bootc overlay or ostree admin unlock we would want normal DNF path to be ran as it will be - # temporary changes (until reboot). - return os.path.isfile(ostree_booted) and not os.access(usr, os.W_OK) + """Returns true is the system is managed as an immutable container, false + otherwise.""" + return os.path.isfile("/run/ostree-booted") + +def _is_bootc_unlocked(): + """Check whether /usr is writeable, e.g. if we are in a normal mutable + system or if we are in a bootc after `bootc usr-overlay` or `ostree admin + unlock` was run.""" + return os.access("/usr", os.W_OK) + +def _bootc_unlock(): + """Set up a writeable overlay on bootc systems.""" + if _is_bootc_unlocked(): + return + + try: + completed_process = subprocess.run(["bootc", "usr-overlay"], text=True) + completed_process.check_returncode() + except FileNotFoundError: + raise dnf.exceptions.Error(_("bootc command not found. Is this a bootc system?")) + except subprocess.CalledProcessError: + raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr))