[CRIU,1/2] Add support for migration of basic runc container

Submitted by Veronika Kabatova on Jan. 24, 2017, 2:48 p.m.

Details

Message ID 1485269325-5565-2-git-send-email-vkabatov@redhat.com
State New
Series "runc container migration"
Headers show

Commit Message

Veronika Kabatova Jan. 24, 2017, 2:48 p.m.
This patch allows migration of a runc container without --tcp-established
option or mounts (a patch with support for mount follows). Example usage
includes running `p.haul-wrap service` on target host and
`p.haul-wrap client <target-ip> runc <container-id>` on source host. Bundle
directory should exist (with the same path) on target host and no container
with the same ID should exist there (a patch with checks, bundle directory
creation and cleanup in case of failed migration may follow later).

The criu API is used directly for checkpoint and restore because the C/R
implemented in runc doesn't allow pre-dump option yet and using external
commands `runc checkpoint` and `runc restore` doesn't integrate well with
p.haul (the migration itself works, but p.haul complains about receiving
empty messages from criu and migration statistics are left out as well).

After migration of process tree itself, a directory with container status
is created with the same `state.json` file as the original container had,
just with a new ID of the root process. This is for the runc process to
acknowledge existence of the container.
---
 phaul/htype.py       |   1 +
 phaul/p_haul_runc.py | 202 +++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 203 insertions(+)
 create mode 100644 phaul/p_haul_runc.py

Patch hide | download patch | download mbox

diff --git a/phaul/htype.py b/phaul/htype.py
index a269652..5e90c0b 100644
--- a/phaul/htype.py
+++ b/phaul/htype.py
@@ -13,6 +13,7 @@  __haul_modules = {
 	"pid": "p_haul_pid",
 	"lxc": "p_haul_lxc",
 	"docker": "p_haul_docker",
+	"runc": "p_haul_runc",
 }
 
 
