Skip to content

Commit

Permalink
Add support for --transient
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
evan-goode committed Nov 7, 2024
1 parent e54fcd4 commit a636a2c
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 18 deletions.
25 changes: 19 additions & 6 deletions dnf/cli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions dnf/cli/option_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand Down
2 changes: 1 addition & 1 deletion dnf/conf/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
34 changes: 23 additions & 11 deletions dnf/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import os
import pwd
import shutil
import subprocess
import sys
import tempfile
import time
Expand Down Expand Up @@ -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))

0 comments on commit a636a2c

Please sign in to comment.