Skip to content

Commit

Permalink
Add new sarus commands
Browse files Browse the repository at this point in the history
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
  • Loading branch information
michele-brambilla committed Jul 11, 2024
1 parent 4d80be2 commit f39e22e
Show file tree
Hide file tree
Showing 14 changed files with 368 additions and 12 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions CI/src/integration_tests/test_command_kill.py
Original file line number Diff line number Diff line change
@@ -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")
51 changes: 51 additions & 0 deletions CI/src/integration_tests/test_command_ps.py
Original file line number Diff line number Diff line change
@@ -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()]))
20 changes: 18 additions & 2 deletions CI/src/integration_tests/test_command_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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")


4 changes: 2 additions & 2 deletions CI/src/integration_tests/test_termination_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,15 @@ 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)

os.kill(sarus_process.pid, 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):
Expand Down
7 changes: 5 additions & 2 deletions doc/quickstart/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions doc/user/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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-<random string>``.
.. code-block:: bash
$ sarus run --name=my-container <other options> <image>
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
Expand Down Expand Up @@ -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
Expand Down
90 changes: 90 additions & 0 deletions src/cli/CommandKill.hpp
Original file line number Diff line number Diff line change
@@ -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 <runtime/Runtime.hpp>
#include <runtime/Utility.hpp>

#include <boost/format.hpp>
#include <boost/program_options.hpp>

namespace sarus {
namespace cli {

class CommandKill : public Command {
public:
CommandKill() { }

CommandKill(const libsarus::CLIArguments &args, std::shared_ptr<common::Config> 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<common::Config> conf;
};

} // namespace cli
} // namespace sarus

#endif
4 changes: 4 additions & 0 deletions src/cli/CommandObjectsFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand All @@ -31,10 +33,12 @@ CommandObjectsFactory::CommandObjectsFactory() {
addCommand<cli::CommandHooks>("hooks");
addCommand<cli::CommandImages>("images");
addCommand<cli::CommandLoad>("load");
addCommand<cli::CommandPs>("ps");
addCommand<cli::CommandPull>("pull");
addCommand<cli::CommandRmi>("rmi");
addCommand<cli::CommandRun>("run");
addCommand<cli::CommandSshKeygen>("ssh-keygen");
addCommand<cli::CommandKill>("kill");
addCommand<cli::CommandVersion>("version");
}

Expand Down
Loading

0 comments on commit f39e22e

Please sign in to comment.