diff --git a/dnf5-plugins/system_upgrade_plugin/CMakeLists.txt b/dnf5-plugins/system_upgrade_plugin/CMakeLists.txt index cf5beaab5..bd6519526 100644 --- a/dnf5-plugins/system_upgrade_plugin/CMakeLists.txt +++ b/dnf5-plugins/system_upgrade_plugin/CMakeLists.txt @@ -10,8 +10,9 @@ add_library(system_upgrade_cmd_plugin MODULE ${SYSTEM_UPGRADE_SOURCES}) set_target_properties(system_upgrade_cmd_plugin PROPERTIES PREFIX "") pkg_check_modules(SDBUS_CPP REQUIRED sdbus-c++) +pkg_check_modules(LIBSYSTEMD REQUIRED libsystemd) -target_link_libraries(system_upgrade_cmd_plugin PRIVATE dnf5 libdnf5 libdnf5-cli ${SDBUS_CPP_LIBRARIES}) +target_link_libraries(system_upgrade_cmd_plugin PRIVATE dnf5 libdnf5 libdnf5-cli ${SDBUS_CPP_LIBRARIES} ${LIBSYSTEMD_LIBRARIES}) install(FILES "dnf5-system-upgrade.service" DESTINATION ${SYSTEMD_UNIT_DIR}) install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink diff --git a/dnf5-plugins/system_upgrade_plugin/system_upgrade.cpp b/dnf5-plugins/system_upgrade_plugin/system_upgrade.cpp index 95d3549ea..7db696ae3 100644 --- a/dnf5-plugins/system_upgrade_plugin/system_upgrade.cpp +++ b/dnf5-plugins/system_upgrade_plugin/system_upgrade.cpp @@ -19,6 +19,8 @@ along with libdnf. If not, see . #include "system_upgrade.hpp" +#include "utils/string.hpp" + #include #include #include @@ -26,12 +28,17 @@ along with libdnf. If not, see . #include #include #include +#include +#include #include #include +#include using namespace libdnf5::cli; +const std::string & ID_TO_IDENTIFY_BOOTS = OFFLINE_STARTED_ID; + OfflineTransactionState::OfflineTransactionState(std::filesystem::path path) : path(std::move(path)) { read(); } @@ -123,6 +130,7 @@ class PlymouthOutput { /// 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, @@ -189,6 +197,7 @@ 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) @@ -215,6 +224,26 @@ 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 SystemUpgradeSubcommand::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 SystemUpgradeDownloadCommand::set_argument_parser() { @@ -242,14 +271,9 @@ void SystemUpgradeDownloadCommand::configure() { SystemUpgradeSubcommand::configure(); auto & ctx = get_context(); - auto & conf = ctx.base.get_config(); // Check --releasever - const auto & installroot = conf.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"); - - if (target_releasever == system_releasever) { + if (get_target_releasever() == get_system_releasever()) { throw libdnf5::cli::CommandExitError(1, M_("Need a --releasever greater than the current system version.")); } @@ -281,7 +305,7 @@ void SystemUpgradeDownloadCommand::run() { ctx.download_and_run(transaction); - if (!transaction.get_transaction_packages_count()) { + 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.")); } @@ -295,8 +319,8 @@ void SystemUpgradeDownloadCommand::run() { auto state = get_state(); state.get_data().cachedir = get_cachedir()->get_value(); - state.get_data().system_releasever = system_releasever; - state.get_data().target_releasever = target_releasever; + state.get_data().system_releasever = get_system_releasever(); + state.get_data().target_releasever = get_target_releasever(); state.get_data().status = STATUS_DOWNLOAD_COMPLETE; libdnf5::repo::RepoQuery enabled_repos(ctx.base); enabled_repos.filter_enabled(true); @@ -451,6 +475,8 @@ void SystemUpgradeUpgradeCommand::configure() { void SystemUpgradeUpgradeCommand::run() { auto & ctx = get_context(); + log_status("Starting offline transaction. This will take a while.", OFFLINE_STARTED_ID); + if (!std::filesystem::is_symlink(get_magic_symlink())) { throw libdnf5::cli::CommandExitError(0, M_("Trigger file does not exist. Exiting.")); } @@ -483,7 +509,8 @@ void SystemUpgradeUpgradeCommand::run() { throw libdnf5::cli::GoalResolveError(transaction); } - auto callbacks = std::make_unique(); + 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)); @@ -505,6 +532,15 @@ void SystemUpgradeUpgradeCommand::run() { std::cerr << entry << std::endl; } + std::string upgrade_complete_message; + if (state.get_data().poweroff_after) { + upgrade_complete_message = "Upgrade complete! Cleaning up and powering off..."; + } else { + upgrade_complete_message = "Upgrade complete! Cleaning up and rebooting..."; + } + plymouth.message(upgrade_complete_message); + log_status(upgrade_complete_message, OFFLINE_STARTED_ID); + // If the upgrade succeeded, remove downloaded data clean(ctx, get_datadir()); @@ -520,4 +556,114 @@ void SystemUpgradeCleanCommand::run() { clean(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)}); + } + SD_JOURNAL_FOREACH(journal) { + uint64_t usec = 0; + rc = sd_journal_get_realtime_usec(journal, &usec); + auto sec = usec / (1000 * 1000); + boots.emplace_back(BootEntry{ + get_journal_field(journal, "_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); + + std::cout << "The following boots appear to contain upgrade 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; + } + if (boot_entries.empty()) { + std::cout << "No logs were found." << 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) { + throw libdnf5::cli::CommandExitError(1, M_("Unable to match systemd journal entry.")); + } +} + +void SystemUpgradeLogCommand::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("Show logs from past upgrades"); + + 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 SystemUpgradeLogCommand::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-plugins/system_upgrade_plugin/system_upgrade.hpp b/dnf5-plugins/system_upgrade_plugin/system_upgrade.hpp index bd7667612..c976b1bc9 100644 --- a/dnf5-plugins/system_upgrade_plugin/system_upgrade.hpp +++ b/dnf5-plugins/system_upgrade_plugin/system_upgrade.hpp @@ -28,6 +28,9 @@ along with libdnf. If not, see . #include const std::filesystem::path PATH_TO_PLYMOUTH{"/usr/bin/plymouth"}; +const std::filesystem::path PATH_TO_JOURNALCTL{"/usr/bin/journalctl"}; +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"}; @@ -93,16 +96,21 @@ class SystemUpgradeSubcommand : public Command { void configure() override; protected: - libdnf5::OptionPath * get_cachedir() { return cachedir; }; - std::filesystem::path get_datadir() { return datadir; }; - std::filesystem::path get_magic_symlink() { return magic_symlink; }; - OfflineTransactionState get_state() { return state; }; + 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; }; + 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{DEFAULT_DATADIR}; std::filesystem::path magic_symlink; OfflineTransactionState state; + std::string target_releasever; + std::string system_releasever; }; class SystemUpgradeDownloadCommand : public SystemUpgradeSubcommand { @@ -115,8 +123,6 @@ class SystemUpgradeDownloadCommand : public SystemUpgradeSubcommand { private: libdnf5::OptionBool * no_downgrade{nullptr}; libdnf5::OptionBool * distro_sync{nullptr}; - std::string target_releasever; - std::string system_releasever; int state_version; }; @@ -150,6 +156,9 @@ class SystemUpgradeLogCommand : public SystemUpgradeSubcommand { explicit SystemUpgradeLogCommand(Context & context) : SystemUpgradeSubcommand(context, "log") {} void set_argument_parser() override; void run() override; + +private: + libdnf5::OptionString * number{nullptr}; }; } // namespace dnf5 diff --git a/dnf5.spec b/dnf5.spec index a27720f87..2b97eb810 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -174,6 +174,7 @@ BuildRequires: pkgconfig(smartcols) %if %{with dnf5_plugins} BuildRequires: libcurl-devel >= 7.62.0 +BuildRequires: systemd-devel %endif %if %{with dnf5daemon_server}