Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/L2-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,8 @@ jobs:
- name: Run the l2 test
working-directory: Dobby/tests/L2_testing/test_runner/
run: |
# Regenerate bundles for cgroupv2 compatibility (GitHub Actions uses cgroupv2)
python3 bundle/regenerate_bundles_cgroupv2.py
python3 runner.py -p 3 -v 5
cp $GITHUB_WORKSPACE/Dobby/tests/L2_testing/test_runner/DobbyL2TestResults.json $GITHUB_WORKSPACE

Expand Down
56 changes: 56 additions & 0 deletions bundle/lib/source/DobbySpecConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@
#include <grp.h>
#include <fcntl.h>
#include <limits.h>
#include <mntent.h>
#include <sys/types.h>
#include <sys/sysinfo.h>
#include <sys/capability.h>
#include <sys/stat.h>
#include <sys/vfs.h>
#include <fstream>

// Compile time generated strings that (in theory) speeds up the processing
Expand Down Expand Up @@ -63,6 +65,8 @@ static const ctemplate::StaticTemplateString USERNS_DISABLED =
static const ctemplate::StaticTemplateString MEM_LIMIT =
STS_INIT(MEM_LIMIT, "MEM_LIMIT");

static const ctemplate::StaticTemplateString SWAPPINESS_ENABLED =
STS_INIT(SWAPPINESS_ENABLED, "SWAPPINESS_ENABLED");
static const ctemplate::StaticTemplateString CPU_SHARES_ENABLED =
STS_INIT(CPU_SHARES_ENABLED, "CPU_SHARES_ENABLED");
static const ctemplate::StaticTemplateString CPU_SHARES_VALUE =
Expand Down Expand Up @@ -190,6 +194,53 @@ static const ctemplate::StaticTemplateString SECCOMP_SYSCALLS =

int DobbySpecConfig::mNumCores = -1;


// -----------------------------------------------------------------------------
/**
* @brief Detects whether the system is using cgroup v2 (unified hierarchy).
*
* This checks if /sys/fs/cgroup is mounted as cgroup2 filesystem.
* On cgroupv2, memory.swappiness is not supported in OCI config.
*
* @return true if running on cgroupv2, false otherwise (cgroupv1 or hybrid)
*/
static bool isCgroupV2()
{
static bool checked = false;
static bool isV2 = false;

if (!checked)
{
checked = true;

// Check if /sys/fs/cgroup is mounted as cgroup2
FILE* procMounts = setmntent("/proc/mounts", "r");
if (procMounts != nullptr)
{
struct mntent mntBuf;
struct mntent* mnt;
char buf[PATH_MAX + 256];

while ((mnt = getmntent_r(procMounts, &mntBuf, buf, sizeof(buf))) != nullptr)
{
if (mnt->mnt_dir && strcmp(mnt->mnt_dir, "/sys/fs/cgroup") == 0)
{
if (mnt->mnt_type && strcmp(mnt->mnt_type, "cgroup2") == 0)
{
AI_LOG_INFO("detected cgroup v2 (unified hierarchy)");
isV2 = true;
}
break;
}
}
endmntent(procMounts);
}
}

return isV2;
}


