Skip to content

Commit f39e22e

Browse files
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/<UID>` to ensure container isolation among users
1 parent 4d80be2 commit f39e22e

14 files changed

+368
-12
lines changed

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1111
- MPI hook: added support for the environment variable `MPI_COMPATIBILITY_TYPE` that defines the behaviour of the compatibility check of the libraries
1212
that the hook mounts. Valid values are `major`, `full` and `strict`. Default value is `major`.
1313
- SSH Hook: added a poststop functionality that kills the Dropbear process in case the hook does not join the container's PID namespace.
14+
- Added the `sarus ps` command to list running containers
15+
- Added the `sarus kill` command to terminate (and subsequently remove) containers
16+
- 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-*`.
1417

1518
### Changed
1619

@@ -45,7 +48,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
4548
- Glibc hook: fixed detection of the container's glibc version, which was causing a shell-init error on some systems
4649
- 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.
4750

48-
4951
## [1.6.3]
5052

5153
### Changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Sarus
2+
#
3+
# Copyright (c) 2018-2023, ETH Zurich. All rights reserved.
4+
#
5+
# Please, refer to the LICENSE file in the root directory.
6+
# SPDX-License-Identifier: BSD-3-Clause
7+
8+
import common.util as util
9+
import psutil
10+
import subprocess
11+
import time
12+
import unittest
13+
14+
from pathlib import Path
15+
16+
17+
class TestCommandKill(unittest.TestCase):
18+
19+
CONTAINER_IMAGE = util.ALPINE_IMAGE
20+
21+
@classmethod
22+
def setUpClass(cls):
23+
try:
24+
util.pull_image_if_necessary(
25+
is_centralized_repository=False, image=cls.CONTAINER_IMAGE)
26+
except Exception as e:
27+
print(e)
28+
29+
def test_kill_command_is_defined(self):
30+
try:
31+
subprocess.check_output(["sarus", "help", "kill"])
32+
except subprocess.CalledProcessError as _:
33+
self.fail("Can't execute command `sarus kill`")
34+
35+
36+
def test_kill_deletes_running_container(self):
37+
sarus_process = psutil.Popen(["sarus", "run", "--name", "test_container", self.CONTAINER_IMAGE, "sleep", "5"])
38+
39+
time.sleep(2)
40+
sarus_children = sarus_process.children(recursive=True)
41+
self.assertGreater(len(sarus_children), 0, "At least the sleep process must be there")
42+
43+
psutil.Popen(["sarus", "kill", "test_container"])
44+
time.sleep(1)
45+
46+
self.assertFalse(any([p.is_running() for p in sarus_children]),
47+
"Sarus child processes were not cleaned up")
48+
self.assertFalse(list(Path("/sys/fs/cgroup/cpuset").glob("test_container")),
49+
"Cgroup subdir was not cleaned up")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Sarus
2+
#
3+
# Copyright (c) 2018-2023, ETH Zurich. All rights reserved.
4+
#
5+
# Please, refer to the LICENSE file in the root directory.
6+
# SPDX-License-Identifier: BSD-3-Clause
7+
8+
import common.util as util
9+
import pytest
10+
import psutil
11+
import subprocess
12+
import time
13+
import unittest
14+
15+
16+
class TestCommandPs(unittest.TestCase):
17+
18+
CONTAINER_IMAGE = util.ALPINE_IMAGE
19+
20+
@classmethod
21+
def setUpClass(cls):
22+
try:
23+
util.pull_image_if_necessary(
24+
is_centralized_repository=False, image=cls.CONTAINER_IMAGE)
25+
except Exception as e:
26+
print(e)
27+
28+
def test_ps_command_is_defined(self):
29+
try:
30+
subprocess.check_output(["sarus", "help", "ps"])
31+
except subprocess.CalledProcessError as _:
32+
self.fail("Can't execute command `sarus ps`")
33+
34+
def test_ps_shows_running_container(self):
35+
sarus_process = psutil.Popen(["sarus", "run", "--name", "test_container", self.CONTAINER_IMAGE, "sleep", "5"])
36+
time.sleep(2)
37+
output = subprocess.check_output(["sarus", "ps"]).decode()
38+
self.assertGreater(len(output.splitlines()),1)
39+
self.assertTrue(any(["test_container" in line for line in output.splitlines()]))
40+
41+
@pytest.mark.skip("This test requires to run with a different identity")
42+
def test_ps_hides_running_container_from_other_users(self):
43+
sarus_process = psutil.Popen(["sarus", "run", "--name", "test_container", self.CONTAINER_IMAGE, "sleep", "5"])
44+
time.sleep(2)
45+
output = subprocess.check_output(["sarus", "ps"], user="janedoe").decode()
46+
47+
try:
48+
self.assertEqual(len(output.splitlines()), 1)
49+
except AssertionError:
50+
self.assertGreater(len(output.splitlines()), 1)
51+
self.assertFalse(any(["test_container" in line for line in output.splitlines()]))

