39
39
help='Force backing images to raw format'),
43
FLAGS.register_opts(image_opts)
43
CONF.register_opts(image_opts)
46
class QemuImgInfo(object):
47
BACKING_FILE_RE = re.compile((r"^(.*?)\s*\(actual\s+path\s*:"
48
r"\s+(.*?)\)\s*$"), re.I)
49
TOP_LEVEL_RE = re.compile(r"^([\w\d\s\_\-]+):(.*)$")
50
SIZE_RE = re.compile(r"\(\s*(\d+)\s+bytes\s*\)", re.I)
52
def __init__(self, cmd_output):
53
details = self._parse(cmd_output)
54
self.image = details.get('image')
55
self.backing_file = details.get('backing_file')
56
self.file_format = details.get('file_format')
57
self.virtual_size = details.get('virtual_size')
58
self.cluster_size = details.get('cluster_size')
59
self.disk_size = details.get('disk_size')
60
self.snapshots = details.get('snapshot_list', [])
61
self.encryption = details.get('encryption')
65
'image: %s' % self.image,
66
'file_format: %s' % self.file_format,
67
'virtual_size: %s' % self.virtual_size,
68
'disk_size: %s' % self.disk_size,
69
'cluster_size: %s' % self.cluster_size,
70
'backing_file: %s' % self.backing_file,
73
lines.append("snapshots: %s" % self.snapshots)
74
return "\n".join(lines)
76
def _canonicalize(self, field):
77
# Standardize on underscores/lc/no dash and no spaces
78
# since qemu seems to have mixed outputs here... and
79
# this format allows for better integration with python
80
# - ie for usage in kwargs and such...
81
field = field.lower().strip()
83
field = field.replace(c, '_')
86
def _extract_bytes(self, details):
87
# Replace it with the byte amount
88
real_size = self.SIZE_RE.search(details)
90
details = real_size.group(1)
92
details = utils.to_bytes(details)
93
except (TypeError, ValueError):
97
def _extract_details(self, root_cmd, root_details, lines_after):
99
real_details = root_details
100
if root_cmd == 'backing_file':
101
# Replace it with the real backing file
102
backing_match = self.BACKING_FILE_RE.match(root_details)
104
real_details = backing_match.group(2).strip()
105
elif root_cmd in ['virtual_size', 'cluster_size', 'disk_size']:
106
# Replace it with the byte amount (if we can convert it)
107
real_details = self._extract_bytes(root_details)
108
elif root_cmd == 'file_format':
109
real_details = real_details.strip().lower()
110
elif root_cmd == 'snapshot_list':
111
# Next line should be a header, starting with 'ID'
112
if not lines_after or not lines_after[0].startswith("ID"):
113
msg = _("Snapshot list encountered but no header found!")
114
raise ValueError(msg)
116
possible_contents = lines_after[1:]
118
# This is the sprintf pattern we will try to match
119
# "%-10s%-20s%7s%20s%15s"
120
# ID TAG VM SIZE DATE VM CLOCK (current header)
121
for line in possible_contents:
122
line_pieces = line.split(None)
123
if len(line_pieces) != 6:
126
# Check against this pattern occuring in the final position
127
# "%02d:%02d:%02d.%03d"
128
date_pieces = line_pieces[5].split(":")
129
if len(date_pieces) != 3:
131
real_details.append({
132
'id': line_pieces[0],
133
'tag': line_pieces[1],
134
'vm_size': line_pieces[2],
135
'date': line_pieces[3],
136
'vm_clock': line_pieces[4] + " " + line_pieces[5],
139
return (real_details, consumed_lines)
141
def _parse(self, cmd_output):
142
# Analysis done of qemu-img.c to figure out what is going on here
143
# Find all points start with some chars and then a ':' then a newline
144
# and then handle the results of those 'top level' items in a separate
147
# TODO(harlowja): newer versions might have a json output format
148
# we should switch to that whenever possible.
149
# see: http://bit.ly/XLJXDX
153
lines = cmd_output.splitlines()
162
top_level = self.TOP_LEVEL_RE.match(line)
164
root = self._canonicalize(top_level.group(1))
168
root_details = top_level.group(2).strip()
169
details, consumed_lines = self._extract_details(root,
172
contents[root] = details
173
i += consumed_lines + 1
46
177
def qemu_img_info(path):
47
"""Return a dict containing the parsed output from qemu-img info."""
178
"""Return a object containing the parsed output from qemu-img info."""
49
179
out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
50
180
'qemu-img', 'info', path)
52
# output of qemu-img is 'field: value'
54
for line in out.splitlines():
55
field, val = line.split(':', 1)
181
return QemuImgInfo(out)
184
def convert_image(source, dest, out_format):
185
"""Convert image to other format"""
186
cmd = ('qemu-img', 'convert', '-O', out_format, source, dest)
63
190
def fetch(context, image_href, path, _user_id, _project_id):
79
206
with utils.remove_path_on_error(path_tmp):
80
207
data = qemu_img_info(path_tmp)
82
fmt = data.get('file format')
209
fmt = data.file_format
84
211
raise exception.ImageUnacceptable(
85
212
reason=_("'qemu-img info' parsing failed."),
86
213
image_id=image_href)
88
backing_file = data.get('backing file')
215
backing_file = data.backing_file
89
216
if backing_file is not None:
90
217
raise exception.ImageUnacceptable(image_id=image_href,
91
218
reason=_("fmt=%(fmt)s backed by: %(backing_file)s") % locals())
93
if fmt != "raw" and FLAGS.force_raw_images:
220
if fmt != "raw" and CONF.force_raw_images:
94
221
staged = "%s.converted" % path
95
222
LOG.debug("%s was %s, converting to raw" % (image_href, fmt))
96
223
with utils.remove_path_on_error(staged):
97
utils.execute('qemu-img', 'convert', '-O', 'raw', path_tmp,
224
convert_image(path_tmp, staged, 'raw')
100
226
data = qemu_img_info(staged)
101
if data.get('file format') != "raw":
227
if data.file_format != "raw":
102
228
raise exception.ImageUnacceptable(image_id=image_href,
103
229
reason=_("Converted to raw, but format is now %s") %
104
data.get('file format'))
106
232
os.rename(staged, path)