From b43548283adc0f2a5afaeff4ae3ff4a1b93172a6 Mon Sep 17 00:00:00 2001 From: Alberto Miranda Date: Thu, 25 May 2023 19:58:21 +0200 Subject: [PATCH 01/10] scord-ctl: Add support for configuration file --- etc/CMakeLists.txt | 2 +- etc/scord-ctl.conf | 45 ++++ src/common/net/server.cpp | 4 +- src/common/net/server.hpp | 13 +- src/lib/scord/types.hpp | 37 ++- src/scord-ctl/CMakeLists.txt | 5 +- src/scord-ctl/config_file.cpp | 432 ++++++++++++++++++++++++++++++++++ src/scord-ctl/config_file.hpp | 224 ++++++++++++++++++ src/scord-ctl/defaults.hpp.in | 38 +++ src/scord-ctl/rpc_server.cpp | 41 ++++ src/scord-ctl/rpc_server.hpp | 9 + src/scord-ctl/scord_ctl.cpp | 22 +- 12 files changed, 853 insertions(+), 19 deletions(-) create mode 100644 etc/scord-ctl.conf create mode 100644 src/scord-ctl/config_file.cpp create mode 100644 src/scord-ctl/config_file.hpp create mode 100644 src/scord-ctl/defaults.hpp.in diff --git a/etc/CMakeLists.txt b/etc/CMakeLists.txt index 7f803286..9673887e 100644 --- a/etc/CMakeLists.txt +++ b/etc/CMakeLists.txt @@ -25,7 +25,7 @@ configure_file(scord.conf.in scord.conf) # install the configuration file to sysconfdir (normally /etc) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/scord.conf +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/scord.conf scord-ctl.conf DESTINATION ${CMAKE_INSTALL_SYSCONFDIR} ) diff --git a/etc/scord-ctl.conf b/etc/scord-ctl.conf new file mode 100644 index 00000000..bd639f5f --- /dev/null +++ b/etc/scord-ctl.conf @@ -0,0 +1,45 @@ +## vim: set filetype=yaml: + +# Configuration of the `scord-ctl` service +config: + # Specific configurations for supported adhoc storage systems + adhoc_storage: + gekkofs: + # The default working directory for adhoc instances of this type + working_directory: /tmp/gekkofs + startup: + # Specific environment variables that should be set for the adhoc + # instance. These will be merged with the environment variables + # already set by Slurm. + environment: + VAR0: value0 + VAR1: value1 + # The command that scord-ctl will use to start an adhoc instance of + # this type. The following variables are supported that will be + # automatically replaced by scord-ctl if found between curly braces: + # * ADHOC_NODES: A comma separated list of valid job hostnames that + # can be used to start the adhoc instance. + # * ADHOC_DIRECTORY: A unique working directory for each specific + # adhoc instance. This directory will be created by scord-ctl under + # `working_directory` and automatically removed after the adhoc + # instance has been shut down. + # * ADHOC_ID: - A unique ID for the adhoc instance. + command: script.sh start + --hosts {ADHOC_NODES} + --workdir {ADHOC_DIRECTORY} + --datadir {ADHOC_DIRECTORY}/data + --mountdir {ADHOC_DIRECTORY}/mnt + shutdown: + environment: + command: script.sh stop --workdir {ADHOC_DIRECTORY} + + +# default storage tiers made available to applications +storage: + lustre: + type: "pfs" + mountpoint: "/mnt/lustre" + + tmp: + type: "tmpfs" + mountpoint: "/tmp" diff --git a/src/common/net/server.cpp b/src/common/net/server.cpp index 494bc219..c7261744 100644 --- a/src/common/net/server.cpp +++ b/src/common/net/server.cpp @@ -263,7 +263,7 @@ server::install_signal_handlers() { } void -server::check_configuration() {} +server::check_configuration() const {} void server::print_greeting() { @@ -276,7 +276,7 @@ server::print_greeting() { } void -server::print_configuration() { +server::print_configuration() const { LOGGER_INFO(""); LOGGER_INFO("[[ Configuration ]]"); LOGGER_INFO(" - running as daemon?: {}", m_daemonize ? "yes" : "no"); diff --git a/src/common/net/server.hpp b/src/common/net/server.hpp index bafa8b82..6b7787d9 100644 --- a/src/common/net/server.hpp +++ b/src/common/net/server.hpp @@ -83,21 +83,22 @@ private: daemonize(); void signal_handler(int); - void init_logger(); void install_signal_handlers(); - - void - check_configuration(); void print_greeting(); void - print_configuration(); - void print_farewell(); +protected: + virtual void + check_configuration() const; + + virtual void + print_configuration() const; + private: std::string m_name; std::string m_address; diff --git a/src/lib/scord/types.hpp b/src/lib/scord/types.hpp index a1002f49..34d40c69 100644 --- a/src/lib/scord/types.hpp +++ b/src/lib/scord/types.hpp @@ -681,9 +681,26 @@ struct fmt::formatter : formatter { }; template <> -struct fmt::formatter - : formatter { - // parse is inherited from formatter. +struct fmt::formatter { + + // Presentation format: 'f' - full, 'e' - enum + char m_presentation = 'f'; + + constexpr auto + parse(format_parse_context& ctx) -> decltype(ctx.begin()) { + + auto it = ctx.begin(), end = ctx.end(); + if(it != end && (*it == 'f' || *it == 'e')) { + m_presentation = *it++; + } + + if(it != end && *it != '}') { + ctx.on_error("invalid format"); + } + + return it; + } + template auto format(const enum scord::adhoc_storage::type& t, FormatContext& ctx) const { @@ -693,20 +710,24 @@ struct fmt::formatter switch(t) { case adhoc_storage::type::gekkofs: - name = "ADM_ADHOC_STORAGE_GEKKOFS"; + name = m_presentation == 'f' ? "ADM_ADHOC_STORAGE_GEKKOFS" + : "gekkofs"; break; case adhoc_storage::type::dataclay: - name = "ADM_ADHOC_STORAGE_DATACLAY"; + name = m_presentation == 'f' ? "ADM_ADHOC_STORAGE_DATACLAY" + : "dataclay"; break; case adhoc_storage::type::expand: - name = "ADM_ADHOC_STORAGE_EXPAND"; + name = m_presentation == 'f' ? "ADM_ADHOC_STORAGE_EXPAND" + : "expand"; break; case adhoc_storage::type::hercules: - name = "ADM_ADHOC_STORAGE_HERCULES"; + name = m_presentation == 'f' ? "ADM_ADHOC_STORAGE_HERCULES" + : "hercules"; break; } - return formatter::format(name, ctx); + return format_to(ctx.out(), "{}", name); } }; diff --git a/src/scord-ctl/CMakeLists.txt b/src/scord-ctl/CMakeLists.txt index 9654c6e3..ee3a3c5d 100644 --- a/src/scord-ctl/CMakeLists.txt +++ b/src/scord-ctl/CMakeLists.txt @@ -27,8 +27,11 @@ add_executable(scord-ctl) target_sources( scord-ctl PRIVATE scord_ctl.cpp rpc_server.cpp rpc_server.hpp + ${CMAKE_CURRENT_BINARY_DIR}/defaults.hpp config_file.hpp config_file.cpp ) +configure_file(defaults.hpp.in ${CMAKE_CURRENT_BINARY_DIR}/defaults.hpp @ONLY) + target_include_directories( scord-ctl PUBLIC ${CMAKE_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR} @@ -37,7 +40,7 @@ target_include_directories( target_link_libraries( scord-ctl PRIVATE common::logger common::network::rpc_server - libscord_cxx_types fmt::fmt CLI11::CLI11 + libscord_cxx_types fmt::fmt CLI11::CLI11 ryml::ryml ) install(TARGETS scord DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/scord-ctl/config_file.cpp b/src/scord-ctl/config_file.cpp new file mode 100644 index 00000000..ff60db1e --- /dev/null +++ b/src/scord-ctl/config_file.cpp @@ -0,0 +1,432 @@ +/****************************************************************************** + * Copyright 2021-2023, Barcelona Supercomputing Center (BSC), Spain + * + * This software was partially supported by the EuroHPC-funded project ADMIRE + * (Project ID: 956748, https://www.admire-eurohpc.eu). + * + * This file is part of scord. + * + * scord 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 3 of the License, or + * (at your option) any later version. + * + * scord 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 scord. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#include +#include +#include +#include +#include +#include "config_file.hpp" + +namespace { + +using namespace scord_ctl::config; + +// convenience operator for creating ryml::csubstr literals +ryml::csubstr +operator""_s(const char* str, std::size_t len) { + return {str, len}; +} + +// convenience function for converting ryml::csubstr to std::string +std::string +to_string(ryml::csubstr str) { + return str.has_str() ? std::string{str.data(), str.size()} : std::string{}; +} + +// hash function for ryml::csubstr +constexpr auto hash = [](const ryml::csubstr& key) { + std::string tmp{key.data(), key.size()}; + return std::hash{}(tmp); +}; + +// convenience function for converting ryml::csubstr to +// scord::adhoc_storage::types +enum scord::adhoc_storage::type +to_adhoc_storage_type(const ryml::csubstr& type) { + + using scord::adhoc_storage; + + const std::unordered_map + valid_types { + std::make_pair("dataclay"_s, adhoc_storage::type::dataclay), + std::make_pair("expand"_s, adhoc_storage::type::expand), + std::make_pair("gekkofs"_s, adhoc_storage::type::gekkofs), + std::make_pair("hercules"_s, adhoc_storage::type::hercules), + }; + + if(valid_types.count(type) == 0) { + throw std::runtime_error{ + fmt::format("Unsupported adhoc storage type '{}' in " + "configuration file", + type)}; + } + + return valid_types.at(type); +} + +/** + * @brief Parse a ryml node into a `scord_ctl::config::environment` object. + * + * The node is expected to be a map with the following structure: + * environment: + * : + * ... + * + * @param node The ryml node to parse. + * + * @return The parsed `scord_ctl::config::environment` object. + */ +scord_ctl::config::environment +parse_environment_node(const ryml::ConstNodeRef& node) { + + scord_ctl::config::environment env; + + for(const auto& child : node) { + if(!child.has_key()) { + continue; + } + + env.set(::to_string(child.key()), ::to_string(child.val())); + } + + return env; +} + +/** + * @brief Parse a ryml node into a `scord_ctl::config::command` object. + * + * The node is expected to be a map with the following structure: + * environment: + * : + * ... + * command: + * + * @param node The ryml node to parse. + * + * @return The parsed `scord_ctl::config::command` object. + */ +command +parse_command_node(const ryml::ConstNodeRef& node) { + + std::string cmdline; + std::optional env; + + for(const auto& child : node) { + if(!child.has_key()) { + continue; + } + + if(child.key() == "environment") { + env = ::parse_environment_node(child); + } else if(child.key() == "command") { + if(child.val_is_null()) { + throw std::runtime_error{"`command` key cannot be empty"}; + } + cmdline = ::to_string(child.val()); + } else { + fmt::print(stderr, "WARNING: Unknown key: '{}'. Ignored.\n", + child.key()); + } + } + + if(cmdline.empty()) { + throw std::runtime_error{"missing required `command` key"}; + } + + return command{cmdline, env}; +} + +/** + * @brief Parse a ryml node into a `scord_ctl::config::adhoc_storage_config` + * object. + * + * The node is expected to be a map with the following structure: + * : + * working_directory: + * startup: + * environment: + * : + * ... + * command: + * shutdown: + * environment: + * : + * ... + * command: + * + * @param node The ryml node to parse. + * @param tag A tag to dispatch the parsing to the correct overload. + * + * @return The parsed `scord_ctl::config::adhoc_storage_config` object. + */ +adhoc_storage_config +parse_adhoc_config_node(const ryml::ConstNodeRef& node) { + + std::filesystem::path working_directory; + std::optional startup_command; + std::optional shutdown_command; + + for(const auto& child : node) { + + if(!child.has_key()) { + continue; + } + + if(child.key() == "working_directory") { + if(child.val_is_null()) { + throw std::runtime_error{ + "`working_directory` key cannot be empty"}; + } + working_directory = ::to_string(child.val()); + } else if(child.key() == "startup") { + startup_command = ::parse_command_node(child); + } else if(child.key() == "shutdown") { + shutdown_command = ::parse_command_node(child); + } else { + fmt::print(stderr, "WARNING: Unknown key: '{}'. Ignored.\n", + child.key()); + } + } + + if(working_directory.empty()) { + throw std::runtime_error{"missing required `working_directory` key"}; + } + + return {working_directory, *startup_command, *shutdown_command}; +} + +/** + * @brief Parse a ryml node into a `scord_ctl::config::adhoc_storage_config_map` + * object. + * + * The node is expected to be a map with the following structure: + * adhoc_storage: + * : + * + * : + * + * ... + * + * @param node + * @return The parsed `scord_ctl::config::adhoc_storage_config_map` object. + */ +adhoc_storage_config_map +parse_adhoc_storage_node(const ryml::ConstNodeRef& node) { + + adhoc_storage_config_map adhoc_configs; + + for(const auto& child : node) { + if(!child.has_key()) { + continue; + } + + const auto adhoc_type = ::to_adhoc_storage_type(child.key()); + const auto adhoc_config = ::parse_adhoc_config_node(child); + adhoc_configs.emplace(adhoc_type, adhoc_config); + } + + return adhoc_configs; +} + +/** + * @brief Parse a ryml node into a `scord_ctl::config::adhoc_storage_config_map` + * object. + * + * The node is expected to be a map with the following structure: + * + * config: + * adhoc_storage: + * : + * + * : + * + * ... + * + * @param node The ryml node to parse. + * @return The parsed `scord_ctl::config::adhoc_storage_config_map` object. + */ +adhoc_storage_config_map +parse_config_node(const ryml::ConstNodeRef& node) { + + adhoc_storage_config_map adhoc_configs; + + for(const auto& child : node) { + if(!child.has_key()) { + continue; + } + + if(child.key() == "adhoc_storage") { + adhoc_configs = ::parse_adhoc_storage_node(child); + } else { + fmt::print(stderr, "WARNING: Unknown key: '{}'. Ignored.\n", + child.key()); + } + } + + return adhoc_configs; +} + +} // namespace + +namespace scord_ctl::config { + +void +environment::set(const std::string& key, const std::string& value) { + m_env[key] = value; +} + +std::string +environment::get(const std::string& key) const { + return m_env.count(key) == 0 ? std::string{} : m_env.at(key); +} + +std::vector +environment::as_vector() const { + // TODO + return {}; +} + +std::unordered_map::const_iterator +environment::begin() const { + return m_env.begin(); +} + +std::unordered_map::const_iterator +environment::end() const { + return m_env.end(); +} + +command::command(std::string cmdline, std::optional env) + : m_cmdline(std::move(cmdline)), m_env(std::move(env)) {} + +const std::string& +command::cmdline() const { + return m_cmdline; +} + +const std::optional& +command::env() const { + return m_env; +} + +std::string +command::eval(const std::string& adhoc_id, + const std::filesystem::path& adhoc_directory, + const std::vector& adhoc_nodes) const { + + // generate a regex from a map of key/value pairs + constexpr auto regex_from_map = + [](const std::map& m) -> std::regex { + std::string result; + for(const auto& [key, value] : m) { + const auto escaped_key = + std::regex_replace(key, std::regex{R"([{}])"}, R"(\$&)"); + result += fmt::format("{}|", escaped_key); + } + result.pop_back(); + return std::regex{result}; + }; + + const std::map replacements{ + {std::string{keywords.at(0)}, adhoc_id}, + {std::string{keywords.at(1)}, adhoc_directory.string()}, + {std::string{keywords.at(2)}, + fmt::format("\"{}\"", fmt::join(adhoc_nodes, ","))}}; + + // make sure that we fail if we ever add a new keyword and forget to add + // a replacement for it + assert(replacements.size() == keywords.size()); + + std::string result; + + const auto re = regex_from_map(replacements); + auto it = std::sregex_iterator(m_cmdline.begin(), m_cmdline.end(), re); + auto end = std::sregex_iterator{}; + + std::string::size_type last_pos = 0; + + for(; it != end; ++it) { + const auto& match = *it; + result += m_cmdline.substr(last_pos, match.position() - last_pos); + result += replacements.at(match.str()); + last_pos = match.position() + match.length(); + } + + result += m_cmdline.substr(last_pos, m_cmdline.length() - last_pos); + + return result; +} + + +adhoc_storage_config::adhoc_storage_config( + std::filesystem::path working_directory, command startup_command, + command shutdown_command) + : m_working_directory(std::move(working_directory)), + m_startup_command(std::move(startup_command)), + m_shutdown_command(std::move(shutdown_command)) {} + +const std::filesystem::path& +adhoc_storage_config::working_directory() const { + return m_working_directory; +} + +const command& +adhoc_storage_config::startup_command() const { + return m_startup_command; +} + +const command& +adhoc_storage_config::shutdown_command() const { + return m_shutdown_command; +} + +config_file::config_file(const std::filesystem::path& path) { + std::ifstream input{path}; + + if(!input) { + throw std::runtime_error{"Failed to open configuration file: " + + path.string()}; + } + + std::string input_str{std::istreambuf_iterator(input), + std::istreambuf_iterator()}; + + + const auto tree = ryml::parse_in_arena(ryml::to_csubstr(input_str)); + + for(const auto& child : tree.crootref()) { + if(!child.has_key()) { + continue; + } + + try { + if(child.key() == "config"_s) { + m_adhoc_configs = ::parse_config_node(child); + } + } catch(const std::exception& e) { + throw std::runtime_error{ + fmt::format("Failed parsing configuration in {}:\n {}", + path, e.what())}; + } + } +} + +const adhoc_storage_config_map& +config_file::adhoc_storage_configs() const { + return m_adhoc_configs; +} + +} // namespace scord_ctl::config diff --git a/src/scord-ctl/config_file.hpp b/src/scord-ctl/config_file.hpp new file mode 100644 index 00000000..edb28a7d --- /dev/null +++ b/src/scord-ctl/config_file.hpp @@ -0,0 +1,224 @@ +/****************************************************************************** + * Copyright 2021-2023, Barcelona Supercomputing Center (BSC), Spain + * + * This software was partially supported by the EuroHPC-funded project ADMIRE + * (Project ID: 956748, https://www.admire-eurohpc.eu). + * + * This file is part of scord. + * + * scord 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 3 of the License, or + * (at your option) any later version. + * + * scord 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 scord. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + + +#ifndef SCORD_CTL_CONFIG_HPP +#define SCORD_CTL_CONFIG_HPP + +#include +#include + +namespace scord_ctl::config { + +/** + * @brief A class representing the environment variables that + * should be set when running a command. + */ +class environment { + +public: + /** + * @brief Set an environment variable. + * + * @param key The name of the environment variable. + * @param value The value of the environment variable. + */ + void + set(const std::string& key, const std::string& value); + + /** + * @brief Get the value of an environment variable. + * + * @param key The name of the environment variable. + * @return The value of the environment variable if it exists, an empty + * string otherwise. + */ + std::string + get(const std::string& key) const; + + /** + * @brief Get the environment variables as a vector of strings. + * + * @return The environment variables as a vector of strings. + */ + std::vector + as_vector() const; + + /** + * @brief Get an iterator to the beginning of the environment variables. + * + * @return An iterator to the beginning of the environment variables. + */ + std::unordered_map::const_iterator + begin() const; + + /** + * @brief Get an iterator to the end of the environment variables. + * + * @return An iterator to the end of the environment variables. + */ + std::unordered_map::const_iterator + end() const; + +private: + std::unordered_map m_env; +}; + +/** + * @brief A class representing a command to be executed. + */ +class command { +public: + /** + * @brief Keywords that can be used in the command line and + * will be expanded with appropriate values when calling `eval()`. + */ + static constexpr std::array keywords = { + "{ADHOC_ID}", "{ADHOC_DIRECTORY}", "{ADHOC_NODES}"}; + + /** + * @brief Construct a command. + * + * @param cmdline The command line to be executed. + * @param env The environment variables to be set when executing the + * command. + */ + explicit command(std::string cmdline, + std::optional env = std::nullopt); + + /** + * @brief Get the template command line to be executed (i.e. without having + * keywords expanded). + * + * @return The command line to be executed. + */ + const std::string& + cmdline() const; + + /** + * @brief Get the environment variables to be set when executing the + * command. + * + * @return The environment variables to be set when executing the command. + */ + const std::optional& + env() const; + + /** + * @brief Substitute the keywords in the command line template with the + * appropriate values and produce the command line to be executed. + * + * @param adhoc_id The ID of the adhoc storage system. + * @param adhoc_directory The directory where the adhoc storage will run. + * @param adhoc_nodes The nodes where the adhoc storage will run. + * @return The evaluated command line. + */ + std::string + eval(const std::string& adhoc_id, + const std::filesystem::path& adhoc_directory, + const std::vector& adhoc_nodes) const; + +private: + std::string m_cmdline; + std::optional m_env; +}; + +/** + * @brief A class representing the configuration of an adhoc storage system. + */ +class adhoc_storage_config { + +public: + /** + * @brief Construct an adhoc_storage_config. + * + * @param working_directory The directory where the adhoc storage will run. + * @param startup_command The command to be executed to start the adhoc + * storage. + * @param shutdown_command The command to be executed to stop the adhoc + * storage. + */ + adhoc_storage_config(std::filesystem::path working_directory, + command startup_command, command shutdown_command); + + /** + * @brief Get the directory where the adhoc storage will run. + * + * @return The directory where the adhoc storage will run. + */ + const std::filesystem::path& + working_directory() const; + + /** + * @brief Get the command to be executed to start the adhoc storage. + * + * @return The command to be executed to start the adhoc storage. + */ + const command& + startup_command() const; + + /** + * @brief Get the command to be executed to stop the adhoc storage. + * + * @return The command to be executed to stop the adhoc storage. + */ + const command& + shutdown_command() const; + +private: + std::filesystem::path m_working_directory; + command m_startup_command; + command m_shutdown_command; +}; + +using adhoc_storage_type = enum scord::adhoc_storage::type; +using adhoc_storage_config_map = + std::unordered_map; + +/** + * @brief A class representing the configuration file of scord-ctl. + */ +class config_file { +public: + /** + * @brief Construct a config_file. + * + * @param path The path to the configuration file. + */ + explicit config_file(const std::filesystem::path& path); + + /** + * @brief Get the adhoc storage configurations. + * @return The adhoc storage configurations. + */ + const adhoc_storage_config_map& + adhoc_storage_configs() const; + +private: + adhoc_storage_config_map m_adhoc_configs; +}; + +} // namespace scord_ctl::config + +#endif // SCORD_CONFIG_HPP diff --git a/src/scord-ctl/defaults.hpp.in b/src/scord-ctl/defaults.hpp.in new file mode 100644 index 00000000..52189e83 --- /dev/null +++ b/src/scord-ctl/defaults.hpp.in @@ -0,0 +1,38 @@ +/****************************************************************************** + * Copyright 2021-2023, Barcelona Supercomputing Center (BSC), Spain + * + * This software was partially supported by the EuroHPC-funded project ADMIRE + * (Project ID: 956748, https://www.admire-eurohpc.eu). + * + * This file is part of scord. + * + * scord 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 3 of the License, or + * (at your option) any later version. + * + * scord 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 scord. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + + +#ifndef SCORD_CTL_DEFAULTS_HPP +#define SCORD_CTL_DEFAULTS_HPP + +#include + +namespace scord_ctl::config::defaults { + +static const std::filesystem::path config_file{ + "@CMAKE_INSTALL_FULL_SYSCONFDIR@/scord-ctl.conf"}; + +} // namespace scord_ctl::config::defaults + +#endif // SCORD_DEFAULTS_HPP diff --git a/src/scord-ctl/rpc_server.cpp b/src/scord-ctl/rpc_server.cpp index bb7da914..f9b3b988 100644 --- a/src/scord-ctl/rpc_server.cpp +++ b/src/scord-ctl/rpc_server.cpp @@ -49,6 +49,47 @@ rpc_server::rpc_server(std::string name, std::string address, bool daemonize, #undef EXPAND } +void +rpc_server::set_config(std::optional config) { + m_config = std::move(config); +} + +void +rpc_server::print_configuration() const { + + server::print_configuration(); + + if(!m_config || m_config->adhoc_storage_configs().empty()) { + return; + } + + const auto print_command = [](const auto& command) { + LOGGER_INFO(" - environment:"); + + if(const auto& env = command.env(); env.has_value()) { + for(const auto& [k, v] : *env) { + LOGGER_INFO(" - {} = {}", k, std::quoted(v)); + } + } + + LOGGER_INFO(" - command:"); + LOGGER_INFO(" {}", std::quoted(command.cmdline())); + }; + + LOGGER_INFO(" - adhoc storage configurations:"); + + for(const auto& [type, adhoc_cfg] : m_config->adhoc_storage_configs()) { + LOGGER_INFO(" * {:e}:", type); + LOGGER_INFO(" - workdir: {}", adhoc_cfg.working_directory()); + LOGGER_INFO(" - startup:"); + print_command(adhoc_cfg.startup_command()); + LOGGER_INFO(" - shutdown:"); + print_command(adhoc_cfg.shutdown_command()); + } + LOGGER_INFO(""); +} + + #define RPC_NAME() ("ADM_"s + __FUNCTION__) void diff --git a/src/scord-ctl/rpc_server.hpp b/src/scord-ctl/rpc_server.hpp index 2988a1c4..e466001e 100644 --- a/src/scord-ctl/rpc_server.hpp +++ b/src/scord-ctl/rpc_server.hpp @@ -28,6 +28,7 @@ #include #include +#include "config_file.hpp" namespace scord_ctl { @@ -38,6 +39,12 @@ public: rpc_server(std::string name, std::string address, bool daemonize, std::filesystem::path rundir); + void + set_config(std::optional config); + + void + print_configuration() const final; + private: void ping(const network::request& req); @@ -48,6 +55,8 @@ private: enum scord::adhoc_storage::type adhoc_type, const scord::adhoc_storage::ctx& adhoc_ctx, const scord::adhoc_storage::resources& adhoc_resources); + + std::optional m_config; }; } // namespace scord_ctl diff --git a/src/scord-ctl/scord_ctl.cpp b/src/scord-ctl/scord_ctl.cpp index b9c0286b..aa485074 100644 --- a/src/scord-ctl/scord_ctl.cpp +++ b/src/scord-ctl/scord_ctl.cpp @@ -29,14 +29,17 @@ #include #include #include +#include +#include #include #include "rpc_server.hpp" +#include "config_file.hpp" +#include "defaults.hpp" namespace fs = std::filesystem; using namespace std::literals; - int main(int argc, char* argv[]) { @@ -64,6 +67,13 @@ main(int argc, char* argv[]) { ->option_text("ADDRESS") ->required(); + app.set_config("-c,--config-file", scord_ctl::config::defaults::config_file, + "Ignore the system-wide configuration file and use the " + "configuration provided by FILENAME", + /*config_required=*/true) + ->option_text("FILENAME") + ->check(CLI::ExistingFile); + app.add_flag_function( "-v,--version", [&](auto /*count*/) { @@ -75,13 +85,23 @@ main(int argc, char* argv[]) { CLI11_PARSE(app, argc, argv); try { + // load configuration file for general information about + // the daemon, such as the supported storage tiers + const auto config = scord_ctl::config::config_file( + app.get_config_ptr()->as()); + scord_ctl::rpc_server srv(progname, cli_args.address, false, fs::current_path()); if(cli_args.output_file) { srv.configure_logger(logger::logger_type::file, *cli_args.output_file); } + + srv.set_config(config); return srv.run(); + } catch(const std::runtime_error& ex) { + fmt::print(stderr, "ERROR: {}\n", ex.what()); + return EXIT_FAILURE; } catch(const std::exception& ex) { fmt::print(stderr, "An unhandled exception reached the top of main(), " -- GitLab From 784f675fa6e0f33bfb3b9529ef023aa3143f4730 Mon Sep 17 00:00:00 2001 From: Alberto Miranda Date: Mon, 29 May 2023 10:25:45 +0200 Subject: [PATCH 02/10] Fix build error with GCC 10 --- src/scord-ctl/config_file.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/scord-ctl/config_file.hpp b/src/scord-ctl/config_file.hpp index edb28a7d..972fc4e3 100644 --- a/src/scord-ctl/config_file.hpp +++ b/src/scord-ctl/config_file.hpp @@ -192,7 +192,12 @@ private: command m_shutdown_command; }; +#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 11 +typedef enum scord::adhoc_storage::type adhoc_storage_type; +#else using adhoc_storage_type = enum scord::adhoc_storage::type; +#endif + using adhoc_storage_config_map = std::unordered_map; -- GitLab From 29a31ecd33d26ff6d6792549210b1417b51c90d4 Mon Sep 17 00:00:00 2001 From: Alberto Miranda Date: Wed, 31 May 2023 11:02:03 +0200 Subject: [PATCH 03/10] Add convenience functions to `environment` class - Implement `environment::as_vector()`. - Add `environment::size()`. --- src/scord-ctl/config_file.cpp | 15 +++++++++++++-- src/scord-ctl/config_file.hpp | 9 +++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/scord-ctl/config_file.cpp b/src/scord-ctl/config_file.cpp index ff60db1e..8f26c14f 100644 --- a/src/scord-ctl/config_file.cpp +++ b/src/scord-ctl/config_file.cpp @@ -295,8 +295,19 @@ environment::get(const std::string& key) const { std::vector environment::as_vector() const { - // TODO - return {}; + + std::vector tmp; + tmp.reserve(m_env.size()); + for(const auto& [key, value] : m_env) { + tmp.emplace_back(fmt::format("{}={}", key, value)); + } + + return tmp; +} + +std::size_t +environment::size() const { + return m_env.size(); } std::unordered_map::const_iterator diff --git a/src/scord-ctl/config_file.hpp b/src/scord-ctl/config_file.hpp index 972fc4e3..5aab14f6 100644 --- a/src/scord-ctl/config_file.hpp +++ b/src/scord-ctl/config_file.hpp @@ -59,12 +59,21 @@ public: /** * @brief Get the environment variables as a vector of strings. + * Each string is of the form `key=value`. * * @return The environment variables as a vector of strings. */ std::vector as_vector() const; + /** + * @brief Get the number of environment variables. + * + * @return The number of environment variables. + */ + std::size_t + size() const; + /** * @brief Get an iterator to the beginning of the environment variables. * -- GitLab From 92549fb9930e953d2bca37a1744731601caf86a1 Mon Sep 17 00:00:00 2001 From: Alberto Miranda Date: Wed, 31 May 2023 12:16:27 +0200 Subject: [PATCH 04/10] Add convenience functions for `command` class - Refactor `eval()`: The function now returns a new `command` instance. - Add `as_vector()`: This function splits the command line by ' ' and returns it as a vector of strings. --- src/scord-ctl/config_file.cpp | 20 ++++++++++++++++++-- src/scord-ctl/config_file.hpp | 20 ++++++++++++++++---- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/scord-ctl/config_file.cpp b/src/scord-ctl/config_file.cpp index 8f26c14f..20f77da6 100644 --- a/src/scord-ctl/config_file.cpp +++ b/src/scord-ctl/config_file.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include "config_file.hpp" namespace { @@ -333,7 +334,7 @@ command::env() const { return m_env; } -std::string +command command::eval(const std::string& adhoc_id, const std::filesystem::path& adhoc_directory, const std::vector& adhoc_nodes) const { @@ -378,7 +379,22 @@ command::eval(const std::string& adhoc_id, result += m_cmdline.substr(last_pos, m_cmdline.length() - last_pos); - return result; + return command{result, m_env}; +} + +std::vector +command::as_vector() const { + std::vector tmp; + + for(auto&& r : std::views::split(m_cmdline, ' ') | + std::views::transform([](auto&& v) -> std::string { + auto c = v | std::views::common; + return std::string{c.begin(), c.end()}; + })) { + tmp.emplace_back(std::move(r)); + } + + return tmp; } diff --git a/src/scord-ctl/config_file.hpp b/src/scord-ctl/config_file.hpp index 5aab14f6..1dd34331 100644 --- a/src/scord-ctl/config_file.hpp +++ b/src/scord-ctl/config_file.hpp @@ -135,19 +135,31 @@ public: env() const; /** - * @brief Substitute the keywords in the command line template with the - * appropriate values and produce the command line to be executed. + * @brief Return a copy of the current `command` where all the keywords in + * its command line template have been replaced with string + * representations of the arguments provided. * * @param adhoc_id The ID of the adhoc storage system. * @param adhoc_directory The directory where the adhoc storage will run. * @param adhoc_nodes The nodes where the adhoc storage will run. - * @return The evaluated command line. + * @return The evaluated command. */ - std::string + command eval(const std::string& adhoc_id, const std::filesystem::path& adhoc_directory, const std::vector& adhoc_nodes) const; + /** + * @brief Get the command line to be executed as a vector of strings. The + * command line is split on spaces with each string in the resulting + * vector being a token in the command line. + * + * @return The command line to be executed as a vector of strings. + */ + std::vector + as_vector() const; + + private: std::string m_cmdline; std::optional m_env; -- GitLab From 7018495d8808e45ba18fb642b0d517c3957c0a10 Mon Sep 17 00:00:00 2001 From: Alberto Miranda Date: Wed, 7 Jun 2023 10:50:27 +0200 Subject: [PATCH 05/10] Add formatter for `scord_ctl::config::command` --- src/scord-ctl/config_file.hpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/scord-ctl/config_file.hpp b/src/scord-ctl/config_file.hpp index 1dd34331..a0b16b91 100644 --- a/src/scord-ctl/config_file.hpp +++ b/src/scord-ctl/config_file.hpp @@ -247,4 +247,23 @@ private: } // namespace scord_ctl::config +/** + * @brief Formatter for `scord_ctl::config::command`. + */ +template <> +struct fmt::formatter { + template + constexpr auto + parse(ParseContext& ctx) { + return ctx.begin(); + } + + template + auto + format(const scord_ctl::config::command& cmd, FormatContext& ctx) { + return fmt::format_to(ctx.out(), "{}", cmd.cmdline()); + } +}; + + #endif // SCORD_CONFIG_HPP -- GitLab From 7c97b5a4e898cde1d8b2d27ae2fe0eeb0d545c8c Mon Sep 17 00:00:00 2001 From: Alberto Miranda Date: Wed, 7 Jun 2023 11:23:31 +0200 Subject: [PATCH 06/10] Refactor `scord_ctl::config_file::command|environment` classes --- src/scord-ctl/CMakeLists.txt | 1 + src/scord-ctl/command.cpp | 121 +++++++++++++++++++++ src/scord-ctl/command.hpp | 191 ++++++++++++++++++++++++++++++++++ src/scord-ctl/config_file.cpp | 130 ++--------------------- src/scord-ctl/config_file.hpp | 154 +-------------------------- 5 files changed, 321 insertions(+), 276 deletions(-) create mode 100644 src/scord-ctl/command.cpp create mode 100644 src/scord-ctl/command.hpp diff --git a/src/scord-ctl/CMakeLists.txt b/src/scord-ctl/CMakeLists.txt index ee3a3c5d..10107f90 100644 --- a/src/scord-ctl/CMakeLists.txt +++ b/src/scord-ctl/CMakeLists.txt @@ -28,6 +28,7 @@ add_executable(scord-ctl) target_sources( scord-ctl PRIVATE scord_ctl.cpp rpc_server.cpp rpc_server.hpp ${CMAKE_CURRENT_BINARY_DIR}/defaults.hpp config_file.hpp config_file.cpp + command.hpp command.cpp ) configure_file(defaults.hpp.in ${CMAKE_CURRENT_BINARY_DIR}/defaults.hpp @ONLY) diff --git a/src/scord-ctl/command.cpp b/src/scord-ctl/command.cpp new file mode 100644 index 00000000..55327b5b --- /dev/null +++ b/src/scord-ctl/command.cpp @@ -0,0 +1,121 @@ +#include +#include +#include +#include "command.hpp" + +namespace scord_ctl { + +void +environment::set(const std::string& key, const std::string& value) { + m_env[key] = value; +} + +std::string +environment::get(const std::string& key) const { + return m_env.count(key) == 0 ? std::string{} : m_env.at(key); +} + +std::vector +environment::as_vector() const { + + std::vector tmp; + tmp.reserve(m_env.size()); + for(const auto& [key, value] : m_env) { + tmp.emplace_back(fmt::format("{}={}", key, value)); + } + + return tmp; +} + +std::size_t +environment::size() const { + return m_env.size(); +} + +std::unordered_map::const_iterator +environment::begin() const { + return m_env.begin(); +} + +std::unordered_map::const_iterator +environment::end() const { + return m_env.end(); +} + +command::command(std::string cmdline, std::optional env) + : m_cmdline(std::move(cmdline)), m_env(std::move(env)) {} + +const std::string& +command::cmdline() const { + return m_cmdline; +} + +const std::optional& +command::env() const { + return m_env; +} + +command +command::eval(const std::string& adhoc_id, + const std::filesystem::path& adhoc_directory, + const std::vector& adhoc_nodes) const { + + // generate a regex from a map of key/value pairs + constexpr auto regex_from_map = + [](const std::map& m) -> std::regex { + std::string result; + for(const auto& [key, value] : m) { + const auto escaped_key = + std::regex_replace(key, std::regex{R"([{}])"}, R"(\$&)"); + result += fmt::format("{}|", escaped_key); + } + result.pop_back(); + return std::regex{result}; + }; + + const std::map replacements{ + {std::string{keywords.at(0)}, adhoc_id}, + {std::string{keywords.at(1)}, adhoc_directory.string()}, + {std::string{keywords.at(2)}, + fmt::format("\"{}\"", fmt::join(adhoc_nodes, ","))}}; + + // make sure that we fail if we ever add a new keyword and forget to add + // a replacement for it + assert(replacements.size() == keywords.size()); + + std::string result; + + const auto re = regex_from_map(replacements); + auto it = std::sregex_iterator(m_cmdline.begin(), m_cmdline.end(), re); + auto end = std::sregex_iterator{}; + + std::string::size_type last_pos = 0; + + for(; it != end; ++it) { + const auto& match = *it; + result += m_cmdline.substr(last_pos, match.position() - last_pos); + result += replacements.at(match.str()); + last_pos = match.position() + match.length(); + } + + result += m_cmdline.substr(last_pos, m_cmdline.length() - last_pos); + + return command{result, m_env}; +} + +std::vector +command::as_vector() const { + std::vector tmp; + + for(auto&& r : std::views::split(m_cmdline, ' ') | + std::views::transform([](auto&& v) -> std::string { + auto c = v | std::views::common; + return std::string{c.begin(), c.end()}; + })) { + tmp.emplace_back(std::move(r)); + } + + return tmp; +} + +} // namespace scord_ctl diff --git a/src/scord-ctl/command.hpp b/src/scord-ctl/command.hpp new file mode 100644 index 00000000..9ed7f6d0 --- /dev/null +++ b/src/scord-ctl/command.hpp @@ -0,0 +1,191 @@ +/****************************************************************************** + * Copyright 2021-2023, Barcelona Supercomputing Center (BSC), Spain + * + * This software was partially supported by the EuroHPC-funded project ADMIRE + * (Project ID: 956748, https://www.admire-eurohpc.eu). + * + * This file is part of scord. + * + * scord 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 3 of the License, or + * (at your option) any later version. + * + * scord 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 scord. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + *****************************************************************************/ + +#ifndef SCORD_CTL_COMMAND_HPP +#define SCORD_CTL_COMMAND_HPP + +#include +#include +#include +#include +#include +#include + +namespace scord_ctl { + +/** + * @brief A class representing the environment variables that + * should be set when running a command. + */ +class environment { + +public: + /** + * @brief Set an environment variable. + * + * @param key The name of the environment variable. + * @param value The value of the environment variable. + */ + void + set(const std::string& key, const std::string& value); + + /** + * @brief Get the value of an environment variable. + * + * @param key The name of the environment variable. + * @return The value of the environment variable if it exists, an empty + * string otherwise. + */ + std::string + get(const std::string& key) const; + + /** + * @brief Get the environment variables as a vector of strings. + * Each string is of the form `key=value`. + * + * @return The environment variables as a vector of strings. + */ + std::vector + as_vector() const; + + /** + * @brief Get the number of environment variables. + * + * @return The number of environment variables. + */ + std::size_t + size() const; + + /** + * @brief Get an iterator to the beginning of the environment variables. + * + * @return An iterator to the beginning of the environment variables. + */ + std::unordered_map::const_iterator + begin() const; + + /** + * @brief Get an iterator to the end of the environment variables. + * + * @return An iterator to the end of the environment variables. + */ + std::unordered_map::const_iterator + end() const; + +private: + std::unordered_map m_env; +}; + +/** + * @brief A class representing a command to be executed. + */ +class command { +public: + /** + * @brief Keywords that can be used in the command line and + * will be expanded with appropriate values when calling `eval()`. + */ + static constexpr std::array keywords = { + "{ADHOC_ID}", "{ADHOC_DIRECTORY}", "{ADHOC_NODES}"}; + + /** + * @brief Construct a command. + * + * @param cmdline The command line to be executed. + * @param env The environment variables to be set when executing the + * command. + */ + explicit command(std::string cmdline, + std::optional env = std::nullopt); + + /** + * @brief Get the template command line to be executed (i.e. without having + * keywords expanded). + * + * @return The command line to be executed. + */ + const std::string& + cmdline() const; + + /** + * @brief Get the environment variables to be set when executing the + * command. + * + * @return The environment variables to be set when executing the command. + */ + const std::optional& + env() const; + + /** + * @brief Return a copy of the current `command` where all the keywords in + * its command line template have been replaced with string + * representations of the arguments provided. + * + * @param adhoc_id The ID of the adhoc storage system. + * @param adhoc_directory The directory where the adhoc storage will run. + * @param adhoc_nodes The nodes where the adhoc storage will run. + * @return The evaluated command. + */ + command + eval(const std::string& adhoc_id, + const std::filesystem::path& adhoc_directory, + const std::vector& adhoc_nodes) const; + + /** + * @brief Get the command line to be executed as a vector of strings. The + * command line is split on spaces with each string in the resulting + * vector being a token in the command line. + * + * @return The command line to be executed as a vector of strings. + */ + std::vector + as_vector() const; + + +private: + std::string m_cmdline; + std::optional m_env; +}; + +} // namespace scord_ctl + +/** + * @brief Formatter for `scord_ctl::config::command`. + */ +template <> +struct fmt::formatter { + template + constexpr auto + parse(ParseContext& ctx) { + return ctx.begin(); + } + + template + auto + format(const scord_ctl::command& cmd, FormatContext& ctx) { + return fmt::format_to(ctx.out(), "{}", cmd.cmdline()); + } +}; + +#endif // SCORD_CTL_COMMAND_HPP diff --git a/src/scord-ctl/config_file.cpp b/src/scord-ctl/config_file.cpp index 20f77da6..f91c6355 100644 --- a/src/scord-ctl/config_file.cpp +++ b/src/scord-ctl/config_file.cpp @@ -26,8 +26,6 @@ #include #include #include -#include -#include #include "config_file.hpp" namespace { @@ -90,10 +88,10 @@ to_adhoc_storage_type(const ryml::csubstr& type) { * * @return The parsed `scord_ctl::config::environment` object. */ -scord_ctl::config::environment +scord_ctl::environment parse_environment_node(const ryml::ConstNodeRef& node) { - scord_ctl::config::environment env; + scord_ctl::environment env; for(const auto& child : node) { if(!child.has_key()) { @@ -119,11 +117,11 @@ parse_environment_node(const ryml::ConstNodeRef& node) { * * @return The parsed `scord_ctl::config::command` object. */ -command +scord_ctl::command parse_command_node(const ryml::ConstNodeRef& node) { std::string cmdline; - std::optional env; + std::optional env; for(const auto& child : node) { if(!child.has_key()) { @@ -147,7 +145,7 @@ parse_command_node(const ryml::ConstNodeRef& node) { throw std::runtime_error{"missing required `command` key"}; } - return command{cmdline, env}; + return scord_ctl::command{cmdline, env}; } /** @@ -177,8 +175,8 @@ adhoc_storage_config parse_adhoc_config_node(const ryml::ConstNodeRef& node) { std::filesystem::path working_directory; - std::optional startup_command; - std::optional shutdown_command; + std::optional startup_command; + std::optional shutdown_command; for(const auto& child : node) { @@ -284,120 +282,6 @@ parse_config_node(const ryml::ConstNodeRef& node) { namespace scord_ctl::config { -void -environment::set(const std::string& key, const std::string& value) { - m_env[key] = value; -} - -std::string -environment::get(const std::string& key) const { - return m_env.count(key) == 0 ? std::string{} : m_env.at(key); -} - -std::vector -environment::as_vector() const { - - std::vector tmp; - tmp.reserve(m_env.size()); - for(const auto& [key, value] : m_env) { - tmp.emplace_back(fmt::format("{}={}", key, value)); - } - - return tmp; -} - -std::size_t -environment::size() const { - return m_env.size(); -} - -std::unordered_map::const_iterator -environment::begin() const { - return m_env.begin(); -} - -std::unordered_map::const_iterator -environment::end() const { - return m_env.end(); -} - -command::command(std::string cmdline, std::optional env) - : m_cmdline(std::move(cmdline)), m_env(std::move(env)) {} - -const std::string& -command::cmdline() const { - return m_cmdline; -} - -const std::optional& -command::env() const { - return m_env; -} - -command -command::eval(const std::string& adhoc_id, - const std::filesystem::path& adhoc_directory, - const std::vector& adhoc_nodes) const { - - // generate a regex from a map of key/value pairs - constexpr auto regex_from_map = - [](const std::map& m) -> std::regex { - std::string result; - for(const auto& [key, value] : m) { - const auto escaped_key = - std::regex_replace(key, std::regex{R"([{}])"}, R"(\$&)"); - result += fmt::format("{}|", escaped_key); - } - result.pop_back(); - return std::regex{result}; - }; - - const std::map replacements{ - {std::string{keywords.at(0)}, adhoc_id}, - {std::string{keywords.at(1)}, adhoc_directory.string()}, - {std::string{keywords.at(2)}, - fmt::format("\"{}\"", fmt::join(adhoc_nodes, ","))}}; - - // make sure that we fail if we ever add a new keyword and forget to add - // a replacement for it - assert(replacements.size() == keywords.size()); - - std::string result; - - const auto re = regex_from_map(replacements); - auto it = std::sregex_iterator(m_cmdline.begin(), m_cmdline.end(), re); - auto end = std::sregex_iterator{}; - - std::string::size_type last_pos = 0; - - for(; it != end; ++it) { - const auto& match = *it; - result += m_cmdline.substr(last_pos, match.position() - last_pos); - result += replacements.at(match.str()); - last_pos = match.position() + match.length(); - } - - result += m_cmdline.substr(last_pos, m_cmdline.length() - last_pos); - - return command{result, m_env}; -} - -std::vector -command::as_vector() const { - std::vector tmp; - - for(auto&& r : std::views::split(m_cmdline, ' ') | - std::views::transform([](auto&& v) -> std::string { - auto c = v | std::views::common; - return std::string{c.begin(), c.end()}; - })) { - tmp.emplace_back(std::move(r)); - } - - return tmp; -} - - adhoc_storage_config::adhoc_storage_config( std::filesystem::path working_directory, command startup_command, command shutdown_command) diff --git a/src/scord-ctl/config_file.hpp b/src/scord-ctl/config_file.hpp index a0b16b91..119c6433 100644 --- a/src/scord-ctl/config_file.hpp +++ b/src/scord-ctl/config_file.hpp @@ -28,143 +28,10 @@ #include #include +#include "command.hpp" namespace scord_ctl::config { -/** - * @brief A class representing the environment variables that - * should be set when running a command. - */ -class environment { - -public: - /** - * @brief Set an environment variable. - * - * @param key The name of the environment variable. - * @param value The value of the environment variable. - */ - void - set(const std::string& key, const std::string& value); - - /** - * @brief Get the value of an environment variable. - * - * @param key The name of the environment variable. - * @return The value of the environment variable if it exists, an empty - * string otherwise. - */ - std::string - get(const std::string& key) const; - - /** - * @brief Get the environment variables as a vector of strings. - * Each string is of the form `key=value`. - * - * @return The environment variables as a vector of strings. - */ - std::vector - as_vector() const; - - /** - * @brief Get the number of environment variables. - * - * @return The number of environment variables. - */ - std::size_t - size() const; - - /** - * @brief Get an iterator to the beginning of the environment variables. - * - * @return An iterator to the beginning of the environment variables. - */ - std::unordered_map::const_iterator - begin() const; - - /** - * @brief Get an iterator to the end of the environment variables. - * - * @return An iterator to the end of the environment variables. - */ - std::unordered_map::const_iterator - end() const; - -private: - std::unordered_map m_env; -}; - -/** - * @brief A class representing a command to be executed. - */ -class command { -public: - /** - * @brief Keywords that can be used in the command line and - * will be expanded with appropriate values when calling `eval()`. - */ - static constexpr std::array keywords = { - "{ADHOC_ID}", "{ADHOC_DIRECTORY}", "{ADHOC_NODES}"}; - - /** - * @brief Construct a command. - * - * @param cmdline The command line to be executed. - * @param env The environment variables to be set when executing the - * command. - */ - explicit command(std::string cmdline, - std::optional env = std::nullopt); - - /** - * @brief Get the template command line to be executed (i.e. without having - * keywords expanded). - * - * @return The command line to be executed. - */ - const std::string& - cmdline() const; - - /** - * @brief Get the environment variables to be set when executing the - * command. - * - * @return The environment variables to be set when executing the command. - */ - const std::optional& - env() const; - - /** - * @brief Return a copy of the current `command` where all the keywords in - * its command line template have been replaced with string - * representations of the arguments provided. - * - * @param adhoc_id The ID of the adhoc storage system. - * @param adhoc_directory The directory where the adhoc storage will run. - * @param adhoc_nodes The nodes where the adhoc storage will run. - * @return The evaluated command. - */ - command - eval(const std::string& adhoc_id, - const std::filesystem::path& adhoc_directory, - const std::vector& adhoc_nodes) const; - - /** - * @brief Get the command line to be executed as a vector of strings. The - * command line is split on spaces with each string in the resulting - * vector being a token in the command line. - * - * @return The command line to be executed as a vector of strings. - */ - std::vector - as_vector() const; - - -private: - std::string m_cmdline; - std::optional m_env; -}; - /** * @brief A class representing the configuration of an adhoc storage system. */ @@ -247,23 +114,4 @@ private: } // namespace scord_ctl::config -/** - * @brief Formatter for `scord_ctl::config::command`. - */ -template <> -struct fmt::formatter { - template - constexpr auto - parse(ParseContext& ctx) { - return ctx.begin(); - } - - template - auto - format(const scord_ctl::config::command& cmd, FormatContext& ctx) { - return fmt::format_to(ctx.out(), "{}", cmd.cmdline()); - } -}; - - #endif // SCORD_CONFIG_HPP -- GitLab From 0093a1c6d8600a531b27e6b03c93ea64806429c9 Mon Sep 17 00:00:00 2001 From: Alberto Miranda Date: Wed, 7 Jun 2023 12:35:16 +0200 Subject: [PATCH 07/10] Add basic validation from `scord-ctl.conf` commands --- src/scord-ctl/config_file.cpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/scord-ctl/config_file.cpp b/src/scord-ctl/config_file.cpp index f91c6355..c8855ea5 100644 --- a/src/scord-ctl/config_file.cpp +++ b/src/scord-ctl/config_file.cpp @@ -44,6 +44,23 @@ to_string(ryml::csubstr str) { return str.has_str() ? std::string{str.data(), str.size()} : std::string{}; } +void +validate_command(const std::string& cmdline) { + + using std::filesystem::path; + const auto command_path = path{cmdline.substr(0, cmdline.find(' '))}; + + if(!command_path.is_absolute()) { + throw std::runtime_error{fmt::format( + "Command {} is not an absolute path", command_path)}; + } + + if(!exists(command_path)) { + throw std::runtime_error{ + fmt::format("Command {} does not exist", command_path)}; + } +} + // hash function for ryml::csubstr constexpr auto hash = [](const ryml::csubstr& key) { std::string tmp{key.data(), key.size()}; @@ -135,6 +152,7 @@ parse_command_node(const ryml::ConstNodeRef& node) { throw std::runtime_error{"`command` key cannot be empty"}; } cmdline = ::to_string(child.val()); + ::validate_command(cmdline); } else { fmt::print(stderr, "WARNING: Unknown key: '{}'. Ignored.\n", child.key()); -- GitLab From d2a55f79d087259574d863ac6be8cc62edf2e34b Mon Sep 17 00:00:00 2001 From: Alberto Miranda Date: Wed, 7 Jun 2023 12:34:32 +0200 Subject: [PATCH 08/10] Add placeholder for `gekkofs` script --- etc/CMakeLists.txt | 6 +++- etc/deploy_scripts/CMakeLists.txt | 35 +++++++++++++++++++++++ etc/deploy_scripts/gekkofs.sh | 3 ++ etc/{scord-ctl.conf => scord-ctl.conf.in} | 7 +++-- 4 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 etc/deploy_scripts/CMakeLists.txt create mode 100644 etc/deploy_scripts/gekkofs.sh rename etc/{scord-ctl.conf => scord-ctl.conf.in} (86%) diff --git a/etc/CMakeLists.txt b/etc/CMakeLists.txt index 9673887e..bcdd9087 100644 --- a/etc/CMakeLists.txt +++ b/etc/CMakeLists.txt @@ -22,10 +22,14 @@ # SPDX-License-Identifier: GPL-3.0-or-later # ################################################################################ +add_subdirectory(deploy_scripts) + configure_file(scord.conf.in scord.conf) +configure_file(scord-ctl.conf.in scord-ctl.conf @ONLY) # install the configuration file to sysconfdir (normally /etc) -install(FILES ${CMAKE_CURRENT_BINARY_DIR}/scord.conf scord-ctl.conf +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/scord.conf + ${CMAKE_CURRENT_BINARY_DIR}/scord-ctl.conf DESTINATION ${CMAKE_INSTALL_SYSCONFDIR} ) diff --git a/etc/deploy_scripts/CMakeLists.txt b/etc/deploy_scripts/CMakeLists.txt new file mode 100644 index 00000000..a8ee9583 --- /dev/null +++ b/etc/deploy_scripts/CMakeLists.txt @@ -0,0 +1,35 @@ +################################################################################ +# Copyright 2021-2023, Barcelona Supercomputing Center (BSC), Spain # +# # +# This software was partially supported by the EuroHPC-funded project ADMIRE # +# (Project ID: 956748, https://www.admire-eurohpc.eu). # +# # +# This file is part of scord. # +# # +# scord 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 3 of the License, or # +# (at your option) any later version. # +# # +# scord 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 scord. If not, see . # +# # +# SPDX-License-Identifier: GPL-3.0-or-later # +################################################################################ + +list(APPEND ADHOC_SCRIPTS "${CMAKE_CURRENT_SOURCE_DIR}/gekkofs.sh") + +# install adhoc scripts to `/scord` (normally /etc/scord) +install( + FILES ${ADHOC_SCRIPTS} + DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}/${PROJECT_NAME} + PERMISSIONS + OWNER_EXECUTE OWNER_WRITE OWNER_READ + GROUP_EXECUTE GROUP_READ + WORLD_EXECUTE WORLD_READ +) diff --git a/etc/deploy_scripts/gekkofs.sh b/etc/deploy_scripts/gekkofs.sh new file mode 100644 index 00000000..0fdcf091 --- /dev/null +++ b/etc/deploy_scripts/gekkofs.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +exit 0 diff --git a/etc/scord-ctl.conf b/etc/scord-ctl.conf.in similarity index 86% rename from etc/scord-ctl.conf rename to etc/scord-ctl.conf.in index bd639f5f..63bb5fda 100644 --- a/etc/scord-ctl.conf +++ b/etc/scord-ctl.conf.in @@ -24,14 +24,17 @@ config: # `working_directory` and automatically removed after the adhoc # instance has been shut down. # * ADHOC_ID: - A unique ID for the adhoc instance. - command: script.sh start + command: @CMAKE_INSTALL_FULL_SYSCONFDIR@/@PROJECT_NAME@/gekkofs.sh + start --hosts {ADHOC_NODES} --workdir {ADHOC_DIRECTORY} --datadir {ADHOC_DIRECTORY}/data --mountdir {ADHOC_DIRECTORY}/mnt shutdown: environment: - command: script.sh stop --workdir {ADHOC_DIRECTORY} + command: @CMAKE_INSTALL_FULL_SYSCONFDIR@/@PROJECT_NAME@/gekkofs.sh + stop + --workdir {ADHOC_DIRECTORY} # default storage tiers made available to applications -- GitLab From aee8fb7046cee4bc7cb9823ef5b489db73e88705 Mon Sep 17 00:00:00 2001 From: Alberto Miranda Date: Wed, 7 Jun 2023 15:12:37 +0200 Subject: [PATCH 09/10] Fixes from clang-tidy --- src/lib/scord/types.hpp | 169 ++++++++++++++++++++-------------------- 1 file changed, 83 insertions(+), 86 deletions(-) diff --git a/src/lib/scord/types.hpp b/src/lib/scord/types.hpp index 34d40c69..910d4835 100644 --- a/src/lib/scord/types.hpp +++ b/src/lib/scord/types.hpp @@ -644,6 +644,18 @@ struct fmt::formatter : formatter { } }; +template <> +struct fmt::formatter> + : fmt::formatter { + // parse is inherited from formatter. + template + auto + format(const std::vector& v, FormatContext& ctx) const { + const auto str = fmt::format("[{}]", fmt::join(v, ", ")); + return formatter::format(str, ctx); + } +}; + template <> struct fmt::formatter : fmt::formatter { // parse is inherited from formatter. @@ -680,6 +692,56 @@ struct fmt::formatter : formatter { } }; +template <> +struct fmt::formatter> + : fmt::formatter { + // parse is inherited from formatter. + template + auto + format(const std::vector& v, FormatContext& ctx) const { + const auto str = fmt::format("[{}]", fmt::join(v, ", ")); + return formatter::format(str, ctx); + } +}; + +template <> +struct fmt::formatter : formatter { + // parse is inherited from formatter. + template + auto + format(const scord::transfer::mapping& m, FormatContext& ctx) const { + + using mapping = scord::transfer::mapping; + + std::string_view name = "unknown"; + + switch(m) { + case mapping::one_to_one: + name = "ADM_MAPPING_ONE_TO_ONE"; + break; + case mapping::one_to_n: + name = "ADM_MAPPING_ONE_TO_N"; + break; + case mapping::n_to_n: + name = "ADM_MAPPING_N_TO_N"; + break; + } + + return formatter::format(name, ctx); + } +}; + +template <> +struct fmt::formatter : fmt::formatter { + // parse is inherited from formatter. + template + auto + format(const scord::transfer& tx, FormatContext& ctx) const { + const auto str = fmt::format("{{id: {}}}", tx.id()); + return formatter::format(str, ctx); + } +}; + template <> struct fmt::formatter { @@ -792,6 +854,20 @@ struct fmt::formatter } }; +template <> +struct fmt::formatter : formatter { + // parse is inherited from formatter. + template + auto + format(const scord::adhoc_storage::ctx& c, FormatContext& ctx) const { + return format_to(ctx.out(), + "{{controller: {}, execution_mode: {}, " + "access_type: {}, walltime: {}, should_flush: {}}}", + std::quoted(c.controller_address()), c.exec_mode(), + c.access_type(), c.walltime(), c.should_flush()); + } +}; + template struct fmt::formatter> : formatter { @@ -831,24 +907,6 @@ struct fmt::formatter } }; - -template <> -struct fmt::formatter : formatter { - // parse is inherited from formatter. - template - auto - format(const scord::adhoc_storage::ctx& c, FormatContext& ctx) const { - - const auto str = - fmt::format("{{controller: {}, execution_mode: {}, " - "access_type: {}, walltime: {}, should_flush: {}}}", - std::quoted(c.controller_address()), c.exec_mode(), - c.access_type(), c.walltime(), c.should_flush()); - - return formatter::format(str, ctx); - } -}; - template <> struct fmt::formatter : formatter { @@ -874,23 +932,23 @@ struct fmt::formatter }; template <> -struct fmt::formatter : formatter { +struct fmt::formatter : formatter { // parse is inherited from formatter. template auto - format(const scord::pfs_storage& s, FormatContext& ctx) const { - const auto str = fmt::format("{{context: {}}}", s.context()); + format(const scord::pfs_storage::ctx& c, FormatContext& ctx) const { + const auto str = fmt::format("{{mount_point: {}}}", c.mount_point()); return formatter::format(str, ctx); } }; template <> -struct fmt::formatter : formatter { +struct fmt::formatter : formatter { // parse is inherited from formatter. template auto - format(const scord::pfs_storage::ctx& c, FormatContext& ctx) const { - const auto str = fmt::format("{{mount_point: {}}}", c.mount_point()); + format(const scord::pfs_storage& s, FormatContext& ctx) const { + const auto str = fmt::format("{{context: {}}}", s.context()); return formatter::format(str, ctx); } }; @@ -1021,70 +1079,9 @@ struct fmt::formatter : formatter { } }; -template <> -struct fmt::formatter : formatter { - // parse is inherited from formatter. - template - auto - format(const scord::transfer::mapping& m, FormatContext& ctx) const { - - using mapping = scord::transfer::mapping; - - std::string_view name = "unknown"; - - switch(m) { - case mapping::one_to_one: - name = "ADM_MAPPING_ONE_TO_ONE"; - break; - case mapping::one_to_n: - name = "ADM_MAPPING_ONE_TO_N"; - break; - case mapping::n_to_n: - name = "ADM_MAPPING_N_TO_N"; - break; - } - - return formatter::format(name, ctx); - } -}; - -template <> -struct fmt::formatter : formatter { - // parse is inherited from formatter. - template - auto - format(const scord::transfer& tx, FormatContext& ctx) const { - const auto str = fmt::format("{{id: {}}}", tx.id()); - return formatter::format(str, ctx); - } -}; - -template <> -struct fmt::formatter> : formatter { - // parse is inherited from formatter. - template - auto - format(const std::vector& v, FormatContext& ctx) const { - const auto str = fmt::format("[{}]", fmt::join(v, ", ")); - return formatter::format(str, ctx); - } -}; - -template <> -struct fmt::formatter> - : formatter { - // parse is inherited from formatter. - template - auto - format(const std::vector& v, FormatContext& ctx) const { - const auto str = fmt::format("[{}]", fmt::join(v, ", ")); - return formatter::format(str, ctx); - } -}; - template <> struct fmt::formatter> - : formatter { + : fmt::formatter { // parse is inherited from formatter. template auto -- GitLab From 6fd1d3b6b735072c3a3dc45b5b1a2102cd3c0ce3 Mon Sep 17 00:00:00 2001 From: Alberto Miranda Date: Wed, 7 Jun 2023 15:14:30 +0200 Subject: [PATCH 10/10] CI: Disable clang build - Reason: There are issues with `std::ranges` implementation in clang-10, clang-11, and clang-12 (at least). --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index abc4566d..02255cbe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -56,7 +56,7 @@ gcc-debug: - build/src/scord/scord - build/src/scord-ctl/scord-ctl -clang: +.clang: stage: build parallel: matrix: -- GitLab