diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index abc4566db55971a66cf461f922e546bab02c7d28..02255cbec78cb79ac7089901a60b0afc31440e19 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: diff --git a/etc/CMakeLists.txt b/etc/CMakeLists.txt index 7f8032860c008e297cd4ff9b36636936d61abbc5..bcdd908716ee66832c2e5978784a835e4b4fee98 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 + ${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 0000000000000000000000000000000000000000..a8ee9583431628b2df3de5080532fc66ea3da140 --- /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 0000000000000000000000000000000000000000..0fdcf0910beedfa75021868b1239cad921f27110 --- /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.in b/etc/scord-ctl.conf.in new file mode 100644 index 0000000000000000000000000000000000000000..63bb5fda163a17b05b8a14453fc5a59fae2b12b9 --- /dev/null +++ b/etc/scord-ctl.conf.in @@ -0,0 +1,48 @@ +## 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: @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: @CMAKE_INSTALL_FULL_SYSCONFDIR@/@PROJECT_NAME@/gekkofs.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 494bc2197cce7fb3ee9c2c394f91879f04ac7ea0..c7261744360868addc49adca8b9520b9844d112c 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 bafa8b82df13c2c2dc9b17bbebf536b5ed93f772..6b7787d9e616126581acc50c93f69d5b6e0f92e7 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 a1002f49e8b14d455141897a62c6e76f286fe466..910d4835734721ec9c66d2a07050c19dbae40175 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. @@ -681,9 +693,76 @@ struct fmt::formatter : formatter { }; template <> -struct fmt::formatter - : formatter { +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 { + + // 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 +772,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); } }; @@ -771,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 { @@ -810,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 { @@ -853,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); } }; @@ -1000,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 diff --git a/src/scord-ctl/CMakeLists.txt b/src/scord-ctl/CMakeLists.txt index 9654c6e364549cc4a29b55b035c54db8e46864b2..10107f907751ff6857372b3a0d711c248f062f4b 100644 --- a/src/scord-ctl/CMakeLists.txt +++ b/src/scord-ctl/CMakeLists.txt @@ -27,8 +27,12 @@ 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) + target_include_directories( scord-ctl PUBLIC ${CMAKE_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR} @@ -37,7 +41,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/command.cpp b/src/scord-ctl/command.cpp new file mode 100644 index 0000000000000000000000000000000000000000..55327b5b2beff5e685743a0a71e127ba30d5c122 --- /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 0000000000000000000000000000000000000000..9ed7f6d0a3502cf40202b210bac9fca1e88647ef --- /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 new file mode 100644 index 0000000000000000000000000000000000000000..c8855ea50afffe9c3995680d9dcad7496e9df877 --- /dev/null +++ b/src/scord-ctl/config_file.cpp @@ -0,0 +1,361 @@ +/****************************************************************************** + * 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 "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{}; +} + +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()}; + 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::environment +parse_environment_node(const ryml::ConstNodeRef& node) { + + scord_ctl::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. + */ +scord_ctl::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()); + ::validate_command(cmdline); + } else { + fmt::print(stderr, "WARNING: Unknown key: '{}'. Ignored.\n", + child.key()); + } + } + + if(cmdline.empty()) { + throw std::runtime_error{"missing required `command` key"}; + } + + return scord_ctl::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 { + +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 0000000000000000000000000000000000000000..119c6433e3cf2de67061c1d2b9fc2cc45134f327 --- /dev/null +++ b/src/scord-ctl/config_file.hpp @@ -0,0 +1,117 @@ +/****************************************************************************** + * 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 +#include "command.hpp" + +namespace scord_ctl::config { + +/** + * @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; +}; + +#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; + +/** + * @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 0000000000000000000000000000000000000000..52189e83e4f6d57b9a5c4c86263dec3d116834fa --- /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 bb7da914262c69defecb9430dd8b6f3c38960110..f9b3b988d45e173bf5e5d00cb8f71c795fd72c55 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 2988a1c4f3f4f565927574bf7764f9cf4bcd7497..e466001e14b45a30e1c3fd94cb7f4df3dd2c03bd 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 b9c0286be6f952de54928575a9d02789ab6922f9..aa485074a2e2a2633556d20adefa1d649387e60f 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(), "