[CRIU] [PATCH][RFC] zdtm: Move towards the new generation of criu testing (v2)

Pavel Emelyanov xemul at parallels.com
Mon Oct 5 02:20:06 PDT 2015


Below are two patches. First one is diff from v1 (for easier review), then
goes the full patch.

Changes from previous version

1. Added parallel tests run. TODO: mutex-s for flock tests.

2. Added ability to trace script execution (--debug, replacement for sh -x).

3. Added page-server.

4. Comments

5. Bugfix stopping the tests when smth fails

Things still todo:

1. Exclude tests from run list

2. Only start the test (Q: new action or option to run?)

3. Add non-zdtm tests

4. Fault injection

5. RPC

Thing todo after v1 discussion

1. Rework list of tests to be auto-generated (Christopher suggestion)

Signed-off-by: Pavel Emelyanov <xemul at parallels.com>

---

--- dif-1.txt	2015-10-05 12:13:22.920646412 +0300
+++ dif-2.txt	2015-10-05 12:13:42.921864062 +0300
@@ -10,10 +10,32 @@ import re
 import stat
 import signal
 import atexit
+import sys
+import linecache
 
+prev_line = None
+def traceit(f, e, a):
+	if e == "line":
+		lineno = f.f_lineno
+		fil = f.f_globals["__file__"]
+		if fil.endswith("zdtm.py"):
+			global prev_line
+			line = linecache.getline(fil, lineno)
+			if line == prev_line:
+				print "        ..."
+			else:
+				prev_line = line
+				print "+%4d: %s" % (lineno, line.rstrip())
+
+	return traceit
+
+
+# Descriptor for abstract test not in list
 default_test={ }
+
+# Root dir for ns and uns flavors. All tests
+# sit in the same dir
 zdtm_root = None
-arch = os.uname()[4]
 
 def clean_zdtm_root():
 	global zdtm_root
@@ -27,6 +49,16 @@ def make_zdtm_root():
 		atexit.register(clean_zdtm_root)
 	return zdtm_root
 
+# Arch we run on
+arch = os.uname()[4]
+
+#
+# Flavors
+#  h -- host, test is run in the same set of namespaces as criu
+#  ns -- namespaces, test is run in itw own set of namespaces
+#  uns -- user namespace, the same as above plus user namespace
+#
+
 class host_flavor:
 	def __init__(self, opts):
 		self.name = "host"
@@ -59,6 +91,9 @@ class ns_flavor:
 
 		ldd = subprocess.Popen(["ldd", test_bin], stdout = subprocess.PIPE)
 		xl = re.compile('^(linux-gate.so|linux-vdso(64)?.so|not a dynamic)')
