From 7e6ee04c1ebbda49ab1b616483cb37195ca85691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= Date: Thu, 6 Feb 2025 15:34:04 +0100 Subject: [PATCH] Add support for transient transactions Resolves: RHEL-76849 --- 0012-Add-support-for-transient.patch | 181 +++++++++++++ ...c-Document-transient-and-persistence.patch | 74 ++++++ ...-GObject-API-to-get-deployment-statu.patch | 166 ++++++++++++ ...ng-use-ostree-admin-unlock-transient.patch | 237 ++++++++++++++++++ 0016-spec-Add-dnf-bootc-subpackage.patch | 56 +++++ ...ibdnf-0.74.0-with-persistence-option.patch | 45 ++++ dnf.spec | 31 ++- 7 files changed, 788 insertions(+), 2 deletions(-) create mode 100644 0012-Add-support-for-transient.patch create mode 100644 0013-bootc-Document-transient-and-persistence.patch create mode 100644 0014-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch create mode 100644 0015-bootc-Re-locking-use-ostree-admin-unlock-transient.patch create mode 100644 0016-spec-Add-dnf-bootc-subpackage.patch create mode 100644 0017-Require-libdnf-0.74.0-with-persistence-option.patch diff --git a/0012-Add-support-for-transient.patch b/0012-Add-support-for-transient.patch new file mode 100644 index 0000000..298cee8 --- /dev/null +++ b/0012-Add-support-for-transient.patch @@ -0,0 +1,181 @@ +From 082973b36646945b1c60be8b96ab628d92d99b92 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Thu, 7 Nov 2024 02:31:25 +0000 +Subject: [PATCH 12/17] Add support for --transient +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: 6091f3fccea988208ca417a504569947e4c99263 + +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. + +Resolves: https://issues.redhat.com/browse/RHEL-76849 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 40 ++++++++++++++++++++++++++++++--------- + dnf/cli/option_parser.py | 3 +++ + dnf/conf/config.py | 2 +- + dnf/util.py | 41 +++++++++++++++++++++++++++++----------- + 4 files changed, 65 insertions(+), 21 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index d3844df34..53f2d9d50 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -205,28 +205,50 @@ class BaseCli(dnf.Base): + 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.")) ++ ++ is_bootc_transaction = dnf.util._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. ++ 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(): ++ 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 " ++ "will reset when the system reboots.")) ++ raise CliError(_("Operation aborted.")) ++ assert self.conf.persistence == "transient" ++ 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.")) + + 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 042d5fbbe..ec4696fd2 100644 +--- a/dnf/cli/option_parser.py ++++ b/dnf/cli/option_parser.py +@@ -317,6 +317,9 @@ class OptionParser(argparse.ArgumentParser): + general_grp.add_argument("--downloadonly", dest="downloadonly", + action="store_true", default=False, + help=_("only download packages")) ++ general_grp.add_argument("--transient", dest="persistence", ++ action="store_const", const="transient", default=None, ++ help=_("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 f9c8d932a..5210ffba2 100644 +--- a/dnf/conf/config.py ++++ b/dnf/conf/config.py +@@ -343,7 +343,7 @@ class MainConf(BaseConfig): + 'best', 'assumeyes', 'assumeno', 'clean_requirements_on_remove', 'gpgcheck', + 'showdupesfromrepos', 'plugins', 'ip_resolve', + 'rpmverbosity', 'disable_excludes', 'color', +- 'downloadonly', 'exclude', 'excludepkgs', 'skip_broken', ++ 'downloadonly', 'persistence', '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 1ba2e27ff..2e270890c 100644 +--- a/dnf/util.py ++++ b/dnf/util.py +@@ -38,6 +38,7 @@ import logging + import os + import pwd + import shutil ++import subprocess + import sys + import tempfile + import time +@@ -642,14 +643,32 @@ 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.""" ++ ostree_booted = "/run/ostree-booted" ++ return os.path.isfile(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.""" ++ usr = "/usr" ++ return os.access(usr, os.W_OK) ++ ++ ++def _bootc_unlock(): ++ """Set up a writeable overlay on bootc systems.""" ++ ++ if _is_bootc_unlocked(): ++ return ++ ++ unlock_command = ["bootc", "usr-overlay"] ++ ++ 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)) +-- +2.48.1 + diff --git a/0013-bootc-Document-transient-and-persistence.patch b/0013-bootc-Document-transient-and-persistence.patch new file mode 100644 index 0000000..1797892 --- /dev/null +++ b/0013-bootc-Document-transient-and-persistence.patch @@ -0,0 +1,74 @@ +From 83177634e2a50887d9910048f87277b838eaa2f2 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 17 Dec 2024 18:58:32 +0000 +Subject: [PATCH 13/17] bootc: Document `--transient` and `persistence` +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: 80a62d89ba3c00f4e0bd3fea995d04ccf9ba8098 + +Documents the new `--transient` command-line argument and `persistence` +configuration option. I tried to use a table for listing the valid +options for `persistence`, but RST does not automatically wrap table +cells containing long lines, so a list was much easier. + +Resolves: https://issues.redhat.com/browse/RHEL-76849 +Signed-off-by: Petr Písař +--- + doc/command_ref.rst | 9 +++++++++ + doc/conf_ref.rst | 11 +++++++++++ + 2 files changed, 20 insertions(+) + +diff --git a/doc/command_ref.rst b/doc/command_ref.rst +index 5684b0611..2337a2e29 100644 +--- a/doc/command_ref.rst ++++ b/doc/command_ref.rst +@@ -388,6 +388,11 @@ Options + ``--showduplicates`` + Show duplicate packages in repositories. Applicable for the list and search commands. + ++.. _transient_option-label: ++ ++``--transient`` ++ Applicable only on bootc (bootable containers) systems. Perform transactions using a transient overlay which will be lost on the next reboot. See also the :ref:`persistence ` configuration option. ++ + .. _verbose_options-label: + + ``-v, --verbose`` +@@ -707,6 +712,10 @@ transactions and act according to this information (assuming the + which specifies a transaction by a package which it manipulated. When no + transaction is specified, list all known transactions. + ++ Note that transient transactions (see :ref:`--transient ++ `) will be listed even though they do not make ++ persistent changes to files under ``/usr`` or to the RPM database. ++ + The "Action(s)" column lists each type of action taken in the transaction. The possible values are: + + * Install (I): a new package was installed on the system +diff --git a/doc/conf_ref.rst b/doc/conf_ref.rst +index 240b35f96..d4e4a2770 100644 +--- a/doc/conf_ref.rst ++++ b/doc/conf_ref.rst +@@ -442,6 +442,17 @@ configuration file by your distribution to override the DNF defaults. + + Directory where DNF stores its persistent data between runs. Default is ``"/var/lib/dnf"``. + ++.. _persistence-label: ++ ++``persistence`` ++ :ref:`string ` ++ ++ Whether changes should persist across system reboots. Default is ``auto``. Passing :ref:`--transient ` will override this setting to ``transient``. Valid values are: ++ ++ * ``auto``: Changes will persist across reboots, unless the target is a running bootc system and the system is already in an unlocked state (i.e. ``/usr`` is writable). ++ * ``transient``: Changes will be lost on the next reboot. Only applicable on bootc systems. Beware that changes to ``/etc`` and ``/var`` will persist, depending on the configuration of your bootc system. See also https://containers.github.io/bootc/man/bootc-usr-overlay.html. ++ * ``persist``: Changes will persist across reboots. ++ + .. _pluginconfpath-label: + + ``pluginconfpath`` +-- +2.48.1 + diff --git a/0014-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch b/0014-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch new file mode 100644 index 0000000..52ab372 --- /dev/null +++ b/0014-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch @@ -0,0 +1,166 @@ +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 + diff --git a/0015-bootc-Re-locking-use-ostree-admin-unlock-transient.patch b/0015-bootc-Re-locking-use-ostree-admin-unlock-transient.patch new file mode 100644 index 0000000..a3975df --- /dev/null +++ b/0015-bootc-Re-locking-use-ostree-admin-unlock-transient.patch @@ -0,0 +1,237 @@ +From 47cd04de2c099171002f2f474084e08eb5c7dddd Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Thu, 16 Jan 2025 14:06:26 -0500 +Subject: [PATCH 15/17] bootc: "Re-locking": use ostree admin unlock + --transient +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: fa47a256ae7add2ce1c99ae8bedce7216001f396 + +To keep /usr read-only after DNF is finished with a transient +transaction, we call `ostree admin unlock --transient` to mount the /usr +overlay as read-only by default. Then, we create a private mount +namespace for DNF and its child processes and remount the /usr overlayfs +as read/write in the private mountns. + +os.unshare is unfortunately only available in Python >= 3.12, so we have +to call libc.unshare via Python ctypes here and hardcode the CLONE_NEWNS +flag that we need to pass. + +Resolves: https://issues.redhat.com/browse/RHEL-76849 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 33 ++++++++++--------- + dnf/util.py | 86 ++++++++++++++++++++++++++++++++++++++------------ + 2 files changed, 83 insertions(+), 36 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index bc11505fc..e2700c092 100644 +--- a/dnf/cli/cli.py ++++ b/dnf/cli/cli.py +@@ -205,8 +205,6 @@ class BaseCli(dnf.Base): + 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 +@@ -218,40 +216,45 @@ 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._Bootc.is_bootc_host() and \ ++ is_bootc_transaction = dnf.util._BootcSystem.is_bootc_system() 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 ++ bootc_system = 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") + +- bootc = dnf.util._Bootc() ++ bootc_system = dnf.util._BootcSystem() + +- if not bootc.is_unlocked(): ++ if not bootc_system.is_writable(): + 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 " +- "will reset when the system reboots.")) ++ "perform this transaction in a transient overlay which will reset when " ++ "the system reboots.")) + raise CliError(_("Operation aborted.")) + assert self.conf.persistence == "transient" +- 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.")) +- elif self.conf.persistence == "transient": +- raise CliError(_("Transient transactions are only supported on bootc systems.")) ++ if not bootc_system.is_unlocked_transient(): ++ # Only tell the user about the transient overlay if ++ # it's not already in place ++ 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.")) ++ else: ++ # Not a bootc transaction. ++ if self.conf.persistence == "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: +- bootc.unlock_and_prepare() ++ if bootc_system: ++ bootc_system.make_writable() + else: + logger.info(_('Nothing to do.')) + return +diff --git a/dnf/util.py b/dnf/util.py +index 51f853d8b..eb987bb8a 100644 +--- a/dnf/util.py ++++ b/dnf/util.py +@@ -25,6 +25,7 @@ from __future__ import unicode_literals + from .pycomp import PY3, basestring + from dnf.i18n import _, ucd + import argparse ++import ctypes + import dnf + import dnf.callback + import dnf.const +@@ -642,11 +643,12 @@ def _is_file_pattern_present(specs): + return False + + +-class _Bootc: ++class _BootcSystem: + usr = "/usr" ++ CLONE_NEWNS = 0x00020000 # defined in linux/include/uapi/linux/sched.h + + def __init__(self): +- if not self.is_bootc_host(): ++ if not self.is_bootc_system(): + raise RuntimeError(_("Not running on a bootc system.")) + + import gi +@@ -664,45 +666,87 @@ class _Bootc: + assert self._booted_deployment is not None + + @staticmethod +- def is_bootc_host(): ++ def is_bootc_system(): + """Returns true is the system is managed as an immutable container, false + otherwise.""" + ostree_booted = "/run/ostree-booted" + return os.path.isfile(ostree_booted) + ++ @classmethod ++ def is_writable(cls): ++ """Returns true if and only if /usr is writable.""" ++ return os.access(cls.usr, os.W_OK) ++ + 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 is_unlocked_transient(self): ++ """Returns true if and only if the bootc system is unlocked in a ++ transient state, i.e. a overlayfs is mounted as read-only on /usr. ++ Changes can be made to the overlayfs by remounting /usr as ++ read/write in a private mount namespace.""" ++ return self._get_unlocked_state() == self._OSTree.DeploymentUnlockedState.TRANSIENT ++ ++ @classmethod ++ def _set_up_mountns(cls): ++ # os.unshare is only available in Python >= 3.12. ++ ++ # Access symbols in libraries loaded by the Python interpreter, ++ # which will include libc. See https://bugs.python.org/issue34592. ++ libc = ctypes.CDLL(None) ++ if libc.unshare(cls.CLONE_NEWNS) != 0: ++ raise OSError("Failed to unshare mount namespace") ++ ++ mount_command = ["mount", "--options-source=disable", "-o", "remount,rw", cls.usr] ++ try: ++ completed_process = subprocess.run(mount_command, text=True) ++ completed_process.check_returncode() ++ except FileNotFoundError: ++ raise dnf.exceptions.Error(_("%s: command not found.") % mount_command[0]) ++ except subprocess.CalledProcessError: ++ raise dnf.exceptions.Error(_("Failed to mount %s as read/write: %s", cls.usr, completed_process.stderr)) + +- def unlock_and_prepare(self): +- """Set up a writeable overlay on bootc systems.""" ++ @staticmethod ++ def _unlock(): ++ unlock_command = ["ostree", "admin", "unlock", "--transient"] ++ try: ++ completed_process = subprocess.run(unlock_command, text=True) ++ completed_process.check_returncode() ++ except FileNotFoundError: ++ raise dnf.exceptions.Error(_("%s: command not found. Is this a bootc system?") % unlock_command[0]) ++ except subprocess.CalledProcessError: ++ raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) ++ ++ def make_writable(self): ++ """Set up a writable 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, ++ 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. ++ writable_unlocked_states = ( ++ self._OSTree.DeploymentUnlockedState.DEVELOPMENT, ++ self._OSTree.DeploymentUnlockedState.HOTFIX, ++ ) ++ if bootc_unlocked_state in writable_unlocked_states: ++ # System is already unlocked in development mode, and usr is ++ # already mounted read/write. + 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)) ++ # System is not unlocked. Unlock it in transient mode, then set up ++ # a mount namespace for DNF. ++ self._unlock() ++ self._set_up_mountns() ++ elif bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.TRANSIENT: ++ # System is unlocked in transient mode, so usr is mounted ++ # read-only. Set up a mount namespace for DNF. ++ self._set_up_mountns() + + assert os.access(self.usr, os.W_OK) +-- +2.48.1 + diff --git a/0016-spec-Add-dnf-bootc-subpackage.patch b/0016-spec-Add-dnf-bootc-subpackage.patch new file mode 100644 index 0000000..39795c2 --- /dev/null +++ b/0016-spec-Add-dnf-bootc-subpackage.patch @@ -0,0 +1,56 @@ +From dc461487b3a1323d96183e5c229a307d274a3ecc Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 28 Jan 2025 11:27:00 -0500 +Subject: [PATCH 16/17] spec: Add dnf-bootc subpackage +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: 76a0c339eb172b1b2a4b1aa8b4db8d6a5145916b + +dnf-bootc's only job is to Require python3-gobject-base, ostree, +ostree-libs, and util-linux-core, which are needed to interact with +bootc systems. We don't want to add these dependencies on `python3-dnf` +because we don't want them on non-bootc systems, so we use a subpackage. + +Resolves: https://issues.redhat.com/browse/RHEL-76849 +Signed-off-by: Petr Písař +--- + dnf.spec | 14 ++++++++++++++ + 1 file changed, 14 insertions(+) + +diff --git a/dnf.spec b/dnf.spec +index 98b6be42c..c749ab41e 100644 +--- a/dnf.spec ++++ b/dnf.spec +@@ -191,6 +191,17 @@ Requires: python3-%{name} = %{version}-%{release} + %description automatic + Systemd units that can periodically download package upgrades and apply them. + ++%package bootc ++Summary: %{pkg_summary} - additional bootc dependencies ++Requires: python3-%{name} = %{version}-%{release} ++Requires: ostree ++Requires: ostree-libs ++Requires: python3-gobject-base ++Requires: util-linux-core ++ ++%description bootc ++Additional dependencies needed to perform transactions on booted bootc (bootable containers) systems. ++ + + %prep + %autosetup +@@ -413,6 +424,9 @@ popd + %{_unitdir}/%{name}-automatic-install.timer + %{python3_sitelib}/%{name}/automatic/ + ++%files bootc ++# bootc subpackage does not include any files ++ + %changelog + * Wed Apr 24 2024 Jan Kolarik - 4.20.0-1 + - repoquery: Fix loading filelists when -f is used (RhBug:2276012) +-- +2.48.1 + diff --git a/0017-Require-libdnf-0.74.0-with-persistence-option.patch b/0017-Require-libdnf-0.74.0-with-persistence-option.patch new file mode 100644 index 0000000..bf59dca --- /dev/null +++ b/0017-Require-libdnf-0.74.0-with-persistence-option.patch @@ -0,0 +1,45 @@ +From 7ffd97532e120f4391792b1bdfa0dbe1510409a7 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Wed, 5 Feb 2025 10:35:08 -0500 +Subject: [PATCH 17/17] Require libdnf >= 0.74.0 with `persistence` option +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Upstream commit: 5a4f6c42e61ed764ff85eea69125947a280d665d + +This backport actually uses RHEL 10 version of libdnf. + +Resolves: https://issues.redhat.com/browse/RHEL-76849 +Signed-off-by: Petr Písař +--- + dnf.spec | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/dnf.spec b/dnf.spec +index c749ab41e..66243c858 100644 +--- a/dnf.spec ++++ b/dnf.spec +@@ -2,7 +2,7 @@ + %define __cmake_in_source_build 1 + + # default dependencies +-%global hawkey_version 0.73.1 ++%global hawkey_version 0.74.0 + %global libcomps_version 0.1.8 + %global libmodulemd_version 2.9.3 + %global rpm_version 4.14.0 +@@ -23,6 +23,10 @@ + %global rpm_version 4.11.3-25.el7.centos.1 + %endif + ++%if 0%{?rhel} == 10 ++ %global hawkey_version 0.73.1-7 ++%endif ++ + # override dependencies for fedora 26 + %if 0%{?fedora} == 26 + %global rpm_version 4.13.0.1-7 +-- +2.48.1 + diff --git a/dnf.spec b/dnf.spec index bf77e00..1db92f3 100644 --- a/dnf.spec +++ b/dnf.spec @@ -2,7 +2,7 @@ %define __cmake_in_source_build 1 # default dependencies -%global hawkey_version 0.73.1 +%global hawkey_version 0.74.0 %global libcomps_version 0.1.8 %global libmodulemd_version 2.9.3 %global rpm_version 4.14.0 @@ -23,6 +23,10 @@ %global rpm_version 4.11.3-25.el7.centos.1 %endif +%if 0%{?rhel} == 10 + %global hawkey_version 0.73.1-7 +%endif + # override dependencies for fedora 26 %if 0%{?fedora} == 26 %global rpm_version 4.13.0.1-7 @@ -68,7 +72,7 @@ It supports RPMs, modules and comps groups & environments. Name: dnf Version: 4.20.0 -Release: 10%{?dist} +Release: 11%{?dist} Summary: %{pkg_summary} # For a breakdown of the licensing, see PACKAGE-LICENSING License: GPL-2.0-or-later AND GPL-1.0-only @@ -85,6 +89,12 @@ Patch8: 0008-Update-bootc-hosts-message-to-point-to-bootc-help.patch Patch9: 0009-Allow-installroot-on-read-only-bootc-system.patch Patch10: 0010-Allow-downloadonly-on-read-only-bootc-system.patch Patch11: 0011-Automatic-check-availability-of-config-file.patch +Patch12: 0012-Add-support-for-transient.patch +Patch13: 0013-bootc-Document-transient-and-persistence.patch +Patch14: 0014-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch +Patch15: 0015-bootc-Re-locking-use-ostree-admin-unlock-transient.patch +Patch16: 0016-spec-Add-dnf-bootc-subpackage.patch +Patch17: 0017-Require-libdnf-0.74.0-with-persistence-option.patch BuildArch: noarch BuildRequires: cmake @@ -203,6 +213,17 @@ Requires: python3-%{name} = %{version}-%{release} %description automatic Systemd units that can periodically download package upgrades and apply them. +%package bootc +Summary: %{pkg_summary} - additional bootc dependencies +Requires: python3-%{name} = %{version}-%{release} +Requires: ostree +Requires: ostree-libs +Requires: python3-gobject-base +Requires: util-linux-core + +%description bootc +Additional dependencies needed to perform transactions on booted bootc (bootable containers) systems. + %prep %autosetup -p1 @@ -425,7 +446,13 @@ popd %{_unitdir}/%{name}-automatic-install.timer %{python3_sitelib}/%{name}/automatic/ +%files bootc +# bootc subpackage does not include any files + %changelog +* Thu Feb 06 2025 Petr Pisar - 4.20.0-11 +- Add support for transient transactions (RHEL-76849) + * Fri Dec 13 2024 Marek Blaha - 4.20.0-10 - automatic: Check availability of config file