// TODO: should we only allowed these if a network namespace is enabled ?
const std::map<std::string, int> DobbySpecConfig::mAllowedCaps =
{
Expand Down Expand Up @@ -1274,6 +1325,11 @@ bool DobbySpecConfig::processMemLimit(const Json::Value& value,
}

dictionary->SetIntValue(MEM_LIMIT, memLimit);
// Only enable swappiness on cgroupv1 - cgroupv2 doesn't support this in OCI config
if (!isCgroupV2())
{
dictionary->ShowSection(SWAPPINESS_ENABLED);
}

return true;
}
Expand Down
4 changes: 2 additions & 2 deletions bundle/lib/source/templates/OciConfigJson1.0.2-dobby.template
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,8 @@ static const char* ociJsonTemplate = R"JSON(
],
"memory": {
"limit": {{MEM_LIMIT}},
"swap": {{MEM_LIMIT}},
"swappiness": 60
"swap": {{MEM_LIMIT}}{{#SWAPPINESS_ENABLED}},
"swappiness": 60{{/SWAPPINESS_ENABLED}}
},
"cpu": {
{{#CPU_SHARES_ENABLED}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,8 @@ static const char* ociJsonTemplate = R"JSON(
],
"memory": {
"limit": {{MEM_LIMIT}},
"swap": {{MEM_LIMIT}},
"swappiness": 60
"swap": {{MEM_LIMIT}}{{#SWAPPINESS_ENABLED}},
"swappiness": 60{{/SWAPPINESS_ENABLED}}
},
"cpu": {
{{#CPU_SHARES_ENABLED}}
Expand Down
29 changes: 27 additions & 2 deletions daemon/lib/source/DobbyEnv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,18 +165,27 @@ std::map<IDobbyEnv::Cgroup, std::string> DobbyEnv::getCgroupMountPoints()
struct mntent mntBuf;
struct mntent* mnt;
char buf[PATH_MAX + 256];
std::string cgroupV2Path;

while ((mnt = getmntent_r(procMounts, &mntBuf, buf, sizeof(buf))) != nullptr)
{
// skip entries that don't have a mountpount, type or options
if (!mnt->mnt_type || !mnt->mnt_dir || !mnt->mnt_opts)
continue;

// skip non-cgroup mounts
// Check for cgroupv2 (unified hierarchy)
if (strcmp(mnt->mnt_type, "cgroup2") == 0)
{
cgroupV2Path = mnt->mnt_dir;
AI_LOG_INFO("found cgroup2 (unified) mounted @ '%s'", mnt->mnt_dir);
continue;
}

// skip non-cgroup mounts (cgroupv1)
if (strcmp(mnt->mnt_type, "cgroup") != 0)
continue;

// check for the cgroup type
// check for the cgroup type (cgroup v1)
for (const std::pair<const std::string, IDobbyEnv::Cgroup> cgroup : cgroupNames)
{
char* mntopt = hasmntopt(mnt, cgroup.first.c_str());
Expand All @@ -196,6 +205,22 @@ std::map<IDobbyEnv::Cgroup, std::string> DobbyEnv::getCgroupMountPoints()

endmntent(procMounts);


// If cgroupv2 is available and we didn't find cgroupv1 mounts,
// use the unified cgroupv2 path for all cgroup types
if (!cgroupV2Path.empty())
{
for (const auto& cgroup : cgroupNames)
{
if (mounts.find(cgroup.second) == mounts.end())
{
AI_LOG_INFO("using cgroup2 path '%s' for '%s'",
cgroupV2Path.c_str(), cgroup.first.c_str());
mounts[cgroup.second] = cgroupV2Path;
}
}
}

AI_LOG_FN_EXIT();
return mounts;
}
Expand Down
35 changes: 29 additions & 6 deletions daemon/lib/source/DobbyStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

#include <set>
#include <regex>
#include <vector>

#include <sstream>
#include <ext/stdio_filebuf.h>
Expand Down Expand Up @@ -263,26 +264,48 @@ Json::Value DobbyStats::readIonCgroupHeaps(const ContainerId& id,
* @brief Reads a maximum of 4096 bytes from the given cgroup file.
*
* The path to read is made up like: <cgroupMntPath>/<id>/<cgroupfileName>
* For cgroupv2, tries multiple possible paths since containers may be in
* different slices depending on systemd configuration.
*
* @param[in] id The string id of the container.
* @param[in] cgroupMntPath The path to the cgroup mount point.
* @param[in] cgroupfileName The name of the cgroup file.
* @param[out] buf Buffer to store the file contents in
* @param[in] bufLen The size of the buffer.
*
* @return The number of characters copied, or
* @return The number of characters copied, or -1 on failure
*/
ssize_t DobbyStats::readCgroupFile(const ContainerId& id,
const std::string& cgroupMntPath,
const std::string& cgroupfileName,
char* buf, size_t bufLen)
{
std::ostringstream filePath;
filePath << cgroupMntPath << "/" << id.str() << "/" << cgroupfileName;
// Build list of possible cgroup paths to try
// cgroupv1: <mount>/<id>/<file>
// cgroupv2: may be in different slices or directly under mount point
std::vector<std::string> pathsToTry = {
cgroupMntPath + "/" + id.str() + "/" + cgroupfileName,
// cgroupv2 with systemd may put containers in system.slice
cgroupMntPath + "/system.slice/" + id.str() + "/" + cgroupfileName,
// Or user.slice
cgroupMntPath + "/user.slice/" + id.str() + "/" + cgroupfileName,
// Some systems use dobby- prefix
cgroupMntPath + "/system.slice/dobby-" + id.str() + ".scope/" + cgroupfileName,
};

int fd = -1;
std::string successPath;

std::string contents;
for (const auto& path : pathsToTry)
{
fd = open(path.c_str(), O_CLOEXEC | O_RDONLY);
if (fd >= 0)
{
successPath = path;
break;
}
}

int fd = open(filePath.str().c_str(), O_CLOEXEC | O_RDONLY);
if (fd < 0)
{
return -1;
Expand All @@ -296,7 +319,7 @@ ssize_t DobbyStats::readCgroupFile(const ContainerId& id,

if (close(fd) != 0)
{
AI_LOG_SYS_ERROR(errno, "failed to close '%s'", filePath.str().c_str());
AI_LOG_SYS_ERROR(errno, "failed to close '%s'", successPath.c_str());
}

return rd;
Expand Down
47 changes: 24 additions & 23 deletions tests/L2_testing/test_runner/basic_sanity_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from subprocess import check_output
import subprocess
from time import sleep
import multiprocessing
import threading
from os.path import basename

tests = (
Expand Down Expand Up @@ -85,49 +85,50 @@ def execute_test():
return test_utils.count_print_results(output_table)


# we need to do this asynchronous as if there is no such string we would end in endless loop
def read_asynchronous(proc, string_to_find, timeout):
"""Reads asynchronous from process. Ends when found string or timeout occurred.
# Module-level function for multiprocessing compatibility (must be picklable)
def _wait_for_string(proc, string_to_find):
"""Waits indefinitely until string is found in process. Must be run with timeout multiprocess.

Parameters:
proc (process): process in which we want to read
string_to_find (string): what we want to find in process
timeout (float): how long we should wait if string not found (seconds)

Returns:
found (bool): True if found string_to_find inside proc.
None: Returns nothing if found, never ends if not found

"""

# as this function should not be used outside asynchronous read, it is moved inside it
def wait_for_string(proc, string_to_find):
"""Waits indefinitely until string is found in process. Must be run with timeout multiprocess.
while True:
# notice that all data are in stderr not in stdout, this is DobbyDaemon design
output = proc.stderr.readline()
if string_to_find in output:
test_utils.print_log("Found string \"%s\"" % string_to_find, test_utils.Severity.debug)
return


Parameters:
proc (process): process in which we want to read
string_to_find (string): what we want to find in process
# we need to do this asynchronous as if there is no such string we would end in endless loop
def read_asynchronous(proc, string_to_find, timeout):
"""Reads asynchronous from process. Ends when found string or timeout occurred.

Returns:
None: Returns nothing if found, never ends if not found
Parameters:
proc (process): process in which we want to read
string_to_find (string): what we want to find in process
timeout (float): how long we should wait if string not found (seconds)

"""
Returns:
found (bool): True if found string_to_find inside proc.

while True:
# notice that all data are in stderr not in stdout, this is DobbyDaemon design
output = proc.stderr.readline()
if string_to_find in output:
test_utils.print_log("Found string \"%s\"" % string_to_find, test_utils.Severity.debug)
return
"""

found = False
reader = multiprocessing.Process(target=wait_for_string, args=(proc, string_to_find), kwargs={})
reader = threading.Thread(target=_wait_for_string, args=(proc, string_to_find))
test_utils.print_log("Starting multithread read", test_utils.Severity.debug)
reader.start()
reader.join(timeout)
# if thread still running
if reader.is_alive():
test_utils.print_log("Reader still exists, closing", test_utils.Severity.debug)
reader.terminate()
# Note: threads cannot be forcefully terminated, but the main process will continue
test_utils.print_log("Not found string \"%s\"" % string_to_find, test_utils.Severity.error)
else:
found = True
Expand Down
Binary file modified tests/L2_testing/test_runner/bundle/filelogging_bundle.tar.gz
Binary file not shown.
Binary file modified tests/L2_testing/test_runner/bundle/network1_bundle.tar.gz
Binary file not shown.
Binary file modified tests/L2_testing/test_runner/bundle/nolog_bundle.tar.gz
Binary file not shown.
Loading
Loading