From eaba0ad8aea5f9e4d1f3334093aa8366e3dfa2c8 Mon Sep 17 00:00:00 2001 From: Jaroslav Rohel Date: Fri, 8 Sep 2023 20:08:09 +0200 Subject: [PATCH] [dnf5 plugin] config-manager Manage libdnf5 configuration. Subcommands: addrepo Add repositories from the specified configuration file or create a new repository using user options setopt Set configuration and repositories options unsetopt Uset/remove configuration and repositories options setvar Set variables unsetvar Unset/remove variables Note: Main configuration: libdnf5 reads the distribution configuration from the drop-in directory ("/usr/share/dnf5/libdnf.conf.d") and system configuration from drop-in directory ("/etc/dnf/libdnf5.conf.d"). Last, it loads the system configuration file (by default "/etc/dnf/dnf.conf"). The latter has the highest priority and is modified by config-manager. Repository configuration: Libdnf5 loads the repositories configuration and then loads the configuration overrides. Configuration overrides are stored in files in the "/usr/share/dnf5/repos.override.d" and "/etc/dnf/repos.override.d" directories. The files are sorted alphabetically. The override from the next file overrides the previous one - the last override value wins. The config-manager writes the repositories configuration changes to the file "/etc/dnf/repos.override.d/99-config-manager.repo". --- dnf5-plugins/CMakeLists.txt | 1 + .../config-manager_plugin/CMakeLists.txt | 19 + .../config-manager_plugin/addrepo.cpp | 488 ++++++++++++++++++ .../config-manager_plugin/addrepo.hpp | 84 +++ .../config-manager_plugin/config-manager.cpp | 60 +++ .../config-manager_plugin/config-manager.hpp | 39 ++ .../config-manager_cmd_plugin.cpp | 74 +++ dnf5-plugins/config-manager_plugin/setopt.cpp | 208 ++++++++ dnf5-plugins/config-manager_plugin/setopt.hpp | 47 ++ dnf5-plugins/config-manager_plugin/setvar.cpp | 106 ++++ dnf5-plugins/config-manager_plugin/setvar.hpp | 43 ++ dnf5-plugins/config-manager_plugin/shared.hpp | 94 ++++ .../config-manager_plugin/unsetopt.cpp | 153 ++++++ .../config-manager_plugin/unsetopt.hpp | 47 ++ .../config-manager_plugin/unsetvar.cpp | 82 +++ .../config-manager_plugin/unsetvar.hpp | 43 ++ dnf5.spec | 9 +- 17 files changed, 1596 insertions(+), 1 deletion(-) create mode 100644 dnf5-plugins/config-manager_plugin/CMakeLists.txt create mode 100644 dnf5-plugins/config-manager_plugin/addrepo.cpp create mode 100644 dnf5-plugins/config-manager_plugin/addrepo.hpp create mode 100644 dnf5-plugins/config-manager_plugin/config-manager.cpp create mode 100644 dnf5-plugins/config-manager_plugin/config-manager.hpp create mode 100644 dnf5-plugins/config-manager_plugin/config-manager_cmd_plugin.cpp create mode 100644 dnf5-plugins/config-manager_plugin/setopt.cpp create mode 100644 dnf5-plugins/config-manager_plugin/setopt.hpp create mode 100644 dnf5-plugins/config-manager_plugin/setvar.cpp create mode 100644 dnf5-plugins/config-manager_plugin/setvar.hpp create mode 100644 dnf5-plugins/config-manager_plugin/shared.hpp create mode 100644 dnf5-plugins/config-manager_plugin/unsetopt.cpp create mode 100644 dnf5-plugins/config-manager_plugin/unsetopt.hpp create mode 100644 dnf5-plugins/config-manager_plugin/unsetvar.cpp create mode 100644 dnf5-plugins/config-manager_plugin/unsetvar.hpp diff --git a/dnf5-plugins/CMakeLists.txt b/dnf5-plugins/CMakeLists.txt index 4c9571d68d..e311b35b9c 100644 --- a/dnf5-plugins/CMakeLists.txt +++ b/dnf5-plugins/CMakeLists.txt @@ -6,5 +6,6 @@ include_directories("${PROJECT_SOURCE_DIR}/dnf5/include/") add_subdirectory("builddep_plugin") add_subdirectory("changelog_plugin") +add_subdirectory("config-manager_plugin") add_subdirectory("copr_plugin") add_subdirectory("repoclosure_plugin") diff --git a/dnf5-plugins/config-manager_plugin/CMakeLists.txt b/dnf5-plugins/config-manager_plugin/CMakeLists.txt new file mode 100644 index 0000000000..73b8d4f866 --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/CMakeLists.txt @@ -0,0 +1,19 @@ +# set gettext domain for translations +add_definitions(-DGETTEXT_DOMAIN=\"dnf5_cmd_config-manager\") + +file(GLOB CONFIG_MANAGER_SOURCES *.cpp) +add_library(config-manager_cmd_plugin MODULE ${CONFIG_MANAGER_SOURCES}) + +# disable the 'lib' prefix in order to create changelog_cmd_plugin.so +set_target_properties(config-manager_cmd_plugin PROPERTIES PREFIX "") + +pkg_check_modules(LIBFMT REQUIRED fmt) +target_link_libraries(libdnf5-cli PUBLIC ${LIBFMT_LIBRARIES}) + +find_package(CURL 7.62.0 REQUIRED) +include_directories(${CURL_INCLUDE_DIR}) + +target_link_libraries(config-manager_cmd_plugin PRIVATE libdnf5 libdnf5-cli) +target_link_libraries(config-manager_cmd_plugin PRIVATE dnf5) + +install(TARGETS config-manager_cmd_plugin LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}/dnf5/plugins/) diff --git a/dnf5-plugins/config-manager_plugin/addrepo.cpp b/dnf5-plugins/config-manager_plugin/addrepo.cpp new file mode 100644 index 0000000000..eb4d384f50 --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/addrepo.cpp @@ -0,0 +1,488 @@ +/* +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 "addrepo.hpp" + +#include "shared.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace dnf5 { + +using namespace libdnf5; + +namespace { + +// Extracts a specific part of the URL from a URL string. +// Returns an empty string if the URL is not valid/supported or the required part is not present. +std::string get_url_part(const std::string & url, CURLUPart what_part) { + std::string ret; + CURLUcode rc; + CURLU * c_url = curl_url(); + rc = curl_url_set(c_url, CURLUPART_URL, url.c_str(), 0); + if (!rc) { + char * part; + rc = curl_url_get(c_url, what_part, &part, 0); + if (!rc) { + ret = part; + curl_free(part); + } + } + curl_url_cleanup(c_url); + return ret; +} + + +// Computes CRC32 checksum of input string. +// Slow bitwise implementation. Used to calculate the checksum of the omitted part of long URLs. +uint32_t crc32(std::string_view input) { + const uint32_t polynomial = 0x04C11DB7; + uint32_t crc = 0; + + for (auto ch : input) { + crc ^= static_cast(ch) << 24; + for (int i = 0; i < 8; ++i) { + if ((crc & 0x80000000) != 0) { + crc = (crc << 1) ^ polynomial; + } else { + crc <<= 1; + } + } + } + return crc; +} + + +// Converts all letters consider illegal in repository id to their "_XX" versions (XX - hex code). +std::string escape(const std::string & text) { + static constexpr const char * digits = "0123456789ABCDEF"; + char tmp[] = "_XX"; + std::string ret; + ret.reserve(text.size() * 3); + for (const char ch : text) { + if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch == '-' || + ch == '.' || ch == ':' || ch == '_') { + ret += ch; + } else { + const auto uch = static_cast(ch); + tmp[2] = digits[uch & 0x0F]; + tmp[1] = digits[(uch >> 4) & 0x0F]; + ret += tmp; + } + } + return ret; +} + + +// Regular expressions to sanitise filename +const std::regex RE_SCHEME{R"(^\w+:/*(\w+:|www\.)?)"}; +const std::regex RE_SLASH{R"([?/:&#|~\*\[\]\(\)'\\]+)"}; +const std::regex RE_BEGIN{"^[,.]*"}; +const std::regex RE_FINAL{"[,.]*$"}; + +// Returns a filename suitable for the filesystem and for repository id. +// Strips dangerous and common characters, encodes some characters and limits the length. +std::string sanitize_url(const std::string & url) { + std::string ret; + ret = std::regex_replace(url, RE_SCHEME, ""); + ret = std::regex_replace(ret, RE_SLASH, "_"); + ret = std::regex_replace(ret, RE_BEGIN, ""); + ret = std::regex_replace(ret, RE_FINAL, ""); + ret = escape(ret); + + // Limits length of url. + // Copies the first and last 100 characters. The substring in between is replaced by a crc32 checksum. + if (ret.size() > 250) { + std::string_view tmp{ret}; + ret = fmt::format( + "{}-{:08X}-{}", tmp.substr(0, 100), crc32(tmp.substr(100, tmp.size() - 200)), tmp.substr(tmp.size() - 100)); + } + + return ret; +} + +} // namespace + + +void ConfigManagerAddRepoCommand::set_argument_parser() { + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + + auto & cmd = *get_argument_parser_command(); + cmd.set_description( + "Add repositories from the specified configuration file or define a new repository using user options"); + cmd.set_long_description( + "Add repositories from the specified configuration file or define a new repository using user options."); + + auto from_repofile_opt = parser.add_new_named_arg("from-repofile"); + from_repofile_opt->set_long_name("from-repofile"); + from_repofile_opt->set_description("Download repository configuration file, test it and put it in reposdir"); + from_repofile_opt->set_has_value(true); + from_repofile_opt->set_arg_value_help("REPO_CONFIGURATION_FILE_URL"); + from_repofile_opt->set_parse_hook_func([this](cli::ArgumentParser::NamedArg *, const char *, const char * value) { + source_repofile.location = value; + // If the URL is invalid. Scheme not found. It can be a path to a local file. + source_repofile.is_local_path = get_url_part(source_repofile.location, CURLUPART_SCHEME) == ""; + if (source_repofile.is_local_path) { + // Tests whether it is really a path to an existing local file. + try { + if (!std::filesystem::exists(source_repofile.location)) { + throw ConfigManagerError(M_("from-repofile: \"{}\" file does not exist"), source_repofile.location); + } + } catch (const std::filesystem::filesystem_error & ex) { + throw ConfigManagerError(M_("from-repofile: {}"), std::string{ex.what()}); + } + } + return true; + }); + cmd.register_named_arg(from_repofile_opt); + + auto repo_id_opt = parser.add_new_named_arg("id"); + repo_id_opt->set_long_name("id"); + repo_id_opt->set_description("Set id for newly created repository"); + repo_id_opt->set_has_value(true); + repo_id_opt->set_arg_value_help("REPO_ID"); + repo_id_opt->set_parse_hook_func([this](cli::ArgumentParser::NamedArg *, const char *, const char * value) { + repo_id = value; + return true; + }); + cmd.register_named_arg(repo_id_opt); + + auto set_opt = parser.add_new_named_arg("set"); + set_opt->set_long_name("set"); + set_opt->set_description("Set option in newly created repository"); + set_opt->set_has_value(true); + set_opt->set_arg_value_help("REPO_OPTION=VALUE"); + set_opt->set_parse_hook_func([this]( + [[maybe_unused]] cli::ArgumentParser::NamedArg * arg, + [[maybe_unused]] const char * option, + const char * value) { + auto val = strchr(value + 1, '='); + if (!val) { + throw cli::ArgumentParserError(M_("set: Badly formatted argument value \"{}\""), std::string{value}); + } + std::string key{value, val}; + std::string key_value{val + 1}; + + // Test if the repository option can be set. + try { + tmp_repo_conf.opt_binds().at(key).new_string(Option::Priority::RUNTIME, key_value); + } catch (const Error & ex) { + throw ConfigManagerError( + M_("Cannot set repository option \"{}={}\": {}"), key, key_value, std::string{ex.what()}); + } + + // Save the repo option for later writing to a file. + const auto [it, inserted] = repo_opts.insert({key, key_value}); + if (!inserted) { + if (it->second != key_value) { + throw ConfigManagerError( + M_("Sets the \"{}\" option again with a different value: \"{}\" != \"{}\""), + key, + it->second, + key_value); + } + } + return true; + }); + cmd.register_named_arg(set_opt); + + auto overwrite_opt = parser.add_new_named_arg("overwrite"); + overwrite_opt->set_long_name("overwrite"); + overwrite_opt->set_description("Allow overwriting of existing repository configuration file"); + overwrite_opt->set_has_value(false); + overwrite_opt->set_parse_hook_func([this](cli::ArgumentParser::NamedArg *, const char *, const char *) { + overwrite = true; + return true; + }); + cmd.register_named_arg(overwrite_opt); + + auto save_filename_opt = parser.add_new_named_arg("save-filename"); + save_filename_opt->set_long_name("save-filename"); + save_filename_opt->set_description( + "Set the name of the configuration file of the added repository. The \".repo\" extension is added if it is " + "missing."); + save_filename_opt->set_has_value(true); + save_filename_opt->set_arg_value_help("FILENAME"); + save_filename_opt->set_parse_hook_func([this](cli::ArgumentParser::NamedArg *, const char *, const char * value) { + save_filename = value; + return true; + }); + cmd.register_named_arg(save_filename_opt); + + // Set conflicting arguments + repo_id_opt->add_conflict_argument(*from_repofile_opt); + set_opt->add_conflict_argument(*from_repofile_opt); +} + + +void ConfigManagerAddRepoCommand::configure() { + auto & ctx = get_context(); + auto & base = ctx.base; + + const auto & repo_dirs = base.get_config().get_reposdir_option().get_value(); + if (repo_dirs.empty()) { + throw ConfigManagerError(M_("Missing path to repository configuration directory")); + } + + std::filesystem::path dest_repo_dir = repo_dirs.front(); + + if (source_repofile.location.empty()) { + create_repo(repo_id, repo_opts, dest_repo_dir); + } else { + add_repos_from_repofile(source_repofile, dest_repo_dir); + } +} + + +void ConfigManagerAddRepoCommand::add_repos_from_repofile( + const SourceRepofile & source_repofile, const std::filesystem::path & dest_repo_dir) { + auto & ctx = get_context(); + auto & base = ctx.base; + auto logger = base.get_logger(); + + if (save_filename.empty()) { + if (source_repofile.is_local_path) { + save_filename = std::filesystem::path(source_repofile.location).filename(); + } else { + save_filename = std::filesystem::path(get_url_part(source_repofile.location, CURLUPART_PATH)).filename(); + } + } + if (!save_filename.ends_with(".repo")) { + save_filename += ".repo"; + } + auto dest_path = dest_repo_dir / save_filename; + + test_if_filepath_not_exist(dest_path); + + // Creates an open temporary file. It then closes it but does not remove it. + // In the following code, this temporary file is used to store the copied/downloaded configuration. + auto tmpfilepath = dest_path.string() + ".XXXXXX"; + auto fd = mkstemp(tmpfilepath.data()); + if (fd == -1) { + throw std::filesystem::filesystem_error( + "cannot create temporary file", tmpfilepath, std::error_code(errno, std::system_category())); + } + close(fd); + + try { + if (source_repofile.is_local_path) { + try { + std::filesystem::copy_file( + source_repofile.location, tmpfilepath, std::filesystem::copy_options::overwrite_existing); + } catch (const std::filesystem::filesystem_error & e) { + throw ConfigManagerError( + M_("Failed to copy repository configuration file \"{}\": {}"), + source_repofile.location, + std::string{e.what()}); + } + } else { + try { + repo::FileDownloader downloader(base); + downloader.add(source_repofile.location, tmpfilepath); + downloader.download(); + } catch (const repo::FileDownloadError & e) { + throw ConfigManagerError( + M_("Failed to download repository configuration file \"{}\": {}"), + source_repofile.location, + std::string{e.what()}); + } + } + + ConfigParser parser; + parser.read(tmpfilepath); + std::vector repo_ids; + repo_ids.reserve(parser.get_data().size()); + for (const auto & [repo_id, opts] : parser.get_data()) { + repo_ids.emplace_back(repo_id); + } + test_if_ids_not_already_exist(repo_ids, dest_path); + + // Test if the repository options can be set. + for (const auto & [repo_id, repo_opts] : parser.get_data()) { + for (const auto & [key, key_val] : repo_opts) { + try { + tmp_repo_conf.opt_binds().at(key).new_string(Option::Priority::RUNTIME, key_val); + } catch (const Error & ex) { + throw ConfigManagerError( + M_("Error in added repository configuration file. Cannot set repository option \"{}={}\": {}"), + key, + key_val, + std::string{ex.what()}); + } + } + } + } catch (const Error & ex) { + std::error_code ec; + std::filesystem::remove(tmpfilepath, ec); + throw; + } + + // All tests passed. Renames the configuration file to the final name. + std::filesystem::rename(tmpfilepath, dest_path); + logger->info("config-manager: Added repofile \"{}\" from \"{}\"", dest_path.string(), source_repofile.location); + set_file_permissions(dest_path); +} + + +void ConfigManagerAddRepoCommand::create_repo( + std::string repo_id, + const std::map & repo_opts, + const std::filesystem::path & dest_repo_dir) { + auto & ctx = get_context(); + auto & base = ctx.base; + auto logger = base.get_logger(); + + // Test for presence of required arguments. + // And sets the URL - used to create the repository ID if not specified. + std::string url; + if (const auto it = repo_opts.find("baseurl"); it != repo_opts.end() && !it->second.empty()) { + const auto urls = OptionStringList(std::vector{}).from_string(it->second); + if (urls.empty() || (url = urls.front()).empty()) { + throw ConfigManagerError(M_("Bad baseurl: {}={}"), it->first, it->second); + } + } else if (const auto it = repo_opts.find("mirrorlist"); it != repo_opts.end() && !it->second.empty()) { + url = it->second; + } else if (const auto it = repo_opts.find("metalink"); it != repo_opts.end() && !it->second.empty()) { + url = it->second; + } else { + throw cli::ArgumentParserMissingDependentArgumentError( + M_("One of --from-repofile=, --set=baseurl=, --set=mirrorlist=, --set=metalink= " + "must be set to a non-empty URL")); + } + + if (repo_id.empty()) { + repo_id = sanitize_url(url); + } + + if (save_filename.empty()) { + save_filename = repo_id; + } + if (!save_filename.ends_with(".repo")) { + save_filename += ".repo"; + } + auto dest_path = dest_repo_dir / save_filename; + + test_if_filepath_not_exist(dest_path); + test_if_ids_not_already_exist({repo_id}, dest_path); + + ConfigParser parser; + parser.add_section(repo_id); + + // Sets the default repository name. May be overwritten with "--set=name=". + parser.set_value(repo_id, "name", "created by dnf5 config-manager"); + // Enables repository by default. The repository can be disabled with "--set=enabled=0". + parser.set_value(repo_id, "enabled", "1"); + + for (const auto & [key, key_val] : repo_opts) { + parser.set_value(repo_id, key, key_val); + } + + try { + parser.write(dest_path, false); + logger->info("config-manager: Added new repo \"{}\" to file \"{}\"", repo_id, dest_path.string()); + } catch (const std::runtime_error & e) { + throw ConfigManagerError( + M_("Failed to save repository configuration file \"{}\": {}"), dest_path.native(), std::string{e.what()}); + } + set_file_permissions(dest_path); +} + + +void ConfigManagerAddRepoCommand::test_if_filepath_not_exist(const std::filesystem::path & path) const { + if (!overwrite && std::filesystem::exists(path)) { + ConfigParser parser; + parser.read(path); + std::string repo_ids; + bool first{true}; + for (const auto & [repo_id, opts] : parser.get_data()) { + if (first) { + first = false; + } else { + repo_ids += ' '; + } + repo_ids += repo_id; + } + throw ConfigManagerError( + M_("File \"{}\" already exists and configures repositories with IDs \"{}\"." + " Add \"--overwrite\" to overwrite."), + path.string(), + repo_ids); + } +} + + +void ConfigManagerAddRepoCommand::test_if_ids_not_already_exist( + const std::vector & repo_ids, const std::filesystem::path & ignore_path) const { + auto & ctx = get_context(); + auto & base = ctx.base; + auto logger = base.get_logger(); + + // The repository can also be defined in the main configuration file. + if (const auto & conf_path = get_config_file_path(base.get_config()); std::filesystem::exists(conf_path)) { + ConfigParser parser; + parser.read(conf_path); + for (const auto & repo_id : repo_ids) { + if (parser.has_section(repo_id)) { + throw ConfigManagerError( + M_("A repository with id \"{}\" already configured in file: {}"), repo_id, conf_path.string()); + } + } + } + + const auto repo_dirs = base.get_config().get_reposdir_option().get_value(); + for (const std::filesystem::path dir : repo_dirs) { + if (std::filesystem::exists(dir)) { + std::error_code ec; + std::filesystem::directory_iterator di(dir, ec); + if (ec) { + logger->warning("Cannot read repositories from directory \"{}\": {}", dir.string(), ec.message()); + continue; + } + for (auto & dentry : di) { + const auto & path = dentry.path(); + if (path == ignore_path) { + continue; + } + if (path.extension() == ".repo") { + ConfigParser parser; + parser.read(path); + for (const auto & repo_id : repo_ids) { + if (parser.has_section(repo_id)) { + throw ConfigManagerError( + M_("A repository with id \"{}\" already configured in file: {}"), + repo_id, + path.string()); + } + } + } + } + } + } +} + +} // namespace dnf5 diff --git a/dnf5-plugins/config-manager_plugin/addrepo.hpp b/dnf5-plugins/config-manager_plugin/addrepo.hpp new file mode 100644 index 0000000000..7af1a52a0e --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/addrepo.hpp @@ -0,0 +1,84 @@ +/* +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_CONFIG_MANAGER_CONFIG_MANAGER_ADDREPO_HPP +#define DNF5_COMMANDS_CONFIG_MANAGER_CONFIG_MANAGER_ADDREPO_HPP + +#include + +#include +#include +#include +#include + +namespace dnf5 { + +class ConfigManagerAddRepoCommand : public Command { +public: + explicit ConfigManagerAddRepoCommand(Context & context) : Command(context, "addrepo") {} + void set_argument_parser() override; + void configure() override; + +private: + struct SourceRepofile { + std::string location; + bool is_local_path; + }; + + /// Copies/downloads the repository configuration file. + /// Tests the content. And if it's ok, it saves it in `dest_repo_dir`. + /// @param source_repofile Location of the source repositories configuration file (URL or local file path). + /// @param dest_repo_dir The path to the directory where the configuration file will be stored. + void add_repos_from_repofile(const SourceRepofile & source_repofile, const std::filesystem::path & dest_repo_dir); + + /// Creates a new repository configuration from repository options. + /// @param repo_id The ID of the new repository, if it is empty, is automatically generated from baseurl, mirrorlist, metalink. + /// @param repo_opts Options for the new repository. + /// @param dest_repo_dir The path to the directory where the configuration file will be stored. + void create_repo( + std::string repo_id, + const std::map & repo_opts, + const std::filesystem::path & dest_repo_dir); + + /// Tests if the file does not exist. + /// @param path Path to check. + /// @throws ConfigManagerError Trown if `path` already exist and overwriting is not allowed. + void test_if_filepath_not_exist(const std::filesystem::path & path) const; + + /// Tests if the repositories IDs in the vector do not already exist in the configuration. + /// @param repo_ids List of repositories IDs to check. + /// @param ignore_path The file in this path will be ignored/skiped. + /// @throws ConfigManagerError Trown if an already existent repository ID was found. + void test_if_ids_not_already_exist( + const std::vector & repo_ids, const std::filesystem::path & ignore_path) const; + + libdnf5::ConfigMain tmp_config; + libdnf5::repo::ConfigRepo tmp_repo_conf{tmp_config, "temporary_to_check_repository_options"}; + + SourceRepofile source_repofile; // Location of source repository configuration file. + std::string repo_id; // The user-defined ID of the newly created repository. + bool overwrite{false}; // Allows to overwrite an existing configuration file. + std::string save_filename; // User-defined name of newly saved configuration file. + std::map repo_opts; // Options for the new repository. +}; + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_CONFIG_MANAGER_CONFIG_MANAGER_ADDREPO_HPP diff --git a/dnf5-plugins/config-manager_plugin/config-manager.cpp b/dnf5-plugins/config-manager_plugin/config-manager.cpp new file mode 100644 index 0000000000..e8039bfaa9 --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/config-manager.cpp @@ -0,0 +1,60 @@ +/* +Copyright Contributors to the libdnf project. + +This file is part of libdnf: https://github.com/rpm-software-management/libdnf/ + +Libdnf is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 2 of the License, or +(at your option) any later version. + +Libdnf is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with libdnf. If not, see . +*/ + +#include "config-manager.hpp" + +#include "addrepo.hpp" +#include "setopt.hpp" +#include "setvar.hpp" +#include "unsetopt.hpp" +#include "unsetvar.hpp" + +namespace dnf5 { + +using namespace libdnf5::cli; + +void ConfigManagerCommand::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 ConfigManagerCommand::set_argument_parser() { + auto & cmd = *get_argument_parser_command(); + cmd.set_description("Manage configuration"); + cmd.set_long_description("Manage main and repositories configuration, variables and add new repositories."); +} + +void ConfigManagerCommand::register_subcommands() { + auto * config_manager_commands_group = get_context().get_argument_parser().add_new_group("config-manager_commands"); + config_manager_commands_group->set_header("Commands:"); + get_argument_parser_command()->register_group(config_manager_commands_group); + register_subcommand(std::make_unique(get_context()), config_manager_commands_group); + register_subcommand(std::make_unique(get_context()), config_manager_commands_group); + register_subcommand(std::make_unique(get_context()), config_manager_commands_group); + register_subcommand(std::make_unique(get_context()), config_manager_commands_group); + register_subcommand(std::make_unique(get_context()), config_manager_commands_group); +} + +void ConfigManagerCommand::pre_configure() { + throw_missing_command(); +} + +} // namespace dnf5 diff --git a/dnf5-plugins/config-manager_plugin/config-manager.hpp b/dnf5-plugins/config-manager_plugin/config-manager.hpp new file mode 100644 index 0000000000..d6dace3188 --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/config-manager.hpp @@ -0,0 +1,39 @@ +/* +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_CONFIG_MANAGER_CONFIG_MANAGER_HPP +#define DNF5_COMMANDS_CONFIG_MANAGER_CONFIG_MANAGER_HPP + +#include + +namespace dnf5 { + +class ConfigManagerCommand : public Command { +public: + explicit ConfigManagerCommand(Context & context) : Command(context, "config-manager") {} + void set_parent_command() override; + void set_argument_parser() override; + void register_subcommands() override; + void pre_configure() override; +}; + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_CONFIG_MANAGER_CONFIG_MANAGER_HPP diff --git a/dnf5-plugins/config-manager_plugin/config-manager_cmd_plugin.cpp b/dnf5-plugins/config-manager_plugin/config-manager_cmd_plugin.cpp new file mode 100644 index 0000000000..8da8769bee --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/config-manager_cmd_plugin.cpp @@ -0,0 +1,74 @@ +#include "config-manager.hpp" + +#include + +#include + +using namespace dnf5; + +namespace { + +constexpr const char * PLUGIN_NAME{"config-manager"}; +constexpr PluginVersion PLUGIN_VERSION{.major = 0, .minor = 1, .micro = 0}; + +constexpr const char * attrs[]{"author.name", "author.email", "description", nullptr}; +constexpr const char * attrs_value[]{"Jaroslav Rohel", "jrohel@redhat.com", "config-manager command"}; + +class ConfigManagerCmdPlugin : public IPlugin { +public: + using IPlugin::IPlugin; + + PluginAPIVersion get_api_version() const noexcept override { return PLUGIN_API_VERSION; } + + const char * get_name() const noexcept override { return PLUGIN_NAME; } + + PluginVersion get_version() const noexcept override { return PLUGIN_VERSION; } + + const char * const * get_attributes() const noexcept override { return attrs; } + + const char * get_attribute(const char * attribute) const noexcept override { + for (size_t i = 0; attrs[i]; ++i) { + if (std::strcmp(attribute, attrs[i]) == 0) { + return attrs_value[i]; + } + } + return nullptr; + } + + std::vector> create_commands() override; + + void finish() noexcept override {} +}; + + +std::vector> ConfigManagerCmdPlugin::create_commands() { + std::vector> commands; + commands.push_back(std::make_unique(get_context())); + return commands; +} + + +} // namespace + + +PluginAPIVersion dnf5_plugin_get_api_version(void) { + return PLUGIN_API_VERSION; +} + +const char * dnf5_plugin_get_name(void) { + return PLUGIN_NAME; +} + +PluginVersion dnf5_plugin_get_version(void) { + return PLUGIN_VERSION; +} + +IPlugin * dnf5_plugin_new_instance([[maybe_unused]] ApplicationVersion application_version, Context & context) try { + return new ConfigManagerCmdPlugin(context); +} catch (...) { + return nullptr; +} + +void dnf5_plugin_delete_instance(IPlugin * plugin_object) { + delete plugin_object; +} diff --git a/dnf5-plugins/config-manager_plugin/setopt.cpp b/dnf5-plugins/config-manager_plugin/setopt.cpp new file mode 100644 index 0000000000..d4b5be8c0f --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/setopt.cpp @@ -0,0 +1,208 @@ +/* +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 "setopt.hpp" + +#include "shared.hpp" + +#include +#include + +#include + +namespace dnf5 { + +using namespace libdnf5; + +namespace { + +constexpr std::string_view REPOS_OVERRIDE_CFG_HEADER = + "# Generated by dnf5 config-manager.\n# Do not modify this file manually, use dnf5 config-manager instead.\n"; + +void modify_config( + ConfigParser & parser, const std::string & section_id, const std::map & opts) { + if (!parser.has_section(section_id)) { + parser.add_section(section_id); + } + for (const auto & [key, value] : opts) { + parser.set_value(section_id, key, value, ""); + } +} + +} // namespace + + +void ConfigManagerSetOptCommand::set_argument_parser() { + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + + auto & cmd = *get_argument_parser_command(); + cmd.set_description("Set configuration and repositories options"); + + auto opts_vals = + parser.add_new_positional_arg("optvals", cli::ArgumentParser::PositionalArg::AT_LEAST_ONE, nullptr, nullptr); + opts_vals->set_description("List of options with values. Format: \"[REPO_ID.]option=value\""); + opts_vals->set_parse_hook_func([this]( + [[maybe_unused]] cli::ArgumentParser::PositionalArg * arg, + int argc, + const char * const argv[]) { + for (int i = 0; i < argc; ++i) { + auto value = argv[i]; + auto val = strchr(value + 1, '='); + if (!val) { + throw cli::ArgumentParserError(M_("optval: Badly formatted argument value \"{}\""), std::string{value}); + } + std::string key{value, val}; + std::string key_value{val + 1}; + auto dot_pos = key.rfind('.'); + if (dot_pos != std::string::npos) { + if (dot_pos == key.size() - 1) { + throw cli::ArgumentParserError( + M_("optval: Badly formatted argument value: Last key character cannot be '.': {}"), + std::string{value}); + } + + // Save the repository option for later processing (solving glob patter, writing to file). + auto repo_id = key.substr(0, dot_pos); + if (repo_id.empty()) { + throw cli::ArgumentParserError( + M_("optval: Empty repository id is not allowed: {}"), std::string{value}); + } + auto repo_key = key.substr(dot_pos + 1); + + // Test if the repository option is known and can be set. + try { + tmp_repo_conf.opt_binds().at(repo_key).new_string(Option::Priority::COMMANDLINE, key_value); + } catch (const Error & ex) { + throw ConfigManagerError( + M_("Cannot set repository option \"{}\": {}"), std::string{value}, std::string{ex.what()}); + } + + const auto [it, inserted] = in_repos_setopts[repo_id].insert({repo_key, key_value}); + if (!inserted) { + if (it->second != key_value) { + throw ConfigManagerError( + M_("Sets the \"{}\" option of the repository \"{}\" again with a different value: \"{}\" " + "!= \"{}\""), + repo_key, + repo_id, + it->second, + key_value); + } + } + } else { + // Test if the global option is known and can be set. + try { + tmp_config.opt_binds().at(key).new_string(Option::Priority::COMMANDLINE, key_value); + } catch (const Error & ex) { + throw ConfigManagerError( + M_("Cannot set option: \"{}\": {}"), std::string{value}, std::string(ex.what())); + } + + // Save the global option for later writing to a file. + const auto [it, inserted] = main_setopts.insert({key, key_value}); + if (!inserted) { + if (it->second != key_value) { + throw ConfigManagerError( + M_("Sets the \"{}\" option again with a different value: \"{}\" != \"{}\""), + key, + it->second, + key_value); + } + } + } + } + return true; + }); + cmd.register_positional_arg(opts_vals); +} + + +void ConfigManagerSetOptCommand::configure() { + auto & ctx = get_context(); + + repo::RepoQuery repo_query(ctx.base); + for (auto & [in_repo_id, repo_setopts] : in_repos_setopts) { + auto query = repo_query; + query.filter_id(in_repo_id, sack::QueryCmp::GLOB); + if (query.empty()) { + throw ConfigManagerError(M_("No matching repository to modify: {}"), in_repo_id); + } + for (auto repo : query) { + auto & repo_conf = repo->get_config(); + auto repo_id = repo_conf.get_id(); + for (const auto & [key, value] : repo_setopts) { + // Save the repository option for later writing to a file. + const auto [it, inserted] = matching_repos_setopts[repo_id].insert({key, value}); + if (!inserted) { + if (it->second != value) { + throw ConfigManagerError( + M_("Sets the \"{}\" option of the repository \"{}\" again with a different value: \"{}\" " + "!= \"{}\""), + key, + repo_id, + it->second, + value); + } + } + } + } + } + + // Write new and modify existing options in the main configuration file. + if (!main_setopts.empty()) { + ConfigParser parser; + + const auto & cfg_filepath = get_config_file_path(ctx.base.get_config()); + + const bool exists = std::filesystem::exists(cfg_filepath); + if (exists) { + parser.read(cfg_filepath); + } + + modify_config(parser, "main", main_setopts); + parser.write(cfg_filepath, false); + if (!exists) { + set_file_permissions(cfg_filepath); + } + } + + // Write new and modify existing options in the repositories overrides configuration file. + if (!matching_repos_setopts.empty()) { + ConfigParser parser; + + const bool exists = std::filesystem::exists(CFG_MANAGER_REPOS_OVERRIDE_FILEPATH); + if (exists) { + parser.read(CFG_MANAGER_REPOS_OVERRIDE_FILEPATH); + } + + parser.get_header() = REPOS_OVERRIDE_CFG_HEADER; + + for (const auto & [repo_id, repo_opts] : matching_repos_setopts) { + modify_config(parser, repo_id, repo_opts); + } + + parser.write(CFG_MANAGER_REPOS_OVERRIDE_FILEPATH, false); + if (!exists) { + set_file_permissions(CFG_MANAGER_REPOS_OVERRIDE_FILEPATH); + } + } +} + +} // namespace dnf5 diff --git a/dnf5-plugins/config-manager_plugin/setopt.hpp b/dnf5-plugins/config-manager_plugin/setopt.hpp new file mode 100644 index 0000000000..27a52e01b8 --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/setopt.hpp @@ -0,0 +1,47 @@ +/* +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_CONFIG_MANAGER_SETOPT_HPP +#define DNF5_COMMANDS_CONFIG_MANAGER_SETOPT_HPP + +#include + +#include +#include + +namespace dnf5 { + +class ConfigManagerSetOptCommand : public Command { +public: + explicit ConfigManagerSetOptCommand(Context & context) : Command(context, "setopt") {} + void set_argument_parser() override; + void configure() override; + +private: + libdnf5::ConfigMain tmp_config; + libdnf5::repo::ConfigRepo tmp_repo_conf{tmp_config, "temporary_to_check_repository_options"}; + std::map main_setopts; + std::map> in_repos_setopts; + std::map> matching_repos_setopts; +}; + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_CONFIG_MANAGER_SETOPT_HPP diff --git a/dnf5-plugins/config-manager_plugin/setvar.cpp b/dnf5-plugins/config-manager_plugin/setvar.cpp new file mode 100644 index 0000000000..7ba9dd5106 --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/setvar.cpp @@ -0,0 +1,106 @@ +/* +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 "setvar.hpp" + +#include "shared.hpp" + +#include + +#include + +namespace dnf5 { + +using namespace libdnf5; + +void ConfigManagerSetVarCommand::set_argument_parser() { + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + + auto & cmd = *get_argument_parser_command(); + cmd.set_description("Set variables"); + + auto vars_vals = + parser.add_new_positional_arg("varvals", cli::ArgumentParser::PositionalArg::AT_LEAST_ONE, nullptr, nullptr); + vars_vals->set_description("List of variables with values. Format: \"variable=value\""); + vars_vals->set_parse_hook_func( + [this, &ctx]([[maybe_unused]] cli::ArgumentParser::PositionalArg * arg, int argc, const char * const argv[]) { + for (int i = 0; i < argc; ++i) { + auto value = argv[i]; + auto val = strchr(value + 1, '='); + if (!val) { + throw cli::ArgumentParserError( + M_("varval: Badly formatted argument value \"{}\""), std::string{value}); + } + std::string var_name{value, val}; + std::string var_value{val + 1}; + + check_variable_name(var_name); + + // Test that the variable is not read-only. + auto vars = ctx.base.get_vars(); + if (vars->is_read_only(var_name)) { + throw ConfigManagerError( + M_("Cannot set \"{}\": Variable \"{}\" is read-only"), std::string{value}, var_name); + } + + // Save the variable for later writing to a file. + const auto [it, inserted] = setvars.insert({var_name, var_value}); + if (!inserted) { + if (it->second != var_value) { + throw ConfigManagerError( + M_("Sets the \"{}\" variable again with a different value: \"{}\" != \"{}\""), + var_name, + it->second, + var_value); + } + } + } + return true; + }); + cmd.register_positional_arg(vars_vals); +} + + +void ConfigManagerSetVarCommand::configure() { + auto & ctx = get_context(); + + if (!setvars.empty()) { + const auto & vars_dir = get_last_vars_dir_path(ctx.base.get_config()); + if (vars_dir.empty()) { + throw ConfigManagerError(M_("Missing path to vars directory")); + } + + for (const auto & [name, value] : setvars) { + const auto filepath = vars_dir / name; + std::ofstream file; + file.exceptions(std::ofstream::failbit | std::ofstream::badbit); + try { + file.open(filepath, std::ios_base::trunc | std::ios_base::binary); + file << value; + } catch (const std::ios_base::failure & e) { + throw ConfigManagerError( + M_("Cannot write variable to file \"{}\": {}"), filepath.native(), std::string{e.what()}); + } + set_file_permissions(filepath); + } + } +} + +} // namespace dnf5 diff --git a/dnf5-plugins/config-manager_plugin/setvar.hpp b/dnf5-plugins/config-manager_plugin/setvar.hpp new file mode 100644 index 0000000000..e897a9add6 --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/setvar.hpp @@ -0,0 +1,43 @@ +/* +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_CONFIG_MANAGER_SETVAR_HPP +#define DNF5_COMMANDS_CONFIG_MANAGER_SETVAR_HPP + +#include + +#include +#include + +namespace dnf5 { + +class ConfigManagerSetVarCommand : public Command { +public: + explicit ConfigManagerSetVarCommand(Context & context) : Command(context, "setvar") {} + void set_argument_parser() override; + void configure() override; + +private: + std::map setvars; +}; + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_CONFIG_MANAGER_SETVAR_HPP diff --git a/dnf5-plugins/config-manager_plugin/shared.hpp b/dnf5-plugins/config-manager_plugin/shared.hpp new file mode 100644 index 0000000000..7145e45fd1 --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/shared.hpp @@ -0,0 +1,94 @@ +/* +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_CONFIG_MANAGER_CONFIG_MANAGER_SHARED_HPP +#define DNF5_COMMANDS_CONFIG_MANAGER_CONFIG_MANAGER_SHARED_HPP + +#include +#include +#include + +#include + +namespace dnf5 { + +const std::filesystem::path CFG_MANAGER_REPOS_OVERRIDE_FILENAME = "99-config_manager.repo"; + +const std::filesystem::path CFG_MANAGER_REPOS_OVERRIDE_FILEPATH = + libdnf5::REPOS_OVERRIDE_DIR / CFG_MANAGER_REPOS_OVERRIDE_FILENAME; + +struct ConfigManagerError : public libdnf5::Error { + using Error::Error; + const char * get_domain_name() const noexcept override { return "dnf5"; } + const char * get_name() const noexcept override { return "ConfigManagerError"; } +}; + +inline void check_variable_name(const std::string & name) { + if (name.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789" + "_") != std::string::npos) { + throw ConfigManagerError(M_("Variable name can contain only ASCII letters, numbers and '_': {}"), name); + } +} + +// Sets permissions to "rw-r--r--" +inline void set_file_permissions(const std::filesystem::path & path) { + std::filesystem::permissions( + path, + std::filesystem::perms::owner_read | std::filesystem::perms::owner_write | std::filesystem::perms::group_read | + std::filesystem::perms::others_read); +} + +// Returns the actual path to the configuration file. It takes installroot into account. +// TODO(jrohel) It should be provided by libdnf. +inline std::filesystem::path get_config_file_path(const libdnf5::ConfigMain & config) { + std::filesystem::path conf_path{config.get_config_file_path_option().get_value()}; + const auto & conf_path_priority = config.get_config_file_path_option().get_priority(); + const auto & use_host_config = config.get_use_host_config_option().get_value(); + if (!use_host_config && conf_path_priority < libdnf5::Option::Priority::COMMANDLINE) { + conf_path = config.get_installroot_option().get_value() / conf_path.relative_path(); + } + return conf_path; +} + +// Returns the actual path to the last variable directory. It takes installroot into account. +// TODO(jrohel) Libdnf should provide paths taking into account installroot. +inline std::filesystem::path get_last_vars_dir_path(const libdnf5::ConfigMain & config) { + std::filesystem::path vars_dir_path; + + const auto & vars_dirs = config.get_varsdir_option().get_value(); + if (vars_dirs.empty()) { + return vars_dir_path; + } + vars_dir_path = vars_dirs.back(); + + const auto & varsdir_path_priority = config.get_varsdir_option().get_priority(); + const auto & use_host_config = config.get_use_host_config_option().get_value(); + if (!use_host_config && varsdir_path_priority < libdnf5::Option::Priority::COMMANDLINE) { + vars_dir_path = config.get_installroot_option().get_value() / vars_dir_path.relative_path(); + } + + return vars_dir_path; +} + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_CONFIG_MANAGER_CONFIG_MANAGER_SHARED_HPP diff --git a/dnf5-plugins/config-manager_plugin/unsetopt.cpp b/dnf5-plugins/config-manager_plugin/unsetopt.cpp new file mode 100644 index 0000000000..4037b5a44d --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/unsetopt.cpp @@ -0,0 +1,153 @@ +/* +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 "unsetopt.hpp" + +#include "shared.hpp" + +#include +#include + +#include + +namespace dnf5 { + +using namespace libdnf5; + +namespace { + +bool remove_from_config(ConfigParser & parser, const std::string & section_id, const std::set & keys) { + bool removed = false; + for (const auto & key : keys) { + removed |= parser.remove_option(section_id, key); + } + return removed; +} + +} // namespace + + +void ConfigManagerUnsetOptCommand::set_argument_parser() { + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + + auto & cmd = *get_argument_parser_command(); + cmd.set_description("Unset/remove configuration and repositories options"); + + auto opts_vals = + parser.add_new_positional_arg("options", cli::ArgumentParser::PositionalArg::AT_LEAST_ONE, nullptr, nullptr); + opts_vals->set_description("List of options to unset"); + opts_vals->set_parse_hook_func( + [this, &ctx]([[maybe_unused]] cli::ArgumentParser::PositionalArg * arg, int argc, const char * const argv[]) { + for (int i = 0; i < argc; ++i) { + auto value = argv[i]; + std::string key{value}; + auto dot_pos = key.rfind('.'); + if (dot_pos != std::string::npos) { + if (dot_pos == key.size() - 1) { + throw cli::ArgumentParserError( + M_("remove-opt: Badly formatted argument value: Last key character cannot be '.': {}"), + std::string{value}); + } + + // Save the repository option for later processing (solving glob patter, writing to file). + auto repo_id = key.substr(0, dot_pos); + if (repo_id.empty()) { + throw cli::ArgumentParserError( + M_("remove-opt: Empty repository id is not allowed: {}"), std::string{value}); + } + auto repo_key = key.substr(dot_pos + 1); + + // Test if the repository options are known. + try { + tmp_repo_conf.opt_binds().at(repo_key); + } catch (const OptionBindsOptionNotFoundError & ex) { + ctx.base.get_logger()->warning( + "config-manager: Request to remove unknown repository option from config file: {}", key); + } + + in_repos_opts_to_remove[repo_id].insert(repo_key); + } else { + // Test if the global option is known. + try { + tmp_config.opt_binds().at(key); + } catch (const OptionBindsOptionNotFoundError & ex) { + ctx.base.get_logger()->warning( + "config-manager: Request to remove unknown main option from config file: {}", key); + } + + // Save the global option for later removing from the file. + main_opts_to_remove.insert(key); + } + } + return true; + }); + cmd.register_positional_arg(opts_vals); +} + + +void ConfigManagerUnsetOptCommand::configure() { + auto & ctx = get_context(); + + // Remove options from main configuration file. + const auto & cfg_filepath = get_config_file_path(ctx.base.get_config()); + if (!main_opts_to_remove.empty() && std::filesystem::exists(cfg_filepath)) { + ConfigParser parser; + bool changed = false; + + parser.read(cfg_filepath); + + changed |= remove_from_config(parser, "main", main_opts_to_remove); + + if (changed) { + parser.write(cfg_filepath, false); + } + } + + // Remove options from repositories overrides configuration file, remove empty sections. + if (!in_repos_opts_to_remove.empty() && std::filesystem::exists(CFG_MANAGER_REPOS_OVERRIDE_FILEPATH)) { + ConfigParser parser; + bool changed = false; + parser.read(CFG_MANAGER_REPOS_OVERRIDE_FILEPATH); + + std::vector empty_config_sections; + for (const auto & [repo_id, setopts] : parser.get_data()) { + for (const auto & [in_repoid, keys] : in_repos_opts_to_remove) { + if (sack::match_string(repo_id, sack::QueryCmp::GLOB, in_repoid)) { + changed |= remove_from_config(parser, repo_id, keys); + } + } + if (setopts.empty()) { + empty_config_sections.emplace_back(repo_id); + } + } + + // Clean config - remove empty sections. + for (const auto & section : empty_config_sections) { + parser.remove_section(section); + changed = true; + } + + if (changed) { + parser.write(CFG_MANAGER_REPOS_OVERRIDE_FILEPATH, false); + } + } +} + +} // namespace dnf5 diff --git a/dnf5-plugins/config-manager_plugin/unsetopt.hpp b/dnf5-plugins/config-manager_plugin/unsetopt.hpp new file mode 100644 index 0000000000..c6a9cfd862 --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/unsetopt.hpp @@ -0,0 +1,47 @@ +/* +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_CONFIG_MANAGER_UNSETOPT_HPP +#define DNF5_COMMANDS_CONFIG_MANAGER_UNSETOPT_HPP + +#include + +#include +#include +#include + +namespace dnf5 { + +class ConfigManagerUnsetOptCommand : public Command { +public: + explicit ConfigManagerUnsetOptCommand(Context & context) : Command(context, "unsetopt") {} + void set_argument_parser() override; + void configure() override; + +private: + libdnf5::ConfigMain tmp_config; + libdnf5::repo::ConfigRepo tmp_repo_conf{tmp_config, "temporary_to_check_repository_options"}; + std::set main_opts_to_remove; + std::map> in_repos_opts_to_remove; +}; + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_CONFIG_MANAGER_UNSETOPT_HPP diff --git a/dnf5-plugins/config-manager_plugin/unsetvar.cpp b/dnf5-plugins/config-manager_plugin/unsetvar.cpp new file mode 100644 index 0000000000..53aaec0996 --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/unsetvar.cpp @@ -0,0 +1,82 @@ +/* +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 "unsetvar.hpp" + +#include "shared.hpp" + +#include + +#include + +namespace dnf5 { + +using namespace libdnf5; + +void ConfigManagerUnsetVarCommand::set_argument_parser() { + auto & ctx = get_context(); + auto & parser = ctx.get_argument_parser(); + + auto & cmd = *get_argument_parser_command(); + cmd.set_description("Unset/remove variables"); + + auto vars = + parser.add_new_positional_arg("variables", cli::ArgumentParser::PositionalArg::AT_LEAST_ONE, nullptr, nullptr); + vars->set_description("List of variables to unset"); + vars->set_parse_hook_func( + [this]([[maybe_unused]] cli::ArgumentParser::PositionalArg * arg, int argc, const char * const argv[]) { + for (int i = 0; i < argc; ++i) { + std::string var_name{argv[i]}; + + check_variable_name(var_name); + + // Save the variable for later removing. + vars_to_remove.insert(var_name); + } + return true; + }); + cmd.register_positional_arg(vars); +} + + +void ConfigManagerUnsetVarCommand::configure() { + auto & ctx = get_context(); + if (!vars_to_remove.empty()) { + const auto & vars_dir = get_last_vars_dir_path(ctx.base.get_config()); + if (vars_dir.empty()) { + throw ConfigManagerError(M_("Missing path to vars directory")); + } + + if (!std::filesystem::exists(vars_dir)) { + return; + } + + for (const auto & name : vars_to_remove) { + const auto filepath = vars_dir / name; + try { + std::filesystem::remove(filepath); + } catch (const std::filesystem::filesystem_error & e) { + throw ConfigManagerError( + M_("Cannot remove variable file \"{}\": {}"), filepath.native(), std::string{e.what()}); + } + } + } +} + +} // namespace dnf5 diff --git a/dnf5-plugins/config-manager_plugin/unsetvar.hpp b/dnf5-plugins/config-manager_plugin/unsetvar.hpp new file mode 100644 index 0000000000..f7b5f31e9e --- /dev/null +++ b/dnf5-plugins/config-manager_plugin/unsetvar.hpp @@ -0,0 +1,43 @@ +/* +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_CONFIG_MANAGER_UNSETVAR_HPP +#define DNF5_COMMANDS_CONFIG_MANAGER_UNSETVAR_HPP + +#include + +#include +#include + +namespace dnf5 { + +class ConfigManagerUnsetVarCommand : public Command { +public: + explicit ConfigManagerUnsetVarCommand(Context & context) : Command(context, "unsetvar") {} + void set_argument_parser() override; + void configure() override; + +private: + std::set vars_to_remove; +}; + +} // namespace dnf5 + +#endif // DNF5_COMMANDS_CONFIG_MANAGER_UNSETVAR_HPP diff --git a/dnf5.spec b/dnf5.spec index e14893dff0..6e29c731dd 100644 --- a/dnf5.spec +++ b/dnf5.spec @@ -170,6 +170,10 @@ BuildRequires: libubsan BuildRequires: pkgconfig(smartcols) %endif +%if %{with dnf5_plugins} +BuildRequires: libcurl-devel >= 7.62.0 +%endif + %if %{with dnf5daemon_server} # required for dnf5daemon-server BuildRequires: pkgconfig(sdbus-c++) >= 0.8.1 @@ -638,13 +642,16 @@ Package management service with a DBus interface. Summary: Plugins for dnf5 License: LGPL-2.1-or-later Requires: dnf5%{?_isa} = %{version}-%{release} +Requires: libcurl%{?_isa} >= 7.62.0 Provides: dnf5-command(builddep) Provides: dnf5-command(changelog) +Provides: dnf5-command(config-manager) Provides: dnf5-command(copr) Provides: dnf5-command(repoclosure) %description -n dnf5-plugins -Core DNF5 plugins that enhance dnf5 with builddep, changelog, copr, and repoclosure commands. +Core DNF5 plugins that enhance dnf5 with builddep, changelog, +config-manager, copr, and repoclosure commands. %files -n dnf5-plugins %{_libdir}/dnf5/plugins/*.so