diff --git a/phaul/p_haul_runc.py b/phaul/p_haul_runc.py
new file mode 100644
index 0000000..188806d
--- /dev/null
+++ b/phaul/p_haul_runc.py
@@ -0,0 +1,202 @@ 
+#
+# Runc container hauler
+#
+
+import json
+import logging
+import os
+import re
+import shutil
+import subprocess as sp
+
+import pycriu
+
+import criu_cr
+import fs_haul_subtree
+
+
+# Some constants for runc
+runc_bin = "/usr/bin/runc"  # "/usr/local/sbin/runc" for compiled version
+runc_run = "/var/run/runc/"
+runc_conf_name = "config.json"
+
+
+class p_haul_type(object):
+	def __init__(self, ctid):
+
+		# Validate provided container ID
+		if (not(re.match("^[\w-]+$", ctid)) or len(ctid) > 1024):
+			raise Exception("Invalid runc container name: %s", ctid)
+
+		self._ctid = ctid
+		self._veths = []
+
+	def init_src(self):
+		try:
+			with open(os.path.join(runc_run, self._ctid, "state.json"), "r") as state:
+				self._container_state = json.loads(state.read())
+			self._labels = self._container_state["config"]["labels"]
+			self._ct_rootfs = self._container_state["config"]["rootfs"]
+			self._root_pid = self._container_state["init_process_pid"]
+			self._ext_descriptors = json.dumps(
+				self._container_state["external_descriptors"])
+		except IOError:
+			raise Exception("No container %s is running", self._ctid)
+		except KeyError:
+			raise Exception("Invalid container state retrieved")
+
+		self._runc_bundle = next(label[len("bundle="):]
+						for label in self._labels
+						if label.startswith("bundle="))
+
+		if any([mount["device"] == "cgroup" for mount in
+				self._container_state["config"]["mounts"]]):
+			cgroup_paths = self._container_state["cgroup_paths"]
+		for mount in self._container_state["config"]["mounts"]:
+			if mount["device"] == "bind":
+				if mount["destination"].startswith(self._ct_rootfs):
+					dst = mount["destination"][len(self._ct_rootfs):]
+				else:
+					dst = mount["destination"]
+				self._binds.update({dst: dst})
+			if mount["device"] == "cgroup":
+				for subsystem, c_mp in cgroup_paths.items():
+					# Remove container ID from path
+					mountpoint = os.path.split(c_mp)[0]
+					dst = os.path.join(mount["destination"],
+							os.path.split(mountpoint)[0])
+					if dst.startswith(self._ct_rootfs):
+						dst = dst[len(self._ct_rootfs):]
+					self._binds.update({dst: dst})
+
+		if self._container_state["config"]["mask_paths"] is not None:
+			masked = self._container_state["config"]["mask_paths"]
+			for path in masked:
+				filepath = os.path.join("/proc", self.root_task_pid, "root", path)
+				if (os.path.exists(filepath) and
+						not os.path.isdir(filepath)):
+					self._binds.update({path: "/dev/null"})
+
+		self.__load_ct_config(self._runc_bundle)
+		logging.info("Container rootfs: %s", self._ct_rootfs)
+
+	def init_dst(self):
+		pass
+
+	def adjust_criu_req(self, req):
+		if req.type in [pycriu.rpc.DUMP, pycriu.rpc.RESTORE]:
+			req.opts.manage_cgroups = True
+			req.opts.notify_scripts = True
+
+		if req.type == pycriu.rpc.RESTORE:
+			req.opts.root = self._ct_rootfs
+			req.opts.rst_sibling = True
+			req.opts.evasive_devices = True
+
+	def root_task_pid(self):
+		return self._root_pid
+
+	def __load_ct_config(self, path):
+		self._ct_config = os.path.join(self._runc_bundle, runc_conf_name)
+		logging.info("Container config: %s", self._ct_config)
+
+	def set_options(self, opts):
+		pass
+
+	def prepare_ct(self, pid):
+		pass
+
+	def mount(self):
+		nroot = os.path.join(self._runc_bundle, "criu_dir")
+		if not os.access(nroot, os.F_OK):
+			os.makedirs(nroot)
+		sp.call(["mount", "--bind", self._ct_rootfs, nroot])
+		return nroot
+
+	def umount(self):
+		nroot = os.path.join(self._runc_bundle, "criu_dir")
+		if os.path.exists(nroot):
+			sp.call(["umount", nroot])
+			shutil.rmtree(nroot)
+
+	def start(self):
+		pass
+
+	def stop(self, umount):
+		pass
+
+	def get_fs(self, fdfs=None):
+		return fs_haul_subtree.p_haul_fs([self._ct_rootfs, self._ct_config])
+
+	def get_fs_receiver(self, fdfs=None):
+		return None
+
+	def get_meta_images(self, path):
+		bundle_path = os.path.join(path, "bundle.txt")
+		with open(bundle_path, "w+") as bundle_file:
+			bundle_file.write(self._runc_bundle)
+		desc_path = os.path.join(path, "descriptors.json")
+		with open(desc_path, "w+") as desc_file:
+			desc_file.write(self._ext_descriptors)
+		shutil.copy(os.path.join(runc_run, self._ctid, "state.json"), path)
+		return [(bundle_path, "bundle.txt"),
+				(desc_path, "descriptors.json"),
+				(os.path.join(path, "state.json"), "state.json")]
+
+	def put_meta_images(self, dir):
+		with open(os.path.join(dir, "bundle.txt")) as bundle_file:
+			self._runc_bundle = bundle_file.read()
+
+	def final_dump(self, pid, img, ccon, fs):
+		criu_cr.criu_dump(self, pid, img, ccon, fs)
+
+	def migration_complete(self, fs, target_host):
+		sp.call([runc_bin, "delete", self._ctid])
+
+	def migration_fail(self, fs):
+		pass
+
+	def target_cleanup(self, src_data):
+		pass
+
+	def final_restore(self, img, connection):
+		try:
+			with open(os.path.join(self._runc_bundle, runc_conf_name), "r") as config:
+				self._container_state = json.loads(config.read())
+			root_path = self._container_state["root"]["path"]
+		except IOError:
+			raise Exception("Unable to get container config")
+		except KeyError:
+			raise Exception("Invalid config")
+
+		if not os.path.isabs(root_path):
+			self._ct_rootfs = os.path.join(self._runc_bundle, root_path)
+		else:
+			self._ct_rootfs = root_path
+
+		ct_path = os.path.join(runc_run, self._ctid)
+		if not os.path.exists(ct_path):
+			os.makedirs(ct_path)
+		with open(os.path.join(img.image_dir(), "state.json"), "r") as old_state_file:
+			self._restore_state = json.loads(old_state_file.read())
+
+		criu_cr.criu_restore(self, img, connection)
+
+	def restored(self, pid):
+		self._restore_state["init_process_pid"] = pid
+		with open(os.path.join(runc_run, self._ctid, "state.json"),
+				"w+") as new_state_file:
+			new_state_file.write(json.dumps(self._restore_state))
+		self.umount()
+
+	def can_pre_dump(self):
+		return True
+
+	def dump_need_page_server(self):
+		return True
+
+	def can_migrate_tcp(self):
+		return False
+
+	def veths(self):
+		return self._veths