From debb8d768510913c0a41fb6d2ea932878f6df173 Mon Sep 17 00:00:00 2001 From: Jaroslav Rohel Date: Wed, 4 Oct 2023 09:39:00 +0200 Subject: [PATCH] [libdnf, actions plugin] Support get/set repositories options, ver 1.1.0 Argument `${conf..[=]}` is substituted by a list of "repoid.option=value" pairs for the matching repositories. Pairs are separated by ',' character. The ',' character in the value is replaced by escape sequence "\x2C". The input `repoid_pattern` - is the repository id and can contain globs. If `value_pattern` is used, only pairs with the matching value are listed. The `value_pattern` can contain globs. Output line format for setting repository option: `conf..=` - sets the value of option `option_name` for all matching repositories. The `repoid_pattern` - is repository id and can contain globs. --- libdnf5-plugins/actions/actions.cpp | 210 +++++++++++++++++++++++++--- 1 file changed, 190 insertions(+), 20 deletions(-) diff --git a/libdnf5-plugins/actions/actions.cpp b/libdnf5-plugins/actions/actions.cpp index 81adb9124..8ead20102 100644 --- a/libdnf5-plugins/actions/actions.cpp +++ b/libdnf5-plugins/actions/actions.cpp @@ -21,9 +21,12 @@ along with libdnf. If not, see . #include #include #include +#include #include +#include #include #include +#include #include #include @@ -42,7 +45,7 @@ using namespace libdnf5; namespace { constexpr const char * PLUGIN_NAME = "actions"; -constexpr plugin::Version PLUGIN_VERSION{1, 0, 0}; +constexpr plugin::Version PLUGIN_VERSION{1, 1, 0}; constexpr const char * attrs[]{"author.name", "author.email", "description", nullptr}; constexpr const char * attrs_value[]{"Jaroslav Rohel", "jrohel@redhat.com", "Actions Plugin."}; @@ -128,6 +131,9 @@ class Actions final : public plugin::IPlugin { [[nodiscard]] std::pair, bool> substitute_args( const libdnf5::base::TransactionPackage * trans_pkg, const libdnf5::rpm::Package * pkg, const Action & action); + std::vector> get_conf(const std::string & key); + std::vector> set_conf(const std::string & key, const std::string & value); + void process_command_output_line(std::string_view line); // Parsed actions for individual hooks @@ -160,6 +166,12 @@ class ActionsPluginError : public libdnf5::Error { }; +// The ConfigError exception is handled internally. It will not leave the actions plugin. +class ConfigError : public std::runtime_error { + using runtime_error::runtime_error; +}; + + bool CommandToRun::operator<(const CommandToRun & other) const noexcept { if (command == other.command) { if (args.size() == other.args.size()) { @@ -197,6 +209,31 @@ std::vector split(const std::string & str) { return ret; } + +// Replaces ',' with the escape sequence "\\x2C". (one '\' is removed later) +std::string escape_list_value(const std::string & value) { + std::size_t escaped_chars = 0; + for (const char ch : value) { + if (ch == ',') { + escaped_chars += 4; + } + } + if (escaped_chars == 0) { + return value; + } + std::string ret; + ret.reserve(value.length() + escaped_chars); + for (const char ch : value) { + if (ch == ',') { + ret += "\\\\x2C"; + } else { + ret += ch; + } + } + return ret; +} + + std::pair Actions::substitute( const libdnf5::base::TransactionPackage * trans_pkg, const libdnf5::rpm::Package * pkg, @@ -236,10 +273,48 @@ std::pair Actions::substitute( var_value = fmt::format("{}.{}.{}", PLUGIN_VERSION.major, PLUGIN_VERSION.minor, PLUGIN_VERSION.micro); } } else if (var_name.starts_with("conf.")) { - auto config_opts = base.get_config().opt_binds(); - auto it = config_opts.find(std::string(var_name.substr(5))); - if (it != config_opts.end()) { - var_value = it->second.get_value_string(); + auto key = std::string(var_name.substr(5)); + const auto dot_pos = key.rfind('.'); + if (dot_pos != std::string::npos) { + // It is a repository option. The repoid is part of the key and can contain globs. + // Will be substituted by a list of "repoid.option=value" pairs for the matching repositories. + // Pairs are separated by ',' character. The ',' character in the value is replaced by escape sequence. + // Supported formats: `.` or `.=` + std::string value_pattern; + const auto equal_pos = key.find('='); + if (equal_pos != std::string::npos) { + value_pattern = key.substr(equal_pos + 1); + key = key.substr(0, equal_pos); + } + try { + const bool is_glob_pattern = utils::is_glob_pattern(value_pattern.c_str()); + const auto list_key_vals = get_conf(key); + for (const auto & [key, val] : list_key_vals) { + if (!value_pattern.empty()) { + if (is_glob_pattern) { + if (!sack::match_string(val, sack::QueryCmp::GLOB, value_pattern)) { + continue; + } + } else { + if (val != value_pattern) { + continue; + } + } + } + if (var_value) { + *var_value += "," + key + '=' + escape_list_value(val); + } else { + var_value = key + '=' + escape_list_value(val); + } + } + } catch (const ConfigError & ex) { + } + } else { + auto config_opts = base.get_config().opt_binds(); + auto it = config_opts.find(key); + if (it != config_opts.end()) { + var_value = it->second.get_value_string(); + } } } else if (var_name.starts_with("var.")) { auto vars = base.get_vars(); @@ -576,6 +651,111 @@ void Actions::parse_action_files() { } } + +// Parses the input key and returns the repoid and option name. +// If there is a global option on the input, repoid will be an empty string +std::pair get_repoid_and_optname_from_key(std::string_view key) { + std::string repo_id; + std::string opt_name; + + auto dot_pos = key.rfind('.'); + if (dot_pos != std::string::npos) { + if (dot_pos == key.size() - 1) { + throw ConfigError(fmt::format("Badly formatted argument value: Last key character cannot be '.': {}", key)); + } + repo_id = key.substr(0, dot_pos); + opt_name = key.substr(dot_pos + 1); + } else { + opt_name = key; + } + + return {repo_id, opt_name}; +} + + +// Returns a list of matching "key=value" pairs. +// The key can be a global option or repository option. The input key can contain globs in repository name. +std::vector> Actions::get_conf(const std::string & key) { + auto & base = get_base(); + std::vector> list_set_key_vals; + + auto [repo_id_pattern, opt_name] = get_repoid_and_optname_from_key(key); + if (!repo_id_pattern.empty()) { + repo::RepoQuery query(base); + query.filter_id(repo_id_pattern, sack::QueryCmp::GLOB); + for (auto repo : query) { + auto config_opts = repo->get_config().opt_binds(); + auto it = config_opts.find(opt_name); + if (it == config_opts.end()) { + throw ConfigError(fmt::format("Unknown repo config option: {}", key)); + } + std::string value; + try { + value = it->second.get_value_string(); + } catch (libdnf5::OptionError & ex) { + throw ConfigError(fmt::format("Cannot get repo config option \"{}\": {}", key, ex.what())); + } + list_set_key_vals.emplace_back(repo->get_id() + '.' + it->first, value); + } + } else { + auto config_opts = base.get_config().opt_binds(); + auto it = config_opts.find(key); + if (it == config_opts.end()) { + throw ConfigError(fmt::format("Unknown config option \"{}\"", key)); + } + std::string value; + try { + value = it->second.get_value_string(); + } catch (libdnf5::OptionError & ex) { + throw ConfigError(fmt::format("Cannot get config option \"{}\": {}", key, ex.what())); + } + list_set_key_vals.emplace_back(key, value); + } + return list_set_key_vals; +} + + +// Sets the matching keys to the given value. +// Returns a list of matching "key=value" pairs. The key can be a global option or repository option. +// The input key can contain globs in repository name. New value is returned. +std::vector> Actions::set_conf(const std::string & key, const std::string & value) { + auto & base = get_base(); + std::vector> list_set_key_vals; + + auto [repo_id_pattern, opt_name] = get_repoid_and_optname_from_key(key); + if (!repo_id_pattern.empty()) { + repo::RepoQuery query(base); + query.filter_id(repo_id_pattern, sack::QueryCmp::GLOB); + for (auto repo : query) { + auto config_opts = repo->get_config().opt_binds(); + auto it = config_opts.find(opt_name); + if (it == config_opts.end()) { + throw ConfigError(fmt::format("Unknown repo config option: {}", key)); + } + try { + it->second.new_string(libdnf5::Option::Priority::PLUGINCONFIG, value); + } catch (libdnf5::OptionError & ex) { + throw ConfigError(fmt::format("Cannot set repo config option \"{}={}\": {}", key, value, ex.what())); + } + list_set_key_vals.emplace_back(repo->get_id() + '.' + it->first, value); + } + } else { + auto config_opts = base.get_config().opt_binds(); + auto it = config_opts.find(key); + if (it == config_opts.end()) { + throw ConfigError(fmt::format("Unknown config option \"{}\"", key)); + } + try { + it->second.new_string(libdnf5::Option::Priority::PLUGINCONFIG, value); + } catch (libdnf5::OptionError & ex) { + throw ConfigError(fmt::format("Cannot set config option \"{}={}\": {}", key, value, ex.what())); + } + list_set_key_vals.emplace_back(key, value); + } + return list_set_key_vals; +} + + void Actions::process_command_output_line(std::string_view line) { auto & base = get_base(); @@ -595,22 +775,12 @@ void Actions::process_command_output_line(std::string_view line) { return; } if (line.starts_with("conf.")) { - std::string var_name(line.substr(5, eq_pos - 5)); - std::string var_value(line.substr(eq_pos + 1)); - auto config_opts = base.get_config().opt_binds(); - auto it = config_opts.find(var_name); - if (it == config_opts.end()) { - base.get_logger()->error("Actions plugin: Command returns unknown config option \"{}\"", var_name); - return; - } + std::string key(line.substr(5, eq_pos - 5)); + std::string conf_value(line.substr(eq_pos + 1)); try { - it->second.new_string(libdnf5::Option::Priority::PLUGINCONFIG, var_value); - } catch (libdnf5::OptionError & ex) { - base.get_logger()->error( - "Actions plugin: Cannot set config value returned by command \"{}={}\": {}", - var_name, - var_value, - std::string(ex.what())); + set_conf(key, conf_value); + } catch (const ConfigError & ex) { + base.get_logger()->error("Actions plugin: {}", ex.what()); } } else if (line.starts_with("var.")) { std::string var_name(line.substr(4, eq_pos - 4));