Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

criu-ns: Add tests for criu-ns script #2114

Merged
merged 5 commits into from
May 28, 2023
Merged
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
4 changes: 2 additions & 2 deletions .cirrus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,11 @@ task:
memory: 8G

setup_script: |
# EPEL is needed for python2-future, python2-junit_xml, python-flake8 and libbsd-devel.
# EPEL is needed for python2-future, python2-junit_xml, python-pathlib, python-flake8 and libbsd-devel.
# Do not fail if latest epel repository definition is already installed
yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm || :
ln -sf /usr/include/google/protobuf/descriptor.proto images/google/protobuf/descriptor.proto
yum install -y findutils gcc git gnutls-devel iproute iptables libaio-devel libasan libcap-devel libnet-devel libnl3-devel libbsd-devel make procps-ng protobuf-c-devel protobuf-devel protobuf-python python python-flake8 python-ipaddress python2-future python2-junit_xml python-yaml python-six sudo tar which e2fsprogs python2-pip rubygem-asciidoctor libselinux-devel
yum install -y findutils gcc git gnutls-devel iproute iptables libaio-devel libasan libcap-devel libnet-devel libnl3-devel libbsd-devel make procps-ng protobuf-c-devel protobuf-devel protobuf-python python python-flake8 python-ipaddress python2-future python2-junit_xml python-yaml python-six python-pathlib sudo tar which e2fsprogs python2-pip rubygem-asciidoctor libselinux-devel
# Even with selinux in permissive mode the selinux tests will be executed
# The Cirrus CI user runs as a service from selinux point of view and is
# much more restricted than a normal shell (system_u:system_r:unconfined_service_t:s0)
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ lint:
flake8 --config=scripts/flake8.cfg lib/py/images/pb2dict.py
flake8 --config=scripts/flake8.cfg lib/py/images/images.py
flake8 --config=scripts/flake8.cfg scripts/criu-ns
flake8 --config=scripts/flake8.cfg test/others/criu-ns/run.py
flake8 --config=scripts/flake8.cfg crit/setup.py
flake8 --config=scripts/flake8.cfg scripts/uninstall_module.py
flake8 --config=scripts/flake8.cfg coredump/
Expand Down
1 change: 1 addition & 0 deletions scripts/ci/run-ci-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,7 @@ if [ -n "$TRAVIS" ] || [ -n "$CIRCLECI" ]; then
# Error (criu/tty.c:1014): tty: Don't have tty to inherit session from, aborting
make -C test/others/shell-job/ run
fi
make -C test/others/criu-ns/ run
make -C test/others/skip-file-rwx-check/ run
make -C test/others/rpc/ run

Expand Down
33 changes: 29 additions & 4 deletions scripts/criu-ns
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python3
#!/usr/bin/env python
import ctypes
import ctypes.util
import errno
Expand Down Expand Up @@ -71,7 +71,19 @@ def _wait_for_process_status(criu_pid):
try:
(pid, status) = os.wait()
if pid == criu_pid:
return os.waitstatus_to_exitcode(status)
# The following code block is based on
# os.waitstatus_to_exitcode() introduced in Python 3.9
# and we implement this for comparability with older
# versions of Python.
if os.WIFSIGNALED(status):
warusadura marked this conversation as resolved.
Show resolved Hide resolved
return os.WTERMSIG(status)
elif os.WIFEXITED(status):
return os.WEXITSTATUS(status)
elif os.WIFSTOPPED(status):
return os.WSTOPSIG(status)
else:
raise Exception("CRIU was terminated by an "
"unidentified reason")
warusadura marked this conversation as resolved.
Show resolved Hide resolved
except OSError:
return -251

