diff --git a/dnf5-plugins/CMakeLists.txt b/dnf5-plugins/CMakeLists.txt
index 4c9571d68d..0704d83e35 100644
--- a/dnf5-plugins/CMakeLists.txt
+++ b/dnf5-plugins/CMakeLists.txt
@@ -4,6 +4,7 @@ endif()
include_directories("${PROJECT_SOURCE_DIR}/dnf5/include/")
+add_subdirectory("automatic_plugin")
add_subdirectory("builddep_plugin")
add_subdirectory("changelog_plugin")
add_subdirectory("copr_plugin")
diff --git a/dnf5-plugins/automatic_plugin/CMakeLists.txt b/dnf5-plugins/automatic_plugin/CMakeLists.txt
new file mode 100644
index 0000000000..f931ba9e22
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/CMakeLists.txt
@@ -0,0 +1,17 @@
+# set gettext domain for translations
+add_definitions(-DGETTEXT_DOMAIN=\"dnf5_cmd_automatic\")
+
+file(GLOB_RECURSE AUTOMATIC_SOURCES *.cpp)
+add_library(automatic_cmd_plugin MODULE ${AUTOMATIC_SOURCES})
+
+# disable the 'lib' prefix in order to create automatic_cmd_plugin.so
+set_target_properties(automatic_cmd_plugin PROPERTIES PREFIX "")
+
+target_link_libraries(automatic_cmd_plugin PRIVATE libdnf5 libdnf5-cli)
+target_link_libraries(automatic_cmd_plugin PRIVATE dnf5)
+
+install(TARGETS automatic_cmd_plugin LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}/dnf5/plugins/)
+install(DIRECTORY "config/etc/" DESTINATION "${CMAKE_INSTALL_FULL_SYSCONFDIR}")
+install(DIRECTORY "config/usr/" DESTINATION "${CMAKE_INSTALL_PREFIX}")
+
+install(PROGRAMS bin/dnf-automatic TYPE BIN)
diff --git a/dnf5-plugins/automatic_plugin/automatic.cpp b/dnf5-plugins/automatic_plugin/automatic.cpp
new file mode 100644
index 0000000000..cbba38a2d0
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/automatic.cpp
@@ -0,0 +1,320 @@
+/*
+Copyright Contributors to the libdnf project.
+
+This file is part of libdnf: https://github.com/rpm-software-management/libdnf/
+
+Libdnf is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+Libdnf is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with libdnf. If not, see .
+*/
+
+#include "automatic.hpp"
+
+#include "download_callbacks_simple.hpp"
+#include "emitters.hpp"
+#include "transaction_callbacks_simple.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+namespace {
+
+/// Sleep for random number of seconds in interval <0, max_value>
+static void random_wait(int max_value) {
+ std::random_device rd;
+ std::mt19937 rng(rd());
+ std::uniform_int_distribution distribution(0, max_value);
+
+ sleep(distribution(rng));
+}
+
+static bool reboot_needed(const libdnf5::base::Transaction & transaction) {
+ static const std::set need_reboot = {
+ "kernel", "kernel-rt", "glibc", "linux-firmware", "systemd", "dbus", "dbus-broker", "dbus-daemon"};
+ for (const auto & pkg : transaction.get_transaction_packages()) {
+ if (need_reboot.find(pkg.get_package().get_name()) != need_reboot.end()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+} // namespace
+
+
+namespace dnf5 {
+
+void AutomaticCommand::set_parent_command() {
+ auto * arg_parser_parent_cmd = get_session().get_argument_parser().get_root_command();
+ auto * arg_parser_this_cmd = get_argument_parser_command();
+ arg_parser_parent_cmd->register_command(arg_parser_this_cmd);
+}
+
+void AutomaticCommand::set_argument_parser() {
+ auto & cmd = *get_argument_parser_command();
+ cmd.set_long_description(
+ _("An alternative CLI to 'dnf upgrade' suitable to be executed automatically and regularly."));
+
+ auto & context = get_context();
+ auto & parser = context.get_argument_parser();
+
+ auto arg_config_file = parser.add_new_positional_arg(
+ "config_path", libdnf5::cli::ArgumentParser::PositionalArg::OPTIONAL, nullptr, nullptr);
+ arg_config_file->set_description(_("Path to dnf5-automatic config file."));
+ arg_config_file->set_parse_hook_func(
+ [&]([[maybe_unused]] libdnf5::cli::ArgumentParser::PositionalArg * arg, int argc, const char * const argv[]) {
+ if (argc > 0) {
+ config_automatic.automatic_config_file_path.set(argv[0]);
+ }
+ return true;
+ });
+ cmd.register_positional_arg(arg_config_file);
+
+ timer = std::make_unique(
+ *this, "timer", '\0', _("Apply random delay before execution."), false);
+ auto downloadupdates = std::make_unique(
+ *this,
+ "downloadupdates",
+ '\0',
+ _("Automatically download updated packages"),
+ false,
+ &config_automatic.config_commands.download_updates);
+ auto nodownloadupdates = std::make_unique(
+ *this,
+ "no-downloadupdates",
+ '\0',
+ _("Do not automatically download updated packages"),
+ true,
+ &config_automatic.config_commands.download_updates);
+ // TODO(mblaha): there is inconsistency in naming between
+ // --(no-)installupdates CLI option which overrides `apply_updates` config option
+ auto installupdates = std::make_unique(
+ *this,
+ "installupdates",
+ '\0',
+ _("Automatically install downloaded updates"),
+ false,
+ &config_automatic.config_commands.apply_updates);
+ auto noinstallupdates = std::make_unique(
+ *this,
+ "no-installupdates",
+ '\0',
+ _("Do not automatically install downloaded updates"),
+ true,
+ &config_automatic.config_commands.apply_updates);
+
+ // downloadupdates and no-downloadupdates options conflict with each other.
+ {
+ auto conflicts =
+ parser.add_conflict_args_group(std::make_unique>());
+ conflicts->push_back(nodownloadupdates->arg);
+ downloadupdates->arg->set_conflict_arguments(conflicts);
+ }
+ // installupdates and no-installupdates options conflict with each other.
+ // installupdates and no-downloadupdates options conflict with each other.
+ {
+ auto conflicts =
+ parser.add_conflict_args_group(std::make_unique>());
+ conflicts->push_back(downloadupdates->arg);
+ conflicts->push_back(installupdates->arg);
+ nodownloadupdates->arg->set_conflict_arguments(conflicts);
+ }
+ {
+ auto conflicts =
+ parser.add_conflict_args_group(std::make_unique>());
+ conflicts->push_back(noinstallupdates->arg);
+ conflicts->push_back(nodownloadupdates->arg);
+ installupdates->arg->set_conflict_arguments(conflicts);
+ }
+ {
+ auto conflicts =
+ parser.add_conflict_args_group(std::make_unique>());
+ conflicts->push_back(installupdates->arg);
+ noinstallupdates->arg->set_conflict_arguments(conflicts);
+ }
+}
+
+void AutomaticCommand::pre_configure() {
+ auto & context = get_context();
+ auto & base = context.base;
+
+ // TODO wait for network
+
+ auto random_sleep = config_automatic.config_commands.random_sleep.get_value();
+ if (timer->get_value() && random_sleep > 0) {
+ random_wait(random_sleep);
+ }
+
+ auto download_callbacks_uptr = std::make_unique(output_stream);
+ base.set_download_callbacks(std::move(download_callbacks_uptr));
+ download_callbacks_set = true;
+
+ libdnf5::ConfigParser parser;
+ parser.read(config_automatic.automatic_config_file_path.get_value());
+ base.get_config().load_from_parser(
+ parser, "base", *base.get_vars(), *base.get_logger(), libdnf5::Option::Priority::AUTOMATICCONFIG);
+ config_automatic.load_from_parser(parser, *base.get_vars(), *base.get_logger());
+
+ context.set_output_stream(output_stream);
+}
+
+void AutomaticCommand::configure() {
+ auto & context = get_context();
+ context.set_load_system_repo(true);
+ context.update_repo_metadata_from_advisory_options(
+ {}, config_automatic.config_commands.upgrade_type.get_value() == "security", false, false, false, {}, {}, {});
+ context.set_load_available_repos(Context::LoadAvailableRepos::ENABLED);
+}
+
+void AutomaticCommand::run() {
+ auto & context = get_context();
+ auto & base = context.base;
+ bool success = true;
+
+ // setup upgrade transaction goal
+ auto settings = libdnf5::GoalJobSettings();
+
+ // TODO(mblaha): Use advisory_query_from_cli_input from dnf5/commands/advisory_shared.hpp?
+ if (config_automatic.config_commands.upgrade_type.get_value() == "security") {
+ auto advisories = libdnf5::advisory::AdvisoryQuery(base);
+ advisories.filter_type("security");
+ settings.set_advisory_filter(advisories);
+ // TODO(mblaha): set also minimal=true?
+ }
+ libdnf5::Goal goal(base);
+ goal.add_rpm_upgrade(settings);
+
+ auto transaction = goal.resolve();
+
+ // print resolve logs and the transaction table to the output stream
+ {
+ output_stream << std::endl << _("Resolved transaction:") << std::endl;
+ libdnf5::cli::output::print_resolve_logs(transaction, output_stream);
+
+ if (!transaction.empty()) {
+ libdnf5::cli::output::TransactionSummary summary;
+ auto tt = libdnf5::cli::output::create_transaction_table(transaction, summary);
+ scols_table_enable_colors(*tt, false);
+ scols_table_set_termwidth(*tt, 80);
+ char * tt_string = nullptr;
+ scols_print_table_to_string(*tt, &tt_string);
+ output_stream << tt_string << std::endl;
+ free(tt_string);
+
+ summary.print(output_stream);
+ }
+ }
+
+ bool do_reboot = false;
+ if (!transaction.empty()) {
+ auto download_updates = config_automatic.config_commands.download_updates.get_value();
+ auto apply_updates = config_automatic.config_commands.apply_updates.get_value();
+ if (download_updates || apply_updates) {
+ output_stream << _("Downloading packages:") << std::endl;
+ try {
+ transaction.download();
+ } catch (const libdnf5::repo::PackageDownloadError & e) {
+ success = false;
+ } catch (const libdnf5::repo::RepoCacheonlyError & e) {
+ success = false;
+ output_stream << e.what() << std::endl;
+ }
+ if (success) {
+ output_stream << _("Packages downloaded.") << std::endl;
+ // TODO: handle downloadonly config option
+ if (apply_updates) {
+ output_stream << _("Running transaction:") << std::endl;
+ transaction.set_callbacks(std::make_unique(output_stream));
+ transaction.set_description(context.get_cmdline());
+ auto comment = context.get_comment();
+ if (comment) {
+ transaction.set_comment(comment);
+ }
+ auto result = transaction.run();
+ if (result == libdnf5::base::Transaction::TransactionRunResult::SUCCESS) {
+ auto reboot = config_automatic.config_commands.reboot.get_value();
+ if (reboot == "when-changed" || (reboot == "when-needed" and reboot_needed(transaction))) {
+ do_reboot = true;
+ }
+ output_stream << _("Transaction finished.") << std::endl;
+ } else {
+ output_stream << _("Transaction failed: ")
+ << libdnf5::base::Transaction::transaction_result_to_string(result) << std::endl;
+ for (auto const & entry : transaction.get_gpg_signature_problems()) {
+ output_stream << entry << std::endl;
+ }
+ for (auto & problem : transaction.get_transaction_problems()) {
+ output_stream << " - " << problem << std::endl;
+ }
+ success = false;
+ }
+ }
+ }
+ }
+ }
+
+ for (const auto & emitter_name : config_automatic.config_emitters.emit_via.get_value()) {
+ std::unique_ptr emitter;
+ if (emitter_name == "stdio") {
+ emitter = std::make_unique(config_automatic, transaction, output_stream, success);
+ } else if (emitter_name == "motd") {
+ emitter = std::make_unique(config_automatic, transaction, output_stream, success);
+ } else if (emitter_name == "command") {
+ emitter = std::make_unique(config_automatic, transaction, output_stream, success);
+ } else if (emitter_name == "command_email") {
+ emitter = std::make_unique(config_automatic, transaction, output_stream, success);
+ } else if (emitter_name == "email") {
+ emitter = std::make_unique(config_automatic, transaction, output_stream, success);
+ } else {
+ auto & logger = *base.get_logger();
+ logger.warning(_("Unknown report emitter for dnf5 automatic: \"{}\"."), emitter_name);
+ continue;
+ }
+ emitter->notify();
+ }
+
+ if (!success) {
+ throw libdnf5::cli::SilentCommandExitError(1);
+ }
+
+ if (do_reboot) {
+ auto reboot_command = config_automatic.config_commands.reboot_command.get_value();
+ int rc = system(reboot_command.c_str());
+ if (rc != 0) {
+ throw libdnf5::cli::CommandExitError(
+ 1, M_("Error: reboot command returned nonzero exit code: {}"), WEXITSTATUS(rc));
+ }
+ }
+}
+
+AutomaticCommand::~AutomaticCommand() {
+ auto & context = get_context();
+ // dnf5::DownloadCallbacksSimple is part of the automatic.so plugin library, which
+ // gets unloaded during ~Context. However, download_callback is destructed later,
+ // during ~Base, resulting in a segmentation fault. Therefore, we need to reset
+ // download_callbacks manually.
+ if (download_callbacks_set) {
+ context.base.set_download_callbacks(nullptr);
+ }
+}
+
+} // namespace dnf5
diff --git a/dnf5-plugins/automatic_plugin/automatic.hpp b/dnf5-plugins/automatic_plugin/automatic.hpp
new file mode 100644
index 0000000000..a0b0bfe35d
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/automatic.hpp
@@ -0,0 +1,60 @@
+/*
+Copyright Contributors to the libdnf project.
+
+This file is part of libdnf: https://github.com/rpm-software-management/libdnf/
+
+Libdnf is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+Libdnf is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with libdnf. If not, see .
+*/
+
+
+#ifndef DNF5_PLUGINS_AUTOMATIC_PLUGIN_AUTOMATIC_HPP
+#define DNF5_PLUGINS_AUTOMATIC_PLUGIN_AUTOMATIC_HPP
+
+#include "config_automatic.hpp"
+
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+
+namespace dnf5 {
+
+
+class AutomaticCommand : public Command {
+public:
+ explicit AutomaticCommand(Context & context) : Command(context, "automatic") {}
+ ~AutomaticCommand();
+ void set_parent_command() override;
+ void set_argument_parser() override;
+ void pre_configure() override;
+ void configure() override;
+ void run() override;
+
+private:
+ std::unique_ptr timer{nullptr};
+ ConfigAutomatic config_automatic;
+ bool download_callbacks_set{false};
+ std::stringstream output_stream;
+};
+
+
+} // namespace dnf5
+
+
+#endif // DNF5_PLUGINS_AUTOMATIC_PLUGIN_AUTOMATIC_HPP
diff --git a/dnf5-plugins/automatic_plugin/automatic_cmd_plugin.cpp b/dnf5-plugins/automatic_plugin/automatic_cmd_plugin.cpp
new file mode 100644
index 0000000000..6b518f7197
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/automatic_cmd_plugin.cpp
@@ -0,0 +1,74 @@
+#include "automatic.hpp"
+
+#include
+
+#include
+
+using namespace dnf5;
+
+namespace {
+
+constexpr const char * PLUGIN_NAME{"automatic"};
+constexpr PluginVersion PLUGIN_VERSION{.major = 1, .minor = 0, .micro = 0};
+
+constexpr const char * attrs[]{"author.name", "author.email", "description", nullptr};
+constexpr const char * attrs_value[]{"Marek Blaha", "mblaha@redhat.com", "automatic command."};
+
+class AutomaticCmdPlugin : public IPlugin {
+public:
+ using IPlugin::IPlugin;
+
+ PluginAPIVersion get_api_version() const noexcept override { return PLUGIN_API_VERSION; }
+
+ const char * get_name() const noexcept override { return PLUGIN_NAME; }
+
+ PluginVersion get_version() const noexcept override { return PLUGIN_VERSION; }
+
+ const char * const * get_attributes() const noexcept override { return attrs; }
+
+ const char * get_attribute(const char * attribute) const noexcept override {
+ for (size_t i = 0; attrs[i]; ++i) {
+ if (std::strcmp(attribute, attrs[i]) == 0) {
+ return attrs_value[i];
+ }
+ }
+ return nullptr;
+ }
+
+ std::vector> create_commands() override;
+
+ void finish() noexcept override {}
+};
+
+
+std::vector> AutomaticCmdPlugin::create_commands() {
+ std::vector> commands;
+ commands.push_back(std::make_unique(get_context()));
+ return commands;
+}
+
+
+} // namespace
+
+
+PluginAPIVersion dnf5_plugin_get_api_version(void) {
+ return PLUGIN_API_VERSION;
+}
+
+const char * dnf5_plugin_get_name(void) {
+ return PLUGIN_NAME;
+}
+
+PluginVersion dnf5_plugin_get_version(void) {
+ return PLUGIN_VERSION;
+}
+
+IPlugin * dnf5_plugin_new_instance([[maybe_unused]] ApplicationVersion application_version, Context & context) try {
+ return new AutomaticCmdPlugin(context);
+} catch (...) {
+ return nullptr;
+}
+
+void dnf5_plugin_delete_instance(IPlugin * plugin_object) {
+ delete plugin_object;
+}
diff --git a/dnf5-plugins/automatic_plugin/bin/dnf-automatic b/dnf5-plugins/automatic_plugin/bin/dnf-automatic
new file mode 100755
index 0000000000..6247767a82
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/bin/dnf-automatic
@@ -0,0 +1,4 @@
+#!/usr/bin/sh
+# Compatibility wrapper to handle old "/usr/bin/dnf-automatic" name from dnf4.
+
+dnf5 automatic "$@"
diff --git a/dnf5-plugins/automatic_plugin/config/etc/dnf/automatic.conf b/dnf5-plugins/automatic_plugin/config/etc/dnf/automatic.conf
new file mode 100644
index 0000000000..f603324da6
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/config/etc/dnf/automatic.conf
@@ -0,0 +1,104 @@
+[commands]
+# What kind of upgrade to perform:
+# default = all available upgrades
+# security = only the security upgrades
+upgrade_type = default
+random_sleep = 0
+
+# Maximum time in seconds to wait until the system is on-line and able to
+# connect to remote repositories.
+network_online_timeout = 60
+
+# To just receive updates use dnf-automatic-notifyonly.timer
+
+# Whether updates should be downloaded when they are available, by
+# dnf-automatic.timer. notifyonly.timer, download.timer and
+# install.timer override this setting.
+download_updates = yes
+
+# Whether updates should be applied when they are available, by
+# dnf-automatic.timer. notifyonly.timer, download.timer and
+# install.timer override this setting.
+apply_updates = no
+
+# When the system should reboot following upgrades:
+# never = don't reboot after upgrades
+# when-changed = reboot after any changes
+# when-needed = reboot when necessary to apply changes
+reboot = never
+
+# The command that is run to trigger a system reboot.
+reboot_command = "shutdown -r +5 'Rebooting after applying package updates'"
+
+
+[emitters]
+# Name to use for this system in messages that are emitted. Default is the
+# hostname.
+# system_name = my-host
+
+# How to send messages. Valid options are stdio, email and motd. If
+# emit_via includes stdio, messages will be sent to stdout; this is useful
+# to have cron send the messages. If emit_via includes email, this
+# program will send email itself according to the configured options.
+# If emit_via includes motd, /etc/motd file will have the messages. if
+# emit_via includes command_email, then messages will be send via a shell
+# command compatible with sendmail.
+# Default is email,stdio.
+# If emit_via is None or left blank, no messages will be sent.
+emit_via = stdio
+
+
+[email]
+# The address to send email messages from.
+email_from = root@example.com
+
+# List of addresses to send messages to.
+email_to = root
+
+# Name of the host to connect to to send email messages.
+email_host = localhost
+
+# Port number to connect to at the email host.
+email_port = 25
+
+# Use TLS or STARTTLS to connect to the email host.
+email_tls = no
+
+# Credentials to use for SMTP server authentication
+#email_username = username
+#email_password = password
+
+
+[command]
+# The shell command to execute. This is a Python format string, as used in
+# str.format(). The format function will pass a shell-quoted argument called
+# `body`.
+# command_format = "cat"
+
+# The contents of stdin to pass to the command. It is a format string with the
+# same arguments as `command_format`.
+# stdin_format = "{body}"
+
+
+[command_email]
+# The shell command to use to send email. This is a Python format string,
+# as used in str.format(). The format function will pass shell-quoted arguments
+# called body, subject, email_from, email_to.
+# command_format = "mail -Ssendwait -s {subject} -r {email_from} {email_to}"
+
+# The contents of stdin to pass to the command. It is a format string with the
+# same arguments as `command_format`.
+# stdin_format = "{body}"
+
+# The address to send email messages from.
+email_from = root@example.com
+
+# List of addresses to send messages to.
+email_to = root
+
+
+[base]
+# This section overrides dnf.conf
+
+# Use this to filter DNF core messages
+debuglevel = 1
diff --git a/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-download.service b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-download.service
new file mode 100644
index 0000000000..5be2c7dc99
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-download.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=dnf automatic download updates
+ConditionPathExists=!/run/ostree-booted
+After=network-online.target
+
+[Service]
+Type=oneshot
+Nice=19
+IOSchedulingClass=2
+IOSchedulingPriority=7
+ExecStart=/usr/bin/dnf5 automatic /etc/dnf/automatic.conf --timer --downloadupdates --no-installupdates
diff --git a/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-download.timer b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-download.timer
new file mode 100644
index 0000000000..fca4058099
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-download.timer
@@ -0,0 +1,12 @@
+[Unit]
+Description=dnf-automatic-download timer
+ConditionPathExists=!/run/ostree-booted
+Wants=network-online.target
+
+[Timer]
+OnCalendar=*-*-* 6:00
+RandomizedDelaySec=60m
+Persistent=true
+
+[Install]
+WantedBy=timers.target
diff --git a/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-install.service b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-install.service
new file mode 100644
index 0000000000..ba7a9c5696
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-install.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=dnf automatic install updates
+ConditionPathExists=!/run/ostree-booted
+After=network-online.target
+
+[Service]
+Type=oneshot
+Nice=19
+IOSchedulingClass=2
+IOSchedulingPriority=7
+ExecStart=/usr/bin/dnf5 automatic /etc/dnf/automatic.conf --timer --installupdates
diff --git a/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-install.timer b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-install.timer
new file mode 100644
index 0000000000..394153a816
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-install.timer
@@ -0,0 +1,12 @@
+[Unit]
+Description=dnf-automatic-install timer
+ConditionPathExists=!/run/ostree-booted
+Wants=network-online.target
+
+[Timer]
+OnCalendar=*-*-* 6:00
+RandomizedDelaySec=60m
+Persistent=true
+
+[Install]
+WantedBy=timers.target
diff --git a/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-notifyonly.service b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-notifyonly.service
new file mode 100644
index 0000000000..78c2c2bebc
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-notifyonly.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=dnf automatic notification of updates
+ConditionPathExists=!/run/ostree-booted
+After=network-online.target
+
+[Service]
+Type=oneshot
+Nice=19
+IOSchedulingClass=2
+IOSchedulingPriority=7
+ExecStart=/usr/bin/dnf5 automatic /etc/dnf/automatic.conf --timer --no-installupdates --no-downloadupdates
diff --git a/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-notifyonly.timer b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-notifyonly.timer
new file mode 100644
index 0000000000..398386a8f3
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic-notifyonly.timer
@@ -0,0 +1,12 @@
+[Unit]
+Description=dnf-automatic-notifyonly timer
+ConditionPathExists=!/run/ostree-booted
+Wants=network-online.target
+
+[Timer]
+OnCalendar=*-*-* 6:00
+RandomizedDelaySec=60m
+Persistent=true
+
+[Install]
+WantedBy=timers.target
diff --git a/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic.service b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic.service
new file mode 100644
index 0000000000..2e92aadf06
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=dnf automatic
+ConditionPathExists=!/run/ostree-booted
+After=network-online.target
+
+[Service]
+Type=oneshot
+Nice=19
+IOSchedulingClass=2
+IOSchedulingPriority=7
+ExecStart=/usr/bin/dnf5 automatic /etc/dnf/automatic.conf --timer
diff --git a/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic.timer b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic.timer
new file mode 100644
index 0000000000..4930fdf4da
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/config/usr/lib/systemd/system/dnf-automatic.timer
@@ -0,0 +1,12 @@
+[Unit]
+Description=dnf-automatic timer
+ConditionPathExists=!/run/ostree-booted
+Wants=network-online.target
+
+[Timer]
+OnCalendar=*-*-* 6:00
+RandomizedDelaySec=60m
+Persistent=true
+
+[Install]
+WantedBy=timers.target
diff --git a/dnf5-plugins/automatic_plugin/config_automatic.cpp b/dnf5-plugins/automatic_plugin/config_automatic.cpp
new file mode 100644
index 0000000000..779f151533
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/config_automatic.cpp
@@ -0,0 +1,89 @@
+/*
+Copyright (C) 2022 Red Hat, Inc.
+
+This file is part of libdnf: https://github.com/rpm-software-management/dnf5/
+
+Libdnf is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+Libdnf is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with libdnf. If not, see .
+*/
+
+#include "config_automatic.hpp"
+
+#include
+
+#include
+
+namespace dnf5 {
+
+void ConfigAutomatic::load_from_parser(
+ const libdnf5::ConfigParser & parser,
+ const libdnf5::Vars & vars,
+ libdnf5::Logger & logger,
+ libdnf5::Option::Priority priority) {
+ config_commands.load_from_parser(parser, "commands", vars, logger, priority);
+ config_emitters.load_from_parser(parser, "emitters", vars, logger, priority);
+ config_email.load_from_parser(parser, "email", vars, logger, priority);
+ config_command.load_from_parser(parser, "command", vars, logger, priority);
+ config_command_email.load_from_parser(parser, "command_email", vars, logger, priority);
+}
+
+
+ConfigAutomaticCommands::ConfigAutomaticCommands() {
+ opt_binds().add("upgrade_type", upgrade_type);
+ opt_binds().add("random_sleep", random_sleep);
+ opt_binds().add("network_online_timeout", network_online_timeout);
+ opt_binds().add("download_updates", download_updates);
+ opt_binds().add("apply_updates", apply_updates);
+ opt_binds().add("reboot", reboot);
+ opt_binds().add("reboot_command", reboot_command);
+}
+
+
+ConfigAutomaticEmitters::ConfigAutomaticEmitters() {
+ opt_binds().add("emit_via", emit_via);
+ opt_binds().add("system_name", system_name);
+}
+
+std::string ConfigAutomaticEmitters::gethostname() {
+ char hostname[HOST_NAME_MAX + 1];
+ ::gethostname(hostname, HOST_NAME_MAX + 1);
+ return std::string(hostname);
+}
+
+
+ConfigAutomaticEmail::ConfigAutomaticEmail() {
+ opt_binds().add("email_to", email_to);
+ opt_binds().add("email_from", email_from);
+ opt_binds().add("email_host", email_host);
+ opt_binds().add("email_port", email_port);
+ opt_binds().add("email_tls", email_tls);
+ opt_binds().add("email_username", email_username);
+ opt_binds().add("email_password", email_password);
+}
+
+
+ConfigAutomaticCommand::ConfigAutomaticCommand() {
+ opt_binds().add("command_format", command_format);
+ opt_binds().add("stdin_format", stdin_format);
+}
+
+
+ConfigAutomaticCommandEmail::ConfigAutomaticCommandEmail() {
+ opt_binds().add("command_format", command_format);
+ opt_binds().add("stdin_format", stdin_format);
+ opt_binds().add("email_to", email_to);
+ opt_binds().add("email_from", email_from);
+}
+
+
+} // namespace dnf5
diff --git a/dnf5-plugins/automatic_plugin/config_automatic.hpp b/dnf5-plugins/automatic_plugin/config_automatic.hpp
new file mode 100644
index 0000000000..4fc2976873
--- /dev/null
+++ b/dnf5-plugins/automatic_plugin/config_automatic.hpp
@@ -0,0 +1,128 @@
+/*
+Copyright (C) 2022 Red Hat, Inc.
+
+This file is part of libdnf: https://github.com/rpm-software-management/dnf5/
+
+Libdnf is free software: you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation, either version 2 of the License, or
+(at your option) any later version.
+
+Libdnf is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with libdnf. If not, see .
+*/
+
+#ifndef DNF5_PLUGINS_AUTOMATIC_PLUGIN_CONFIG_AUTOMATIC_HPP
+#define DNF5_PLUGINS_AUTOMATIC_PLUGIN_CONFIG_AUTOMATIC_HPP
+
+#include "libdnf5/conf/config.hpp"
+#include "libdnf5/conf/option_bool.hpp"
+#include "libdnf5/conf/option_enum.hpp"
+#include "libdnf5/conf/option_number.hpp"
+#include "libdnf5/conf/option_string.hpp"
+#include "libdnf5/conf/option_string_list.hpp"
+
+#include