Adds common image utils

This is the first step to merge the code in nova/virt/images.py
and cinder/image/image_utils.py.

This patch pulls up the QemuImgInfo class which is common
in the two modules above.
The test script is based on nova/tests/virt/test_images.py with
an additional test_qemu_info_blank testcase.

Partially implements bp imageutils

Change-Id: Ia07739492776180e579b03920aa32b9215ce941d
This commit is contained in:
Zhongyue Luo
2013-08-12 13:04:52 +08:00
committed by Gerrit Code Review
parent aae2b1c388
commit 0356685286
3 changed files with 270 additions and 0 deletions

View File

@@ -153,6 +153,12 @@ M: Mark McLoughlin <markmc@redhat.com>
S: Maintained
F: gettextutils.py
== imageutils ==
M: Zhongyue Luo <zhongyue.nah@intel.com>
S: Maintained
F: imageutils.py
== importutils ==
M:

View File

@@ -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 openstack.common.gettextutils import _ # noqa
from 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

View File

@@ -0,0 +1,120 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright (C) 2012 Yahoo! Inc.
# All Rights Reserved.
#
# 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.
from openstack.common import imageutils
from tests import utils as utils_test
class ImageUtilsTestCase(utils_test.BaseTestCase):
def test_qemu_img_info_blank(self):
example_output = """image: None
file_format: None
virtual_size: None
disk_size: None
cluster_size: None
backing_file: None"""
image_info = imageutils.QemuImgInfo()
self.assertEqual(str(image_info), example_output)
self.assertEqual(len(image_info.snapshots), 0)
def test_qemu_info_canon(self):
example_output = """image: disk.config
file format: raw
virtual size: 64M (67108864 bytes)
cluster_size: 65536
disk size: 96K
blah BLAH: bb
"""
image_info = imageutils.QemuImgInfo(example_output)
self.assertEquals('disk.config', image_info.image)
self.assertEquals('raw', image_info.file_format)
self.assertEquals(67108864, image_info.virtual_size)
self.assertEquals(98304, image_info.disk_size)
self.assertEquals(65536, image_info.cluster_size)
def test_qemu_info_canon2(self):
example_output = """image: disk.config
file format: QCOW2
virtual size: 67108844
cluster_size: 65536
disk size: 963434
backing file: /var/lib/nova/a328c7998805951a_2
"""
image_info = imageutils.QemuImgInfo(example_output)
self.assertEquals('disk.config', image_info.image)
self.assertEquals('qcow2', image_info.file_format)
self.assertEquals(67108844, image_info.virtual_size)
self.assertEquals(963434, image_info.disk_size)
self.assertEquals(65536, image_info.cluster_size)
self.assertEquals('/var/lib/nova/a328c7998805951a_2',
image_info.backing_file)
def test_qemu_backing_file_actual(self):
example_output = """image: disk.config
file format: raw
virtual size: 64M (67108864 bytes)
cluster_size: 65536
disk size: 96K
Snapshot list:
ID TAG VM SIZE DATE VM CLOCK
1 d9a9784a500742a7bb95627bb3aace38 0 2012-08-20 10:52:46 00:00:00.000
backing file: /var/lib/nova/a328c7998805951a_2 (actual path: /b/3a988059e51a_2)
"""
image_info = imageutils.QemuImgInfo(example_output)
self.assertEquals('disk.config', image_info.image)
self.assertEquals('raw', image_info.file_format)
self.assertEquals(67108864, image_info.virtual_size)
self.assertEquals(98304, image_info.disk_size)
self.assertEquals(1, len(image_info.snapshots))
self.assertEquals('/b/3a988059e51a_2',
image_info.backing_file)
def test_qemu_info_convert(self):
example_output = """image: disk.config
file format: raw
virtual size: 64M
disk size: 96K
Snapshot list:
ID TAG VM SIZE DATE VM CLOCK
1 d9a9784a500742a7bb95627bb3aace38 0 2012-08-20 10:52:46 00:00:00.000
3 d9a9784a500742a7bb95627bb3aace38 0 2012-08-20 10:52:46 00:00:00.000
4 d9a9784a500742a7bb95627bb3aace38 0 2012-08-20 10:52:46 00:00:00.000
junk stuff: bbb
"""
image_info = imageutils.QemuImgInfo(example_output)
self.assertEquals('disk.config', image_info.image)
self.assertEquals('raw', image_info.file_format)
self.assertEquals(67108864, image_info.virtual_size)
self.assertEquals(98304, image_info.disk_size)
def test_qemu_info_snaps(self):
example_output = """image: disk.config
file format: raw
virtual size: 64M (67108864 bytes)
disk size: 96K
Snapshot list:
ID TAG VM SIZE DATE VM CLOCK
1 d9a9784a500742a7bb95627bb3aace38 0 2012-08-20 10:52:46 00:00:00.000
3 d9a9784a500742a7bb95627bb3aace38 0 2012-08-20 10:52:46 00:00:00.000
4 d9a9784a500742a7bb95627bb3aace38 0 2012-08-20 10:52:46 00:00:00.000
"""
image_info = imageutils.QemuImgInfo(example_output)
self.assertEquals('disk.config', image_info.image)
self.assertEquals('raw', image_info.file_format)
self.assertEquals(67108864, image_info.virtual_size)
self.assertEquals(98304, image_info.disk_size)
self.assertEquals(3, len(image_info.snapshots))