diff --git a/cinder/image/image_utils.py b/cinder/image/image_utils.py index a364a696155..7ce3c1768e0 100644 --- a/cinder/image/image_utils.py +++ b/cinder/image/image_utils.py @@ -28,13 +28,13 @@ we should look at maybe pushing this up to Oslo import contextlib import os -import re import tempfile from oslo.config import cfg from cinder import exception from cinder.openstack.common import fileutils +from cinder.openstack.common import imageutils from cinder.openstack.common import log as logging from cinder.openstack.common import processutils from cinder.openstack.common import strutils @@ -52,144 +52,13 @@ CONF = cfg.CONF CONF.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 = strutils.to_bytes(details) - except TypeError: - 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.""" cmd = ('env', 'LC_ALL=C', 'LANG=C', 'qemu-img', 'info', path) if os.name == 'nt': cmd = cmd[3:] out, err = utils.execute(*cmd, run_as_root=True) - return QemuImgInfo(out) + return imageutils.QemuImgInfo(out) def convert_image(source, dest, out_format): diff --git a/cinder/openstack/common/imageutils.py b/cinder/openstack/common/imageutils.py new file mode 100644 index 00000000000..312ffb5d1fb --- /dev/null +++ b/cinder/openstack/common/imageutils.py @@ -0,0 +1,144 @@ +# 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. +""" + +import re + +from cinder.openstack.common.gettextutils import _ # noqa +from cinder.openstack.common import strutils + + +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=None): + details = self._parse(cmd_output or '') + 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 = strutils.to_bytes(details) + except TypeError: + pass + return details + + def _extract_details(self, root_cmd, root_details, lines_after): + 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) + del lines_after[0] + 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) + while lines_after: + line = lines_after[0] + line_pieces = line.split() + if len(line_pieces) != 6: + break + # Check against this pattern 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], + }) + del lines_after[0] + return real_details + + 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 + contents = {} + lines = [x for x in cmd_output.splitlines() if x.strip()] + while lines: + line = lines.pop(0) + top_level = self.TOP_LEVEL_RE.match(line) + if top_level: + root = self._canonicalize(top_level.group(1)) + if not root: + continue + root_details = top_level.group(2).strip() + details = self._extract_details(root, root_details, lines) + contents[root] = details + return contents diff --git a/cinder/tests/test_glusterfs.py b/cinder/tests/test_glusterfs.py index ef34fd46344..62276114a29 100644 --- a/cinder/tests/test_glusterfs.py +++ b/cinder/tests/test_glusterfs.py @@ -30,6 +30,7 @@ from cinder import context from cinder import db from cinder import exception from cinder.image import image_utils +from cinder.openstack.common import imageutils from cinder.openstack.common import processutils as putils from cinder import test from cinder.tests.compute import test_nova @@ -790,7 +791,7 @@ class GlusterFsDriverTestCase(test.TestCase): mox.StubOutWithMock(drv, '_write_info_file') mox.StubOutWithMock(image_utils, 'qemu_img_info') - img_info = image_utils.QemuImgInfo(qemu_img_info_output) + img_info = imageutils.QemuImgInfo(qemu_img_info_output) image_utils.qemu_img_info(snap_path_2).AndReturn(img_info) info_file_dict = {'active': snap_file_2, @@ -896,7 +897,7 @@ class GlusterFsDriverTestCase(test.TestCase): info_path = drv._local_path_volume(volume) + '.info' drv._read_info_file(info_path).AndReturn(info_file_dict) - img_info = image_utils.QemuImgInfo(qemu_img_info_output_snap_1) + img_info = imageutils.QemuImgInfo(qemu_img_info_output_snap_1) image_utils.qemu_img_info(snap_path).AndReturn(img_info) snap_ref = {'name': 'test snap', @@ -971,7 +972,7 @@ class GlusterFsDriverTestCase(test.TestCase): disk size: 473K """ % self.VOLUME_UUID - img_info = image_utils.QemuImgInfo(qemu_img_info_output) + img_info = imageutils.QemuImgInfo(qemu_img_info_output) mox.StubOutWithMock(drv, '_execute') mox.StubOutWithMock(drv, 'get_active_image_from_info') @@ -1156,7 +1157,7 @@ class GlusterFsDriverTestCase(test.TestCase): disk size: 173K backing file: %s """ % (snap_file, volume_file) - img_info = image_utils.QemuImgInfo(qemu_img_info_output) + img_info = imageutils.QemuImgInfo(qemu_img_info_output) image_utils.qemu_img_info(snap_path).AndReturn(img_info) drv._read_info_file(info_path, empty_if_missing=True).\ @@ -1244,7 +1245,7 @@ class GlusterFsDriverTestCase(test.TestCase): disk size: 173K backing file: %s """ % (snap_file, volume_file) - img_info = image_utils.QemuImgInfo(qemu_img_info_output) + img_info = imageutils.QemuImgInfo(qemu_img_info_output) image_utils.qemu_img_info(snap_path).AndReturn(img_info) @@ -1327,7 +1328,7 @@ class GlusterFsDriverTestCase(test.TestCase): disk size: 173K backing file: %s """ % (snap_file, volume_file) - img_info = image_utils.QemuImgInfo(qemu_img_info_output) + img_info = imageutils.QemuImgInfo(qemu_img_info_output) image_utils.qemu_img_info(snap_path).AndReturn(img_info) @@ -1407,9 +1408,9 @@ class GlusterFsDriverTestCase(test.TestCase): qemu_img_output_3 = qemu_img_output % {'image_name': vol_filename_3, 'backing_file': vol_filename_2} - info_1 = image_utils.QemuImgInfo(qemu_img_output_1) - info_2 = image_utils.QemuImgInfo(qemu_img_output_2) - info_3 = image_utils.QemuImgInfo(qemu_img_output_3) + info_1 = imageutils.QemuImgInfo(qemu_img_output_1) + info_2 = imageutils.QemuImgInfo(qemu_img_output_2) + info_3 = imageutils.QemuImgInfo(qemu_img_output_3) drv._local_volume_dir(volume).AndReturn(vol_dir) image_utils.qemu_img_info(vol_path_3).\ @@ -1476,7 +1477,7 @@ class GlusterFsDriverTestCase(test.TestCase): disk size: 173K backing file: %s """ % (snap_file, src_volume['name']) - img_info = image_utils.QemuImgInfo(qemu_img_output) + img_info = imageutils.QemuImgInfo(qemu_img_output) image_utils.qemu_img_info(snap_path).AndReturn(img_info) @@ -1533,7 +1534,7 @@ class GlusterFsDriverTestCase(test.TestCase): virtual size: 1.0G (1073741824 bytes) disk size: 173K """ % volume['name'] - img_info = image_utils.QemuImgInfo(qemu_img_output) + img_info = imageutils.QemuImgInfo(qemu_img_output) mox.StubOutWithMock(drv, 'get_active_image_from_info') mox.StubOutWithMock(image_utils, 'qemu_img_info') diff --git a/openstack-common.conf b/openstack-common.conf index ec8eac4e376..50294c5945f 100644 --- a/openstack-common.conf +++ b/openstack-common.conf @@ -8,6 +8,7 @@ module=excutils module=fileutils module=flakes module=gettextutils +module=imageutils module=importutils module=install_venv_common module=jsonutils