diff --git a/contrib/kyua/Makefile.am b/contrib/kyua/Makefile.am --- a/contrib/kyua/Makefile.am +++ b/contrib/kyua/Makefile.am @@ -59,6 +59,7 @@ include drivers/Makefile.am.inc include engine/Makefile.am.inc include examples/Makefile.am.inc +include freebsd/Makefile.am.inc include integration/Makefile.am.inc include misc/Makefile.am.inc include model/Makefile.am.inc @@ -68,7 +69,7 @@ bin_PROGRAMS = kyua kyua_SOURCES = main.cpp kyua_CXXFLAGS = $(CLI_CFLAGS) $(ENGINE_CFLAGS) $(UTILS_CFLAGS) -kyua_LDADD = $(CLI_LIBS) $(ENGINE_LIBS) $(UTILS_LIBS) +kyua_LDADD = $(CLI_LIBS) $(ENGINE_LIBS) $(FREEBSD_LIBS) $(UTILS_LIBS) CHECK_ENVIRONMENT = KYUA_CONFDIR="/non-existent" \ KYUA_DOCDIR="$(abs_top_srcdir)" \ diff --git a/contrib/kyua/cli/cmd_config_test.cpp b/contrib/kyua/cli/cmd_config_test.cpp --- a/contrib/kyua/cli/cmd_config_test.cpp +++ b/contrib/kyua/cli/cmd_config_test.cpp @@ -61,6 +61,7 @@ { config::tree user_config = engine::default_config(); user_config.set_string("architecture", "the-architecture"); + user_config.set_string("execenv", "the-env"); user_config.set_string("parallelism", "128"); user_config.set_string("platform", "the-platform"); //user_config.set_string("unprivileged_user", ""); @@ -83,12 +84,13 @@ cmdline::ui_mock ui; ATF_REQUIRE_EQ(EXIT_SUCCESS, cmd.main(&ui, args, fake_config())); - ATF_REQUIRE_EQ(5, ui.out_log().size()); + ATF_REQUIRE_EQ(6, ui.out_log().size()); ATF_REQUIRE_EQ("architecture = the-architecture", ui.out_log()[0]); - ATF_REQUIRE_EQ("parallelism = 128", ui.out_log()[1]); - ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[2]); - ATF_REQUIRE_EQ("test_suites.foo.bar = first", ui.out_log()[3]); - ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[4]); + ATF_REQUIRE_EQ("execenv = the-env", ui.out_log()[1]); + ATF_REQUIRE_EQ("parallelism = 128", ui.out_log()[2]); + ATF_REQUIRE_EQ("platform = the-platform", ui.out_log()[3]); + ATF_REQUIRE_EQ("test_suites.foo.bar = first", ui.out_log()[4]); + ATF_REQUIRE_EQ("test_suites.foo.baz = second", ui.out_log()[5]); ATF_REQUIRE(ui.err_log().empty()); } diff --git a/contrib/kyua/configure.ac b/contrib/kyua/configure.ac --- a/contrib/kyua/configure.ac +++ b/contrib/kyua/configure.ac @@ -169,5 +169,6 @@ AM_CONDITIONAL(TARGET_SRCDIR_EMPTY, [test -z "${target_srcdir}"]) AC_SUBST([target_srcdir]) +AM_CONDITIONAL([FreeBSD], [test "$(uname -o)" = "FreeBSD"]) AC_OUTPUT diff --git a/contrib/kyua/drivers/report_junit_test.cpp b/contrib/kyua/drivers/report_junit_test.cpp --- a/contrib/kyua/drivers/report_junit_test.cpp +++ b/contrib/kyua/drivers/report_junit_test.cpp @@ -63,6 +63,8 @@ "allowed_architectures is empty\n" "allowed_platforms is empty\n" "description is empty\n" + "execenv is empty\n" + "execenv_jail is empty\n" "has_cleanup = false\n" "is_exclusive = false\n" "required_configs is empty\n" @@ -80,6 +82,8 @@ "allowed_architectures is empty\n" "allowed_platforms is empty\n" "description = Textual description\n" + "execenv is empty\n" + "execenv_jail is empty\n" "has_cleanup = false\n" "is_exclusive = false\n" "required_configs is empty\n" @@ -199,6 +203,8 @@ .add_allowed_architecture("arch1") .add_allowed_platform("platform1") .set_description("This is a test") + .set_execenv("jail") + .set_execenv_jail("vnet") .set_has_cleanup(true) .set_is_exclusive(true) .add_required_config("config1") @@ -215,6 +221,8 @@ + "allowed_architectures = arch1\n" + "allowed_platforms = platform1\n" + "description = This is a test\n" + + "execenv = jail\n" + + "execenv_jail = vnet\n" + "has_cleanup = true\n" + "is_exclusive = true\n" + "required_configs = config1\n" diff --git a/contrib/kyua/engine/Makefile.am.inc b/contrib/kyua/engine/Makefile.am.inc --- a/contrib/kyua/engine/Makefile.am.inc +++ b/contrib/kyua/engine/Makefile.am.inc @@ -153,3 +153,5 @@ engine_scheduler_test_CXXFLAGS = $(ENGINE_CFLAGS) $(ATF_CXX_CFLAGS) engine_scheduler_test_LDADD = $(ENGINE_LIBS) $(ATF_CXX_LIBS) endif + +include engine/execenv/Makefile.am.inc diff --git a/contrib/kyua/engine/atf.cpp b/contrib/kyua/engine/atf.cpp --- a/contrib/kyua/engine/atf.cpp +++ b/contrib/kyua/engine/atf.cpp @@ -39,6 +39,7 @@ #include "engine/atf_list.hpp" #include "engine/atf_result.hpp" #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" #include "model/test_result.hpp" @@ -54,6 +55,7 @@ #include "utils/stream.hpp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace process = utils::process; @@ -190,7 +192,10 @@ args.push_back(F("-r%s") % (control_directory / result_name)); args.push_back(test_case_name); - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->init(); + e->exec(args); } @@ -219,7 +224,9 @@ } args.push_back(F("%s:cleanup") % test_case_name); - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->exec(args); } diff --git a/contrib/kyua/engine/atf_list.cpp b/contrib/kyua/engine/atf_list.cpp --- a/contrib/kyua/engine/atf_list.cpp +++ b/contrib/kyua/engine/atf_list.cpp @@ -121,6 +121,10 @@ mdbuilder.set_string("has_cleanup", value); } else if (name == "require.arch") { mdbuilder.set_string("allowed_architectures", value); + } else if (name == "execenv") { + mdbuilder.set_string("execenv", value); + } else if (name == "execenv.jail") { + mdbuilder.set_string("execenv_jail", value); } else if (name == "require.config") { mdbuilder.set_string("required_configs", value); } else if (name == "require.files") { diff --git a/contrib/kyua/engine/config.cpp b/contrib/kyua/engine/config.cpp --- a/contrib/kyua/engine/config.cpp +++ b/contrib/kyua/engine/config.cpp @@ -35,6 +35,7 @@ #include #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "utils/config/exceptions.hpp" #include "utils/config/parser.hpp" #include "utils/config/tree.ipp" @@ -43,6 +44,7 @@ #include "utils/text/operations.ipp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace passwd = utils::passwd; namespace text = utils::text; @@ -59,6 +61,7 @@ init_tree(config::tree& tree) { tree.define< config::string_node >("architecture"); + tree.define< config::strings_set_node >("execenv"); tree.define< config::positive_int_node >("parallelism"); tree.define< config::string_node >("platform"); tree.define< engine::user_node >("unprivileged_user"); @@ -74,6 +77,14 @@ set_defaults(config::tree& tree) { tree.set< config::string_node >("architecture", KYUA_ARCHITECTURE); + + std::set< std::string > supported; + for (auto em : execenv::execenvs()) + if (em->is_supported()) + supported.insert(em->name()); + supported.insert("host"); + tree.set< config::strings_set_node >("execenv", supported); + // TODO(jmmv): Automatically derive this from the number of CPUs in the // machine and forcibly set to a value greater than 1. Still testing // the new parallel implementation as of 2015-02-27 though. @@ -229,6 +240,13 @@ { config::tree tree(false); init_tree(tree); + + // Tests of Kyua itself tend to use an empty config, and they want + // to allow running usual host based test cases. + std::set< std::string > supported; + supported.insert("host"); + tree.set< config::strings_set_node >("execenv", supported); + return tree; } diff --git a/contrib/kyua/engine/execenv/Makefile.am.inc b/contrib/kyua/engine/execenv/Makefile.am.inc new file mode 100644 --- /dev/null +++ b/contrib/kyua/engine/execenv/Makefile.am.inc @@ -0,0 +1,32 @@ +# Copyright 2024 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +libengine_a_SOURCES += engine/execenv/execenv.hpp +libengine_a_SOURCES += engine/execenv/execenv.cpp +libengine_a_SOURCES += engine/execenv/execenv_host.hpp +libengine_a_SOURCES += engine/execenv/execenv_host.cpp diff --git a/contrib/kyua/engine/execenv/execenv.hpp b/contrib/kyua/engine/execenv/execenv.hpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/engine/execenv/execenv.hpp @@ -0,0 +1,144 @@ +// Copyright (c) 2023 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/execenv/execenv.hpp +/// Execution environment subsystem interface. + +#if !defined(ENGINE_EXECENV_EXECENV_HPP) +#define ENGINE_EXECENV_EXECENV_HPP + +#include "model/test_program.hpp" +#include "utils/optional.ipp" +#include "utils/process/operations_fwd.hpp" + +using utils::process::args_vector; +using utils::optional; + +namespace engine { +namespace execenv { + +/// Abstract interface of an execution environment. +class interface { +protected: + const model::test_program& _test_program; + const std::string& _test_case_name; + +public: + /// Constructor. + /// + /// \param program The test program. + /// \param test_case_name Name of the test case. + interface(const model::test_program& test_program, + const std::string& test_case_name) : + _test_program(test_program), + _test_case_name(test_case_name) + {} + + /// Destructor. + virtual ~interface() {} + + /// Initializes execution environment. + /// + /// It's expected to be called inside a fork which runs + /// scheduler::interface::exec_test(), so we can fail a test fast if its + /// execution environment setup fails, and test execution could use the + /// configured proc environment, if expected. + virtual void init() const = 0; + + /// Cleanups or removes execution environment. + /// + /// It's expected to be called inside a fork for execenv cleanup. + virtual void cleanup() const = 0; + + /// Executes a test within the execution environment. + /// + /// It's expected to be called inside a fork which runs + /// scheduler::interface::exec_test() or exec_cleanup(). + /// + /// \param args The arguments to pass to the binary. + virtual void exec(const args_vector& args) const UTILS_NORETURN = 0; +}; + + +/// Abstract interface of an execution environment manager. +class manager { +public: + /// Destructor. + virtual ~manager() {} + + /// Returns name of an execution environment. + virtual const std::string& name() const = 0; + + /// Returns whether this execution environment is actually supported. + /// + /// It can be compile time and/or runtime check. + virtual bool is_supported() const = 0; + + /// Returns execution environment for a test. + /// + /// It checks if the given test is designed for this execution environment. + /// + /// \param program The test program. + /// \param test_case_name Name of the test case. + /// + /// \return An execenv object if the test conforms, or none. + virtual std::unique_ptr< interface > probe( + const model::test_program& test_program, + const std::string& test_case_name) const = 0; + + // TODO: execenv related extra metadata could be provided by a manager + // not to know how exactly and where it should be added to the kyua +}; + + +/// Registers an execution environment. +/// +/// \param manager Execution environment manager. +void register_execenv(const std::shared_ptr< manager > manager); + + +/// Returns list of registered execenv managers, except default host one. +/// +/// \return A vector of pointers to execenv managers. +const std::vector< std::shared_ptr< manager> > execenvs(); + + +/// Returns execution environment for a test case. +/// +/// \param program The test program. +/// \param test_case_name Name of the test case. +/// +/// \return An execution environment of a test. +std::unique_ptr< execenv::interface > get( + const model::test_program& test_program, + const std::string& test_case_name); + + +} // namespace execenv +} // namespace engine + +#endif // !defined(ENGINE_EXECENV_EXECENV_HPP) diff --git a/contrib/kyua/engine/execenv/execenv.cpp b/contrib/kyua/engine/execenv/execenv.cpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/engine/execenv/execenv.cpp @@ -0,0 +1,70 @@ +// Copyright (c) 2023 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/execenv/execenv.hpp" + +#include "engine/execenv/execenv_host.hpp" + +namespace execenv = engine::execenv; + +using utils::none; + + +/// List of registered execution environments, except default host one. +/// +/// Use register_execenv() to add an entry to this global list. +static std::vector< std::shared_ptr< execenv::manager > > + execenv_managers; + + +void +execenv::register_execenv(const std::shared_ptr< execenv::manager > manager) +{ + execenv_managers.push_back(manager); +} + + +const std::vector< std::shared_ptr< execenv::manager> > +execenv::execenvs() +{ + return execenv_managers; +} + + +std::unique_ptr< execenv::interface > +execenv::get(const model::test_program& test_program, + const std::string& test_case_name) +{ + for (auto m : execenv_managers) { + auto e = m->probe(test_program, test_case_name); + if (e != nullptr) + return e; + } + + return std::unique_ptr< execenv::interface >( + new execenv::execenv_host(test_program, test_case_name)); +} diff --git a/contrib/kyua/engine/execenv/execenv_host.hpp b/contrib/kyua/engine/execenv/execenv_host.hpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/engine/execenv/execenv_host.hpp @@ -0,0 +1,62 @@ +// Copyright (c) 2024 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file engine/execenv/execenv_host.hpp +/// Default execution environment. + +#if !defined(ENGINE_EXECENV_EXECENV_HOST_HPP) +#define ENGINE_EXECENV_EXECENV_HOST_HPP + +#include "engine/execenv/execenv.hpp" + +#include "utils/process/operations_fwd.hpp" + +namespace execenv = engine::execenv; + +using utils::process::args_vector; + +namespace engine { +namespace execenv { + + +class execenv_host : public execenv::interface { +public: + execenv_host(const model::test_program& test_program, + const std::string& test_case_name) : + execenv::interface(test_program, test_case_name) + {} + + void init() const; + void cleanup() const; + void exec(const args_vector& args) const UTILS_NORETURN; +}; + + +} // namespace execenv +} // namespace engine + +#endif // !defined(ENGINE_EXECENV_EXECENV_HOST_HPP) diff --git a/contrib/kyua/engine/execenv/execenv_host.cpp b/contrib/kyua/engine/execenv/execenv_host.cpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/engine/execenv/execenv_host.cpp @@ -0,0 +1,51 @@ +// Copyright (c) 2024 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "engine/execenv/execenv_host.hpp" + +#include "utils/fs/path.hpp" +#include "utils/process/operations.hpp" + +void +execenv::execenv_host::init() const +{ + // nothing to do +} + + +void +execenv::execenv_host::cleanup() const +{ + // nothing to do +} + + +void +execenv::execenv_host::exec(const args_vector& args) const +{ + utils::process::exec(_test_program.absolute_path(), args); +} diff --git a/contrib/kyua/engine/plain.cpp b/contrib/kyua/engine/plain.cpp --- a/contrib/kyua/engine/plain.cpp +++ b/contrib/kyua/engine/plain.cpp @@ -34,6 +34,7 @@ #include +#include "engine/execenv/execenv.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" #include "model/test_result.hpp" @@ -47,6 +48,7 @@ #include "utils/sanity.hpp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace process = utils::process; @@ -104,7 +106,10 @@ } process::args_vector args; - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->init(); + e->exec(args); } diff --git a/contrib/kyua/engine/requirements.cpp b/contrib/kyua/engine/requirements.cpp --- a/contrib/kyua/engine/requirements.cpp +++ b/contrib/kyua/engine/requirements.cpp @@ -100,6 +100,34 @@ } +/// Checks if test's execenv matches the user configuration. +/// +/// \param execenv Execution environment name a test is designed for. +/// \param user_config Runtime user configuration. +/// +/// \return Empty if the execenv is in the list or an error message otherwise. +static std::string +check_execenv(const std::string& execenv, const config::tree& user_config) +{ + std::string name = execenv; + if (name.empty()) + name = "host"; // if a test claims nothing then it's host based + + std::set< std::string > execenvs; + try { + execenvs = user_config.lookup< config::strings_set_node >("execenv"); + } catch (const config::unknown_key_error&) { + // okay, user config does not define it, empty set then + } + + if (execenvs.find(name) == execenvs.end()) + return F("'%s' execenv is not supported or not allowed by " + "the runtime user configuration") % name; + + return ""; +} + + /// Checks if the allowed platforms match the current architecture. /// /// \param allowed_platforms Set of allowed platforms. @@ -263,6 +291,10 @@ if (!reason.empty()) return reason; + reason = check_execenv(md.execenv(), cfg); + if (!reason.empty()) + return reason; + reason = check_allowed_platforms(md.allowed_platforms(), cfg); if (!reason.empty()) return reason; diff --git a/contrib/kyua/engine/scheduler.hpp b/contrib/kyua/engine/scheduler.hpp --- a/contrib/kyua/engine/scheduler.hpp +++ b/contrib/kyua/engine/scheduler.hpp @@ -262,6 +262,7 @@ extern utils::datetime::delta cleanup_timeout; +extern utils::datetime::delta execenv_cleanup_timeout; extern utils::datetime::delta list_timeout; diff --git a/contrib/kyua/engine/scheduler.cpp b/contrib/kyua/engine/scheduler.cpp --- a/contrib/kyua/engine/scheduler.cpp +++ b/contrib/kyua/engine/scheduler.cpp @@ -40,6 +40,7 @@ #include "engine/config.hpp" #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "engine/requirements.hpp" #include "model/context.hpp" #include "model/metadata.hpp" @@ -68,6 +69,7 @@ namespace config = utils::config; namespace datetime = utils::datetime; +namespace execenv = engine::execenv; namespace executor = utils::process::executor; namespace fs = utils::fs; namespace logging = utils::logging; @@ -87,6 +89,10 @@ datetime::delta scheduler::cleanup_timeout(60, 0); +/// Timeout for the test case execenv cleanup operation. +datetime::delta scheduler::execenv_cleanup_timeout(60, 0); + + /// Timeout for the test case listing operation. /// /// TODO(jmmv): This is here only for testing purposes. Maybe we should expose @@ -206,6 +212,18 @@ /// denote that no further attempts shall be made at cleaning this up. bool needs_cleanup; + /// Whether this test case still needs to have its execenv cleanup executed. + /// + /// This is set externally when the cleanup routine is actually invoked to + /// denote that no further attempts shall be made at cleaning this up. + bool needs_execenv_cleanup; + + /// Original PID of the test case subprocess. + /// + /// This is used for the cleanup upon termination by a signal, to reap the + /// leftovers and form missing exit_handle. + int pid; + /// The exit_handle for this test once it has completed. /// /// This is set externally when the test case has finished, as we need this @@ -222,12 +240,14 @@ test_exec_data(const model::test_program_ptr test_program_, const std::string& test_case_name_, const std::shared_ptr< scheduler::interface > interface_, - const config::tree& user_config_) : + const config::tree& user_config_, + const int pid_) : exec_data(test_program_, test_case_name_), - interface(interface_), user_config(user_config_) + interface(interface_), user_config(user_config_), pid(pid_) { const model::test_case& test_case = test_program->find(test_case_name); needs_cleanup = test_case.get_metadata().has_cleanup(); + needs_execenv_cleanup = test_case.get_metadata().has_execenv(); } }; @@ -266,6 +286,40 @@ }; +/// Maintenance data held while a test execenv cleanup is being executed. +/// +/// Instances of this object are related to a previous test_exec_data, as +/// cleanup routines can only exist once the test has been run. +struct execenv_exec_data : public exec_data { + /// The exit handle of the test. This is necessary so that we can return + /// the correct exit_handle to the user of the scheduler. + executor::exit_handle body_exit_handle; + + /// The final result of the test's body. This is necessary to compute the + /// right return value for a test with a cleanup routine: the body result is + /// respected if it is a "bad" result; else the result of the cleanup + /// routine is used if it has failed. + model::test_result body_result; + + /// Constructor. + /// + /// \param test_program_ Test program data for this test case. + /// \param test_case_name_ Name of the test case. + /// \param body_exit_handle_ If not none, exit handle of the body + /// corresponding to the cleanup routine represented by this exec_data. + /// \param body_result_ If not none, result of the body corresponding to the + /// cleanup routine represented by this exec_data. + execenv_exec_data(const model::test_program_ptr test_program_, + const std::string& test_case_name_, + const executor::exit_handle& body_exit_handle_, + const model::test_result& body_result_) : + exec_data(test_program_, test_case_name_), + body_exit_handle(body_exit_handle_), body_result(body_result_) + { + } +}; + + /// Shared pointer to exec_data. /// /// We require this because we want exec_data to not be copyable, and thus we @@ -492,6 +546,40 @@ }; +/// Functor to execute a test execenv cleanup in a child process. +class run_execenv_cleanup { + /// Test program to execute. + const model::test_program _test_program; + + /// Name of the test case to execute. + const std::string& _test_case_name; + +public: + /// Constructor. + /// + /// \param test_program Test program to execute. + /// \param test_case_name Name of the test case to execute. + run_execenv_cleanup( + const model::test_program_ptr test_program, + const std::string& test_case_name) : + _test_program(force_absolute_paths(*test_program)), + _test_case_name(test_case_name) + { + } + + /// Body of the subprocess. + /// + /// \param control_directory The testcase directory where cleanup will be + /// run from. + void + operator()(const fs::path& /* control_directory */) + { + auto e = execenv::get(_test_program, _test_case_name); + e->cleanup(); + } +}; + + /// Obtains the right scheduler interface for a given test program. /// /// \param name The name of the interface of the test program. @@ -835,6 +923,22 @@ % test_data->test_case_name); } } + + const test_exec_data_vector td = tests_needing_execenv_cleanup(); + + for (test_exec_data_vector::const_iterator iter = td.begin(); + iter != td.end(); ++iter) { + const test_exec_data* test_data = *iter; + + try { + sync_execenv_cleanup(test_data); + } catch (const std::runtime_error& e) { + LW(F("Failed to run execenv cleanup routine for %s:%s on abrupt " + "termination") + % test_data->test_program->relative_path() + % test_data->test_case_name); + } + } } /// Finds any pending exec_datas that correspond to tests needing cleanup. @@ -856,6 +960,8 @@ if (test_data->needs_cleanup) { tests_data.push_back(test_data); test_data->needs_cleanup = false; + if (!test_data->exit_handle) + test_data->exit_handle = generic.reap(test_data->pid); } } catch (const std::bad_cast& e) { // Do nothing for cleanup_exec_data objects. @@ -865,6 +971,37 @@ return tests_data; } + /// Finds any pending exec_datas that correspond to tests needing execenv + /// cleanup. + /// + /// \return The collection of test_exec_data objects that have their + /// specific execenv property set. + test_exec_data_vector + tests_needing_execenv_cleanup(void) + { + test_exec_data_vector tests_data; + + for (exec_data_map::const_iterator iter = all_exec_data.begin(); + iter != all_exec_data.end(); ++iter) { + const exec_data_ptr data = (*iter).second; + + try { + test_exec_data* test_data = &dynamic_cast< test_exec_data& >( + *data.get()); + if (test_data->needs_execenv_cleanup) { + tests_data.push_back(test_data); + test_data->needs_execenv_cleanup = false; + if (!test_data->exit_handle) + test_data->exit_handle = generic.reap(test_data->pid); + } + } catch (const std::bad_cast& e) { + // Do nothing for other objects. + } + } + + return tests_data; + } + /// Cleans up a single test case synchronously. /// /// \param test_data The data of the previously executed test case to be @@ -926,6 +1063,61 @@ return handle; } + + /// Cleans up a single test case execenv synchronously. + /// + /// \param test_data The data of the previously executed test case to be + /// cleaned up. + void + sync_execenv_cleanup(const test_exec_data* test_data) + { + // The message in this result should never be seen by the user, but use + // something reasonable just in case it leaks and we need to pinpoint + // the call site. + model::test_result result(model::test_result_broken, + "Test case died abruptly"); + + const executor::exec_handle cleanup_handle = spawn_execenv_cleanup( + test_data->test_program, test_data->test_case_name, + test_data->exit_handle.get(), result); + generic.wait(cleanup_handle); + } + + /// Forks and executes a test case execenv cleanup asynchronously. + /// + /// \param test_program The container test program. + /// \param test_case_name The name of the test case to run. + /// \param body_handle The exit handle of the test case's corresponding + /// body. The cleanup will be executed in the same context. + /// \param body_result The result of the test case's corresponding body. + /// + /// \return A handle for the background operation. Used to match the result + /// of the execution returned by wait_any() with this invocation. + executor::exec_handle + spawn_execenv_cleanup(const model::test_program_ptr test_program, + const std::string& test_case_name, + const executor::exit_handle& body_handle, + const model::test_result& body_result) + { + generic.check_interrupt(); + + LI(F("Spawning %s:%s (execenv cleanup)") + % test_program->absolute_path() % test_case_name); + + const executor::exec_handle handle = generic.spawn_followup( + run_execenv_cleanup(test_program, test_case_name), + body_handle, execenv_cleanup_timeout); + + const exec_data_ptr data(new execenv_exec_data( + test_program, test_case_name, body_handle, body_result)); + LD(F("Inserting %s into all_exec_data (execenv cleanup)") % handle.pid()); + INV_MSG(all_exec_data.find(handle.pid()) == all_exec_data.end(), + F("PID %s already in all_exec_data; not properly cleaned " + "up or reused too fast") % handle.pid());; + all_exec_data.insert(exec_data_map::value_type(handle.pid(), data)); + + return handle; + } }; @@ -1115,7 +1307,7 @@ unprivileged_user); const exec_data_ptr data(new test_exec_data( - test_program, test_case_name, interface, user_config)); + test_program, test_case_name, interface, user_config, handle.pid())); LD(F("Inserting %s into all_exec_data") % handle.pid()); INV_MSG( _pimpl->all_exec_data.find(handle.pid()) == _pimpl->all_exec_data.end(), @@ -1150,6 +1342,8 @@ _pimpl->generic, handle); optional< model::test_result > result; + + // test itself try { test_exec_data* test_data = &dynamic_cast< test_exec_data& >( *data.get()); @@ -1185,6 +1379,7 @@ // if the test's body reports a skip (because actions could have // already been taken). test_data->needs_cleanup = false; + test_data->needs_execenv_cleanup = false; } } if (!result) { @@ -1209,7 +1404,6 @@ _pimpl->spawn_cleanup(test_data->test_program, test_data->test_case_name, test_data->user_config, handle, result.get()); - test_data->needs_cleanup = false; // TODO(jmmv): Chaining this call is ugly. We'd be better off by // looping over terminated processes until we got a result suitable @@ -1218,7 +1412,21 @@ // of test cases do not have cleanup routines. return wait_any(); } + + if (test_data->needs_execenv_cleanup) { + INV(test_case.get_metadata().has_execenv()); + _pimpl->spawn_execenv_cleanup(test_data->test_program, + test_data->test_case_name, + handle, result.get()); + test_data->needs_execenv_cleanup = false; + return wait_any(); + } } catch (const std::bad_cast& e) { + // ok, let's check for another type + } + + // test cleanup + try { const cleanup_exec_data* cleanup_data = &dynamic_cast< const cleanup_exec_data& >(*data.get()); LD(F("Got %s from all_exec_data (cleanup)") % handle.original_pid()); @@ -1257,7 +1465,65 @@ _pimpl->all_exec_data.erase(handle.original_pid()); handle = cleanup_data->body_exit_handle; + + const exec_data_map::iterator it = _pimpl->all_exec_data.find( + handle.original_pid()); + if (it != _pimpl->all_exec_data.end()) { + exec_data_ptr d = (*it).second; + test_exec_data* test_data = &dynamic_cast< test_exec_data& >( + *d.get()); + const model::test_case& test_case = + cleanup_data->test_program->find(cleanup_data->test_case_name); + test_data->needs_cleanup = false; + + if (test_data->needs_execenv_cleanup) { + INV(test_case.get_metadata().has_execenv()); + _pimpl->spawn_execenv_cleanup(cleanup_data->test_program, + cleanup_data->test_case_name, + handle, result.get()); + test_data->needs_execenv_cleanup = false; + return wait_any(); + } + } + } catch (const std::bad_cast& e) { + // ok, let's check for another type } + + // execenv cleanup + try { + const execenv_exec_data* execenv_data = + &dynamic_cast< const execenv_exec_data& >(*data.get()); + LD(F("Got %s from all_exec_data (execenv cleanup)") % handle.original_pid()); + + const model::test_result& body_result = execenv_data->body_result; + if (body_result.good()) { + if (!handle.status()) { + result = model::test_result(model::test_result_broken, + "Test case execenv cleanup timed out"); + } else { + if (!handle.status().get().exited() || + handle.status().get().exitstatus() != EXIT_SUCCESS) { + result = model::test_result( + model::test_result_broken, + "Test case execenv cleanup did not terminate successfully"); // ? + } else { + result = body_result; + } + } + } else { + result = body_result; + } + + LD(F("Removing %s from all_exec_data (execenv cleanup) in favor of %s") + % handle.original_pid() + % execenv_data->body_exit_handle.original_pid()); + _pimpl->all_exec_data.erase(handle.original_pid()); + + handle = execenv_data->body_exit_handle; + } catch (const std::bad_cast& e) { + // ok, it was one of the types above + } + INV(result); std::shared_ptr< result_handle::bimpl > result_handle_bimpl( diff --git a/contrib/kyua/engine/tap.cpp b/contrib/kyua/engine/tap.cpp --- a/contrib/kyua/engine/tap.cpp +++ b/contrib/kyua/engine/tap.cpp @@ -35,6 +35,7 @@ #include #include "engine/exceptions.hpp" +#include "engine/execenv/execenv.hpp" #include "engine/tap_parser.hpp" #include "model/test_case.hpp" #include "model/test_program.hpp" @@ -48,6 +49,7 @@ #include "utils/sanity.hpp" namespace config = utils::config; +namespace execenv = engine::execenv; namespace fs = utils::fs; namespace process = utils::process; @@ -151,7 +153,10 @@ } process::args_vector args; - process::exec(test_program.absolute_path(), args); + + auto e = execenv::get(test_program, test_case_name); + e->init(); + e->exec(args); } diff --git a/contrib/kyua/examples/kyua.conf b/contrib/kyua/examples/kyua.conf --- a/contrib/kyua/examples/kyua.conf +++ b/contrib/kyua/examples/kyua.conf @@ -43,6 +43,9 @@ -- Name of the system architecture (aka processor type). architecture = "x86_64" +-- List of execution environments. +execenv = "host jail" + -- Maximum number of jobs (such as test case runs) to execute concurrently. parallelism = 16 diff --git a/contrib/kyua/freebsd/Makefile.am.inc b/contrib/kyua/freebsd/Makefile.am.inc new file mode 100644 --- /dev/null +++ b/contrib/kyua/freebsd/Makefile.am.inc @@ -0,0 +1,46 @@ +# Copyright 2024 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +FREEBSD_CFLAGS = +FREEBSD_LIBS = libfreebsd.a + +noinst_LIBRARIES += libfreebsd.a +libfreebsd_a_CPPFLAGS = -DGDB=\"$(GDB)\" +libfreebsd_a_SOURCES = freebsd/main.hpp +libfreebsd_a_SOURCES += freebsd/main.cpp +libfreebsd_a_SOURCES += freebsd/execenv_jail.hpp +libfreebsd_a_SOURCES += freebsd/execenv_jail_manager.hpp +libfreebsd_a_SOURCES += freebsd/execenv_jail_manager.cpp + +if FreeBSD +FREEBSD_LIBS += -ljail +libfreebsd_a_SOURCES += freebsd/execenv_jail.cpp +include freebsd/utils/Makefile.am.inc +else +libfreebsd_a_SOURCES += freebsd/execenv_jail_stub.cpp +endif diff --git a/contrib/kyua/freebsd/execenv_jail.hpp b/contrib/kyua/freebsd/execenv_jail.hpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/freebsd/execenv_jail.hpp @@ -0,0 +1,64 @@ +// Copyright (c) 2024 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file freebsd/execenv_jail.hpp +/// FreeBSD jail execution environment. + +#if !defined(FREEBSD_EXECENV_JAIL_HPP) +#define FREEBSD_EXECENV_JAIL_HPP + +#include "engine/execenv/execenv.hpp" + +#include "utils/process/operations_fwd.hpp" + +namespace execenv = engine::execenv; + +using utils::process::args_vector; + + +namespace freebsd { + + +extern bool execenv_jail_supported; + + +class execenv_jail : public execenv::interface { +public: + execenv_jail(const model::test_program& test_program, + const std::string& test_case_name) : + execenv::interface(test_program, test_case_name) + {} + + void init() const; + void cleanup() const; + void exec(const args_vector& args) const UTILS_NORETURN; +}; + + +} // namespace freebsd + +#endif // !defined(FREEBSD_EXECENV_JAIL_HPP) diff --git a/contrib/kyua/freebsd/execenv_jail.cpp b/contrib/kyua/freebsd/execenv_jail.cpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/freebsd/execenv_jail.cpp @@ -0,0 +1,76 @@ +// Copyright (c) 2024 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "freebsd/execenv_jail.hpp" + +#include "freebsd/utils/jail.hpp" +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "utils/fs/path.hpp" + +using freebsd::utils::jail; + + +namespace freebsd { + + +bool execenv_jail_supported = true; + + +void +execenv_jail::init() const +{ + auto test_case = _test_program.find(_test_case_name); + + jail().create( + jail().make_name(_test_program.absolute_path(), _test_case_name), + test_case.get_metadata().execenv_jail() + ); +} + + +void +execenv_jail::cleanup() const +{ + jail().remove( + jail().make_name(_test_program.absolute_path(), _test_case_name) + ); +} + + +void +execenv_jail::exec(const args_vector& args) const +{ + jail().exec( + jail().make_name(_test_program.absolute_path(), _test_case_name), + _test_program.absolute_path(), + args + ); +} + + +} // namespace freebsd diff --git a/contrib/kyua/freebsd/execenv_jail_manager.hpp b/contrib/kyua/freebsd/execenv_jail_manager.hpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/freebsd/execenv_jail_manager.hpp @@ -0,0 +1,53 @@ +// Copyright (c) 2024 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file freebsd/execenv_jail_manager.hpp +/// FreeBSD jail execution environment manager. + +#if !defined(FREEBSD_EXECENV_JAIL_MANAGER_HPP) +#define FREEBSD_EXECENV_JAIL_MANAGER_HPP + +#include "engine/execenv/execenv.hpp" + +namespace execenv = engine::execenv; + +namespace freebsd { + + +class execenv_jail_manager : public execenv::manager { +public: + const std::string& name() const; + bool is_supported() const; + std::unique_ptr< execenv::interface > probe( + const model::test_program& test_program, + const std::string& test_case_name) const; +}; + + +} // namespace freebsd + +#endif // !defined(FREEBSD_EXECENV_JAIL_MANAGER_HPP) diff --git a/contrib/kyua/freebsd/execenv_jail_manager.cpp b/contrib/kyua/freebsd/execenv_jail_manager.cpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/freebsd/execenv_jail_manager.cpp @@ -0,0 +1,62 @@ +// Copyright (c) 2024 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "freebsd/execenv_jail_manager.hpp" + +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "freebsd/execenv_jail.hpp" + +static const std::string execenv_name = "jail"; + +const std::string& +freebsd::execenv_jail_manager::name() const +{ + return execenv_name; +} + + +bool +freebsd::execenv_jail_manager::is_supported() const +{ + return freebsd::execenv_jail_supported; +} + + +std::unique_ptr< execenv::interface > +freebsd::execenv_jail_manager::probe( + const model::test_program& test_program, + const std::string& test_case_name) const +{ + auto test_case = test_program.find(test_case_name); + if (test_case.get_metadata().execenv() != execenv_name) + return nullptr; + + return std::unique_ptr< execenv::interface >( + new freebsd::execenv_jail(test_program, test_case_name) + ); +} diff --git a/contrib/kyua/freebsd/execenv_jail_stub.cpp b/contrib/kyua/freebsd/execenv_jail_stub.cpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/freebsd/execenv_jail_stub.cpp @@ -0,0 +1,74 @@ +// Copyright (c) 2024 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "freebsd/execenv_jail.hpp" + +#include + +#include "utils/process/operations_fwd.hpp" + +using utils::process::args_vector; + + +static inline void requires_freebsd(void) UTILS_NORETURN; + +static inline void +requires_freebsd(void) +{ + std::cerr << "execenv=\"jail\" requires FreeBSD with jail feature.\n"; + std::exit(EXIT_FAILURE); +} + + +namespace freebsd { + + +bool execenv_jail_supported = false; + + +void +execenv_jail::init() const +{ + requires_freebsd(); +} + + +void +execenv_jail::cleanup() const +{ + requires_freebsd(); +} + + +void +execenv_jail::exec(const args_vector&) const +{ + requires_freebsd(); +} + + +} // namespace freebsd diff --git a/contrib/kyua/freebsd/main.hpp b/contrib/kyua/freebsd/main.hpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/freebsd/main.hpp @@ -0,0 +1,40 @@ +// Copyright (c) 2024 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file freebsd/main.hpp +/// FreeBSD related features initialization. + +#if !defined(FREEBSD_MAIN_HPP) +#define FREEBSD_MAIN_HPP + +namespace freebsd { + +int main(const int argc, const char* const* const argv); + +} // namespace freebsd + +#endif // !defined(FREEBSD_MAIN_HPP) diff --git a/contrib/kyua/freebsd/main.cpp b/contrib/kyua/freebsd/main.cpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/freebsd/main.cpp @@ -0,0 +1,53 @@ +// Copyright (c) 2024 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "freebsd/main.hpp" + +#include "engine/execenv/execenv.hpp" +#include "freebsd/execenv_jail_manager.hpp" + +namespace execenv = engine::execenv; + +/// FreeBSD related features initialization. +/// +/// \param argc The number of arguments passed on the command line. +/// \param argv NULL-terminated array containing the command line arguments. +/// +/// \return 0 on success, some other integer on error. +/// +/// \throw std::exception This throws any uncaught exception. Such exceptions +/// are bugs, but we let them propagate so that the runtime will abort and +/// dump core. +int +freebsd::main(const int, const char* const* const) +{ + execenv::register_execenv( + std::shared_ptr< execenv::manager >(new freebsd::execenv_jail_manager()) + ); + + return 0; +} diff --git a/contrib/kyua/freebsd/utils/Makefile.am.inc b/contrib/kyua/freebsd/utils/Makefile.am.inc new file mode 100644 --- /dev/null +++ b/contrib/kyua/freebsd/utils/Makefile.am.inc @@ -0,0 +1,30 @@ +# Copyright 2024 The Kyua Authors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of Google Inc. nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +libfreebsd_a_SOURCES += freebsd/utils/jail.hpp +libfreebsd_a_SOURCES += freebsd/utils/jail.cpp diff --git a/contrib/kyua/freebsd/utils/jail.hpp b/contrib/kyua/freebsd/utils/jail.hpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/freebsd/utils/jail.hpp @@ -0,0 +1,64 @@ +// Copyright (c) 2024 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +/// \file freebsd/utils/jail.hpp +/// FreeBSD jail utilities. + +#if !defined(FREEBSD_UTILS_JAIL_HPP) +#define FREEBSD_UTILS_JAIL_HPP + +#include "utils/defs.hpp" +#include "utils/fs/path_fwd.hpp" +#include "utils/process/operations_fwd.hpp" + +namespace fs = utils::fs; + +using utils::process::args_vector; + +namespace freebsd { +namespace utils { + + +// TODO: do we want a singleton here? +class jail { +public: + std::vector< std::string > parse_params_string(const std::string& str); + std::string make_name(const fs::path& program, + const std::string& test_case_name); + void create(const std::string& jail_name, + const std::string& jail_params); + void exec(const std::string& jail_name, + const fs::path& program, + const args_vector& args) throw() UTILS_NORETURN; + void remove(const std::string& jail_name); +}; + + +} // namespace utils +} // namespace freebsd + +#endif // !defined(FREEBSD_UTILS_JAIL_HPP) diff --git a/contrib/kyua/freebsd/utils/jail.cpp b/contrib/kyua/freebsd/utils/jail.cpp new file mode 100644 --- /dev/null +++ b/contrib/kyua/freebsd/utils/jail.cpp @@ -0,0 +1,323 @@ +// Copyright (c) 2024 Igor Ostapenko +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// * Neither the name of Google Inc. nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +#include "freebsd/utils/jail.hpp" + +extern "C" { +#include +#include +#include + +// FreeBSD sysctl facility +#include + +// FreeBSD Jail syscalls +#include +#include + +// FreeBSD Jail library +#include +} + +#include +#include +#include + +#include "model/metadata.hpp" +#include "model/test_case.hpp" +#include "model/test_program.hpp" +#include "utils/fs/path.hpp" +#include "utils/process/child.ipp" +#include "utils/format/macros.hpp" +#include "utils/process/operations.hpp" +#include "utils/process/status.hpp" + +namespace process = utils::process; +namespace fs = utils::fs; + +using utils::process::args_vector; +using utils::process::child; + + +static const int jail_name_max_len = MAXHOSTNAMELEN - 1; +static const char* jail_name_prefix = "kyua"; + + +/// Functor to run a program. +class run { + /// Program binary absolute path. + const utils::fs::path& _program; + + /// Program arguments. + const args_vector& _args; + +public: + /// Constructor. + /// + /// \param program Program binary absolute path. + /// \param args Program arguments. + run( + const utils::fs::path& program, + const args_vector& args) : + _program(program), + _args(args) + { + } + + /// Body of the subprocess. + void + operator()(void) + { + process::exec(_program, _args); + } +}; + + +namespace freebsd { +namespace utils { + + +std::vector< std::string > +jail::parse_params_string(const std::string& str) +{ + std::vector< std::string > params; + std::string p; + char quote = 0; + + for (const char& c : str) { + // whitespace delimited parameter + if (quote == 0) { + if (std::isspace(c)) { + if (p.empty()) + continue; + params.push_back(p); + p = ""; + } + else if (c == '"' || c == '\'') { + if (!p.empty()) + params.push_back(p); + p = ""; + quote = c; + } + else + p += c; + } + + // quoted parameter + else { + if (c == quote) { + if (!p.empty()) + params.push_back(p); + p = ""; + quote = 0; + } + else + p += c; + } + } + + // leftovers + if (!p.empty()) + params.push_back(p); + + return params; +} + + +/// Constructs a jail name based on program and test case. +/// +/// The formula is "kyua" + + "_" + . +/// All non-alphanumeric chars are replaced with "_". +/// +/// If a resulting string exceeds maximum allowed length of a jail name, +/// then it's shortened from the left side keeping the "kyua" prefix. +/// +/// \param program The test program. +/// \param test_case_name Name of the test case. +/// +/// \return A jail name string. +std::string +jail::make_name(const fs::path& program, + const std::string& test_case_name) +{ + std::string name = std::regex_replace( + program.str() + "_" + test_case_name, + std::regex(R"([^A-Za-z0-9_])"), + "_"); + + const std::string::size_type limit = + jail_name_max_len - strlen(jail_name_prefix); + if (name.length() > limit) + name.erase(0, name.length() - limit); + + return jail_name_prefix + name; +} + + +/// Create a jail with a given name and params string. +/// +/// A new jail will always be 'persist', thus the caller is expected to remove +/// the jail eventually via remove(). +/// +/// It's expected to be run in a subprocess. +/// +/// \param jail_name Name of a new jail. +/// \param jail_params String of jail parameters. +void +jail::create(const std::string& jail_name, + const std::string& jail_params) +{ + args_vector av; + + // creation flag + av.push_back("-qc"); + + // jail name + av.push_back("name=" + jail_name); + + // determine maximum allowed children.max + int max; + size_t len = sizeof(max); + if (::sysctlbyname("security.jail.children.max", &max, &len, NULL, 0) != 0) { + std::cerr << "sysctlbyname(security.jail.children.max) errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + if (len < sizeof(max)) { + std::cerr << "sysctlbyname(security.jail.children.max) provides less " + "data (" << len << ") than expected (" << sizeof(max) << ").\n"; + std::exit(EXIT_FAILURE); + } + if (max < 0) { + std::cerr << "sysctlbyname(security.jail.children.max) yields " + "abnormal " << max << ".\n"; + std::exit(EXIT_FAILURE); + } + if (max > 0) + max--; // a child jail must have less than parent's children.max + av.push_back("children.max=" + std::to_string(max)); + + // test defined jail params + const std::vector< std::string > params = parse_params_string(jail_params); + for (const std::string& p : params) + av.push_back(p); + + // it must be persist + av.push_back("persist"); + + // invoke jail + std::auto_ptr< process::child > child = child::fork_capture( + run(fs::path("/usr/sbin/jail"), av)); + process::status status = child->wait(); + + // expect success + if (status.exited() && status.exitstatus() == EXIT_SUCCESS) + return; + + // otherwise, let us know what jail thinks and fail fast + std::cerr << child->output().rdbuf(); + std::exit(EXIT_FAILURE); +} + + +/// Executes an external binary in a jail and replaces the current process. +/// +/// \param jail_name Name of the jail to run within. +/// \param program The test program binary absolute path. +/// \param args The arguments to pass to the binary, without the program name. +void +jail::exec(const std::string& jail_name, + const fs::path& program, + const args_vector& args) throw() +{ + // get work dir prepared by kyua + char cwd[PATH_MAX]; + if (::getcwd(cwd, sizeof(cwd)) == NULL) { + std::cerr << "process::jail::exec: getcwd() errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + + // get jail id by its name + int jid = ::jail_getid(jail_name.c_str()); + if (jid < 0) { + std::cerr << "process::jail::exec: jail_getid() errors: " + << strerror(errno) << ": " << jail_errmsg << ".\n"; + std::exit(EXIT_FAILURE); + } + + // attach to the jail + if (::jail_attach(jid) == -1) { + std::cerr << "process::jail::exec: jail_attach() errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + + // set back the expected work dir + if (::chdir(cwd) == -1) { + std::cerr << "process::jail::exec: chdir() errors: " + << strerror(errno) << ".\n"; + std::exit(EXIT_FAILURE); + } + + process::exec(program, args); +} + + +/// Removes a jail with a given name. +/// +/// It's expected to be run in a subprocess. +/// +/// \param jail_name Name of a jail to remove. +void +jail::remove(const std::string& jail_name) +{ + args_vector av; + + // removal flag + av.push_back("-r"); + + // jail name + av.push_back(jail_name); + + // invoke jail + std::auto_ptr< process::child > child = child::fork_capture( + run(fs::path("/usr/sbin/jail"), av)); + process::status status = child->wait(); + + // expect success + if (status.exited() && status.exitstatus() == EXIT_SUCCESS) + std::exit(EXIT_SUCCESS); + + // otherwise, let us know what jail thinks and fail fast + std::cerr << child->output().rdbuf(); + std::exit(EXIT_FAILURE); +} + + +} // namespace utils +} // namespace freebsd diff --git a/contrib/kyua/integration/cmd_config_test.sh b/contrib/kyua/integration/cmd_config_test.sh --- a/contrib/kyua/integration/cmd_config_test.sh +++ b/contrib/kyua/integration/cmd_config_test.sh @@ -42,6 +42,7 @@ cat >"${HOME}/.kyua/kyua.conf" <expout <("description"); tree.define< config::bool_node >("has_cleanup"); tree.define< config::bool_node >("is_exclusive"); + tree.define< config::string_node >("execenv"); + tree.define< config::string_node >("execenv_jail"); tree.define< config::strings_set_node >("required_configs"); tree.define< bytes_node >("required_disk_space"); tree.define< paths_set_node >("required_files"); @@ -272,6 +274,8 @@ tree.set< config::string_node >("description", ""); tree.set< config::bool_node >("has_cleanup", false); tree.set< config::bool_node >("is_exclusive", false); + tree.set< config::string_node >("execenv", ""); + tree.set< config::string_node >("execenv_jail", ""); tree.set< config::strings_set_node >("required_configs", model::strings_set()); tree.set< bytes_node >("required_disk_space", units::bytes(0)); @@ -493,6 +497,45 @@ } +/// Returns execution environment name. +/// +/// \return Name of configured execution environment. +const std::string& +model::metadata::execenv(void) const +{ + if (_pimpl->props.is_set("execenv")) { + return _pimpl->props.lookup< config::string_node >("execenv"); + } else { + return get_defaults().lookup< config::string_node >("execenv"); + } +} + + +/// Returns whether the test has any specific execenv apart from "host" one. +/// +/// \return True if there is a non-host execenv configured; false otherwise. +bool +model::metadata::has_execenv(void) const +{ + const std::string& name = execenv(); + return !name.empty() && name != "host"; +} + + +/// Returns execenv jail parameters string to run a test with. +/// +/// \return String of jail parameters. +const std::string& +model::metadata::execenv_jail(void) const +{ + if (_pimpl->props.is_set("execenv_jail")) { + return _pimpl->props.lookup< config::string_node >("execenv_jail"); + } else { + return get_defaults().lookup< config::string_node >("execenv_jail"); + } +} + + /// Returns the list of configuration variables needed by the test. /// /// \return Set of configuration variables. @@ -920,6 +963,36 @@ } +/// Sets execution environment name. +/// +/// \param name Execution environment name. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_execenv(const std::string& name) +{ + set< config::string_node >(_pimpl->props, "execenv", name); + return *this; +} + + +/// Sets execenv jail parameters string to run the test with. +/// +/// \param params String of jail parameters. +/// +/// \return A reference to this builder. +/// +/// \throw model::error If the value is invalid. +model::metadata_builder& +model::metadata_builder::set_execenv_jail(const std::string& params) +{ + set< config::string_node >(_pimpl->props, "execenv_jail", params); + return *this; +} + + /// Sets the list of configuration variables needed by the test. /// /// \param vars Set of configuration variables. diff --git a/contrib/kyua/model/metadata_test.cpp b/contrib/kyua/model/metadata_test.cpp --- a/contrib/kyua/model/metadata_test.cpp +++ b/contrib/kyua/model/metadata_test.cpp @@ -315,6 +315,8 @@ props["allowed_platforms"] = ""; props["custom.foo"] = "bar"; props["description"] = ""; + props["execenv"] = ""; + props["execenv_jail"] = ""; props["has_cleanup"] = "false"; props["is_exclusive"] = "false"; props["required_configs"] = ""; @@ -406,7 +408,8 @@ std::ostringstream str; str << model::metadata_builder().build(); ATF_REQUIRE_EQ("metadata{allowed_architectures='', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', " "required_disk_space='0', required_files='', " "required_memory='0', " @@ -428,7 +431,8 @@ .build(); ATF_REQUIRE_EQ( "metadata{allowed_architectures='abc', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='true', " + "description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='true', " "required_configs='', " "required_disk_space='0', required_files='bar foo', " "required_memory='1.00K', " diff --git a/contrib/kyua/model/test_case_test.cpp b/contrib/kyua/model/test_case_test.cpp --- a/contrib/kyua/model/test_case_test.cpp +++ b/contrib/kyua/model/test_case_test.cpp @@ -200,7 +200,8 @@ ATF_REQUIRE_EQ( "test_case{name='the-name', " "metadata=metadata{allowed_architectures='', allowed_platforms='foo', " - "custom.bar='baz', description='', has_cleanup='false', " + "custom.bar='baz', description='', execenv='', execenv_jail='', " + "has_cleanup='false', " "is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " diff --git a/contrib/kyua/model/test_program_test.cpp b/contrib/kyua/model/test_program_test.cpp --- a/contrib/kyua/model/test_program_test.cpp +++ b/contrib/kyua/model/test_program_test.cpp @@ -544,7 +544,8 @@ "test_program{interface='plain', binary='binary/path', " "root='/the/root', test_suite='suite-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}, " @@ -593,21 +594,23 @@ "test_program{interface='plain', binary='binary/path', " "root='/the/root', test_suite='suite-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}, " "test_cases=map(" "another-name=test_case{name='another-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='', " - "description='', has_cleanup='false', is_exclusive='false', " + "description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}}, " "the-name=test_case{name='the-name', " "metadata=metadata{allowed_architectures='a', allowed_platforms='foo', " - "custom.bar='baz', description='', has_cleanup='false', " - "is_exclusive='false', " + "custom.bar='baz', description='', execenv='', execenv_jail='', " + "has_cleanup='false', is_exclusive='false', " "required_configs='', required_disk_space='0', required_files='', " "required_memory='0', " "required_programs='', required_user='', timeout='300'}})}", diff --git a/contrib/kyua/utils/config/nodes.ipp b/contrib/kyua/utils/config/nodes.ipp --- a/contrib/kyua/utils/config/nodes.ipp +++ b/contrib/kyua/utils/config/nodes.ipp @@ -382,9 +382,14 @@ template< typename ValueType > void config::base_set_node< ValueType >::set_lua( - lutok::state& /* state */, - const int /* value_index */) + lutok::state& state, + const int value_index) { + if (state.is_string(value_index)) { + set_string(state.to_string(value_index)); + return; + } + UNREACHABLE; } diff --git a/contrib/kyua/utils/process/executor.hpp b/contrib/kyua/utils/process/executor.hpp --- a/contrib/kyua/utils/process/executor.hpp +++ b/contrib/kyua/utils/process/executor.hpp @@ -215,6 +215,7 @@ exit_handle wait(const exec_handle); exit_handle wait_any(void); + exit_handle reap(const int); void check_interrupt(void) const; }; diff --git a/contrib/kyua/utils/process/executor.cpp b/contrib/kyua/utils/process/executor.cpp --- a/contrib/kyua/utils/process/executor.cpp +++ b/contrib/kyua/utils/process/executor.cpp @@ -689,6 +689,34 @@ data._pimpl->state_owners, all_exec_handles))); } + + executor::exit_handle + reap(const int original_pid) + { + const exec_handles_map::iterator iter = all_exec_handles.find( + original_pid); + exec_handle& data = (*iter).second; + data._pimpl->timer.unprogram(); + + if (!fs::exists(data.stdout_file())) { + std::ofstream new_stdout(data.stdout_file().c_str()); + } + if (!fs::exists(data.stderr_file())) { + std::ofstream new_stderr(data.stderr_file().c_str()); + } + + return exit_handle(std::shared_ptr< exit_handle::impl >( + new exit_handle::impl( + data.pid(), + none, + data._pimpl->unprivileged_user, + data._pimpl->start_time, datetime::timestamp::now(), + data.control_directory(), + data.stdout_file(), + data.stderr_file(), + data._pimpl->state_owners, + all_exec_handles))); + } }; @@ -879,6 +907,20 @@ } +/// Forms exit_handle for the given PID subprocess. +/// +/// Can be used in the cases when we want to do cleanup(s) of a killed test +/// subprocess, but we do not have exit handle as we usually do after normal +/// wait mechanism. +/// +/// \return A pointer to an object describing the subprocess. +executor::exit_handle +executor::executor_handle::reap(const int pid) +{ + return _pimpl->reap(pid); +} + + /// Checks if an interrupt has fired. /// /// Calls to this function should be sprinkled in strategic places through the diff --git a/usr.bin/kyua/Makefile b/usr.bin/kyua/Makefile --- a/usr.bin/kyua/Makefile +++ b/usr.bin/kyua/Makefile @@ -128,7 +128,12 @@ engine/scanner.cpp \ engine/tap.cpp \ engine/tap_parser.cpp \ - engine/scheduler.cpp + engine/scheduler.cpp \ + engine/execenv/execenv.cpp \ + engine/execenv/execenv_host.cpp + +SRCS+= freebsd/execenv_jail_manager.cpp\ + freebsd/main.cpp \ SRCS+= store/dbtypes.cpp \ store/exceptions.cpp \ @@ -161,6 +166,14 @@ cli/config.cpp \ cli/main.cpp +.if ${MK_JAIL} == "no" +SRCS+= freebsd/execenv_jail_stub.cpp +.else +SRCS+= freebsd/execenv_jail.cpp \ + freebsd/utils/jail.cpp +LIBADD+= jail +.endif + FILESGROUPS= DOCS MISC STORE .if ${MK_EXAMPLES} != "no"