1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2010 United States Government as represented by the
4
# Administrator of the National Aeronautics and Space Administration.
6
# Copyright (c) 2010 Citrix Systems, Inc.
8
# Licensed under the Apache License, Version 2.0 (the "License"); you may
9
# not use this file except in compliance with the License. You may obtain
10
# a copy of the License at
12
# http://www.apache.org/licenses/LICENSE-2.0
14
# Unless required by applicable law or agreed to in writing, software
15
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17
# License for the specific language governing permissions and limitations
21
Helper methods to deal with images.
23
This is essentially a copy from nova.virt.images.py
24
Some slight modifications, but at some point
25
we should look at maybe pushign this up to OSLO
32
from cinder import exception
33
from cinder import flags
34
from cinder.openstack.common import cfg
35
from cinder.openstack.common import log as logging
36
from cinder import utils
39
LOG = logging.getLogger(__name__)
41
image_helper_opt = [cfg.StrOpt('image_conversion_dir',
43
help='parent dir for tempdir used for image conversion'), ]
46
FLAGS.register_opts(image_helper_opt)
49
class QemuImgInfo(object):
50
BACKING_FILE_RE = re.compile((r"^(.*?)\s*\(actual\s+path\s*:"
51
r"\s+(.*?)\)\s*$"), re.I)
52
TOP_LEVEL_RE = re.compile(r"^([\w\d\s\_\-]+):(.*)$")
53
SIZE_RE = re.compile(r"\(\s*(\d+)\s+bytes\s*\)", re.I)
55
def __init__(self, cmd_output):
56
details = self._parse(cmd_output)
57
self.image = details.get('image')
58
self.backing_file = details.get('backing_file')
59
self.file_format = details.get('file_format')
60
self.virtual_size = details.get('virtual_size')
61
self.cluster_size = details.get('cluster_size')
62
self.disk_size = details.get('disk_size')
63
self.snapshots = details.get('snapshot_list', [])
64
self.encryption = details.get('encryption')
68
'image: %s' % self.image,
69
'file_format: %s' % self.file_format,
70
'virtual_size: %s' % self.virtual_size,
71
'disk_size: %s' % self.disk_size,
72
'cluster_size: %s' % self.cluster_size,
73
'backing_file: %s' % self.backing_file,
76
lines.append("snapshots: %s" % self.snapshots)
77
return "\n".join(lines)
79
def _canonicalize(self, field):
80
# Standardize on underscores/lc/no dash and no spaces
81
# since qemu seems to have mixed outputs here... and
82
# this format allows for better integration with python
83
# - ie for usage in kwargs and such...
84
field = field.lower().strip()
86
field = field.replace(c, '_')
89
def _extract_bytes(self, details):
90
# Replace it with the byte amount
91
real_size = self.SIZE_RE.search(details)
93
details = real_size.group(1)
95
details = utils.to_bytes(details)
96
except (TypeError, ValueError):
100
def _extract_details(self, root_cmd, root_details, lines_after):
102
real_details = root_details
103
if root_cmd == 'backing_file':
104
# Replace it with the real backing file
105
backing_match = self.BACKING_FILE_RE.match(root_details)
107
real_details = backing_match.group(2).strip()
108
elif root_cmd in ['virtual_size', 'cluster_size', 'disk_size']:
109
# Replace it with the byte amount (if we can convert it)
110
real_details = self._extract_bytes(root_details)
111
elif root_cmd == 'file_format':
112
real_details = real_details.strip().lower()
113
elif root_cmd == 'snapshot_list':
114
# Next line should be a header, starting with 'ID'
115
if not lines_after or not lines_after[0].startswith("ID"):
116
msg = _("Snapshot list encountered but no header found!")
117
raise ValueError(msg)
119
possible_contents = lines_after[1:]
121
# This is the sprintf pattern we will try to match
122
# "%-10s%-20s%7s%20s%15s"
123
# ID TAG VM SIZE DATE VM CLOCK (current header)
124
for line in possible_contents:
125
line_pieces = line.split(None)
126
if len(line_pieces) != 6:
129
# Check against this pattern occuring in the final position
130
# "%02d:%02d:%02d.%03d"
131
date_pieces = line_pieces[5].split(":")
132
if len(date_pieces) != 3:
134
real_details.append({
135
'id': line_pieces[0],
136
'tag': line_pieces[1],
137
'vm_size': line_pieces[2],
138
'date': line_pieces[3],
139
'vm_clock': line_pieces[4] + " " + line_pieces[5],
142
return (real_details, consumed_lines)
144
def _parse(self, cmd_output):
145
# Analysis done of qemu-img.c to figure out what is going on here
146
# Find all points start with some chars and then a ':' then a newline
147
# and then handle the results of those 'top level' items in a separate
150
# TODO(harlowja): newer versions might have a json output format
151
# we should switch to that whenever possible.
152
# see: http://bit.ly/XLJXDX
156
lines = cmd_output.splitlines()
165
top_level = self.TOP_LEVEL_RE.match(line)
167
root = self._canonicalize(top_level.group(1))
171
root_details = top_level.group(2).strip()
172
details, consumed_lines = self._extract_details(root,
175
contents[root] = details
176
i += consumed_lines + 1
180
def qemu_img_info(path):
181
"""Return a object containing the parsed output from qemu-img info."""
182
out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
183
'qemu-img', 'info', path,
185
return QemuImgInfo(out)
188
def convert_image(source, dest, out_format):
189
"""Convert image to other format"""
190
cmd = ('qemu-img', 'convert', '-O', out_format, source, dest)
191
utils.execute(*cmd, run_as_root=True)
194
def fetch(context, image_service, image_id, path, _user_id, _project_id):
195
# TODO(vish): Improve context handling and add owner and auth data
196
# when it is added to glance. Right now there is no
197
# auth checking in glance, so we assume that access was
198
# checked before we got here.
199
with utils.remove_path_on_error(path):
200
with open(path, "wb") as image_file:
201
image_service.download(context, image_id, image_file)
204
def fetch_to_raw(context, image_service,
206
user_id=None, project_id=None):
207
if (FLAGS.image_conversion_dir and not
208
os.path.exists(FLAGS.image_conversion_dir)):
209
os.makedirs(FLAGS.image_conversion_dir)
211
fd, tmp = tempfile.mkstemp(dir=FLAGS.image_conversion_dir)
213
with utils.remove_path_on_error(tmp):
214
fetch(context, image_service, image_id, tmp, user_id, project_id)
216
data = qemu_img_info(tmp)
217
fmt = data.file_format
219
raise exception.ImageUnacceptable(
220
reason=_("'qemu-img info' parsing failed."),
223
backing_file = data.backing_file
224
if backing_file is not None:
225
raise exception.ImageUnacceptable(
227
reason=_("fmt=%(fmt)s backed by:"
228
"%(backing_file)s") % locals())
230
# NOTE(jdg): I'm using qemu-img convert to write
231
# to the volume regardless if it *needs* conversion or not
232
LOG.debug("%s was %s, converting to raw" % (image_id, fmt))
233
convert_image(tmp, dest, 'raw')
235
data = qemu_img_info(dest)
236
if data.file_format != "raw":
237
raise exception.ImageUnacceptable(
239
reason=_("Converted to raw, but format is now %s") %