From c84b5cff9b48fcd0ea1032e0344d1d8bfbca9e2d Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Thu, 29 Feb 2024 01:46:29 +0000 Subject: [PATCH 01/14] system-upgrade: Add `offline`, `system-upgrade` commands Implement `offline` and `system-upgrade` commands. `dnf5 system-upgrade` is intended to be backwards-compatible with `dnf4 system-upgrade`. All of system-upgrade's functionality, with the exception of the `download` subcommand, is implemented in the new `offline` command, and system-upgrade shares subcommands with it. This is more or less a port of `dnf4 system-upgrade`. This behavior of `dnf5 offline` was proposed here: https://github.com/rpm-software-management/dnf5/issues/1224#issuecomment-1936651917. For https://github.com/rpm-software-management/dnf5/issues/1052 --- .../needs_restarting.hpp | 2 +- dnf5.spec | 5 + dnf5/CMakeLists.txt | 12 +- dnf5/commands/offline/offline.cpp | 582 ++++++++++++++++++ dnf5/commands/offline/offline.hpp | 118 ++++ .../system-upgrade/system-upgrade.cpp | 158 +++++ .../system-upgrade/system-upgrade.hpp | 75 +++ .../system/dnf5-offline-transaction.service | 19 + dnf5/include/dnf5/context.hpp | 79 +++ dnf5/main.cpp | 4 + include/libdnf5/offline/offline.hpp | 91 +++ libdnf5/offline/offline.cpp | 51 ++ 12 files changed, 1194 insertions(+), 2 deletions(-) create mode 100644 dnf5/commands/offline/offline.cpp create mode 100644 dnf5/commands/offline/offline.hpp create mode 100644 dnf5/commands/system-upgrade/system-upgrade.cpp create mode 100644 dnf5/commands/system-upgrade/system-upgrade.hpp create mode 100644 dnf5/config/systemd/system/dnf5-offline-transaction.service create mode 100644 include/libdnf5/offline/offline.hpp create mode 100644 libdnf5/offline/offline.cpp diff --git a/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp b/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp index 0ea4cbeb0..d9e806332 100644 --- a/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp +++ b/dnf5-plugins/needs_restarting_plugin/needs_restarting.hpp @@ -46,4 +46,4 @@ class NeedsRestartingCommand : public Command { } // namespace dnf5 -#endif // DNF5_COMMANDS_CHANGELOG_HPP +#endif // DNF5_COMMANDS_NEEDS_RESTARTING_HPP diff --git a/dnf5.spec b/dnf5.spec index cfa254c6d..7185dae58 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -62,6 +62,8 @@ Provides: dnf5-command(advisory) Provides: dnf5-command(clean) Provides: dnf5-command(download) Provides: dnf5-command(makecache) +Provides: dnf5-command(offline) +Provides: dnf5-command(system-upgrade) # ========== build options ========== @@ -134,7 +136,9 @@ BuildRequires: pkgconfig(librepo) >= %{librepo_version} BuildRequires: pkgconfig(libsolv) >= %{libsolv_version} BuildRequires: pkgconfig(libsolvext) >= %{libsolv_version} BuildRequires: pkgconfig(rpm) >= 4.17.0 +BuildRequires: pkgconfig(sdbus-c++) >= 0.8.1 BuildRequires: pkgconfig(sqlite3) >= %{sqlite_version} +BuildRequires: systemd-devel BuildRequires: toml11-static %if %{with clang} @@ -254,6 +258,7 @@ It supports RPM packages, modulemd modules, and comps groups & environments. %dir %{_datadir}/dnf5 %dir %{_datadir}/dnf5/aliases.d %config %{_datadir}/dnf5/aliases.d/compatibility.conf +%config %{_unitdir}/dnf5-offline-transaction.service %dir %{_libdir}/dnf5 %dir %{_libdir}/dnf5/plugins %dir %{_datadir}/dnf5/dnf5-plugins diff --git a/dnf5/CMakeLists.txt b/dnf5/CMakeLists.txt index d3c18fc99..8099a0b79 100644 --- a/dnf5/CMakeLists.txt +++ b/dnf5/CMakeLists.txt @@ -2,6 +2,8 @@ if(NOT WITH_DNF5) return() endif() +set(SYSTEMD_SYSTEM_UNIT_DIR /usr/lib/systemd/system) + find_package(Threads) # set gettext domain for translations @@ -32,7 +34,10 @@ target_link_libraries(dnf5 PRIVATE common libdnf5 libdnf5-cli Threads::Threads) install(TARGETS dnf5 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) pkg_check_modules(RPM REQUIRED rpm>=4.17.0) -target_link_libraries(dnf5 PRIVATE ${RPM_LIBRARIES}) +pkg_check_modules(SDBUS_CPP REQUIRED sdbus-c++) +pkg_check_modules(LIBSYSTEMD REQUIRED libsystemd) + +target_link_libraries(dnf5 PRIVATE ${RPM_LIBRARIES} ${SDBUS_CPP_LIBRARIES} ${LIBSYSTEMD_LIBRARIES}) find_package(bash-completion) @@ -47,6 +52,11 @@ install(FILES bash-completion/dnf5 DESTINATION ${BASH_COMPLETION_COMPLETIONSDIR} install(FILES "README.plugins" DESTINATION "${CMAKE_INSTALL_FULL_LIBDIR}/dnf5/plugins" RENAME "README") install(DIRECTORY "config/usr/" DESTINATION "${CMAKE_INSTALL_PREFIX}" PATTERN ".gitkeep" EXCLUDE) install(DIRECTORY "config/etc/" DESTINATION "${CMAKE_INSTALL_FULL_SYSCONFDIR}" PATTERN ".gitkeep" EXCLUDE) +install(DIRECTORY "config/systemd/system/" DESTINATION "${SYSTEMD_SYSTEM_UNIT_DIR}" PATTERN ".gitkeep" EXCLUDE) +install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink + ${SYSTEMD_SYSTEM_UNIT_DIR}/dnf5-offline-transaction.service + ${SYSTEMD_SYSTEM_UNIT_DIR}/system-update.target.wants/dnf5-offline-transaction.service)" +) # Makes an empty directory for dnf5-plugins configuration files install(DIRECTORY DESTINATION "${CMAKE_INSTALL_FULL_SYSCONFDIR}/dnf/dnf5-plugins") diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp new file mode 100644 index 000000000..570af38e7 --- /dev/null +++ b/dnf5/commands/offline/offline.cpp @@ -0,0 +1,582 @@ +/* +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 "offline.hpp" + +#include "utils/string.hpp" + +#include "libdnf5/offline/offline.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace libdnf5::cli; + +const std::string & ID_TO_IDENTIFY_BOOTS = libdnf5::offline::OFFLINE_STARTED_ID; + +int call(const std::string & command, const std::vector & args) { + std::vector c_args; + c_args.emplace_back(const_cast(command.c_str())); + for (const auto & arg : args) { + c_args.emplace_back(const_cast(arg.c_str())); + } + c_args.emplace_back(nullptr); + + const auto pid = fork(); + if (pid == -1) { + return -1; + } + if (pid == 0) { + int rc = execvp(command.c_str(), c_args.data()); + exit(rc == 0 ? 0 : -1); + } else { + int status; + int rc = waitpid(pid, &status, 0); + if (rc == -1) { + return -1; + } + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } + if (WIFSIGNALED(status)) { + return 128 + WTERMSIG(status); + } + return -1; + } +} + +namespace dnf5 { + +/// Helper for displaying messages with Plymouth +/// +/// Derived from DNF 4 system-upgrade PlymouthOutput implementation. Filters +/// duplicate calls, and stops calling the plymouth binary if we fail to +/// contact it. +class PlymouthOutput { +public: + bool ping() { return plymouth({"ping"}); } + bool set_mode() { return plymouth({"change-mode", "--system-upgrade"}); } + bool message(const std::string & message) { + if (last_message.has_value() && message == last_message) { + plymouth({"hide-message", "--text", last_message.value()}); + } + last_message = message; + return plymouth({"display-message", "--text", message}); + } + bool progress(const int percentage) { + return plymouth({"system-update", "--progress", std::to_string(percentage)}); + } + +private: + bool alive = true; + std::map> last_subcommand_args; + std::optional last_message; + bool plymouth(const std::vector & args) { + const auto & command = args.at(0); + const auto & last_args = last_subcommand_args.find(command); + + bool dupe_cmd = (last_args != last_subcommand_args.end() && args == last_args->second); + if ((alive && !dupe_cmd) || command == "--ping") { + alive = call(PATH_TO_PLYMOUTH, args) == 0; + last_subcommand_args[command] = args; + } + return alive; + } +}; + +/// Extend RpmTransCB to also display messages with Plymouth +class PlymouthTransCB : public RpmTransCB { +public: + PlymouthTransCB(PlymouthOutput plymouth) : plymouth(std::move(plymouth)) {} + void elem_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) override { + RpmTransCB::elem_progress(item, amount, total); + + plymouth.progress(static_cast(100 * static_cast(amount) / static_cast(total))); + + std::string action; + switch (item.get_action()) { + case libdnf5::transaction::TransactionItemAction::UPGRADE: + action = "Upgrading"; + break; + case libdnf5::transaction::TransactionItemAction::DOWNGRADE: + action = "Downgrading"; + break; + case libdnf5::transaction::TransactionItemAction::REINSTALL: + action = "Reinstalling"; + break; + case libdnf5::transaction::TransactionItemAction::INSTALL: + action = "Installing"; + break; + case libdnf5::transaction::TransactionItemAction::REMOVE: + action = "Removing"; + break; + case libdnf5::transaction::TransactionItemAction::REPLACED: + action = "Replacing"; + 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()))); + break; + } + const auto & message = fmt::format("[{}/{}] {} {}...", amount, total, action, item.get_package().get_name()); + plymouth.message(message); + } + +private: + PlymouthOutput plymouth; +}; + +void OfflineCommand::pre_configure() { + throw_missing_command(); +} + +void OfflineCommand::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); + arg_parser_parent_cmd->get_group("subcommands").register_argument(arg_parser_this_cmd); +} + +void OfflineCommand::set_argument_parser() { + get_argument_parser_command()->set_description("Manage offline transactions"); +} + +void OfflineCommand::register_subcommands() { + register_subcommand(std::make_unique(get_context())); + register_subcommand(std::make_unique(get_context())); + register_subcommand(std::make_unique(get_context())); + register_subcommand(std::make_unique(get_context())); +} + +OfflineSubcommand::OfflineSubcommand(Context & context, const std::string & name) : Command(context, name) {} + +void OfflineSubcommand::set_argument_parser() { + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + auto & cmd = *get_argument_parser_command(); + + cachedir = + dynamic_cast(parser.add_init_value(std::make_unique(datadir))); + + auto * download_dir_arg = parser.add_new_named_arg("downloaddir"); + download_dir_arg->set_long_name("downloaddir"); + download_dir_arg->set_description("Redirect download of packages to provided "); + download_dir_arg->link_value(cachedir); + cmd.register_named_arg(download_dir_arg); +} + +void OfflineSubcommand::configure() { + auto & ctx = get_context(); + const std::filesystem::path installroot{ctx.base.get_config().get_installroot_option().get_value()}; + magic_symlink = installroot / "system-update"; + datadir = libdnf5::offline::get_offline_datadir(installroot); + std::filesystem::create_directories(datadir); + state = std::make_optional(datadir / "offline-transaction-state.toml"); + + system_releasever = *libdnf5::Vars::detect_release(ctx.base.get_weak_ptr(), installroot); + target_releasever = ctx.base.get_vars()->get_value("releasever"); +} + +void OfflineSubcommand::log_status(const std::string & message, const std::string & message_id) const { + const auto & version = get_application_version(); + const std::string & version_string = fmt::format("{}.{}.{}", version.major, version.minor, version.micro); + sd_journal_send( + "MESSAGE=%s", + message.c_str(), + "MESSAGE_ID=%s", + message_id.c_str(), + "SYSTEM_RELEASEVER=%s", + get_system_releasever().c_str(), + "TARGET_RELEASEVER=%s", + get_target_releasever().c_str(), + "DNF_VERSION=%s", + version_string.c_str(), + NULL); +} + +void check_state(const libdnf5::offline::OfflineTransactionState & state) { + const auto & read_exception = state.get_read_exception(); + if (read_exception != nullptr) { + try { + std::rethrow_exception(read_exception); + } catch (const std::exception & ex) { + const std::string message{ex.what()}; + throw libdnf5::cli::CommandExitError( + 1, + M_("Error reading state: {}. Rerun the command you used to initiate the offline transaction, e.g. " + "`dnf5 system-upgrade download [OPTIONS]`."), + message); + } + } +} + +void reboot(bool poweroff = false) { + const std::string systemd_destination_name{"org.freedesktop.systemd1"}; + const std::string systemd_object_path{"/org/freedesktop/systemd1"}; + const std::string systemd_manager_interface{"org.freedesktop.systemd1.Manager"}; + + if (std::getenv("DNF_SYSTEM_UPGRADE_NO_REBOOT")) { + std::cerr << "DNF_SYSTEM_UPGRADE_NO_REBOOT is set, not rebooting." << std::endl; + return; + } + + poweroff = !poweroff; + std::unique_ptr connection; + try { + connection = sdbus::createSystemBusConnection(); + } catch (const sdbus::Error & ex) { + const std::string error_message{ex.what()}; + throw libdnf5::cli::CommandExitError(1, M_("Couldn't connect to D-Bus: {}"), error_message); + } + auto proxy = sdbus::createProxy(systemd_destination_name, systemd_object_path); + if (poweroff) { + proxy->callMethod("Poweroff").onInterface(systemd_manager_interface); + } else { + proxy->callMethod("Reboot").onInterface(systemd_manager_interface); + } +} + +void clean_datadir(Context & ctx, const std::filesystem::path & datadir) { + ctx.base.get_logger()->info("Cleaning up downloaded data..."); + + for (const auto & entry : std::filesystem::directory_iterator(datadir)) { + std::filesystem::remove_all(entry.path()); + } +} + +void OfflineRebootCommand::set_argument_parser() { + OfflineSubcommand::set_argument_parser(); + + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + auto & cmd = *get_argument_parser_command(); + + cmd.set_description("Prepare the system to perform the offline transaction and reboots to start the transaction."); + + poweroff_after = + dynamic_cast(parser.add_init_value(std::make_unique(true))); + + auto * poweroff_after_arg = parser.add_new_named_arg("poweroff"); + poweroff_after_arg->set_long_name("poweroff"); + poweroff_after_arg->set_description("Power off the system after the operation is complete"); + poweroff_after_arg->link_value(poweroff_after); + + cmd.register_named_arg(poweroff_after_arg); +} + +void OfflineRebootCommand::run() { + auto & ctx = get_context(); + + check_state(*state); + if (state->get_data().status != libdnf5::offline::STATUS_DOWNLOAD_COMPLETE) { + throw libdnf5::cli::CommandExitError(1, M_("system is not ready for offline transaction")); + } + if (std::filesystem::is_symlink(get_magic_symlink())) { + throw libdnf5::cli::CommandExitError(1, M_("offline {} is already scheduled"), state->get_data().verb); + } + + if (!std::filesystem::is_directory(get_datadir())) { + throw libdnf5::cli::CommandExitError(1, M_("data directory {} does not exist"), get_datadir().string()); + } + + if (state->get_data().verb == "system-upgrade download") { + std::cout << "The system will now reboot to upgrade to release version " << state->get_data().target_releasever + << "." << std::endl; + } else { + std::cout << "The system will now reboot to perform the offline transaction initiated by the following command:" + << std::endl + << "\t" << state->get_data().cmd_line << std::endl; + } + if (!libdnf5::cli::utils::userconfirm::userconfirm(ctx.base.get_config())) { + return; + } + + std::filesystem::create_symlink(get_datadir(), get_magic_symlink()); + + state->get_data().status = libdnf5::offline::STATUS_READY; + state->get_data().poweroff_after = poweroff_after->get_value(); + state->write(); + + reboot(poweroff_after->get_value()); +} + +void OfflineExecuteCommand::set_argument_parser() { + OfflineSubcommand::set_argument_parser(); + auto & cmd = *get_argument_parser_command(); + cmd.set_description( + "Internal use only, not intended to be run by the user. Executes the transaction in the offline environment."); +} + +void OfflineExecuteCommand::configure() { + OfflineSubcommand::configure(); + auto & ctx = get_context(); + + ctx.set_load_system_repo(true); + ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); + + check_state(*state); + + // Set same set of enabled/disabled repos used during `system-upgrade download` + for (const auto & repo_id : state->get_data().enabled_repos) { + ctx.setopts.emplace_back(repo_id + ".enabled", "1"); + } + for (const auto & repo_id : state->get_data().disabled_repos) { + ctx.setopts.emplace_back(repo_id + ".disabled", "1"); + } + + // Don't try to refresh metadata, we are offline + ctx.base.get_config().get_cacheonly_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, "all"); + // Don't ask any questions + ctx.base.get_config().get_assumeyes_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, true); + // Override `assumeno` too since it takes priority over `assumeyes` + ctx.base.get_config().get_assumeno_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, false); + // Upgrade operation already removes all element that must be removed. + // Additional removal could trigger unwanted changes in transaction. + ctx.base.get_config().get_clean_requirements_on_remove_option().set( + libdnf5::Option::Priority::PLUGINDEFAULT, false); + ctx.base.get_config().get_install_weak_deps_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, false); +} + +void OfflineExecuteCommand::run() { + auto & ctx = get_context(); + + log_status("Starting offline transaction. This will take a while.", libdnf5::offline::OFFLINE_STARTED_ID); + + if (!std::filesystem::is_symlink(get_magic_symlink())) { + throw libdnf5::cli::CommandExitError(0, M_("Trigger file does not exist. Exiting.")); + } + + const auto & symlinked_path = std::filesystem::read_symlink(get_magic_symlink()); + if (symlinked_path != get_datadir()) { + throw libdnf5::cli::CommandExitError(0, M_("Another offline transaction tool is running. Exiting.")); + } + + std::filesystem::remove(get_magic_symlink()); + + if (state->get_data().status != libdnf5::offline::STATUS_READY) { + throw libdnf5::cli::CommandExitError(1, M_("Use `dnf5 offline reboot` to begin the transaction.")); + } + + const auto & installroot = get_context().base.get_config().get_installroot_option().get_value(); + const auto & datadir = libdnf5::offline::get_offline_datadir(installroot); + std::filesystem::create_directories(datadir); + const auto & transaction_json_path = datadir / libdnf5::offline::TRANSACTION_JSON_FILENAME; + auto transaction_json_file = libdnf5::utils::fs::File(transaction_json_path, "r"); + + const auto & json = transaction_json_file.read(); + transaction_json_file.close(); + + const auto & goal = std::make_unique(ctx.base); + + auto settings = libdnf5::GoalJobSettings(); + goal->add_serialized_transaction(json, settings); + + auto transaction = goal->resolve(); + if (transaction.get_problems() != libdnf5::GoalProblem::NO_PROBLEM) { + throw libdnf5::cli::GoalResolveError(transaction); + } + + PlymouthOutput plymouth; + auto callbacks = std::make_unique(plymouth); + /* callbacks->get_multi_progress_bar()->set_total_num_of_bars(num_of_actions); */ + transaction.set_callbacks(std::move(callbacks)); + + const auto result = transaction.run(); + std::cout << std::endl; + if (result != libdnf5::base::Transaction::TransactionRunResult::SUCCESS) { + std::cerr << "Transaction failed: " << libdnf5::base::Transaction::transaction_result_to_string(result) + << std::endl; + for (auto const & entry : transaction.get_gpg_signature_problems()) { + std::cerr << entry << std::endl; + } + for (auto & problem : transaction.get_transaction_problems()) { + std::cerr << " - " << problem << std::endl; + } + throw libdnf5::cli::SilentCommandExitError(1); + } + + for (auto const & entry : transaction.get_gpg_signature_problems()) { + std::cerr << entry << std::endl; + } + + std::string transaction_complete_message; + if (state->get_data().poweroff_after) { + transaction_complete_message = "Transaction complete! Cleaning up and powering off..."; + } else { + transaction_complete_message = "Transaction complete! Cleaning up and rebooting..."; + } + plymouth.message(transaction_complete_message); + log_status(transaction_complete_message, libdnf5::offline::OFFLINE_STARTED_ID); + + // If the transaction succeeded, remove downloaded data + clean_datadir(ctx, get_datadir()); + + reboot(state->get_data().poweroff_after); +} + +void OfflineCleanCommand::set_argument_parser() { + OfflineSubcommand::set_argument_parser(); + auto & cmd = *get_argument_parser_command(); + cmd.set_description("Remove any stored offline transaction and delete cached package files."); +} + +void OfflineCleanCommand::run() { + auto & ctx = get_context(); + clean_datadir(ctx, get_datadir()); +} + +struct BootEntry { + std::string boot_id; + std::string timestamp; + std::string system_releasever; + std::string target_releasever; +}; + +std::string get_journal_field(sd_journal * journal, const std::string & field) { + const char * data = nullptr; + size_t length = 0; + auto rc = sd_journal_get_data(journal, field.c_str(), reinterpret_cast(&data), &length); + if (rc < 0 || data == nullptr) { + return ""; + } + const auto prefix_length = field.length() + 1; + return std::string{data + prefix_length, length - prefix_length}; +} + +std::vector find_boots(const std::string & message_id) { + std::vector boots{}; + + sd_journal * journal = nullptr; + auto rc = sd_journal_open(&journal, SD_JOURNAL_LOCAL_ONLY); + if (rc < 0) { + throw libdnf5::RuntimeError(M_("Error reading journal: {}"), std::string{std::strerror(-rc)}); + } + + const auto & uid_filter_string = fmt::format("MESSAGE_ID={}", message_id); + rc = sd_journal_add_match(journal, uid_filter_string.c_str(), 0); + if (rc < 0) { + throw libdnf5::RuntimeError(M_("Add journal match failed: {}"), std::string{std::strerror(-rc)}); + } + + std::optional current_boot_id; + + SD_JOURNAL_FOREACH(journal) { + uint64_t usec = 0; + rc = sd_journal_get_realtime_usec(journal, &usec); + auto sec = usec / (1000 * 1000); + const auto & boot_id = get_journal_field(journal, "_BOOT_ID"); + if (boot_id != current_boot_id) { + current_boot_id = boot_id; + boots.emplace_back(BootEntry{ + boot_id, + libdnf5::utils::string::format_epoch(sec), + get_journal_field(journal, "SYSTEM_RELEASEVER"), + get_journal_field(journal, "TARGET_RELEASEVER"), + }); + } + } + + return boots; +} + +void list_logs() { + const auto & boot_entries = find_boots(ID_TO_IDENTIFY_BOOTS); + + if (boot_entries.empty()) { + std::cout << "No logs were found." << std::endl; + return; + } + + std::cout << "The following boots appear to contain offline transaction logs:" << std::endl; + for (size_t index = 0; index < boot_entries.size(); index += 1) { + const auto & entry = boot_entries[index]; + std::cout << fmt::format( + "{} / {}: {} {}→{}", + index + 1, + entry.boot_id, + entry.timestamp, + entry.system_releasever, + entry.target_releasever) + << std::endl; + } +} + +void show_log(size_t boot_index) { + const auto & boot_entries = find_boots(ID_TO_IDENTIFY_BOOTS); + if (boot_index >= boot_entries.size()) { + throw libdnf5::cli::CommandExitError(1, M_("Cannot find logs with this index.")); + } + + const auto & boot_id = boot_entries[boot_index].boot_id; + const auto rc = call(PATH_TO_JOURNALCTL, {"--boot", boot_id}); + + if (rc != 0 && rc != 141) { + throw libdnf5::cli::CommandExitError(1, M_("Unable to match systemd journal entry.")); + } +} + +void OfflineLogCommand::set_argument_parser() { + OfflineSubcommand::set_argument_parser(); + + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + auto & cmd = *get_argument_parser_command(); + + cmd.set_description("Show logs from past offline transactions"); + + number = dynamic_cast(parser.add_init_value(std::make_unique(""))); + + auto * number_arg = parser.add_new_named_arg("number"); + number_arg->set_long_name("number"); + number_arg->set_has_value(true); + + number_arg->set_description("Which log to show. Run without any arguments to get a list of available logs."); + number_arg->link_value(number); + cmd.register_named_arg(number_arg); +} + +void OfflineLogCommand::run() { + if (number->get_value().empty()) { + list_logs(); + } else { + std::string number_string{number->get_value()}; + show_log(std::stoul(number_string) - 1); + } +} + +} // namespace dnf5 diff --git a/dnf5/commands/offline/offline.hpp b/dnf5/commands/offline/offline.hpp new file mode 100644 index 000000000..523ce12c9 --- /dev/null +++ b/dnf5/commands/offline/offline.hpp @@ -0,0 +1,118 @@ +/* +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_COMMANDS_OFFLINE_HPP +#define DNF5_COMMANDS_OFFLINE_HPP + +#include +#include +#include +#include +#include +#include +#include + +const std::filesystem::path PATH_TO_PLYMOUTH{"/usr/bin/plymouth"}; +const std::filesystem::path PATH_TO_JOURNALCTL{"/usr/bin/journalctl"}; + +namespace dnf5 { + +class OfflineCommand : public Command { +public: + explicit OfflineCommand(Context & context) : Command(context, "offline") {} + void set_parent_command() override; + void set_argument_parser() override; + void register_subcommands() override; + void pre_configure() override; +}; + +class OfflineSubcommand : public Command { +public: + explicit OfflineSubcommand(Context & context, const std::string & name); + void set_argument_parser() override; + void configure() override; + +protected: + libdnf5::OptionPath * get_cachedir() const { return cachedir; }; + std::filesystem::path get_magic_symlink() const { return magic_symlink; }; + std::filesystem::path get_datadir() const { return datadir; }; + std::optional state; + std::string get_system_releasever() const { return system_releasever; }; + std::string get_target_releasever() const { return target_releasever; }; + void log_status(const std::string & message, const std::string & message_id) const; + +private: + libdnf5::OptionPath * cachedir{nullptr}; + std::filesystem::path magic_symlink; + std::filesystem::path datadir; + std::string target_releasever; + std::string system_releasever; +}; + +class OfflineDownloadCommand : public OfflineSubcommand { +public: + explicit OfflineDownloadCommand(Context & context) : OfflineSubcommand{context, "download"} {} + void set_argument_parser() override; + void configure() override; + void run() override; + +private: + libdnf5::OptionBool * no_downgrade{nullptr}; + libdnf5::OptionBool * distro_sync{nullptr}; + int state_version; +}; + +class OfflineRebootCommand : public OfflineSubcommand { +public: + explicit OfflineRebootCommand(Context & context) : OfflineSubcommand(context, "reboot") {} + void set_argument_parser() override; + void run() override; + +private: + libdnf5::OptionBool * poweroff_after{nullptr}; +}; + +class OfflineExecuteCommand : public OfflineSubcommand { +public: + explicit OfflineExecuteCommand(Context & context) : OfflineSubcommand(context, "_execute") {} + void set_argument_parser() override; + void configure() override; + void run() override; +}; + +class OfflineCleanCommand : public OfflineSubcommand { +public: + explicit OfflineCleanCommand(Context & context) : OfflineSubcommand(context, "clean") {} + void set_argument_parser() override; + void run() override; +}; + +class OfflineLogCommand : public OfflineSubcommand { +public: + explicit OfflineLogCommand(Context & context) : OfflineSubcommand(context, "log") {} + void set_argument_parser() override; + void run() override; + +private: + libdnf5::OptionString * number{nullptr}; +}; + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_OFFLINE_HPP diff --git a/dnf5/commands/system-upgrade/system-upgrade.cpp b/dnf5/commands/system-upgrade/system-upgrade.cpp new file mode 100644 index 000000000..101c30471 --- /dev/null +++ b/dnf5/commands/system-upgrade/system-upgrade.cpp @@ -0,0 +1,158 @@ +/* +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 "system-upgrade.hpp" + +#include "../offline/offline.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace libdnf5::cli; + +namespace dnf5 { + +void SystemUpgradeCommand::pre_configure() { + throw_missing_command(); +} + +void SystemUpgradeCommand::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); + arg_parser_parent_cmd->get_group("subcommands").register_argument(arg_parser_this_cmd); +} + +void SystemUpgradeCommand::set_argument_parser() { + get_argument_parser_command()->set_description("Prepare system for upgrade to a new release"); +} + +void SystemUpgradeCommand::register_subcommands() { + register_subcommand(std::make_unique(get_context())); + register_subcommand(std::make_unique(get_context())); + register_subcommand(std::make_unique(get_context())); + register_subcommand(std::make_unique(get_context())); +} + +SystemUpgradeSubcommand::SystemUpgradeSubcommand(Context & context, const std::string & name) + : Command(context, name), + datadir(std::filesystem::path{libdnf5::SYSTEM_STATE_DIR} / "system-upgrade"), + state(datadir / "state.toml") {} + +void SystemUpgradeSubcommand::set_argument_parser() { + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + auto & cmd = *get_argument_parser_command(); + + cachedir = + dynamic_cast(parser.add_init_value(std::make_unique(datadir))); + + auto * download_dir_arg = parser.add_new_named_arg("downloaddir"); + download_dir_arg->set_long_name("downloaddir"); + download_dir_arg->set_description("Redirect download of packages to provided "); + download_dir_arg->link_value(cachedir); + cmd.register_named_arg(download_dir_arg); +} + +void SystemUpgradeSubcommand::configure() { + auto & ctx = get_context(); + const std::filesystem::path installroot{ctx.base.get_config().get_installroot_option().get_value()}; + magic_symlink = installroot / "system-update"; + + system_releasever = *libdnf5::Vars::detect_release(ctx.base.get_weak_ptr(), installroot); + target_releasever = ctx.base.get_vars()->get_value("releasever"); +} + +void SystemUpgradeDownloadCommand::set_argument_parser() { + SystemUpgradeSubcommand::set_argument_parser(); + + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + auto & cmd = *get_argument_parser_command(); + + cmd.set_description("Downloads everything needed to upgrade to a new release"); + + no_downgrade = + dynamic_cast(parser.add_init_value(std::make_unique(true))); + + auto * no_downgrade_arg = parser.add_new_named_arg("no-downgrade"); + no_downgrade_arg->set_long_name("no-downgrade"); + no_downgrade_arg->set_description( + "Do not install packages from the new release if they are older than what is currently installed"); + no_downgrade_arg->link_value(no_downgrade); + + cmd.register_named_arg(no_downgrade_arg); +} + +void SystemUpgradeDownloadCommand::configure() { + SystemUpgradeSubcommand::configure(); + + auto & ctx = get_context(); + + // Check --releasever + if (get_target_releasever() == get_system_releasever()) { + throw libdnf5::cli::CommandExitError(1, M_("Need a --releasever greater than the current system version.")); + } + + ctx.set_load_system_repo(true); + ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); + + ctx.base.get_config().get_cachedir_option().set( + libdnf5::Option::Priority::PLUGINDEFAULT, get_cachedir()->get_value()); +} + +void SystemUpgradeDownloadCommand::run() { + auto & ctx = get_context(); + + const auto & goal = std::make_unique(ctx.base); + + if (no_downgrade->get_value()) { + goal->add_rpm_upgrade(); + } else { + goal->add_rpm_distro_sync(); + } + + auto transaction = goal->resolve(); + if (transaction.get_problems() != libdnf5::GoalProblem::NO_PROBLEM) { + throw libdnf5::cli::GoalResolveError(transaction); + } + + if (transaction.get_transaction_packages_count() == 0) { + throw libdnf5::cli::CommandExitError( + 1, M_("The system-upgrade transaction is empty; your system is already up-to-date.")); + } + + ctx.should_store_offline = true; + ctx.download_and_run(transaction); + + std::cout << "Download complete! Use `dnf5 system-upgrade reboot` to start the upgrade." << std::endl + << "To cancel the upgrade and delete the downloaded upgrade files, use `dnf5 system-upgrade clean`." + << std::endl; +} + +} // namespace dnf5 diff --git a/dnf5/commands/system-upgrade/system-upgrade.hpp b/dnf5/commands/system-upgrade/system-upgrade.hpp new file mode 100644 index 000000000..19f44799d --- /dev/null +++ b/dnf5/commands/system-upgrade/system-upgrade.hpp @@ -0,0 +1,75 @@ +/* +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_COMMANDS_SYSTEM_UPGRADE_HPP +#define DNF5_COMMANDS_SYSTEM_UPGRADE_HPP + +#include +#include + +namespace dnf5 { + +class SystemUpgradeCommand : public Command { +public: + explicit SystemUpgradeCommand(Context & context) : Command(context, "system-upgrade") {} + void set_parent_command() override; + void set_argument_parser() override; + void register_subcommands() override; + void pre_configure() override; +}; + +class SystemUpgradeSubcommand : public Command { +public: + explicit SystemUpgradeSubcommand(Context & context, const std::string & name); + void set_argument_parser() override; + void configure() override; + +protected: + libdnf5::OptionPath * get_cachedir() const { return cachedir; }; + std::filesystem::path get_datadir() const { return datadir; }; + std::filesystem::path get_magic_symlink() const { return magic_symlink; }; + libdnf5::offline::OfflineTransactionState get_state() const { return state; }; + std::string get_system_releasever() const { return system_releasever; }; + std::string get_target_releasever() const { return target_releasever; }; + void log_status(const std::string & message, const std::string & message_id) const; + +private: + libdnf5::OptionPath * cachedir{nullptr}; + std::filesystem::path datadir{libdnf5::offline::DEFAULT_DATADIR}; + std::filesystem::path magic_symlink; + libdnf5::offline::OfflineTransactionState state; + std::string target_releasever; + std::string system_releasever; +}; + +class SystemUpgradeDownloadCommand : public SystemUpgradeSubcommand { +public: + explicit SystemUpgradeDownloadCommand(Context & context) : SystemUpgradeSubcommand{context, "download"} {} + void set_argument_parser() override; + void configure() override; + void run() override; + +private: + libdnf5::OptionBool * no_downgrade{nullptr}; + int state_version; +}; + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_SYSTEM_UPGRADE_HPP diff --git a/dnf5/config/systemd/system/dnf5-offline-transaction.service b/dnf5/config/systemd/system/dnf5-offline-transaction.service new file mode 100644 index 000000000..91a26b5e8 --- /dev/null +++ b/dnf5/config/systemd/system/dnf5-offline-transaction.service @@ -0,0 +1,19 @@ +[Unit] +Description=Offline upgrades/transactions using DNF 5 +ConditionPathExists=/system-update +Documentation=https://www.freedesktop.org/wiki/Software/systemd/SystemUpdates + +DefaultDependencies=no +Requires=sysinit.target +After=sysinit.target systemd-journald.socket system-update-pre.target +Before=poweroff.target reboot.target shutdown.target system-update.target + +[Service] +# We are done when the script exits, not before +Type=oneshot +# Upgrade output goes to journal and on-screen. +StandardOutput=journal+console +ExecStart=/usr/bin/dnf5 offline _execute + +[Install] +WantedBy=system-update.target diff --git a/dnf5/include/dnf5/context.hpp b/dnf5/include/dnf5/context.hpp index 925042d02..4159e7594 100644 --- a/dnf5/include/dnf5/context.hpp +++ b/dnf5/include/dnf5/context.hpp @@ -187,6 +187,85 @@ class RpmTransactionItem : public libdnf5::rpm::TransactionItem { Actions action; }; +class RpmTransCB : public libdnf5::rpm::TransactionCallbacks { +public: + RpmTransCB(); + ~RpmTransCB(); + libdnf5::cli::progressbar::MultiProgressBar * get_multi_progress_bar(); + + void install_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + uint64_t amount, + [[maybe_unused]] uint64_t total) override; + + void install_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override; + void install_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) override; + + void transaction_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override; + + void transaction_start(uint64_t total) override; + + void transaction_stop([[maybe_unused]] uint64_t total) override; + + void uninstall_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + uint64_t amount, + [[maybe_unused]] uint64_t total) override; + + void uninstall_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override; + + void uninstall_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] 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; + + void elem_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) override; + + void verify_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override; + + void verify_start([[maybe_unused]] uint64_t total) override; + + void verify_stop([[maybe_unused]] uint64_t total) override; + +private: + void new_progress_bar(int64_t total, const std::string & descr); + + static bool is_time_to_print(); + + static std::chrono::time_point prev_print_time; + + libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar; + libdnf5::cli::progressbar::DownloadProgressBar * active_progress_bar{nullptr}; +}; + void run_transaction(libdnf5::rpm::Transaction & transaction); /// Returns the names of matching packages and paths of matching package file names and directories. diff --git a/dnf5/main.cpp b/dnf5/main.cpp index 1019a1bf3..3a54877d8 100644 --- a/dnf5/main.cpp +++ b/dnf5/main.cpp @@ -36,6 +36,7 @@ along with libdnf. If not, see . #include "commands/makecache/makecache.hpp" #include "commands/mark/mark.hpp" #include "commands/module/module.hpp" +#include "commands/offline/offline.hpp" #include "commands/provides/provides.hpp" #include "commands/reinstall/reinstall.hpp" #include "commands/remove/remove.hpp" @@ -43,6 +44,7 @@ along with libdnf. If not, see . #include "commands/repoquery/repoquery.hpp" #include "commands/search/search.hpp" #include "commands/swap/swap.hpp" +#include "commands/system-upgrade/system-upgrade.hpp" #include "commands/upgrade/upgrade.hpp" #include "commands/versionlock/versionlock.hpp" #include "dnf5/context.hpp" @@ -691,6 +693,8 @@ static void add_commands(Context & context) { context.add_and_initialize_command(std::make_unique(context)); context.add_and_initialize_command(std::make_unique(context)); context.add_and_initialize_command(std::make_unique(context)); + context.add_and_initialize_command(std::make_unique(context)); + context.add_and_initialize_command(std::make_unique(context)); } static void load_plugins(Context & context) { diff --git a/include/libdnf5/offline/offline.hpp b/include/libdnf5/offline/offline.hpp new file mode 100644 index 000000000..988748672 --- /dev/null +++ b/include/libdnf5/offline/offline.hpp @@ -0,0 +1,91 @@ +/* +Copyright (C) 2024 Red Hat, Inc. + +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 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 LIBDNF5_OFFLINE_HPP +#define LIBDNF5_OFFLINE_HPP + +#include +#include +#include +#include +#include +#include + +namespace libdnf5::offline { + +const std::string OFFLINE_STARTED_ID{"3e0a5636d16b4ca4bbe5321d06c6aa62"}; +const std::string OFFLINE_FINISHED_ID{"8cec00a1566f4d3594f116450395f06c"}; + +const std::string STATUS_DOWNLOAD_INCOMPLETE{"download-incomplete"}; +const std::string STATUS_DOWNLOAD_COMPLETE{"download-complete"}; +const std::string STATUS_READY{"ready"}; +const std::string STATUS_UPGRADE_INCOMPLETE{"upgrade-incomplete"}; + +const int STATE_VERSION = 0; +const std::string STATE_HEADER{"offline-transaction-state"}; + +const std::filesystem::path DEFAULT_DATADIR{std::filesystem::path(libdnf5::SYSTEM_STATE_DIR) / "offline"}; +const std::filesystem::path TRANSACTION_STATE_FILENAME{"offline-transaction-state.toml"}; +const std::filesystem::path TRANSACTION_JSON_FILENAME{"offline-transaction.json"}; + +std::filesystem::path get_offline_datadir(const std::filesystem::path & installroot); + +struct OfflineTransactionStateData { + int state_version = STATE_VERSION; + std::string status = STATUS_DOWNLOAD_INCOMPLETE; + std::string cachedir; + std::string target_releasever; + std::string system_releasever; + std::string verb; + std::string cmd_line; + bool poweroff_after = false; + std::vector enabled_repos; + std::vector disabled_repos; +}; + +class OfflineTransactionState { +public: + void write(); + OfflineTransactionState(std::filesystem::path path); + OfflineTransactionStateData & get_data() { return data; }; + const std::exception_ptr & get_read_exception() const { return read_exception; }; + +private: + void read(); + std::exception_ptr read_exception; + std::filesystem::path path; + OfflineTransactionStateData data; +}; + +} // namespace libdnf5::offline + +TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( + libdnf5::offline::OfflineTransactionStateData, + state_version, + status, + cachedir, + target_releasever, + system_releasever, + verb, + cmd_line, + poweroff_after, + enabled_repos, + disabled_repos) + +#endif // LIBDNF5_OFFLINE_HPP diff --git a/libdnf5/offline/offline.cpp b/libdnf5/offline/offline.cpp new file mode 100644 index 000000000..85de7294b --- /dev/null +++ b/libdnf5/offline/offline.cpp @@ -0,0 +1,51 @@ +/* +Copyright (C) 2024 Red Hat, Inc. + +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 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 +#include +#include + +namespace libdnf5::offline { + +OfflineTransactionState::OfflineTransactionState(std::filesystem::path path) : path(std::move(path)) { + read(); +} +void OfflineTransactionState::read() { + try { + const auto & value = toml::parse(path); + data = toml::find(value, STATE_HEADER); + if (data.state_version != STATE_VERSION) { + throw libdnf5::RuntimeError(M_("incompatible version of state data")); + } + } catch (const std::exception & ex) { + read_exception = std::current_exception(); + data = OfflineTransactionStateData{}; + } +} +void OfflineTransactionState::write() { + auto file = libdnf5::utils::fs::File(path, "w"); + file.write(toml::format(toml::value{{STATE_HEADER, data}})); + file.close(); +} + +std::filesystem::path get_offline_datadir(const std::filesystem::path & installroot) { + return installroot / DEFAULT_DATADIR.relative_path(); +} + +} // namespace libdnf5::offline From 276fd5d2bd23411f1d3044e36db714b37efb0922 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Thu, 29 Feb 2024 01:55:04 +0000 Subject: [PATCH 02/14] system-upgrade: Add `--offline` flag Adds a new --offline argument that allows any operation to be performed offline, as proposed here: https://github.com/rpm-software-management/dnf5/issues/1224#issuecomment-1936651917. The following commands support `--offline`: `autoremove`, `distro-sync`, `downgrade`, `install`, `group install`, `group upgrade`, `reinstall`, `remove`, `swap`, and `upgrade`. --- dnf5/commands/autoremove/autoremove.cpp | 2 + dnf5/commands/distro-sync/distro-sync.cpp | 1 + dnf5/commands/downgrade/downgrade.cpp | 1 + dnf5/commands/group/group_install.cpp | 1 + dnf5/commands/group/group_upgrade.cpp | 1 + dnf5/commands/install/install.cpp | 1 + dnf5/commands/offline/offline.cpp | 4 +- dnf5/commands/reinstall/reinstall.cpp | 1 + dnf5/commands/remove/remove.cpp | 4 + dnf5/commands/swap/swap.cpp | 2 + dnf5/commands/upgrade/upgrade.cpp | 1 + dnf5/context.cpp | 425 ++++++++++++---------- dnf5/include/dnf5/context.hpp | 9 +- dnf5/include/dnf5/shared_options.hpp | 3 + dnf5/shared_options.cpp | 23 ++ 15 files changed, 285 insertions(+), 194 deletions(-) diff --git a/dnf5/commands/autoremove/autoremove.cpp b/dnf5/commands/autoremove/autoremove.cpp index eba1a3eb1..5ef870684 100644 --- a/dnf5/commands/autoremove/autoremove.cpp +++ b/dnf5/commands/autoremove/autoremove.cpp @@ -19,6 +19,7 @@ along with libdnf. If not, see . #include "autoremove.hpp" +#include #include namespace dnf5 { @@ -35,6 +36,7 @@ void AutoremoveCommand::set_parent_command() { void AutoremoveCommand::set_argument_parser() { get_argument_parser_command()->set_description( "Remove all unneeded packages originally installed as dependencies."); + create_offline_option(*this); } void AutoremoveCommand::configure() { diff --git a/dnf5/commands/distro-sync/distro-sync.cpp b/dnf5/commands/distro-sync/distro-sync.cpp index 2c92c747f..9c434bc31 100644 --- a/dnf5/commands/distro-sync/distro-sync.cpp +++ b/dnf5/commands/distro-sync/distro-sync.cpp @@ -52,6 +52,7 @@ void DistroSyncCommand::set_argument_parser() { allow_erasing = std::make_unique(*this); auto skip_unavailable = std::make_unique(*this); auto skip_broken = std::make_unique(*this); + create_offline_option(*this); } void DistroSyncCommand::configure() { diff --git a/dnf5/commands/downgrade/downgrade.cpp b/dnf5/commands/downgrade/downgrade.cpp index 5aaa279ca..0b2c4c147 100644 --- a/dnf5/commands/downgrade/downgrade.cpp +++ b/dnf5/commands/downgrade/downgrade.cpp @@ -56,6 +56,7 @@ void DowngradeCommand::set_argument_parser() { auto skip_unavailable = std::make_unique(*this); auto skip_broken = std::make_unique(*this); create_allow_downgrade_options(*this); + create_offline_option(*this); } void DowngradeCommand::configure() { diff --git a/dnf5/commands/group/group_install.cpp b/dnf5/commands/group/group_install.cpp index a8cfd0383..3bca825ab 100644 --- a/dnf5/commands/group/group_install.cpp +++ b/dnf5/commands/group/group_install.cpp @@ -44,6 +44,7 @@ void GroupInstallCommand::set_argument_parser() { auto skip_broken = std::make_unique(*this); create_allow_downgrade_options(*this); create_downloadonly_option(*this); + create_offline_option(*this); } void GroupInstallCommand::configure() { diff --git a/dnf5/commands/group/group_upgrade.cpp b/dnf5/commands/group/group_upgrade.cpp index 940f3f31e..915d4a386 100644 --- a/dnf5/commands/group/group_upgrade.cpp +++ b/dnf5/commands/group/group_upgrade.cpp @@ -40,6 +40,7 @@ void GroupUpgradeCommand::set_argument_parser() { allow_erasing = std::make_unique(*this); auto skip_unavailable = std::make_unique(*this); create_allow_downgrade_options(*this); + create_offline_option(*this); } void GroupUpgradeCommand::configure() { diff --git a/dnf5/commands/install/install.cpp b/dnf5/commands/install/install.cpp index d9a3cdca3..fcff03838 100644 --- a/dnf5/commands/install/install.cpp +++ b/dnf5/commands/install/install.cpp @@ -67,6 +67,7 @@ void InstallCommand::set_argument_parser() { auto skip_unavailable = std::make_unique(*this); auto skip_broken = std::make_unique(*this); create_allow_downgrade_options(*this); + create_offline_option(*this); } void InstallCommand::configure() { diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp index 570af38e7..73a1cbfbf 100644 --- a/dnf5/commands/offline/offline.cpp +++ b/dnf5/commands/offline/offline.cpp @@ -114,7 +114,7 @@ class PlymouthOutput { /// Extend RpmTransCB to also display messages with Plymouth class PlymouthTransCB : public RpmTransCB { public: - PlymouthTransCB(PlymouthOutput plymouth) : plymouth(std::move(plymouth)) {} + PlymouthTransCB(Context & context, PlymouthOutput plymouth) : RpmTransCB(context), plymouth(std::move(plymouth)) {} void elem_progress( [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, [[maybe_unused]] uint64_t amount, @@ -412,7 +412,7 @@ void OfflineExecuteCommand::run() { } PlymouthOutput plymouth; - auto callbacks = std::make_unique(plymouth); + auto callbacks = std::make_unique(ctx, plymouth); /* callbacks->get_multi_progress_bar()->set_total_num_of_bars(num_of_actions); */ transaction.set_callbacks(std::move(callbacks)); diff --git a/dnf5/commands/reinstall/reinstall.cpp b/dnf5/commands/reinstall/reinstall.cpp index 84b0c7945..21b25e8c4 100644 --- a/dnf5/commands/reinstall/reinstall.cpp +++ b/dnf5/commands/reinstall/reinstall.cpp @@ -56,6 +56,7 @@ void ReinstallCommand::set_argument_parser() { auto skip_unavailable = std::make_unique(*this); auto skip_broken = std::make_unique(*this); create_allow_downgrade_options(*this); + create_offline_option(*this); } void ReinstallCommand::configure() { diff --git a/dnf5/commands/remove/remove.cpp b/dnf5/commands/remove/remove.cpp index ca02d3176..48bae15d2 100644 --- a/dnf5/commands/remove/remove.cpp +++ b/dnf5/commands/remove/remove.cpp @@ -19,6 +19,8 @@ along with libdnf. If not, see . #include "remove.hpp" +#include + namespace dnf5 { using namespace libdnf5::cli; @@ -55,6 +57,8 @@ void RemoveCommand::set_argument_parser() { }); keys->set_complete_hook_func([&ctx](const char * arg) { return match_specs(ctx, arg, true, false, false, true); }); cmd.register_positional_arg(keys); + + create_offline_option(*this); } void RemoveCommand::configure() { diff --git a/dnf5/commands/swap/swap.cpp b/dnf5/commands/swap/swap.cpp index ec0eda706..86b2d5694 100644 --- a/dnf5/commands/swap/swap.cpp +++ b/dnf5/commands/swap/swap.cpp @@ -68,6 +68,8 @@ void SwapCommand::set_argument_parser() { cmd.register_positional_arg(install_spec_arg); allow_erasing = std::make_unique(*this); + + create_offline_option(*this); } void SwapCommand::configure() { diff --git a/dnf5/commands/upgrade/upgrade.cpp b/dnf5/commands/upgrade/upgrade.cpp index 996f7f9c5..89bf7db1f 100644 --- a/dnf5/commands/upgrade/upgrade.cpp +++ b/dnf5/commands/upgrade/upgrade.cpp @@ -70,6 +70,7 @@ void UpgradeCommand::set_argument_parser() { create_destdir_option(*this); auto & destdir = parser.get_named_arg("upgrade.destdir", false); destdir.set_description(destdir.get_description() + " Automatically sets the --downloadonly option."); + create_offline_option(*this); advisory_name = std::make_unique(*this); advisory_security = std::make_unique(*this); diff --git a/dnf5/context.cpp b/dnf5/context.cpp index 1818e92d1..a03b6b036 100644 --- a/dnf5/context.cpp +++ b/dnf5/context.cpp @@ -24,6 +24,8 @@ along with libdnf. If not, see . #include "utils/string.hpp" #include "utils/url.hpp" +#include "libdnf5/offline/offline.hpp" + #include #include #include @@ -36,6 +38,7 @@ along with libdnf. If not, see . #include #include #include +#include #include #include @@ -184,234 +187,223 @@ void Context::load_repos(bool load_system) { print_info("Repositories loaded."); } -namespace { - -class RpmTransCB : public libdnf5::rpm::TransactionCallbacks { -public: - RpmTransCB(Context & context) : context(context) { - multi_progress_bar.set_total_bar_visible_limit( - libdnf5::cli::progressbar::MultiProgressBar::NEVER_VISIBLE_LIMIT); - } +RpmTransCB::RpmTransCB(Context & context) : context(context) { + multi_progress_bar.set_total_bar_visible_limit( + libdnf5::cli::progressbar::MultiProgressBar::NEVER_VISIBLE_LIMIT); +} - ~RpmTransCB() { - if (active_progress_bar && - active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { - active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); - } - if (active_progress_bar) { - multi_progress_bar.print(); - } +RpmTransCB::~RpmTransCB() { + if (active_progress_bar && + active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { + active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); } - - libdnf5::cli::progressbar::MultiProgressBar * get_multi_progress_bar() { return &multi_progress_bar; } - - void install_progress( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - uint64_t amount, - [[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(amount)); - if (is_time_to_print()) { - multi_progress_bar.print(); - } + if (active_progress_bar) { + multi_progress_bar.print(); } +} - void install_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override { - const char * msg{nullptr}; - switch (item.get_action()) { - case libdnf5::transaction::TransactionItemAction::UPGRADE: - msg = "Upgrading "; - break; - case libdnf5::transaction::TransactionItemAction::DOWNGRADE: - msg = "Downgrading "; - break; - case libdnf5::transaction::TransactionItemAction::REINSTALL: - msg = "Reinstalling "; - break; - case libdnf5::transaction::TransactionItemAction::INSTALL: - 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: - auto & logger = *context.base.get_logger(); - logger.warning( - "Unexpected action in TransactionPackage: {}", - static_cast>( - item.get_action())); - return; - } - if (!msg) { - msg = "Installing "; - } - new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra()); - } +libdnf5::cli::progressbar::MultiProgressBar * RpmTransCB::get_multi_progress_bar() { return &multi_progress_bar; } - void install_stop( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - [[maybe_unused]] uint64_t amount, - [[maybe_unused]] uint64_t total) override { +void RpmTransCB::install_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + uint64_t amount, + [[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(amount)); + if (is_time_to_print()) { multi_progress_bar.print(); } +} - void transaction_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(amount)); - if (is_time_to_print()) { - multi_progress_bar.print(); - } +void RpmTransCB::install_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) { + const char * msg{nullptr}; + switch (item.get_action()) { + case libdnf5::transaction::TransactionItemAction::UPGRADE: + msg = "Upgrading "; + break; + case libdnf5::transaction::TransactionItemAction::DOWNGRADE: + msg = "Downgrading "; + break; + case libdnf5::transaction::TransactionItemAction::REINSTALL: + msg = "Reinstalling "; + break; + case libdnf5::transaction::TransactionItemAction::INSTALL: + 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: + auto & logger = *context.base.get_logger(); + logger.warning( + "Unexpected action in TransactionPackage: {}", + static_cast>( + item.get_action())); + return; } - - void transaction_start(uint64_t total) override { - new_progress_bar(static_cast(total), "Prepare transaction"); + if (!msg) { + msg = "Installing "; } + new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra()); +} + +void RpmTransCB::install_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) { + multi_progress_bar.print(); +} - void transaction_stop([[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(total)); +void RpmTransCB::transaction_progress(uint64_t amount, [[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(amount)); + if (is_time_to_print()) { multi_progress_bar.print(); } +} - void uninstall_progress( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - uint64_t amount, - [[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(amount)); - if (is_time_to_print()) { - multi_progress_bar.print(); - } - } +void RpmTransCB::transaction_start(uint64_t total) { + new_progress_bar(static_cast(total), "Prepare transaction"); +} - void uninstall_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) override { - const char * msg{nullptr}; - if (item.get_action() == libdnf5::transaction::TransactionItemAction::REMOVE || - item.get_action() == libdnf5::transaction::TransactionItemAction::REPLACED) { - msg = "Erasing "; - } - if (!msg) { - msg = "Cleanup "; - } - new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra()); - } +void RpmTransCB::transaction_stop([[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(total)); + multi_progress_bar.print(); +} - void uninstall_stop( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - [[maybe_unused]] uint64_t amount, - [[maybe_unused]] uint64_t total) override { +void RpmTransCB::uninstall_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + uint64_t amount, + [[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(amount)); + if (is_time_to_print()) { multi_progress_bar.print(); } +} - - void unpack_error(const libdnf5::rpm::TransactionItem & item) override { - active_progress_bar->add_message( - libdnf5::cli::progressbar::MessageType::ERROR, "Unpack error: " + item.get_package().get_full_nevra()); - active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR); - multi_progress_bar.print(); +void RpmTransCB::uninstall_start(const libdnf5::rpm::TransactionItem & item, uint64_t total) { + const char * msg{nullptr}; + if (item.get_action() == libdnf5::transaction::TransactionItemAction::REMOVE || + item.get_action() == libdnf5::transaction::TransactionItemAction::REPLACED) { + msg = "Erasing "; } - - void cpio_error(const libdnf5::rpm::TransactionItem & item) override { - active_progress_bar->add_message( - libdnf5::cli::progressbar::MessageType::ERROR, "Cpio error: " + item.get_package().get_full_nevra()); - active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR); - multi_progress_bar.print(); + if (!msg) { + msg = "Cleanup "; } + new_progress_bar(static_cast(total), msg + item.get_package().get_full_nevra()); +} - void script_error( - [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, - libdnf5::rpm::Nevra nevra, - libdnf5::rpm::TransactionCallbacks::ScriptType type, - uint64_t return_code) override { - active_progress_bar->add_message( - libdnf5::cli::progressbar::MessageType::ERROR, - fmt::format( - "Error in {} scriptlet: {} return code {}", - script_type_to_string(type), - to_full_nevra_string(nevra), - return_code)); - multi_progress_bar.print(); - } +void RpmTransCB::uninstall_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) { + multi_progress_bar.print(); +} - void script_start( - [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, - libdnf5::rpm::Nevra nevra, - libdnf5::rpm::TransactionCallbacks::ScriptType type) override { - active_progress_bar->add_message( - libdnf5::cli::progressbar::MessageType::INFO, - fmt::format("Running {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra))); - multi_progress_bar.print(); - } - 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 { - active_progress_bar->add_message( - libdnf5::cli::progressbar::MessageType::INFO, - fmt::format("Stop {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra))); - multi_progress_bar.print(); - } +void RpmTransCB::unpack_error(const libdnf5::rpm::TransactionItem & item) { + active_progress_bar->add_message( + libdnf5::cli::progressbar::MessageType::ERROR, "Unpack error: " + item.get_package().get_full_nevra()); + active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR); + multi_progress_bar.print(); +} - void elem_progress( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - [[maybe_unused]] uint64_t amount, - [[maybe_unused]] uint64_t total) override { - //std::cout << "Element progress: " << header.get_full_nevra() << " " << amount << '/' << total << std::endl; - } +void RpmTransCB::cpio_error(const libdnf5::rpm::TransactionItem & item) { + active_progress_bar->add_message( + libdnf5::cli::progressbar::MessageType::ERROR, "Cpio error: " + item.get_package().get_full_nevra()); + active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::ERROR); + multi_progress_bar.print(); +} - void verify_progress(uint64_t amount, [[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(amount)); - if (is_time_to_print()) { - multi_progress_bar.print(); - } - } +void RpmTransCB::script_error( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type, + uint64_t return_code) { + active_progress_bar->add_message( + libdnf5::cli::progressbar::MessageType::ERROR, + fmt::format( + "Error in {} scriptlet: {} return code {}", + script_type_to_string(type), + to_full_nevra_string(nevra), + return_code)); + multi_progress_bar.print(); +} - void verify_start([[maybe_unused]] uint64_t total) override { - new_progress_bar(static_cast(total), "Verify package files"); - } +void RpmTransCB::script_start( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type) { + active_progress_bar->add_message( + libdnf5::cli::progressbar::MessageType::INFO, + fmt::format("Running {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra))); + multi_progress_bar.print(); +} - void verify_stop([[maybe_unused]] uint64_t total) override { - active_progress_bar->set_ticks(static_cast(total)); +void RpmTransCB::script_stop( + [[maybe_unused]] const libdnf5::rpm::TransactionItem * item, + libdnf5::rpm::Nevra nevra, + libdnf5::rpm::TransactionCallbacks::ScriptType type, + [[maybe_unused]] uint64_t return_code) { + active_progress_bar->add_message( + libdnf5::cli::progressbar::MessageType::INFO, + fmt::format("Stop {} scriptlet: {}", script_type_to_string(type), to_full_nevra_string(nevra))); + multi_progress_bar.print(); +} + +void RpmTransCB::elem_progress( + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, + [[maybe_unused]] uint64_t amount, + [[maybe_unused]] uint64_t total) { + //std::cout << "Element progress: " << header.get_full_nevra() << " " << amount << '/' << total << std::endl; +} + +void RpmTransCB::verify_progress(uint64_t amount, [[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(amount)); + if (is_time_to_print()) { multi_progress_bar.print(); } +} -private: - void new_progress_bar(int64_t total, const std::string & descr) { - if (active_progress_bar && - active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { - active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); - } - auto progress_bar = - std::make_unique(static_cast(total), descr); - progress_bar->set_auto_finish(false); - progress_bar->start(); - active_progress_bar = progress_bar.get(); - multi_progress_bar.add_bar(std::move(progress_bar)); - } - - static bool is_time_to_print() { - auto now = std::chrono::steady_clock::now(); - auto delta = now - prev_print_time; - auto ms = std::chrono::duration_cast(delta).count(); - if (ms > 100) { - // 100ms equals to 10 FPS and that seems to be smooth enough - prev_print_time = now; - return true; - } - return false; - } +void RpmTransCB::verify_start([[maybe_unused]] uint64_t total) { + new_progress_bar(static_cast(total), "Verify package files"); +} - static std::chrono::time_point prev_print_time; +void RpmTransCB::verify_stop([[maybe_unused]] uint64_t total) { + active_progress_bar->set_ticks(static_cast(total)); + multi_progress_bar.print(); +} - libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar; - libdnf5::cli::progressbar::DownloadProgressBar * active_progress_bar{nullptr}; - Context & context; -}; +void RpmTransCB::new_progress_bar(int64_t total, const std::string & descr) { + if (active_progress_bar && + active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { + active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); + } + auto progress_bar = + std::make_unique(static_cast(total), descr); + progress_bar->set_auto_finish(false); + progress_bar->start(); + active_progress_bar = progress_bar.get(); + multi_progress_bar.add_bar(std::move(progress_bar)); +} -std::chrono::time_point RpmTransCB::prev_print_time = std::chrono::steady_clock::now(); +bool RpmTransCB::is_time_to_print() { + auto now = std::chrono::steady_clock::now(); + auto delta = now - prev_print_time; + auto ms = std::chrono::duration_cast(delta).count(); + if (ms > 100) { + // 100ms equals to 10 FPS and that seems to be smooth enough + prev_print_time = now; + return true; + } + return false; +} -} // namespace +std::chrono::time_point RpmTransCB::prev_print_time = std::chrono::steady_clock::now(); void Context::download_and_run(libdnf5::base::Transaction & transaction) { + if (should_store_offline) { + base.get_config().get_tsflags_option().set(libdnf5::Option::Priority::RUNTIME, "test"); + } transaction.download(); if (base.get_config().get_downloadonly_option().get_value()) { @@ -461,6 +453,57 @@ void Context::download_and_run(libdnf5::base::Transaction & transaction) { std::cerr << entry << std::endl; } // TODO(mblaha): print a summary of successful transaction + + if (should_store_offline) { + store_offline(transaction); + std::cout << "Transaction stored to be performed offline. Run `dnf5 offline reboot` to reboot and run the " + "transaction." + << std::endl; + } +} + +void Context::store_offline(libdnf5::base::Transaction & transaction) { + const auto & installroot = base.get_config().get_installroot_option().get_value(); + const auto & offline_datadir = libdnf5::offline::get_offline_datadir(installroot); + std::filesystem::create_directories(offline_datadir); + + const std::filesystem::path state_path{offline_datadir / libdnf5::offline::TRANSACTION_STATE_FILENAME}; + libdnf5::offline::OfflineTransactionState state{state_path}; + + if (state.get_data().status != libdnf5::offline::STATUS_DOWNLOAD_INCOMPLETE) { + std::cout << "There is already an offline transaction queued, initiated by the following command:" << std::endl + << "\t" << state.get_data().cmd_line << std::endl + << "Continuing will cancel the old offline transaction and replace it with this one." << std::endl; + if (!libdnf5::cli::utils::userconfirm::userconfirm(base.get_config())) { + throw libdnf5::cli::SilentCommandExitError(0); + } + } + + const std::filesystem::path transaction_json_path{offline_datadir / libdnf5::offline::TRANSACTION_JSON_FILENAME}; + + const std::string json = transaction.serialize(); + + auto transaction_json_file = libdnf5::utils::fs::File(transaction_json_path, "w"); + + transaction_json_file.write(json); + transaction_json_file.close(); + + state.get_data().status = libdnf5::offline::STATUS_DOWNLOAD_COMPLETE; + state.get_data().cachedir = base.get_config().get_cachedir_option().get_value(); + + std::vector command_vector; + auto * current_command = get_selected_command(); + while (current_command != get_root_command()) { + command_vector.emplace_back(current_command->get_argument_parser_command()->get_id()); + current_command = current_command->get_parent_command(); + } + state.get_data().verb = libdnf5::utils::string::join(command_vector, " "); + state.get_data().cmd_line = get_cmdline(); + + state.get_data().system_releasever = *libdnf5::Vars::detect_release(base.get_weak_ptr(), installroot); + state.get_data().target_releasever = base.get_vars()->get_value("releasever"); + + state.write(); } libdnf5::Goal * Context::get_goal(bool new_if_not_exist) { diff --git a/dnf5/include/dnf5/context.hpp b/dnf5/include/dnf5/context.hpp index 4159e7594..7ca88c1bc 100644 --- a/dnf5/include/dnf5/context.hpp +++ b/dnf5/include/dnf5/context.hpp @@ -23,6 +23,7 @@ along with libdnf. If not, see . #include "version.hpp" #include +#include #include #include #include @@ -78,6 +79,10 @@ class Context : public libdnf5::cli::session::Session { std::vector enable_plugins_patterns; std::vector disable_plugins_patterns; + bool should_store_offline = false; + + void store_offline(libdnf5::base::Transaction & transaction); + /// Gets user comment. const char * get_comment() const noexcept { return comment; } @@ -153,6 +158,7 @@ class Context : public libdnf5::cli::session::Session { std::vector dump_repo_config_id_list; bool dump_variables{false}; bool show_new_leaves{false}; + std::string get_cmd_line(); std::reference_wrapper output_stream = std::cout; @@ -189,7 +195,7 @@ class RpmTransactionItem : public libdnf5::rpm::TransactionItem { class RpmTransCB : public libdnf5::rpm::TransactionCallbacks { public: - RpmTransCB(); + RpmTransCB(Context & context); ~RpmTransCB(); libdnf5::cli::progressbar::MultiProgressBar * get_multi_progress_bar(); @@ -264,6 +270,7 @@ class RpmTransCB : public libdnf5::rpm::TransactionCallbacks { libdnf5::cli::progressbar::MultiProgressBar multi_progress_bar; libdnf5::cli::progressbar::DownloadProgressBar * active_progress_bar{nullptr}; + Context & context; }; void run_transaction(libdnf5::rpm::Transaction & transaction); diff --git a/dnf5/include/dnf5/shared_options.hpp b/dnf5/include/dnf5/shared_options.hpp index 55ef1a03f..6c4d0268c 100644 --- a/dnf5/include/dnf5/shared_options.hpp +++ b/dnf5/include/dnf5/shared_options.hpp @@ -73,6 +73,9 @@ void create_downloadonly_option(dnf5::Command & command); /// The values are stored in the `forcearch` configuration option [[deprecated("--forcearch is now a global argument")]] void create_forcearch_option(dnf5::Command & command); +/// Create the `--offline` option for a command provided as an argument. +void create_offline_option(dnf5::Command & command); + } // namespace dnf5 #endif // DNF5_COMMANDS_SHARED_OPTIONS_HPP diff --git a/dnf5/shared_options.cpp b/dnf5/shared_options.cpp index 78a25dff8..8a35e9206 100644 --- a/dnf5/shared_options.cpp +++ b/dnf5/shared_options.cpp @@ -22,6 +22,7 @@ along with libdnf. If not, see . #include "libdnf5/utils/bgettext/bgettext-mark-domain.h" +#include #include namespace dnf5 { @@ -108,5 +109,27 @@ void create_downloadonly_option(dnf5::Command & command) { command.get_argument_parser_command()->register_named_arg(downloadonly); } +void create_offline_option(dnf5::Command & command) { + auto & ctx = command.get_context(); + auto & parser = command.get_context().get_argument_parser(); + auto offline = parser.add_new_named_arg("offline"); + offline->set_long_name("offline"); + offline->set_description("Store the transaction to be performed offline"); + offline->set_const_value("true"); + offline->set_parse_hook_func([&ctx]( + [[maybe_unused]] libdnf5::cli::ArgumentParser::NamedArg * arg, + [[maybe_unused]] const char * option, + [[maybe_unused]] const char * value) { + const auto & installroot = ctx.base.get_config().get_installroot_option().get_value(); + const auto & offline_datadir = libdnf5::offline::get_offline_datadir(installroot); + std::filesystem::create_directories(offline_datadir); + + ctx.base.get_config().get_cachedir_option().set(libdnf5::Option::Priority::RUNTIME, offline_datadir); + ctx.should_store_offline = true; + return true; + }); + command.get_argument_parser_command()->register_named_arg(offline); +} + } // namespace dnf5 From 7d690a0e040fcadb7dc740efc0596c7c43ebe3cf Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Tue, 27 Feb 2024 20:08:32 +0000 Subject: [PATCH 03/14] system-upgrade: Add warning to `dnf5 offline _execute` Addresses https://github.com/rpm-software-management/dnf-plugins-core/issues/512 --- dnf5/commands/offline/offline.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp index 73a1cbfbf..94749dff2 100644 --- a/dnf5/commands/offline/offline.cpp +++ b/dnf5/commands/offline/offline.cpp @@ -377,6 +377,10 @@ void OfflineExecuteCommand::run() { log_status("Starting offline transaction. This will take a while.", libdnf5::offline::OFFLINE_STARTED_ID); + std::cout << "Warning: the `_execute` command is for internal use only and is not intended to be run directly by " + "the user. To initiate the system upgrade/offline transaction, you should run `dnf5 offline reboot`." + << std::endl; + if (!std::filesystem::is_symlink(get_magic_symlink())) { throw libdnf5::cli::CommandExitError(0, M_("Trigger file does not exist. Exiting.")); } From 9f3909d00fd894feced85cbc06707646f552a5d8 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Tue, 27 Feb 2024 20:13:04 +0000 Subject: [PATCH 04/14] system-upgrade: Add OFFLINE_STARTED_ID comment, cleanup --- dnf5/commands/offline/offline.hpp | 13 ------------- include/libdnf5/offline/offline.hpp | 4 ++++ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/dnf5/commands/offline/offline.hpp b/dnf5/commands/offline/offline.hpp index 523ce12c9..0e75d5126 100644 --- a/dnf5/commands/offline/offline.hpp +++ b/dnf5/commands/offline/offline.hpp @@ -65,19 +65,6 @@ class OfflineSubcommand : public Command { std::string system_releasever; }; -class OfflineDownloadCommand : public OfflineSubcommand { -public: - explicit OfflineDownloadCommand(Context & context) : OfflineSubcommand{context, "download"} {} - void set_argument_parser() override; - void configure() override; - void run() override; - -private: - libdnf5::OptionBool * no_downgrade{nullptr}; - libdnf5::OptionBool * distro_sync{nullptr}; - int state_version; -}; - class OfflineRebootCommand : public OfflineSubcommand { public: explicit OfflineRebootCommand(Context & context) : OfflineSubcommand(context, "reboot") {} diff --git a/include/libdnf5/offline/offline.hpp b/include/libdnf5/offline/offline.hpp index 988748672..44bdf03df 100644 --- a/include/libdnf5/offline/offline.hpp +++ b/include/libdnf5/offline/offline.hpp @@ -29,6 +29,10 @@ along with libdnf. If not, see . namespace libdnf5::offline { +// Unique identifiers used to mark and identify system-upgrade boots in +// journald logs. These are the same as they are in `dnf4 system-upgrade`, so +// `dnf5 offline log` will find offline transactions performed by DNF 4 and +// vice-versa. const std::string OFFLINE_STARTED_ID{"3e0a5636d16b4ca4bbe5321d06c6aa62"}; const std::string OFFLINE_FINISHED_ID{"8cec00a1566f4d3594f116450395f06c"}; From a0886d2eecd9583448b62f8b0894ab19d38ec12a Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Tue, 27 Feb 2024 21:02:05 +0000 Subject: [PATCH 05/14] system-upgrade: Handle _execute errors --- dnf5.spec | 1 + dnf5/commands/offline/offline.cpp | 3 +++ .../system/dnf5-offline-transaction-cleanup.service | 10 ++++++++++ .../systemd/system/dnf5-offline-transaction.service | 1 + 4 files changed, 15 insertions(+) create mode 100644 dnf5/config/systemd/system/dnf5-offline-transaction-cleanup.service diff --git a/dnf5.spec b/dnf5.spec index 7185dae58..02d503c70 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -259,6 +259,7 @@ It supports RPM packages, modulemd modules, and comps groups & environments. %dir %{_datadir}/dnf5/aliases.d %config %{_datadir}/dnf5/aliases.d/compatibility.conf %config %{_unitdir}/dnf5-offline-transaction.service +%config %{_unitdir}/dnf5-offline-transaction-cleanup.service %dir %{_libdir}/dnf5 %dir %{_libdir}/dnf5/plugins %dir %{_datadir}/dnf5/dnf5-plugins diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp index 94749dff2..eef597800 100644 --- a/dnf5/commands/offline/offline.cpp +++ b/dnf5/commands/offline/offline.cpp @@ -412,6 +412,9 @@ void OfflineExecuteCommand::run() { auto transaction = goal->resolve(); if (transaction.get_problems() != libdnf5::GoalProblem::NO_PROBLEM) { + std::cerr << "Failed to resolve transaction. This indicates some bigger problem, since the offline transaction " + "was already successfully resolved before. Was the cache at " + << datadir << " modified?" << std::endl; throw libdnf5::cli::GoalResolveError(transaction); } diff --git a/dnf5/config/systemd/system/dnf5-offline-transaction-cleanup.service b/dnf5/config/systemd/system/dnf5-offline-transaction-cleanup.service new file mode 100644 index 000000000..4179eeb68 --- /dev/null +++ b/dnf5/config/systemd/system/dnf5-offline-transaction-cleanup.service @@ -0,0 +1,10 @@ +[Unit] +Description=Offline upgrade/transaction using DNF 5 failed +DefaultDependencies=no + +[Service] +Type=oneshot +# Remove the symlink if it's still there, to protect against reboot loops. +ExecStart=/usr/bin/rm -fv /system-update +# If anything goes wrong, reboot back to the normal system. +ExecStart=/usr/bin/systemctl --no-block reboot diff --git a/dnf5/config/systemd/system/dnf5-offline-transaction.service b/dnf5/config/systemd/system/dnf5-offline-transaction.service index 91a26b5e8..335e71165 100644 --- a/dnf5/config/systemd/system/dnf5-offline-transaction.service +++ b/dnf5/config/systemd/system/dnf5-offline-transaction.service @@ -7,6 +7,7 @@ DefaultDependencies=no Requires=sysinit.target After=sysinit.target systemd-journald.socket system-update-pre.target Before=poweroff.target reboot.target shutdown.target system-update.target +OnFailure=dnf5-offline-transaction-cleanup.service [Service] # We are done when the script exits, not before From 8ae666f24dcbb734f2a5d0fb19c2764fbb2f6641 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Wed, 6 Mar 2024 00:02:20 +0000 Subject: [PATCH 06/14] system-upgrade: Fixes and cleanup, respond to review - Disable autocomplete for dnf5 offline _execute - Collapse SystemUpgradeSubcommand and its subclass SystemUpgradeDownloadCommand - Move include/libdnf5/offline/offline.hpp to dnf5/include/dnf5/offline.hpp - Add journald messages that use OFFLINE_FINISHED_ID, REBOOT_REQUESTED_ID, and DOWNLOAD_FINISHED_ID - Make Context.should_store_offline private with a getter and setter - Make cacheonly setting in `dnf5 offline _execute` actually work by applying it in `pre_configure`, not `configure` - Translate some strings --- dnf5/commands/offline/offline.cpp | 183 ++++++++++-------- dnf5/commands/offline/offline.hpp | 10 +- .../system-upgrade/system-upgrade.cpp | 74 ++++--- .../system-upgrade/system-upgrade.hpp | 35 +--- dnf5/context.cpp | 40 ++-- dnf5/include/dnf5/context.hpp | 9 +- .../offline => dnf5/include/dnf5}/offline.cpp | 10 +- .../offline => dnf5/include/dnf5}/offline.hpp | 25 ++- dnf5/shared_options.cpp | 8 +- 9 files changed, 195 insertions(+), 199 deletions(-) rename {libdnf5/offline => dnf5/include/dnf5}/offline.cpp (86%) rename {include/libdnf5/offline => dnf5/include/dnf5}/offline.hpp (82%) diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp index eef597800..1de6e7650 100644 --- a/dnf5/commands/offline/offline.cpp +++ b/dnf5/commands/offline/offline.cpp @@ -21,12 +21,11 @@ along with libdnf. If not, see . #include "utils/string.hpp" -#include "libdnf5/offline/offline.hpp" - #include #include #include #include +#include #include #include #include @@ -39,7 +38,7 @@ along with libdnf. If not, see . using namespace libdnf5::cli; -const std::string & ID_TO_IDENTIFY_BOOTS = libdnf5::offline::OFFLINE_STARTED_ID; +const std::string & ID_TO_IDENTIFY_BOOTS = dnf5::offline::OFFLINE_STARTED_ID; int call(const std::string & command, const std::vector & args) { std::vector c_args; @@ -74,6 +73,27 @@ int call(const std::string & command, const std::vector & args) { namespace dnf5 { +void offline::log_status( + const std::string & message, + const std::string & message_id, + const std::string & system_releasever, + const std::string & target_releasever) { + const auto & version = get_application_version(); + const std::string & version_string = fmt::format("{}.{}.{}", version.major, version.minor, version.micro); + sd_journal_send( + "MESSAGE=%s", + message.c_str(), + "MESSAGE_ID=%s", + message_id.c_str(), + "SYSTEM_RELEASEVER=%s", + system_releasever.c_str(), + "TARGET_RELEASEVER=%s", + target_releasever.c_str(), + "DNF_VERSION=%s", + version_string.c_str(), + NULL); +} + /// Helper for displaying messages with Plymouth /// /// Derived from DNF 4 system-upgrade PlymouthOutput implementation. Filters @@ -173,7 +193,7 @@ void OfflineCommand::set_parent_command() { } void OfflineCommand::set_argument_parser() { - get_argument_parser_command()->set_description("Manage offline transactions"); + get_argument_parser_command()->set_description(_("Manage offline transactions")); } void OfflineCommand::register_subcommands() { @@ -185,51 +205,21 @@ void OfflineCommand::register_subcommands() { OfflineSubcommand::OfflineSubcommand(Context & context, const std::string & name) : Command(context, name) {} -void OfflineSubcommand::set_argument_parser() { - auto & ctx = get_context(); - auto & parser = ctx.get_argument_parser(); - auto & cmd = *get_argument_parser_command(); - - cachedir = - dynamic_cast(parser.add_init_value(std::make_unique(datadir))); - - auto * download_dir_arg = parser.add_new_named_arg("downloaddir"); - download_dir_arg->set_long_name("downloaddir"); - download_dir_arg->set_description("Redirect download of packages to provided "); - download_dir_arg->link_value(cachedir); - cmd.register_named_arg(download_dir_arg); +void OfflineSubcommand::pre_configure() { + magic_symlink = "/system-update"; + datadir = dnf5::offline::get_offline_datadir(); + std::filesystem::create_directories(datadir); + state = std::make_optional(datadir / "offline-transaction-state.toml"); } void OfflineSubcommand::configure() { auto & ctx = get_context(); - const std::filesystem::path installroot{ctx.base.get_config().get_installroot_option().get_value()}; - magic_symlink = installroot / "system-update"; - datadir = libdnf5::offline::get_offline_datadir(installroot); - std::filesystem::create_directories(datadir); - state = std::make_optional(datadir / "offline-transaction-state.toml"); - + const std::filesystem::path installroot = ctx.base.get_config().get_installroot_option().get_value(); system_releasever = *libdnf5::Vars::detect_release(ctx.base.get_weak_ptr(), installroot); target_releasever = ctx.base.get_vars()->get_value("releasever"); } -void OfflineSubcommand::log_status(const std::string & message, const std::string & message_id) const { - const auto & version = get_application_version(); - const std::string & version_string = fmt::format("{}.{}.{}", version.major, version.minor, version.micro); - sd_journal_send( - "MESSAGE=%s", - message.c_str(), - "MESSAGE_ID=%s", - message_id.c_str(), - "SYSTEM_RELEASEVER=%s", - get_system_releasever().c_str(), - "TARGET_RELEASEVER=%s", - get_target_releasever().c_str(), - "DNF_VERSION=%s", - version_string.c_str(), - NULL); -} - -void check_state(const libdnf5::offline::OfflineTransactionState & state) { +void check_state(const dnf5::offline::OfflineTransactionState & state) { const auto & read_exception = state.get_read_exception(); if (read_exception != nullptr) { try { @@ -286,14 +276,15 @@ void OfflineRebootCommand::set_argument_parser() { auto & parser = ctx.get_argument_parser(); auto & cmd = *get_argument_parser_command(); - cmd.set_description("Prepare the system to perform the offline transaction and reboots to start the transaction."); + cmd.set_description( + _("Prepare the system to perform the offline transaction and reboots to start the transaction.")); poweroff_after = dynamic_cast(parser.add_init_value(std::make_unique(true))); auto * poweroff_after_arg = parser.add_new_named_arg("poweroff"); poweroff_after_arg->set_long_name("poweroff"); - poweroff_after_arg->set_description("Power off the system after the operation is complete"); + poweroff_after_arg->set_description(_("Power off the system after the operation is complete")); poweroff_after_arg->link_value(poweroff_after); cmd.register_named_arg(poweroff_after_arg); @@ -303,11 +294,12 @@ void OfflineRebootCommand::run() { auto & ctx = get_context(); check_state(*state); - if (state->get_data().status != libdnf5::offline::STATUS_DOWNLOAD_COMPLETE) { + if (state->get_data().status != dnf5::offline::STATUS_DOWNLOAD_COMPLETE && + state->get_data().status != dnf5::offline::STATUS_READY) { throw libdnf5::cli::CommandExitError(1, M_("system is not ready for offline transaction")); } if (std::filesystem::is_symlink(get_magic_symlink())) { - throw libdnf5::cli::CommandExitError(1, M_("offline {} is already scheduled"), state->get_data().verb); + throw libdnf5::cli::CommandExitError(1, M_("offline `{}` is already scheduled"), state->get_data().verb); } if (!std::filesystem::is_directory(get_datadir())) { @@ -315,12 +307,13 @@ void OfflineRebootCommand::run() { } if (state->get_data().verb == "system-upgrade download") { - std::cout << "The system will now reboot to upgrade to release version " << state->get_data().target_releasever - << "." << std::endl; + std::cout << _("The system will now reboot to upgrade to release version ") + << state->get_data().target_releasever << "." << std::endl; } else { - std::cout << "The system will now reboot to perform the offline transaction initiated by the following command:" - << std::endl - << "\t" << state->get_data().cmd_line << std::endl; + std::cout + << _("The system will now reboot to perform the offline transaction initiated by the following command:") + << std::endl + << "\t" << state->get_data().cmd_line << std::endl; } if (!libdnf5::cli::utils::userconfirm::userconfirm(ctx.base.get_config())) { return; @@ -328,29 +321,46 @@ void OfflineRebootCommand::run() { std::filesystem::create_symlink(get_datadir(), get_magic_symlink()); - state->get_data().status = libdnf5::offline::STATUS_READY; + state->get_data().status = dnf5::offline::STATUS_READY; state->get_data().poweroff_after = poweroff_after->get_value(); state->write(); + dnf5::offline::log_status( + "Rebooting to perform offline transaction.", + dnf5::offline::REBOOT_REQUESTED_ID, + get_system_releasever(), + get_target_releasever()); + reboot(poweroff_after->get_value()); } void OfflineExecuteCommand::set_argument_parser() { OfflineSubcommand::set_argument_parser(); auto & cmd = *get_argument_parser_command(); - cmd.set_description( - "Internal use only, not intended to be run by the user. Executes the transaction in the offline environment."); + cmd.set_complete(false); + cmd.set_description(_( + "Internal use only, not intended to be run by the user. Executes the transaction in the offline environment.")); } -void OfflineExecuteCommand::configure() { - OfflineSubcommand::configure(); +void OfflineExecuteCommand::pre_configure() { + OfflineSubcommand::pre_configure(); auto & ctx = get_context(); - ctx.set_load_system_repo(true); - ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); - check_state(*state); + // Don't try to refresh metadata, we are offline + ctx.base.get_config().get_cacheonly_option().set("all"); + // Don't ask any questions + ctx.base.get_config().get_assumeyes_option().set(true); + // Override `assumeno` too since it takes priority over `assumeyes` + ctx.base.get_config().get_assumeno_option().set(false); + // Upgrade operation already removes all element that must be removed. + // Additional removal could trigger unwanted changes in transaction. + ctx.base.get_config().get_clean_requirements_on_remove_option().set(false); + ctx.base.get_config().get_install_weak_deps_option().set(false); + // Get the cache from the cachedir specified in the state file + ctx.base.get_config().get_cachedir_option().set(state->get_data().cachedir); + // Set same set of enabled/disabled repos used during `system-upgrade download` for (const auto & repo_id : state->get_data().enabled_repos) { ctx.setopts.emplace_back(repo_id + ".enabled", "1"); @@ -358,28 +368,29 @@ void OfflineExecuteCommand::configure() { for (const auto & repo_id : state->get_data().disabled_repos) { ctx.setopts.emplace_back(repo_id + ".disabled", "1"); } +} - // Don't try to refresh metadata, we are offline - ctx.base.get_config().get_cacheonly_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, "all"); - // Don't ask any questions - ctx.base.get_config().get_assumeyes_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, true); - // Override `assumeno` too since it takes priority over `assumeyes` - ctx.base.get_config().get_assumeno_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, false); - // Upgrade operation already removes all element that must be removed. - // Additional removal could trigger unwanted changes in transaction. - ctx.base.get_config().get_clean_requirements_on_remove_option().set( - libdnf5::Option::Priority::PLUGINDEFAULT, false); - ctx.base.get_config().get_install_weak_deps_option().set(libdnf5::Option::Priority::PLUGINDEFAULT, false); +void OfflineExecuteCommand::configure() { + OfflineSubcommand::configure(); + auto & ctx = get_context(); + + ctx.set_load_system_repo(true); + ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); } void OfflineExecuteCommand::run() { auto & ctx = get_context(); - log_status("Starting offline transaction. This will take a while.", libdnf5::offline::OFFLINE_STARTED_ID); + dnf5::offline::log_status( + "Starting offline transaction. This will take a while.", + dnf5::offline::OFFLINE_STARTED_ID, + get_system_releasever(), + get_target_releasever()); - std::cout << "Warning: the `_execute` command is for internal use only and is not intended to be run directly by " - "the user. To initiate the system upgrade/offline transaction, you should run `dnf5 offline reboot`." - << std::endl; + std::cout + << _("Warning: the `_execute` command is for internal use only and is not intended to be run directly by " + "the user. To initiate the system upgrade/offline transaction, you should run `dnf5 offline reboot`.") + << std::endl; if (!std::filesystem::is_symlink(get_magic_symlink())) { throw libdnf5::cli::CommandExitError(0, M_("Trigger file does not exist. Exiting.")); @@ -392,14 +403,13 @@ void OfflineExecuteCommand::run() { std::filesystem::remove(get_magic_symlink()); - if (state->get_data().status != libdnf5::offline::STATUS_READY) { + if (state->get_data().status != dnf5::offline::STATUS_READY) { throw libdnf5::cli::CommandExitError(1, M_("Use `dnf5 offline reboot` to begin the transaction.")); } - const auto & installroot = get_context().base.get_config().get_installroot_option().get_value(); - const auto & datadir = libdnf5::offline::get_offline_datadir(installroot); + const auto & datadir = dnf5::offline::get_offline_datadir(); std::filesystem::create_directories(datadir); - const auto & transaction_json_path = datadir / libdnf5::offline::TRANSACTION_JSON_FILENAME; + const auto & transaction_json_path = datadir / dnf5::offline::TRANSACTION_JSON_FILENAME; auto transaction_json_file = libdnf5::utils::fs::File(transaction_json_path, "r"); const auto & json = transaction_json_file.read(); @@ -426,7 +436,7 @@ void OfflineExecuteCommand::run() { const auto result = transaction.run(); std::cout << std::endl; if (result != libdnf5::base::Transaction::TransactionRunResult::SUCCESS) { - std::cerr << "Transaction failed: " << libdnf5::base::Transaction::transaction_result_to_string(result) + std::cerr << _("Transaction failed: ") << libdnf5::base::Transaction::transaction_result_to_string(result) << std::endl; for (auto const & entry : transaction.get_gpg_signature_problems()) { std::cerr << entry << std::endl; @@ -447,8 +457,13 @@ void OfflineExecuteCommand::run() { } else { transaction_complete_message = "Transaction complete! Cleaning up and rebooting..."; } - plymouth.message(transaction_complete_message); - log_status(transaction_complete_message, libdnf5::offline::OFFLINE_STARTED_ID); + + plymouth.message(_(transaction_complete_message.c_str())); + dnf5::offline::log_status( + transaction_complete_message, + dnf5::offline::OFFLINE_FINISHED_ID, + get_system_releasever(), + get_target_releasever()); // If the transaction succeeded, remove downloaded data clean_datadir(ctx, get_datadir()); @@ -525,11 +540,11 @@ void list_logs() { const auto & boot_entries = find_boots(ID_TO_IDENTIFY_BOOTS); if (boot_entries.empty()) { - std::cout << "No logs were found." << std::endl; + std::cout << _("No logs were found.") << std::endl; return; } - std::cout << "The following boots appear to contain offline transaction logs:" << std::endl; + std::cout << _("The following boots appear to contain offline transaction logs:") << std::endl; for (size_t index = 0; index < boot_entries.size(); index += 1) { const auto & entry = boot_entries[index]; std::cout << fmt::format( @@ -564,7 +579,7 @@ void OfflineLogCommand::set_argument_parser() { auto & parser = ctx.get_argument_parser(); auto & cmd = *get_argument_parser_command(); - cmd.set_description("Show logs from past offline transactions"); + cmd.set_description(_("Show logs from past offline transactions")); number = dynamic_cast(parser.add_init_value(std::make_unique(""))); @@ -572,7 +587,7 @@ void OfflineLogCommand::set_argument_parser() { number_arg->set_long_name("number"); number_arg->set_has_value(true); - number_arg->set_description("Which log to show. Run without any arguments to get a list of available logs."); + number_arg->set_description(_("Which log to show. Run without any arguments to get a list of available logs.")); number_arg->link_value(number); cmd.register_named_arg(number_arg); } diff --git a/dnf5/commands/offline/offline.hpp b/dnf5/commands/offline/offline.hpp index 0e75d5126..32c3868cc 100644 --- a/dnf5/commands/offline/offline.hpp +++ b/dnf5/commands/offline/offline.hpp @@ -21,10 +21,10 @@ along with libdnf. If not, see . #define DNF5_COMMANDS_OFFLINE_HPP #include +#include #include #include #include -#include #include #include @@ -45,20 +45,17 @@ class OfflineCommand : public Command { class OfflineSubcommand : public Command { public: explicit OfflineSubcommand(Context & context, const std::string & name); - void set_argument_parser() override; + void pre_configure() override; void configure() override; protected: - libdnf5::OptionPath * get_cachedir() const { return cachedir; }; std::filesystem::path get_magic_symlink() const { return magic_symlink; }; std::filesystem::path get_datadir() const { return datadir; }; - std::optional state; + std::optional state; std::string get_system_releasever() const { return system_releasever; }; std::string get_target_releasever() const { return target_releasever; }; - void log_status(const std::string & message, const std::string & message_id) const; private: - libdnf5::OptionPath * cachedir{nullptr}; std::filesystem::path magic_symlink; std::filesystem::path datadir; std::string target_releasever; @@ -79,6 +76,7 @@ class OfflineExecuteCommand : public OfflineSubcommand { public: explicit OfflineExecuteCommand(Context & context) : OfflineSubcommand(context, "_execute") {} void set_argument_parser() override; + void pre_configure() override; void configure() override; void run() override; }; diff --git a/dnf5/commands/system-upgrade/system-upgrade.cpp b/dnf5/commands/system-upgrade/system-upgrade.cpp index 101c30471..12fc531dd 100644 --- a/dnf5/commands/system-upgrade/system-upgrade.cpp +++ b/dnf5/commands/system-upgrade/system-upgrade.cpp @@ -21,6 +21,8 @@ along with libdnf. If not, see . #include "../offline/offline.hpp" +#include "libdnf5/utils/bgettext/bgettext-lib.h" + #include #include #include @@ -49,7 +51,7 @@ void SystemUpgradeCommand::set_parent_command() { } void SystemUpgradeCommand::set_argument_parser() { - get_argument_parser_command()->set_description("Prepare system for upgrade to a new release"); + get_argument_parser_command()->set_description(_("Prepare system for upgrade to a new release")); } void SystemUpgradeCommand::register_subcommands() { @@ -59,43 +61,22 @@ void SystemUpgradeCommand::register_subcommands() { register_subcommand(std::make_unique(get_context())); } -SystemUpgradeSubcommand::SystemUpgradeSubcommand(Context & context, const std::string & name) - : Command(context, name), - datadir(std::filesystem::path{libdnf5::SYSTEM_STATE_DIR} / "system-upgrade"), - state(datadir / "state.toml") {} - -void SystemUpgradeSubcommand::set_argument_parser() { +void SystemUpgradeDownloadCommand::set_argument_parser() { auto & ctx = get_context(); auto & parser = ctx.get_argument_parser(); auto & cmd = *get_argument_parser_command(); - cachedir = - dynamic_cast(parser.add_init_value(std::make_unique(datadir))); + cmd.set_description(_("Downloads everything needed to upgrade to a new release")); + + download_dir = dynamic_cast( + parser.add_init_value(std::make_unique(dnf5::offline::DEFAULT_DATADIR))); auto * download_dir_arg = parser.add_new_named_arg("downloaddir"); download_dir_arg->set_long_name("downloaddir"); - download_dir_arg->set_description("Redirect download of packages to provided "); - download_dir_arg->link_value(cachedir); + download_dir_arg->set_has_value(true); + download_dir_arg->set_description(_("Redirect download of packages to provided ")); + download_dir_arg->link_value(download_dir); cmd.register_named_arg(download_dir_arg); -} - -void SystemUpgradeSubcommand::configure() { - auto & ctx = get_context(); - const std::filesystem::path installroot{ctx.base.get_config().get_installroot_option().get_value()}; - magic_symlink = installroot / "system-update"; - - system_releasever = *libdnf5::Vars::detect_release(ctx.base.get_weak_ptr(), installroot); - target_releasever = ctx.base.get_vars()->get_value("releasever"); -} - -void SystemUpgradeDownloadCommand::set_argument_parser() { - SystemUpgradeSubcommand::set_argument_parser(); - - auto & ctx = get_context(); - auto & parser = ctx.get_argument_parser(); - auto & cmd = *get_argument_parser_command(); - - cmd.set_description("Downloads everything needed to upgrade to a new release"); no_downgrade = dynamic_cast(parser.add_init_value(std::make_unique(true))); @@ -103,27 +84,37 @@ void SystemUpgradeDownloadCommand::set_argument_parser() { auto * no_downgrade_arg = parser.add_new_named_arg("no-downgrade"); no_downgrade_arg->set_long_name("no-downgrade"); no_downgrade_arg->set_description( - "Do not install packages from the new release if they are older than what is currently installed"); + _("Do not install packages from the new release if they are older than what is currently installed")); no_downgrade_arg->link_value(no_downgrade); - cmd.register_named_arg(no_downgrade_arg); } void SystemUpgradeDownloadCommand::configure() { - SystemUpgradeSubcommand::configure(); - auto & ctx = get_context(); + const std::filesystem::path installroot{ctx.base.get_config().get_installroot_option().get_value()}; + + system_releasever = *libdnf5::Vars::detect_release(ctx.base.get_weak_ptr(), installroot); + target_releasever = ctx.base.get_vars()->get_value("releasever"); + // Check --releasever - if (get_target_releasever() == get_system_releasever()) { + if (target_releasever == system_releasever) { throw libdnf5::cli::CommandExitError(1, M_("Need a --releasever greater than the current system version.")); } ctx.set_load_system_repo(true); ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); - ctx.base.get_config().get_cachedir_option().set( - libdnf5::Option::Priority::PLUGINDEFAULT, get_cachedir()->get_value()); + // Specifically for system-upgrade, the `downloaddir` argument should take + // priority over the `cachedir` specified by the user. We also prepend the + // installroot here since it won't be done in Base::setup if the option + // priority is greater than Priority::COMMANDLINE. + if (download_dir->get_priority() < libdnf5::Option::Priority::COMMANDLINE) { + ctx.base.get_config().get_cachedir_option().set( + libdnf5::Option::Priority::RUNTIME, installroot / download_dir->get_value()); + } else { + ctx.base.get_config().get_cachedir_option().set(libdnf5::Option::Priority::RUNTIME, download_dir->get_value()); + } } void SystemUpgradeDownloadCommand::run() { @@ -147,12 +138,15 @@ void SystemUpgradeDownloadCommand::run() { 1, M_("The system-upgrade transaction is empty; your system is already up-to-date.")); } - ctx.should_store_offline = true; + ctx.set_should_store_offline(true); ctx.download_and_run(transaction); - std::cout << "Download complete! Use `dnf5 system-upgrade reboot` to start the upgrade." << std::endl - << "To cancel the upgrade and delete the downloaded upgrade files, use `dnf5 system-upgrade clean`." + std::cout << _("Download complete! Use `dnf5 system-upgrade reboot` to start the upgrade.\n" + "To cancel the upgrade and delete the downloaded upgrade files, use `dnf5 system-upgrade clean`.") << std::endl; + + dnf5::offline::log_status( + "Download finished.", dnf5::offline::DOWNLOAD_FINISHED_ID, system_releasever, target_releasever); } } // namespace dnf5 diff --git a/dnf5/commands/system-upgrade/system-upgrade.hpp b/dnf5/commands/system-upgrade/system-upgrade.hpp index 19f44799d..9b03a0a22 100644 --- a/dnf5/commands/system-upgrade/system-upgrade.hpp +++ b/dnf5/commands/system-upgrade/system-upgrade.hpp @@ -21,7 +21,7 @@ along with libdnf. If not, see . #define DNF5_COMMANDS_SYSTEM_UPGRADE_HPP #include -#include +#include namespace dnf5 { @@ -34,40 +34,19 @@ class SystemUpgradeCommand : public Command { void pre_configure() override; }; -class SystemUpgradeSubcommand : public Command { +class SystemUpgradeDownloadCommand : public Command { public: - explicit SystemUpgradeSubcommand(Context & context, const std::string & name); - void set_argument_parser() override; - void configure() override; - -protected: - libdnf5::OptionPath * get_cachedir() const { return cachedir; }; - std::filesystem::path get_datadir() const { return datadir; }; - std::filesystem::path get_magic_symlink() const { return magic_symlink; }; - libdnf5::offline::OfflineTransactionState get_state() const { return state; }; - std::string get_system_releasever() const { return system_releasever; }; - std::string get_target_releasever() const { return target_releasever; }; - void log_status(const std::string & message, const std::string & message_id) const; - -private: - libdnf5::OptionPath * cachedir{nullptr}; - std::filesystem::path datadir{libdnf5::offline::DEFAULT_DATADIR}; - std::filesystem::path magic_symlink; - libdnf5::offline::OfflineTransactionState state; - std::string target_releasever; - std::string system_releasever; -}; - -class SystemUpgradeDownloadCommand : public SystemUpgradeSubcommand { -public: - explicit SystemUpgradeDownloadCommand(Context & context) : SystemUpgradeSubcommand{context, "download"} {} + explicit SystemUpgradeDownloadCommand(Context & context) : Command{context, "download"} {} void set_argument_parser() override; void configure() override; void run() override; private: libdnf5::OptionBool * no_downgrade{nullptr}; - int state_version; + libdnf5::OptionPath * download_dir{nullptr}; + std::filesystem::path datadir{dnf5::offline::DEFAULT_DATADIR}; + std::string target_releasever; + std::string system_releasever; }; } // namespace dnf5 diff --git a/dnf5/context.cpp b/dnf5/context.cpp index a03b6b036..f6d23446d 100644 --- a/dnf5/context.cpp +++ b/dnf5/context.cpp @@ -19,13 +19,12 @@ along with libdnf. If not, see . #include "dnf5/context.hpp" +#include "dnf5/offline.hpp" #include "download_callbacks.hpp" #include "plugins.hpp" #include "utils/string.hpp" #include "utils/url.hpp" -#include "libdnf5/offline/offline.hpp" - #include #include #include @@ -188,13 +187,11 @@ void Context::load_repos(bool load_system) { } RpmTransCB::RpmTransCB(Context & context) : context(context) { - multi_progress_bar.set_total_bar_visible_limit( - libdnf5::cli::progressbar::MultiProgressBar::NEVER_VISIBLE_LIMIT); + multi_progress_bar.set_total_bar_visible_limit(libdnf5::cli::progressbar::MultiProgressBar::NEVER_VISIBLE_LIMIT); } RpmTransCB::~RpmTransCB() { - if (active_progress_bar && - active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { + if (active_progress_bar && active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); } if (active_progress_bar) { @@ -202,12 +199,12 @@ RpmTransCB::~RpmTransCB() { } } -libdnf5::cli::progressbar::MultiProgressBar * RpmTransCB::get_multi_progress_bar() { return &multi_progress_bar; } +libdnf5::cli::progressbar::MultiProgressBar * RpmTransCB::get_multi_progress_bar() { + return &multi_progress_bar; +} void RpmTransCB::install_progress( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - uint64_t amount, - [[maybe_unused]] uint64_t total) { + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, uint64_t amount, [[maybe_unused]] uint64_t total) { active_progress_bar->set_ticks(static_cast(amount)); if (is_time_to_print()) { multi_progress_bar.print(); @@ -271,9 +268,7 @@ void RpmTransCB::transaction_stop([[maybe_unused]] uint64_t total) { } void RpmTransCB::uninstall_progress( - [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, - uint64_t amount, - [[maybe_unused]] uint64_t total) { + [[maybe_unused]] const libdnf5::rpm::TransactionItem & item, uint64_t amount, [[maybe_unused]] uint64_t total) { active_progress_bar->set_ticks(static_cast(amount)); if (is_time_to_print()) { multi_progress_bar.print(); @@ -374,8 +369,7 @@ void RpmTransCB::verify_stop([[maybe_unused]] uint64_t total) { } void RpmTransCB::new_progress_bar(int64_t total, const std::string & descr) { - if (active_progress_bar && - active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { + if (active_progress_bar && active_progress_bar->get_state() != libdnf5::cli::progressbar::ProgressBarState::ERROR) { active_progress_bar->set_state(libdnf5::cli::progressbar::ProgressBarState::SUCCESS); } auto progress_bar = @@ -463,14 +457,13 @@ void Context::download_and_run(libdnf5::base::Transaction & transaction) { } void Context::store_offline(libdnf5::base::Transaction & transaction) { - const auto & installroot = base.get_config().get_installroot_option().get_value(); - const auto & offline_datadir = libdnf5::offline::get_offline_datadir(installroot); + const auto & offline_datadir = dnf5::offline::get_offline_datadir(); std::filesystem::create_directories(offline_datadir); - const std::filesystem::path state_path{offline_datadir / libdnf5::offline::TRANSACTION_STATE_FILENAME}; - libdnf5::offline::OfflineTransactionState state{state_path}; + const std::filesystem::path state_path{offline_datadir / dnf5::offline::TRANSACTION_STATE_FILENAME}; + dnf5::offline::OfflineTransactionState state{state_path}; - if (state.get_data().status != libdnf5::offline::STATUS_DOWNLOAD_INCOMPLETE) { + if (state.get_data().status != dnf5::offline::STATUS_DOWNLOAD_INCOMPLETE) { std::cout << "There is already an offline transaction queued, initiated by the following command:" << std::endl << "\t" << state.get_data().cmd_line << std::endl << "Continuing will cancel the old offline transaction and replace it with this one." << std::endl; @@ -479,7 +472,7 @@ void Context::store_offline(libdnf5::base::Transaction & transaction) { } } - const std::filesystem::path transaction_json_path{offline_datadir / libdnf5::offline::TRANSACTION_JSON_FILENAME}; + const std::filesystem::path transaction_json_path{offline_datadir / dnf5::offline::TRANSACTION_JSON_FILENAME}; const std::string json = transaction.serialize(); @@ -488,18 +481,19 @@ void Context::store_offline(libdnf5::base::Transaction & transaction) { transaction_json_file.write(json); transaction_json_file.close(); - state.get_data().status = libdnf5::offline::STATUS_DOWNLOAD_COMPLETE; + state.get_data().status = dnf5::offline::STATUS_DOWNLOAD_COMPLETE; state.get_data().cachedir = base.get_config().get_cachedir_option().get_value(); std::vector command_vector; auto * current_command = get_selected_command(); while (current_command != get_root_command()) { - command_vector.emplace_back(current_command->get_argument_parser_command()->get_id()); + command_vector.insert(command_vector.begin(), current_command->get_argument_parser_command()->get_id()); current_command = current_command->get_parent_command(); } state.get_data().verb = libdnf5::utils::string::join(command_vector, " "); state.get_data().cmd_line = get_cmdline(); + const auto & installroot = base.get_config().get_installroot_option().get_value(); state.get_data().system_releasever = *libdnf5::Vars::detect_release(base.get_weak_ptr(), installroot); state.get_data().target_releasever = base.get_vars()->get_value("releasever"); diff --git a/dnf5/include/dnf5/context.hpp b/dnf5/include/dnf5/context.hpp index 7ca88c1bc..0664d4f6c 100644 --- a/dnf5/include/dnf5/context.hpp +++ b/dnf5/include/dnf5/context.hpp @@ -79,8 +79,6 @@ class Context : public libdnf5::cli::session::Session { std::vector enable_plugins_patterns; std::vector disable_plugins_patterns; - bool should_store_offline = false; - void store_offline(libdnf5::base::Transaction & transaction); /// Gets user comment. @@ -147,12 +145,19 @@ class Context : public libdnf5::cli::session::Session { void set_output_stream(std::ostream & new_output_stream) { output_stream = new_output_stream; } + // Store the transaction to be run later in a minimal boot environment, + // using `dnf5 offline` + void set_should_store_offline(bool should_store_offline) { this->should_store_offline = should_store_offline; } + bool get_should_store_offline() const { return should_store_offline; } + private: std::string cmdline; /// Points to user comment. const char * comment{nullptr}; + bool should_store_offline = false; + bool quiet{false}; bool dump_main_config{false}; std::vector dump_repo_config_id_list; diff --git a/libdnf5/offline/offline.cpp b/dnf5/include/dnf5/offline.cpp similarity index 86% rename from libdnf5/offline/offline.cpp rename to dnf5/include/dnf5/offline.cpp index 85de7294b..ddb913bfc 100644 --- a/libdnf5/offline/offline.cpp +++ b/dnf5/include/dnf5/offline.cpp @@ -17,11 +17,11 @@ You should have received a copy of the GNU Lesser General Public License along with libdnf. If not, see . */ -#include +#include #include #include -namespace libdnf5::offline { +namespace dnf5::offline { OfflineTransactionState::OfflineTransactionState(std::filesystem::path path) : path(std::move(path)) { read(); @@ -44,8 +44,8 @@ void OfflineTransactionState::write() { file.close(); } -std::filesystem::path get_offline_datadir(const std::filesystem::path & installroot) { - return installroot / DEFAULT_DATADIR.relative_path(); +std::filesystem::path get_offline_datadir() { + return DEFAULT_DATADIR; } -} // namespace libdnf5::offline +} // namespace dnf5::offline diff --git a/include/libdnf5/offline/offline.hpp b/dnf5/include/dnf5/offline.hpp similarity index 82% rename from include/libdnf5/offline/offline.hpp rename to dnf5/include/dnf5/offline.hpp index 44bdf03df..e2bb79c9d 100644 --- a/include/libdnf5/offline/offline.hpp +++ b/dnf5/include/dnf5/offline.hpp @@ -17,22 +17,27 @@ You should have received a copy of the GNU Lesser General Public License along with libdnf. If not, see . */ -#ifndef LIBDNF5_OFFLINE_HPP -#define LIBDNF5_OFFLINE_HPP +#ifndef DNF5_OFFLINE_HPP +#define DNF5_OFFLINE_HPP + +#include "dnf5/version.hpp" #include #include #include #include #include +#include #include -namespace libdnf5::offline { +namespace dnf5::offline { // Unique identifiers used to mark and identify system-upgrade boots in // journald logs. These are the same as they are in `dnf4 system-upgrade`, so // `dnf5 offline log` will find offline transactions performed by DNF 4 and // vice-versa. +const std::string DOWNLOAD_FINISHED_ID{"9348174c5cc74001a71ef26bd79d302e"}; +const std::string REBOOT_REQUESTED_ID{"9348174c5cc74001a71ef26bd79d302e"}; const std::string OFFLINE_STARTED_ID{"3e0a5636d16b4ca4bbe5321d06c6aa62"}; const std::string OFFLINE_FINISHED_ID{"8cec00a1566f4d3594f116450395f06c"}; @@ -48,7 +53,7 @@ const std::filesystem::path DEFAULT_DATADIR{std::filesystem::path(libdnf5::SYSTE const std::filesystem::path TRANSACTION_STATE_FILENAME{"offline-transaction-state.toml"}; const std::filesystem::path TRANSACTION_JSON_FILENAME{"offline-transaction.json"}; -std::filesystem::path get_offline_datadir(const std::filesystem::path & installroot); +std::filesystem::path get_offline_datadir(); struct OfflineTransactionStateData { int state_version = STATE_VERSION; @@ -77,10 +82,16 @@ class OfflineTransactionState { OfflineTransactionStateData data; }; -} // namespace libdnf5::offline +void log_status( + const std::string & message, + const std::string & message_id, + const std::string & system_releasever, + const std::string & target_releasever); + +} // namespace dnf5::offline TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( - libdnf5::offline::OfflineTransactionStateData, + dnf5::offline::OfflineTransactionStateData, state_version, status, cachedir, @@ -92,4 +103,4 @@ TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( enabled_repos, disabled_repos) -#endif // LIBDNF5_OFFLINE_HPP +#endif // DNF5_OFFLINE_HPP diff --git a/dnf5/shared_options.cpp b/dnf5/shared_options.cpp index 8a35e9206..86a07de60 100644 --- a/dnf5/shared_options.cpp +++ b/dnf5/shared_options.cpp @@ -20,9 +20,10 @@ along with libdnf. If not, see . #include "dnf5/shared_options.hpp" +#include "dnf5/offline.hpp" + #include "libdnf5/utils/bgettext/bgettext-mark-domain.h" -#include #include namespace dnf5 { @@ -120,12 +121,11 @@ void create_offline_option(dnf5::Command & command) { [[maybe_unused]] libdnf5::cli::ArgumentParser::NamedArg * arg, [[maybe_unused]] const char * option, [[maybe_unused]] const char * value) { - const auto & installroot = ctx.base.get_config().get_installroot_option().get_value(); - const auto & offline_datadir = libdnf5::offline::get_offline_datadir(installroot); + const auto & offline_datadir = dnf5::offline::get_offline_datadir(); std::filesystem::create_directories(offline_datadir); ctx.base.get_config().get_cachedir_option().set(libdnf5::Option::Priority::RUNTIME, offline_datadir); - ctx.should_store_offline = true; + ctx.set_should_store_offline(true); return true; }); command.get_argument_parser_command()->register_named_arg(offline); From e0a09b6e1ee711adc1f6915cd298bb5071d7b87c Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Wed, 6 Mar 2024 22:42:08 +0000 Subject: [PATCH 07/14] system-upgrade: Fix installroot --installroot with `system-upgrade` and `offline` should now work as it does in DNF 4. --- dnf5/commands/offline/offline.cpp | 42 ++++++++++--------- dnf5/commands/offline/offline.hpp | 1 - .../system-upgrade/system-upgrade.cpp | 27 +++--------- .../system-upgrade/system-upgrade.hpp | 1 - dnf5/context.cpp | 9 ++-- dnf5/include/dnf5/offline.cpp | 4 -- dnf5/include/dnf5/offline.hpp | 2 - dnf5/shared_options.cpp | 8 ++-- 8 files changed, 38 insertions(+), 56 deletions(-) diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp index 1de6e7650..ebee1c2a5 100644 --- a/dnf5/commands/offline/offline.cpp +++ b/dnf5/commands/offline/offline.cpp @@ -205,17 +205,19 @@ void OfflineCommand::register_subcommands() { OfflineSubcommand::OfflineSubcommand(Context & context, const std::string & name) : Command(context, name) {} -void OfflineSubcommand::pre_configure() { +void OfflineSubcommand::configure() { + auto & ctx = get_context(); magic_symlink = "/system-update"; - datadir = dnf5::offline::get_offline_datadir(); + + const std::filesystem::path installroot = ctx.base.get_config().get_installroot_option().get_value(); + datadir = installroot / dnf5::offline::DEFAULT_DATADIR.relative_path(); std::filesystem::create_directories(datadir); state = std::make_optional(datadir / "offline-transaction-state.toml"); -} -void OfflineSubcommand::configure() { - auto & ctx = get_context(); - const std::filesystem::path installroot = ctx.base.get_config().get_installroot_option().get_value(); - system_releasever = *libdnf5::Vars::detect_release(ctx.base.get_weak_ptr(), installroot); + const auto & detected_releasever = libdnf5::Vars::detect_release(ctx.base.get_weak_ptr(), installroot); + if (detected_releasever != nullptr) { + system_releasever = *detected_releasever; + } target_releasever = ctx.base.get_vars()->get_value("releasever"); } @@ -343,11 +345,8 @@ void OfflineExecuteCommand::set_argument_parser() { } void OfflineExecuteCommand::pre_configure() { - OfflineSubcommand::pre_configure(); auto & ctx = get_context(); - check_state(*state); - // Don't try to refresh metadata, we are offline ctx.base.get_config().get_cacheonly_option().set("all"); // Don't ask any questions @@ -358,7 +357,19 @@ void OfflineExecuteCommand::pre_configure() { // Additional removal could trigger unwanted changes in transaction. ctx.base.get_config().get_clean_requirements_on_remove_option().set(false); ctx.base.get_config().get_install_weak_deps_option().set(false); +} + +void OfflineExecuteCommand::configure() { + OfflineSubcommand::configure(); + auto & ctx = get_context(); + + check_state(*state); + + ctx.set_load_system_repo(true); + ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); + // Get the cache from the cachedir specified in the state file + ctx.base.get_config().get_system_cachedir_option().set(state->get_data().cachedir); ctx.base.get_config().get_cachedir_option().set(state->get_data().cachedir); // Set same set of enabled/disabled repos used during `system-upgrade download` @@ -370,14 +381,6 @@ void OfflineExecuteCommand::pre_configure() { } } -void OfflineExecuteCommand::configure() { - OfflineSubcommand::configure(); - auto & ctx = get_context(); - - ctx.set_load_system_repo(true); - ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); -} - void OfflineExecuteCommand::run() { auto & ctx = get_context(); @@ -407,7 +410,8 @@ void OfflineExecuteCommand::run() { throw libdnf5::cli::CommandExitError(1, M_("Use `dnf5 offline reboot` to begin the transaction.")); } - const auto & datadir = dnf5::offline::get_offline_datadir(); + const auto & installroot = get_context().base.get_config().get_installroot_option().get_value(); + const auto & datadir = installroot / dnf5::offline::DEFAULT_DATADIR.relative_path(); std::filesystem::create_directories(datadir); const auto & transaction_json_path = datadir / dnf5::offline::TRANSACTION_JSON_FILENAME; auto transaction_json_file = libdnf5::utils::fs::File(transaction_json_path, "r"); diff --git a/dnf5/commands/offline/offline.hpp b/dnf5/commands/offline/offline.hpp index 32c3868cc..45ad33bdb 100644 --- a/dnf5/commands/offline/offline.hpp +++ b/dnf5/commands/offline/offline.hpp @@ -45,7 +45,6 @@ class OfflineCommand : public Command { class OfflineSubcommand : public Command { public: explicit OfflineSubcommand(Context & context, const std::string & name); - void pre_configure() override; void configure() override; protected: diff --git a/dnf5/commands/system-upgrade/system-upgrade.cpp b/dnf5/commands/system-upgrade/system-upgrade.cpp index 12fc531dd..2327e16d1 100644 --- a/dnf5/commands/system-upgrade/system-upgrade.cpp +++ b/dnf5/commands/system-upgrade/system-upgrade.cpp @@ -68,16 +68,6 @@ void SystemUpgradeDownloadCommand::set_argument_parser() { cmd.set_description(_("Downloads everything needed to upgrade to a new release")); - download_dir = dynamic_cast( - parser.add_init_value(std::make_unique(dnf5::offline::DEFAULT_DATADIR))); - - auto * download_dir_arg = parser.add_new_named_arg("downloaddir"); - download_dir_arg->set_long_name("downloaddir"); - download_dir_arg->set_has_value(true); - download_dir_arg->set_description(_("Redirect download of packages to provided ")); - download_dir_arg->link_value(download_dir); - cmd.register_named_arg(download_dir_arg); - no_downgrade = dynamic_cast(parser.add_init_value(std::make_unique(true))); @@ -94,7 +84,11 @@ void SystemUpgradeDownloadCommand::configure() { const std::filesystem::path installroot{ctx.base.get_config().get_installroot_option().get_value()}; - system_releasever = *libdnf5::Vars::detect_release(ctx.base.get_weak_ptr(), installroot); + const auto & detected_releasever = libdnf5::Vars::detect_release(ctx.base.get_weak_ptr(), installroot); + if (detected_releasever == nullptr) { + throw libdnf5::cli::CommandExitError(1, M_("Couldn't detect the current release version of the system.")); + } + system_releasever = *detected_releasever; target_releasever = ctx.base.get_vars()->get_value("releasever"); // Check --releasever @@ -104,17 +98,6 @@ void SystemUpgradeDownloadCommand::configure() { ctx.set_load_system_repo(true); ctx.set_load_available_repos(Context::LoadAvailableRepos::ENABLED); - - // Specifically for system-upgrade, the `downloaddir` argument should take - // priority over the `cachedir` specified by the user. We also prepend the - // installroot here since it won't be done in Base::setup if the option - // priority is greater than Priority::COMMANDLINE. - if (download_dir->get_priority() < libdnf5::Option::Priority::COMMANDLINE) { - ctx.base.get_config().get_cachedir_option().set( - libdnf5::Option::Priority::RUNTIME, installroot / download_dir->get_value()); - } else { - ctx.base.get_config().get_cachedir_option().set(libdnf5::Option::Priority::RUNTIME, download_dir->get_value()); - } } void SystemUpgradeDownloadCommand::run() { diff --git a/dnf5/commands/system-upgrade/system-upgrade.hpp b/dnf5/commands/system-upgrade/system-upgrade.hpp index 9b03a0a22..019829b39 100644 --- a/dnf5/commands/system-upgrade/system-upgrade.hpp +++ b/dnf5/commands/system-upgrade/system-upgrade.hpp @@ -43,7 +43,6 @@ class SystemUpgradeDownloadCommand : public Command { private: libdnf5::OptionBool * no_downgrade{nullptr}; - libdnf5::OptionPath * download_dir{nullptr}; std::filesystem::path datadir{dnf5::offline::DEFAULT_DATADIR}; std::string target_releasever; std::string system_releasever; diff --git a/dnf5/context.cpp b/dnf5/context.cpp index f6d23446d..9184c4fca 100644 --- a/dnf5/context.cpp +++ b/dnf5/context.cpp @@ -457,7 +457,8 @@ void Context::download_and_run(libdnf5::base::Transaction & transaction) { } void Context::store_offline(libdnf5::base::Transaction & transaction) { - const auto & offline_datadir = dnf5::offline::get_offline_datadir(); + const auto & installroot = base.get_config().get_installroot_option().get_value(); + const auto & offline_datadir = installroot / dnf5::offline::DEFAULT_DATADIR.relative_path(); std::filesystem::create_directories(offline_datadir); const std::filesystem::path state_path{offline_datadir / dnf5::offline::TRANSACTION_STATE_FILENAME}; @@ -493,8 +494,10 @@ void Context::store_offline(libdnf5::base::Transaction & transaction) { state.get_data().verb = libdnf5::utils::string::join(command_vector, " "); state.get_data().cmd_line = get_cmdline(); - const auto & installroot = base.get_config().get_installroot_option().get_value(); - state.get_data().system_releasever = *libdnf5::Vars::detect_release(base.get_weak_ptr(), installroot); + const auto & detected_releasever = libdnf5::Vars::detect_release(base.get_weak_ptr(), installroot); + if (detected_releasever != nullptr) { + state.get_data().system_releasever = *detected_releasever; + } state.get_data().target_releasever = base.get_vars()->get_value("releasever"); state.write(); diff --git a/dnf5/include/dnf5/offline.cpp b/dnf5/include/dnf5/offline.cpp index ddb913bfc..652646109 100644 --- a/dnf5/include/dnf5/offline.cpp +++ b/dnf5/include/dnf5/offline.cpp @@ -44,8 +44,4 @@ void OfflineTransactionState::write() { file.close(); } -std::filesystem::path get_offline_datadir() { - return DEFAULT_DATADIR; -} - } // namespace dnf5::offline diff --git a/dnf5/include/dnf5/offline.hpp b/dnf5/include/dnf5/offline.hpp index e2bb79c9d..f733ab021 100644 --- a/dnf5/include/dnf5/offline.hpp +++ b/dnf5/include/dnf5/offline.hpp @@ -53,8 +53,6 @@ const std::filesystem::path DEFAULT_DATADIR{std::filesystem::path(libdnf5::SYSTE const std::filesystem::path TRANSACTION_STATE_FILENAME{"offline-transaction-state.toml"}; const std::filesystem::path TRANSACTION_JSON_FILENAME{"offline-transaction.json"}; -std::filesystem::path get_offline_datadir(); - struct OfflineTransactionStateData { int state_version = STATE_VERSION; std::string status = STATUS_DOWNLOAD_INCOMPLETE; diff --git a/dnf5/shared_options.cpp b/dnf5/shared_options.cpp index 86a07de60..0dcaf6bf6 100644 --- a/dnf5/shared_options.cpp +++ b/dnf5/shared_options.cpp @@ -121,10 +121,10 @@ void create_offline_option(dnf5::Command & command) { [[maybe_unused]] libdnf5::cli::ArgumentParser::NamedArg * arg, [[maybe_unused]] const char * option, [[maybe_unused]] const char * value) { - const auto & offline_datadir = dnf5::offline::get_offline_datadir(); - std::filesystem::create_directories(offline_datadir); - - ctx.base.get_config().get_cachedir_option().set(libdnf5::Option::Priority::RUNTIME, offline_datadir); + // The installroot will be prepended to the cachedir and system_cachedir later in Base.setup() + const auto & offline_datadir = dnf5::offline::DEFAULT_DATADIR; + ctx.base.get_config().get_system_cachedir_option().set(libdnf5::Option::Priority::INSTALLROOT, offline_datadir); + ctx.base.get_config().get_cachedir_option().set(libdnf5::Option::Priority::INSTALLROOT, offline_datadir); ctx.set_should_store_offline(true); return true; }); From 73a7da5d2f99389bb0877f0e0a6ae896af8a372e Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Fri, 8 Mar 2024 19:06:08 +0000 Subject: [PATCH 08/14] system-upgrade: offline-distrosync, offline-upgrade aliases --- .../share/dnf5/aliases.d/compatibility.conf | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/dnf5/config/usr/share/dnf5/aliases.d/compatibility.conf b/dnf5/config/usr/share/dnf5/aliases.d/compatibility.conf index e0776f297..40cb4698f 100644 --- a/dnf5/config/usr/share/dnf5/aliases.d/compatibility.conf +++ b/dnf5/config/usr/share/dnf5/aliases.d/compatibility.conf @@ -131,6 +131,24 @@ descr = "Alias for 'makecache'" group_id = 'commands-compatibility-aliases' complete = false +['offline-distrosync'] +type = 'command' +attached_command = 'distro-sync' +group_id = 'commands-compatibility-aliases' +complete = true +attached_named_args = [ + { id_path = 'distro-sync.offline' } +] + +['offline-upgrade'] +type = 'command' +attached_command = 'upgrade' +group_id = 'commands-compatibility-aliases' +complete = true +attached_named_args = [ + { id_path = 'upgrade.offline' } +] + ['rei'] type = 'command' attached_command = 'reinstall' From 20dab2a9d08ed3cc89284a6a53f25b94654d34f9 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Fri, 8 Mar 2024 19:30:38 +0000 Subject: [PATCH 09/14] system-upgrade: fix DNF 4 regressions - Store module_platform_id in offline-transaction-state.toml - Set nogpgcheck in `dnf5 offline execute` since the gpgcheck should have already been performed when preparing the transaction --- dnf5/commands/offline/offline.cpp | 8 ++++++++ dnf5/context.cpp | 4 ++++ dnf5/include/dnf5/offline.hpp | 4 +++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp index ebee1c2a5..b8f21f88c 100644 --- a/dnf5/commands/offline/offline.cpp +++ b/dnf5/commands/offline/offline.cpp @@ -357,6 +357,10 @@ void OfflineExecuteCommand::pre_configure() { // Additional removal could trigger unwanted changes in transaction. ctx.base.get_config().get_clean_requirements_on_remove_option().set(false); ctx.base.get_config().get_install_weak_deps_option().set(false); + // Disable gpgcheck entirely, since GPG integrity will have already been + // checked when the transaction was prepared and serialized. This way, we + // don't need to keep track of which packages need to be gpgchecked. + ctx.base.get_config().get_gpgcheck_option().set(false); } void OfflineExecuteCommand::configure() { @@ -372,6 +376,10 @@ void OfflineExecuteCommand::configure() { ctx.base.get_config().get_system_cachedir_option().set(state->get_data().cachedir); ctx.base.get_config().get_cachedir_option().set(state->get_data().cachedir); + if (!state->get_data().module_platform_id.empty()) { + ctx.base.get_config().get_module_platform_id_option().set(state->get_data().module_platform_id); + } + // Set same set of enabled/disabled repos used during `system-upgrade download` for (const auto & repo_id : state->get_data().enabled_repos) { ctx.setopts.emplace_back(repo_id + ".enabled", "1"); diff --git a/dnf5/context.cpp b/dnf5/context.cpp index 9184c4fca..5b411525f 100644 --- a/dnf5/context.cpp +++ b/dnf5/context.cpp @@ -500,6 +500,10 @@ void Context::store_offline(libdnf5::base::Transaction & transaction) { } state.get_data().target_releasever = base.get_vars()->get_value("releasever"); + if (!base.get_config().get_module_platform_id_option().empty()) { + state.get_data().module_platform_id = base.get_config().get_module_platform_id_option().get_value(); + } + state.write(); } diff --git a/dnf5/include/dnf5/offline.hpp b/dnf5/include/dnf5/offline.hpp index f733ab021..2cf3198a4 100644 --- a/dnf5/include/dnf5/offline.hpp +++ b/dnf5/include/dnf5/offline.hpp @@ -64,6 +64,7 @@ struct OfflineTransactionStateData { bool poweroff_after = false; std::vector enabled_repos; std::vector disabled_repos; + std::string module_platform_id; }; class OfflineTransactionState { @@ -99,6 +100,7 @@ TOML11_DEFINE_CONVERSION_NON_INTRUSIVE( cmd_line, poweroff_after, enabled_repos, - disabled_repos) + disabled_repos, + module_platform_id) #endif // DNF5_OFFLINE_HPP From 3fb5fb87d3289e586ce7246aadb008c239382bbf Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Fri, 8 Mar 2024 20:10:36 +0000 Subject: [PATCH 10/14] system-upgrade: offline status subcommand --- dnf5/commands/offline/offline.cpp | 41 ++++++++++++++++++++++++++----- dnf5/commands/offline/offline.hpp | 6 +++++ dnf5/include/dnf5/offline.cpp | 6 +++++ dnf5/include/dnf5/offline.hpp | 3 ++- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp index b8f21f88c..99fb51f2a 100644 --- a/dnf5/commands/offline/offline.cpp +++ b/dnf5/commands/offline/offline.cpp @@ -19,6 +19,7 @@ along with libdnf. If not, see . #include "offline.hpp" +#include "dnf5/offline.hpp" #include "utils/string.hpp" #include @@ -201,6 +202,7 @@ void OfflineCommand::register_subcommands() { register_subcommand(std::make_unique(get_context())); register_subcommand(std::make_unique(get_context())); register_subcommand(std::make_unique(get_context())); + register_subcommand(std::make_unique(get_context())); } OfflineSubcommand::OfflineSubcommand(Context & context, const std::string & name) : Command(context, name) {} @@ -298,12 +300,8 @@ void OfflineRebootCommand::run() { check_state(*state); if (state->get_data().status != dnf5::offline::STATUS_DOWNLOAD_COMPLETE && state->get_data().status != dnf5::offline::STATUS_READY) { - throw libdnf5::cli::CommandExitError(1, M_("system is not ready for offline transaction")); + throw libdnf5::cli::CommandExitError(1, M_("System is not ready for offline transaction.")); } - if (std::filesystem::is_symlink(get_magic_symlink())) { - throw libdnf5::cli::CommandExitError(1, M_("offline `{}` is already scheduled"), state->get_data().verb); - } - if (!std::filesystem::is_directory(get_datadir())) { throw libdnf5::cli::CommandExitError(1, M_("data directory {} does not exist"), get_datadir().string()); } @@ -321,7 +319,9 @@ void OfflineRebootCommand::run() { return; } - std::filesystem::create_symlink(get_datadir(), get_magic_symlink()); + if (!std::filesystem::is_symlink(get_magic_symlink())) { + std::filesystem::create_symlink(get_datadir(), get_magic_symlink()); + } state->get_data().status = dnf5::offline::STATUS_READY; state->get_data().poweroff_after = poweroff_after->get_value(); @@ -418,6 +418,9 @@ void OfflineExecuteCommand::run() { throw libdnf5::cli::CommandExitError(1, M_("Use `dnf5 offline reboot` to begin the transaction.")); } + state->get_data().status = dnf5::offline::STATUS_TRANSACTION_INCOMPLETE; + state->write(); + const auto & installroot = get_context().base.get_config().get_installroot_option().get_value(); const auto & datadir = installroot / dnf5::offline::DEFAULT_DATADIR.relative_path(); std::filesystem::create_directories(datadir); @@ -613,4 +616,30 @@ void OfflineLogCommand::run() { } } +void OfflineStatusCommand::run() { + const std::string no_transaction_message{"No offline transaction is stored."}; + + if (!std::filesystem::exists(state->get_path())) { + std::cerr << no_transaction_message << std::endl; + return; + } + check_state(*state); + + const auto & status = state->get_data().status; + if (status == offline::STATUS_DOWNLOAD_INCOMPLETE) { + std::cout << no_transaction_message << std::endl; + } else if (status == offline::STATUS_DOWNLOAD_COMPLETE || status == offline::STATUS_READY) { + std::cout << _("An offline transaction was initiated by the following command:") << std::endl + << "\t" << state->get_data().cmd_line << std::endl + << _("Run `dnf5 offline reboot` to reboot and perform the offline transaction.") << std::endl; + } else if (status == offline::STATUS_TRANSACTION_INCOMPLETE) { + std::cout << _("An offline transaction was started, but it did not finish. Run `dnf5 offline log` for more " + "information. The command that initiated the transaction was:") + << std::endl + << "\t" << state->get_data().cmd_line << std::endl; + } else { + std::cout << _("Unknown offline transaction status: ") << "`" << state->get_data().status << "`" << std::endl; + } +} + } // namespace dnf5 diff --git a/dnf5/commands/offline/offline.hpp b/dnf5/commands/offline/offline.hpp index 45ad33bdb..c04dd224c 100644 --- a/dnf5/commands/offline/offline.hpp +++ b/dnf5/commands/offline/offline.hpp @@ -97,6 +97,12 @@ class OfflineLogCommand : public OfflineSubcommand { libdnf5::OptionString * number{nullptr}; }; +class OfflineStatusCommand : public OfflineSubcommand { +public: + explicit OfflineStatusCommand(Context & context) : OfflineSubcommand(context, "status") {} + void run() override; +}; + } // namespace dnf5 #endif // DNF5_COMMANDS_OFFLINE_HPP diff --git a/dnf5/include/dnf5/offline.cpp b/dnf5/include/dnf5/offline.cpp index 652646109..a02d182bb 100644 --- a/dnf5/include/dnf5/offline.cpp +++ b/dnf5/include/dnf5/offline.cpp @@ -17,6 +17,8 @@ You should have received a copy of the GNU Lesser General Public License along with libdnf. If not, see . */ +#include "libdnf5/common/exception.hpp" + #include #include #include @@ -28,6 +30,10 @@ OfflineTransactionState::OfflineTransactionState(std::filesystem::path path) : p } void OfflineTransactionState::read() { try { + const std::ifstream file{path}; + if (!file.good()) { + throw libdnf5::FileSystemError(errno, path, M_("Error reading offline state file.")); + } const auto & value = toml::parse(path); data = toml::find(value, STATE_HEADER); if (data.state_version != STATE_VERSION) { diff --git a/dnf5/include/dnf5/offline.hpp b/dnf5/include/dnf5/offline.hpp index 2cf3198a4..ee331763a 100644 --- a/dnf5/include/dnf5/offline.hpp +++ b/dnf5/include/dnf5/offline.hpp @@ -44,7 +44,7 @@ const std::string OFFLINE_FINISHED_ID{"8cec00a1566f4d3594f116450395f06c"}; const std::string STATUS_DOWNLOAD_INCOMPLETE{"download-incomplete"}; const std::string STATUS_DOWNLOAD_COMPLETE{"download-complete"}; const std::string STATUS_READY{"ready"}; -const std::string STATUS_UPGRADE_INCOMPLETE{"upgrade-incomplete"}; +const std::string STATUS_TRANSACTION_INCOMPLETE{"transaction-incomplete"}; const int STATE_VERSION = 0; const std::string STATE_HEADER{"offline-transaction-state"}; @@ -73,6 +73,7 @@ class OfflineTransactionState { OfflineTransactionState(std::filesystem::path path); OfflineTransactionStateData & get_data() { return data; }; const std::exception_ptr & get_read_exception() const { return read_exception; }; + std::filesystem::path get_path() const { return path; }; private: void read(); From 56a4a61e9926f491155268ca4ce02f4d285b821a Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Mon, 11 Mar 2024 20:09:58 +0000 Subject: [PATCH 11/14] system-upgrade: systemd behind build flag --- CMakeLists.txt | 1 + dnf5.spec | 9 +++- dnf5/CMakeLists.txt | 12 ++++-- dnf5/commands/offline/offline.cpp | 41 +++++++++---------- dnf5/commands/offline/offline.hpp | 2 - .../system-upgrade/system-upgrade.cpp | 3 +- dnf5/include/dnf5/offline.cpp | 32 +++++++++++++++ dnf5/include/dnf5/offline.hpp | 3 +- doc/commands/index.rst | 1 + 9 files changed, 70 insertions(+), 34 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0626c9d57..49de28c28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ option(WITH_PYTHON_PLUGINS_LOADER "Build a special dnf5 plugin that loads Python option(WITH_COMPS "Build with comps groups and environments support" ON) option(WITH_MODULEMD "Build with modulemd modules support" ON) option(WITH_ZCHUNK "Build with zchunk delta compression support" ON) +option(WITH_SYSTEMD "Build with systemd and D-Bus features" ON) option(ENABLE_SOLV_URPMREORDER "Build with support for URPM-like solution reordering?" OFF) # build options - documentation diff --git a/dnf5.spec b/dnf5.spec index 02d503c70..42c25959c 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -84,6 +84,7 @@ Provides: dnf5-command(system-upgrade) %else %bcond_without zchunk %endif +%bcond_without systemd %bcond_with html %if 0%{?rhel} == 8 @@ -136,9 +137,7 @@ BuildRequires: pkgconfig(librepo) >= %{librepo_version} BuildRequires: pkgconfig(libsolv) >= %{libsolv_version} BuildRequires: pkgconfig(libsolvext) >= %{libsolv_version} BuildRequires: pkgconfig(rpm) >= 4.17.0 -BuildRequires: pkgconfig(sdbus-c++) >= 0.8.1 BuildRequires: pkgconfig(sqlite3) >= %{sqlite_version} -BuildRequires: systemd-devel BuildRequires: toml11-static %if %{with clang} @@ -165,6 +164,11 @@ BuildRequires: pkgconfig(modulemd-2.0) >= %{libmodulemd_version} BuildRequires: pkgconfig(zck) >= %{zchunk_version} %endif +%if %{with systemd} +BuildRequires: pkgconfig(sdbus-c++) >= 0.8.1 +BuildRequires: systemd-devel +%endif + %if %{with html} || %{with man} BuildRequires: python3dist(breathe) BuildRequires: python3dist(sphinx) >= 4.1.2 @@ -750,6 +754,7 @@ automatically and regularly from systemd timers, cron jobs or similar. -DWITH_COMPS=%{?with_comps:ON}%{!?with_comps:OFF} \ -DWITH_MODULEMD=%{?with_modulemd:ON}%{!?with_modulemd:OFF} \ -DWITH_ZCHUNK=%{?with_zchunk:ON}%{!?with_zchunk:OFF} \ + -DWITH_SYSTEMD=%{?with_systemd:ON}%{!?with_systemd:OFF} \ \ -DWITH_HTML=%{?with_html:ON}%{!?with_html:OFF} \ -DWITH_MAN=%{?with_man:ON}%{!?with_man:OFF} \ diff --git a/dnf5/CMakeLists.txt b/dnf5/CMakeLists.txt index 8099a0b79..63313eac4 100644 --- a/dnf5/CMakeLists.txt +++ b/dnf5/CMakeLists.txt @@ -34,11 +34,15 @@ target_link_libraries(dnf5 PRIVATE common libdnf5 libdnf5-cli Threads::Threads) install(TARGETS dnf5 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) pkg_check_modules(RPM REQUIRED rpm>=4.17.0) -pkg_check_modules(SDBUS_CPP REQUIRED sdbus-c++) -pkg_check_modules(LIBSYSTEMD REQUIRED libsystemd) - -target_link_libraries(dnf5 PRIVATE ${RPM_LIBRARIES} ${SDBUS_CPP_LIBRARIES} ${LIBSYSTEMD_LIBRARIES}) +if(WITH_SYSTEMD) + pkg_check_modules(SDBUS_CPP REQUIRED sdbus-c++) + pkg_check_modules(LIBSYSTEMD REQUIRED libsystemd) + add_definitions(-DWITH_SYSTEMD) + target_link_libraries(dnf5 PRIVATE ${RPM_LIBRARIES} ${SDBUS_CPP_LIBRARIES} ${LIBSYSTEMD_LIBRARIES}) +else() + target_link_libraries(dnf5 PRIVATE ${RPM_LIBRARIES}) +endif() find_package(bash-completion) if(BASH_COMPLETION_FOUND) diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp index 99fb51f2a..dc0f438a1 100644 --- a/dnf5/commands/offline/offline.cpp +++ b/dnf5/commands/offline/offline.cpp @@ -30,7 +30,11 @@ along with libdnf. If not, see . #include #include #include + +#ifdef WITH_SYSTEMD +#include #include +#endif #include #include @@ -74,27 +78,6 @@ int call(const std::string & command, const std::vector & args) { namespace dnf5 { -void offline::log_status( - const std::string & message, - const std::string & message_id, - const std::string & system_releasever, - const std::string & target_releasever) { - const auto & version = get_application_version(); - const std::string & version_string = fmt::format("{}.{}.{}", version.major, version.minor, version.micro); - sd_journal_send( - "MESSAGE=%s", - message.c_str(), - "MESSAGE_ID=%s", - message_id.c_str(), - "SYSTEM_RELEASEVER=%s", - system_releasever.c_str(), - "TARGET_RELEASEVER=%s", - target_releasever.c_str(), - "DNF_VERSION=%s", - version_string.c_str(), - NULL); -} - /// Helper for displaying messages with Plymouth /// /// Derived from DNF 4 system-upgrade PlymouthOutput implementation. Filters @@ -239,7 +222,7 @@ void check_state(const dnf5::offline::OfflineTransactionState & state) { } } -void reboot(bool poweroff = false) { +void reboot([[maybe_unused]] bool poweroff = false) { const std::string systemd_destination_name{"org.freedesktop.systemd1"}; const std::string systemd_object_path{"/org/freedesktop/systemd1"}; const std::string systemd_manager_interface{"org.freedesktop.systemd1.Manager"}; @@ -249,6 +232,7 @@ void reboot(bool poweroff = false) { return; } +#ifdef WITH_SYSTEMD poweroff = !poweroff; std::unique_ptr connection; try { @@ -263,6 +247,9 @@ void reboot(bool poweroff = false) { } else { proxy->callMethod("Reboot").onInterface(systemd_manager_interface); } +#else + std::cerr << "Can't connect to D-Bus; this build of DNF 5 does not support D-Bus." << std::endl; +#endif // #ifdef WITH_SYSTEMD } void clean_datadir(Context & ctx, const std::filesystem::path & datadir) { @@ -328,6 +315,7 @@ void OfflineRebootCommand::run() { state->write(); dnf5::offline::log_status( + ctx, "Rebooting to perform offline transaction.", dnf5::offline::REBOOT_REQUESTED_ID, get_system_releasever(), @@ -393,6 +381,7 @@ void OfflineExecuteCommand::run() { auto & ctx = get_context(); dnf5::offline::log_status( + ctx, "Starting offline transaction. This will take a while.", dnf5::offline::OFFLINE_STARTED_ID, get_system_releasever(), @@ -475,6 +464,7 @@ void OfflineExecuteCommand::run() { plymouth.message(_(transaction_complete_message.c_str())); dnf5::offline::log_status( + ctx, transaction_complete_message, dnf5::offline::OFFLINE_FINISHED_ID, get_system_releasever(), @@ -504,6 +494,7 @@ struct BootEntry { std::string target_releasever; }; +#ifdef WITH_SYSTEMD std::string get_journal_field(sd_journal * journal, const std::string & field) { const char * data = nullptr; size_t length = 0; @@ -586,6 +577,7 @@ void show_log(size_t boot_index) { throw libdnf5::cli::CommandExitError(1, M_("Unable to match systemd journal entry.")); } } +#endif // #ifdef WITH_SYSTEMD void OfflineLogCommand::set_argument_parser() { OfflineSubcommand::set_argument_parser(); @@ -608,12 +600,17 @@ void OfflineLogCommand::set_argument_parser() { } void OfflineLogCommand::run() { +#ifdef WITH_SYSTEMD if (number->get_value().empty()) { list_logs(); } else { std::string number_string{number->get_value()}; show_log(std::stoul(number_string) - 1); } +#else + throw libdnf5::cli::CommandExitError( + 1, M_("systemd is not supported in this build of DNF 5; the `log` subcommand is unavailable.")); +#endif } void OfflineStatusCommand::run() { diff --git a/dnf5/commands/offline/offline.hpp b/dnf5/commands/offline/offline.hpp index c04dd224c..59d15dcb1 100644 --- a/dnf5/commands/offline/offline.hpp +++ b/dnf5/commands/offline/offline.hpp @@ -25,8 +25,6 @@ along with libdnf. If not, see . #include #include #include -#include -#include const std::filesystem::path PATH_TO_PLYMOUTH{"/usr/bin/plymouth"}; const std::filesystem::path PATH_TO_JOURNALCTL{"/usr/bin/journalctl"}; diff --git a/dnf5/commands/system-upgrade/system-upgrade.cpp b/dnf5/commands/system-upgrade/system-upgrade.cpp index 2327e16d1..61599d77a 100644 --- a/dnf5/commands/system-upgrade/system-upgrade.cpp +++ b/dnf5/commands/system-upgrade/system-upgrade.cpp @@ -30,7 +30,6 @@ along with libdnf. If not, see . #include #include #include -#include #include #include @@ -129,7 +128,7 @@ void SystemUpgradeDownloadCommand::run() { << std::endl; dnf5::offline::log_status( - "Download finished.", dnf5::offline::DOWNLOAD_FINISHED_ID, system_releasever, target_releasever); + ctx, "Download finished.", dnf5::offline::DOWNLOAD_FINISHED_ID, system_releasever, target_releasever); } } // namespace dnf5 diff --git a/dnf5/include/dnf5/offline.cpp b/dnf5/include/dnf5/offline.cpp index a02d182bb..d3deaf6f3 100644 --- a/dnf5/include/dnf5/offline.cpp +++ b/dnf5/include/dnf5/offline.cpp @@ -23,6 +23,10 @@ along with libdnf. If not, see . #include #include +#ifdef WITH_SYSTEMD +#include +#endif + namespace dnf5::offline { OfflineTransactionState::OfflineTransactionState(std::filesystem::path path) : path(std::move(path)) { @@ -50,4 +54,32 @@ void OfflineTransactionState::write() { file.close(); } +void log_status( + Context & context, + const std::string & message, + [[maybe_unused]] const std::string & message_id, + [[maybe_unused]] const std::string & system_releasever, + [[maybe_unused]] const std::string & target_releasever) { + const auto & version = get_application_version(); + const std::string & version_string = fmt::format("{}.{}.{}", version.major, version.minor, version.micro); + + auto logger = context.base.get_logger(); + logger->info(message); + +#ifdef WITH_SYSTEMD + sd_journal_send( + "MESSAGE=%s", + message.c_str(), + "MESSAGE_ID=%s", + message_id.c_str(), + "SYSTEM_RELEASEVER=%s", + system_releasever.c_str(), + "TARGET_RELEASEVER=%s", + target_releasever.c_str(), + "DNF_VERSION=%s", + version_string.c_str(), + NULL); +#endif +} + } // namespace dnf5::offline diff --git a/dnf5/include/dnf5/offline.hpp b/dnf5/include/dnf5/offline.hpp index ee331763a..ba2345163 100644 --- a/dnf5/include/dnf5/offline.hpp +++ b/dnf5/include/dnf5/offline.hpp @@ -26,8 +26,6 @@ along with libdnf. If not, see . #include #include #include -#include -#include #include namespace dnf5::offline { @@ -83,6 +81,7 @@ class OfflineTransactionState { }; void log_status( + Context & context, const std::string & message, const std::string & message_id, const std::string & system_releasever, diff --git a/doc/commands/index.rst b/doc/commands/index.rst index d38a112a7..a5be315be 100644 --- a/doc/commands/index.rst +++ b/doc/commands/index.rst @@ -26,6 +26,7 @@ DNF5 Commands repoquery.8 search.8 swap.8 + system-upgrade.8 upgrade.8 versionlock.8 From 520d21b4bc901c7fafa0cb90f957c6bb2c8c1a3c Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Tue, 12 Mar 2024 04:34:49 +0000 Subject: [PATCH 12/14] system-upgrade: Documentation Document the system-upgrade and offline commands and the --offline flag. --- dnf5/commands/group/group_remove.cpp | 2 + dnf5/commands/offline/offline.cpp | 8 +- dnf5/commands/offline/offline.hpp | 1 + .../system-upgrade/system-upgrade.cpp | 2 +- doc/changes.rst | 12 +++ doc/commands/distro-sync.8.rst | 3 + doc/commands/downgrade.8.rst | 3 + doc/commands/group.8.rst | 3 + doc/commands/index.rst | 1 + doc/commands/install.8.rst | 3 + doc/commands/offline.8.rst | 87 +++++++++++++++++ doc/commands/reinstall.8.rst | 3 + doc/commands/remove.8.rst | 7 ++ doc/commands/swap.8.rst | 2 + doc/commands/system-upgrade.8.rst | 95 +++++++++++++++++++ doc/commands/upgrade.8.rst | 3 + doc/dnf5.8.rst | 2 + 17 files changed, 234 insertions(+), 3 deletions(-) create mode 100644 doc/commands/offline.8.rst create mode 100644 doc/commands/system-upgrade.8.rst diff --git a/dnf5/commands/group/group_remove.cpp b/dnf5/commands/group/group_remove.cpp index 5938ca0b3..0fc870040 100644 --- a/dnf5/commands/group/group_remove.cpp +++ b/dnf5/commands/group/group_remove.cpp @@ -19,6 +19,7 @@ along with libdnf. If not, see . #include "group_remove.hpp" +#include #include #include #include @@ -36,6 +37,7 @@ void GroupRemoveCommand::set_argument_parser() { no_packages = std::make_unique(*this); group_specs = std::make_unique(*this, ArgumentParser::PositionalArg::AT_LEAST_ONE); + create_offline_option(*this); } void GroupRemoveCommand::configure() { diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp index dc0f438a1..99630ec5f 100644 --- a/dnf5/commands/offline/offline.cpp +++ b/dnf5/commands/offline/offline.cpp @@ -268,7 +268,7 @@ void OfflineRebootCommand::set_argument_parser() { auto & cmd = *get_argument_parser_command(); cmd.set_description( - _("Prepare the system to perform the offline transaction and reboots to start the transaction.")); + _("Prepare the system to perform the offline transaction and reboot to start the transaction.")); poweroff_after = dynamic_cast(parser.add_init_value(std::make_unique(true))); @@ -329,7 +329,7 @@ void OfflineExecuteCommand::set_argument_parser() { auto & cmd = *get_argument_parser_command(); cmd.set_complete(false); cmd.set_description(_( - "Internal use only, not intended to be run by the user. Executes the transaction in the offline environment.")); + "Internal use only, not intended to be run by the user. Execute the transaction in the offline environment.")); } void OfflineExecuteCommand::pre_configure() { @@ -613,6 +613,10 @@ void OfflineLogCommand::run() { #endif } +void OfflineStatusCommand::set_argument_parser() { + get_argument_parser_command()->set_description(_("Show status of the current offline transaction")); +} + void OfflineStatusCommand::run() { const std::string no_transaction_message{"No offline transaction is stored."}; diff --git a/dnf5/commands/offline/offline.hpp b/dnf5/commands/offline/offline.hpp index 59d15dcb1..80c937114 100644 --- a/dnf5/commands/offline/offline.hpp +++ b/dnf5/commands/offline/offline.hpp @@ -98,6 +98,7 @@ class OfflineLogCommand : public OfflineSubcommand { class OfflineStatusCommand : public OfflineSubcommand { public: explicit OfflineStatusCommand(Context & context) : OfflineSubcommand(context, "status") {} + void set_argument_parser() override; void run() override; }; diff --git a/dnf5/commands/system-upgrade/system-upgrade.cpp b/dnf5/commands/system-upgrade/system-upgrade.cpp index 61599d77a..e1fb0d6ae 100644 --- a/dnf5/commands/system-upgrade/system-upgrade.cpp +++ b/dnf5/commands/system-upgrade/system-upgrade.cpp @@ -65,7 +65,7 @@ void SystemUpgradeDownloadCommand::set_argument_parser() { auto & parser = ctx.get_argument_parser(); auto & cmd = *get_argument_parser_command(); - cmd.set_description(_("Downloads everything needed to upgrade to a new release")); + cmd.set_description(_("Download everything needed to upgrade to a new release")); no_downgrade = dynamic_cast(parser.add_init_value(std::make_unique(true))); diff --git a/doc/changes.rst b/doc/changes.rst index 3a733d8f3..3408a79a4 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -156,6 +156,14 @@ Needs-restarting command * Dropped ``-r, --reboothint`` option; this is now the default behavior. * Dropped ``-u, --useronly`` option. +Offline-distrosync command +-------------------------- + * Now an alias of ``dnf5 distro-sync --offline`` + +Offline-upgrade command +-------------------------- + * Now an alias of ``dnf5 upgrade --offline`` + Remove command -------------- * Command does not remove packages according to provides, but only according NEVRA or file provide match @@ -188,6 +196,10 @@ Repoquery command ``--provides``, ``--suggests``,... options instead it takes the PACKAGE_ATTRIBUTE value directly. E.g., ``dnf rq --resolve --requires glibc`` -> ``dnf rq --providers-of=requires glibc``. +System-upgrade command +-------------------------- + * Moved from a plugin to a built-in command + Upgrade command --------------- * New dnf5 option ``--minimal`` (``upgrade-minimal`` command still exists as a compatibility alias for diff --git a/doc/commands/distro-sync.8.rst b/doc/commands/distro-sync.8.rst index 0b508eecc..026f55f58 100644 --- a/doc/commands/distro-sync.8.rst +++ b/doc/commands/distro-sync.8.rst @@ -50,6 +50,9 @@ Options ``--skip-unavailable`` | Allow skipping packages that are not possible to synchronize. All remaining packages will be synchronized. +``--offline`` + | Store the transaction to be performed offline. See :manpage:`dnf5-offline(8)`, :ref:`Offline command `. + Examples ======== diff --git a/doc/commands/downgrade.8.rst b/doc/commands/downgrade.8.rst index 0b45ebc29..522bbae56 100644 --- a/doc/commands/downgrade.8.rst +++ b/doc/commands/downgrade.8.rst @@ -48,6 +48,9 @@ Options ``--skip-unavailable`` | Allow skipping packages that are not possible to downgrade. All remaining packages will be downgraded. +``--offline`` + | Store the transaction to be performed offline. See :manpage:`dnf5-offline(8)`, :ref:`Offline command `. + Examples ======== diff --git a/doc/commands/group.8.rst b/doc/commands/group.8.rst index fec217814..03ddb896c 100644 --- a/doc/commands/group.8.rst +++ b/doc/commands/group.8.rst @@ -112,6 +112,9 @@ Options | Used with ``install`` and ``upgrade`` to allow skipping packages that are not possible to install or upgrade. | All remaining packages will be installed or upgraded. +``--offline`` + | Used with ``install``, ``remove``, and ``upgrade``. Store the transaction to be performed offline. See :manpage:`dnf5-offline(8)`, :ref:`Offline command `. + Examples ======== diff --git a/doc/commands/index.rst b/doc/commands/index.rst index a5be315be..42266379a 100644 --- a/doc/commands/index.rst +++ b/doc/commands/index.rst @@ -19,6 +19,7 @@ DNF5 Commands leaves.8 makecache.8 mark.8 + offline.8 provides.8 reinstall.8 remove.8 diff --git a/doc/commands/install.8.rst b/doc/commands/install.8.rst index cd61180df..27f9d33e5 100644 --- a/doc/commands/install.8.rst +++ b/doc/commands/install.8.rst @@ -80,6 +80,9 @@ Options ``--newpackage`` | Consider only content contained in newpackage advisories. +``--offline`` + | Store the transaction to be performed offline. See :manpage:`dnf5-offline(8)`, :ref:`Offline command `. + Examples ======== diff --git a/doc/commands/offline.8.rst b/doc/commands/offline.8.rst new file mode 100644 index 000000000..adcfc476e --- /dev/null +++ b/doc/commands/offline.8.rst @@ -0,0 +1,87 @@ +.. + 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 . + +.. _offline_command_ref-label: + +################ + Offline Command +################ + +Synopsis +======== + +``dnf5 offline [options]`` + + +Description +=========== + +The ``offline`` command is used to manage "offline" transactions---transactions that run when the system is booted into a minimal environment. Running a transaction in this stripped-down environment can be safer than running it when the system is booted normally since the transaction is less likely to interfere with running processes. + +Offline transactions can be initiated by specifying the ``--offline`` flag on any operation (``install``, ``upgrade``, ``distro-sync``, etc.), or via ``dnf5 system-upgrade download``. Once an offline transaction is initiated, run ``dnf5 offline reboot`` to reboot and begin the transaction. + +Data for offline transactions is stored under the "system state" directory at ``/usr/lib/sysimage/libdnf5/offline``. + +Subcommands +=========== + +``clean`` + Removes any stored offline transaction and deletes cached package files. + +``log`` + Used to see a list of boots during which an offline transaction was attempted, or show the logs from an attempted offline transaction. The logs for one of the boots can be shown by specifying one of the numbers in the first column with the ``--number`` argument. Negative numbers can be used to number the boots from last to first. For example, ``log --number=-1`` can be used to see the logs for the last offline transaction. + +``reboot`` + Prepares the system to perform the offline transaction and reboots to start the transaction. This command can only be run after an offline transaction is initiated (e.g. by ``dnf5 system-upgrade download``). + +``status`` + Shows the status of the current offline transaction. + +Options +======= + +``--number=`` + Used with the ``log`` subcommand. Show the log specified by the number. Run ``dnf5 offline log`` with ``--number`` first to get a list of logs to choose from. + +``--poweroff`` + Used with the ``reboot`` subcommand. The system will power off after the transaction is completed instead of restarting. If the transaction failed, the system will reboot instead of powering off even with this flag. + +Examples +======== + +``dnf5 install --offline hello`` + | Prepares the installation of the ``hello`` package as an offline transaction. + +``dnf5 offline status`` + | Shows the status of the current offline transaction. + +``dnf5 offline reboot --poweroff`` + | Reboot and run the offline transaction, then power the system off after the transaction is complete. + +``dnf5 offline log`` + | List boots during which an offline transaction was attempted. + +``dnf5 offline log --number=-1`` + | View the log from the latest boot during which an offline transaction was attempted. + + +See Also +======== + + | :manpage:`dnf5-system-upgrade(8)`, :ref:`System-upgrade command ` + | https://www.freedesktop.org/wiki/Software/systemd/SystemUpdates diff --git a/doc/commands/reinstall.8.rst b/doc/commands/reinstall.8.rst index cd37059fe..11aacc6dd 100644 --- a/doc/commands/reinstall.8.rst +++ b/doc/commands/reinstall.8.rst @@ -47,6 +47,9 @@ Options ``--skip-unavailable`` | Allow skipping packages that are not possible to reinstall. All remaining packages will be reinstalled. +``--offline`` + | Store the transaction to be performed offline. See :manpage:`dnf5-offline(8)`, :ref:`Offline command `. + Examples ======== diff --git a/doc/commands/remove.8.rst b/doc/commands/remove.8.rst index 3a50bf569..53a149a4f 100644 --- a/doc/commands/remove.8.rst +++ b/doc/commands/remove.8.rst @@ -38,6 +38,13 @@ If you want to keep the dependencies that were installed together with the given set the ``clean_requirements_on_remove`` configuration option to ``False``. +Options +======= + +``--offline`` + | Store the transaction to be performed offline. See :manpage:`dnf5-offline(8)`, :ref:`Offline command `. + + Examples ======== diff --git a/doc/commands/swap.8.rst b/doc/commands/swap.8.rst index 95d2f2358..e241609fd 100644 --- a/doc/commands/swap.8.rst +++ b/doc/commands/swap.8.rst @@ -41,6 +41,8 @@ Options ``--allowerasing`` | Allow erasing of installed packages to resolve any potential dependency problems. +``--offline`` + | Store the transaction to be performed offline. See :manpage:`dnf5-offline(8)`, :ref:`Offline command `. Examples diff --git a/doc/commands/system-upgrade.8.rst b/doc/commands/system-upgrade.8.rst new file mode 100644 index 000000000..278d592fb --- /dev/null +++ b/doc/commands/system-upgrade.8.rst @@ -0,0 +1,95 @@ +.. + 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 . + +.. _system_upgrade_command_ref-label: + +################ + System-upgrade Command +################ + +Synopsis +======== + +``dnf5 system-upgrade [options]`` + + +Description +=========== + +The ``system-upgrade`` command is used to upgrade the system to a new major release. First, the ``download`` subcommand downloads packages while the system is running normally. Then, the ``reboot`` subcommand reboots the system into a minimal "offline" environment to apply the upgrades. + +``dnf5 system-upgrade`` is a recommended way to upgrade a system to a new major release. Before you proceed, ensure that your system is fully upgraded (``dnf5 --refresh upgrade``). + +``system-upgrade`` shares many subcommands with the :ref:`offline subcommand `. + + +Subcommands +=========== + +``clean`` + | See :manpage:`dnf5-offline(8)`, :ref:`Offline command ` + +``download`` + | Downloads all packages needed to upgrade to a new major release and checks that they can be installed. + +``log`` + | See :manpage:`dnf5-offline(8)`, :ref:`Offline command ` + +``reboot`` + | See :manpage:`dnf5-offline(8)`, :ref:`Offline command ` + +Options +======= + +``--releasever=`` + | Required. The version to upgrade to. Sets ``$releasever`` in all enabled repos. Usually a number, or ``rawhide``. + +``--no-downgrade`` + | Behave like ``dnf5 update``: do not install packages from the new release if they are older than what is currently installed. This is the opposite of the default behavior, which behaves like ``dnf5 distro-sync``, always installing packages from the new release, even if they are older than the currently-installed version. + +``--number=`` + | See :manpage:`dnf5-offline(8)`, :ref:`Offline command ` + +``--poweroff`` + | See :manpage:`dnf5-offline(8)`, :ref:`Offline command ` + + +Examples +======== + +Typical upgrade usage +--------------------- + +``dnf5 --refresh upgrade`` + +``dnf5 system-upgrade download --releasever 40`` + +``dnf5 system-upgrade reboot`` + + +Show logs from last upgrade attempt +----------------------------------- + +``dnf5 system-upgrade log --number=-1`` + + +See Also +======== + + | :manpage:`dnf5-offline(8)`, :ref:`Offline command ` + | https://www.freedesktop.org/wiki/Software/systemd/SystemUpdates diff --git a/doc/commands/upgrade.8.rst b/doc/commands/upgrade.8.rst index 8ec1d0fa4..6580bdb6e 100644 --- a/doc/commands/upgrade.8.rst +++ b/doc/commands/upgrade.8.rst @@ -87,6 +87,9 @@ Options ``--newpackage`` | Consider only content contained in newpackage advisories. +``--offline`` + | Store the transaction to be performed offline. See :manpage:`dnf5-offline(8)`, :ref:`Offline command `. + Examples ======== diff --git a/doc/dnf5.8.rst b/doc/dnf5.8.rst index d95779fbf..6af3329ae 100644 --- a/doc/dnf5.8.rst +++ b/doc/dnf5.8.rst @@ -365,6 +365,7 @@ Commands in detail: | :manpage:`dnf5-leaves(8)`, :ref:`Leaves command ` | :manpage:`dnf5-makecache(8)`, :ref:`Makecache command ` | :manpage:`dnf5-mark(8)`, :ref:`Mark command ` + | :manpage:`dnf5-offline(8)`, :ref:`Offline command ` | :manpage:`dnf5-provides(8)`, :ref:`Provides command ` | :manpage:`dnf5-reinstall(8)`, :ref:`Reinstall command ` | :manpage:`dnf5-remove(8)`, :ref:`Remove command ` @@ -372,6 +373,7 @@ Commands in detail: | :manpage:`dnf5-repoquery(8)`, :ref:`Repoquery command ` | :manpage:`dnf5-search(8)`, :ref:`Search command ` | :manpage:`dnf5-swap(8)`, :ref:`Swap command ` + | :manpage:`dnf5-system-upgrade(8)`, :ref:`System-upgrade command ` | :manpage:`dnf5-upgrade(8)`, :ref:`Upgrade command ` .. From 377527c4237d630fe8c681a2f02ff2b402e77b17 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Thu, 14 Mar 2024 18:04:29 +0000 Subject: [PATCH 13/14] system-upgrade: misc fixes --- dnf5.spec | 15 +++++- dnf5/CMakeLists.txt | 12 ++--- dnf5/commands/offline/offline.cpp | 48 +++++++++++++++---- .../system-upgrade/system-upgrade.cpp | 4 +- dnf5/context.cpp | 3 +- 5 files changed, 60 insertions(+), 22 deletions(-) diff --git a/dnf5.spec b/dnf5.spec index 42c25959c..0b6a5afff 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -262,8 +262,6 @@ It supports RPM packages, modulemd modules, and comps groups & environments. %dir %{_datadir}/dnf5 %dir %{_datadir}/dnf5/aliases.d %config %{_datadir}/dnf5/aliases.d/compatibility.conf -%config %{_unitdir}/dnf5-offline-transaction.service -%config %{_unitdir}/dnf5-offline-transaction-cleanup.service %dir %{_libdir}/dnf5 %dir %{_libdir}/dnf5/plugins %dir %{_datadir}/dnf5/dnf5-plugins @@ -317,6 +315,12 @@ It supports RPM packages, modulemd modules, and comps groups & environments. %{_mandir}/man5/dnf5.conf-todo.5.* %{_mandir}/man5/dnf5.conf-deprecated.5.* +%if %{with systemd} +%{_unitdir}/dnf5-offline-transaction.service +%{_unitdir}/dnf5-offline-transaction-cleanup.service +%{_unitdir}/system-update.target.wants/dnf5-offline-transaction.service +%endif + # ========== libdnf5 ========== %package -n libdnf5 Summary: Package management library @@ -808,6 +812,13 @@ done ln -sr %{buildroot}%{_bindir}/dnf5 %{buildroot}%{_bindir}/microdnf %endif +%if %{with systemd} +mkdir -p %{buildroot}%{_unitdir}/system-update.target.wants/ +pushd %{buildroot}%{_unitdir}/system-update.target.wants/ + ln -sr ../dnf5-offline-transaction.service +popd +%endif + %find_lang dnf5 %find_lang dnf5-plugin-automatic %find_lang dnf5-plugin-builddep diff --git a/dnf5/CMakeLists.txt b/dnf5/CMakeLists.txt index 63313eac4..4b7ba6eed 100644 --- a/dnf5/CMakeLists.txt +++ b/dnf5/CMakeLists.txt @@ -2,7 +2,8 @@ if(NOT WITH_DNF5) return() endif() -set(SYSTEMD_SYSTEM_UNIT_DIR /usr/lib/systemd/system) +pkg_check_modules(SYSTEMD REQUIRED systemd) +pkg_get_variable(SYSTEMD_SYSTEM_UNIT_DIR systemd systemdsystemunitdir) find_package(Threads) @@ -56,11 +57,10 @@ install(FILES bash-completion/dnf5 DESTINATION ${BASH_COMPLETION_COMPLETIONSDIR} install(FILES "README.plugins" DESTINATION "${CMAKE_INSTALL_FULL_LIBDIR}/dnf5/plugins" RENAME "README") install(DIRECTORY "config/usr/" DESTINATION "${CMAKE_INSTALL_PREFIX}" PATTERN ".gitkeep" EXCLUDE) install(DIRECTORY "config/etc/" DESTINATION "${CMAKE_INSTALL_FULL_SYSCONFDIR}" PATTERN ".gitkeep" EXCLUDE) -install(DIRECTORY "config/systemd/system/" DESTINATION "${SYSTEMD_SYSTEM_UNIT_DIR}" PATTERN ".gitkeep" EXCLUDE) -install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink - ${SYSTEMD_SYSTEM_UNIT_DIR}/dnf5-offline-transaction.service - ${SYSTEMD_SYSTEM_UNIT_DIR}/system-update.target.wants/dnf5-offline-transaction.service)" -) + +if(WITH_SYSTEMD) + install(DIRECTORY "config/systemd/system/" DESTINATION "${SYSTEMD_SYSTEM_UNIT_DIR}" PATTERN ".gitkeep" EXCLUDE) +endif() # Makes an empty directory for dnf5-plugins configuration files install(DIRECTORY DESTINATION "${CMAKE_INSTALL_FULL_SYSCONFDIR}/dnf/dnf5-plugins") diff --git a/dnf5/commands/offline/offline.cpp b/dnf5/commands/offline/offline.cpp index 99630ec5f..c4d018a33 100644 --- a/dnf5/commands/offline/offline.cpp +++ b/dnf5/commands/offline/offline.cpp @@ -45,6 +45,12 @@ using namespace libdnf5::cli; const std::string & ID_TO_IDENTIFY_BOOTS = dnf5::offline::OFFLINE_STARTED_ID; +const std::string SYSTEMD_DESTINATION_NAME{"org.freedesktop.systemd1"}; +const std::string SYSTEMD_OBJECT_PATH{"/org/freedesktop/systemd1"}; +const std::string SYSTEMD_MANAGER_INTERFACE{"org.freedesktop.systemd1.Manager"}; +const std::string SYSTEMD_UNIT_INTERFACE{"org.freedesktop.systemd1.Unit"}; +const std::string SYSTEMD_SERVICE_NAME{"dnf5-offline-transaction.service"}; + int call(const std::string & command, const std::vector & args) { std::vector c_args; c_args.emplace_back(const_cast(command.c_str())); @@ -192,6 +198,9 @@ OfflineSubcommand::OfflineSubcommand(Context & context, const std::string & name void OfflineSubcommand::configure() { auto & ctx = get_context(); + // This value comes from systemd, see + // https://www.freedesktop.org/wiki/Software/systemd/SystemUpdates or + // systemd.offline-updates(7). magic_symlink = "/system-update"; const std::filesystem::path installroot = ctx.base.get_config().get_installroot_option().get_value(); @@ -223,10 +232,6 @@ void check_state(const dnf5::offline::OfflineTransactionState & state) { } void reboot([[maybe_unused]] bool poweroff = false) { - const std::string systemd_destination_name{"org.freedesktop.systemd1"}; - const std::string systemd_object_path{"/org/freedesktop/systemd1"}; - const std::string systemd_manager_interface{"org.freedesktop.systemd1.Manager"}; - if (std::getenv("DNF_SYSTEM_UPGRADE_NO_REBOOT")) { std::cerr << "DNF_SYSTEM_UPGRADE_NO_REBOOT is set, not rebooting." << std::endl; return; @@ -241,11 +246,11 @@ void reboot([[maybe_unused]] bool poweroff = false) { const std::string error_message{ex.what()}; throw libdnf5::cli::CommandExitError(1, M_("Couldn't connect to D-Bus: {}"), error_message); } - auto proxy = sdbus::createProxy(systemd_destination_name, systemd_object_path); + auto proxy = sdbus::createProxy(SYSTEMD_DESTINATION_NAME, SYSTEMD_OBJECT_PATH); if (poweroff) { - proxy->callMethod("Poweroff").onInterface(systemd_manager_interface); + proxy->callMethod("Poweroff").onInterface(SYSTEMD_MANAGER_INTERFACE); } else { - proxy->callMethod("Reboot").onInterface(systemd_manager_interface); + proxy->callMethod("Reboot").onInterface(SYSTEMD_MANAGER_INTERFACE); } #else std::cerr << "Can't connect to D-Bus; this build of DNF 5 does not support D-Bus." << std::endl; @@ -290,9 +295,33 @@ void OfflineRebootCommand::run() { throw libdnf5::cli::CommandExitError(1, M_("System is not ready for offline transaction.")); } if (!std::filesystem::is_directory(get_datadir())) { - throw libdnf5::cli::CommandExitError(1, M_("data directory {} does not exist"), get_datadir().string()); + throw libdnf5::cli::CommandExitError(1, M_("Data directory {} does not exist."), get_datadir().string()); } +#ifdef WITH_SYSTEMD + // Check that dnf5-offline-transaction.service is present and wanted by system-update.target + std::unique_ptr connection; + try { + connection = sdbus::createSystemBusConnection(); + } catch (const sdbus::Error & ex) { + const std::string error_message{ex.what()}; + throw libdnf5::cli::CommandExitError(1, M_("Couldn't connect to D-Bus: {}"), error_message); + } + auto systemd_proxy = sdbus::createProxy(SYSTEMD_DESTINATION_NAME, SYSTEMD_OBJECT_PATH); + + sdbus::ObjectPath unit_object_path; + systemd_proxy->callMethod("LoadUnit") + .onInterface(SYSTEMD_MANAGER_INTERFACE) + .withArguments("system-update.target") + .storeResultsTo(unit_object_path); + + auto unit_proxy = sdbus::createProxy(SYSTEMD_DESTINATION_NAME, unit_object_path); + const std::vector & wants = unit_proxy->getProperty("Wants").onInterface(SYSTEMD_UNIT_INTERFACE); + if (std::find(wants.begin(), wants.end(), SYSTEMD_SERVICE_NAME) == wants.end()) { + throw libdnf5::cli::CommandExitError(1, M_("{} is not wanted by system-update.target."), SYSTEMD_SERVICE_NAME); + } +#endif + if (state->get_data().verb == "system-upgrade download") { std::cout << _("The system will now reboot to upgrade to release version ") << state->get_data().target_releasever << "." << std::endl; @@ -396,8 +425,7 @@ void OfflineExecuteCommand::run() { throw libdnf5::cli::CommandExitError(0, M_("Trigger file does not exist. Exiting.")); } - const auto & symlinked_path = std::filesystem::read_symlink(get_magic_symlink()); - if (symlinked_path != get_datadir()) { + if (!std::filesystem::equivalent(get_magic_symlink(), get_datadir())) { throw libdnf5::cli::CommandExitError(0, M_("Another offline transaction tool is running. Exiting.")); } diff --git a/dnf5/commands/system-upgrade/system-upgrade.cpp b/dnf5/commands/system-upgrade/system-upgrade.cpp index e1fb0d6ae..a6297c768 100644 --- a/dnf5/commands/system-upgrade/system-upgrade.cpp +++ b/dnf5/commands/system-upgrade/system-upgrade.cpp @@ -123,9 +123,7 @@ void SystemUpgradeDownloadCommand::run() { ctx.set_should_store_offline(true); ctx.download_and_run(transaction); - std::cout << _("Download complete! Use `dnf5 system-upgrade reboot` to start the upgrade.\n" - "To cancel the upgrade and delete the downloaded upgrade files, use `dnf5 system-upgrade clean`.") - << std::endl; + std::cout << _("Download complete!") << std::endl; dnf5::offline::log_status( ctx, "Download finished.", dnf5::offline::DOWNLOAD_FINISHED_ID, system_releasever, target_releasever); diff --git a/dnf5/context.cpp b/dnf5/context.cpp index 5b411525f..231071699 100644 --- a/dnf5/context.cpp +++ b/dnf5/context.cpp @@ -451,7 +451,8 @@ void Context::download_and_run(libdnf5::base::Transaction & transaction) { if (should_store_offline) { store_offline(transaction); std::cout << "Transaction stored to be performed offline. Run `dnf5 offline reboot` to reboot and run the " - "transaction." + "transaction. To cancel the transaction and delete the downloaded files, use `dnf5 " + "offline clean`." << std::endl; } } From b402ddf00c895b7ef21658f28dd54a7b8a415993 Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Mon, 18 Mar 2024 13:49:13 +0000 Subject: [PATCH 14/14] system-upgrade: fix pkgconfig systemd variable --- dnf5.spec | 4 ++++ dnf5/CMakeLists.txt | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/dnf5.spec b/dnf5.spec index 0b6a5afff..c64c4bcc8 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -167,6 +167,10 @@ BuildRequires: pkgconfig(zck) >= %{zchunk_version} %if %{with systemd} BuildRequires: pkgconfig(sdbus-c++) >= 0.8.1 BuildRequires: systemd-devel + + # We need to get the SYSTEMD_SYSTEM_UNIT_DIR from + # /usr/share/pkgconfig/systemd.pc +BuildRequires: systemd %endif %if %{with html} || %{with man} diff --git a/dnf5/CMakeLists.txt b/dnf5/CMakeLists.txt index 4b7ba6eed..47f911e4f 100644 --- a/dnf5/CMakeLists.txt +++ b/dnf5/CMakeLists.txt @@ -2,8 +2,6 @@ if(NOT WITH_DNF5) return() endif() -pkg_check_modules(SYSTEMD REQUIRED systemd) -pkg_get_variable(SYSTEMD_SYSTEM_UNIT_DIR systemd systemdsystemunitdir) find_package(Threads) @@ -59,6 +57,8 @@ install(DIRECTORY "config/usr/" DESTINATION "${CMAKE_INSTALL_PREFIX}" PATTERN ". install(DIRECTORY "config/etc/" DESTINATION "${CMAKE_INSTALL_FULL_SYSCONFDIR}" PATTERN ".gitkeep" EXCLUDE) if(WITH_SYSTEMD) + pkg_check_modules(SYSTEMD REQUIRED systemd) + pkg_get_variable(SYSTEMD_SYSTEM_UNIT_DIR systemd systemdsystemunitdir) install(DIRECTORY "config/systemd/system/" DESTINATION "${SYSTEMD_SYSTEM_UNIT_DIR}" PATTERN ".gitkeep" EXCLUDE) endif()