From: john-griffith Date: Tue, 4 Dec 2012 04:02:19 +0000 (-0700) Subject: Make copy_to_volume a bit more useful. X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=6a31bf3e436c1b975630615af0b381df3b7c8756;p=openstack-build%2Fcinder-build.git Make copy_to_volume a bit more useful. 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 --- diff --git a/cinder/image/image_utils.py b/cinder/image/image_utils.py new file mode 100644 index 000000000..e38c59a64 --- /dev/null +++ b/cinder/image/image_utils.py @@ -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) diff --git a/cinder/tests/test_volume.py b/cinder/tests/test_volume.py index 35f467afa..5c3cd94cc 100644 --- a/cinder/tests/test_volume.py +++ b/cinder/tests/test_volume.py @@ -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) diff --git a/cinder/utils.py b/cinder/utils.py index 9a789bb8d..1d5e89c5c 100644 --- a/cinder/utils.py +++ b/cinder/utils.py @@ -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 diff --git a/cinder/volume/driver.py b/cinder/volume/driver.py index 69a666d56..71481eda9 100644 --- a/cinder/volume/driver.py +++ b/cinder/volume/driver.py @@ -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.""" diff --git a/etc/cinder/rootwrap.d/volume.filters b/etc/cinder/rootwrap.d/volume.filters index 936dbbd3c..a36649d2b 100644 --- a/etc/cinder/rootwrap.d/volume.filters +++ b/etc/cinder/rootwrap.d/volume.filters @@ -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