2021-09-27 18:57:14 +01:00
|
|
|
#!/usr/bin/env python3
|
2016-08-03 18:14:00 +03:00
|
|
|
import ctypes
|
|
|
|
import ctypes.util
|
|
|
|
import errno
|
|
|
|
import sys
|
|
|
|
import os
|
2022-06-22 12:12:07 +03:00
|
|
|
import fcntl
|
|
|
|
import termios
|
2023-04-25 12:40:12 +08:00
|
|
|
import time
|
2016-08-03 18:14:00 +03:00
|
|
|
|
|
|
|
# <sched.h> constants for unshare
|
|
|
|
CLONE_NEWNS = 0x00020000
|
|
|
|
CLONE_NEWPID = 0x20000000
|
|
|
|
|
|
|
|
# <sys/mount.h> - constants for mount
|
|
|
|
MS_REC = 16384
|
|
|
|
MS_PRIVATE = 1 << 18
|
|
|
|
MS_SLAVE = 1 << 19
|
|
|
|
|
|
|
|
# Load libc bindings
|
|
|
|
_libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
|
|
|
|
|
|
|
|
try:
|
2020-12-21 20:12:44 +00:00
|
|
|
_unshare = _libc.unshare
|
2016-08-03 18:14:00 +03:00
|
|
|
except AttributeError:
|
2020-12-21 20:12:44 +00:00
|
|
|
raise OSError(errno.EINVAL, "unshare is not supported on this platform")
|
2016-08-03 18:14:00 +03:00
|
|
|
else:
|
2020-12-21 20:15:05 +00:00
|
|
|
_unshare.argtypes = [ctypes.c_int]
|
2020-12-21 20:12:44 +00:00
|
|
|
_unshare.restype = ctypes.c_int
|
2016-08-03 18:14:00 +03:00
|
|
|
|
|
|
|
try:
|
2020-12-21 20:12:44 +00:00
|
|
|
_setns = _libc.setns
|
2016-08-03 18:14:00 +03:00
|
|
|
except AttributeError:
|
2020-12-21 20:12:44 +00:00
|
|
|
raise OSError(errno.EINVAL, "setns is not supported on this platform")
|
2016-08-03 18:14:00 +03:00
|
|
|
else:
|
2020-12-21 20:15:05 +00:00
|
|
|
_setns.argtypes = [ctypes.c_int, ctypes.c_int]
|
2020-12-21 20:12:44 +00:00
|
|
|
_setns.restype = ctypes.c_int
|
2016-08-03 18:14:00 +03:00
|
|
|
|
|
|
|
try:
|
2020-12-21 20:12:44 +00:00
|
|
|
_mount = _libc.mount
|
2016-08-03 18:14:00 +03:00
|
|
|
except AttributeError:
|
2020-12-21 20:12:44 +00:00
|
|
|
raise OSError(errno.EINVAL, "mount is not supported on this platform")
|
2016-08-03 18:14:00 +03:00
|
|
|
else:
|
2020-12-21 20:12:44 +00:00
|
|
|
_mount.argtypes = [
|
|
|
|
ctypes.c_char_p,
|
|
|
|
ctypes.c_char_p,
|
|
|
|
ctypes.c_char_p,
|
|
|
|
ctypes.c_ulong,
|
|
|
|
ctypes.c_void_p
|
|
|
|
]
|
|
|
|
_mount.restype = ctypes.c_int
|
2016-08-03 18:14:00 +03:00
|
|
|
|
|
|
|
|
2020-12-21 20:25:47 +00:00
|
|
|
def _mount_new_proc():
|
|
|
|
"""
|
|
|
|
Mount new /proc filesystem.
|
|
|
|
"""
|
2021-07-05 07:19:18 +00:00
|
|
|
if _mount(None, b"/", None, MS_SLAVE | MS_REC, None):
|
2020-12-21 20:25:47 +00:00
|
|
|
_errno = ctypes.get_errno()
|
|
|
|
raise OSError(_errno, errno.errorcode[_errno])
|
|
|
|
if _mount(b'proc', b'/proc', b'proc', 0, None):
|
|
|
|
_errno = ctypes.get_errno()
|
|
|
|
raise OSError(_errno, errno.errorcode[_errno])
|
|
|
|
|
|
|
|
|
2020-12-21 20:31:50 +00:00
|
|
|
def _wait_for_process_status(criu_pid):
|
|
|
|
"""
|
|
|
|
Wait for CRIU to exit and report the status back.
|
|
|
|
"""
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
(pid, status) = os.wait()
|
|
|
|
if pid == criu_pid:
|
2022-02-04 20:41:07 +00:00
|
|
|
return os.waitstatus_to_exitcode(status)
|
2020-12-21 20:31:50 +00:00
|
|
|
except OSError:
|
|
|
|
return -251
|
|
|
|
|
|
|
|
|
2020-12-22 11:57:58 +00:00
|
|
|
def run_criu(args):
|
2020-12-21 20:50:18 +00:00
|
|
|
"""
|
|
|
|
Spawn CRIU binary
|
|
|
|
"""
|
2020-12-21 20:12:44 +00:00
|
|
|
print(sys.argv)
|
2023-04-04 13:54:11 +05:30
|
|
|
|
|
|
|
if "--criu-binary" in args:
|
|
|
|
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")
|
2016-08-03 18:14:00 +03:00
|
|
|
|
|
|
|
|
2022-03-20 00:43:45 -07:00
|
|
|
# pidns_holder creates a process that is reparented to the init.
|
|
|
|
#
|
|
|
|
# The init process can exit if it doesn't have any child processes and its
|
|
|
|
# pidns is destroyed in this case. CRIU dump is running in the target pid
|
|
|
|
# namespace and it kills dumped processes at the end. We need to create a
|
|
|
|
# holder process to be sure that the pid namespace will not be destroy before
|
|
|
|
# criu exits.
|
|
|
|
def pidns_holder():
|
|
|
|
r, w = os.pipe()
|
|
|
|
pid = os.fork()
|
|
|
|
if pid == 0:
|
|
|
|
pid = os.fork()
|
|
|
|
if pid == 0:
|
|
|
|
os.close(w)
|
|
|
|
# The write end is owned by the parent process and it is closed by
|
|
|
|
# kernel when the parent process exits.
|
|
|
|
os.read(r, 1)
|
|
|
|
sys.exit(0)
|
|
|
|
os.waitpid(pid, 0)
|
|
|
|
|
|
|
|
|
2016-08-03 18:14:00 +03:00
|
|
|
def wrap_restore():
|
2020-12-22 12:26:03 +00:00
|
|
|
restore_args = sys.argv[1:]
|
|
|
|
if '--restore-sibling' in restore_args:
|
|
|
|
raise OSError(errno.EINVAL, "--restore-sibling is not supported")
|
|
|
|
|
2023-04-25 12:40:12 +08:00
|
|
|
# Unshare pid namespace
|
|
|
|
if _unshare(CLONE_NEWPID) != 0:
|
2020-12-21 20:12:44 +00:00
|
|
|
_errno = ctypes.get_errno()
|
|
|
|
raise OSError(_errno, errno.errorcode[_errno])
|
|
|
|
|
2020-12-22 12:15:54 +00:00
|
|
|
restore_detached = False
|
|
|
|
if '-d' in restore_args:
|
|
|
|
restore_detached = True
|
|
|
|
restore_args.remove('-d')
|
|
|
|
if '--restore-detached' in restore_args:
|
|
|
|
restore_detached = True
|
|
|
|
restore_args.remove('--restore-detached')
|
|
|
|
|
2023-04-25 12:40:12 +08:00
|
|
|
restore_pidfile = None
|
|
|
|
if '--pidfile' in restore_args:
|
|
|
|
try:
|
|
|
|
opt_index = restore_args.index('--pidfile')
|
|
|
|
restore_pidfile = restore_args[opt_index + 1]
|
|
|
|
del restore_args[opt_index:opt_index + 2]
|
|
|
|
except (ValueError, IndexError, FileNotFoundError):
|
|
|
|
raise OSError(errno.ENOENT, "--pidfile missing argument")
|
|
|
|
|
|
|
|
if not restore_pidfile.startswith('/'):
|
|
|
|
for base_dir_opt in ['--work-dir', '-W', '--images-dir', '-D']:
|
|
|
|
if base_dir_opt in restore_args:
|
|
|
|
try:
|
|
|
|
opt_index = restore_args.index(base_dir_opt)
|
|
|
|
restore_pidfile = os.path.join(restore_args[opt_index + 1], restore_pidfile)
|
|
|
|
break
|
|
|
|
except (ValueError, IndexError, FileNotFoundError):
|
|
|
|
raise OSError(errno.ENOENT, base_dir_opt + " missing argument")
|
|
|
|
|
2020-12-21 20:12:44 +00:00
|
|
|
criu_pid = os.fork()
|
|
|
|
if criu_pid == 0:
|
2023-04-25 12:40:12 +08:00
|
|
|
# Unshare mount namespace
|
|
|
|
if _unshare(CLONE_NEWNS) != 0:
|
|
|
|
_errno = ctypes.get_errno()
|
|
|
|
raise OSError(_errno, errno.errorcode[_errno])
|
|
|
|
|
2021-10-18 18:43:14 +03:00
|
|
|
os.setsid()
|
2022-06-22 12:12:07 +03:00
|
|
|
# Set stdin tty to be a controlling tty of our new session, this is
|
|
|
|
# required by --shell-job option, as for it CRIU would try to set a
|
|
|
|
# process group of restored root task to be a foreground group on the
|
|
|
|
# terminal.
|
|
|
|
if '--shell-job' in restore_args or '-j' in restore_args:
|
|
|
|
if os.isatty(sys.stdin.fileno()):
|
|
|
|
fcntl.ioctl(sys.stdin.fileno(), termios.TIOCSCTTY, 1)
|
|
|
|
else:
|
|
|
|
raise OSError(errno.EINVAL, 'The stdin is not a tty for a --shell-job')
|
|
|
|
|
2020-12-21 20:25:47 +00:00
|
|
|
_mount_new_proc()
|
2020-12-22 12:15:54 +00:00
|
|
|
run_criu(restore_args)
|
|
|
|
|
2023-04-25 12:40:12 +08:00
|
|
|
if restore_pidfile:
|
|
|
|
restored_pid = None
|
|
|
|
retry = 5
|
|
|
|
|
|
|
|
while not restored_pid and retry:
|
|
|
|
with open('/proc/%d/task/%d/children' % (criu_pid, criu_pid)) as f:
|
|
|
|
line = f.readline().strip()
|
|
|
|
if len(line):
|
|
|
|
restored_pid = line
|
|
|
|
break
|
|
|
|
retry -= 1
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
if restored_pid:
|
|
|
|
with open(restore_pidfile, 'w+') as f:
|
|
|
|
f.write(restored_pid)
|
|
|
|
else:
|
|
|
|
print("Warn: Search of restored pid for --pidfile option timeouted")
|
|
|
|
|
2020-12-22 12:15:54 +00:00
|
|
|
if restore_detached:
|
|
|
|
return 0
|
|
|
|
|
2020-12-21 20:31:50 +00:00
|
|
|
return _wait_for_process_status(criu_pid)
|
2016-08-03 18:14:00 +03:00
|
|
|
|
|
|
|
|
|
|
|
def get_varg(args):
|
2020-12-21 20:12:44 +00:00
|
|
|
for i in range(1, len(sys.argv)):
|
|
|
|
if not sys.argv[i] in args:
|
|
|
|
continue
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2020-12-21 20:12:44 +00:00
|
|
|
if i + 1 >= len(sys.argv):
|
|
|
|
break
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2020-12-21 20:12:44 +00:00
|
|
|
return (sys.argv[i + 1], i + 1)
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2020-12-21 20:12:44 +00:00
|
|
|
return (None, None)
|
2016-08-03 18:14:00 +03:00
|
|
|
|
|
|
|
|
2020-12-22 11:46:03 +00:00
|
|
|
def _set_namespace(fd):
|
|
|
|
"""Join namespace referred to by fd"""
|
|
|
|
if _setns(fd, 0) != 0:
|
|
|
|
_errno = ctypes.get_errno()
|
|
|
|
raise OSError(_errno, errno.errorcode[_errno])
|
|
|
|
|
|
|
|
|
2022-05-30 15:57:07 -04:00
|
|
|
def is_my_namespace(fd, ns):
|
2020-12-22 11:46:03 +00:00
|
|
|
"""Returns True if fd refers to current namespace"""
|
2022-05-30 15:57:07 -04:00
|
|
|
return os.stat('/proc/self/ns/%s' % ns).st_ino == os.fstat(fd).st_ino
|
2020-12-22 11:46:03 +00:00
|
|
|
|
2016-08-03 18:14:00 +03:00
|
|
|
|
|
|
|
def set_pidns(tpid, pid_idx):
|
2020-12-21 20:50:18 +00:00
|
|
|
"""
|
|
|
|
Join pid namespace. Note, that the given pid should
|
|
|
|
be changed in -t option, as task lives in different
|
|
|
|
pid namespace.
|
|
|
|
"""
|
2020-12-21 20:12:44 +00:00
|
|
|
ns_fd = os.open('/proc/%s/ns/pid' % tpid, os.O_RDONLY)
|
2022-05-30 15:57:07 -04:00
|
|
|
if not is_my_namespace(ns_fd, "pid"):
|
2021-07-05 07:19:18 +00:00
|
|
|
for line in open('/proc/%s/status' % tpid):
|
|
|
|
if not line.startswith('NSpid:'):
|
2020-12-21 20:12:44 +00:00
|
|
|
continue
|
2021-07-05 07:19:18 +00:00
|
|
|
ls = line.split()
|
2020-12-21 20:12:44 +00:00
|
|
|
if ls[1] != tpid:
|
2021-01-17 13:05:42 +00:00
|
|
|
os.close(ns_fd)
|
2020-12-21 20:12:44 +00:00
|
|
|
raise OSError(errno.ESRCH, 'No such pid')
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2020-12-21 20:12:44 +00:00
|
|
|
print('Replace pid {} with {}'.format(tpid, ls[2]))
|
|
|
|
sys.argv[pid_idx] = ls[2]
|
|
|
|
break
|
|
|
|
else:
|
2021-01-17 13:05:42 +00:00
|
|
|
os.close(ns_fd)
|
2020-12-21 20:12:44 +00:00
|
|
|
raise OSError(errno.ENOENT, 'Cannot find NSpid field in proc')
|
2020-12-22 11:46:03 +00:00
|
|
|
_set_namespace(ns_fd)
|
2020-12-21 20:12:44 +00:00
|
|
|
os.close(ns_fd)
|
2016-08-03 18:14:00 +03:00
|
|
|
|
|
|
|
|
|
|
|
def set_mntns(tpid):
|
2020-12-21 20:50:18 +00:00
|
|
|
"""
|
|
|
|
Join mount namespace. Trick here too -- check / and .
|
|
|
|
will be the same in target mntns.
|
|
|
|
"""
|
2020-12-21 20:12:44 +00:00
|
|
|
ns_fd = os.open('/proc/%s/ns/mnt' % tpid, os.O_RDONLY)
|
2022-05-30 15:57:07 -04:00
|
|
|
if not is_my_namespace(ns_fd, "mnt"):
|
2020-12-21 20:12:44 +00:00
|
|
|
root_st = os.stat('/')
|
|
|
|
cwd_st = os.stat('.')
|
|
|
|
cwd_path = os.path.realpath('.')
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2020-12-22 11:46:03 +00:00
|
|
|
_set_namespace(ns_fd)
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2020-12-21 20:12:44 +00:00
|
|
|
os.chdir(cwd_path)
|
|
|
|
root_nst = os.stat('/')
|
|
|
|
cwd_nst = os.stat('.')
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2020-12-21 20:12:44 +00:00
|
|
|
def steq(st, nst):
|
|
|
|
return (st.st_dev, st.st_ino) == (nst.st_dev, nst.st_ino)
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2020-12-21 20:12:44 +00:00
|
|
|
if not steq(root_st, root_nst):
|
2021-01-17 13:05:42 +00:00
|
|
|
os.close(ns_fd)
|
2020-12-21 20:12:44 +00:00
|
|
|
raise OSError(errno.EXDEV, 'Target ns / is not as current')
|
|
|
|
if not steq(cwd_st, cwd_nst):
|
2021-01-17 13:05:42 +00:00
|
|
|
os.close(ns_fd)
|
2020-12-21 20:12:44 +00:00
|
|
|
raise OSError(errno.EXDEV, 'Target ns . is not as current')
|
|
|
|
os.close(ns_fd)
|
2016-08-03 18:14:00 +03:00
|
|
|
|
|
|
|
|
|
|
|
def wrap_dump():
|
2020-12-21 20:12:44 +00:00
|
|
|
(pid, pid_idx) = get_varg(('-t', '--tree'))
|
|
|
|
if pid is None:
|
|
|
|
raise OSError(errno.EINVAL, 'No --tree option given')
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2020-12-21 20:12:44 +00:00
|
|
|
set_pidns(pid, pid_idx)
|
|
|
|
set_mntns(pid)
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2022-03-20 00:43:45 -07:00
|
|
|
pidns_holder()
|
|
|
|
|
2020-12-21 20:12:44 +00:00
|
|
|
criu_pid = os.fork()
|
|
|
|
if criu_pid == 0:
|
2020-12-22 11:57:58 +00:00
|
|
|
run_criu(sys.argv[1:])
|
2022-02-01 13:44:42 +00:00
|
|
|
return _wait_for_process_status(criu_pid)
|
2016-08-03 18:14:00 +03:00
|
|
|
|
|
|
|
|
2021-07-26 21:21:59 +01:00
|
|
|
def show_usage():
|
2020-12-21 20:12:44 +00:00
|
|
|
print("""
|
2019-04-07 20:43:12 +01:00
|
|
|
Usage:
|
|
|
|
{0} dump|pre-dump -t PID [<options>]
|
|
|
|
{0} restore [<options>]
|
|
|
|
\nCommands:
|
|
|
|
dump checkpoint a process/tree identified by pid
|
|
|
|
pre-dump pre-dump task(s) minimizing their frozen time
|
|
|
|
restore restore a process/tree
|
2021-07-26 21:39:07 +01:00
|
|
|
check checks whether the kernel support is up-to-date
|
2019-04-07 20:43:12 +01:00
|
|
|
""".format(sys.argv[0]))
|
|
|
|
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2021-07-26 21:21:59 +01:00
|
|
|
if __name__ == "__main__":
|
|
|
|
if len(sys.argv) == 1:
|
|
|
|
show_usage()
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
action = sys.argv[1]
|
|
|
|
if action == 'restore':
|
|
|
|
res = wrap_restore()
|
|
|
|
elif action in ['dump', 'pre-dump']:
|
|
|
|
res = wrap_dump()
|
2021-07-26 21:39:07 +01:00
|
|
|
elif action == 'check':
|
|
|
|
run_criu(sys.argv[1:])
|
2021-07-26 21:21:59 +01:00
|
|
|
else:
|
|
|
|
print('Unsupported action {} for criu-ns'.format(action))
|
|
|
|
res = -1
|
2016-08-03 18:14:00 +03:00
|
|
|
|
2021-07-26 21:21:59 +01:00
|
|
|
sys.exit(res)
|