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 +#include +#include + +namespace dnf5 { + +// options in [commands] section +class ConfigAutomaticCommands : public libdnf5::Config { +public: + ConfigAutomaticCommands(); + ~ConfigAutomaticCommands() = default; + + libdnf5::OptionEnum upgrade_type{"default", {"default", "security"}}; + libdnf5::OptionNumber random_sleep{0}; + libdnf5::OptionNumber network_online_timeout{60}; + libdnf5::OptionBool download_updates{true}; + libdnf5::OptionBool apply_updates{false}; + libdnf5::OptionEnum reboot{"never", {"never", "when-changed", "when-needed"}}; + libdnf5::OptionString reboot_command{"shutdown -r +5 'Rebooting after applying package updates'"}; +}; + + +// options in [emitters] section +class ConfigAutomaticEmitters : public libdnf5::Config { +public: + ConfigAutomaticEmitters(); + ~ConfigAutomaticEmitters() = default; + + libdnf5::OptionStringList emit_via{std::vector{"stdio"}}; + libdnf5::OptionString system_name{gethostname()}; + +private: + static std::string gethostname(); +}; + + +// options in [email] section +class ConfigAutomaticEmail : public libdnf5::Config { +public: + ConfigAutomaticEmail(); + ~ConfigAutomaticEmail() = default; + + libdnf5::OptionStringList email_to{std::vector{"root"}}; + libdnf5::OptionString email_from{"root"}; + libdnf5::OptionString email_host{"localhost"}; + libdnf5::OptionNumber email_port{25}; + libdnf5::OptionEnum email_tls{"no", {"no", "yes", "starttls"}}; + libdnf5::OptionString email_username{""}; + libdnf5::OptionString email_password{""}; +}; + + +// options in [command] section +class ConfigAutomaticCommand : public libdnf5::Config { +public: + ConfigAutomaticCommand(); + ~ConfigAutomaticCommand() = default; + + libdnf5::OptionString command_format{"cat"}; + libdnf5::OptionString stdin_format{"{body}"}; +}; + + +// options in [command_email] section +class ConfigAutomaticCommandEmail : public libdnf5::Config { +public: + ConfigAutomaticCommandEmail(); + ~ConfigAutomaticCommandEmail() = default; + + libdnf5::OptionString command_format{"mail -Ssendwait -s {subject} -r {email_from} {email_to}"}; + libdnf5::OptionString stdin_format{"{body}"}; + libdnf5::OptionStringList email_to{std::vector{"root"}}; + libdnf5::OptionString email_from{"root"}; +}; + + +class ConfigAutomatic { +public: + ConfigAutomatic(){}; + ~ConfigAutomatic() = default; + + void load_from_parser( + const libdnf5::ConfigParser & parser, + const libdnf5::Vars & vars, + libdnf5::Logger & logger, + libdnf5::Option::Priority priority = libdnf5::Option::Priority::AUTOMATICCONFIG); + + libdnf5::OptionString automatic_config_file_path{"/etc/dnf/automatic.conf"}; + + ConfigAutomaticCommands config_commands; + ConfigAutomaticEmitters config_emitters; + ConfigAutomaticEmail config_email; + ConfigAutomaticCommand config_command; + ConfigAutomaticCommandEmail config_command_email; +}; + +} // namespace dnf5 + +#endif diff --git a/dnf5-plugins/automatic_plugin/download_callbacks_simple.cpp b/dnf5-plugins/automatic_plugin/download_callbacks_simple.cpp new file mode 100644 index 0000000000..90f9d4f793 --- /dev/null +++ b/dnf5-plugins/automatic_plugin/download_callbacks_simple.cpp @@ -0,0 +1,68 @@ +/* +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 "download_callbacks_simple.hpp" + +#include +#include + +namespace dnf5 { + +void * DownloadCallbacksSimple::add_new_download( + [[maybe_unused]] void * user_data, const char * description, [[maybe_unused]] double total_to_download) { + // We cannot print the download description right here, because for packages + // the `add_new_download` is called for each package before the download + // actually starts. So just store the description to print later in `end` callback + // together with download status. + return &active_downloads.emplace_front(description); +} + +int DownloadCallbacksSimple::end(void * user_cb_data, TransferStatus status, const char * msg) { + // check that user_cb_data is really present in active_downloads. + std::string * description{nullptr}; + for (const auto & item : active_downloads) { + if (&item == user_cb_data) { + description = reinterpret_cast(user_cb_data); + break; + } + } + if (!description) { + return 0; + } + + // print the download status of the item + std::string message; + switch (status) { + case TransferStatus::SUCCESSFUL: + output_stream << " Downloaded: " << *description << std::endl; + break; + case TransferStatus::ALREADYEXISTS: + output_stream << " Already downloaded: " << *description << std::endl; + break; + case TransferStatus::ERROR: + output_stream << " Error downloading: " << *description << ": " << msg << std::endl; + break; + } + + // remove the finished item from the active_downloads list + active_downloads.remove_if([description](const std::string & item) { return &item == description; }); + return 0; +} + +} // namespace dnf5 diff --git a/dnf5-plugins/automatic_plugin/download_callbacks_simple.hpp b/dnf5-plugins/automatic_plugin/download_callbacks_simple.hpp new file mode 100644 index 0000000000..f82b24311b --- /dev/null +++ b/dnf5-plugins/automatic_plugin/download_callbacks_simple.hpp @@ -0,0 +1,50 @@ +/* +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 Lesser General Public License as published by +the Free Software Foundation, either version 2.1 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_DOWNLOAD_CALLBACKS_SIMPLE_HPP +#define DNF5_PLUGINS_AUTOMATIC_PLUGIN_DOWNLOAD_CALLBACKS_SIMPLE_HPP + +#include + +#include +#include +#include + +namespace dnf5 { + +/// Simple callbacks class. It does not print any progressbars, only +/// the result of the download. +class DownloadCallbacksSimple : public libdnf5::repo::DownloadCallbacks { +public: + explicit DownloadCallbacksSimple(std::stringstream & output_stream) : output_stream(output_stream) {} + +private: + void * add_new_download(void * user_data, const char * description, double total_to_download) override; + + int end(void * user_cb_data, TransferStatus status, const char * msg) override; + + /// keeps list of descriptions of currently active downloads + std::forward_list active_downloads; + + std::stringstream & output_stream; +}; + +} // namespace dnf5 + +#endif diff --git a/dnf5-plugins/automatic_plugin/email_message.cpp b/dnf5-plugins/automatic_plugin/email_message.cpp new file mode 100644 index 0000000000..fbb5c0510b --- /dev/null +++ b/dnf5-plugins/automatic_plugin/email_message.cpp @@ -0,0 +1,61 @@ +/* +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 "email_message.hpp" + +#include + +#include + +namespace dnf5 { + +// TODO(mblaha): use some library to create an email instead of this template +constexpr const char * MESSAGE_TEMPLATE = + "Date: {date}\r\n" + "To: {to}\r\n" + "From: {from}\r\n" + "Subject: {subject}\r\n" + "X-Mailer: dnf5-automatic\r\n" + "\r\n" + "{body}"; + +std::string EmailMessage::str() { + const auto now = std::chrono::system_clock::now(); + std::string date = std::format("{:%a, %d %b %Y %H:%M:%S %z}", now); + + std::string to_str; + for (const auto & eml : to) { + if (!to_str.empty()) { + to_str += ", "; + } + to_str += eml; + } + + std::string msg; + msg = libdnf5::utils::sformat( + MESSAGE_TEMPLATE, + fmt::arg("date", date), + fmt::arg("to", to_str), + fmt::arg("from", from), + fmt::arg("subject", subject), + fmt::arg("body", body)); + return msg; +} + +} // namespace dnf5 diff --git a/dnf5-plugins/automatic_plugin/email_message.hpp b/dnf5-plugins/automatic_plugin/email_message.hpp new file mode 100644 index 0000000000..21a86ecbdc --- /dev/null +++ b/dnf5-plugins/automatic_plugin/email_message.hpp @@ -0,0 +1,56 @@ +/* +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_EMAIL_MESSAGE_HPP +#define DNF5_PLUGINS_AUTOMATIC_PLUGIN_EMAIL_MESSAGE_HPP + +#include +#include + +namespace dnf5 { + +/// Class for creating simple email messages +class EmailMessage { +public: + EmailMessage() {} + + /// Set the Subject header value + void set_subject(std::string_view subject) { this->subject = subject; }; + /// Set the From header value + void set_from(std::string_view from) { this->from = from; }; + /// Set the To header value + void set_to(const std::vector & to) { this->to = to; }; + /// Set the message body + void set_body(std::string_view body) { this->body = body; }; + + /// Return string representation of the message + std::string str(); + +private: + std::string subject; + std::string from; + std::vector to; + std::string body; +}; + +} // namespace dnf5 + + +#endif // DNF5_PLUGINS_AUTOMATIC_PLUGIN_EMAIL_MESSAGE_HPP diff --git a/dnf5-plugins/automatic_plugin/emitters.cpp b/dnf5-plugins/automatic_plugin/emitters.cpp new file mode 100644 index 0000000000..2ab48d38f3 --- /dev/null +++ b/dnf5-plugins/automatic_plugin/emitters.cpp @@ -0,0 +1,243 @@ +/* +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 "emitters.hpp" + +#include "email_message.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace dnf5 { + +constexpr const char * MOTD_FILENAME = "/etc/motd.d/dnf5-automatic"; +enum class AutomaticStage { CHECK, DOWNLOAD, APPLY }; + +int Emitter::upgrades_count() { + int count = 0; + for (const auto & pkg : transaction.get_transaction_packages()) { + if (transaction_item_action_is_outbound(pkg.get_action())) { + ++count; + } + } + return count; +} + +std::string Emitter::short_message() { + std::string message; + + auto stage = AutomaticStage::CHECK; + if (config_automatic.config_commands.apply_updates.get_value()) { + stage = AutomaticStage::APPLY; + } else if (config_automatic.config_commands.download_updates.get_value()) { + stage = AutomaticStage::DOWNLOAD; + } + + if (success) { + if (transaction.empty()) { + message = _("No new upgrades available."); + } else { + switch (stage) { + case AutomaticStage::CHECK: + message = _("{} packages can be upgraded."); + break; + case AutomaticStage::DOWNLOAD: + message = _("{} new upgrades have been downloaded."); + break; + case AutomaticStage::APPLY: + message = _("{} new upgrades have been installed."); + break; + } + message = libdnf5::utils::sformat(message, upgrades_count()); + } + } else { + switch (stage) { + case AutomaticStage::CHECK: + message = _("Failed to check for upgrades."); + break; + case AutomaticStage::DOWNLOAD: + message = _("Failed to download upgrades."); + break; + case AutomaticStage::APPLY: + message = _("Failed to install upgrades."); + break; + } + } + return message; +} + +void EmitterStdIO::notify() { + std::cout << short_message() << std::endl; + auto output = output_stream.str(); + if (!output.empty()) { + std::cout << std::endl; + std::cout << output; + } +} + +void EmitterMotd::notify() { + std::ofstream motd_file_stream(MOTD_FILENAME); + if (!motd_file_stream.is_open()) { + return; + } + motd_file_stream << "dnf5-automatic: " << short_message() << std::endl; + motd_file_stream.close(); +} + +std::string quote(std::string_view str) { + std::ostringstream temp_stream; + temp_stream << std::quoted(str); + return temp_stream.str(); +} + +void EmitterCommand::notify() { + std::string command_format = config_automatic.config_command.command_format.get_value(); + + FILE * command_pipe = popen(command_format.c_str(), "w"); + if (command_pipe) { + std::string stdin_format = config_automatic.config_command.stdin_format.get_value(); + fputs(libdnf5::utils::sformat(stdin_format, fmt::arg("body", output_stream.str())).c_str(), command_pipe); + std::fflush(command_pipe); + pclose(command_pipe); + } +} + +void EmitterCommandEmail::notify() { + std::string command_format = config_automatic.config_command_email.command_format.get_value(); + std::string email_from = config_automatic.config_command_email.email_from.get_value(); + std::string email_to; + for (const auto & email : config_automatic.config_command_email.email_to.get_value()) { + if (!email_to.empty()) { + email_to += " "; + } + email_to += email; + } + std::string subject = libdnf5::utils::sformat( + _("[{}] dnf5-automatic: {}"), config_automatic.config_emitters.system_name.get_value(), short_message()); + + std::string command_string = libdnf5::utils::sformat( + command_format, + fmt::arg("body", quote(output_stream.str())), + fmt::arg("subject", quote(subject)), + fmt::arg("email_from", quote(email_from)), + fmt::arg("email_to", quote(email_to))); + + FILE * command_pipe = popen(command_string.c_str(), "w"); + if (command_pipe) { + std::string stdin_format = config_automatic.config_command_email.stdin_format.get_value(); + fputs(libdnf5::utils::sformat(stdin_format, fmt::arg("body", output_stream.str())).c_str(), command_pipe); + std::fflush(command_pipe); + pclose(command_pipe); + } +} + +void EmitterEmail::notify() { + EmailMessage message; + std::string subject = libdnf5::utils::sformat( + _("[{}] dnf5-automatic: {}"), config_automatic.config_emitters.system_name.get_value(), short_message()); + + std::vector to = config_automatic.config_email.email_to.get_value(); + std::string from = config_automatic.config_email.email_from.get_value(); + message.set_to(to); + message.set_from(from); + message.set_subject(subject); + message.set_body(output_stream.str()); + + { + // use curl to send the message + std::string payload = message.str(); + std::string tls = config_automatic.config_email.email_tls.get_value().c_str(); + + CURL * curl; + CURLcode res = CURLE_OK; + struct curl_slist * recipients = NULL; + + curl = curl_easy_init(); + if (curl) { + std::string username = config_automatic.config_email.email_username.get_value(); + std::string password = config_automatic.config_email.email_password.get_value(); + if (!username.empty()) { + curl_easy_setopt(curl, CURLOPT_USERNAME, username.c_str()); + curl_easy_setopt(curl, CURLOPT_PASSWORD, password.c_str()); + } + + const char * protocol = "smtp"; + if (tls == "starttls") { + curl_easy_setopt(curl, CURLOPT_USE_SSL, (long)CURLUSESSL_ALL); + } else if (tls == "yes") { + protocol = "smtps"; + } + + // TODO(mblaha): check smtp protocol availability? + curl_version_info_data * ver; + ver = curl_version_info(CURLVERSION_NOW); + bool protocol_supported = false; + for (auto ptr = ver->protocols; *ptr; ++ptr) { + if (strcmp(*ptr, protocol) == 0) { + protocol_supported = true; + break; + } + } + if (protocol_supported) { + std::string email_host = libdnf5::utils::sformat( + "{}://{}:{}/", + protocol, + config_automatic.config_email.email_host.get_value(), + config_automatic.config_email.email_port.get_value()); + curl_easy_setopt(curl, CURLOPT_URL, email_host.c_str()); + + curl_easy_setopt(curl, CURLOPT_MAIL_FROM, from.c_str()); + + for (const auto & eml : to) { + recipients = curl_slist_append(recipients, eml.c_str()); + } + curl_easy_setopt(curl, CURLOPT_MAIL_RCPT, recipients); + + FILE * payload_file = fmemopen(payload.data(), payload.size(), "r"); + curl_easy_setopt(curl, CURLOPT_READDATA, payload_file); + + curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + + res = curl_easy_perform(curl); + if (res != CURLE_OK) { + std::cerr << "libcurl error while sending e-mail: " << curl_easy_strerror(res) << std::endl; + } + } else { + std::cerr << "Error: installed version of libcurl does not support " << protocol + << " protocol. Cannot use \"email\" emitter to send the results. On Fedora please check that " + "libcurl package is installed." + << std::endl; + } + + curl_slist_free_all(recipients); + curl_easy_cleanup(curl); + } + } +} + +} // namespace dnf5 diff --git a/dnf5-plugins/automatic_plugin/emitters.hpp b/dnf5-plugins/automatic_plugin/emitters.hpp new file mode 100644 index 0000000000..d11281e932 --- /dev/null +++ b/dnf5-plugins/automatic_plugin/emitters.hpp @@ -0,0 +1,113 @@ +/* +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_EMITTERS_HPP +#define DNF5_PLUGINS_AUTOMATIC_PLUGIN_EMITTERS_HPP + +#include "config_automatic.hpp" + +#include + +#include + +namespace dnf5 { + + +class Emitter { +public: + Emitter( + const ConfigAutomatic & config_automatic, + const libdnf5::base::Transaction & transaction, + const std::stringstream & output_stream, + const bool success) + : config_automatic(config_automatic), + transaction(transaction), + output_stream(output_stream), + success(success) {} + + /// Notify the user about the status of dnf-automatic run. + virtual void notify() = 0; + + /// Return short message containing basic information about automatic upgrade. + std::string short_message(); + +protected: + // dnf automatic configuration + const ConfigAutomatic & config_automatic; + // resolved upgrade transaction + const libdnf5::base::Transaction & transaction; + // stream with captured upgrade outputs + const std::stringstream & output_stream; + const bool success; + + /// Return number of available upgrades. + int upgrades_count(); +}; + +/// Print the results to standard output. +class EmitterStdIO : public Emitter { +public: + using Emitter::Emitter; + EmitterStdIO() = delete; + + void notify() override; +}; + +/// Send the results to /etc/motd.d/dnf-automatic file +class EmitterMotd : public Emitter { +public: + using Emitter::Emitter; + EmitterMotd() = delete; + + void notify() override; +}; + + +/// Send the results using the shell command +class EmitterCommand : public Emitter { +public: + using Emitter::Emitter; + EmitterCommand() = delete; + + void notify() override; +}; + +/// Send the results via email using the shell command +class EmitterCommandEmail : public Emitter { +public: + using Emitter::Emitter; + EmitterCommandEmail() = delete; + + void notify() override; +}; + +/// Send the results via email +class EmitterEmail : public Emitter { +public: + using Emitter::Emitter; + EmitterEmail() = delete; + + void notify() override; +}; + +} // namespace dnf5 + + +#endif // DNF5_PLUGINS_AUTOMATIC_PLUGIN_EMITTERS_HPP diff --git a/dnf5-plugins/automatic_plugin/transaction_callbacks_simple.cpp b/dnf5-plugins/automatic_plugin/transaction_callbacks_simple.cpp new file mode 100644 index 0000000000..a263c0179b --- /dev/null +++ b/dnf5-plugins/automatic_plugin/transaction_callbacks_simple.cpp @@ -0,0 +1,109 @@ +/* +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 "transaction_callbacks_simple.hpp" + +#include +#include +#include + +#include + +namespace dnf5 { + +void TransactionCallbacksSimple::transaction_start([[maybe_unused]] uint64_t total) { + output_stream << " Prepare transaction" << std::endl; +} + + +void TransactionCallbacksSimple::install_start( + const libdnf5::rpm::TransactionItem & item, [[maybe_unused]] uint64_t total) { + switch (item.get_action()) { + case libdnf5::transaction::TransactionItemAction::UPGRADE: + output_stream << " Upgrading "; + break; + case libdnf5::transaction::TransactionItemAction::DOWNGRADE: + output_stream << " Downgrading "; + break; + case libdnf5::transaction::TransactionItemAction::REINSTALL: + output_stream << " Reinstalling "; + break; + case libdnf5::transaction::TransactionItemAction::INSTALL: + output_stream << " Installing "; + break; + case libdnf5::transaction::TransactionItemAction::REMOVE: + case libdnf5::transaction::TransactionItemAction::REPLACED: + break; + case libdnf5::transaction::TransactionItemAction::REASON_CHANGE: + case libdnf5::transaction::TransactionItemAction::ENABLE: + case libdnf5::transaction::TransactionItemAction::DISABLE: + case libdnf5::transaction::TransactionItemAction::RESET: + throw std::logic_error(fmt::format( + "Unexpected action in TransactionPackage: {}", + static_cast>( + item.get_action()))); + } + output_stream << item.get_package().get_full_nevra() << std::endl; +} + +void TransactionCallbacksSimple::uninstall_start( + const libdnf5::rpm::TransactionItem & item, [[maybe_unused]] uint64_t total) { + if (item.get_action() == libdnf5::transaction::TransactionItemAction::REMOVE) { + output_stream << " Erasing "; + } else { + output_stream << " Cleanup "; + } + output_stream << item.get_package().get_full_nevra() << std::endl; +} + +void TransactionCallbacksSimple::unpack_error(const libdnf5::rpm::TransactionItem & item) { + output_stream << " Unpack error: " << item.get_package().get_full_nevra() << std::endl; +} + +void TransactionCallbacksSimple::cpio_error(const libdnf5::rpm::TransactionItem & item) { + output_stream << " Cpio error: " << item.get_package().get_full_nevra() << std::endl; +} + +void TransactionCallbacksSimple::script_error( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type, + uint64_t return_code) { + output_stream << " Error in " << script_type_to_string(type) << " scriptlet: " << to_full_nevra_string(nevra) + << " return code " << return_code << std::endl; +} + +void TransactionCallbacksSimple::script_start( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type) { + output_stream << " Running " << script_type_to_string(type) << " scriptlet: " << to_full_nevra_string(nevra) + << std::endl; +} + +void TransactionCallbacksSimple::script_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type, + [[maybe_unused]] uint64_t return_code) { + output_stream << " Stop " << script_type_to_string(type) << " scriptlet: " << to_full_nevra_string(nevra) + << std::endl; +} + +} // namespace dnf5 diff --git a/dnf5-plugins/automatic_plugin/transaction_callbacks_simple.hpp b/dnf5-plugins/automatic_plugin/transaction_callbacks_simple.hpp new file mode 100644 index 0000000000..5d6b75a952 --- /dev/null +++ b/dnf5-plugins/automatic_plugin/transaction_callbacks_simple.hpp @@ -0,0 +1,62 @@ +/* +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 Lesser General Public License as published by +the Free Software Foundation, either version 2.1 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_TRANSACTION_CALLBACKS_SIMPLE_HPP +#define DNF5_PLUGINS_AUTOMATIC_PLUGIN_TRANSACTION_CALLBACKS_SIMPLE_HPP + +#include + +#include + +namespace dnf5 { + +/// Simple callbacks class. It does not print any progressbars, only +/// the rpm transaction error messages. +class TransactionCallbacksSimple : public libdnf5::rpm::TransactionCallbacks { +public: + explicit TransactionCallbacksSimple(std::stringstream & output_stream) : output_stream(output_stream) {} + + void transaction_start(uint64_t total) override; + void install_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override; + void uninstall_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override; + void unpack_error(const libdnf5::rpm::TransactionItem & item) override; + void cpio_error(const libdnf5::rpm::TransactionItem & item) override; + void script_error( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type, + uint64_t return_code) override; + void script_start( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type) override; + void script_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type, + [[maybe_unused]] uint64_t return_code) override; + + +private: + std::stringstream & output_stream; +}; + +} // namespace dnf5 + +#endif diff --git a/dnf5.spec b/dnf5.spec index e14893dff0..2424e81fd7 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -632,8 +632,8 @@ Package management service with a DBus interface. # ========== dnf5-plugins ========== - %if %{with dnf5_plugins} + %package -n dnf5-plugins Summary: Plugins for dnf5 License: LGPL-2.1-or-later @@ -647,10 +647,52 @@ Provides: dnf5-command(repoclosure) Core DNF5 plugins that enhance dnf5 with builddep, changelog, copr, and repoclosure commands. %files -n dnf5-plugins -%{_libdir}/dnf5/plugins/*.so +%{_libdir}/dnf5/plugins/builddep_cmd_plugin.so +%{_libdir}/dnf5/plugins/changelog_cmd_plugin.so +%{_libdir}/dnf5/plugins/copr_cmd_plugin.so +%{_libdir}/dnf5/plugins/repoclosure_cmd_plugin.so %{_mandir}/man8/dnf5-builddep.8.* %{_mandir}/man8/dnf5-copr.8.* %{_mandir}/man8/dnf5-repoclosure.8.* + + +# ========== dnf5-automatic plugin ========== + +%package plugin-automatic +Summary: Package manager - automated upgrades +License: LGPL-2.1-or-later +Requires: dnf5%{?_isa} = %{version}-%{release} +Requires: libcurl-full%{?_isa} +Provides: dnf5-command(automatic) +%if %{with dnf5_obsoletes_dnf} +Provides: dnf-automatic = %{version}-%{release} +Obsoletes: dnf-automatic < 5 +%else +Conflicts: dnf-automatic < 5 +%endif + +%description plugin-automatic +Alternative command-line interface "dnf upgrade" suitable to be executed +automatically and regularly from systemd timers, cron jobs or similar. + +%files plugin-automatic +%ghost %{_sysconfdir}/motd.d/dnf5-automatic +%config(noreplace) %{_sysconfdir}/dnf/automatic.conf +%{_libdir}/dnf5/plugins/automatic_cmd_plugin.so +%{_unitdir}/dnf-automatic-download.service +%{_unitdir}/dnf-automatic-download.timer +%{_unitdir}/dnf-automatic-install.service +%{_unitdir}/dnf-automatic-install.timer +%{_unitdir}/dnf-automatic-notifyonly.service +%{_unitdir}/dnf-automatic-notifyonly.timer +%{_unitdir}/dnf-automatic.service +%{_unitdir}/dnf-automatic.timer +%if %{with dnf5_obsoletes_dnf} +%{_bindir}/dnf-automatic +%else +%exclude %{_bindir}/dnf-automatic +%endif + %endif diff --git a/doc/conf.py.in b/doc/conf.py.in index 753973f947..728eec02db 100644 --- a/doc/conf.py.in +++ b/doc/conf.py.in @@ -127,6 +127,7 @@ man_pages = [ ('commands/search.8', 'dnf5-search', 'Search Command', AUTHORS, 8), ('commands/swap.8', 'dnf5-swap', 'Swap Command', AUTHORS, 8), ('commands/upgrade.8', 'dnf5-upgrade', 'Upgrade Command', AUTHORS, 8), + ('dnf5_plugins/automatic.8', 'dnf5-automatic', 'Automatic Command', AUTHORS, 8), ('dnf5_plugins/builddep.8', 'dnf5-builddep', 'Builddep Command', AUTHORS, 8), ('dnf5_plugins/copr.8', 'dnf5-copr', 'Copr Command', AUTHORS, 8), ('dnf5_plugins/repoclosure.8', 'dnf5-repoclosure', 'Repoclosure Command', AUTHORS, 8), diff --git a/doc/dnf5.8.rst b/doc/dnf5.8.rst index 62406cc20b..6f2bbee7d1 100644 --- a/doc/dnf5.8.rst +++ b/doc/dnf5.8.rst @@ -120,6 +120,9 @@ Plugin commands Here is the list of the commands available as plugins. These are available after installing the ``dnf5-plugins`` package. +:ref:`automatic ` + | Alternative CLI to ``dnf upgrade`` suitable to be executed automatically and regularly from systemd timers, cron jobs and similar. + :ref:`builddep ` | Install missing dependencies for building an RPM package. diff --git a/doc/dnf5_plugins/automatic.8.rst b/doc/dnf5_plugins/automatic.8.rst new file mode 100644 index 0000000000..87bf608f53 --- /dev/null +++ b/doc/dnf5_plugins/automatic.8.rst @@ -0,0 +1,238 @@ +.. + 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 . + +.. _automatic_plugin_ref-label: + +################## + Automatic command +################## + +Synopsis +========== + +``dnf5 automatic [options] []`` + + +Options +======= + +``--timer`` + Apply random delay before execution. + +``--downloadupdates`` + Automatically download updated packages. + +``--no-downloadupdates`` + Do not automatically download updated packages. + +``--installupdates`` + Automatically install downloaded updates (implies --downloadupdates). + +``--no-installupdates`` + Do not automatically install downloaded updates. + + +Arguments +========= + +```` + Path to configuration file. Defaults to ``/etc/dnf/automatic.conf``. + + +Description +============= + +Alternative CLI to ``dnf upgrade`` with specific facilities to make it suitable to be executed automatically and regularly from systemd timers, cron jobs and similar. + +The operation of the tool is usually controlled by the configuration file or the function-specific timer units (see below). The command only accepts a single optional argument pointing to the config file, and some control arguments intended for use by the services that back the timer units. If no configuration file is passed from the command line, ``/etc/dnf/automatic.conf`` is used. + +The tool synchronizes package metadata as needed and then checks for updates available for the given system and then either exits, downloads the packages or downloads and applies the updates. The outcome of the operation is then reported by a selected mechanism, for instance via the standard output, email or MOTD messages. + +The systemd timer unit ``dnf-automatic.timer`` will behave as the configuration file specifies (see below) with regard to whether to download and apply updates. Some other timer units are provided which override the configuration file with some standard behaviours: + +- dnf-automatic-notifyonly +- dnf-automatic-download +- dnf-automatic-install + +Regardless of the configuration file settings, the first will only notify of available updates. The second will download, but not install them. The third will download and install them. + + +Run dnf-automatic +=================== + +You can select one that most closely fits your needs, customize ``/etc/dnf/automatic.conf`` for any specific behaviors, and enable the timer unit. + +For example: ``systemctl enable --now dnf-automatic-notifyonly.timer`` + + +Configuration File Format +=========================== + +The configuration file is separated into topical sections. + +---------------------- +``[commands]`` section +---------------------- + +Setting the mode of operation of the program. + +``apply_updates`` + boolean, default: False + + Whether packages comprising the available updates should be applied by ``dnf-automatic.timer``, i.e. installed via RPM. Implies ``download_updates``. Note that if this is set to ``False``, downloaded packages will be left in the cache till the next successful DNF transaction. Note that the other timer units override this setting. + +``download_updates`` + boolean, default: False + + Whether packages comprising the available updates should be downloaded by ``dnf-automatic.timer``. Note that the other timer units override this setting. + +``network_online_timeout`` + time in seconds, default: 60 + + Maximal time dnf-automatic will wait until the system is online. 0 means that network availability detection will be skipped. + +``random_sleep`` + time in seconds, default: 0 + + Maximal random delay before downloading. Note that, by default, the ``systemd`` timers also apply a random delay of up to 1 hour. + +.. _upgrade_type_automatic-label: + +``upgrade_type`` + either one of ``default``, ``security``, default: ``default`` + + What kind of upgrades to look at. ``default`` signals looking for all available updates, ``security`` only those with an issued security advisory. + +``reboot`` + either one of ``never``, ``when-changed``, ``when-needed``, default: ``never`` + + When the system should reboot following upgrades. ``never`` does not reboot the system. ``when-changed`` triggers a reboot after any upgrade. ``when-needed`` triggers a reboot only when rebooting is necessary to apply changes, such as when systemd or the kernel is upgraded. + +``reboot_command`` + string, default: ``shutdown -r +5 'Rebooting after applying package updates'`` + + Specify the command to run to trigger a reboot of the system. For example, to skip the 5-minute delay and wall message, use ``shutdown -r`` + + + +---------------------- +``[emitters]`` section +---------------------- + +Choosing how the results should be reported. + +.. _emit_via_automatic-label: + +``emit_via`` + list, default: ``stdio`` + + List of emitters to report the results through. Available emitters are ``stdio`` to print the result to standard output, ``command`` to send the result to a custom command, ``command_email`` to send an email using a command, ``email`` to send the report via email using SMTP sever, and ``motd`` sends the result to */etc/motd.d/dnf5-automatic* file. + +``system_name`` + string, default: hostname of the given system + + How the system is called in the reports. + +--------------------- +``[command]`` section +--------------------- + +The command emitter configuration. Variables usable in format string arguments are ``body`` with the message body. + +``command_format`` + format string, default: ``cat`` + + The shell command to execute. + +``stdin_format`` + format string, default: ``{body}`` + + The data to pass to the command on stdin. + +--------------------------- +``[command_email]`` section +--------------------------- + +The command email emitter configuration. Variables usable in format string arguments are ``body`` with message body, ``subject`` with email subject, ``email_from`` with the "From:" address and ``email_to`` with a space-separated list of recipients. + +``command_format`` + format string, default: ``mail -Ssendwait -s {subject} -r {email_from} {email_to}`` + + The shell command to execute. + +``email_from`` + string, default: ``root`` + + Message's "From:" address. + +``email_to`` + list, default: ``root`` + + List of recipients of the message. + +``stdin_format`` + format string, default: ``{body}`` + + The data to pass to the command on stdin. + +------------------- +``[email]`` section +------------------- + +The email emitter configuration. + +``email_from`` + string, default: ``root`` + + Message's "From:" address. + +``email_to`` + list, default: ``root`` + + List of recipients of the message. + +``email_host`` + string, default: ``localhost`` + + Hostname of the SMTP server used to send the message. + +``email_port`` + integer, default: ``25`` + + Port number to connect to at the SMTP server. + +``email_tls`` + either one of ``no``, ``yes``, ``starttls``, default: ``no`` + + Whether to use TLS, STARTTLS or no encryption to connect to the SMTP server. + +``email_username`` + string, default empty. + + Username to use for SMTP server authentication. + +``email_password`` + string, default empty. + + Password to use for SMTP server authentication. + +------------------ +``[base]`` section +------------------ + +Can be used to override settings from DNF's main configuration file. See :manpage:`dnf5-conf(5)`. diff --git a/doc/dnf5_plugins/index.rst b/doc/dnf5_plugins/index.rst index 4ab7b17009..87628d7f5f 100644 --- a/doc/dnf5_plugins/index.rst +++ b/doc/dnf5_plugins/index.rst @@ -6,6 +6,7 @@ DNF5 Plugins .. toctree:: :maxdepth: 1 + automatic.8 builddep.8 copr.8 repoclosure.8