From 6b985e7c61bad23af3050b8f593c9b57b2721de2 Mon Sep 17 00:00:00 2001 From: David Cantrell Date: Fri, 18 Oct 2024 13:32:58 -0400 Subject: [PATCH 1/7] copr: Add Copr build files Support automatic builds on git commits. Used to generate up to date RPMs of dnf for testing and development purposes. --- .copr/Makefile | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 .copr/Makefile diff --git a/.copr/Makefile b/.copr/Makefile new file mode 100644 index 0000000000..d4eda23355 --- /dev/null +++ b/.copr/Makefile @@ -0,0 +1,64 @@ +# Copyright David Cantrell +# SPDX-License-Identifier: GPL-3.0-or-later + +# Set the top level source directory +topdir := $(shell realpath $(dir $(lastword $(MAKEFILE_LIST)))/..) + +# Install packages before anything else +_install := $(shell dnf install -y git) +_safedir := $(shell git config --global --add safe.directory $(topdir)) + +# Pass BUILDTYPE=release to generate a release SRPM +BUILDTYPE ?= copr + +# Spec file and template +SPEC_TEMPLATE = $(shell ls -1 $(topdir)/*.spec) +SPEC = $(topdir)/$(shell basename $(SPEC_TEMPLATE)) + +# Replace placeholders in the spec file template +RPMDATE = $(shell date +'%a %b %d %Y') +#RPMAUTHOR = $(shell git log | grep ^Author: | head -n 1 | cut -d ' ' -f 2,3,4) +RPMAUTHOR = David Cantrell + +# Various things we need to generate a tarball +PKG = $(shell rpmspec -P "$(SPEC_TEMPLATE)" | grep ^Name: | awk '{ print $$2; }') +VER = $(shell rpmspec -P "$(SPEC_TEMPLATE)" | grep ^Version: | awk '{ print $$2; }') + +ifeq ($(BUILDTYPE),copr) +GITDATE = $(shell date +'%Y%m%d%H%M') +GITHASH = $(shell git rev-parse --short HEAD) +TARBALL_BASENAME = $(PKG)-$(VER)-$(GITDATE)git$(GITHASH) +TAG = HEAD +else +TAG = $(shell git describe --tags --abbrev=0) +endif + +ifeq ($(BUILDTYPE),release) +TARBALL_BASENAME = $(PKG)-$(VER) +endif + +# Where to insert the changelog entry +STARTING_POINT = $(shell expr $(shell grep -n ^%changelog "$(SPEC)" | cut -d ':' -f 1) + 1) + +srpm: + sed -i -e '1i %global source_date_epoch_from_changelog 0' "$(SPEC)" + sed -e 's|%%VERSION%%|$(VER)|g' < "$(SPEC_TEMPLATE)" > "$(SPEC)".new + mv "$(SPEC)".new "$(SPEC)" +ifeq ($(BUILDTYPE),copr) + sed -i -e '/^Release:/ s/1[^%]*/0.1.$(GITDATE)git$(GITHASH)/' "$(SPEC)" + sed -i -e 's|^Source0:.*$$|Source0: $(TARBALL_BASENAME).tar.gz|g' "$(SPEC)" + sed -i -e 's|^%autosetup.*$$|%autosetup -n $(TARBALL_BASENAME)|g' "$(SPEC)" + sed -i -e '$(STARTING_POINT)a\\' "$(SPEC)" + sed -i -e '$(STARTING_POINT)a - Build $(PKG)-$(VER)-$(GITDATE)git$(GITHASH) snapshot' "$(SPEC)" + sed -i -e '$(STARTING_POINT)a * $(RPMDATE) $(RPMAUTHOR) - $(VER)-$(GITDATE)git$(GITHASH)' "$(SPEC)" +endif + git archive \ + --format=tar \ + --output='$(topdir)/$(TARBALL_BASENAME).tar' \ + --prefix='$(TARBALL_BASENAME)/' $(TAG) $(topdir) + gzip -9f $(topdir)/$(TARBALL_BASENAME).tar + rpmbuild \ + -bs --nodeps \ + --define "_sourcedir $(topdir)" \ + --define "_srcrpmdir $(outdir)" \ + --define "_rpmdir $(outdir)" "$(SPEC)" From 58fafe3b1e55f49fdb9f3e4c6d8948ffb197a755 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Thu, 7 Nov 2024 02:31:25 +0000 Subject: [PATCH 2/7] 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 | 40 +++++++++++++++++++++++++++++++--------- dnf/cli/option_parser.py | 3 +++ dnf/conf/config.py | 2 +- dnf/util.py | 40 +++++++++++++++++++++++++++++----------- 4 files changed, 64 insertions(+), 21 deletions(-) diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py index 2d20cf425c..609f05c3f5 100644 --- a/dnf/cli/cli.py +++ b/dnf/cli/cli.py @@ -205,28 +205,50 @@ 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.")) + + 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 042d5fbbee..ec4696fd2c 100644 --- a/dnf/cli/option_parser.py +++ b/dnf/cli/option_parser.py @@ -317,6 +317,9 @@ 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="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 f9c8d932a5..5210ffba2a 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', '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 0327321caf..2e270890ca 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,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)) From 0da8dc14a54ba45069b4b174a874bd5070207776 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Tue, 17 Dec 2024 18:58:32 +0000 Subject: [PATCH 3/7] bootc: Document `--transient` and `persistence` 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. --- 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 ebbb6d6a76..e75a08a4d2 100644 --- a/doc/command_ref.rst +++ b/doc/command_ref.rst @@ -389,6 +389,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 b7bd4b71f4..26bf6f645a 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`` From 5776b1a5c33e7192e65a2a8148c1594250a9fcf3 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Wed, 15 Jan 2025 21:43:58 +0000 Subject: [PATCH 4/7] bootc: Use ostree GObject API to get deployment status Using libostree gives us more detail about the current state of the deployment than only checking whether /usr is writable. --- 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 609f05c3f5..fd62982af0 100644 --- a/dnf/cli/cli.py +++ b/dnf/cli/cli.py @@ -218,18 +218,22 @@ def do_transaction(self, display=()): 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 @@ def do_transaction(self, display=()): 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 @@ def do_transaction(self, display=()): 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 2e270890ca..51f853d8b1 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) From 825cdb8ad54a0ca0e5a19132114cfe0e82207c49 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Thu, 16 Jan 2025 14:06:26 -0500 Subject: [PATCH 5/7] bootc: "Re-locking": use ostree admin unlock --transient 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. --- 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 fd62982af0..c63c2538f8 100644 --- a/dnf/cli/cli.py +++ b/dnf/cli/cli.py @@ -205,8 +205,6 @@ 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 @@ -218,40 +216,45 @@ def do_transaction(self, display=()): 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 51f853d8b1..eb987bb8a3 100644 --- a/dnf/util.py +++ b/dnf/util.py @@ -25,6 +25,7 @@ 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 @@ def __init__(self): 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) From d22e532753d42ac90a5c406e092d64bcd030ce54 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Tue, 28 Jan 2025 11:27:00 -0500 Subject: [PATCH 6/7] spec: Add dnf-bootc subpackage 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. --- dnf.spec | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/dnf.spec b/dnf.spec index 67cd1859da..74127c9c66 100644 --- a/dnf.spec +++ b/dnf.spec @@ -190,6 +190,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 @@ -420,6 +431,9 @@ popd %{python3_sitelib}/%{name}/automatic/ %endif +%files bootc +# bootc subpackage does not include any files + %changelog * Tue Nov 12 2024 Evan Goode - 4.22.0-1 - doc: Naming of source and debug repos From d455f2d9e9c4630a542c8e586112ab434478f7ec Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Wed, 5 Feb 2025 10:35:08 -0500 Subject: [PATCH 7/7] Require libdnf >= 0.74.0 with `persistence` option --- dnf.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnf.spec b/dnf.spec index 74127c9c66..4155a93209 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