Expand All @@ -81,8 +93,21 @@ def run_criu(args):
Spawn CRIU binary
"""
print(sys.argv)
os.execlp('criu', *['criu'] + args)
raise OSError(errno.ENOENT, "No such command")

if "--criu-binary" in args:
warusadura marked this conversation as resolved.
Show resolved Hide resolved
try:
opt_index = args.index("--criu-binary")
path = args[opt_index + 1]
del args[opt_index:opt_index + 2]
args.insert(0, "criu")
os.execv(path, args)
raise OSError(errno.ENOENT, "No such command")
except (ValueError, IndexError, FileNotFoundError):
raise OSError(errno.ENOENT, "--criu-binary missing argument")
else:
args.insert(0, "criu")
os.execvp("criu", args)
raise OSError(errno.ENOENT, "No such command")
warusadura marked this conversation as resolved.
Show resolved Hide resolved


# pidns_holder creates a process that is reparented to the init.
Expand Down
2 changes: 1 addition & 1 deletion test/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ all:
$(MAKE) zdtm-freezer
.PHONY: all

TESTS = unix-callback mem-snap rpc libcriu mounts/ext security pipes crit socketpairs overlayfs mnt-ext-dev shell-job skip-file-rwx-check
TESTS = unix-callback mem-snap rpc libcriu mounts/ext security pipes crit socketpairs overlayfs mnt-ext-dev shell-job criu-ns skip-file-rwx-check

other:
for t in $(TESTS); do \
Expand Down
3 changes: 3 additions & 0 deletions test/others/criu-ns/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
run:
warusadura marked this conversation as resolved.
Show resolved Hide resolved
@make -C ../.. zdtm_ct
../../zdtm_ct run.py
258 changes: 258 additions & 0 deletions test/others/criu-ns/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
#!/usr/bin/env python
warusadura marked this conversation as resolved.
Show resolved Hide resolved

import fcntl
import os
import pathlib
import pty
import shutil
import subprocess
import sys
import termios
import time


CRIU_BIN = "../../../criu/criu"
CRIU_NS = "../../../scripts/criu-ns"
IMG_DIR = "dumpdir"
DUMP_LOG = "dump.log"
RESTORE_LOG = "restore.log"
PIDFILE = "pidfile"


def check_dumpdir(path=IMG_DIR):
if os.path.isdir(path):
shutil.rmtree(path)
os.mkdir(path, 0o755)


def set_blocking(fd, blocking):
"""Implement os.set_blocking() for compatibility with Python
versions earlier than 3.5"""
flags = fcntl.fcntl(fd, fcntl.F_GETFL)

if blocking:
flags &= ~os.O_NONBLOCK
else:
flags |= os.O_NONBLOCK

fcntl.fcntl(fd, fcntl.F_SETFL, flags)


def run_task_with_own_pty(task):
fd_m, fd_s = pty.openpty()

pid = os.fork()
if pid == 0:
os.close(fd_m)
os.setsid()
os.dup2(fd_s, 0)
os.dup2(fd_s, 1)
os.dup2(fd_s, 2)
fcntl.ioctl(fd_s, termios.TIOCSCTTY, 1)
os.close(fd_s)
task()
exit(0)

os.close(fd_s)
fd_m = os.fdopen(fd_m, "rb")
set_blocking(fd_m.fileno(), False)

while True:
try:
data = fd_m.read()
except IOError:
break
if data is not None:
print(data.decode("utf-8"))

_, status = os.waitpid(pid, 0)

try:
data = fd_m.read()
except IOError as err:
print(err)

if data is not None:
print(data.decode("utf-8"))
fd_m.close()

if status != 0:
print("task %s exited badly: %d" % (task.__name__, status))
exit(1)

return 0


def create_pty():
fd_m, fd_s = pty.openpty()
return (os.fdopen(fd_m, "wb"), os.fdopen(fd_s, "wb"))


def create_isolated_dumpee():
pathlib.Path("running").touch()
fd_m, fd_s = create_pty()
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
pid = os.fork()
if pid == 0:
os.setsid()
warusadura marked this conversation as resolved.
Show resolved Hide resolved
os.dup2(fd_s.fileno(), 0)
os.dup2(fd_s.fileno(), 1)
os.dup2(fd_s.fileno(), 2)
warusadura marked this conversation as resolved.
Show resolved Hide resolved
fcntl.ioctl(fd_s.fileno(), termios.TIOCSCTTY, 1)
while True:
if not os.access("running", os.F_OK):
sys.exit(0)
time.sleep(1)
fd_m.close()
Snorch marked this conversation as resolved.
Show resolved Hide resolved
fd_s.close()
return pid


def criu_ns_dump(pid, shell_job=False):
cmd = [CRIU_NS, "dump", "-D", IMG_DIR, "-v4", "-t", str(pid),
"--log-file", DUMP_LOG, "--criu-binary", CRIU_BIN]
if shell_job:
cmd.append("--shell-job")
ret = subprocess.Popen(cmd).wait()
warusadura marked this conversation as resolved.
Show resolved Hide resolved
return ret


def criu_ns_restore(shell_job=False, restore_detached=False):
cmd = [CRIU_NS, "restore", "-D", IMG_DIR, "-v4", "--log-file",
RESTORE_LOG, "--criu-binary", CRIU_BIN]
if shell_job:
cmd.append("--shell-job")
if restore_detached:
cmd += ["--restore-detached", "--pidfile", PIDFILE]
ret = subprocess.Popen(cmd).wait()
return ret


def read_log_file(filename):
logfile_path = os.path.join(IMG_DIR, filename)
with open(logfile_path) as logfile:
print(logfile.read())


def test_dump_and_restore_with_shell_job():
print("Test criu-ns dump and restore with --shell-job option")
check_dumpdir()
pathlib.Path("running").touch()
pid = os.fork()
if pid == 0:
while True:
if not os.access("running", os.F_OK):
sys.exit(0)
time.sleep(1)

ret = criu_ns_dump(pid, shell_job=True)
if ret != 0:
read_log_file(DUMP_LOG)
sys.exit(ret)

os.unlink("running")
fd_m, fd_s = create_pty()
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Show resolved Hide resolved
pid = os.fork()
if pid == 0:
os.setsid()
fd_m.close()
# since criu-ns takes control of the tty stdin
os.dup2(fd_s.fileno(), 0)
warusadura marked this conversation as resolved.
Show resolved Hide resolved
ret = criu_ns_restore(shell_job=True)
if ret != 0:
read_log_file(RESTORE_LOG)
sys.exit(ret)
os._exit(0)

fd_s.close()
mihalicyn marked this conversation as resolved.
Show resolved Hide resolved
os.waitpid(pid, 0)


def test_dump_and_restore_without_shell_job(restore_detached=False):
print("Test criu-ns dump and restore with an isolated process"
"(%d)" % restore_detached)
check_dumpdir()
pid = create_isolated_dumpee()
ret = criu_ns_dump(pid)
if ret != 0:
read_log_file(DUMP_LOG)
sys.exit(ret)

if not restore_detached:
os.unlink("running")

pid = os.fork()
if pid == 0:
os.setsid()
warusadura marked this conversation as resolved.
Show resolved Hide resolved
ret = criu_ns_restore(restore_detached=restore_detached)
if ret != 0:
read_log_file(RESTORE_LOG)
sys.exit(ret)
warusadura marked this conversation as resolved.
Show resolved Hide resolved
os._exit(0)

os.waitpid(pid, 0)


def test_dump_and_restore_in_pidns():
if os.system("grep NSpid /proc/self/status"):
return

print("Test criu-ns dump and restore in namespaces")

def _dump():
pid = create_isolated_dumpee()
ret = criu_ns_dump(pid)
if ret != 0:
read_log_file(DUMP_LOG)
sys.exit(ret)

def _restore():
ret = criu_ns_restore(restore_detached=True)
if ret != 0:
read_log_file(RESTORE_LOG)
sys.exit(ret)

def _get_restored_pid():
restored_pid = 0
pidfile_path = os.path.join(IMG_DIR, PIDFILE)
if not os.path.exists(pidfile_path):
raise FileNotFoundError("pidfile not found")
with open(pidfile_path, "r") as pidfile:
restored_pid = pidfile.read().strip()
return int(restored_pid)

def _redump():
global IMG_DIR
try:
restored_pid = _get_restored_pid()
except FileNotFoundError:
sys.exit(1)
IMG_DIR = "dumpdir2"
check_dumpdir(IMG_DIR)
ret = criu_ns_dump(restored_pid)
warusadura marked this conversation as resolved.
Show resolved Hide resolved
if ret != 0:
read_log_file(DUMP_LOG)
sys.exit(ret)

def _re_restore():
Fixed Show fixed Hide fixed
os.unlink("running")
ret = criu_ns_restore()
if ret != 0:
read_log_file(RESTORE_LOG)
sys.exit(ret)

check_dumpdir()
_dump()
_restore()
_redump()
_re_restore()


def main():
test_dump_and_restore_with_shell_job()
test_dump_and_restore_without_shell_job()
test_dump_and_restore_without_shell_job(restore_detached=True)
test_dump_and_restore_in_pidns()


if __name__ == "__main__":
run_task_with_own_pty(main)