+
+		# This Mayakovsky-style code gets list of libraries a binary
+		# needs minus vdso and gate .so-s
 		libs = map(lambda x: x[1] == '=>' and x[2] or x[0],		\
 				map(lambda x: x.split(),			\
 					filter(lambda x: not xl.match(x),	\
@@ -69,6 +104,8 @@ class ns_flavor:
 		for lib in libs:
 			tlib = self.root + lib
 			if not os.access(tlib, os.F_OK):
+				# Copying should be atomic as tests can be
+				# run in parallel
 				dst = tempfile.mktemp(".tso", "", self.root + os.path.dirname(lib))
 				shutil.copy2(lib, dst)
 				os.rename(dst, tlib)
@@ -85,18 +122,49 @@ class userns_flavor(ns_flavor):
 
 flavors = { 'h': host_flavor, 'ns': ns_flavor, 'uns': userns_flavor }
 
+#
+# Helpers
+#
+
 def tail(path):
 	p = subprocess.Popen(['tail', '-n1', path],
 			stdout = subprocess.PIPE)
 	return p.stdout.readline()
 
+def rpidfile(path):
+	return open(path).readline().strip()
+
+def wait_pid_die(pid, who, tmo = 3):
+	stime = 0.1
+	while stime < tmo:
+		try:
+			os.kill(int(pid), 0)
+		except: # Died
+			break
+
+		print "Wait for %s to die for %f" % (who, stime)
+		time.sleep(stime)
+		stime *= 2
+	else:
+		raise test_fail_exc("%s die" % who)
+
 def test_flag(tdesc, flag):
 	return flag in tdesc.get('flags', '').split()
 
+#
+# Exception thrown when something inside the test goes wrong,
+# e.g. test doesn't start, criu returns with non zero code or
+# test checks fail
+#
+
 class test_fail_exc:
 	def __init__(self, step):
 		self.step = step
 
+#
+# A test from zdtm/ directory.
+#
+
 class zdtm_test:
 	def __init__(self, name, desc, flavor):
 		self.__name = name
@@ -127,18 +195,7 @@ class zdtm_test:
 			return self.__getpath('.pid')
 
 	def __wait_task_die(self):
-		tmo = 0.1
-		while tmo < 3:
-			try:
-				os.kill(int(self.__pid), 0)
-			except: # Died
-				break
-
-			print "Wait for %s to die for %f" % (self.__name, tmo)
-			time.sleep(tmo)
-			tmo *= 2
-		else:
-			raise test_fail_exc("test die")
+		wait_pid_die(int(self.__pid), self.__name)
 
 	def start(self):
 		env = {}
@@ -184,7 +241,7 @@ class zdtm_test:
 
 	def getpid(self):
 		if self.__pid == 0:
-			self.__pid = open(self.__pidfile()).readline().strip()
+			self.__pid = rpidfile(self.__pidfile())
 
 		return self.__pid
 
@@ -203,23 +260,27 @@ class zdtm_test:
 		if force or self.__flavor.ns:
 			os.unlink(self.__pidfile())
 
+#
+# CRIU when launched using CLI
+#
+
 class criu_cli:
-	def __init__(self, test):
+	def __init__(self, test, opts):
 		self.__test = test
 		self.__dump_path = "dump/" + test.getname() + "/" + test.getpid()
 		self.__iter = 0
 		os.makedirs(self.__dump_path)
+		self.__page_server = (opts['page_server'] and True or False)
 
 	def __ddir(self):
 		return os.path.join(self.__dump_path, "%d" % self.__iter)
 
-	def __criu(self, action, opts = [], log = None):
+	def __criu_act(self, action, opts, log = None):
 		if not log:
 			log = action + ".log"
 
 		s_args = ["../criu", action, "-o", log, "-D", self.__ddir(), "-v4"]
 		s_args += opts
-		s_args += self.__test.getcropts()
 
 		print "Run CRIU: [" + " ".join(s_args) + "]"
 		cr = subprocess.Popen(s_args)
@@ -227,6 +288,9 @@ class criu_cli:
 		if ret != 0:
 			raise test_fail_exc("CRIU %s" % action)
 
+	def __criu_cr(self, action, opts):
+		self.__criu_act(action, opts = opts + self.__test.getcropts())
+
 	def dump(self, action, opts = []):
 		self.__iter += 1
 		os.mkdir(self.__ddir())
@@ -235,16 +299,29 @@ class criu_cli:
 		if self.__iter > 1:
 			a_opts += ["--prev-images-dir", "../%d" % (self.__iter - 1), "--track-mem"]
 
-		self.__criu(action, opts = a_opts + opts)
+		if self.__page_server:
+			print "Adding page server"
+			self.__criu_act("page-server", opts = [ "--port", "12345", \
+					"--daemon", "--pidfile", "ps.pid"])
+			a_opts += ["--page-server", "--address", "127.0.0.1", "--port", "12345"]
+
+		self.__criu_cr(action, opts = a_opts + opts)
+
+		if self.__page_server:
+			wait_pid_die(int(rpidfile(self.__ddir() + "/ps.pid")), "page server")
 
 	def restore(self):
-		self.__criu("restore", opts = ["--restore-detached"])
+		self.__criu_cr("restore", opts = ["--restore-detached"])
+
+#
+# Main testing entity -- dump (probably with pre-dumps) and restore
+#
 
 def cr(test, opts):
 	if opts['nocr']:
 		return
 
-	cr_api = criu_cli(test)
+	cr_api = criu_cli(test, opts)
 
 	for i in xrange(0, int(opts['iters'] or 1)):
 		for p in xrange(0, int(opts['pre'] or 0)):
@@ -257,6 +334,8 @@ def cr(test, opts):
 			test.gone()
 			cr_api.restore()
 
+# Additional checks that can be done outside of test process
+
 def get_visible_state(test):
 	fds = os.listdir("/proc/%s/fdinfo" % test.getpid())
 	maps = map(lambda x: x.split()[0], open("/proc/%s/maps" % test.getpid()).readlines())
@@ -269,41 +348,72 @@ def check_visible_state(test, state):
 	if new[1] != state[1]:
 		raise test_fail_exc("maps compare")
 
-def run_test_flav(tname, tdesc, flavor, opts):
-	print "Run %s in %s" % (tname, flavor)
+def do_run_test(tname, tdesc, flavs, opts):
+	print "Run %s in %s" % (tname, flavs)
 
-	f = flavors[flavor](opts)
-	t = zdtm_test(tname, tdesc, f)
-
-	try:
-		t.start()
-		s = get_visible_state(t)
-		cr(t, opts)
-		check_visible_state(t, s)
-		t.stop()
-	except test_fail_exc as e:
-		t.kill()
-		print "Test %s FAIL at %s" % (tname, e.step)
-	else:
-		print "Test %s PASS\n" % tname
+	for f in flavs:
+		flav = flavors[f](opts)
+		t = zdtm_test(tname, tdesc, flav)
 
-def run_test(tname, opts, tlist):
-	global arch
-
-	tdesc = tlist.get(tname, default_test) or default_test
-	if tdesc.get('arch', arch) != arch:
-		print "Test not available for %s" % arch
-		return
+		try:
+			t.start()
+			s = get_visible_state(t)
+			cr(t, opts)
+			check_visible_state(t, s)
+			t.stop()
+		except test_fail_exc as e:
+			t.kill()
+			print "Test %s FAIL at %s" % (tname, e.step)
+			# This exit does two things -- exits from subprocess and
+			# aborts the main script execution on the 1st error met
+			sys.exit(1)
+		else:
+			print "Test %s PASS" % tname
 
-	tflav = tdesc.get('flavor', 'h ns uns').split()
-	rflav = (opts['flavor'] or 'h,ns,uns').split(',')
-	flav = set(tflav) & set(rflav)
-
-	if flav:
-		for f in flav:
-			run_test_flav(tname, tdesc, f, opts)
-	else:
-		print "Selected flavor(s) not supported for test"
+class launcher:
+	def __init__(self, opts):
+		self.__opts = opts
+		self.__max = int(opts['parallel'] or 0)
+		self.__subs = {}
+		self.__fail = False
+
+	def run_test(self, name, desc, flavor):
+		if self.__max == 0:
+			do_run_test(name, desc, flavor, self.__opts)
+			return
+
+		if len(self.__subs) >= self.__max:
+			self.wait()
+			if self.__fail:
+				raise test_fail_exc('')
+
+		nd = ('nocr', 'norst', 'pre', 'iters', 'page_server')
+		arg = repr((name, desc, flavor, { d: self.__opts[d] for d in nd }))
+		log = name.replace('/', '_') + ".log"
+		sub = subprocess.Popen(["zdtm.py"], env = { 'ZDTM_CT_TEST_INFO': arg }, \
+				stdout = open(log, "w"), stderr = subprocess.STDOUT)
+		self.__subs[sub.pid] = { 'sub': sub, 'log': log }
+
+	def __wait_one(self, flags):
+		pid, status = os.waitpid(0, flags)
+		if pid != 0:
+			sub = self.__subs.pop(pid)
+			if status != 0:
+				self.__fail = True
+
+			print open(sub['log']).read()
+			os.unlink(sub['log'])
+
+	def wait(self):
+		self.__wait_one(0)
+		while self.__subs:
+			self.__wait_one(os.WNOHANG)
+
+	def finish(self):
+		while self.__subs:
+			self.__wait_one(0)
+		if self.__fail:
+			sys.exit(1)
 
 def run_tests(opts, tlist):
 	if opts['all']:
@@ -314,8 +424,24 @@ def run_tests(opts, tlist):
 		print "Specify test with -t <name> or -a"
 		return
 
-	for t in torun:
-		run_test(t, opts, tlist)
+	l = launcher(opts)
+	try:
+		for t in torun:
+			global arch
+
+			tdesc = tlist.get(t, default_test) or default_test
+			if tdesc.get('arch', arch) != arch:
+				print "Test %s not available for %s" % (t, arch)
+				continue
+
+			test_flavs = tdesc.get('flavor', 'h ns uns').split()
+			opts_flavs = (opts['flavor'] or 'h,ns,uns').split(',')
+			run_flavs = set(test_flavs) & set(opts_flavs)
+
+			if run_flavs:
+				l.run_test(t, tdesc, run_flavs)
+	finally:
+		l.finish()
 
 def list_tests(opts, tlist):
 	for t in tlist:
@@ -325,7 +451,14 @@ def list_tests(opts, tlist):
 # main() starts here
 #
 
+if os.environ.has_key('ZDTM_CT_TEST_INFO'):
+	tinfo = eval(os.environ['ZDTM_CT_TEST_INFO'])
+	do_run_test(tinfo[0], tinfo[1], tinfo[2], tinfo[3])
+	sys.exit(0)
+
 p = argparse.ArgumentParser("ZDTM test suite")
+p.add_argument("--debug", help = "Print what's being executed", action = 'store_true')
+
 sp = p.add_subparsers(help = "Use --help for list of actions")
 
 rp = sp.add_parser("run", help = "Run test(s)")
@@ -339,10 +472,16 @@ rp.add_argument("--nocr", help = "Do not
 rp.add_argument("--norst", help = "Don't restore tasks, leave them running after dump", action = 'store_true')
 rp.add_argument("--iters", help = "Do CR cycle several times before check")
 
+rp.add_argument("--page-server", help = "Use page server dump", action = 'store_true')
+rp.add_argument("-p", "--parallel", help = "Run test in parallel")
+
 lp = sp.add_parser("list", help = "List tests")
 lp.set_defaults(action = list_tests)
 
 opts = vars(p.parse_args())
 tlist = yaml.load(open("zdtm.list"))
 
+if opts['debug']:
+	sys.settrace(traceit)
+
 opts['action'](opts, tlist)


Now the full patch

Signed-off-by: Pavel Emelyanov <xemul at parallels.com>
---
 test/zdtm.list | 328 ++++++++++++++++++++++++++++++++++++++
 test/zdtm.py   | 487 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 815 insertions(+)
 create mode 100644 test/zdtm.list
 create mode 100755 test/zdtm.py

diff --git a/test/zdtm.list b/test/zdtm.list
new file mode 100644
index 0000000..d1cd184
--- /dev/null
+++ b/test/zdtm.list
@@ -0,0 +1,328 @@
+static/pipe00:
+static/pipe01:
+static/pipe02:
+static/busyloop00:
+static/cwd00:
+static/cwd01:
+static/cwd02:
+static/env00:
+static/maps00:
+static/maps01:
+  flags: suid
+  flavor: h ns
+static/maps02:
+static/maps04:
+static/maps05:
+static/mlock_setuid:
+  flags: suid
+  flavor: h ns
+static/maps_file_prot:
+static/mprotect00:
+static/mtime_mmap:
+static/sleeping00:
+static/write_read00:
+static/write_read01:
+static/write_read02:
+static/write_read10:
+static/wait00:
+static/vdso00:
+static/sched_prio00:
+  flags: suid
+  flavor: h ns
+static/sched_policy00:
+  flags: suid
+  flavor: h ns
+static/file_shared:
+static/file_append:
+static/timers:
+static/posix_timers:
+static/futex:
+static/futex-rl:
+static/xids00:
+static/groups:
+  flags: suid
+static/pthread00:
+static/pthread01:
+static/umask00:
+streaming/pipe_loop00:
+streaming/pipe_shared00:
+transition/file_read:
+static/sockets00:
+  flags: suid
+static/sockets01:
+static/sockets02:
+static/sock_opts00:
+  flags: suid
+static/sock_opts01:
+  flags: suid
+static/sockets_spair:
+static/sockets_dgram:
+static/socket_dgram_data:
+static/socket_queues:
+static/deleted_unix_sock:
+static/sk-unix-unconn:
+static/sk-unix-rel:
+static/pid00:
+  flags: suid
+static/pstree:
+static/caps00:
+  flags: suid
+static/cmdlinenv00:
+  flags: suid
+static/socket_listen:
+static/socket_listen6:
+static/packet_sock:
+  flags: suid
+static/packet_sock_mmap:
+  flags: suid
+static/socket_udp:
+static/sock_filter:
+static/socket6_udp:
+static/socket_udplite:
+static/selfexe00:
+static/link10:
+static/unlink_fstat00:
+static/unlink_fstat01:
+static/unlink_fstat02:
+static/unlink_fstat03:
+  opts: --link-remap
+static/unlink_mmap00:
+static/unlink_mmap01:
+static/unlink_mmap02:
+static/rmdir_open:
+static/eventfs00:
+static/signalfd00:
+static/inotify00:
+  opts: --link-remap
+static/inotify_irmap:
+  flags: suid
+static/fanotify00:
+  flags: suid
+  flavor: h ns
+static/unbound_sock:
+static/fifo-rowo-pair:
+static/fifo-ghost:
+static/fifo:
+static/fifo_wronly:
+static/fifo_ro:
+static/unlink_fifo:
+static/unlink_fifo_wronly:
+static/zombie00:
+static/rlimits00:
+transition/fork:
+transition/fork2:
+transition/thread-bomb:
+static/pty00:
+static/pty01:
+static/pty04:
+static/tty02:
+static/tty03:
+static/console:
+  flags: suid
+  flavor: h ns
+static/vt:
+  flags: suid
+  flavor: h ns
+static/child_opened_proc:
+static/cow01:
+  flags: suid
+  flavor: h ns
+static/pdeath_sig:
+static/fdt_shared:
+static/file_locks00:
+  flags: excl
+  opts: --file-locks
+static/file_locks01:
+  flags: excl
+  opts: --file-locks
+static/file_locks02:
+  flags: excl
+  opts: --file-locks
+static/file_locks03:
+  flags: excl
+  opts: --file-locks
+static/file_locks04:
+  flags: excl
+  opts: --file-locks
+static/file_locks05:
+  flags: excl
+  opts: --file-locks
+static/sigpending:
+static/sigaltstack:
+static/sk-netlink:
+  flags: suid
+static/proc-self:
+static/grow_map:
+static/grow_map02:
+static/grow_map03:
+static/stopped:
+static/chroot:
+  flags: suid
+static/chroot-file:
+  flags: suid
+static/rtc:
+  flags: nouns suid crlib
+transition/maps007:
+  flags: suid
+static/dumpable01:
+static/dumpable02:
+  flavor: h ns
+static/deleted_dev:
+  flags: suid
+  flavor: h ns
+static/fpu00:
+  arch: x86_64
+static/fpu01:
+  arch: x86_64
+static/mmx00:
+  arch: x86_64
+static/sse00:
+  arch: x86_64
+static/sse20:
+  arch: x86_64
+static/vdso01:
+  arch: x86_64
+static/vsx:
+  arch: ppc64le
+static/file_fown:
+  flavor: h
+static/socket-ext:
+  flavor: h
+  opts: --ext-unix-sk
+static/socket-tcp:
+  flavor: h
+  opts: --tcp-established
+static/socket-tcp6:
+  flavor: h
+  opts: --tcp-established
+streaming/socket-tcp:
+  flavor: h
+  opts: --tcp-established
+streaming/socket-tcp6:
+  flavor: h
+  opts: --tcp-established
+static/socket-tcpbuf:
+  flavor: h
+  opts: --tcp-established
+static/socket-tcpbuf-local:
+  flavor: h
+  opts: --tcp-established
+static/socket-tcpbuf6:
+  flavor: h
+  opts: --tcp-established
+static/pty03:
+  flavor: h
+static/mountpoints:
+  flags: suid
+  flavor: h
+static/utsname:
+  flavor: h
+static/ipc_namespace:
+  flavor: h
+static/shm:
+  flavor: h
+static/msgque:
+  flavor: h
+static/sem:
+  flavor: h
+transition/ipc:
+  flavor: h
+static/netns-nf:
+  flavor: h
+static/netns:
+  flavor: h
+static/cgroup00:
+  flags: suid
+  flavor: h
+  opts: --manage-cgroups
+static/cgroup01:
+  flags: suid
+  flavor: h
+  opts: --manage-cgroups
+static/cgroup02:
+  flags: suid
+  flavor: h
+  opts: --manage-cgroups --cgroup-root /newroot --cgroup-root name=zdtmtst:/zdtmtstroot
+static/remap_dead_pid:
+  flavor: h
+static/poll:
+  flavor: h
+static/apparmor:
+  flags: suid
+static/different_creds:
+  flags: suid crfail
+  flavor: h
+static/aio00:
+  feature: aio
+static/timerfd:
+  feature: timerfd
+static/session00:
+  flavor: ns uns
+static/session01:
+  flavor: ns uns
+static/tempfs:
+  flags: suid
+  flavor: ns uns
+static/tempfs_ro:
+  flags: suid
+  flavor: ns
+static/mnt_ro_bind:
+  flags: suid
+  flavor: ns uns
+static/mount_paths:
+  flags: suid
+  flavor: ns uns
+static/bind-mount:
+  flags: suid
+  flavor: ns uns
+static/netns-dev:
+  flags: suid
+  flavor: ns uns
+static/mnt_ext_auto:
+  flavor: ns uns
+  feature: mntid
+  opts: --ext-mount-map auto --enable-external-sharing
+static/mnt_ext_master:
+  flavor: ns uns
+  feature: mntid
+  opts: --ext-mount-map auto --enable-external-masters
+static/mntns_open:
+  flags: suid
+  flavor: ns uns
+  feature: mntid
+static/mntns_link_remap:
+  flags: suid
+  flavor: ns
+  feature: mntid
+  opts: --link-remap
+static/mntns_link_ghost:
+  flags: suid
+  flavor: ns
+  feature: mntid
+static/mntns_shared_bind:
+  flags: suid
+  flavor: ns uns
+  feature: mntid
+static/mntns_shared_bind02:
+  flags: suid
+  flavor: ns uns
+  feature: mntid
+static/mntns_root_bind:
+  flags: suid
+  flavor: ns uns
+  feature: mntid
+static/mntns_deleted:
+  flags: suid
+  flavor: ns uns
+  feature: mntid
+static/tun:
+  flags: suid
+  flavor: ns uns
+  feature: tun
+static/seccomp_strict:
+  flags: suid
+  flavor: h
+  feature: seccomp_suspend
+static/clean_mntns:
+  flags: suid
+  flavor: ns
diff --git a/test/zdtm.py b/test/zdtm.py
new file mode 100755
index 0000000..0e7d3d0
--- /dev/null
+++ b/test/zdtm.py
@@ -0,0 +1,487 @@
+#!/bin/env python
+import argparse
+import yaml
+import os
+import subprocess
+import time
+import tempfile
+import shutil
+import re
+import stat
+import signal
+import atexit
+import sys
+import linecache
+
+prev_line = None
+def traceit(f, e, a):
+	if e == "line":
+		lineno = f.f_lineno
+		fil = f.f_globals["__file__"]
+		if fil.endswith("zdtm.py"):
+			global prev_line
+			line = linecache.getline(fil, lineno)
+			if line == prev_line:
+				print "        ..."
+			else:
+				prev_line = line
+				print "+%4d: %s" % (lineno, line.rstrip())
+
+	return traceit
+
+
+# Descriptor for abstract test not in list
+default_test={ }
+
+# Root dir for ns and uns flavors. All tests
+# sit in the same dir
+zdtm_root = None
+
+def clean_zdtm_root():
+	global zdtm_root
+	if zdtm_root:
+		os.rmdir(zdtm_root)
+
+def make_zdtm_root():
+	global zdtm_root
+	if not zdtm_root:
+		zdtm_root = tempfile.mkdtemp("", "criu-root-", "/tmp")
+		atexit.register(clean_zdtm_root)
+	return zdtm_root
+
+# Arch we run on
+arch = os.uname()[4]
+
+#
+# Flavors
+#  h -- host, test is run in the same set of namespaces as criu
+#  ns -- namespaces, test is run in itw own set of namespaces
+#  uns -- user namespace, the same as above plus user namespace
+#
+
+class host_flavor:
+	def __init__(self, opts):
+		self.name = "host"
+		self.ns = False
+		self.root = None
+
+	def init(self, test_bin):
+		pass
+
+	def fini(self):
+		pass
+
+class ns_flavor:
+	def __init__(self, opts):
+		self.name = "ns"
+		self.ns = True
+		self.uns = False
+		self.root = make_zdtm_root()
+
+	def init(self, test_bin):
+		print "Construct root for %s" % test_bin
+		subprocess.check_call(["mount", "--make-private", "--bind", ".", self.root])
+
+		if not os.access(self.root + "/.constructed", os.F_OK):
+			for dir in ["/bin", "/etc", "/lib", "/lib64", "/dev", "/tmp"]:
+				os.mkdir(self.root + dir)
+
+			os.mknod(self.root + "/dev/tty", stat.S_IFCHR | 0666, os.makedev(5, 0))
+			os.mknod(self.root + "/.constructed", stat.S_IFREG | 0600)
+
+		ldd = subprocess.Popen(["ldd", test_bin], stdout = subprocess.PIPE)
+		xl = re.compile('^(linux-gate.so|linux-vdso(64)?.so|not a dynamic)')
+
+		# This Mayakovsky-style code gets list of libraries a binary
+		# needs minus vdso and gate .so-s
+		libs = map(lambda x: x[1] == '=>' and x[2] or x[0],		\
+				map(lambda x: x.split(),			\
+					filter(lambda x: not xl.match(x),	\
+						map(lambda x: x.strip(),	\
+							filter(lambda x: x.startswith('\t'), ldd.stdout.readlines())))))
+		ldd.wait()
+
+		for lib in libs:
+			tlib = self.root + lib
+			if not os.access(tlib, os.F_OK):
+				# Copying should be atomic as tests can be
+				# run in parallel
+				dst = tempfile.mktemp(".tso", "", self.root + os.path.dirname(lib))
+				shutil.copy2(lib, dst)
+				os.rename(dst, tlib)
+
+	def fini(self):
+		subprocess.check_call(["mount", "--make-private", self.root])
+		subprocess.check_call(["umount", "-l", self.root])
+
+class userns_flavor(ns_flavor):
+	def __init__(self, opts):
+		ns_flavor.__init__(self, opts)
+		self.name = "userns"
+		self.uns = True
+
+flavors = { 'h': host_flavor, 'ns': ns_flavor, 'uns': userns_flavor }
+
+#
+# Helpers
+#
+
+def tail(path):
+	p = subprocess.Popen(['tail', '-n1', path],
+			stdout = subprocess.PIPE)
+	return p.stdout.readline()
+
+def rpidfile(path):
+	return open(path).readline().strip()
+
+def wait_pid_die(pid, who, tmo = 3):
+	stime = 0.1
+	while stime < tmo:
+		try:
+			os.kill(int(pid), 0)
+		except: # Died
+			break
+
+		print "Wait for %s to die for %f" % (who, stime)
+		time.sleep(stime)
+		stime *= 2
+	else:
+		raise test_fail_exc("%s die" % who)
+
+def test_flag(tdesc, flag):
+	return flag in tdesc.get('flags', '').split()
+
+#
+# Exception thrown when something inside the test goes wrong,
+# e.g. test doesn't start, criu returns with non zero code or
+# test checks fail
+#
+
+class test_fail_exc:
+	def __init__(self, step):
+		self.step = step
+
+#
+# A test from zdtm/ directory.
+#
+
+class zdtm_test:
+	def __init__(self, name, desc, flavor):
+		self.__name = name
+		self.__desc = desc
+		self.__make_action('cleanout')
+		self.__pid = 0
+		self.__flavor = flavor
+
+	def __getpath(self, typ = ''):
+		return os.path.join("zdtm/live/", self.__name + typ)
+
+	def __make_action(self, act, env = None, root = None):
+		tpath = self.__getpath('.' + act)
+		s_args = ['make', '--no-print-directory', \
+			 	'-C', os.path.dirname(tpath), \
+				      os.path.basename(tpath)]
+
+		if env:
+			env = dict(os.environ, **env)
+
+		s = subprocess.Popen(s_args, env = env, cwd = root)
+		s.wait()
+
+	def __pidfile(self):
+		if self.__flavor.ns:
+			return self.__getpath('.init.pid')
+		else:
+			return self.__getpath('.pid')
+
+	def __wait_task_die(self):
+		wait_pid_die(int(self.__pid), self.__name)
+
+	def start(self):
+		env = {}
+		self.__flavor.init(self.__getpath())
+
+		print "Start test"
+		if not test_flag(self.__desc, 'suid'):
+			env['ZDTM_UID'] = "18943"
+			env['ZDTM_GID'] = "58467"
+			env['ZDTM_GROUPS'] = "27495 48244"
+		else:
+			print "Test is SUID"
+
+		if self.__flavor.ns:
+			env['ZDTM_NEWNS'] = "1"
+			env['ZDTM_PIDFILE'] = os.path.realpath(self.__getpath('.init.pid'))
+			env['ZDTM_ROOT'] = self.__flavor.root
+
+			if self.__flavor.uns:
+				env['ZDTM_USERNS'] = "1"
+
+		self.__make_action('pid', env, self.__flavor.root)
+
+		try:
+			os.kill(int(self.getpid()), 0)
+		except:
+			raise test_fail_exc("start")
+
+	def kill(self, sig = signal.SIGKILL):
+		if self.__pid:
+			os.kill(int(self.__pid), sig)
+			self.gone(sig == signal.SIGKILL)
+
+		self.__flavor.fini()
+
+	def stop(self):
+		print "Stop test"
+		self.kill(signal.SIGTERM)
+
+		res = tail(self.__getpath('.out'))
+		if not 'PASS' in res.split():
+			raise test_fail_exc("result check")
+
+	def getpid(self):
+		if self.__pid == 0:
+			self.__pid = rpidfile(self.__pidfile())
+
+		return self.__pid
+
+	def getname(self):
+		return self.__name
+
+	def getcropts(self):
+		opts = self.__desc.get('opts', []) + ["--pidfile", os.path.realpath(self.__pidfile())]
+		if self.__flavor.ns:
+			opts += ["--root", self.__flavor.root]
+		return opts
+
+	def gone(self, force = True):
+		self.__wait_task_die()
+		self.__pid = 0
+		if force or self.__flavor.ns:
+			os.unlink(self.__pidfile())
+
+#
+# CRIU when launched using CLI
+#
+
+class criu_cli:
+	def __init__(self, test, opts):
+		self.__test = test
+		self.__dump_path = "dump/" + test.getname() + "/" + test.getpid()
+		self.__iter = 0
+		os.makedirs(self.__dump_path)
+		self.__page_server = (opts['page_server'] and True or False)
+
+	def __ddir(self):
+		return os.path.join(self.__dump_path, "%d" % self.__iter)
+
+	def __criu_act(self, action, opts, log = None):
+		if not log:
+			log = action + ".log"
+
+		s_args = ["../criu", action, "-o", log, "-D", self.__ddir(), "-v4"]
+		s_args += opts
+
+		print "Run CRIU: [" + " ".join(s_args) + "]"
+		cr = subprocess.Popen(s_args)
+		ret = cr.wait()
+		if ret != 0:
+			raise test_fail_exc("CRIU %s" % action)
+
+	def __criu_cr(self, action, opts):
+		self.__criu_act(action, opts = opts + self.__test.getcropts())
+
+	def dump(self, action, opts = []):
+		self.__iter += 1
+		os.mkdir(self.__ddir())
+
+		a_opts = ["-t", self.__test.getpid()]
+		if self.__iter > 1:
+			a_opts += ["--prev-images-dir", "../%d" % (self.__iter - 1), "--track-mem"]
+
+		if self.__page_server:
+			print "Adding page server"
+			self.__criu_act("page-server", opts = [ "--port", "12345", \
+					"--daemon", "--pidfile", "ps.pid"])
+			a_opts += ["--page-server", "--address", "127.0.0.1", "--port", "12345"]
+
+		self.__criu_cr(action, opts = a_opts + opts)
+
+		if self.__page_server:
+			wait_pid_die(int(rpidfile(self.__ddir() + "/ps.pid")), "page server")
+
+	def restore(self):
+		self.__criu_cr("restore", opts = ["--restore-detached"])
+
+#
+# Main testing entity -- dump (probably with pre-dumps) and restore
+#
+
+def cr(test, opts):
+	if opts['nocr']:
+		return
+
+	cr_api = criu_cli(test, opts)
+
+	for i in xrange(0, int(opts['iters'] or 1)):
+		for p in xrange(0, int(opts['pre'] or 0)):
+			cr_api.dump("pre-dump")
+
+		if opts['norst']:
+			cr_api.dump("dump", opts = ["--leave-running"])
+		else:
+			cr_api.dump("dump")
+			test.gone()
+			cr_api.restore()
+
+# Additional checks that can be done outside of test process
+
+def get_visible_state(test):
+	fds = os.listdir("/proc/%s/fdinfo" % test.getpid())
+	maps = map(lambda x: x.split()[0], open("/proc/%s/maps" % test.getpid()).readlines())
+	return (set(fds), set(maps))
+
+def check_visible_state(test, state):
+	new = get_visible_state(test)
+	if new[0] != state[0]:
+		raise test_fail_exc("fds compare")
+	if new[1] != state[1]:
+		raise test_fail_exc("maps compare")
+
+def do_run_test(tname, tdesc, flavs, opts):
+	print "Run %s in %s" % (tname, flavs)
+
+	for f in flavs:
+		flav = flavors[f](opts)
+		t = zdtm_test(tname, tdesc, flav)
+
+		try:
+			t.start()
+			s = get_visible_state(t)
+			cr(t, opts)
+			check_visible_state(t, s)
+			t.stop()
+		except test_fail_exc as e:
+			t.kill()
+			print "Test %s FAIL at %s" % (tname, e.step)
+			# This exit does two things -- exits from subprocess and
+			# aborts the main script execution on the 1st error met
+			sys.exit(1)
+		else:
+			print "Test %s PASS" % tname
+
+class launcher:
+	def __init__(self, opts):
+		self.__opts = opts
+		self.__max = int(opts['parallel'] or 0)
+		self.__subs = {}
+		self.__fail = False
+
+	def run_test(self, name, desc, flavor):
+		if self.__max == 0:
+			do_run_test(name, desc, flavor, self.__opts)
+			return
+
+		if len(self.__subs) >= self.__max:
+			self.wait()
+			if self.__fail:
+				raise test_fail_exc('')
+
+		nd = ('nocr', 'norst', 'pre', 'iters', 'page_server')
+		arg = repr((name, desc, flavor, { d: self.__opts[d] for d in nd }))
+		log = name.replace('/', '_') + ".log"
+		sub = subprocess.Popen(["zdtm.py"], env = { 'ZDTM_CT_TEST_INFO': arg }, \
+				stdout = open(log, "w"), stderr = subprocess.STDOUT)
+		self.__subs[sub.pid] = { 'sub': sub, 'log': log }
+
+	def __wait_one(self, flags):
+		pid, status = os.waitpid(0, flags)
+		if pid != 0:
+			sub = self.__subs.pop(pid)
+			if status != 0:
+				self.__fail = True
+
+			print open(sub['log']).read()
+			os.unlink(sub['log'])
+
+	def wait(self):
+		self.__wait_one(0)
+		while self.__subs:
+			self.__wait_one(os.WNOHANG)
+
+	def finish(self):
+		while self.__subs:
+			self.__wait_one(0)
+		if self.__fail:
+			sys.exit(1)
+
+def run_tests(opts, tlist):
+	if opts['all']:
+		torun = tlist
+	elif opts['test']:
+		torun = opts['test']
+	else:
+		print "Specify test with -t <name> or -a"
+		return
+
+	l = launcher(opts)
+	try:
+		for t in torun:
+			global arch
+
+			tdesc = tlist.get(t, default_test) or default_test
+			if tdesc.get('arch', arch) != arch:
+				print "Test %s not available for %s" % (t, arch)
+				continue
+
+			test_flavs = tdesc.get('flavor', 'h ns uns').split()
+			opts_flavs = (opts['flavor'] or 'h,ns,uns').split(',')
+			run_flavs = set(test_flavs) & set(opts_flavs)
+
+			if run_flavs:
+				l.run_test(t, tdesc, run_flavs)
+	finally:
+		l.finish()
+
+def list_tests(opts, tlist):
+	for t in tlist:
+		print t
+
+#
+# main() starts here
+#
+
+if os.environ.has_key('ZDTM_CT_TEST_INFO'):
+	tinfo = eval(os.environ['ZDTM_CT_TEST_INFO'])
+	do_run_test(tinfo[0], tinfo[1], tinfo[2], tinfo[3])
+	sys.exit(0)
+
+p = argparse.ArgumentParser("ZDTM test suite")
+p.add_argument("--debug", help = "Print what's being executed", action = 'store_true')
+
+sp = p.add_subparsers(help = "Use --help for list of actions")
+
+rp = sp.add_parser("run", help = "Run test(s)")
+rp.set_defaults(action = run_tests)
+rp.add_argument("-a", "--all", action = 'store_true')
+rp.add_argument("-t", "--test", help = "Test name", action = 'append')
+rp.add_argument("-f", "--flavor", help = "Flavor to run")
+
+rp.add_argument("--pre", help = "Do some pre-dumps before dump")
+rp.add_argument("--nocr", help = "Do not CR anything, just check test works", action = 'store_true')
+rp.add_argument("--norst", help = "Don't restore tasks, leave them running after dump", action = 'store_true')
+rp.add_argument("--iters", help = "Do CR cycle several times before check")
+
+rp.add_argument("--page-server", help = "Use page server dump", action = 'store_true')
+rp.add_argument("-p", "--parallel", help = "Run test in parallel")
+
+lp = sp.add_parser("list", help = "List tests")
+lp.set_defaults(action = list_tests)
+
+opts = vars(p.parse_args())
+tlist = yaml.load(open("zdtm.list"))
+
+if opts['debug']:
+	sys.settrace(traceit)
+
+opts['action'](opts, tlist)
-- 
1.9.3




More information about the CRIU mailing list