~ubuntu-branches/ubuntu/raring/cinder/raring-updates

« back to all changes in this revision

Viewing changes to cinder/image/image_utils.py

Tags: upstream-2013.1~g2
ImportĀ upstreamĀ versionĀ 2013.1~g2

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
2
 
 
3
# Copyright 2010 United States Government as represented by the
 
4
# Administrator of the National Aeronautics and Space Administration.
 
5
# All Rights Reserved.
 
6
# Copyright (c) 2010 Citrix Systems, Inc.
 
7
#
 
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
 
11
#
 
12
#         http://www.apache.org/licenses/LICENSE-2.0
 
13
#
 
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
 
18
#    under the License.
 
19
 
 
20
"""
 
21
Helper methods to deal with images.
 
22
 
 
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
 
26
"""
 
27
 
 
28
import os
 
29
import re
 
30
import tempfile
 
31
 
 
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
 
37
 
 
38
 
 
39
LOG = logging.getLogger(__name__)
 
40
 
 
41
image_helper_opt = [cfg.StrOpt('image_conversion_dir',
 
42
                    default='/tmp',
 
43
                    help='parent dir for tempdir used for image conversion'), ]
 
44
 
 
45
FLAGS = flags.FLAGS
 
46
FLAGS.register_opts(image_helper_opt)
 
47
 
 
48
 
 
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)
 
54
 
 
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')
 
65
 
 
66
    def __str__(self):
 
67
        lines = [
 
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,
 
74
        ]
 
75
        if self.snapshots:
 
76
            lines.append("snapshots: %s" % self.snapshots)
 
77
        return "\n".join(lines)
 
78
 
 
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()
 
85
        for c in (" ", "-"):
 
86
            field = field.replace(c, '_')
 
87
        return field
 
88
 
 
89
    def _extract_bytes(self, details):
 
90
        # Replace it with the byte amount
 
91
        real_size = self.SIZE_RE.search(details)
 
92
        if real_size:
 
93
            details = real_size.group(1)
 
94
        try:
 
95
            details = utils.to_bytes(details)
 
96
        except (TypeError, ValueError):
 
97
            pass
 
98
        return details
 
99
 
 
100
    def _extract_details(self, root_cmd, root_details, lines_after):
 
101
        consumed_lines = 0
 
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)
 
106
            if backing_match:
 
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)
 
118
            consumed_lines += 1
 
119
            possible_contents = lines_after[1:]
 
120
            real_details = []
 
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:
 
127
                    break
 
128
                else:
 
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:
 
133
                        break
 
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],
 
140
                    })
 
141
                    consumed_lines += 1
 
142
        return (real_details, consumed_lines)
 
143
 
 
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
 
148
        # function.
 
149
        #
 
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
 
153
        if not cmd_output:
 
154
            cmd_output = ''
 
155
        contents = {}
 
156
        lines = cmd_output.splitlines()
 
157
        i = 0
 
158
        line_am = len(lines)
 
159
        while i < line_am:
 
160
            line = lines[i]
 
161
            if not line.strip():
 
162
                i += 1
 
163
                continue
 
164
            consumed_lines = 0
 
165
            top_level = self.TOP_LEVEL_RE.match(line)
 
166
            if top_level:
 
167
                root = self._canonicalize(top_level.group(1))
 
168
                if not root:
 
169
                    i += 1
 
170
                    continue
 
171
                root_details = top_level.group(2).strip()
 
172
                details, consumed_lines = self._extract_details(root,
 
173
                                                                root_details,
 
174
                                                                lines[i + 1:])
 
175
                contents[root] = details
 
176
            i += consumed_lines + 1
 
177
        return contents
 
178
 
 
179
 
 
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,
 
184
                             run_as_root=True)
 
185
    return QemuImgInfo(out)
 
186
 
 
187
 
 
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)
 
192
 
 
193
 
 
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)
 
202
 
 
203
 
 
204
def fetch_to_raw(context, image_service,
 
205
                 image_id, dest,
 
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)
 
210
 
 
211
    fd, tmp = tempfile.mkstemp(dir=FLAGS.image_conversion_dir)
 
212
    os.close(fd)
 
213
    with utils.remove_path_on_error(tmp):
 
214
        fetch(context, image_service, image_id, tmp, user_id, project_id)
 
215
 
 
216
        data = qemu_img_info(tmp)
 
217
        fmt = data.file_format
 
218
        if fmt is None:
 
219
            raise exception.ImageUnacceptable(
 
220
                reason=_("'qemu-img info' parsing failed."),
 
221
                image_id=image_id)
 
222
 
 
223
        backing_file = data.backing_file
 
224
        if backing_file is not None:
 
225
            raise exception.ImageUnacceptable(
 
226
                image_id=image_id,
 
227
                reason=_("fmt=%(fmt)s backed by:"
 
228
                         "%(backing_file)s") % locals())
 
229
 
 
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')
 
234
 
 
235
        data = qemu_img_info(dest)
 
236
        if data.file_format != "raw":
 
237
            raise exception.ImageUnacceptable(
 
238
                image_id=image_id,
 
239
                reason=_("Converted to raw, but format is now %s") %
 
240
                data.file_format)
 
241
        os.unlink(tmp)