From c0f2329c2ed71f373aad26c7f1786494f6e75b76 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Wed, 15 Jan 2025 21:43:58 +0000 Subject: [PATCH 14/17] bootc: Use ostree GObject API to get deployment status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream commit: f3abee56452e40ce475f714666c3a16426759c96 Using libostree gives us more detail about the current state of the deployment than only checking whether /usr is writable. Resolves: https://issues.redhat.com/browse/RHEL-76849 Signed-off-by: Petr Písař --- dnf/cli/cli.py | 13 ++++---- dnf/util.py | 80 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 65 insertions(+), 28 deletions(-) diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py index 53f2d9d50..bc11505fc 100644 --- a/dnf/cli/cli.py +++ b/dnf/cli/cli.py @@ -218,18 +218,22 @@ class BaseCli(dnf.Base): logger.info(_("{prog} will only download packages, install gpg keys, and check the " "transaction.").format(prog=dnf.util.MAIN_PROG_UPPER)) - is_bootc_transaction = dnf.util._is_bootc_host() and \ + is_bootc_transaction = dnf.util._Bootc.is_bootc_host() and \ os.path.realpath(self.conf.installroot) == "/" and \ not self.conf.downloadonly # Handle bootc transactions. `--transient` must be specified if # /usr is not already writeable. + bootc = None if is_bootc_transaction: if self.conf.persistence == "persist": logger.info(_("Persistent transactions aren't supported on bootc systems.")) raise CliError(_("Operation aborted.")) assert self.conf.persistence in ("auto", "transient") - if not dnf.util._is_bootc_unlocked(): + + bootc = dnf.util._Bootc() + + if not bootc.is_unlocked(): if self.conf.persistence == "auto": logger.info(_("This bootc system is configured to be read-only. Pass --transient to " "perform this and subsequent transactions in a transient overlay which " @@ -239,7 +243,6 @@ class BaseCli(dnf.Base): logger.info(_("A transient overlay 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.persistence == "transient": raise CliError(_("Transient transactions are only supported on bootc systems.")) @@ -247,8 +250,8 @@ class BaseCli(dnf.Base): if self.conf.assumeno or not self.output.userconfirm(): raise CliError(_("Operation aborted.")) - if bootc_unlock_requested: - dnf.util._bootc_unlock() + if bootc: + bootc.unlock_and_prepare() else: logger.info(_('Nothing to do.')) return diff --git a/dnf/util.py b/dnf/util.py index 2e270890c..51f853d8b 100644 --- a/dnf/util.py +++ b/dnf/util.py @@ -642,33 +642,67 @@ def _is_file_pattern_present(specs): return False -def _is_bootc_host(): - """Returns true is the system is managed as an immutable container, false - otherwise.""" - ostree_booted = "/run/ostree-booted" - return os.path.isfile(ostree_booted) +class _Bootc: + usr = "/usr" + def __init__(self): + if not self.is_bootc_host(): + raise RuntimeError(_("Not running on a bootc system.")) -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.""" - usr = "/usr" - return os.access(usr, os.W_OK) + import gi + self._gi = gi + gi.require_version("OSTree", "1.0") + from gi.repository import OSTree -def _bootc_unlock(): - """Set up a writeable overlay on bootc systems.""" + self._OSTree = OSTree - if _is_bootc_unlocked(): - return + self._sysroot = self._OSTree.Sysroot.new_default() + assert self._sysroot.load(None) - unlock_command = ["bootc", "usr-overlay"] + self._booted_deployment = self._sysroot.require_booted_deployment() + assert self._booted_deployment is not None - try: - completed_process = subprocess.run(unlock_command, 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)) + @staticmethod + def is_bootc_host(): + """Returns true is the system is managed as an immutable container, false + otherwise.""" + ostree_booted = "/run/ostree-booted" + return os.path.isfile(ostree_booted) + + def _get_unlocked_state(self): + return self._booted_deployment.get_unlocked() + + def is_unlocked(self): + return self._get_unlocked_state() != self._OSTree.DeploymentUnlockedState.NONE + + def unlock_and_prepare(self): + """Set up a writeable overlay on bootc systems.""" + + bootc_unlocked_state = self._get_unlocked_state() + + valid_bootc_unlocked_states = ( + self._OSTree.DeploymentUnlockedState.NONE, + self._OSTree.DeploymentUnlockedState.DEVELOPMENT, + # self._OSTree.DeploymentUnlockedState.TRANSIENT, + # self._OSTree.DeploymentUnlockedState.HOTFIX, + ) + + if bootc_unlocked_state not in valid_bootc_unlocked_states: + raise ValueError(_("Unhandled bootc unlocked state: %s") % bootc_unlocked_state.value_nick) + + if bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.DEVELOPMENT: + # System is already unlocked. + pass + elif bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.NONE: + unlock_command = ["ostree", "admin", "unlock"] + + try: + completed_process = subprocess.run(unlock_command, text=True) + completed_process.check_returncode() + except FileNotFoundError: + raise dnf.exceptions.Error(_("ostree command not found. Is this a bootc system?")) + except subprocess.CalledProcessError: + raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) + + assert os.access(self.usr, os.W_OK) -- 2.48.1