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(), "