import CS dnf-4.20.0-12.el10

This commit is contained in:
eabdullin 2025-03-27 13:04:38 +00:00
parent 8ed3aeb040
commit 33736f2fb0
19 changed files with 2021 additions and 2 deletions

View File

@ -0,0 +1,98 @@
From b00c7171f58dbbda3df4bf5f2e65cbc7eff37a5b Mon Sep 17 00:00:00 2001
From: David Cantrell <dcantrell@redhat.com>
Date: Thu, 15 Feb 2024 14:03:32 -0500
Subject: [PATCH] Add detection for ostree-based systems and warn users about
losing changes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Upstream commit: 5c050ba2324c5fb95bf0e0501c7925f38f6a09dc
On ostree-based systems, users can use dnf to customize the
environment but those changes will be lost at the next ostree-based
image update. If you want to retain changes between ostree-updates
you need to make use of rpm-ostree right now.
Signed-off-by: David Cantrell <dcantrell@redhat.com>
Resolves: https://issues.redhat.com/browse/RHEL-49671
Signed-off-by: Petr Písař <ppisar@redhat.com>
---
dnf/cli/cli.py | 9 +++++++++
dnf/util.py | 31 +++++++++++++++++++++++++++++++
2 files changed, 40 insertions(+)
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
index 1824bd00e..c14f83639 100644
--- a/dnf/cli/cli.py
+++ b/dnf/cli/cli.py
@@ -214,6 +214,15 @@ class BaseCli(dnf.Base):
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_container():
+ _container_msg = _("""
+*** This system is managed with ostree. Changes to the system
+*** made with dnf will be lost with the next ostree-based update.
+*** If you do not want to lose these changes, use 'rpm-ostree'.
+""")
+ logger.info(_container_msg)
+ raise CliError(_("Operation aborted."))
+
if self._promptWanted():
if self.conf.assumeno or not self.output.userconfirm():
raise CliError(_("Operation aborted."))
diff --git a/dnf/util.py b/dnf/util.py
index 6cd7ad41f..1b465bda5 100644
--- a/dnf/util.py
+++ b/dnf/util.py
@@ -33,11 +33,13 @@ import errno
import functools
import hawkey
import itertools
+import json
import locale
import logging
import os
import pwd
import shutil
+import subprocess
import sys
import tempfile
import time
@@ -639,3 +641,32 @@ def _is_file_pattern_present(specs):
if subj._filename_pattern:
return True
return False
+
+
+def is_container():
+ """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.
+ """
+
+ bootc = '/usr/bin/bootc'
+ ostree = '/sysroot/ostree'
+
+ if os.path.isfile(bootc) and os.access(bootc, os.X_OK):
+ p = subprocess.Popen([bootc, "status", "--json"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ (out, err) = p.communicate()
+
+ if p.returncode == 0:
+ # check the output of 'bootc status'
+ j = json.loads(out)
+
+ # XXX: the API from bootc status is evolving
+ status = j.get("status", "")
+ kind = j.get("kind", "")
+
+ if kind.lower() == "bootchost" and bool(status.get("isContainer", None)):
+ return True
+ elif os.path.isdir(ostree):
+ return True
+
+ return False
--
2.46.2

View File

@ -0,0 +1,106 @@
From e2dbb97b9e13a73c47dd59827d7f2214bbdde99f Mon Sep 17 00:00:00 2001
From: Joseph Marrero <jmarrero@redhat.com>
Date: Tue, 16 Jul 2024 15:48:41 -0400
Subject: [PATCH] Update ostree/bootc host system check.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Upstream commit: 6120fe52511775b60b6031d4169988c025610ab5
This changes the is_container() func for _is_bootc_host()
and updates the logic and message. This should detect on
all ostree and bootc hosts to date that are not using
bootc usroverlay or ostree admin unlock for development
purposes.
Resolves: https://issues.redhat.com/browse/RHEL-49671
Signed-off-by: Petr Písař <ppisar@redhat.com>
---
dnf/cli/cli.py | 11 +++++------
dnf/util.py | 33 ++++++++-------------------------
2 files changed, 13 insertions(+), 31 deletions(-)
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
index c14f83639..83b190026 100644
--- a/dnf/cli/cli.py
+++ b/dnf/cli/cli.py
@@ -214,13 +214,12 @@ class BaseCli(dnf.Base):
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_container():
- _container_msg = _("""
-*** This system is managed with ostree. Changes to the system
-*** made with dnf will be lost with the next ostree-based update.
-*** If you do not want to lose these changes, use 'rpm-ostree'.
+ if dnf.util._is_bootc_host():
+ _bootc_host_msg = _("""
+*** Error: system is configured to be read-only; for more
+*** information run `bootc status` or `ostree admin status`.
""")
- logger.info(_container_msg)
+ logger.info(_bootc_host_msg)
raise CliError(_("Operation aborted."))
if self._promptWanted():
diff --git a/dnf/util.py b/dnf/util.py
index 1b465bda5..1ba2e27ff 100644
--- a/dnf/util.py
+++ b/dnf/util.py
@@ -33,13 +33,11 @@ import errno
import functools
import hawkey
import itertools
-import json
import locale
import logging
import os
import pwd
import shutil
-import subprocess
import sys
import tempfile
import time
@@ -643,30 +641,15 @@ def _is_file_pattern_present(specs):
return False
-def is_container():
+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.
"""
-
- bootc = '/usr/bin/bootc'
- ostree = '/sysroot/ostree'
-
- if os.path.isfile(bootc) and os.access(bootc, os.X_OK):
- p = subprocess.Popen([bootc, "status", "--json"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- (out, err) = p.communicate()
-
- if p.returncode == 0:
- # check the output of 'bootc status'
- j = json.loads(out)
-
- # XXX: the API from bootc status is evolving
- status = j.get("status", "")
- kind = j.get("kind", "")
-
- if kind.lower() == "bootchost" and bool(status.get("isContainer", None)):
- return True
- elif os.path.isdir(ostree):
- return True
-
- return False
+ 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)
--
2.46.2

View File

@ -0,0 +1,32 @@
From 15aedf5f4e70695e7801c80498d4da52e49ac626 Mon Sep 17 00:00:00 2001
From: Joseph Marrero <jmarrero@redhat.com>
Date: Mon, 22 Jul 2024 15:33:32 -0400
Subject: [PATCH] Update bootc hosts message to point to bootc --help
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Upstream commit: e2535589ce16bc36b96b37369502a3c312f6056a
Resolves: https://issues.redhat.com/browse/RHEL-49671
Signed-off-by: Petr Písař <ppisar@redhat.com>
---
dnf/cli/cli.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
index 83b190026..0eda2c8cb 100644
--- a/dnf/cli/cli.py
+++ b/dnf/cli/cli.py
@@ -217,7 +217,7 @@ class BaseCli(dnf.Base):
if dnf.util._is_bootc_host():
_bootc_host_msg = _("""
*** Error: system is configured to be read-only; for more
-*** information run `bootc status` or `ostree admin status`.
+*** information run `bootc --help`.
""")
logger.info(_bootc_host_msg)
raise CliError(_("Operation aborted."))
--
2.46.2

View File

@ -0,0 +1,47 @@
From ff86cee7cf33f44e4b10538ceeee5f284d6735ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= <ppisar@redhat.com>
Date: Thu, 15 Aug 2024 14:04:55 +0200
Subject: [PATCH] Allow --installroot on read-only bootc system
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Upstream commit: a1aa8d0e048751859a2bec1b2fb12fcca93c6e83
Some people use --installroot on a read-only bootc system to install
a system into a chroot subtree. However, current bootc check did not
take into account --installroot and rejected the operation.
This patch augments the check for the installroot being different
from /.
It's pointless to check for installroot writability here because
installroot is written before this check when updating the
repositories and computing a transaction. Moving this check sooner
would not help because some directories (/opt, /) are kept read-only
even on writable bootc.
Resolves: #2108
Resolves: https://issues.redhat.com/browse/RHEL-49671
Signed-off-by: Petr Písař <ppisar@redhat.com>
---
dnf/cli/cli.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
index 0eda2c8cb..008262ea0 100644
--- a/dnf/cli/cli.py
+++ b/dnf/cli/cli.py
@@ -214,7 +214,8 @@ class BaseCli(dnf.Base):
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():
+ if dnf.util._is_bootc_host() and \
+ os.path.realpath(self.conf.installroot) == "/":
_bootc_host_msg = _("""
*** Error: system is configured to be read-only; for more
*** information run `bootc --help`.
--
2.46.2

View File

@ -0,0 +1,40 @@
From 86bc1d60e1b8188ca5a682974d734ac3a0cdc102 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= <ppisar@redhat.com>
Date: Thu, 10 Oct 2024 10:57:48 +0200
Subject: [PATCH] Allow --downloadonly on read-only bootc system
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Upstream commit: 8d888d26e6da27ba37243d7504eb42472f389bde
"dnf install --downloadonly" failed on read-only bootc system despite
not running the transaction. The downloaded packages are stored under
writable /var or to a directory explicitly choosen by a user.
This patch suppresses the bootc read-only bailout if --downloadonly
option is used.
https://issues.redhat.com/browse/RHEL-62028
Signed-off-by: Petr Písař <ppisar@redhat.com>
---
dnf/cli/cli.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
index 008262ea0..d3844df34 100644
--- a/dnf/cli/cli.py
+++ b/dnf/cli/cli.py
@@ -215,7 +215,8 @@ 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))
if dnf.util._is_bootc_host() and \
- os.path.realpath(self.conf.installroot) == "/":
+ 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`.
--
2.47.0

View File

@ -0,0 +1,131 @@
From 36389b63b12c2f6996772bd2bc7e98b6f3bacdf0 Mon Sep 17 00:00:00 2001
From: Marek Blaha <mblaha@redhat.com>
Date: Thu, 17 Oct 2024 13:30:21 +0200
Subject: [PATCH 1/2] automatic: Check availability of config file
Upstream commit: 13ecc3921fb1566c2af7b80d5cb515d8fc4e5ec9
If a configuration file is explicitly specified on the command line,
ensure that it exists and is readable. If the file is not found, notify
the user immediately and terminate the process.
This resolves issues where users may run dnf-automatic with unrecognized
positional arguments, such as `dnf-automatic install`.
The most natural approach to handle a non-existing config file would be
by catching the exception thrown by the `read()` method of the
`libdnf.conf.ConfigParser` class. Unfortunately, the Python bindings
override the `read()` method at the SWIG level, causing it to suppress any
potentially raised IOError.
For details see this section of the commit
https://github.com/rpm-software-management/libdnf/commit/8f1fedf8551b72d6bc24018f5980714b3a103aeb
def ConfigParser__newRead(self, filenames):
parsedFNames = []
try:
if isinstance(filenames, str) or isinstance(filenames, unicode):
filenames = [filenames]
except NameError:
pass
for fname in filenames:
try:
self.readFileName(fname)
parsedFNames.append(fname)
except IOError:
pass
except Exception as e:
raise RuntimeError("Parsing file '%s' failed: %s" % (fname, str(e)))
return parsedFNames
ConfigParser.read = ConfigParser__newRead
Resolves: https://issues.redhat.com/browse/RHEL-68979
---
dnf/automatic/main.py | 19 +++++++++++++++----
1 file changed, 15 insertions(+), 4 deletions(-)
diff --git a/dnf/automatic/main.py b/dnf/automatic/main.py
index 243e3015..21aa82b7 100644
--- a/dnf/automatic/main.py
+++ b/dnf/automatic/main.py
@@ -74,7 +74,7 @@ def build_emitters(conf):
def parse_arguments(args):
parser = argparse.ArgumentParser()
- parser.add_argument('conf_path', nargs='?', default=dnf.const.CONF_AUTOMATIC_FILENAME)
+ parser.add_argument('conf_path', nargs='?')
parser.add_argument('--timer', action='store_true')
parser.add_argument('--installupdates', dest='installupdates', action='store_true')
parser.add_argument('--downloadupdates', dest='downloadupdates', action='store_true')
@@ -89,7 +89,17 @@ def parse_arguments(args):
class AutomaticConfig(object):
def __init__(self, filename=None, downloadupdates=None,
installupdates=None):
- if not filename:
+ if filename:
+ # Specific config file was explicitely requested. Check that it exists
+ # and is readable.
+ if os.access(filename, os.F_OK):
+ if not os.access(filename, os.R_OK):
+ raise dnf.exceptions.Error(
+ "Configuration file \"{}\" is not readable.".format(filename))
+ else:
+ raise dnf.exceptions.Error(
+ "Configuration file \"{}\" not found.".format(filename))
+ else:
filename = dnf.const.CONF_AUTOMATIC_FILENAME
self.commands = CommandsConfig()
self.email = EmailConfig()
@@ -302,11 +312,12 @@ def wait_for_network(repos, timeout):
def main(args):
(opts, parser) = parse_arguments(args)
+ conf = None
+ emitters = None
try:
conf = AutomaticConfig(opts.conf_path, opts.downloadupdates,
opts.installupdates)
- emitters = None
with dnf.Base() as base:
cli = dnf.cli.Cli(base)
cli._read_conf_file()
@@ -371,7 +382,7 @@ def main(args):
raise dnf.exceptions.Error('reboot command returned nonzero exit code: %d', exit_code)
except dnf.exceptions.Error as exc:
logger.error(_('Error: %s'), ucd(exc))
- if conf.emitters.send_error_messages and emitters != None:
+ if conf is not None and conf.emitters.send_error_messages and emitters is not None:
emitters.notify_error(_('Error: %s') % str(exc))
emitters.commit()
return 1
--
2.47.1
From bfc4d7ae2d9a567aaf656388c8b914b7a03db39d Mon Sep 17 00:00:00 2001
From: Marek Blaha <mblaha@redhat.com>
Date: Thu, 12 Dec 2024 08:09:48 +0100
Subject: [PATCH 2/2] automatic: Fix incorrect Error class instantiation
dnf.exceptions.Error class constructor accepts only one argument - error
message.
---
dnf/automatic/main.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dnf/automatic/main.py b/dnf/automatic/main.py
index 21aa82b7..1a1e1cf0 100644
--- a/dnf/automatic/main.py
+++ b/dnf/automatic/main.py
@@ -379,7 +379,7 @@ def main(args):
(conf.commands.reboot == 'when-needed' and base.reboot_needed())):
exit_code = os.waitstatus_to_exitcode(os.system(conf.commands.reboot_command))
if exit_code != 0:
- raise dnf.exceptions.Error('reboot command returned nonzero exit code: %d', exit_code)
+ raise dnf.exceptions.Error('reboot command returned nonzero exit code: %d' % exit_code)
except dnf.exceptions.Error as exc:
logger.error(_('Error: %s'), ucd(exc))
if conf is not None and conf.emitters.send_error_messages and emitters is not None:
--
2.47.1

View File

@ -0,0 +1,181 @@
From 082973b36646945b1c60be8b96ab628d92d99b92 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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ř <ppisar@redhat.com>
---
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

View File

@ -0,0 +1,74 @@
From 83177634e2a50887d9910048f87277b838eaa2f2 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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ř <ppisar@redhat.com>
---
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 <persistence-label>` 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
+ <transient_option-label>`) 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 <string-label>`
+
+ Whether changes should persist across system reboots. Default is ``auto``. Passing :ref:`--transient <transient_option-label>` 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

View File

@ -0,0 +1,166 @@
From c0f2329c2ed71f373aad26c7f1786494f6e75b76 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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ř <ppisar@redhat.com>
---
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

View File

@ -0,0 +1,237 @@
From 47cd04de2c099171002f2f474084e08eb5c7dddd Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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ř <ppisar@redhat.com>
---
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

View File

@ -0,0 +1,56 @@
From dc461487b3a1323d96183e5c229a307d274a3ecc Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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ř <ppisar@redhat.com>
---
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 <jkolarik@redhat.com> - 4.20.0-1
- repoquery: Fix loading filelists when -f is used (RhBug:2276012)
--
2.48.1

View File

@ -0,0 +1,45 @@
From 7ffd97532e120f4391792b1bdfa0dbe1510409a7 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
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ř <ppisar@redhat.com>
---
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

View File

@ -0,0 +1,162 @@
From ad24340a6ccf06a4c948824f394a0de5b6a7826d Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Mon, 20 Jan 2025 21:36:18 +0000
Subject: [PATCH] Derive releasever_{major,minor} in conf, not substitutions
Upstream commit: 0e283fb3b0b06f1d31c6d3bd9c14c5fa89bf64a4
This allows setting a releasever_major or releasever_minor
independent of releasever, which is needed by EPEL.
Related: https://issues.redhat.com/browse/RHEL-68034
---
dnf/conf/config.py | 26 ++++++++++++++++++++++++++
dnf/conf/substitutions.py | 17 +++--------------
tests/conf/test_substitutions.py | 19 +++++++++----------
tests/test_config.py | 16 ++++++++++++++++
4 files changed, 54 insertions(+), 24 deletions(-)
diff --git a/dnf/conf/config.py b/dnf/conf/config.py
index 5210ffba..6cf28724 100644
--- a/dnf/conf/config.py
+++ b/dnf/conf/config.py
@@ -430,6 +430,32 @@ class MainConf(BaseConfig):
return
self.substitutions['releasever'] = str(val)
+ @property
+ def releasever_major(self):
+ # :api
+ return self.substitutions.get('releasever_major')
+
+ @releasever_major.setter
+ def releasever_major(self, val):
+ # :api
+ if val is None:
+ self.substitutions.pop('releasever_major', None)
+ return
+ self.substitutions['releasever_major'] = str(val)
+
+ @property
+ def releasever_minor(self):
+ # :api
+ return self.substitutions.get('releasever_minor')
+
+ @releasever_minor.setter
+ def releasever_minor(self, val):
+ # :api
+ if val is None:
+ self.substitutions.pop('releasever_minor', None)
+ return
+ self.substitutions['releasever_minor'] = str(val)
+
@property
def arch(self):
# :api
diff --git a/dnf/conf/substitutions.py b/dnf/conf/substitutions.py
index 5c736a8d..8582d5d8 100644
--- a/dnf/conf/substitutions.py
+++ b/dnf/conf/substitutions.py
@@ -22,11 +22,12 @@ import logging
import os
import re
+from libdnf.conf import ConfigParser
from dnf.i18n import _
from dnf.exceptions import ReadOnlyVariableError
ENVIRONMENT_VARS_RE = re.compile(r'^DNF_VAR_[A-Za-z0-9_]+$')
-READ_ONLY_VARIABLES = frozenset(("releasever_major", "releasever_minor"))
+READ_ONLY_VARIABLES = frozenset()
logger = logging.getLogger('dnf')
@@ -45,18 +46,6 @@ class Substitutions(dict):
elif key in numericvars:
self[key] = val
- @staticmethod
- def _split_releasever(releasever):
- # type: (str) -> tuple[str, str]
- pos = releasever.find(".")
- if pos == -1:
- releasever_major = releasever
- releasever_minor = ""
- else:
- releasever_major = releasever[:pos]
- releasever_minor = releasever[pos + 1:]
- return releasever_major, releasever_minor
-
def __setitem__(self, key, value):
if Substitutions.is_read_only(key):
raise ReadOnlyVariableError(f"Variable \"{key}\" is read-only", variable_name=key)
@@ -65,7 +54,7 @@ class Substitutions(dict):
setitem(key, value)
if key == "releasever" and value:
- releasever_major, releasever_minor = Substitutions._split_releasever(value)
+ releasever_major, releasever_minor = ConfigParser.splitReleasever(value)
setitem("releasever_major", releasever_major)
setitem("releasever_minor", releasever_minor)
diff --git a/tests/conf/test_substitutions.py b/tests/conf/test_substitutions.py
index d8ac3c20..78d3e727 100644
--- a/tests/conf/test_substitutions.py
+++ b/tests/conf/test_substitutions.py
@@ -56,16 +56,6 @@ class SubstitutionsFromEnvironmentTest(tests.support.TestCase):
self.assertEqual('opera', conf.substitutions['GENRE'])
-class SubstitutionsReadOnlyTest(tests.support.TestCase):
- def test_set_readonly(self):
- conf = dnf.conf.Conf()
- variable_name = "releasever_major"
- self.assertTrue(Substitutions.is_read_only(variable_name))
- with self.assertRaises(ReadOnlyVariableError) as cm:
- conf.substitutions["releasever_major"] = "1"
- self.assertEqual(cm.exception.variable_name, "releasever_major")
-
-
class SubstitutionsReleaseverTest(tests.support.TestCase):
def test_releasever_simple(self):
conf = dnf.conf.Conf()
@@ -84,3 +74,12 @@ class SubstitutionsReleaseverTest(tests.support.TestCase):
conf.substitutions["releasever"] = "1.23.45"
self.assertEqual(conf.substitutions["releasever_major"], "1")
self.assertEqual(conf.substitutions["releasever_minor"], "23.45")
+
+ def test_releasever_major_minor_overrides(self):
+ conf = dnf.conf.Conf()
+ conf.substitutions["releasever"] = "1.23"
+ conf.substitutions["releasever_major"] = "45"
+ conf.substitutions["releasever_minor"] = "67"
+ self.assertEqual(conf.substitutions["releasever"], "1.23")
+ self.assertEqual(conf.substitutions["releasever_major"], "45")
+ self.assertEqual(conf.substitutions["releasever_minor"], "67")
diff --git a/tests/test_config.py b/tests/test_config.py
index d8502670..16bdcccb 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -145,3 +145,19 @@ class ConfTest(tests.support.TestCase):
conf = Conf()
with self.assertRaises(dnf.exceptions.ConfigError):
conf.debuglevel = '11'
+
+ def test_releasever_major_minor(self):
+ conf = Conf()
+ conf.releasever = '1.2'
+ self.assertEqual(conf.releasever_major, '1')
+ self.assertEqual(conf.releasever_minor, '2')
+
+ # override releasever_major
+ conf.releasever_major = '3'
+ self.assertEqual(conf.releasever_major, '3')
+ self.assertEqual(conf.releasever_minor, '2')
+
+ # override releasever_minor
+ conf.releasever_minor = '4'
+ self.assertEqual(conf.releasever_major, '3')
+ self.assertEqual(conf.releasever_minor, '4')
--
2.48.1

View File

@ -0,0 +1,179 @@
From 66fb0b5ec8ea3d3165e6b2f38e1a01f692d1ec93 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Tue, 21 Jan 2025 19:16:13 +0000
Subject: [PATCH] Override releasever_{major,minor} with provides
Upstream commit: 75e3ff0c43a5d605aa43d6ddc2d9e2ef89942999
The releasever_major and releasever_minor substitution variables are
usually derived by splitting releasever on the first `.`. However, to
support EPEL 10 [1], we would like a way for distributions to override these
values. Specifically, we would like RHEL 10 to have a releasever of `10`
with a releasever_major of `10` and a releasever_minor of `0` (later
incrementing to `1`, `2`, to correspond with the RHEL minor version).
This commit adds a new API function, `detect_releasevers`, which derives
releasever, releasever_major, and releasever_minor from virtual provides
on the system-release package (any of `DISTROVERPKG`). The detection of
releasever is unchanged. releasever_major and releasever_minor are
specified by the versions of the `system-release-major` and
`system-release-minor` provides, respectively.
If the user specifies a `--releasever=X.Y` on the command line, the
distribution settings for releasever, releasever_major, and releasever_minor
will all be overridden: releasever will be set to X.Y, releasever_major will be
set to X, and releasever_minor will be set to Y, same as before. If a user
wants to specify a custom releasever_major and releasever_minor, they have to
set all three with `--setopt=releasever=X --setopt=releasever_major=Y
--setopt=releasever_minor=z`, taking care to put `releasever_major` and
`releasever_minor` after `releasever` so they are not overridden.
[1] https://issues.redhat.com/browse/RHEL-68034
---
dnf/base.py | 10 ++++++++--
dnf/cli/cli.py | 11 +++++++++--
dnf/const.py.in | 2 ++
dnf/rpm/__init__.py | 43 +++++++++++++++++++++++++++++++++++++++----
4 files changed, 58 insertions(+), 8 deletions(-)
diff --git a/dnf/base.py b/dnf/base.py
index dac3cefd..c4a4f854 100644
--- a/dnf/base.py
+++ b/dnf/base.py
@@ -157,8 +157,14 @@ class Base(object):
conf = dnf.conf.Conf()
subst = conf.substitutions
if 'releasever' not in subst:
- subst['releasever'] = \
- dnf.rpm.detect_releasever(conf.installroot)
+ releasever, major, minor = \
+ dnf.rpm.detect_releasevers(conf.installroot)
+ subst['releasever'] = releasever
+ if major is not None:
+ subst['releasever_major'] = major
+ if minor is not None:
+ subst['releasever_minor'] = minor
+
return conf
def _setup_modular_excludes(self):
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
index e2700c09..118ce4e7 100644
--- a/dnf/cli/cli.py
+++ b/dnf/cli/cli.py
@@ -971,13 +971,20 @@ class Cli(object):
from_root = "/"
subst = conf.substitutions
subst.update_from_etc(from_root, varsdir=conf._get_value('varsdir'))
+
# cachedir, logs, releasever, and gpgkey are taken from or stored in installroot
+ major = None
+ minor = None
if releasever is None and conf.releasever is None:
- releasever = dnf.rpm.detect_releasever(conf.installroot)
+ releasever, major, minor = dnf.rpm.detect_releasevers(conf.installroot)
elif releasever == '/':
- releasever = dnf.rpm.detect_releasever(releasever)
+ releasever, major, minor = dnf.rpm.detect_releasevers(releasever)
if releasever is not None:
conf.releasever = releasever
+ if major is not None:
+ conf.releasever_major = major
+ if minor is not None:
+ conf.releasever_minor = minor
if conf.releasever is None:
logger.warning(_("Unable to detect release version (use '--releasever' to specify "
"release version)"))
diff --git a/dnf/const.py.in b/dnf/const.py.in
index bcadc804..07aab7a4 100644
--- a/dnf/const.py.in
+++ b/dnf/const.py.in
@@ -25,6 +25,8 @@ CONF_AUTOMATIC_FILENAME='/etc/dnf/automatic.conf'
DISTROVERPKG=('system-release(releasever)', 'system-release',
'distribution-release(releasever)', 'distribution-release',
'redhat-release', 'suse-release')
+DISTROVER_MAJOR_PKG='system-release(releasever_major)'
+DISTROVER_MINOR_PKG='system-release(releasever_minor)'
GROUP_PACKAGE_TYPES = ('mandatory', 'default', 'conditional') # :api
INSTALLONLYPKGS=['kernel', 'kernel-PAE',
'installonlypkg(kernel)',
diff --git a/dnf/rpm/__init__.py b/dnf/rpm/__init__.py
index 12efca7f..d4be4d03 100644
--- a/dnf/rpm/__init__.py
+++ b/dnf/rpm/__init__.py
@@ -26,12 +26,21 @@ import dnf.exceptions
import rpm # used by ansible (dnf.rpm.rpm.labelCompare in lib/ansible/modules/packaging/os/dnf.py)
-def detect_releasever(installroot):
+def detect_releasevers(installroot):
# :api
- """Calculate the release version for the system."""
+ """Calculate the release version for the system, including releasever_major
+ and releasever_minor if they are overriden by the system-release-major or
+ system-release-minor provides."""
ts = transaction.initReadOnlyTransaction(root=installroot)
ts.pushVSFlags(~(rpm._RPMVSF_NOSIGNATURES | rpm._RPMVSF_NODIGESTS))
+
+ distrover_major_pkg = dnf.const.DISTROVER_MAJOR_PKG
+ distrover_minor_pkg = dnf.const.DISTROVER_MINOR_PKG
+ if dnf.pycomp.PY3:
+ distrover_major_pkg = bytes(distrover_major_pkg, 'utf-8')
+ distrover_minor_pkg = bytes(distrover_minor_pkg, 'utf-8')
+
for distroverpkg in dnf.const.DISTROVERPKG:
if dnf.pycomp.PY3:
distroverpkg = bytes(distroverpkg, 'utf-8')
@@ -47,6 +56,8 @@ def detect_releasever(installroot):
msg = 'Error: rpmdb failed to list provides. Try: rpm --rebuilddb'
raise dnf.exceptions.Error(msg)
releasever = hdr['version']
+ releasever_major = None
+ releasever_minor = None
try:
try:
# header returns bytes -> look for bytes
@@ -61,13 +72,37 @@ def detect_releasever(installroot):
if hdr['name'] not in (distroverpkg, distroverpkg.decode("utf8")):
# override the package version
releasever = ver
+
+ for provide, flag, ver in zip(
+ hdr[rpm.RPMTAG_PROVIDENAME],
+ hdr[rpm.RPMTAG_PROVIDEFLAGS],
+ hdr[rpm.RPMTAG_PROVIDEVERSION]):
+ if isinstance(provide, str):
+ provide = bytes(provide, "utf-8")
+ if provide == distrover_major_pkg and flag == rpm.RPMSENSE_EQUAL and ver:
+ releasever_major = ver
+ if provide == distrover_minor_pkg and flag == rpm.RPMSENSE_EQUAL and ver:
+ releasever_minor = ver
+
except (ValueError, KeyError, IndexError):
pass
if is_py3bytes(releasever):
releasever = str(releasever, "utf-8")
- return releasever
- return None
+ if is_py3bytes(releasever_major):
+ releasever_major = str(releasever_major, "utf-8")
+ if is_py3bytes(releasever_minor):
+ releasever_minor = str(releasever_minor, "utf-8")
+ return releasever, releasever_major, releasever_minor
+ return (None, None, None)
+
+
+def detect_releasever(installroot):
+ # :api
+ """Calculate the release version for the system."""
+
+ releasever, _, _ = detect_releasevers(installroot)
+ return releasever
def _header(path):
--
2.48.1

View File

@ -0,0 +1,124 @@
From 310def9d6995728d6bcaa7bc8bb5f311bcd64ca3 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Fri, 24 Jan 2025 22:50:22 +0000
Subject: [PATCH] Add --releasever-major and --releasever-minor options
Upstream commit: 017bbab0a253d84978f645cd358cdeb63e9ecb18
Allows the user to override the $releasever_major and $releasever_minor
variables on the command line, like --releasever.
---
dnf/cli/cli.py | 29 +++++++++++++++++------------
dnf/cli/option_parser.py | 6 ++++++
doc/command_ref.rst | 8 ++++++++
doc/conf_ref.rst | 2 ++
4 files changed, 33 insertions(+), 12 deletions(-)
diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py
index 118ce4e7..0be9559d 100644
--- a/dnf/cli/cli.py
+++ b/dnf/cli/cli.py
@@ -836,7 +836,7 @@ class Cli(object):
dnf.conf.PRIO_DEFAULT)
self.demands.cacheonly = True
self.base.conf._configure_from_options(opts)
- self._read_conf_file(opts.releasever)
+ self._read_conf_file(opts.releasever, opts.releasever_major, opts.releasever_minor)
if 'arch' in opts:
self.base.conf.arch = opts.arch
self.base.conf._adjust_conf_options()
@@ -945,7 +945,7 @@ class Cli(object):
)
)
- def _read_conf_file(self, releasever=None):
+ def _read_conf_file(self, releasever=None, releasever_major=None, releasever_minor=None):
timer = dnf.logging.Timer('config')
conf = self.base.conf
@@ -973,18 +973,23 @@ class Cli(object):
subst.update_from_etc(from_root, varsdir=conf._get_value('varsdir'))
# cachedir, logs, releasever, and gpgkey are taken from or stored in installroot
- major = None
- minor = None
+
+ det_major = None
+ det_minor = None
if releasever is None and conf.releasever is None:
- releasever, major, minor = dnf.rpm.detect_releasevers(conf.installroot)
+ releasever, det_major, det_minor = dnf.rpm.detect_releasevers(conf.installroot)
elif releasever == '/':
- releasever, major, minor = dnf.rpm.detect_releasevers(releasever)
- if releasever is not None:
- conf.releasever = releasever
- if major is not None:
- conf.releasever_major = major
- if minor is not None:
- conf.releasever_minor = minor
+ releasever, det_major, det_minor = dnf.rpm.detect_releasevers(releasever)
+
+ def or_else(*args):
+ for arg in args:
+ if arg is not None:
+ return arg
+ return None
+ conf.releasever = or_else(releasever, conf.releasever)
+ conf.releasever_major = or_else(releasever_major, det_major, conf.releasever_major)
+ conf.releasever_minor = or_else(releasever_minor, det_minor, conf.releasever_minor)
+
if conf.releasever is None:
logger.warning(_("Unable to detect release version (use '--releasever' to specify "
"release version)"))
diff --git a/dnf/cli/option_parser.py b/dnf/cli/option_parser.py
index ec4696fd..fba37614 100644
--- a/dnf/cli/option_parser.py
+++ b/dnf/cli/option_parser.py
@@ -199,6 +199,12 @@ class OptionParser(argparse.ArgumentParser):
general_grp.add_argument("--releasever", default=None,
help=_("override the value of $releasever"
" in config and repo files"))
+ general_grp.add_argument("--releasever-major", default=None,
+ help=_("override the value of $releasever_major"
+ " in config and repo files"))
+ general_grp.add_argument("--releasever-minor", default=None,
+ help=_("override the value of $releasever_minor"
+ " in config and repo files"))
general_grp.add_argument("--setopt", dest="setopts", default=[],
action=self._SetoptsCallback,
help=_("set arbitrary config and repo options"))
diff --git a/doc/command_ref.rst b/doc/command_ref.rst
index 2337a2e2..9ffd4c1c 100644
--- a/doc/command_ref.rst
+++ b/doc/command_ref.rst
@@ -334,6 +334,14 @@ Options
Configure DNF as if the distribution release was ``<release>``. This can
affect cache paths, values in configuration files and mirrorlist URLs.
+``--releasever_major=<major version>``
+ Override the releasever_major variable, which is usually automatically
+ detected or taken from the part of ``$releasever`` before the first ``.``.
+
+``--releasever_minor=<minor version>``
+ Override the releasever_minor variable, which is usually automatically
+ detected or taken from the part of ``$releasever`` after the first ``.``.
+
.. _repofrompath_options-label:
diff --git a/doc/conf_ref.rst b/doc/conf_ref.rst
index d4e4a277..441aa77b 100644
--- a/doc/conf_ref.rst
+++ b/doc/conf_ref.rst
@@ -503,6 +503,8 @@ configuration file by your distribution to override the DNF defaults.
The ``$releasever_major`` and ``$releasever_minor`` variables will be automatically derived from ``$releasever`` by splitting it on the first ``.``. For example, if ``$releasever`` is set to ``1.23``, then ``$releasever_major`` will be ``1`` and ``$releasever_minor`` will be ``23``.
+ ``$releasever_major`` and ``$releasever_minor`` can also be set by the distribution.
+
See also :ref:`repo variables <repo-variables-label>`.
.. _reposdir-label:
--
2.48.1

View File

@ -0,0 +1,57 @@
From ce877f038577a0080ca9e8f69a7fe28047e829ad Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Mon, 27 Jan 2025 18:49:11 +0000
Subject: [PATCH] doc: Document detect_releasevers and update example
Upstream commit: 593ab0c377cdca78895628c9f7a4676ca220215c
Adds dnf.rpm.detect_releasevers to the API docs and mention it is
now preferred over dnf.rpm.detect_releasever.
Updates examples/install_extension.py to use detect_releasevers and set
the releasever_major and releasever_minor substitution variables.
---
doc/api_rpm.rst | 8 ++++++++
doc/examples/install_extension.py | 6 +++++-
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/doc/api_rpm.rst b/doc/api_rpm.rst
index c59ed67d..562be41a 100644
--- a/doc/api_rpm.rst
+++ b/doc/api_rpm.rst
@@ -27,6 +27,14 @@
Returns ``None`` if the information can not be determined (perhaps because the tree has no RPMDB).
+.. function:: detect_releasevers(installroot)
+
+ Returns a tuple of the release name, overridden major release, and overridden minor release of the distribution of the tree rooted at `installroot`. The function uses information from RPMDB found under the tree. The major and minor release versions are usually derived from the release version by splitting it on the first ``.``, but distributions can override the derived major and minor versions. It's preferred to use ``detect_releasevers`` over ``detect_releasever``; if you use the latter, you will not be aware of distribution overrides for the major and minor release versions.
+
+ Returns ``(None, None, None)`` if the information can not be determined (perhaps because the tree has no RPMDB).
+
+ If the distribution does not override the release major version, then the second item of the returned tuple will be ``None``. Likewise, if the release minor version is not overridden, the third return value will be ``None``.
+
.. function:: basearch(arch)
Return base architecture of the processor based on `arch` type given. E.g. when `arch` i686 is given then the returned value will be i386.
diff --git a/doc/examples/install_extension.py b/doc/examples/install_extension.py
index dbd3b890..b1540e12 100644
--- a/doc/examples/install_extension.py
+++ b/doc/examples/install_extension.py
@@ -32,8 +32,12 @@ if __name__ == '__main__':
with dnf.Base() as base:
# Substitutions are needed for correct interpretation of repo files.
- RELEASEVER = dnf.rpm.detect_releasever(base.conf.installroot)
+ RELEASEVER, MAJOR, MINOR = dnf.rpm.detect_releasever(base.conf.installroot)
base.conf.substitutions['releasever'] = RELEASEVER
+ if MAJOR is not None:
+ base.conf.substitutions['releasever_major'] = MAJOR
+ if MINOR is not None:
+ base.conf.substitutions['releasever_minor'] = MINOR
# Repositories are needed if we want to install anything.
base.read_all_repos()
# A sack is required by marking methods and dependency resolving.
--
2.48.1

View File

@ -0,0 +1,129 @@
From 71fda4446391fc17e121d4c8c5eea70c981a2f0f Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Mon, 27 Jan 2025 19:20:14 +0000
Subject: [PATCH] tests: Patch detect_releasevers, not detect_releasever
Upstream commit: ba5ddbddf1ffb983fe9dc1a329e22eacd2ef35f7
---
tests/api/test_dnf_rpm.py | 4 ++++
tests/cli/commands/test_clean.py | 2 +-
tests/support.py | 2 +-
tests/test_base.py | 4 ++--
tests/test_cli.py | 10 +++++-----
5 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/tests/api/test_dnf_rpm.py b/tests/api/test_dnf_rpm.py
index e6d8de84..fb606ffc 100644
--- a/tests/api/test_dnf_rpm.py
+++ b/tests/api/test_dnf_rpm.py
@@ -14,6 +14,10 @@ class DnfRpmApiTest(TestCase):
# dnf.rpm.detect_releasever
self.assertHasAttr(dnf.rpm, "detect_releasever")
+ def test_detect_releasevers(self):
+ # dnf.rpm.detect_releasevers
+ self.assertHasAttr(dnf.rpm, "detect_releasevers")
+
def test_basearch(self):
# dnf.rpm.basearch
self.assertHasAttr(dnf.rpm, "basearch")
diff --git a/tests/cli/commands/test_clean.py b/tests/cli/commands/test_clean.py
index cc0a5df3..c77cb3ef 100644
--- a/tests/cli/commands/test_clean.py
+++ b/tests/cli/commands/test_clean.py
@@ -31,7 +31,7 @@ from tests.support import mock
'''
def _run(cli, args):
with mock.patch('sys.stdout', new_callable=StringIO), \
- mock.patch('dnf.rpm.detect_releasever', return_value=69):
+ mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)):
cli.configure(['clean', '--config', '/dev/null'] + args)
cli.run()
diff --git a/tests/support.py b/tests/support.py
index e50684ef..d03683ed 100644
--- a/tests/support.py
+++ b/tests/support.py
@@ -177,7 +177,7 @@ def command_run(cmd, args):
class Base(dnf.Base):
def __init__(self, *args, **kwargs):
- with mock.patch('dnf.rpm.detect_releasever', return_value=69):
+ with mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)):
super(Base, self).__init__(*args, **kwargs)
# mock objects
diff --git a/tests/test_base.py b/tests/test_base.py
index ad3ef675..9e0a656d 100644
--- a/tests/test_base.py
+++ b/tests/test_base.py
@@ -57,7 +57,7 @@ class BaseTest(tests.support.TestCase):
self.assertIsNotNone(base)
base.close()
- @mock.patch('dnf.rpm.detect_releasever', lambda x: 'x')
+ @mock.patch('dnf.rpm.detect_releasevers', lambda x: ('x', None, None))
@mock.patch('dnf.util.am_i_root', lambda: True)
def test_default_config_root(self):
base = dnf.Base()
@@ -67,7 +67,7 @@ class BaseTest(tests.support.TestCase):
self.assertIsNotNone(reg.match(base.conf.cachedir))
base.close()
- @mock.patch('dnf.rpm.detect_releasever', lambda x: 'x')
+ @mock.patch('dnf.rpm.detect_releasevers', lambda x: ('x', None, None))
@mock.patch('dnf.util.am_i_root', lambda: False)
def test_default_config_user(self):
base = dnf.Base()
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 9c130c36..573d4ae2 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -191,7 +191,7 @@ class ConfigureTest(tests.support.DnfBaseTestCase):
# call setUp() once again *after* am_i_root() is mocked so the cachedir is set as expected
self.setUp()
self.base._conf.installroot = self._installroot
- with mock.patch('dnf.rpm.detect_releasever', return_value=69):
+ with mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)):
self.cli.configure(['update', '-c', self.conffile])
reg = re.compile('^' + self._installroot + '/var/tmp/dnf-[.a-zA-Z0-9_-]+$')
self.assertIsNotNone(reg.match(self.base.conf.cachedir))
@@ -203,7 +203,7 @@ class ConfigureTest(tests.support.DnfBaseTestCase):
def test_configure_root(self):
""" Test Cli.configure as root."""
self.base._conf = dnf.conf.Conf()
- with mock.patch('dnf.rpm.detect_releasever', return_value=69):
+ with mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)):
self.cli.configure(['update', '--nogpgcheck', '-c', self.conffile])
reg = re.compile('^/var/cache/dnf$')
self.assertIsNotNone(reg.match(self.base.conf.system_cachedir))
@@ -213,7 +213,7 @@ class ConfigureTest(tests.support.DnfBaseTestCase):
def test_configure_verbose(self):
self.base._conf.installroot = self._installroot
- with mock.patch('dnf.rpm.detect_releasever', return_value=69):
+ with mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)):
self.cli.configure(['-v', 'update', '-c', self.conffile])
parser = argparse.ArgumentParser()
expected = "%s -v update -c %s " % (parser.prog, self.conffile)
@@ -225,7 +225,7 @@ class ConfigureTest(tests.support.DnfBaseTestCase):
@mock.patch('os.path.exists', return_value=True)
def test_conf_exists_in_installroot(self, ospathexists):
with mock.patch('logging.Logger.warning'), \
- mock.patch('dnf.rpm.detect_releasever', return_value=69):
+ mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)):
self.cli.configure(['--installroot', '/roots/dnf', 'update'])
self.assertEqual(self.base.conf.config_file_path, '/roots/dnf/etc/dnf/dnf.conf')
self.assertEqual(self.base.conf.installroot, '/roots/dnf')
@@ -233,7 +233,7 @@ class ConfigureTest(tests.support.DnfBaseTestCase):
@mock.patch('dnf.cli.cli.Cli._parse_commands', new=mock.MagicMock)
@mock.patch('os.path.exists', return_value=False)
def test_conf_notexists_in_installroot(self, ospathexists):
- with mock.patch('dnf.rpm.detect_releasever', return_value=69):
+ with mock.patch('dnf.rpm.detect_releasevers', return_value=(69, None, None)):
self.cli.configure(['--installroot', '/roots/dnf', 'update'])
self.assertEqual(self.base.conf.config_file_path, '/etc/dnf/dnf.conf')
self.assertEqual(self.base.conf.installroot, '/roots/dnf')
--
2.48.1

View File

@ -0,0 +1,97 @@
From 0d750818c8aa92fd08dd5179839f5734a1b1be96 Mon Sep 17 00:00:00 2001
From: Evan Goode <mail@evangoo.de>
Date: Tue, 4 Feb 2025 23:01:43 +0000
Subject: [PATCH] Document how --releasever, --releasever_{major,minor} affect
each other
Upstream commit: e931960d26a0782a23e8d89a6a662ee2442153fc
---
dnf/conf/config.py | 16 ++++++++++++++++
doc/command_ref.rst | 2 ++
tests/test_config.py | 3 +++
3 files changed, 21 insertions(+)
diff --git a/dnf/conf/config.py b/dnf/conf/config.py
index 6cf28724..7e5e8a38 100644
--- a/dnf/conf/config.py
+++ b/dnf/conf/config.py
@@ -425,6 +425,12 @@ class MainConf(BaseConfig):
@releasever.setter
def releasever(self, val):
# :api
+ """
+ Sets the releasever variable and sets releasever_major and
+ releasever_minor accordingly. releasever_major is set to the part of
+ $releasever before the first ".". releasever_minor is set to the part
+ after the first ".".
+ """
if val is None:
self.substitutions.pop('releasever', None)
return
@@ -438,6 +444,11 @@ class MainConf(BaseConfig):
@releasever_major.setter
def releasever_major(self, val):
# :api
+ """
+ Override the releasever_major variable, which is usually derived from
+ the releasever variable. This setter does not update the value of
+ $releasever.
+ """
if val is None:
self.substitutions.pop('releasever_major', None)
return
@@ -446,6 +457,11 @@ class MainConf(BaseConfig):
@property
def releasever_minor(self):
# :api
+ """
+ Override the releasever_minor variable, which is usually derived from
+ the releasever variable. This setter does not update the value of
+ $releasever.
+ """
return self.substitutions.get('releasever_minor')
@releasever_minor.setter
diff --git a/doc/command_ref.rst b/doc/command_ref.rst
index 9ffd4c1c..f7b8e22c 100644
--- a/doc/command_ref.rst
+++ b/doc/command_ref.rst
@@ -337,10 +337,12 @@ Options
``--releasever_major=<major version>``
Override the releasever_major variable, which is usually automatically
detected or taken from the part of ``$releasever`` before the first ``.``.
+ This option does not affect the ``$releasever`` variable.
``--releasever_minor=<minor version>``
Override the releasever_minor variable, which is usually automatically
detected or taken from the part of ``$releasever`` after the first ``.``.
+ This option does not affect the ``$releasever`` variable.
.. _repofrompath_options-label:
diff --git a/tests/test_config.py b/tests/test_config.py
index 16bdcccb..69ba988c 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -149,15 +149,18 @@ class ConfTest(tests.support.TestCase):
def test_releasever_major_minor(self):
conf = Conf()
conf.releasever = '1.2'
+ self.assertEqual(conf.releasever, '1.2')
self.assertEqual(conf.releasever_major, '1')
self.assertEqual(conf.releasever_minor, '2')
# override releasever_major
conf.releasever_major = '3'
+ self.assertEqual(conf.releasever, '1.2')
self.assertEqual(conf.releasever_major, '3')
self.assertEqual(conf.releasever_minor, '2')
# override releasever_minor
conf.releasever_minor = '4'
+ self.assertEqual(conf.releasever, '1.2')
self.assertEqual(conf.releasever_major, '3')
self.assertEqual(conf.releasever_minor, '4')
--
2.48.1

View File

@ -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-8
%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: 6%{?dist}
Release: 12%{?dist}
Summary: %{pkg_summary}
# For a breakdown of the licensing, see PACKAGE-LICENSING
License: GPL-2.0-or-later AND GPL-1.0-only
@ -79,6 +83,25 @@ Patch2: 0002-Limit-queries-to-nevra-forms-when-provided-by-comman.patch
Patch3: 0003-doc-Remove-provide-of-spec-definition-for-repoquery-.patch
Patch4: 0004-Drop-collect-file-for-ABRT.patch
Patch5: 0005-tests-Use-PGP-keys-without-SHA-1.patch
Patch6: 0006-Add-detection-for-ostree-based-systems-and-warn-user.patch
Patch7: 0007-Update-ostree-bootc-host-system-check.patch
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
Patch18: 0018-Derive-releasever_-major-minor-in-conf-not-substitut.patch
Patch19: 0019-Override-releasever_-major-minor-with-provides.patch
Patch20: 0020-Add-releasever-major-and-releasever-minor-options.patch
Patch21: 0021-doc-Document-detect_releasevers-and-update-example.patch
Patch22: 0022-tests-Patch-detect_releasevers-not-detect_releasever.patch
Patch23: 0023-Document-how-releasever-releasever_-major-minor-affe.patch
BuildArch: noarch
BuildRequires: cmake
BuildRequires: gettext
@ -196,6 +219,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
@ -418,7 +452,31 @@ popd
%{_unitdir}/%{name}-automatic-install.timer
%{python3_sitelib}/%{name}/automatic/
%files bootc
# bootc subpackage does not include any files
%changelog
* Fri Feb 07 2025 Carl George <carl@redhat.com> - 4.20.0-12
- Override releasever_{major,minor} with system-release provides (RHEL-68034)
* Thu Feb 06 2025 Petr Pisar <ppisar@redhat.com> - 4.20.0-11
- Add support for transient transactions (RHEL-76849)
* Fri Dec 13 2024 Marek Blaha <mblaha@redhat.com> - 4.20.0-10
- automatic: Check availability of config file
* Tue Oct 29 2024 Troy Dawson <tdawson@redhat.com> - 4.20.0-9
- Bump release for October 2024 mass rebuild:
Resolves: RHEL-64018
* Tue Oct 15 2024 Petr Pisar <ppisar@redhat.com> - 4.20.0-8
- Allow "dnf install --downloadonly" on locked OSTree and bootc systems
(RHEL-62028)
* Fri Sep 20 2024 Petr Pisar <ppisar@redhat.com> - 4.20.0-7
- More specific error message on a locked OSTree system or a bootc system
without a usr-overlay (RHEL-49671)
* Tue Aug 06 2024 Petr Pisar <ppisar@redhat.com> - 4.20.0-6
- Revert more specific error message on a locked OSTree system or a bootc system
without a usr-overlay (RHEL-49671)