diff --git a/Makefile.am b/Makefile.am index c907ae44d3..a5bcebf904 100644 --- a/Makefile.am +++ b/Makefile.am @@ -200,6 +200,7 @@ PYTHON_TESTS = tests/test_capabilities.py \ tests/test_devices.py \ tests/test_hostname.py \ tests/test_limits.py \ + tests/test_oci_features.py \ tests/test_mounts.py \ tests/test_paths.py \ tests/test_pid.py \ diff --git a/tests/test_oci_features.py b/tests/test_oci_features.py new file mode 100644 index 0000000000..aede65a476 --- /dev/null +++ b/tests/test_oci_features.py @@ -0,0 +1,239 @@ +#!/bin/env python3 +# crun - OCI runtime written in C +# +# crun is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# crun is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with crun. If not, see . + +import subprocess +import json +import ctypes +from tests_utils import * + +def is_systemd_enabled(): + try: + subprocess.check_output(["systemctl", "status"]) + return True + except subprocess.CalledProcessError: + return False + +def is_seccomp_enabled(): + try: + with open('/proc/self/status', 'r') as status_file: + for line in status_file: + if line.startswith('Seccomp:'): + value = line.split(':')[1].strip() + return int(value) > 0 + except IOError: + pass + + return False +def get_libseccomp_version(): + # Load the libseccomp library using ctypes + libseccomp = ctypes.CDLL('libseccomp.so') + + # Define the required C function signature + libseccomp.seccomp_version.argtypes = [] + libseccomp.seccomp_version.restype = ctypes.c_uint32 + + # Call the seccomp_version function to get the version + version = libseccomp.seccomp_version() + + # Extract the major, minor, and micro version numbers + major = (version >> 16) & 0xff + minor = (version >> 8) & 0xff + micro = version & 0xff + + # Return the version as a string + return f'{major}.{minor}.{micro}' + +def get_crun_info(): + try: + # Get crun commit + commit = subprocess.check_output(["crun", "--version"]).decode().strip() + + # Get crun version + version = subprocess.check_output(["crun", "--commit"]).decode().strip() + + return commit, version + except subprocess.CalledProcessError: + # Handle error if crun command fails + return None, None + +def test_crun_features(): + try: + output = run_crun_command(["features"]) + features = json.loads(output) + print("Complete features dictionary:", features) + expected_features = { + "ociVersionMin": "1.0.0", + "ociVersionMax": "1.1.0-rc.3", + "hooks": [ + "prestart", + "createRuntime", + "createContainer", + "startContainer", + "poststart", + "poststop" + ], + "mountOptions": [ + "rw", + "rrw", + "ro", + "rro", + "rdirsync", + "rdiratime", + "rnodev", + "rnorelatime", + "nodiratime", + "rnodiratime", + "diratime", + "rnoatime", + "rnomand", + "ratime", + "rmand", + "mand", + "idmap", + "noatime", + "nomand", + "dirsync", + "rnosuid", + "atime", + "rnoexec", + "nodev", + "rbind", + "norelatime", + "bind", + "rnostrictatime", + "strictatime", + "rstrictatime", + "rprivate", + "rsuid", + "remount", + "suid", + "nostrictatime", + "rrelatime", + "nosuid", + "noexec", + "rslave", + "dev", + "rdev", + "rsync", + "relatime", + "sync", + "shared", + "rshared", + "unbindable", + "runbindable", + "defaults", + "async", + "rasync", + "private", + "tmpcopyup", + "rexec", + "exec", + "slave" + ], + "linux": { + "namespaces": [ + "cgroup", + "ipc", + "mount", + "network", + "pid", + "user", + "uts" + ], + "capabilities": [ + ], + "cgroup": { + "v1": True, + "v2": True, + }, + "seccomp": { + "actions": [ + "SCMP_ACT_ALLOW", + "SCMP_ACT_ERRNO", + "SCMP_ACT_KILL", + "SCMP_ACT_KILL_PROCESS", + "SCMP_ACT_KILL_THREAD", + "SCMP_ACT_LOG", + "SCMP_ACT_NOTIFY", + "SCMP_ACT_TRACE", + "SCMP_ACT_TRAP" + ], + "operators": [ + "SCMP_CMP_NE", + "SCMP_CMP_LT", + "SCMP_CMP_LE", + "SCMP_CMP_EQ", + "SCMP_CMP_GE", + "SCMP_CMP_GT", + "SCMP_CMP_MASKED_EQ" + ] + }, + "apparmor": { + "enabled": True + }, + "selinux": { + "enabled": True + } + }, + "annotations": { + "org.opencontainers.runc.checkpoint.enabled": True, + "run.oci.crun.checkpoint.enabled": True, + } + } + + systemd_enabled = is_systemd_enabled() + seccomp_enabled = is_seccomp_enabled() + + if seccomp_enabled: + expected_features["linux"]["seccomp"]["enabled"] = True + expected_features["annotations"]["io.github.seccomp.libseccomp.version"] = get_libseccomp_version() + crun_commit, crun_version = get_crun_info() + expected_features["io.github.containers.crun.commit"] = crun_commit + expected_features["io.github.containers.crun.version"] = crun_version + + + # Check if systemd is enabled and set systemdUser accordingly + if systemd_enabled: + expected_features["linux"]["cgroup"]["systemd"] = True + expected_features["linux"]["cgroup"]["systemdUser"] = True + + for key, value in expected_features.items(): + if key == "linux" and isinstance(value, dict) and "capabilities" in value: + if "capabilities" in features.get("linux", {}): + capabilities = features["linux"]["capabilities"] + if not ("CAP_SYS_ADMIN" in capabilities and "CAP_KILL" in capabilities and "CAP_NET_BIND_SERVICE" in capabilities): + return -1 + continue + + if key not in features or features[key] != value: + print(f"Mismatch in feature: {key}") + print(f"Expected: {value}") + print(f"Actual: {features.get(key)}") + return -1 + + print("All features matched partially!") + return 0 + + except Exception as e: + print("Error running crun features:", str(e)) + return -1 + +all_tests = { + "crun-features" : test_crun_features, +} + +if __name__ == "__main__": + tests_main(all_tests)