Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add copy-on-write support

When this feature is enabled, a copy-on-write volume is created to store
modifications to the virtual machine image.

If the image cache is also enabled, images are directly hard linked from
the image cache into the virtual machine secureimages directory.  These
images stay unmodified because all writes are stored in the
copy-on-write file.

Since this feature is experimental and needs careful management of disk
space, it is disabled by default.

Closes #106.
  • Loading branch information...
commit 1d14155107b81535251cc577fb1816717c543d93 1 parent 2556c49
Pierre Riteau priteau authored
17 control/etc/workspace-control/images.conf
View
@@ -1,4 +1,3 @@
-
[images]
# local image repository directory
@@ -112,3 +111,19 @@ fail_if_present: false
tmplease: tmp-lease.sh
+# -------------------------------------------------------------------------
+
+[cow]
+
+# *Experimental*
+#
+# Enabling this preliminary feature lets you store VM disk writes in
+# copy-on-write files, which can speed up propagation by bypassing image
+# transfer or copy.
+#
+# Copy-on-write images need to be created using the qemu-img tool. This
+# setting is the path to the qemu-img tool as it is installed on your system.
+#
+# When 'qemu_img' is set, the feature is enabled.
+
+#qemu_img: /usr/bin/qemu-img
5 control/src/python/workspacecontrol/api/objects/ILocalFileSet.py
View
@@ -8,4 +8,7 @@ class ILocalFileSet(workspacecontrol.api.IWCObject):
def flist():
"""Return Python list of LocalFile instances
"""
-
+
+ def instance_dir():
+ """Return the instance directory within securelocaldir
+ """
132 control/src/python/workspacecontrol/defaults/ImageEditing.py
View
@@ -5,6 +5,7 @@
import stat
import struct
import sys
+import uuid
import zope.interface
import workspacecontrol.api.modules
@@ -37,6 +38,7 @@ def __init__(self, params, common):
self.mounttool_path = None
self.fdisk_path = None
self.qemu_nbd_path = None
+ self.qemu_img_path = None
self.mountdir = None
self.tmpdir = None
@@ -53,6 +55,12 @@ def validate(self):
if not self.qemu_nbd_path:
self.c.log.warn("no qemu_nbd configuration, mount+edit functionality for qcow2 images is disabled")
+ self.qemu_img_path = self.p.get_conf_or_none("cow", "qemu_img")
+ if not self.qemu_img_path:
+ self.c.log.warn("no qemu_img configuration, copy-on-write support is disabled")
+ elif not self.qemu_nbd_path:
+ self.c.log.warn("cannot enable copy-on-write support without qemu_nbd configuration")
+
# if functionality is disabled but arg exists, should fail program
self._validate_args_if_exist()
@@ -222,11 +230,16 @@ def process_after_procurement(self, local_file_set):
Return nothing, local_file_set will be modified as necessary.
"""
-
+
for lf in local_file_set.flist():
if lf.path.count(".gz") > 0 :
lf.path = self._gunzip_file_inplace(lf.path)
+ # copy-on-write
+ if self.qemu_img_path:
+ for lf in local_file_set.flist():
+ lf.path = self._create_cow_file(lf.path, local_file_set.instance_dir())
+
# disabled
if not self.mounttool_path:
return
@@ -268,7 +281,39 @@ def process_after_shutdown(self, local_file_set):
Return nothing, local_file_set will be modified as necessary.
"""
-
+
+ for lf in local_file_set.flist():
+ instance_dir = local_file_set.instance_dir()
+ if instance_dir is not None:
+ image_name = os.path.basename(lf.path)
+ image_local_path = os.path.join(instance_dir, image_name)
+
+ cow_name = image_name + "__cow__.qcow2"
+ cow_path = os.path.join(instance_dir, cow_name)
+
+ # If a file with a suffix of __cow__.qcow2 exists in the
+ # instance directory, it means that we were using
+ # copy-on-write.
+ if os.path.exists(cow_path):
+ # If the base image is from a shared location (started with
+ # file:///), make a copy in the instance directory first
+ if not os.path.exists(image_local_path):
+ shutil.copy(lf.path, image_local_path)
+ else:
+ # If the base image is linked from the cache, make a
+ # copy and replace the link by it
+ filestat = os.stat(image_local_path)
+ if filestat[stat.ST_NLINK] > 1:
+ tmpfile = image_local_path + uuid.uuid4().hex
+ shutil.copy(image_local_path, tmpfile)
+ os.unlink(image_local_path)
+ os.rename(tmpfile, image_local_path)
+ # Add write permissions to the image
+ os.chmod(image_local_path, 0600)
+
+ # Commit the changes back into the backing image.
+ lf.path = self._commit_cow_file(image_local_path, cow_path)
+
for lf in local_file_set.flist():
# The following edit is applicable for either case, if unprop target
@@ -376,6 +421,89 @@ def _gunzip_file_inplace(self, path):
# --------------------------------------------------------------------------
+ # COPY-ON-WRITE IMPL
+ # --------------------------------------------------------------------------
+
+ # returns newfilename
+ def _create_cow_file(self, path, instance_dir):
+
+ if instance_dir is None:
+ self.c.log.warn("skipping copy-on-write volume creation because instance_dir is None")
+ return path
+
+ self.c.log.info("creating copy-on-write volume for base image '%s'" % path)
+
+ image_name = os.path.basename(path)
+ cow_name = image_name + "__cow__.qcow2"
+ newpath = os.path.join(instance_dir, cow_name)
+
+ if os.path.exists(newpath):
+ errstr = "copy-on-write file already exists: '%s'" % newpath
+ self.c.log.error(errstr)
+ raise UnexpectedError(errstr)
+
+ # Create the copy-on-write volume
+ cmd = "%s create -f qcow2 -o backing_file=%s %s" % (self.qemu_img_path, path, newpath)
+ if self.c.dryrun:
+ self.c.log.debug("dryrun, command is: %s" % cmd)
+ return newpath
+
+ (ret, output) = getstatusoutput(cmd)
+ if ret:
+ errmsg = "problem running command: '%s' ::: return code" % cmd
+ errmsg += ": %d ::: output:\n%s" % (ret, output)
+ self.c.log.error(errmsg)
+ raise UnexpectedError(errmsg)
+ else:
+ errstr = "successfully created copy-on-write volume '%s' for image '%s'" % (newpath, path)
+ self.c.log.info(errstr)
+
+ return newpath
+
+ def _commit_cow_file(self, image_local_path, cow_path):
+ self.c.log.info("committing copy-on-write changes of '%s' into '%s'" % (cow_path, image_local_path))
+
+ try:
+ cmd = "%s rebase -f qcow2 -u -b %s %s" % (self.qemu_img_path, image_local_path, cow_path)
+ if self.c.dryrun:
+ self.c.log.debug("dryrun, command is: %s" % cmd)
+ else:
+ (ret, output) = getstatusoutput(cmd)
+ if ret:
+ errmsg = "problem running command: '%s' ::: return code" % cmd
+ errmsg += ": %d ::: output:\n%s" % (ret, output)
+ self.c.log.error(errmsg)
+ raise UnexpectedError(errmsg)
+
+ self.c.log.info("rebased '%s'" % cow_path)
+
+ cmd = "%s commit %s" % (self.qemu_img_path, cow_path)
+ if self.c.dryrun:
+ self.c.log.debug("dryrun, command is: %s" % cmd)
+ else:
+ (ret, output) = getstatusoutput(cmd)
+ if ret:
+ errmsg = "problem running command: '%s' ::: return code" % cmd
+ errmsg += ": %d ::: output:\n%s" % (ret, output)
+ self.c.log.error(errmsg)
+ raise UnexpectedError(errmsg)
+
+ self.c.log.info("committed '%s' into '%s'" % (cow_path, image_local_path))
+ return image_local_path
+
+ except:
+ exception_type = sys.exc_type
+ try:
+ exceptname = exception_type.__name__
+ except AttributeError:
+ exceptname = exception_type
+ errstr = "problem committing '%s': %s: %s" % \
+ (cow_path, str(exceptname), str(sys.exc_value))
+ self.c.log.error(errstr)
+ raise UnexpectedError(errstr)
+
+
+ # --------------------------------------------------------------------------
# MOUNT/COPY IMPL
# --------------------------------------------------------------------------
13 control/src/python/workspacecontrol/defaults/LocalFileSet.py
View
@@ -2,12 +2,15 @@
import workspacecontrol.api.objects
class DefaultLocalFileSet:
-
+
zope.interface.implements(workspacecontrol.api.objects.ILocalFileSet)
-
- def __init__(self, lfs_list):
+
+ def __init__(self, lfs_list, instance_dir=None):
self.lfs_list = lfs_list
-
+ self.local_instance_dir = instance_dir
+
def flist(self):
return self.lfs_list
-
+
+ def instance_dir(self):
+ return self.local_instance_dir
21 control/src/python/workspacecontrol/defaults/imageprocurement/propagate_cache.py
View
@@ -1,7 +1,8 @@
import fcntl
-import shutil
-import os
import logging
+import os
+import shutil
+import stat
class WSCCacheObj(object):
@@ -30,7 +31,7 @@ def _mangle_name(self, md5sum):
def _unmangle_name(self, name):
return name.replace(self._prefix + "_", "")
- def lookup(self, md5sum, newname):
+ def lookup(self, md5sum, newname, link=False):
name = self._mangle_name(md5sum)
absname = os.path.abspath(self._dir + "/" + name)
self._lock()
@@ -39,7 +40,13 @@ def lookup(self, md5sum, newname):
if name in list:
# touch the file
os.utime(absname, None)
- shutil.copyfile(absname, newname)
+
+ if link:
+ os.link(absname, newname)
+ # Mark the cache image as read-only
+ os.chmod(absname, 0400)
+ else:
+ shutil.copyfile(absname, newname)
return True
return False
finally:
@@ -57,6 +64,11 @@ def _get_size(self, list):
def _make_room_for(self, list, sz):
list = self._order_dir(list)
+
+ # Keep only files without hardlinks, because linked files cannot
+ # free any room in cache
+ filter(lambda x: os.stat(os.path.join(self._dir, x))[stat.ST_NLINK] == 1, list)
+
current_size = self._get_size(list)
max = self._max_size
fsinfo = os.statvfs(self._dir)
@@ -133,4 +145,3 @@ def flush_cache(self):
list = self.list_cache()
for l in list:
self.remove(l)
-
11 control/src/python/workspacecontrol/defaults/imageprocurement/propagate_common.py
View
@@ -275,10 +275,12 @@ def obtain(self):
"""
action = self.p.get_arg_or_none(wc_args.ACTION)
+ instance_dir = None
if action in [ACTIONS.CREATE, ACTIONS.PROPAGATE]:
self._ensure_instance_dir()
+ instance_dir = self._derive_instance_dir()
l_files = self._process_image_args()
if self._is_propagation_needed(l_files):
@@ -291,6 +293,7 @@ def obtain(self):
elif action in [ACTIONS.UNPROPAGATE]:
self._ensure_instance_dir()
+ instance_dir = self._derive_instance_dir()
l_files = self._process_image_args(unprop=True)
@@ -312,7 +315,7 @@ def obtain(self):
self._blankspace(l_files)
local_file_set_cls = self.c.get_class_by_keyword("LocalFileSet")
- local_file_set = local_file_set_cls(l_files)
+ local_file_set = local_file_set_cls(l_files, instance_dir=instance_dir)
return local_file_set
# --------------------------------------------------------------------------
@@ -473,9 +476,13 @@ def _propagate(self, l_files):
continue
if cache:
+ cow = self.p.get_conf_or_none("cow", "qemu_img")
try:
self.c.log.debug("cache lookup %s" % (cache_key))
- rc = cache.lookup(cache_key, l_file.path)
+ if cow is not None:
+ rc = cache.lookup(cache_key, l_file.path, link=True)
+ else:
+ rc = cache.lookup(cache_key, l_file.path, link=False)
if rc:
self.c.log.info("The file was found in the cache and copied to %s" % (l_file.path))
return
Please sign in to comment.
Something went wrong with that request. Please try again.