From f39e22e168330af6c343d35df980a62628be533f Mon Sep 17 00:00:00 2001 From: Michele Brambilla Date: Thu, 11 Jul 2024 09:54:25 +0000 Subject: [PATCH] Add new sarus commands This commits adds two new commands to sarus * `ps`: list the containers * `kill`: stops and destroy a container In addition * introduces the option `-n, --name` to the `run` command that allows to specify the name of the container * changes the default name of the container from `container-*` to `sarus-container-*` * changes the root path of `crun` from `/run/runc` to `/run/runc/` to ensure container isolation among users --- CHANGELOG.md | 4 +- CI/src/integration_tests/test_command_kill.py | 49 ++++++++++ CI/src/integration_tests/test_command_ps.py | 51 +++++++++++ CI/src/integration_tests/test_command_run.py | 20 ++++- .../test_termination_cleanup.py | 4 +- doc/quickstart/quickstart.rst | 7 +- doc/user/user_guide.rst | 37 ++++++++ src/cli/CommandKill.hpp | 90 +++++++++++++++++++ src/cli/CommandObjectsFactory.cpp | 4 + src/cli/CommandPs.hpp | 69 ++++++++++++++ src/cli/CommandRun.hpp | 10 ++- src/cli/test/test_CLI.cpp | 17 ++++ src/common/Config.hpp | 1 + src/runtime/Runtime.cpp | 17 +++- 14 files changed, 368 insertions(+), 12 deletions(-) create mode 100755 CI/src/integration_tests/test_command_kill.py create mode 100755 CI/src/integration_tests/test_command_ps.py create mode 100644 src/cli/CommandKill.hpp create mode 100644 src/cli/CommandPs.hpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fabdb62..aebb466e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - MPI hook: added support for the environment variable `MPI_COMPATIBILITY_TYPE` that defines the behaviour of the compatibility check of the libraries that the hook mounts. Valid values are `major`, `full` and `strict`. Default value is `major`. - SSH Hook: added a poststop functionality that kills the Dropbear process in case the hook does not join the container's PID namespace. +- Added the `sarus ps` command to list running containers +- Added the `sarus kill` command to terminate (and subsequently remove) containers +- Added the `-n, --name` option the `sarus run` command to specify the name of the container to run. If the option is not specified, Sarus assigns a default name in the form `sarus-container-*`. ### Changed @@ -45,7 +48,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Glibc hook: fixed detection of the container's glibc version, which was causing a shell-init error on some systems - SSH hook: permissions on the container's authorized keys file are now set explicitly, fixing possible errors caused by applying unsuitable defaults from the process. - ## [1.6.3] ### Changed diff --git a/CI/src/integration_tests/test_command_kill.py b/CI/src/integration_tests/test_command_kill.py new file mode 100755 index 00000000..4ac22968 --- /dev/null +++ b/CI/src/integration_tests/test_command_kill.py @@ -0,0 +1,49 @@ +# Sarus +# +# Copyright (c) 2018-2023, ETH Zurich. All rights reserved. +# +# Please, refer to the LICENSE file in the root directory. +# SPDX-License-Identifier: BSD-3-Clause + +import common.util as util +import psutil +import subprocess +import time +import unittest + +from pathlib import Path + + +class TestCommandKill(unittest.TestCase): + + CONTAINER_IMAGE = util.ALPINE_IMAGE + + @classmethod + def setUpClass(cls): + try: + util.pull_image_if_necessary( + is_centralized_repository=False, image=cls.CONTAINER_IMAGE) + except Exception as e: + print(e) + + def test_kill_command_is_defined(self): + try: + subprocess.check_output(["sarus", "help", "kill"]) + except subprocess.CalledProcessError as _: + self.fail("Can't execute command `sarus kill`") + + + def test_kill_deletes_running_container(self): + sarus_process = psutil.Popen(["sarus", "run", "--name", "test_container", self.CONTAINER_IMAGE, "sleep", "5"]) + + time.sleep(2) + sarus_children = sarus_process.children(recursive=True) + self.assertGreater(len(sarus_children), 0, "At least the sleep process must be there") + + psutil.Popen(["sarus", "kill", "test_container"]) + time.sleep(1) + + self.assertFalse(any([p.is_running() for p in sarus_children]), + "Sarus child processes were not cleaned up") + self.assertFalse(list(Path("/sys/fs/cgroup/cpuset").glob("test_container")), + "Cgroup subdir was not cleaned up") diff --git a/CI/src/integration_tests/test_command_ps.py b/CI/src/integration_tests/test_command_ps.py new file mode 100755 index 00000000..bf212f97 --- /dev/null +++ b/CI/src/integration_tests/test_command_ps.py @@ -0,0 +1,51 @@ +# Sarus +# +# Copyright (c) 2018-2023, ETH Zurich. All rights reserved. +# +# Please, refer to the LICENSE file in the root directory. +# SPDX-License-Identifier: BSD-3-Clause + +import common.util as util +import pytest +import psutil +import subprocess +import time +import unittest + + +class TestCommandPs(unittest.TestCase): + + CONTAINER_IMAGE = util.ALPINE_IMAGE + + @classmethod + def setUpClass(cls): + try: + util.pull_image_if_necessary( + is_centralized_repository=False, image=cls.CONTAINER_IMAGE) + except Exception as e: + print(e) + + def test_ps_command_is_defined(self): + try: + subprocess.check_output(["sarus", "help", "ps"]) + except subprocess.CalledProcessError as _: + self.fail("Can't execute command `sarus ps`") + + def test_ps_shows_running_container(self): + sarus_process = psutil.Popen(["sarus", "run", "--name", "test_container", self.CONTAINER_IMAGE, "sleep", "5"]) + time.sleep(2) + output = subprocess.check_output(["sarus", "ps"]).decode() + self.assertGreater(len(output.splitlines()),1) + self.assertTrue(any(["test_container" in line for line in output.splitlines()])) + + @pytest.mark.skip("This test requires to run with a different identity") + def test_ps_hides_running_container_from_other_users(self): + sarus_process = psutil.Popen(["sarus", "run", "--name", "test_container", self.CONTAINER_IMAGE, "sleep", "5"]) + time.sleep(2) + output = subprocess.check_output(["sarus", "ps"], user="janedoe").decode() + + try: + self.assertEqual(len(output.splitlines()), 1) + except AssertionError: + self.assertGreater(len(output.splitlines()), 1) + self.assertFalse(any(["test_container" in line for line in output.splitlines()])) diff --git a/CI/src/integration_tests/test_command_run.py b/CI/src/integration_tests/test_command_run.py index 5d01042e..f6289350 100755 --- a/CI/src/integration_tests/test_command_run.py +++ b/CI/src/integration_tests/test_command_run.py @@ -6,9 +6,15 @@ # SPDX-License-Identifier: BSD-3-Clause import common.util as util +import concurrent.futures import pytest +import psutil +import subprocess +import time import unittest +from pathlib import Path + class TestCommandRun(unittest.TestCase): """ @@ -121,7 +127,17 @@ def _run_ps_in_container(self, with_private_pid_namespace, with_init_process): return processes def _is_repository_metadata_owned_by_user(self): - import os, pathlib - repository_metadata = pathlib.Path(util.get_local_repository_path(), "metadata.json") + import os + repository_metadata = Path(util.get_local_repository_path(), "metadata.json") metadata_stat = repository_metadata.stat() return metadata_stat.st_uid == os.getuid() and metadata_stat.st_gid == os.getgid() + + def test_give_name_to_the_container(self): + util.pull_image_if_necessary(is_centralized_repository=True, image=self.DEFAULT_IMAGE) + + sarus_process = psutil.Popen(["sarus", "run", "--name", "test_container", self.DEFAULT_IMAGE, "sleep", "5"]) + time.sleep(2) + self.assertEqual(len(list(Path("/sys/fs/cgroup/cpuset").glob("test_container"))), 1, + "Could not find cgroup subdir for the container") + + diff --git a/CI/src/integration_tests/test_termination_cleanup.py b/CI/src/integration_tests/test_termination_cleanup.py index dc610af9..e6d69921 100644 --- a/CI/src/integration_tests/test_termination_cleanup.py +++ b/CI/src/integration_tests/test_termination_cleanup.py @@ -81,7 +81,7 @@ def _run_test(self, run_options, commands, sig): # test the runtime process has been created self.assertEqual(len(sarus_process.children()), 1, "Did not find single child process of Sarus") - self.assertEqual(len(list(self.cpuset_cgroup_path.glob("container-*"))), 1, + self.assertEqual(len(list(self.cpuset_cgroup_path.glob("sarus-container-*"))), 1, "Could not find cgroup subdir for the container") self.sarus_children = sarus_process.children(recursive=True) @@ -89,7 +89,7 @@ def _run_test(self, run_options, commands, sig): time.sleep(1) self.assertFalse(any([p.is_running() for p in self.sarus_children]), "Sarus child processes were not cleaned up") - self.assertFalse(list(self.cpuset_cgroup_path.glob("container-*")), + self.assertFalse(list(self.cpuset_cgroup_path.glob("sarus-container-*")), "Cgroup subdir was not cleaned up") def _terminate_or_kill(self, process): diff --git a/doc/quickstart/quickstart.rst b/doc/quickstart/quickstart.rst index 00cb7059..255e48aa 100644 --- a/doc/quickstart/quickstart.rst +++ b/doc/quickstart/quickstart.rst @@ -79,8 +79,11 @@ Now Sarus is ready to be used. Below is a list of the available commands: .. code-block:: bash help: Print help message about a command - images: List images + hooks: List configured hooks + images: List locally available images + kill: Stop and destroy a container load: Load the contents of a tarball to create a filesystem image + ps: List running containers pull: Pull an image from a registry rmi: Remove an image run: Run a command in a new container @@ -109,7 +112,7 @@ Below is an example of some basic usage of Sarus: REPOSITORY TAG IMAGE ID CREATED SIZE SERVER alpine latest a366738a1861 2022-05-25T09:19:59 2.59MB docker.io - $ sarus run alpine cat /etc/os-release + $ sarus run --name quickstart alpine cat /etc/os-release NAME="Alpine Linux" ID=alpine VERSION_ID=3.16.0 diff --git a/doc/user/user_guide.rst b/doc/user/user_guide.rst index 5e83baed..23fe23fe 100644 --- a/doc/user/user_guide.rst +++ b/doc/user/user_guide.rst @@ -629,6 +629,42 @@ To remove images pulled by digest, append the digest to the image name using $ sarus rmi ubuntu@sha256:dcc176d1ab45d154b767be03c703a35fe0df16cfb1cc7ea5dd3b6f9af99b6718 removed image docker.io/library/ubuntu@sha256:dcc176d1ab45d154b767be03c703a35fe0df16cfb1cc7ea5dd3b6f9af99b6718 +Naming the container +-------------------- + +The :program:`sarus run` command line option ``--name`` can be used to assign a custom name to the container. +If the option is not specified, Sarus assigns a name in the form ``sarus-container-``. + +.. code-block:: bash + + $ sarus run --name=my-container + +Kill a container +---------------- + +A running container can be killed, *i.e.* stopped and deleted, using the :program:`sarus kill` command, +for example: + +.. code-block:: bash + + $ sarus kill my-container + +Listing running containers +-------------------------- + +Users can list their currently running containers with the :program:`sarus ps` command. +Containers started by other users are not shown. + +.. code-block:: bash + + $ sarus run --name my-container -t ubuntu:22.04 + ... + + $ sarus ps + ID PID STATUS BUNDLE CREATED OWNER + my-container 651945 running /opt/sarus/default/var/OCIBundleDir 2024-02-19T12:57:26.053166138Z root + + .. _user-environment: Environment @@ -1068,6 +1104,7 @@ To print information about a command (e.g. command-specific options), use --entrypoint arg Overwrite the default ENTRYPOINT of the image --mount arg Mount custom directories into the container -m [ --mpi ] Enable MPI support + -n [ --name ] arg Assign a name to the container --ssh Enable SSH in the container diff --git a/src/cli/CommandKill.hpp b/src/cli/CommandKill.hpp new file mode 100644 index 00000000..899d6289 --- /dev/null +++ b/src/cli/CommandKill.hpp @@ -0,0 +1,90 @@ +/* + * Sarus + * + * Copyright (c) 2018-2023, ETH Zurich. All rights reserved. + * + * Please, refer to the LICENSE file in the root directory. + * SPDX-License-Identifier: BSD-3-Clause + * + */ + +#ifndef cli_CommandStop_hpp +#define cli_CommandStop_hpp + +#include "cli/Command.hpp" +#include "cli/HelpMessage.hpp" +#include "cli/Utility.hpp" + +#include +#include + +#include +#include + +namespace sarus { +namespace cli { + +class CommandKill : public Command { +public: + CommandKill() { } + + CommandKill(const libsarus::CLIArguments &args, std::shared_ptr conf) + : conf{std::move(conf)} { + parseCommandArguments(args); + } + + void execute() override { + libsarus::logMessage(boost::format("kill container: %s") % containerName, libsarus::LogLevel::INFO); + + auto runcPath = conf->json["runcPath"].GetString(); + auto args = libsarus::CLIArguments{runcPath, + "--root", "/run/runc/" + std::to_string(conf->userIdentity.uid), + "kill", containerName, "SIGHUP"}; + + // execute runc + auto status = libsarus::forkExecWait(args); + + if (status != 0) { + auto message = boost::format("%s exited with code %d") % args % status; + libsarus::logMessage(message, libsarus::LogLevel::WARN); + exit(status); + } + }; + + bool requiresRootPrivileges() const override { return true; }; + std::string getBriefDescription() const override { return "Kill a running container"; }; + void printHelpMessage() const override { + auto printer = cli::HelpMessage() + .setUsage("sarus kill [NAME]\n") + .setDescription(getBriefDescription()); + std::cout << printer; + }; + +private: + + void parseCommandArguments(const libsarus::CLIArguments &args) { + cli::utility::printLog("parsing CLI arguments of kill command", libsarus::LogLevel::DEBUG); + + libsarus::CLIArguments nameAndOptionArgs, positionalArgs; + std::tie(nameAndOptionArgs, positionalArgs) = cli::utility::groupOptionsAndPositionalArguments(args, boost::program_options::options_description{}); + + // the kill command expects exactly one positional argument (the container name) + cli::utility::validateNumberOfPositionalArguments(positionalArgs, 1, 1, "kill"); + + try { + containerName = positionalArgs.argv()[0]; + } catch (std::exception &e) { + auto message = boost::format("%s\nSee 'sarus help kill'") % e.what(); + cli::utility::printLog(message, libsarus::LogLevel::GENERAL, std::cerr); + SARUS_THROW_ERROR(message.str(), libsarus::LogLevel::INFO); + } + } + + std::string containerName; + std::shared_ptr conf; +}; + +} // namespace cli +} // namespace sarus + +#endif \ No newline at end of file diff --git a/src/cli/CommandObjectsFactory.cpp b/src/cli/CommandObjectsFactory.cpp index ba934fcc..d4ebfcd5 100644 --- a/src/cli/CommandObjectsFactory.cpp +++ b/src/cli/CommandObjectsFactory.cpp @@ -15,11 +15,13 @@ #include "cli/CommandHelpOfCommand.hpp" #include "cli/CommandHooks.hpp" #include "cli/CommandImages.hpp" +#include "cli/CommandPs.hpp" #include "cli/CommandLoad.hpp" #include "cli/CommandPull.hpp" #include "cli/CommandRmi.hpp" #include "cli/CommandRun.hpp" #include "cli/CommandSshKeygen.hpp" +#include "cli/CommandKill.hpp" #include "cli/CommandVersion.hpp" @@ -31,10 +33,12 @@ CommandObjectsFactory::CommandObjectsFactory() { addCommand("hooks"); addCommand("images"); addCommand("load"); + addCommand("ps"); addCommand("pull"); addCommand("rmi"); addCommand("run"); addCommand("ssh-keygen"); + addCommand("kill"); addCommand("version"); } diff --git a/src/cli/CommandPs.hpp b/src/cli/CommandPs.hpp new file mode 100644 index 00000000..f5462f89 --- /dev/null +++ b/src/cli/CommandPs.hpp @@ -0,0 +1,69 @@ +/* + * Sarus + * + * Copyright (c) 2018-2023, ETH Zurich. All rights reserved. + * + * Please, refer to the LICENSE file in the root directory. + * SPDX-License-Identifier: BSD-3-Clause + * + */ + +#ifndef cli_CommandPs_hpp +#define cli_CommandPs_hpp + +#include "cli/Command.hpp" +#include "cli/HelpMessage.hpp" +#include "cli/Utility.hpp" + +#include +#include + +#include +#include + +namespace sarus { +namespace cli { + +class CommandPs : public Command { +public: + CommandPs() {} + + CommandPs(const libsarus::CLIArguments &args, std::shared_ptr conf) + : conf{std::move(conf)} + {} + + void execute() override { + auto runcPath = conf->json["runcPath"].GetString(); + auto args = libsarus::CLIArguments{runcPath, + "--root", "/run/runc/" + std::to_string(conf->userIdentity.uid), + "list"}; + + // execute runc + auto status = libsarus::forkExecWait(args); + + if (status != 0) { + auto message = boost::format("%s exited with code %d") % args % status; + libsarus::logMessage(message, libsarus::LogLevel::WARN); + exit(status); + } + + }; + + bool requiresRootPrivileges() const override { return true; }; + std::string getBriefDescription() const override { return "List running containers"; } + + void printHelpMessage() const override { + auto printer = cli::HelpMessage() + .setUsage("sarus ps\n") + .setDescription(getBriefDescription()); + std::cout << printer; + }; + + private: + std::shared_ptr conf; +}; + +} // namespace cli +} // namespace sarus + +#endif \ No newline at end of file diff --git a/src/cli/CommandRun.hpp b/src/cli/CommandRun.hpp index 57d738df..a422ca90 100644 --- a/src/cli/CommandRun.hpp +++ b/src/cli/CommandRun.hpp @@ -125,6 +125,10 @@ class CommandRun : public Command { "Enable MPI support for a specific MPI implementation. If no value is supplied, " "Sarus will use the default configured by the administrator. " "Implies '--mpi' and '--glibc'") + ("name,n", + boost::program_options::value(&containerName), + "Assign a name to the container" + ) ("pid", boost::program_options::value(&pid), "Set the PID namespace mode for the container. Supported values: 'host', 'private'. " @@ -206,7 +210,10 @@ class CommandRun : public Command { else { conf->commandRun.useMPI = false; } - + if(values.count("name")) { + conf->commandRun.containerName = containerName; + cli::utility::printLog("name of container: " + containerName, libsarus::LogLevel::DEBUG); + } if(values.count("pid")) { if(pid == std::string{"private"}) { conf->commandRun.createNewPIDNamespace = true; @@ -549,6 +556,7 @@ class CommandRun : public Command { std::vector env; std::string entrypoint; std::string mpiType; + std::string containerName; std::string pid; std::string workdir; }; diff --git a/src/cli/test/test_CLI.cpp b/src/cli/test/test_CLI.cpp index fd6af6bb..aca25b81 100644 --- a/src/cli/test/test_CLI.cpp +++ b/src/cli/test/test_CLI.cpp @@ -25,7 +25,9 @@ #include "cli/CommandHelpOfCommand.hpp" #include "cli/CommandHooks.hpp" #include "cli/CommandImages.hpp" +#include "cli/CommandKill.hpp" #include "cli/CommandLoad.hpp" +#include "cli/CommandPs.hpp" #include "cli/CommandPull.hpp" #include "cli/CommandRmi.hpp" #include "cli/CommandRun.hpp" @@ -83,9 +85,15 @@ TEST(CLITestGroup, CommandTypes) { command = generateCommandFromCLIArguments({"sarus", "images"}); checkCommandDynamicType(*command); + command = generateCommandFromCLIArguments({"sarus", "kill", "name"}); + checkCommandDynamicType(*command); + command = generateCommandFromCLIArguments({"sarus", "load", "archive.tar", "image"}); checkCommandDynamicType(*command); + command = generateCommandFromCLIArguments({"sarus", "ps"}); + checkCommandDynamicType(*command); + command = generateCommandFromCLIArguments({"sarus", "pull", "image"}); checkCommandDynamicType(*command); @@ -405,6 +413,15 @@ TEST(CLITestGroup, generated_config_for_CommandRun) { CHECK_EQUAL(conf->commandRun.execArgs.argv()[1], std::string{"--option1"}); CHECK_EQUAL(conf->commandRun.execArgs.argv()[2], std::string{"-q"}); } + // name + { + auto conf = generateConfig({"run", "image"}); + CHECK_FALSE(conf->commandRun.containerName); + conf = generateConfig({"run", "--name", "test", "image"}); + CHECK_EQUAL(conf->commandRun.containerName.get(), std::string{"test"}); + conf = generateConfig({"run", "-n", "test", "image"}); + CHECK_EQUAL(conf->commandRun.containerName.get(), std::string{"test"}); + } // combined test { auto conf = generateConfig({"run", diff --git a/src/common/Config.hpp b/src/common/Config.hpp index 6a124c27..192d6c31 100644 --- a/src/common/Config.hpp +++ b/src/common/Config.hpp @@ -72,6 +72,7 @@ class Config { boost::optional mpiType; boost::optional workdir; boost::optional entrypoint; + boost::optional containerName; libsarus::CLIArguments execArgs; bool createNewPIDNamespace = false; bool allocatePseudoTTY = false; diff --git a/src/runtime/Runtime.cpp b/src/runtime/Runtime.cpp index b1987112..4e2f3cd0 100644 --- a/src/runtime/Runtime.cpp +++ b/src/runtime/Runtime.cpp @@ -67,8 +67,15 @@ void Runtime::setupOCIBundle() { utility::logMessage("Successfully set up OCI Bundle", libsarus::LogLevel::INFO); } +static std::string getContainerName(const common::Config::CommandRun& commandRun) { + if(commandRun.containerName) { + return commandRun.containerName.get(); + } + return "sarus-container-" + libsarus::generateRandomString(16); +} + void Runtime::executeContainer() const { - auto containerID = "container-" + libsarus::generateRandomString(16); + auto containerID = getContainerName(config->commandRun); utility::logMessage("Executing " + containerID, libsarus::LogLevel::INFO); // chdir to bundle @@ -77,9 +84,11 @@ void Runtime::executeContainer() const { // assemble runc args auto runcPath = config->json["runcPath"].GetString(); auto extraFileDescriptors = std::to_string(fdHandler.getExtraFileDescriptors()); - auto args = libsarus::CLIArguments{runcPath, "run", - "--preserve-fds", extraFileDescriptors, - containerID}; + auto args = libsarus::CLIArguments{runcPath, + "--root", "/run/runc/" + std::to_string(config->userIdentity.uid), + "run", + "--preserve-fds", extraFileDescriptors, + containerID}; // prepare a pre-exec function for the forked process (i.e. the OCI runtime) // to set a parent-death signal, in the attempt to gracefully terminate the container