From ae3d5eecd651e7555d10badf6c0898c593aef45a Mon Sep 17 00:00:00 2001 From: Jaroslav Rohel Date: Wed, 4 Oct 2023 11:06:38 +0200 Subject: [PATCH 1/3] [libdnf, actions plugin] Use enum for pipe ends instead of magic values --- libdnf5-plugins/actions/actions.cpp | 35 +++++++++++++++-------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/libdnf5-plugins/actions/actions.cpp b/libdnf5-plugins/actions/actions.cpp index c95cf45c9..81adb9124 100644 --- a/libdnf5-plugins/actions/actions.cpp +++ b/libdnf5-plugins/actions/actions.cpp @@ -625,6 +625,7 @@ void Actions::process_command_output_line(std::string_view line) { } void Actions::execute_command(CommandToRun & command) { + enum PipeEnd { READ = 0, WRITE = 1 }; auto & base = get_base(); int pipe_out_from_child[2]; @@ -635,8 +636,8 @@ void Actions::execute_command(CommandToRun & command) { } if (pipe(pipe_out_from_child) == -1) { auto errnum = errno; - close(pipe_to_child[1]); - close(pipe_to_child[0]); + close(pipe_to_child[PipeEnd::WRITE]); + close(pipe_to_child[PipeEnd::READ]); base.get_logger()->error("Actions plugin: Cannot create pipe: {}", std::strerror(errnum)); return; } @@ -644,28 +645,28 @@ void Actions::execute_command(CommandToRun & command) { auto child_pid = fork(); if (child_pid == -1) { auto errnum = errno; - close(pipe_to_child[1]); - close(pipe_to_child[0]); - close(pipe_out_from_child[1]); - close(pipe_out_from_child[0]); + close(pipe_to_child[PipeEnd::WRITE]); + close(pipe_to_child[PipeEnd::READ]); + close(pipe_out_from_child[PipeEnd::WRITE]); + close(pipe_out_from_child[PipeEnd::READ]); base.get_logger()->error("Actions plugin: Cannot fork: {}", std::strerror(errnum)); } else if (child_pid == 0) { - close(pipe_to_child[1]); // close writing end of the pipe on the child side - close(pipe_out_from_child[0]); // close reading end of the pipe on the child side + close(pipe_to_child[PipeEnd::WRITE]); // close writing end of the pipe on the child side + close(pipe_out_from_child[PipeEnd::READ]); // close reading end of the pipe on the child side // bind stdin of the child process to the reading end of the pipe - if (dup2(pipe_to_child[0], fileno(stdin)) == -1) { + if (dup2(pipe_to_child[PipeEnd::READ], fileno(stdin)) == -1) { base.get_logger()->error("Actions plugin: Cannot bind command stdin: {}", std::strerror(errno)); _exit(255); } - close(pipe_to_child[0]); + close(pipe_to_child[PipeEnd::READ]); // bind stdout of the child process to the writing end of the pipe - if (dup2(pipe_out_from_child[1], fileno(stdout)) == -1) { + if (dup2(pipe_out_from_child[PipeEnd::WRITE], fileno(stdout)) == -1) { base.get_logger()->error("Actions plugin: Cannot bind command stdout: {}", std::strerror(errno)); _exit(255); } - close(pipe_out_from_child[1]); + close(pipe_out_from_child[PipeEnd::WRITE]); std::vector args; args.reserve(command.args.size() + 1); @@ -685,15 +686,15 @@ void Actions::execute_command(CommandToRun & command) { "Actions plugin: Cannot execute \"{}{}\": {}", command.command, args_string, std::strerror(errnum)); _exit(255); } else { - close(pipe_to_child[0]); - close(pipe_to_child[1]); + close(pipe_to_child[PipeEnd::READ]); + close(pipe_to_child[PipeEnd::WRITE]); - close(pipe_out_from_child[1]); + close(pipe_out_from_child[PipeEnd::WRITE]); char read_buf[256]; std::string input; std::size_t num_tested_chars = 0; do { - auto len = read(pipe_out_from_child[0], read_buf, sizeof(read_buf)); + auto len = read(pipe_out_from_child[PipeEnd::READ], read_buf, sizeof(read_buf)); if (len > 0) { std::size_t line_begin_pos = 0; input.append(read_buf, static_cast(len)); @@ -719,7 +720,7 @@ void Actions::execute_command(CommandToRun & command) { break; } } while (true); - close(pipe_out_from_child[0]); + close(pipe_out_from_child[PipeEnd::READ]); waitpid(child_pid, nullptr, 0); } From debb8d768510913c0a41fb6d2ea932878f6df173 Mon Sep 17 00:00:00 2001 From: Jaroslav Rohel Date: Wed, 4 Oct 2023 09:39:00 +0200 Subject: [PATCH 2/3] [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)); From 98a735821e7edce32258634fef5e787cf9d2cee6 Mon Sep 17 00:00:00 2001 From: Jaroslav Rohel Date: Mon, 10 Jun 2024 10:13:22 +0200 Subject: [PATCH 3/3] [libdnf, actions plugin] Documentation: get/set repositories options --- doc/libdnf5_plugins/actions.8.rst | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/doc/libdnf5_plugins/actions.8.rst b/doc/libdnf5_plugins/actions.8.rst index b6b8e5dff..dd78689f4 100644 --- a/doc/libdnf5_plugins/actions.8.rst +++ b/doc/libdnf5_plugins/actions.8.rst @@ -94,6 +94,7 @@ Each non-comment line defines an action and consists of five items separated by * ``${pid}`` - process ID * ``${plugin.version}`` - version of the actions plugin (added in version 0.3.0) * ``${conf.}`` - option from base configuration + * ``${conf..[=]}`` - list of "repoid.option=value" pairs (added in version 1.1.0) * ``${var.}`` - variable * ``${tmp.}`` - variable exists only in actions plugin context * ``${pkg.}`` - value of the package attribute @@ -132,19 +133,30 @@ Each non-comment line defines an action and consists of five items separated by of when a particular ``package_filter`` is invoked depends on the position of the corresponding package in the transaction. + The ``repoid.option=value`` pairs in the list are separated by the ',' character. + The ',' character in the value is replaced by the escape sequence ``"\x2C"``. + If ``value_pattern`` is used, only pairs with the matching value are listed. + The ``repoid_pattern`` and ``value_pattern`` can contain globs. + Action standard output format ============================= The standard output of each executed action (command) is captured and processed. -Each line of output can set the value or unset one actions plugin variable. It can also -change the value of an option from the base configuration or a variable. +Each line of output can change the value of a base configuration option, the value +of a configuration option in matching repositories, or a variable. +It can also set or unset one actions plugin variable. The value of this variable is available +for the following command using the ``${tmp.}`` substitution. + +Actions should change the repositories configuration in the ``repos_configured`` hook. +At this point, the repositories configuration is loaded but not yet applied. Output line format ------------------ * tmp.= - sets the value of action plugins variable * tmp. - removes the action plugins variable if it exists * conf.= - sets the value of option in the base configuration +* conf..= - sets the value of option in the matching repositories (added in version 1.1.0) * var.= - sets value of the vatiable @@ -164,6 +176,15 @@ An example actions file: # Prints a message that the "post_base_setup" callback was called. post_base_setup::::/usr/bin/sh -c echo\ libdnf5\ post_base_setup\ was\ called.\ >>/tmp/actions-trans.log + # Prints a list of configured repositories with their enable state. + repos_configured::::/usr/bin/sh -c echo\ Repositories:\ ${conf.*.enabled}\ >>/tmp/repos.log + + # Prints a list of repositories that use the http protocol in baseurl. + repos_configured::::/usr/bin/sh -c echo\ "${conf.*.baseurl=*http://*}"\ >>/tmp/baseurl_http.log + + # Disables all repositories whose id starts with "rpmfusion". + repos_configured::::/usr/bin/sh -c echo\ conf.rpmfusion*.enabled=0 + # Prints the information about the start of the transaction. # Since package_filter is empty, it executes the commands once. pre_transaction::::/usr/bin/sh -c echo\ Transaction\ start.\ Packages\ in\ transaction:\ >>/tmp/actions-trans.log