CI/src/integration_tests/test_command_run.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,15 @@
66
# SPDX-License-Identifier: BSD-3-Clause
77

88
import common.util as util
9+
import concurrent.futures
910
import pytest
11+
import psutil
12+
import subprocess
13+
import time
1014
import unittest
1115

16+
from pathlib import Path
17+
1218

1319
class TestCommandRun(unittest.TestCase):
1420
"""
@@ -121,7 +127,17 @@ def _run_ps_in_container(self, with_private_pid_namespace, with_init_process):
121127
return processes
122128

123129
def _is_repository_metadata_owned_by_user(self):
124-
import os, pathlib
125-
repository_metadata = pathlib.Path(util.get_local_repository_path(), "metadata.json")
130+
import os
131+
repository_metadata = Path(util.get_local_repository_path(), "metadata.json")
126132
metadata_stat = repository_metadata.stat()
127133
return metadata_stat.st_uid == os.getuid() and metadata_stat.st_gid == os.getgid()
134+
135+
def test_give_name_to_the_container(self):
136+
util.pull_image_if_necessary(is_centralized_repository=True, image=self.DEFAULT_IMAGE)
137+
138+
sarus_process = psutil.Popen(["sarus", "run", "--name", "test_container", self.DEFAULT_IMAGE, "sleep", "5"])
139+
time.sleep(2)
140+
self.assertEqual(len(list(Path("/sys/fs/cgroup/cpuset").glob("test_container"))), 1,
141+
"Could not find cgroup subdir for the container")
142+
143+

CI/src/integration_tests/test_termination_cleanup.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,15 @@ def _run_test(self, run_options, commands, sig):
8181
# test the runtime process has been created
8282
self.assertEqual(len(sarus_process.children()), 1,
8383
"Did not find single child process of Sarus")
84-
self.assertEqual(len(list(self.cpuset_cgroup_path.glob("container-*"))), 1,
84+
self.assertEqual(len(list(self.cpuset_cgroup_path.glob("sarus-container-*"))), 1,
8585
"Could not find cgroup subdir for the container")
8686
self.sarus_children = sarus_process.children(recursive=True)
8787

8888
os.kill(sarus_process.pid, sig)
8989
time.sleep(1)
9090
self.assertFalse(any([p.is_running() for p in self.sarus_children]),
9191
"Sarus child processes were not cleaned up")
92-
self.assertFalse(list(self.cpuset_cgroup_path.glob("container-*")),
92+
self.assertFalse(list(self.cpuset_cgroup_path.glob("sarus-container-*")),
9393
"Cgroup subdir was not cleaned up")
9494

9595
def _terminate_or_kill(self, process):

doc/quickstart/quickstart.rst

+5-2
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,11 @@ Now Sarus is ready to be used. Below is a list of the available commands:
7979
.. code-block:: bash
8080
8181
help: Print help message about a command
82-
images: List images
82+
hooks: List configured hooks
83+
images: List locally available images
84+
kill: Stop and destroy a container
8385
load: Load the contents of a tarball to create a filesystem image
86+
ps: List running containers
8487
pull: Pull an image from a registry
8588
rmi: Remove an image
8689
run: Run a command in a new container
@@ -109,7 +112,7 @@ Below is an example of some basic usage of Sarus:
109112
REPOSITORY TAG IMAGE ID CREATED SIZE SERVER
110113
alpine latest a366738a1861 2022-05-25T09:19:59 2.59MB docker.io
111114
112-
$ sarus run alpine cat /etc/os-release
115+
$ sarus run --name quickstart alpine cat /etc/os-release
113116
NAME="Alpine Linux"
114117
ID=alpine
115118
VERSION_ID=3.16.0

doc/user/user_guide.rst

+37
Original file line numberDiff line numberDiff line change
@@ -629,6 +629,42 @@ To remove images pulled by digest, append the digest to the image name using
629629
$ sarus rmi ubuntu@sha256:dcc176d1ab45d154b767be03c703a35fe0df16cfb1cc7ea5dd3b6f9af99b6718
630630
removed image docker.io/library/ubuntu@sha256:dcc176d1ab45d154b767be03c703a35fe0df16cfb1cc7ea5dd3b6f9af99b6718
631631
632+
Naming the container
633+
--------------------
634+
635+
The :program:`sarus run` command line option ``--name`` can be used to assign a custom name to the container.
636+
If the option is not specified, Sarus assigns a name in the form ``sarus-container-<random string>``.
637+
638+
.. code-block:: bash
639+
640+
$ sarus run --name=my-container <other options> <image>
641+
642+
Kill a container
643+
----------------
644+
645+
A running container can be killed, *i.e.* stopped and deleted, using the :program:`sarus kill` command,
646+
for example:
647+
648+
.. code-block:: bash
649+
650+
$ sarus kill my-container
651+
652+
Listing running containers
653+
--------------------------
654+
655+
Users can list their currently running containers with the :program:`sarus ps` command.
656+
Containers started by other users are not shown.
657+
658+
.. code-block:: bash
659+
660+
$ sarus run --name my-container -t ubuntu:22.04
661+
...
662+
663+
$ sarus ps
664+
ID PID STATUS BUNDLE CREATED OWNER
665+
my-container 651945 running /opt/sarus/default/var/OCIBundleDir 2024-02-19T12:57:26.053166138Z root
666+
667+
632668
.. _user-environment:
633669
634670
Environment
@@ -1068,6 +1104,7 @@ To print information about a command (e.g. command-specific options), use
10681104
--entrypoint arg Overwrite the default ENTRYPOINT of the image
10691105
--mount arg Mount custom directories into the container
10701106
-m [ --mpi ] Enable MPI support
1107+
-n [ --name ] arg Assign a name to the container
10711108
--ssh Enable SSH in the container
10721109
10731110

src/cli/CommandKill.hpp

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/*
2+
* Sarus
3+
*
4+
* Copyright (c) 2018-2023, ETH Zurich. All rights reserved.
5+
*
6+
* Please, refer to the LICENSE file in the root directory.
7+
* SPDX-License-Identifier: BSD-3-Clause
8+
*
9+
*/
10+
11+
#ifndef cli_CommandStop_hpp
12+
#define cli_CommandStop_hpp
13+
14+
#include "cli/Command.hpp"
15+
#include "cli/HelpMessage.hpp"
16+
#include "cli/Utility.hpp"
17+
18+
#include <runtime/Runtime.hpp>
19+
#include <runtime/Utility.hpp>
20+
21+
#include <boost/format.hpp>
22+
#include <boost/program_options.hpp>
23+
24+
namespace sarus {
25+
namespace cli {
26+
27+
class CommandKill : public Command {
28+
public:
29+
CommandKill() { }
30+
31+
CommandKill(const libsarus::CLIArguments &args, std::shared_ptr<common::Config> conf)
32+
: conf{std::move(conf)} {
33+
parseCommandArguments(args);
34+
}
35+
36+
void execute() override {
37+
libsarus::logMessage(boost::format("kill container: %s") % containerName, libsarus::LogLevel::INFO);
38+
39+
auto runcPath = conf->json["runcPath"].GetString();
40+
auto args = libsarus::CLIArguments{runcPath,
41+
"--root", "/run/runc/" + std::to_string(conf->userIdentity.uid),
42+
"kill", containerName, "SIGHUP"};
43+
44+
// execute runc
45+
auto status = libsarus::forkExecWait(args);
46+
47+
if (status != 0) {
48+
auto message = boost::format("%s exited with code %d") % args % status;
49+
libsarus::logMessage(message, libsarus::LogLevel::WARN);
50+
exit(status);
51+
}
52+
};
53+
54+
bool requiresRootPrivileges() const override { return true; };
55+
std::string getBriefDescription() const override { return "Kill a running container"; };
56+
void printHelpMessage() const override {
57+
auto printer = cli::HelpMessage()
58+
.setUsage("sarus kill [NAME]\n")
59+
.setDescription(getBriefDescription());
60+
std::cout << printer;
61+
};
62+
63+
private:
64+
65+
void parseCommandArguments(const libsarus::CLIArguments &args) {
66+
cli::utility::printLog("parsing CLI arguments of kill command", libsarus::LogLevel::DEBUG);
67+
68+
libsarus::CLIArguments nameAndOptionArgs, positionalArgs;
69+
std::tie(nameAndOptionArgs, positionalArgs) = cli::utility::groupOptionsAndPositionalArguments(args, boost::program_options::options_description{});
70+
71+
// the kill command expects exactly one positional argument (the container name)
72+
cli::utility::validateNumberOfPositionalArguments(positionalArgs, 1, 1, "kill");
73+
74+
try {
75+
containerName = positionalArgs.argv()[0];
76+
} catch (std::exception &e) {
77+
auto message = boost::format("%s\nSee 'sarus help kill'") % e.what();
78+
cli::utility::printLog(message, libsarus::LogLevel::GENERAL, std::cerr);
79+
SARUS_THROW_ERROR(message.str(), libsarus::LogLevel::INFO);
80+
}
81+
}
82+
83+
std::string containerName;
84+
std::shared_ptr<common::Config> conf;
85+
};
86+
87+
} // namespace cli
88+
} // namespace sarus
89+
90+
#endif

src/cli/CommandObjectsFactory.cpp

+4
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
#include "cli/CommandHelpOfCommand.hpp"
1616
#include "cli/CommandHooks.hpp"
1717
#include "cli/CommandImages.hpp"
18+
#include "cli/CommandPs.hpp"
1819
#include "cli/CommandLoad.hpp"
1920
#include "cli/CommandPull.hpp"
2021
#include "cli/CommandRmi.hpp"
2122
#include "cli/CommandRun.hpp"
2223
#include "cli/CommandSshKeygen.hpp"
24+
#include "cli/CommandKill.hpp"
2325
#include "cli/CommandVersion.hpp"
2426

2527

@@ -31,10 +33,12 @@ CommandObjectsFactory::CommandObjectsFactory() {
3133
addCommand<cli::CommandHooks>("hooks");
3234
addCommand<cli::CommandImages>("images");
3335
addCommand<cli::CommandLoad>("load");
36+
addCommand<cli::CommandPs>("ps");
3437
addCommand<cli::CommandPull>("pull");
3538
addCommand<cli::CommandRmi>("rmi");
3639
addCommand<cli::CommandRun>("run");
3740
addCommand<cli::CommandSshKeygen>("ssh-keygen");
41+
addCommand<cli::CommandKill>("kill");
3842
addCommand<cli::CommandVersion>("version");
3943
}
4044

0 commit comments

Comments
 (0)