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..2449d462b3 --- /dev/null +++ b/dnf5-plugins/automatic_plugin/CMakeLists.txt @@ -0,0 +1,14 @@ +# 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}") diff --git a/dnf5-plugins/automatic_plugin/automatic.cpp b/dnf5-plugins/automatic_plugin/automatic.cpp new file mode 100644 index 0000000000..7f0710f80f --- /dev/null +++ b/dnf5-plugins/automatic_plugin/automatic.cpp @@ -0,0 +1,288 @@ +/* +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 + +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)); +} + +} // 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); + } + } + + if (!transaction.empty()) { + if (config_automatic.config_commands.download_updates.get_value()) { + 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 (config_automatic.config_commands.apply_updates.get_value()) { + 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) { + 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 { + 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); + } + + // TODO reboot +} + +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/config/etc/dnf/automatic.conf b/dnf5-plugins/automatic_plugin/config/etc/dnf/automatic.conf new file mode 100644 index 0000000000..f60f75b9e9 --- /dev/null +++ b/dnf5-plugins/automatic_plugin/config/etc/dnf/automatic.conf @@ -0,0 +1,100 @@ +[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 + + +[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_automatic.cpp b/dnf5-plugins/automatic_plugin/config_automatic.cpp new file mode 100644 index 0000000000..5f2720aafa --- /dev/null +++ b/dnf5-plugins/automatic_plugin/config_automatic.cpp @@ -0,0 +1,87 @@ +/* +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); +} + + +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..98e0d529c4 --- /dev/null +++ b/dnf5-plugins/automatic_plugin/config_automatic.hpp @@ -0,0 +1,126 @@ +/* +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{"email", "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"}}; +}; + + +// 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/emitters.cpp b/dnf5-plugins/automatic_plugin/emitters.cpp new file mode 100644 index 0000000000..e5d00caf16 --- /dev/null +++ b/dnf5-plugins/automatic_plugin/emitters.cpp @@ -0,0 +1,154 @@ +/* +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 +#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 on [{}]: {}"), 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); + } +} + +} // namespace dnf5 diff --git a/dnf5-plugins/automatic_plugin/emitters.hpp b/dnf5-plugins/automatic_plugin/emitters.hpp new file mode 100644 index 0000000000..5c1fa692f4 --- /dev/null +++ b/dnf5-plugins/automatic_plugin/emitters.hpp @@ -0,0 +1,104 @@ +/* +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; +}; + +} // 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 2a5183794c..d64b9333e4 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -647,10 +647,31 @@ 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 automatic +Summary: Package manager - automated upgrades +License: LGPL-2.1-or-later +Requires: dnf5%{?_isa} = %{version}-%{release} +Provides: dnf5-command(automatic) + +%description automatic +Alternative command-line interface "dnf upgrade" suitable to be executed +automatically and regularly from systemd timers, cron jobs or similar. + +%files automatic +%ghost %{_sysconfdir}/motd.d/dnf5-automatic +%config(noreplace) %{_sysconfdir}/dnf/automatic.conf +%{_libdir}/dnf5/plugins/automatic_cmd_plugin.so %endif