]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Make copy_to_volume a bit more useful.
authorjohn-griffith <john.griffith@solidfire.com>
Tue, 4 Dec 2012 04:02:19 +0000 (21:02 -0700)
committerJohn Griffith <john.griffith@solidfire.com>
Thu, 6 Dec 2012 21:01:06 +0000 (14:01 -0700)
The current implementation of cinder.driver.copy_to_volume
does not take in to account image type and is only valid
for raw images.  This doesn't do much good for the majority
of cases, and rather than require folks to have a raw version
uploaded to Glance, let's just do a convert if needed on the fly.

This simply creates a temp file, does the conversion and then
writes the temp file directly to the volume.

Change-Id: I849b04b8aae76da068abcd2a20c1fcecca8a5caa

cinder/image/image_utils.py [new file with mode: 0644]
cinder/tests/test_volume.py
cinder/utils.py
cinder/volume/driver.py
etc/cinder/rootwrap.d/volume.filters

diff --git a/cinder/image/image_utils.py b/cinder/image/image_utils.py
new file mode 100644 (file)
index 0000000..e38c59a
--- /dev/null
@@ -0,0 +1,241 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+# Copyright (c) 2010 Citrix Systems, Inc.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""
+Helper methods to deal with images.
+
+This is essentially a copy from nova.virt.images.py
+Some slight modifications, but at some point
+we should look at maybe pushign this up to OSLO
+"""
+
+import os
+import re
+import tempfile
+
+from cinder import exception
+from cinder import flags
+from cinder.openstack.common import cfg
+from cinder.openstack.common import log as logging
+from cinder import utils
+
+
+LOG = logging.getLogger(__name__)
+
+image_helper_opt = [cfg.StrOpt('image_conversion_dir',
+                    default='/tmp',
+                    help='parent dir for tempdir used for image conversion'), ]
+
+FLAGS = flags.FLAGS
+FLAGS.register_opts(image_helper_opt)
+
+
+class QemuImgInfo(object):
+    BACKING_FILE_RE = re.compile((r"^(.*?)\s*\(actual\s+path\s*:"
+                                  r"\s+(.*?)\)\s*$"), re.I)
+    TOP_LEVEL_RE = re.compile(r"^([\w\d\s\_\-]+):(.*)$")
+    SIZE_RE = re.compile(r"\(\s*(\d+)\s+bytes\s*\)", re.I)
+
+    def __init__(self, cmd_output):
+        details = self._parse(cmd_output)
+        self.image = details.get('image')
+        self.backing_file = details.get('backing_file')
+        self.file_format = details.get('file_format')
+        self.virtual_size = details.get('virtual_size')
+        self.cluster_size = details.get('cluster_size')
+        self.disk_size = details.get('disk_size')
+        self.snapshots = details.get('snapshot_list', [])
+        self.encryption = details.get('encryption')
+
+    def __str__(self):
+        lines = [
+            'image: %s' % self.image,
+            'file_format: %s' % self.file_format,
+            'virtual_size: %s' % self.virtual_size,
+            'disk_size: %s' % self.disk_size,
+            'cluster_size: %s' % self.cluster_size,
+            'backing_file: %s' % self.backing_file,
+        ]
+        if self.snapshots:
+            lines.append("snapshots: %s" % self.snapshots)
+        return "\n".join(lines)
+
+    def _canonicalize(self, field):
+        # Standardize on underscores/lc/no dash and no spaces
+        # since qemu seems to have mixed outputs here... and
+        # this format allows for better integration with python
+        # - ie for usage in kwargs and such...
+        field = field.lower().strip()
+        for c in (" ", "-"):
+            field = field.replace(c, '_')
+        return field
+
+    def _extract_bytes(self, details):
+        # Replace it with the byte amount
+        real_size = self.SIZE_RE.search(details)
+        if real_size:
+            details = real_size.group(1)
+        try:
+            details = utils.to_bytes(details)
+        except (TypeError, ValueError):
+            pass
+        return details
+
+    def _extract_details(self, root_cmd, root_details, lines_after):
+        consumed_lines = 0
+        real_details = root_details
+        if root_cmd == 'backing_file':
+            # Replace it with the real backing file
+            backing_match = self.BACKING_FILE_RE.match(root_details)
+            if backing_match:
+                real_details = backing_match.group(2).strip()
+        elif root_cmd in ['virtual_size', 'cluster_size', 'disk_size']:
+            # Replace it with the byte amount (if we can convert it)
+            real_details = self._extract_bytes(root_details)
+        elif root_cmd == 'file_format':
+            real_details = real_details.strip().lower()
+        elif root_cmd == 'snapshot_list':
+            # Next line should be a header, starting with 'ID'
+            if not lines_after or not lines_after[0].startswith("ID"):
+                msg = _("Snapshot list encountered but no header found!")
+                raise ValueError(msg)
+            consumed_lines += 1
+            possible_contents = lines_after[1:]
+            real_details = []
+            # This is the sprintf pattern we will try to match
+            # "%-10s%-20s%7s%20s%15s"
+            # ID TAG VM SIZE DATE VM CLOCK (current header)
+            for line in possible_contents:
+                line_pieces = line.split(None)
+                if len(line_pieces) != 6:
+                    break
+                else:
+                    # Check against this pattern occuring in the final position
+                    # "%02d:%02d:%02d.%03d"
+                    date_pieces = line_pieces[5].split(":")
+                    if len(date_pieces) != 3:
+                        break
+                    real_details.append({
+                        'id': line_pieces[0],
+                        'tag': line_pieces[1],
+                        'vm_size': line_pieces[2],
+                        'date': line_pieces[3],
+                        'vm_clock': line_pieces[4] + " " + line_pieces[5],
+                    })
+                    consumed_lines += 1
+        return (real_details, consumed_lines)
+
+    def _parse(self, cmd_output):
+        # Analysis done of qemu-img.c to figure out what is going on here
+        # Find all points start with some chars and then a ':' then a newline
+        # and then handle the results of those 'top level' items in a separate
+        # function.
+        #
+        # TODO(harlowja): newer versions might have a json output format
+        #                 we should switch to that whenever possible.
+        #                 see: http://bit.ly/XLJXDX
+        if not cmd_output:
+            cmd_output = ''
+        contents = {}
+        lines = cmd_output.splitlines()
+        i = 0
+        line_am = len(lines)
+        while i < line_am:
+            line = lines[i]
+            if not line.strip():
+                i += 1
+                continue
+            consumed_lines = 0
+            top_level = self.TOP_LEVEL_RE.match(line)
+            if top_level:
+                root = self._canonicalize(top_level.group(1))
+                if not root:
+                    i += 1
+                    continue
+                root_details = top_level.group(2).strip()
+                details, consumed_lines = self._extract_details(root,
+                                                                root_details,
+                                                                lines[i + 1:])
+                contents[root] = details
+            i += consumed_lines + 1
+        return contents
+
+
+def qemu_img_info(path):
+    """Return a object containing the parsed output from qemu-img info."""
+    out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
+                             'qemu-img', 'info', path,
+                             run_as_root=True)
+    return QemuImgInfo(out)
+
+
+def convert_image(source, dest, out_format):
+    """Convert image to other format"""
+    cmd = ('qemu-img', 'convert', '-O', out_format, source, dest)
+    utils.execute(*cmd, run_as_root=True)
+
+
+def fetch(context, image_service, image_id, path, _user_id, _project_id):
+    # TODO(vish): Improve context handling and add owner and auth data
+    #             when it is added to glance.  Right now there is no
+    #             auth checking in glance, so we assume that access was
+    #             checked before we got here.
+    with utils.remove_path_on_error(path):
+        with open(path, "wb") as image_file:
+            image_service.download(context, image_id, image_file)
+
+
+def fetch_to_raw(context, image_service,
+                 image_id, dest,
+                 user_id=None, project_id=None):
+    if (FLAGS.image_conversion_dir and not
+            os.path.exists(FLAGS.image_conversion_dir)):
+        os.makedirs(FLAGS.image_conversion_dir)
+
+    with tempfile.NamedTemporaryFile(dir=FLAGS.image_conversion_dir) as tmp:
+        fetch(context, image_service, image_id, tmp.name, user_id, project_id)
+        tmp.flush()
+
+        data = qemu_img_info(tmp.name)
+        with utils.remove_path_on_error(tmp.name):
+            fmt = data.file_format
+            if fmt is None:
+                raise exception.ImageUnacceptable(
+                    reason=_("'qemu-img info' parsing failed."),
+                    image_id=image_id)
+
+            backing_file = data.backing_file
+            if backing_file is not None:
+                raise exception.ImageUnacceptable(
+                    image_id=image_id,
+                    reason=_("fmt=%(fmt)s backed by:"
+                             "%(backing_file)s") % locals())
+
+            # NOTE(jdg): I'm using qemu-img convert to write
+            # to the volume regardless if it *needs* conversion or not
+            LOG.debug("%s was %s, converting to raw" % (image_id, fmt))
+            convert_image(tmp.name, dest, 'raw')
+            tmp.close()
+
+            data = qemu_img_info(dest)
+            if data.file_format != "raw":
+                raise exception.ImageUnacceptable(
+                    image_id=image_id,
+                    reason=_("Converted to raw, but format is now %s") %
+                    data.file_format)
index 35f467afa8b70be3d2d2b38638ed90a90d2983e1..5c3cd94cced464ee6bffb87fd97a708df8161971 100644 (file)
@@ -31,6 +31,7 @@ from cinder import context
 from cinder import db
 from cinder import exception
 from cinder import flags
+from cinder.image import image_utils
 from cinder.openstack.common import importutils
 from cinder.openstack.common.notifier import api as notifier_api
 from cinder.openstack.common.notifier import test_notifier
@@ -493,9 +494,13 @@ class VolumeTestCase(test.TestCase):
         def fake_copy_image_to_volume(context, volume, image_id):
             pass
 
+        def fake_fetch_to_raw(context, image_service, image_id, vol_path):
+            pass
+
         dst_fd, dst_path = tempfile.mkstemp()
         os.close(dst_fd)
         self.stubs.Set(self.volume.driver, 'local_path', fake_local_path)
+        self.stubs.Set(image_utils, 'fetch_to_raw', fake_fetch_to_raw)
         if fakeout_copy_image_to_volume:
             self.stubs.Set(self.volume, '_copy_image_to_volume',
                            fake_copy_image_to_volume)
index 9a789bb8d6310236cfb5d5d70d926e8b0bfd1aa1..1d5e89c5c9017a79cd37170fbc7ce38c5c1ca272 100644 (file)
@@ -1126,3 +1126,39 @@ def ensure_tree(path):
                 raise
         else:
             raise
+
+
+def to_bytes(text, default=0):
+    """Try to turn a string into a number of bytes. Looks at the last
+    characters of the text to determine what conversion is needed to
+    turn the input text into a byte number.
+
+    Supports: B/b, K/k, M/m, G/g, T/t (or the same with b/B on the end)
+
+    """
+    BYTE_MULTIPLIERS = {
+        '': 1,
+        't': 1024 ** 4,
+        'g': 1024 ** 3,
+        'm': 1024 ** 2,
+        'k': 1024,
+    }
+
+    # Take off everything not number 'like' (which should leave
+    # only the byte 'identifier' left)
+    mult_key_org = text.lstrip('-1234567890')
+    mult_key = mult_key_org.lower()
+    mult_key_len = len(mult_key)
+    if mult_key.endswith("b"):
+        mult_key = mult_key[0:-1]
+    try:
+        multiplier = BYTE_MULTIPLIERS[mult_key]
+        if mult_key_len:
+            # Empty cases shouldn't cause text[0:-0]
+            text = text[0:-mult_key_len]
+        return int(text) * multiplier
+    except KeyError:
+        msg = _('Unknown byte multiplier: %s') % mult_key_org
+        raise TypeError(msg)
+    except ValueError:
+        return default
index 69a666d56f30b4078a51295191cee35ae9802e2a..71481eda95b4db17b242121d5fc02e5f4f1bc9ad 100644 (file)
@@ -26,6 +26,7 @@ import time
 
 from cinder import exception
 from cinder import flags
+from cinder.image import image_utils
 from cinder.openstack.common import cfg
 from cinder.openstack.common import log as logging
 from cinder import utils
@@ -586,10 +587,10 @@ class ISCSIDriver(VolumeDriver):
 
     def copy_image_to_volume(self, context, volume, image_service, image_id):
         """Fetch the image from image_service and write it to the volume."""
-        volume_path = self.local_path(volume)
-        with utils.temporary_chown(volume_path):
-            with utils.file_open(volume_path, "wb") as image_file:
-                image_service.download(context, image_id, image_file)
+        image_utils.fetch_to_raw(context,
+                                 image_service,
+                                 image_id,
+                                 self.local_path(volume))
 
     def copy_volume_to_image(self, context, volume, image_service, image_id):
         """Copy the volume to the specified image."""
index 936dbbd3c42e2b853260fe01db20271ee4b02aa6..a36649d2b1cd3c243443a137d17252d273d907dc 100644 (file)
@@ -35,3 +35,5 @@ chown: CommandFilter, /bin/chown, root
 dmsetup: CommandFilter, /sbin/dmsetup, root
 dmsetup_usr: CommandFilter, /usr/sbin/dmsetup, root
 ln: CommandFilter, /bin/ln, root
+qemu-img: CommandFilter, /usr/bin/qemu-img, root
+env: CommandFilter, /usr/bin/env, root