diff --git a/SOURCES/0027-Add-detection-for-ostree-based-systems-and-warn-user.patch b/SOURCES/0027-Add-detection-for-ostree-based-systems-and-warn-user.patch new file mode 100644 index 0000000..725f2fb --- /dev/null +++ b/SOURCES/0027-Add-detection-for-ostree-based-systems-and-warn-user.patch @@ -0,0 +1,99 @@ +From 6af6633b3e1e92405ec63ef4d3d697891213f8cb Mon Sep 17 00:00:00 2001 +From: David Cantrell +Date: Thu, 15 Feb 2024 14:03:32 -0500 +Subject: [PATCH 1/4] 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 +Resolves: https://issues.redhat.com/browse/RHEL-49670 +Signed-off-by: Petr Písař +--- + 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 0c4f4c6ad..1fd0e96c3 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 16c5bc89c..9909f8fea 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 +@@ -631,3 +633,32 @@ def _post_transaction_output(base, transaction, action_callback): + def _name_unset_wrapper(input_name): + # returns for everything that evaluates to False (None, empty..) + return input_name if input_name else _("") ++ ++ ++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 +\ No newline at end of file +-- +2.46.2 + diff --git a/SOURCES/0028-Update-ostree-bootc-host-system-check.patch b/SOURCES/0028-Update-ostree-bootc-host-system-check.patch new file mode 100644 index 0000000..e3ba74c --- /dev/null +++ b/SOURCES/0028-Update-ostree-bootc-host-system-check.patch @@ -0,0 +1,107 @@ +From 6157248a5035a7752115143d8d29773384c1db3f Mon Sep 17 00:00:00 2001 +From: Joseph Marrero +Date: Tue, 16 Jul 2024 15:48:41 -0400 +Subject: [PATCH 2/4] 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-49670 +Signed-off-by: Petr Písař +--- + 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 1fd0e96c3..8521dd351 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 9909f8fea..e8f587a03 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 +@@ -635,30 +633,15 @@ def _name_unset_wrapper(input_name): + return input_name if input_name else _("") + + +-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 +\ No newline at end of file ++ 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 + diff --git a/SOURCES/0029-Update-bootc-hosts-message-to-point-to-bootc-help.patch b/SOURCES/0029-Update-bootc-hosts-message-to-point-to-bootc-help.patch new file mode 100644 index 0000000..0adda93 --- /dev/null +++ b/SOURCES/0029-Update-bootc-hosts-message-to-point-to-bootc-help.patch @@ -0,0 +1,32 @@ +From 0bab6b9620f9872271c9458212538eb03b8b085c Mon Sep 17 00:00:00 2001 +From: Joseph Marrero +Date: Mon, 22 Jul 2024 15:33:32 -0400 +Subject: [PATCH 3/4] 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-49670 + +Signed-off-by: Petr Písař +--- + 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 8521dd351..99af9069b 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 + diff --git a/SOURCES/0030-Allow-installroot-on-read-only-bootc-system.patch b/SOURCES/0030-Allow-installroot-on-read-only-bootc-system.patch new file mode 100644 index 0000000..c73c8c0 --- /dev/null +++ b/SOURCES/0030-Allow-installroot-on-read-only-bootc-system.patch @@ -0,0 +1,47 @@ +From ca4c52214b16fe298c44939cd7aaccd9b7549869 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= +Date: Thu, 15 Aug 2024 14:04:55 +0200 +Subject: [PATCH 4/4] 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-49670 +Signed-off-by: Petr Písař +--- + 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 99af9069b..36cfa74b5 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 + diff --git a/SOURCES/0031-smtplib-catch-OSError-not-SMTPException.patch b/SOURCES/0031-smtplib-catch-OSError-not-SMTPException.patch new file mode 100644 index 0000000..28a5409 --- /dev/null +++ b/SOURCES/0031-smtplib-catch-OSError-not-SMTPException.patch @@ -0,0 +1,33 @@ +From ea2d17cc484c7c49686145f4b2e98e4b73b9c967 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Mon, 13 Mar 2023 14:50:41 -0400 +Subject: [PATCH] smtplib: catch OSError, not SMTPException + +Some, but not all, types of connection error are caught by smtplib and +reraised as an smtplib.SMTPException. Notably, TimeoutError, +socket.gaierror (name resolution failure), and ConnectionRefusedError +and are not caught. + +The more generic OSError should be caught here instead. + +Resolves #1905 +--- + dnf/automatic/emitter.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/dnf/automatic/emitter.py b/dnf/automatic/emitter.py +index 4aea4b02..648f1a1d 100644 +--- a/dnf/automatic/emitter.py ++++ b/dnf/automatic/emitter.py +@@ -106,7 +106,7 @@ class EmailEmitter(Emitter): + smtp = smtplib.SMTP(self._conf.email_host, timeout=300) + smtp.sendmail(email_from, email_to, message.as_string()) + smtp.close() +- except smtplib.SMTPException as exc: ++ except OSError as exc: + msg = _("Failed to send an email via '%s': %s") % ( + self._conf.email_host, exc) + logger.error(msg) +-- +2.46.1 + diff --git a/SOURCES/0032-Allow-downloadonly-on-read-only-bootc-system.patch b/SOURCES/0032-Allow-downloadonly-on-read-only-bootc-system.patch new file mode 100644 index 0000000..49238fe --- /dev/null +++ b/SOURCES/0032-Allow-downloadonly-on-read-only-bootc-system.patch @@ -0,0 +1,40 @@ +From fdeb208b7b6522bd458142867683b1bf68cd355d Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Petr=20P=C3=ADsa=C5=99?= +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-61745 +Signed-off-by: Petr Písař +--- + 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 36cfa74b5..3dc08d616 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 + diff --git a/SOURCES/0033-automatic-Check-availability-of-config-file.patch b/SOURCES/0033-automatic-Check-availability-of-config-file.patch new file mode 100644 index 0000000..aa169ac --- /dev/null +++ b/SOURCES/0033-automatic-Check-availability-of-config-file.patch @@ -0,0 +1,81 @@ +From d8bd8174426a2d053b20acc7c2bcd83a572fc1d5 Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Thu, 17 Oct 2024 13:30:21 +0200 +Subject: [PATCH] automatic: Check availability of config file + +Upstream commit: 13ecc3921fb1566c2af7b80d5cb515d8fc4e5ec9 +RHEL issue: https://issues.redhat.com/browse/RHEL-49743 + +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-46030 +--- + dnf/automatic/main.py | 14 ++++++++++++-- + 1 file changed, 12 insertions(+), 2 deletions(-) + +diff --git a/dnf/automatic/main.py b/dnf/automatic/main.py +index caef627f..bb0bd493 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() +-- +2.47.0 + diff --git a/SOURCES/0034-automatic-emitters-send-error-messages.patch b/SOURCES/0034-automatic-emitters-send-error-messages.patch new file mode 100644 index 0000000..f17bbf7 --- /dev/null +++ b/SOURCES/0034-automatic-emitters-send-error-messages.patch @@ -0,0 +1,247 @@ +From 130e44556d49cf9fa4a0468bb73b56182691ab86 Mon Sep 17 00:00:00 2001 +From: derickdiaz +Date: Sun, 15 Oct 2023 10:58:03 -0500 +Subject: [PATCH 1/4] Added feature to allow emitters to invoke on dnf error + +--- + AUTHORS | 1 + + dnf/automatic/emitter.py | 22 ++++++++++++++++++---- + dnf/automatic/main.py | 5 +++++ + 3 files changed, 24 insertions(+), 4 deletions(-) + +diff --git a/AUTHORS b/AUTHORS +index 699a92c4..2e16c8ae 100644 +--- a/AUTHORS ++++ b/AUTHORS +@@ -68,6 +68,7 @@ DNF CONTRIBUTORS + Christopher Meng + Daniel Mach + Dave Johansen ++ Derick Diaz + Dylan Pindur + Eduard Cuba + Evan Goode +diff --git a/dnf/automatic/emitter.py b/dnf/automatic/emitter.py +index 648f1a1d..673da082 100644 +--- a/dnf/automatic/emitter.py ++++ b/dnf/automatic/emitter.py +@@ -33,6 +33,7 @@ APPLIED = _("The following updates have been applied on '%s':") + APPLIED_TIMESTAMP = _("Updates completed at %s") + AVAILABLE = _("The following updates are available on '%s':") + DOWNLOADED = _("The following updates were downloaded on '%s':") ++ERROR = _("An error has occured on: '%s'") + + logger = logging.getLogger('dnf') + +@@ -44,10 +45,15 @@ class Emitter(object): + self._downloaded = False + self._system_name = system_name + self._trans_msg = None ++ self._error = False ++ self._error_msg = None + + def _prepare_msg(self): + msg = [] +- if self._applied: ++ if self._error: ++ msg.append(ERROR % self._system_name) ++ msg.append(self._error_msg) ++ elif self._applied: + msg.append(APPLIED % self._system_name) + msg.append(self._available_msg) + msg.append(APPLIED_TIMESTAMP % time.strftime("%c")) +@@ -72,6 +78,10 @@ class Emitter(object): + assert self._available_msg + self._downloaded = True + ++ def notify_error(self, msg): ++ self._error = True ++ self._error_msg = msg ++ + + class EmailEmitter(Emitter): + def __init__(self, system_name, conf): +@@ -79,7 +89,9 @@ class EmailEmitter(Emitter): + self._conf = conf + + def _prepare_msg(self): +- if self._applied: ++ if self._error: ++ subj = _("An error has occured on '%s'.") % self._system_name ++ elif self._applied: + subj = _("Updates applied on '%s'.") % self._system_name + elif self._downloaded: + subj = _("Updates downloaded on '%s'.") % self._system_name +@@ -95,6 +107,8 @@ class EmailEmitter(Emitter): + message.set_charset('utf-8') + email_from = self._conf.email_from + email_to = self._conf.email_to ++ email_host = self._conf.email_host ++ email_port = self._conf.email_port + message['Date'] = email.utils.formatdate() + message['From'] = email_from + message['Subject'] = subj +@@ -103,12 +117,12 @@ class EmailEmitter(Emitter): + + # Send the email + try: +- smtp = smtplib.SMTP(self._conf.email_host, timeout=300) ++ smtp = smtplib.SMTP(email_host, email_port, timeout=300) + smtp.sendmail(email_from, email_to, message.as_string()) + smtp.close() + except OSError as exc: + msg = _("Failed to send an email via '%s': %s") % ( +- self._conf.email_host, exc) ++ email_host, exc) + logger.error(msg) + + +diff --git a/dnf/automatic/main.py b/dnf/automatic/main.py +index bb0bd493..8aca210a 100644 +--- a/dnf/automatic/main.py ++++ b/dnf/automatic/main.py +@@ -314,6 +314,7 @@ def main(args): + 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() +@@ -376,9 +377,13 @@ def main(args): + exit_code = os.waitstatus_to_exitcode(os.system(conf.commands.reboot_command)) + if exit_code != 0: + logger.error('Error: reboot command returned nonzero exit code: %d', exit_code) ++ emitters.notify_error('Error: reboot command returned nonzero exit code: %d', exit_code) ++ emitters.commit() + return 1 + except dnf.exceptions.Error as exc: + logger.error(_('Error: %s'), ucd(exc)) ++ emitters.notify_error(_('Error: %s') % str(exc)) ++ emitters.commit() + return 1 + return 0 + +-- +2.47.1 + + +From 1d18e7c6c03d6de084a6845db3a2dc50f02c8e4c Mon Sep 17 00:00:00 2001 +From: derickdiaz +Date: Thu, 19 Oct 2023 04:58:45 -0500 +Subject: [PATCH 2/4] Checks if emitter is null incase build_emitters throws a + ConfigError + +--- + dnf/automatic/main.py | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +diff --git a/dnf/automatic/main.py b/dnf/automatic/main.py +index 8aca210a..04926346 100644 +--- a/dnf/automatic/main.py ++++ b/dnf/automatic/main.py +@@ -382,8 +382,9 @@ def main(args): + return 1 + except dnf.exceptions.Error as exc: + logger.error(_('Error: %s'), ucd(exc)) +- emitters.notify_error(_('Error: %s') % str(exc)) +- emitters.commit() ++ if conf.emitters != None: ++ emitters.notify_error(_('Error: %s') % str(exc)) ++ emitters.commit() + return 1 + return 0 + +-- +2.47.1 + + +From 3e45752f0b74d2c4297da429a57b3e5148374195 Mon Sep 17 00:00:00 2001 +From: derickdiaz +Date: Thu, 19 Oct 2023 05:21:23 -0500 +Subject: [PATCH 3/4] Added 'send_error_messages' Boolean Option and updated + man docs + +Added option 'send_error_messages' + +Fixed Option String List + +Changed option to Boolean +--- + dnf/automatic/main.py | 8 +++----- + doc/automatic.rst | 5 +++++ + 2 files changed, 8 insertions(+), 5 deletions(-) + +diff --git a/dnf/automatic/main.py b/dnf/automatic/main.py +index 04926346..0a9d5041 100644 +--- a/dnf/automatic/main.py ++++ b/dnf/automatic/main.py +@@ -239,6 +239,7 @@ class EmittersConfig(Config): + libdnf.conf.VectorString(['email', 'stdio']))) + self.add_option('output_width', libdnf.conf.OptionNumberInt32(80)) + self.add_option('system_name', libdnf.conf.OptionString(socket.gethostname())) ++ self.add_option('send_error_messages', libdnf.conf.OptionBool(False)) + + + def gpgsigcheck(base, pkgs): +@@ -376,13 +377,10 @@ 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: +- logger.error('Error: reboot command returned nonzero exit code: %d', exit_code) +- emitters.notify_error('Error: reboot command returned nonzero exit code: %d', exit_code) +- emitters.commit() +- return 1 ++ 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 != None: ++ if conf.emitters.send_error_messages and emitters != None: + emitters.notify_error(_('Error: %s') % str(exc)) + emitters.commit() + return 1 +diff --git a/doc/automatic.rst b/doc/automatic.rst +index 329c2f46..2d514b78 100644 +--- a/doc/automatic.rst ++++ b/doc/automatic.rst +@@ -120,6 +120,11 @@ Choosing how the results should be reported. + + How the system is called in the reports. + ++``send_error_messages`` ++ boolean, default: False ++ ++ Invokes emitters when an errors occurs ++ + --------------------- + ``[command]`` section + --------------------- +-- +2.47.1 + + +From 33d52a8072c47b74603cf38ec807c26657799578 Mon Sep 17 00:00:00 2001 +From: derickdiaz +Date: Fri, 27 Oct 2023 12:00:21 -0500 +Subject: [PATCH 4/4] Fixed Typo in docs + +--- + doc/automatic.rst | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/doc/automatic.rst b/doc/automatic.rst +index 2d514b78..4f8eec68 100644 +--- a/doc/automatic.rst ++++ b/doc/automatic.rst +@@ -123,7 +123,7 @@ Choosing how the results should be reported. + ``send_error_messages`` + boolean, default: False + +- Invokes emitters when an errors occurs ++ Invokes emitters when an error occurs. + + --------------------- + ``[command]`` section +-- +2.47.1 + diff --git a/SOURCES/0035-automatic-Enhance-errors-reporting.patch b/SOURCES/0035-automatic-Enhance-errors-reporting.patch new file mode 100644 index 0000000..d58f88c --- /dev/null +++ b/SOURCES/0035-automatic-Enhance-errors-reporting.patch @@ -0,0 +1,60 @@ +From 2a1046f4dbf855902056e463a9d35612e93f786e Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Mon, 9 Dec 2024 13:42:44 +0100 +Subject: [PATCH] automatic: Enhance errors reporting + +Emitters must be initialized early to ensure they can report errors that +might occur in earlier stages of execution, before transaction +resolution. Also a broader range of exceptions must be caught to +ensure they are communicated to the user through the configured +emitters. +For example "No space left on the device" error can be raised during the +fill_sack() call. This patch should report it via configured emitters. + +Resolves: https://issues.redhat.com/browse/RHEL-61882 +--- + dnf/automatic/main.py | 9 +++++---- + 1 file changed, 5 insertions(+), 4 deletions(-) + +diff --git a/dnf/automatic/main.py b/dnf/automatic/main.py +index 0a9d5041..5ca4ad39 100644 +--- a/dnf/automatic/main.py ++++ b/dnf/automatic/main.py +@@ -311,11 +311,13 @@ 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 ++ emitters = build_emitters(conf) + with dnf.Base() as base: + cli = dnf.cli.Cli(base) + cli._read_conf_file() +@@ -349,7 +351,6 @@ def main(args): + return 0 + + lst = output.list_transaction(trans, total_width=80) +- emitters = build_emitters(conf) + emitters.notify_available(lst) + if not conf.commands.download_updates: + emitters.commit() +@@ -378,9 +379,9 @@ def main(args): + 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) +- except dnf.exceptions.Error as exc: ++ except Exception 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 + diff --git a/SOURCES/0036-Update-need_reboot-for-dnf-automatic.patch b/SOURCES/0036-Update-need_reboot-for-dnf-automatic.patch new file mode 100644 index 0000000..3771b71 --- /dev/null +++ b/SOURCES/0036-Update-need_reboot-for-dnf-automatic.patch @@ -0,0 +1,29 @@ +From ed14b8c8425c6fb6dbade3028ac0118086052b1b Mon Sep 17 00:00:00 2001 +From: Klaas Demter +Date: Tue, 15 Oct 2024 11:58:15 +0200 +Subject: [PATCH] Update need_reboot for dnf-automatic + +The need_reboot from dnf-automatic did not match NEED_REBOOT from +needs-restarting. +--- + dnf/base.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/dnf/base.py b/dnf/base.py +index 13222407..168207b5 100644 +--- a/dnf/base.py ++++ b/dnf/base.py +@@ -2832,8 +2832,8 @@ class Base(object): + + # List taken from DNF needs-restarting + need_reboot = frozenset(('kernel', 'kernel-rt', 'glibc', +- 'linux-firmware', 'systemd', 'dbus', +- 'dbus-broker', 'dbus-daemon')) ++ 'linux-firmware', 'systemd', 'dbus', ++ 'dbus-broker', 'dbus-daemon', 'microcode_ctl')) + changed_pkgs = self.transaction.install_set | self.transaction.remove_set + return any(pkg.name in need_reboot for pkg in changed_pkgs) + +-- +2.47.1 + diff --git a/SOURCES/0037-automatic-Fix-incorrect-Error-class-instantiation.patch b/SOURCES/0037-automatic-Fix-incorrect-Error-class-instantiation.patch new file mode 100644 index 0000000..9d18a14 --- /dev/null +++ b/SOURCES/0037-automatic-Fix-incorrect-Error-class-instantiation.patch @@ -0,0 +1,27 @@ +From c57c26e0cf8a3bba061f5ddf619c40f36a94aab9 Mon Sep 17 00:00:00 2001 +From: Marek Blaha +Date: Thu, 12 Dec 2024 08:09:48 +0100 +Subject: [PATCH] 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 5ca4ad39..51057dfb 100644 +--- a/dnf/automatic/main.py ++++ b/dnf/automatic/main.py +@@ -378,7 +378,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 Exception 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 + diff --git a/SOURCES/0038-doc-disableexcludepkgs-all-doesn-t-affect-just-file.patch b/SOURCES/0038-doc-disableexcludepkgs-all-doesn-t-affect-just-file.patch new file mode 100644 index 0000000..9fbdb78 --- /dev/null +++ b/SOURCES/0038-doc-disableexcludepkgs-all-doesn-t-affect-just-file.patch @@ -0,0 +1,39 @@ +From be3f218b6964116bc2948a9b7b93247322dc979f Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= +Date: Thu, 12 Dec 2024 12:59:03 +0100 +Subject: [PATCH] doc: `--disableexcludepkgs=all` doesn't affect just file + configuration + +The option `--disableexcludepkgs=all` disables all configuration +includes and excludes including packages specified on the commandline +via `--exclude=`, `-x=`, the deprecated `--excludepkgs=` and file +configuration via `--excludepkgs=`, `--includepkgs=`. +--- + doc/command_ref.rst | 9 +++++---- + 1 file changed, 5 insertions(+), 4 deletions(-) + +diff --git a/doc/command_ref.rst b/doc/command_ref.rst +index 36817c00..75ded68c 100644 +--- a/doc/command_ref.rst ++++ b/doc/command_ref.rst +@@ -165,12 +165,13 @@ Options + .. _disableexcludes-label: + + ``--disableexcludes=[all|main|], --disableexcludepkgs=[all|main|]`` ++ Disable ``excludepkgs`` and ``includepkgs`` configuration options. Takes one of the following three options: + +- Disable the configuration file excludes. Takes one of the following three options: ++ * ``all``, disables all ``excludepkgs`` and ``includepkgs`` configurations ++ * ``main``, disables ``excludepkgs`` and ``includepkgs`` defined in the ``[main]`` section ++ * ``repoid``, disables ``excludepkgs`` and ``includepkgs`` defined for the given repository + +- * ``all``, disables all configuration file excludes +- * ``main``, disables excludes defined in the ``[main]`` section +- * ``repoid``, disables excludes defined for the given repository ++ Note that the \-\ :ref:`-exclude ` option appends to the ``[main]`` ``excludepkgs`` configuration and therefore is disabled when ``main`` or ``all`` is specified. + + ``--disable, --set-disabled`` + Disable specified repositories (automatically saves). The option has to be used together with the +-- +2.46.2 + diff --git a/SOURCES/0039-Add-support-for-transient.patch b/SOURCES/0039-Add-support-for-transient.patch new file mode 100644 index 0000000..b58a76b --- /dev/null +++ b/SOURCES/0039-Add-support-for-transient.patch @@ -0,0 +1,181 @@ +From e236290f4aec12ad9b2e5cdfa48ec8e3172bb89c Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Thu, 7 Nov 2024 02:31:25 +0000 +Subject: [PATCH 39/44] 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-70917 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 40 ++++++++++++++++++++++++++++++--------- + dnf/cli/option_parser.py | 3 +++ + dnf/conf/config.py | 2 +- + dnf/util.py | 41 +++++++++++++++++++++++++++++----------- + 4 files changed, 65 insertions(+), 21 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 3dc08d616..33fe20aab 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 41ff16451..66e69cc98 100644 +--- a/dnf/cli/option_parser.py ++++ b/dnf/cli/option_parser.py +@@ -320,6 +320,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 32516d1a8..ed6daeb2d 100644 +--- a/dnf/conf/config.py ++++ b/dnf/conf/config.py +@@ -342,7 +342,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 e8f587a03..f22e3901b 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 +@@ -634,14 +635,32 @@ def _name_unset_wrapper(input_name): + + + def _is_bootc_host(): +- """Returns true is the system is managed as an immutable container, +- false otherwise. If msg is True, a warning message is displayed +- for the user. +- """ +- ostree_booted = '/run/ostree-booted' +- usr = '/usr/' +- # Check if usr is writtable and we are in a running ostree system. +- # We want this code to return true only when the system is in locked state. If someone ran +- # bootc overlay or ostree admin unlock we would want normal DNF path to be ran as it will be +- # temporary changes (until reboot). +- return os.path.isfile(ostree_booted) and not os.access(usr, os.W_OK) ++ """Returns true is the system is managed as an immutable container, false ++ otherwise.""" ++ ostree_booted = "/run/ostree-booted" ++ return os.path.isfile(ostree_booted) ++ ++ ++def _is_bootc_unlocked(): ++ """Check whether /usr is writeable, e.g. if we are in a normal mutable ++ system or if we are in a bootc after `bootc usr-overlay` or `ostree admin ++ unlock` was run.""" ++ usr = "/usr" ++ return os.access(usr, os.W_OK) ++ ++ ++def _bootc_unlock(): ++ """Set up a writeable overlay on bootc systems.""" ++ ++ if _is_bootc_unlocked(): ++ return ++ ++ unlock_command = ["bootc", "usr-overlay"] ++ ++ try: ++ completed_process = subprocess.run(unlock_command, text=True) ++ completed_process.check_returncode() ++ except FileNotFoundError: ++ raise dnf.exceptions.Error(_("bootc command not found. Is this a bootc system?")) ++ except subprocess.CalledProcessError: ++ raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) +-- +2.48.1 + diff --git a/SOURCES/0040-bootc-Document-transient-and-persistence.patch b/SOURCES/0040-bootc-Document-transient-and-persistence.patch new file mode 100644 index 0000000..89ff90b --- /dev/null +++ b/SOURCES/0040-bootc-Document-transient-and-persistence.patch @@ -0,0 +1,74 @@ +From ec5cbd19adc6f384923e95d42357f23696b3c950 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 17 Dec 2024 18:58:32 +0000 +Subject: [PATCH 40/44] 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-70917 +Signed-off-by: Petr Písař +--- + doc/command_ref.rst | 9 +++++++++ + doc/conf_ref.rst | 11 +++++++++++ + 2 files changed, 20 insertions(+) + +diff --git a/doc/command_ref.rst b/doc/command_ref.rst +index 75ded68cd..8b55d5a76 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`` +@@ -708,6 +713,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 42a9a37e5..a34e355b6 100644 +--- a/doc/conf_ref.rst ++++ b/doc/conf_ref.rst +@@ -430,6 +430,17 @@ configuration file by your distribution to override the DNF defaults. + + Directory where DNF stores its persistent data between runs. Default is ``"/var/lib/dnf"``. + ++.. _persistence-label: ++ ++``persistence`` ++ :ref:`string ` ++ ++ Whether changes should persist across system reboots. Default is ``auto``. Passing :ref:`--transient ` will override this setting to ``transient``. Valid values are: ++ ++ * ``auto``: Changes will persist across reboots, unless the target is a running bootc system and the system is already in an unlocked state (i.e. ``/usr`` is writable). ++ * ``transient``: Changes will be lost on the next reboot. Only applicable on bootc systems. Beware that changes to ``/etc`` and ``/var`` will persist, depending on the configuration of your bootc system. See also https://containers.github.io/bootc/man/bootc-usr-overlay.html. ++ * ``persist``: Changes will persist across reboots. ++ + .. _pluginconfpath-label: + + ``pluginconfpath`` +-- +2.48.1 + diff --git a/SOURCES/0041-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch b/SOURCES/0041-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch new file mode 100644 index 0000000..45c2c80 --- /dev/null +++ b/SOURCES/0041-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch @@ -0,0 +1,166 @@ +From 5a5572b8adc335075cdae321ad1447e1b8bd8760 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Wed, 15 Jan 2025 21:43:58 +0000 +Subject: [PATCH 41/44] 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-70917 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 13 ++++---- + dnf/util.py | 80 +++++++++++++++++++++++++++++++++++--------------- + 2 files changed, 65 insertions(+), 28 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index 33fe20aab..e7ca86ba8 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 f22e3901b..994fddafc 100644 +--- a/dnf/util.py ++++ b/dnf/util.py +@@ -634,33 +634,67 @@ def _name_unset_wrapper(input_name): + return input_name if input_name else _("") + + +-def _is_bootc_host(): +- """Returns true is the system is managed as an immutable container, false +- otherwise.""" +- ostree_booted = "/run/ostree-booted" +- return os.path.isfile(ostree_booted) ++class _Bootc: ++ usr = "/usr" + ++ def __init__(self): ++ if not self.is_bootc_host(): ++ raise RuntimeError(_("Not running on a bootc system.")) + +-def _is_bootc_unlocked(): +- """Check whether /usr is writeable, e.g. if we are in a normal mutable +- system or if we are in a bootc after `bootc usr-overlay` or `ostree admin +- unlock` was run.""" +- usr = "/usr" +- return os.access(usr, os.W_OK) ++ import gi ++ self._gi = gi + ++ gi.require_version("OSTree", "1.0") ++ from gi.repository import OSTree + +-def _bootc_unlock(): +- """Set up a writeable overlay on bootc systems.""" ++ self._OSTree = OSTree + +- if _is_bootc_unlocked(): +- return ++ self._sysroot = self._OSTree.Sysroot.new_default() ++ assert self._sysroot.load(None) + +- unlock_command = ["bootc", "usr-overlay"] ++ self._booted_deployment = self._sysroot.require_booted_deployment() ++ assert self._booted_deployment is not None + +- try: +- completed_process = subprocess.run(unlock_command, text=True) +- completed_process.check_returncode() +- except FileNotFoundError: +- raise dnf.exceptions.Error(_("bootc command not found. Is this a bootc system?")) +- except subprocess.CalledProcessError: +- raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) ++ @staticmethod ++ def is_bootc_host(): ++ """Returns true is the system is managed as an immutable container, false ++ otherwise.""" ++ ostree_booted = "/run/ostree-booted" ++ return os.path.isfile(ostree_booted) ++ ++ def _get_unlocked_state(self): ++ return self._booted_deployment.get_unlocked() ++ ++ def is_unlocked(self): ++ return self._get_unlocked_state() != self._OSTree.DeploymentUnlockedState.NONE ++ ++ def unlock_and_prepare(self): ++ """Set up a writeable overlay on bootc systems.""" ++ ++ bootc_unlocked_state = self._get_unlocked_state() ++ ++ valid_bootc_unlocked_states = ( ++ self._OSTree.DeploymentUnlockedState.NONE, ++ self._OSTree.DeploymentUnlockedState.DEVELOPMENT, ++ # self._OSTree.DeploymentUnlockedState.TRANSIENT, ++ # self._OSTree.DeploymentUnlockedState.HOTFIX, ++ ) ++ ++ if bootc_unlocked_state not in valid_bootc_unlocked_states: ++ raise ValueError(_("Unhandled bootc unlocked state: %s") % bootc_unlocked_state.value_nick) ++ ++ if bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.DEVELOPMENT: ++ # System is already unlocked. ++ pass ++ elif bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.NONE: ++ unlock_command = ["ostree", "admin", "unlock"] ++ ++ try: ++ completed_process = subprocess.run(unlock_command, text=True) ++ completed_process.check_returncode() ++ except FileNotFoundError: ++ raise dnf.exceptions.Error(_("ostree command not found. Is this a bootc system?")) ++ except subprocess.CalledProcessError: ++ raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) ++ ++ assert os.access(self.usr, os.W_OK) +-- +2.48.1 + diff --git a/SOURCES/0042-bootc-Re-locking-use-ostree-admin-unlock-transient.patch b/SOURCES/0042-bootc-Re-locking-use-ostree-admin-unlock-transient.patch new file mode 100644 index 0000000..0d9d5c3 --- /dev/null +++ b/SOURCES/0042-bootc-Re-locking-use-ostree-admin-unlock-transient.patch @@ -0,0 +1,237 @@ +From 7e0180ad97a677e6701031f13069c20beec1d8ff Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Thu, 16 Jan 2025 14:06:26 -0500 +Subject: [PATCH 42/44] 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-70917 +Signed-off-by: Petr Písař +--- + dnf/cli/cli.py | 33 ++++++++++--------- + dnf/util.py | 86 ++++++++++++++++++++++++++++++++++++++------------ + 2 files changed, 83 insertions(+), 36 deletions(-) + +diff --git a/dnf/cli/cli.py b/dnf/cli/cli.py +index e7ca86ba8..23170a82b 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 994fddafc..0161f80d8 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 +@@ -634,11 +635,12 @@ def _name_unset_wrapper(input_name): + return input_name if input_name else _("") + + +-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 +@@ -656,45 +658,87 @@ class _Bootc: + assert self._booted_deployment is not None + + @staticmethod +- def is_bootc_host(): ++ def is_bootc_system(): + """Returns true is the system is managed as an immutable container, false + otherwise.""" + ostree_booted = "/run/ostree-booted" + return os.path.isfile(ostree_booted) + ++ @classmethod ++ def is_writable(cls): ++ """Returns true if and only if /usr is writable.""" ++ return os.access(cls.usr, os.W_OK) ++ + def _get_unlocked_state(self): + return self._booted_deployment.get_unlocked() + +- def is_unlocked(self): +- return self._get_unlocked_state() != self._OSTree.DeploymentUnlockedState.NONE ++ def is_unlocked_transient(self): ++ """Returns true if and only if the bootc system is unlocked in a ++ transient state, i.e. a overlayfs is mounted as read-only on /usr. ++ Changes can be made to the overlayfs by remounting /usr as ++ read/write in a private mount namespace.""" ++ return self._get_unlocked_state() == self._OSTree.DeploymentUnlockedState.TRANSIENT ++ ++ @classmethod ++ def _set_up_mountns(cls): ++ # os.unshare is only available in Python >= 3.12. ++ ++ # Access symbols in libraries loaded by the Python interpreter, ++ # which will include libc. See https://bugs.python.org/issue34592. ++ libc = ctypes.CDLL(None) ++ if libc.unshare(cls.CLONE_NEWNS) != 0: ++ raise OSError("Failed to unshare mount namespace") ++ ++ mount_command = ["mount", "--options-source=disable", "-o", "remount,rw", cls.usr] ++ try: ++ completed_process = subprocess.run(mount_command, text=True) ++ completed_process.check_returncode() ++ except FileNotFoundError: ++ raise dnf.exceptions.Error(_("%s: command not found.") % mount_command[0]) ++ except subprocess.CalledProcessError: ++ raise dnf.exceptions.Error(_("Failed to mount %s as read/write: %s", cls.usr, completed_process.stderr)) + +- def unlock_and_prepare(self): +- """Set up a writeable overlay on bootc systems.""" ++ @staticmethod ++ def _unlock(): ++ unlock_command = ["ostree", "admin", "unlock", "--transient"] ++ try: ++ completed_process = subprocess.run(unlock_command, text=True) ++ completed_process.check_returncode() ++ except FileNotFoundError: ++ raise dnf.exceptions.Error(_("%s: command not found. Is this a bootc system?") % unlock_command[0]) ++ except subprocess.CalledProcessError: ++ raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) ++ ++ def make_writable(self): ++ """Set up a writable overlay on bootc systems.""" + + bootc_unlocked_state = self._get_unlocked_state() + + valid_bootc_unlocked_states = ( + self._OSTree.DeploymentUnlockedState.NONE, + self._OSTree.DeploymentUnlockedState.DEVELOPMENT, +- # self._OSTree.DeploymentUnlockedState.TRANSIENT, +- # self._OSTree.DeploymentUnlockedState.HOTFIX, ++ self._OSTree.DeploymentUnlockedState.TRANSIENT, ++ self._OSTree.DeploymentUnlockedState.HOTFIX, + ) +- + if bootc_unlocked_state not in valid_bootc_unlocked_states: + raise ValueError(_("Unhandled bootc unlocked state: %s") % bootc_unlocked_state.value_nick) + +- if bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.DEVELOPMENT: +- # System is already unlocked. ++ writable_unlocked_states = ( ++ self._OSTree.DeploymentUnlockedState.DEVELOPMENT, ++ self._OSTree.DeploymentUnlockedState.HOTFIX, ++ ) ++ if bootc_unlocked_state in writable_unlocked_states: ++ # System is already unlocked in development mode, and usr is ++ # already mounted read/write. + pass + elif bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.NONE: +- unlock_command = ["ostree", "admin", "unlock"] +- +- try: +- completed_process = subprocess.run(unlock_command, text=True) +- completed_process.check_returncode() +- except FileNotFoundError: +- raise dnf.exceptions.Error(_("ostree command not found. Is this a bootc system?")) +- except subprocess.CalledProcessError: +- raise dnf.exceptions.Error(_("Failed to unlock system: %s", completed_process.stderr)) ++ # System is not unlocked. Unlock it in transient mode, then set up ++ # a mount namespace for DNF. ++ self._unlock() ++ self._set_up_mountns() ++ elif bootc_unlocked_state == self._OSTree.DeploymentUnlockedState.TRANSIENT: ++ # System is unlocked in transient mode, so usr is mounted ++ # read-only. Set up a mount namespace for DNF. ++ self._set_up_mountns() + + assert os.access(self.usr, os.W_OK) +-- +2.48.1 + diff --git a/SOURCES/0043-spec-Add-dnf-bootc-subpackage.patch b/SOURCES/0043-spec-Add-dnf-bootc-subpackage.patch new file mode 100644 index 0000000..0122c34 --- /dev/null +++ b/SOURCES/0043-spec-Add-dnf-bootc-subpackage.patch @@ -0,0 +1,56 @@ +From 9c2a1a6a36d748d059140bf6ee9113d5e9641477 Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Tue, 28 Jan 2025 11:27:00 -0500 +Subject: [PATCH 43/44] 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-70917 +Signed-off-by: Petr Písař +--- + dnf.spec | 14 ++++++++++++++ + 1 file changed, 14 insertions(+) + +diff --git a/dnf.spec b/dnf.spec +index e9abd9041..b60f2692b 100644 +--- a/dnf.spec ++++ b/dnf.spec +@@ -180,6 +180,17 @@ Requires: %{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 +@@ -358,6 +369,9 @@ popd + %{_unitdir}/%{name}-automatic-install.timer + %{python3_sitelib}/%{name}/automatic/ + ++%files bootc ++# bootc subpackage does not include any files ++ + %changelog + * Fri Sep 09 2022 Jaroslav Rohel - 4.14.0-1 + - doc: Describe how gpg keys are stored for `repo_ggpcheck` (RhBug:2020678) +-- +2.48.1 + diff --git a/SOURCES/0044-Require-libdnf-0.74.0-with-persistence-option.patch b/SOURCES/0044-Require-libdnf-0.74.0-with-persistence-option.patch new file mode 100644 index 0000000..782af9f --- /dev/null +++ b/SOURCES/0044-Require-libdnf-0.74.0-with-persistence-option.patch @@ -0,0 +1,45 @@ +From 19754a7c80e1c2232c370f394ad0d36f5713bf1e Mon Sep 17 00:00:00 2001 +From: Evan Goode +Date: Wed, 5 Feb 2025 10:35:08 -0500 +Subject: [PATCH 44/44] 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-9 libdnf version. + +Resolves: https://issues.redhat.com/browse/RHEL-70917 +Signed-off-by: Petr Písař +--- + dnf.spec | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/dnf.spec b/dnf.spec +index b60f2692b..313a3cb2a 100644 +--- a/dnf.spec ++++ b/dnf.spec +@@ -2,7 +2,7 @@ + %define __cmake_in_source_build 1 + + # default dependencies +-%global hawkey_version 0.66.0 ++%global hawkey_version 0.74.0 + %global libcomps_version 0.1.8 + %global libmodulemd_version 2.9.3 + %global rpm_version 4.14.0 +@@ -21,6 +21,10 @@ + %global rpm_version 4.11.3-25.el7.centos.1 + %endif + ++%if 0%{?rhel} == 9 ++ %global hawkey_version 0.69.0-13 ++%endif ++ + # override dependencies for fedora 26 + %if 0%{?fedora} == 26 + %global rpm_version 4.13.0.1-7 +-- +2.48.1 + diff --git a/SPECS/dnf.spec b/SPECS/dnf.spec index 02d6ec9..d02d544 100644 --- a/SPECS/dnf.spec +++ b/SPECS/dnf.spec @@ -2,7 +2,7 @@ %define __cmake_in_source_build 1 # default dependencies -%global hawkey_version 0.66.0 +%global hawkey_version 0.74.0 %global libcomps_version 0.1.8 %global libmodulemd_version 2.9.3 %global rpm_version 4.14.0 @@ -21,6 +21,10 @@ %global rpm_version 4.11.3-25.el7.centos.1 %endif +%if 0%{?rhel} == 9 + %global hawkey_version 0.69.0-13 +%endif + # override dependencies for fedora 26 %if 0%{?fedora} == 26 %global rpm_version 4.13.0.1-7 @@ -69,7 +73,7 @@ It supports RPMs, modules and comps groups & environments. Name: dnf Version: 4.14.0 -Release: 17%{?dist} +Release: 25%{?dist} Summary: %{pkg_summary} # For a breakdown of the licensing, see PACKAGE-LICENSING License: GPLv2+ @@ -101,6 +105,24 @@ Patch23: 0023-Limit-queries-to-nevra-forms-when-provided-by-comman.patch Patch24: 0024-doc-Remove-provide-of-spec-definition-for-repoquery-.patch Patch25: 0025-man-Improve-upgrade-minimal-command-docs-RHEL-6417.patch Patch26: 0026-doc-Makecache-with-timer-tries-only-one-mirror.patch +Patch27: 0027-Add-detection-for-ostree-based-systems-and-warn-user.patch +Patch28: 0028-Update-ostree-bootc-host-system-check.patch +Patch29: 0029-Update-bootc-hosts-message-to-point-to-bootc-help.patch +Patch30: 0030-Allow-installroot-on-read-only-bootc-system.patch +Patch31: 0031-smtplib-catch-OSError-not-SMTPException.patch +Patch32: 0032-Allow-downloadonly-on-read-only-bootc-system.patch +Patch33: 0033-automatic-Check-availability-of-config-file.patch +Patch34: 0034-automatic-emitters-send-error-messages.patch +Patch35: 0035-automatic-Enhance-errors-reporting.patch +Patch36: 0036-Update-need_reboot-for-dnf-automatic.patch +Patch37: 0037-automatic-Fix-incorrect-Error-class-instantiation.patch +PAtch38: 0038-doc-disableexcludepkgs-all-doesn-t-affect-just-file.patch +Patch39: 0039-Add-support-for-transient.patch +Patch40: 0040-bootc-Document-transient-and-persistence.patch +Patch41: 0041-bootc-Use-ostree-GObject-API-to-get-deployment-statu.patch +Patch42: 0042-bootc-Re-locking-use-ostree-admin-unlock-transient.patch +Patch43: 0043-spec-Add-dnf-bootc-subpackage.patch +Patch44: 0044-Require-libdnf-0.74.0-with-persistence-option.patch BuildArch: noarch BuildRequires: cmake @@ -210,6 +232,17 @@ Requires: %{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 @@ -388,7 +421,39 @@ popd %{_unitdir}/%{name}-automatic-install.timer %{python3_sitelib}/%{name}/automatic/ +%files bootc +# bootc subpackage does not include any files + %changelog +* Tue Feb 04 2025 Petr Pisar - 4.14.0-25 +- Add support for transient transactions (RHEL-70917) + +* Mon Jan 13 2025 Ales Matej - 4.14.0-24 +- doc: `--disableexcludepkgs=all` doesn't affect just file configuration + (RHEL-28779) + +* Thu Dec 12 2024 Marek Blaha - 4.14.0-23 +- automatic: Update need_reboot to match needs-restarting (RHEL-62830) + +* Wed Dec 11 2024 Marek Blaha - 4.14.0-22 +- automatic: Added feature to allow emitters to invoke on dnf error + (RHEL-45505, RHEL-61882) + +* Mon Oct 21 2024 Marek Blaha - 4.14.0-21 +- automatic: Check availability of config file (RHEL-49743) + +* Thu Oct 10 2024 Petr Pisar - 4.14.0-20 +- Allow "dnf install --downloadonly" on locked OSTree and bootc systems + (RHEL-61745) + +* Mon Oct 07 2024 Marek Blaha - 4.14.0-19 +- Catch more specific OSError instead of SMTPException in dnf-automatic email + emitter (RHEL-49743) + +* Tue Oct 01 2024 Petr Pisar - 4.14.0-18 +- More specific error message on a locked OSTree system or a bootc system + without a usr-overlay (RHEL-49670) + * Tue Aug 06 2024 Petr Pisar - 4.14.0-17 - Revert more specific error message on a locked OSTree system or a bootc system without a usr-overlay (RHEL-49670)