1 Upstream patch fixed in Grizzly 2013.1.5, Havana 2013.2.1, Icehouse |
|
2 |
|
3 commit 135faa7b5d9855312bedc19e5e1ecebae34d3d18 |
|
4 Author: Pádraig Brady <[email protected]> |
|
5 Date: Fri Sep 27 04:07:14 2013 +0100 |
|
6 |
|
7 ensure we don't boot oversized images |
|
8 |
|
9 Since we can't generally shrink incoming images, add extra checks |
|
10 to ensure oversized images are not allowed through. |
|
11 All cases when populating the libvirt image cache are now handled, |
|
12 including the initial download from glance, where we avoid |
|
13 converting to raw, as that could generate non sparse images |
|
14 much larger than the downloaded image. |
|
15 |
|
16 * nova/virt/libvirt/utils.py (fetch_image): Allow passing through |
|
17 of the max_size parameter. |
|
18 * nova/virt/images.py (fetch_to_raw): Accept the max_size parameter, |
|
19 and use it to discard images with larger (virtual) sizes. |
|
20 * nova/virt/libvirt/imagebackend.py (verify_base_size): A new |
|
21 refactored function to identify and raise exception to oversized images. |
|
22 (Raw.create_image): Pass the max_size to the fetch function. |
|
23 Also enforce virtual image size checking for already fetched images, |
|
24 as this class (despite the name) can be handling qcow files. |
|
25 (Qcow2.create_image): Pass the max_size to the fetch function, |
|
26 or verify the virtual size for the instance as done previously. |
|
27 (Lvm.create_image): Pass the max_size to the fetch function. |
|
28 Also check the size before transferring to the volume to improve |
|
29 efficiency by not even attempting the transfer of oversized images. |
|
30 (Rbd.create_image): Likewise. |
|
31 * nova/tests/fake_libvirt_utils.py: Support max_size arg. |
|
32 * nova/tests/test_libvirt.py (test_fetch_raw_image): |
|
33 Add a case to check oversized images are discarded. |
|
34 * nova/tests/test_imagebackend.py (test_create_image_too_small): |
|
35 Adjust to avoid the fetch size check. |
|
36 |
|
37 Fixes bug: 1177830 |
|
38 Fixes bug: 1206081 |
|
39 |
|
40 Conflicts: |
|
41 |
|
42 nova/tests/test_imagebackend.py |
|
43 nova/virt/libvirt/imagebackend.py |
|
44 |
|
45 Change-Id: Idc35fce580be4f74e23883d1b4bea6475c3f6e30 |
|
46 |
|
47 diff --git a/nova/tests/fake_libvirt_utils.py b/nova/tests/fake_libvirt_utils.py |
|
48 index 23b758e..ecf357a 100644 |
|
49 --- a/nova/tests/fake_libvirt_utils.py |
|
50 +++ b/nova/tests/fake_libvirt_utils.py |
|
51 @@ -193,7 +193,7 @@ def get_fs_info(path): |
|
52 'free': 84 * (1024 ** 3)} |
|
53 |
|
54 |
|
55 -def fetch_image(context, target, image_id, user_id, project_id): |
|
56 +def fetch_image(context, target, image_id, user_id, project_id, max_size=0): |
|
57 pass |
|
58 |
|
59 |
|
60 diff --git a/nova/tests/test_imagebackend.py b/nova/tests/test_imagebackend.py |
|
61 index 77446e8..93ed23d 100644 |
|
62 --- a/nova/tests/test_imagebackend.py |
|
63 +++ b/nova/tests/test_imagebackend.py |
|
64 @@ -189,7 +189,7 @@ class RawTestCase(_ImageTestCase, test.TestCase): |
|
65 |
|
66 def test_create_image(self): |
|
67 fn = self.prepare_mocks() |
|
68 - fn(target=self.TEMPLATE_PATH, image_id=None) |
|
69 + fn(target=self.TEMPLATE_PATH, max_size=None, image_id=None) |
|
70 imagebackend.libvirt_utils.copy_image(self.TEMPLATE_PATH, self.PATH) |
|
71 self.mox.ReplayAll() |
|
72 |
|
73 @@ -210,7 +210,7 @@ class RawTestCase(_ImageTestCase, test.TestCase): |
|
74 |
|
75 def test_create_image_extend(self): |
|
76 fn = self.prepare_mocks() |
|
77 - fn(target=self.TEMPLATE_PATH, image_id=None) |
|
78 + fn(max_size=self.SIZE, target=self.TEMPLATE_PATH, image_id=None) |
|
79 imagebackend.libvirt_utils.copy_image(self.TEMPLATE_PATH, self.PATH) |
|
80 imagebackend.disk.extend(self.PATH, self.SIZE) |
|
81 self.mox.ReplayAll() |
|
82 @@ -260,7 +260,7 @@ class Qcow2TestCase(_ImageTestCase, test.TestCase): |
|
83 |
|
84 def test_create_image(self): |
|
85 fn = self.prepare_mocks() |
|
86 - fn(target=self.TEMPLATE_PATH) |
|
87 + fn(max_size=None, target=self.TEMPLATE_PATH) |
|
88 imagebackend.libvirt_utils.create_cow_image(self.TEMPLATE_PATH, |
|
89 self.PATH) |
|
90 self.mox.ReplayAll() |
|
91 @@ -272,15 +272,12 @@ class Qcow2TestCase(_ImageTestCase, test.TestCase): |
|
92 |
|
93 def test_create_image_with_size(self): |
|
94 fn = self.prepare_mocks() |
|
95 - fn(target=self.TEMPLATE_PATH) |
|
96 + fn(max_size=self.SIZE, target=self.TEMPLATE_PATH) |
|
97 self.mox.StubOutWithMock(os.path, 'exists') |
|
98 - self.mox.StubOutWithMock(imagebackend.disk, 'get_disk_size') |
|
99 if self.OLD_STYLE_INSTANCE_PATH: |
|
100 os.path.exists(self.OLD_STYLE_INSTANCE_PATH).AndReturn(False) |
|
101 os.path.exists(self.TEMPLATE_PATH).AndReturn(False) |
|
102 os.path.exists(self.PATH).AndReturn(False) |
|
103 - imagebackend.disk.get_disk_size(self.TEMPLATE_PATH |
|
104 - ).AndReturn(self.SIZE) |
|
105 os.path.exists(self.PATH).AndReturn(False) |
|
106 imagebackend.libvirt_utils.create_cow_image(self.TEMPLATE_PATH, |
|
107 self.PATH) |
|
108 @@ -294,27 +291,24 @@ class Qcow2TestCase(_ImageTestCase, test.TestCase): |
|
109 |
|
110 def test_create_image_too_small(self): |
|
111 fn = self.prepare_mocks() |
|
112 - fn(target=self.TEMPLATE_PATH) |
|
113 self.mox.StubOutWithMock(os.path, 'exists') |
|
114 self.mox.StubOutWithMock(imagebackend.disk, 'get_disk_size') |
|
115 if self.OLD_STYLE_INSTANCE_PATH: |
|
116 os.path.exists(self.OLD_STYLE_INSTANCE_PATH).AndReturn(False) |
|
117 - os.path.exists(self.TEMPLATE_PATH).AndReturn(False) |
|
118 - os.path.exists(self.PATH).AndReturn(False) |
|
119 + os.path.exists(self.TEMPLATE_PATH).AndReturn(True) |
|
120 imagebackend.disk.get_disk_size(self.TEMPLATE_PATH |
|
121 ).AndReturn(self.SIZE) |
|
122 self.mox.ReplayAll() |
|
123 |
|
124 image = self.image_class(self.INSTANCE, self.NAME) |
|
125 - self.assertRaises(exception.ImageTooLarge, image.create_image, fn, |
|
126 - self.TEMPLATE_PATH, 1) |
|
127 + self.assertRaises(exception.InstanceTypeDiskTooSmall, |
|
128 + image.create_image, fn, self.TEMPLATE_PATH, 1) |
|
129 self.mox.VerifyAll() |
|
130 |
|
131 def test_generate_resized_backing_files(self): |
|
132 fn = self.prepare_mocks() |
|
133 - fn(target=self.TEMPLATE_PATH) |
|
134 + fn(max_size=self.SIZE, target=self.TEMPLATE_PATH) |
|
135 self.mox.StubOutWithMock(os.path, 'exists') |
|
136 - self.mox.StubOutWithMock(imagebackend.disk, 'get_disk_size') |
|
137 self.mox.StubOutWithMock(imagebackend.libvirt_utils, |
|
138 'get_disk_backing_file') |
|
139 if self.OLD_STYLE_INSTANCE_PATH: |
|
140 @@ -329,8 +323,6 @@ class Qcow2TestCase(_ImageTestCase, test.TestCase): |
|
141 self.QCOW2_BASE) |
|
142 imagebackend.disk.extend(self.QCOW2_BASE, self.SIZE) |
|
143 |
|
144 - imagebackend.disk.get_disk_size(self.TEMPLATE_PATH |
|
145 - ).AndReturn(self.SIZE) |
|
146 os.path.exists(self.PATH).AndReturn(True) |
|
147 self.mox.ReplayAll() |
|
148 |
|
149 @@ -341,9 +333,8 @@ class Qcow2TestCase(_ImageTestCase, test.TestCase): |
|
150 |
|
151 def test_qcow2_exists_and_has_no_backing_file(self): |
|
152 fn = self.prepare_mocks() |
|
153 - fn(target=self.TEMPLATE_PATH) |
|
154 + fn(max_size=self.SIZE, target=self.TEMPLATE_PATH) |
|
155 self.mox.StubOutWithMock(os.path, 'exists') |
|
156 - self.mox.StubOutWithMock(imagebackend.disk, 'get_disk_size') |
|
157 self.mox.StubOutWithMock(imagebackend.libvirt_utils, |
|
158 'get_disk_backing_file') |
|
159 if self.OLD_STYLE_INSTANCE_PATH: |
|
160 @@ -353,8 +344,6 @@ class Qcow2TestCase(_ImageTestCase, test.TestCase): |
|
161 |
|
162 imagebackend.libvirt_utils.get_disk_backing_file(self.PATH)\ |
|
163 .AndReturn(None) |
|
164 - imagebackend.disk.get_disk_size(self.TEMPLATE_PATH |
|
165 - ).AndReturn(self.SIZE) |
|
166 os.path.exists(self.PATH).AndReturn(True) |
|
167 self.mox.ReplayAll() |
|
168 |
|
169 @@ -391,7 +380,7 @@ class LvmTestCase(_ImageTestCase, test.TestCase): |
|
170 |
|
171 def _create_image(self, sparse): |
|
172 fn = self.prepare_mocks() |
|
173 - fn(target=self.TEMPLATE_PATH) |
|
174 + fn(max_size=None, target=self.TEMPLATE_PATH) |
|
175 self.libvirt_utils.create_lvm_image(self.VG, |
|
176 self.LV, |
|
177 self.TEMPLATE_SIZE, |
|
178 @@ -423,7 +412,7 @@ class LvmTestCase(_ImageTestCase, test.TestCase): |
|
179 |
|
180 def _create_image_resize(self, sparse): |
|
181 fn = self.prepare_mocks() |
|
182 - fn(target=self.TEMPLATE_PATH) |
|
183 + fn(max_size=self.SIZE, target=self.TEMPLATE_PATH) |
|
184 self.libvirt_utils.create_lvm_image(self.VG, self.LV, |
|
185 self.SIZE, sparse=sparse) |
|
186 self.disk.get_disk_size(self.TEMPLATE_PATH |
|
187 @@ -462,7 +451,7 @@ class LvmTestCase(_ImageTestCase, test.TestCase): |
|
188 |
|
189 def test_create_image_negative(self): |
|
190 fn = self.prepare_mocks() |
|
191 - fn(target=self.TEMPLATE_PATH) |
|
192 + fn(max_size=self.SIZE, target=self.TEMPLATE_PATH) |
|
193 self.libvirt_utils.create_lvm_image(self.VG, |
|
194 self.LV, |
|
195 self.SIZE, |
|
196 diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py |
|
197 index d8c4cf2..e422ec7 100644 |
|
198 --- a/nova/tests/test_libvirt.py |
|
199 +++ b/nova/tests/test_libvirt.py |
|
200 @@ -4826,7 +4826,8 @@ disk size: 4.4M''', '')) |
|
201 image_id = '4' |
|
202 user_id = 'fake' |
|
203 project_id = 'fake' |
|
204 - images.fetch_to_raw(context, image_id, target, user_id, project_id) |
|
205 + images.fetch_to_raw(context, image_id, target, user_id, project_id, |
|
206 + max_size=0) |
|
207 |
|
208 self.mox.ReplayAll() |
|
209 libvirt_utils.fetch_image(context, target, image_id, |
|
210 @@ -4856,20 +4857,27 @@ disk size: 4.4M''', '')) |
|
211 file_format = path.split('.')[-2] |
|
212 elif file_format == 'converted': |
|
213 file_format = 'raw' |
|
214 + |
|
215 if 'backing' in path: |
|
216 backing_file = 'backing' |
|
217 else: |
|
218 backing_file = None |
|
219 |
|
220 + if 'big' in path: |
|
221 + virtual_size = 2 |
|
222 + else: |
|
223 + virtual_size = 1 |
|
224 + |
|
225 FakeImgInfo.file_format = file_format |
|
226 FakeImgInfo.backing_file = backing_file |
|
227 + FakeImgInfo.virtual_size = virtual_size |
|
228 |
|
229 return FakeImgInfo() |
|
230 |
|
231 self.stubs.Set(utils, 'execute', fake_execute) |
|
232 self.stubs.Set(os, 'rename', fake_rename) |
|
233 self.stubs.Set(os, 'unlink', fake_unlink) |
|
234 - self.stubs.Set(images, 'fetch', lambda *_: None) |
|
235 + self.stubs.Set(images, 'fetch', lambda *_, **__: None) |
|
236 self.stubs.Set(images, 'qemu_img_info', fake_qemu_img_info) |
|
237 self.stubs.Set(utils, 'delete_if_exists', fake_rm_on_errror) |
|
238 |
|
239 @@ -4884,7 +4892,8 @@ disk size: 4.4M''', '')) |
|
240 't.qcow2.part', 't.qcow2.converted'), |
|
241 ('rm', 't.qcow2.part'), |
|
242 ('mv', 't.qcow2.converted', 't.qcow2')] |
|
243 - images.fetch_to_raw(context, image_id, target, user_id, project_id) |
|
244 + images.fetch_to_raw(context, image_id, target, user_id, project_id, |
|
245 + max_size=1) |
|
246 self.assertEqual(self.executes, expected_commands) |
|
247 |
|
248 target = 't.raw' |
|
249 @@ -4901,6 +4910,15 @@ disk size: 4.4M''', '')) |
|
250 context, image_id, target, user_id, project_id) |
|
251 self.assertEqual(self.executes, expected_commands) |
|
252 |
|
253 + target = 'big.qcow2' |
|
254 + self.executes = [] |
|
255 + expected_commands = [('rm', '-f', 'big.qcow2.part')] |
|
256 + self.assertRaises(exception.InstanceTypeDiskTooSmall, |
|
257 + images.fetch_to_raw, |
|
258 + context, image_id, target, user_id, project_id, |
|
259 + max_size=1) |
|
260 + self.assertEqual(self.executes, expected_commands) |
|
261 + |
|
262 del self.executes |
|
263 |
|
264 def test_get_disk_backing_file(self): |
|
265 diff --git a/nova/virt/images.py b/nova/virt/images.py |
|
266 index b40f566..541779a 100755 |
|
267 --- a/nova/virt/images.py |
|
268 +++ b/nova/virt/images.py |
|
269 @@ -190,7 +190,7 @@ def convert_image(source, dest, out_format, run_as_root=False): |
|
270 utils.execute(*cmd, run_as_root=run_as_root) |
|
271 |
|
272 |
|
273 -def fetch(context, image_href, path, _user_id, _project_id): |
|
274 +def fetch(context, image_href, path, _user_id, _project_id, max_size=0): |
|
275 # TODO(vish): Improve context handling and add owner and auth data |
|
276 # when it is added to glance. Right now there is no |
|
277 # auth checking in glance, so we assume that access was |
|
278 @@ -202,9 +202,10 @@ def fetch(context, image_href, path, _user_id, _project_id): |
|
279 image_service.download(context, image_id, image_file) |
|
280 |
|
281 |
|
282 -def fetch_to_raw(context, image_href, path, user_id, project_id): |
|
283 +def fetch_to_raw(context, image_href, path, user_id, project_id, max_size=0): |
|
284 path_tmp = "%s.part" % path |
|
285 - fetch(context, image_href, path_tmp, user_id, project_id) |
|
286 + fetch(context, image_href, path_tmp, user_id, project_id, |
|
287 + max_size=max_size) |
|
288 |
|
289 with utils.remove_path_on_error(path_tmp): |
|
290 data = qemu_img_info(path_tmp) |
|
291 @@ -220,6 +221,23 @@ def fetch_to_raw(context, image_href, path, user_id, project_id): |
|
292 raise exception.ImageUnacceptable(image_id=image_href, |
|
293 reason=_("fmt=%(fmt)s backed by: %(backing_file)s") % locals()) |
|
294 |
|
295 + # We can't generally shrink incoming images, so disallow |
|
296 + # images > size of the flavor we're booting. Checking here avoids |
|
297 + # an immediate DoS where we convert large qcow images to raw |
|
298 + # (which may compress well but not be sparse). |
|
299 + # TODO(p-draigbrady): loop through all flavor sizes, so that |
|
300 + # we might continue here and not discard the download. |
|
301 + # If we did that we'd have to do the higher level size checks |
|
302 + # irrespective of whether the base image was prepared or not. |
|
303 + disk_size = data.virtual_size |
|
304 + if max_size and max_size < disk_size: |
|
305 + msg = _('%(base)s virtual size %(disk_size)s ' |
|
306 + 'larger than flavor root disk size %(size)s') |
|
307 + LOG.error(msg % {'base': path, |
|
308 + 'disk_size': disk_size, |
|
309 + 'size': max_size}) |
|
310 + raise exception.InstanceTypeDiskTooSmall() |
|
311 + |
|
312 if fmt != "raw" and CONF.force_raw_images: |
|
313 staged = "%s.converted" % path |
|
314 LOG.debug("%s was %s, converting to raw" % (image_href, fmt)) |
|
315 diff --git a/nova/virt/libvirt/imagebackend.py b/nova/virt/libvirt/imagebackend.py |
|
316 index e2c7ccf..dc85c97 100755 |
|
317 --- a/nova/virt/libvirt/imagebackend.py |
|
318 +++ b/nova/virt/libvirt/imagebackend.py |
|
319 @@ -177,6 +177,36 @@ class Image(object): |
|
320 (CONF.preallocate_images, self.path)) |
|
321 return can_fallocate |
|
322 |
|
323 + @staticmethod |
|
324 + def verify_base_size(base, size, base_size=0): |
|
325 + """Check that the base image is not larger than size. |
|
326 + Since images can't be generally shrunk, enforce this |
|
327 + constraint taking account of virtual image size. |
|
328 + """ |
|
329 + |
|
330 + # Note(pbrady): The size and min_disk parameters of a glance |
|
331 + # image are checked against the instance size before the image |
|
332 + # is even downloaded from glance, but currently min_disk is |
|
333 + # adjustable and doesn't currently account for virtual disk size, |
|
334 + # so we need this extra check here. |
|
335 + # NOTE(cfb): Having a flavor that sets the root size to 0 and having |
|
336 + # nova effectively ignore that size and use the size of the |
|
337 + # image is considered a feature at this time, not a bug. |
|
338 + |
|
339 + if size is None: |
|
340 + return |
|
341 + |
|
342 + if size and not base_size: |
|
343 + base_size = disk.get_disk_size(base) |
|
344 + |
|
345 + if size < base_size: |
|
346 + msg = _('%(base)s virtual size %(base_size)s ' |
|
347 + 'larger than flavor root disk size %(size)s') |
|
348 + LOG.error(msg % {'base': base, |
|
349 + 'base_size': base_size, |
|
350 + 'size': size}) |
|
351 + raise exception.InstanceTypeDiskTooSmall() |
|
352 + |
|
353 def snapshot_create(self): |
|
354 raise NotImplementedError |
|
355 |
|
356 @@ -217,7 +247,8 @@ class Raw(Image): |
|
357 #Generating image in place |
|
358 prepare_template(target=self.path, *args, **kwargs) |
|
359 else: |
|
360 - prepare_template(target=base, *args, **kwargs) |
|
361 + prepare_template(target=base, max_size=size, *args, **kwargs) |
|
362 + self.verify_base_size(base, size) |
|
363 if not os.path.exists(self.path): |
|
364 with utils.remove_path_on_error(self.path): |
|
365 copy_raw_image(base, self.path, size) |
|
366 @@ -257,7 +288,9 @@ class Qcow2(Image): |
|
367 |
|
368 # Download the unmodified base image unless we already have a copy. |
|
369 if not os.path.exists(base): |
|
370 - prepare_template(target=base, *args, **kwargs) |
|
371 + prepare_template(target=base, max_size=size, *args, **kwargs) |
|
372 + else: |
|
373 + self.verify_base_size(base, size) |
|
374 |
|
375 legacy_backing_size = None |
|
376 legacy_base = base |
|
377 @@ -283,13 +316,6 @@ class Qcow2(Image): |
|
378 libvirt_utils.copy_image(base, legacy_base) |
|
379 disk.extend(legacy_base, legacy_backing_size) |
|
380 |
|
381 - # NOTE(cfb): Having a flavor that sets the root size to 0 and having |
|
382 - # nova effectively ignore that size and use the size of the |
|
383 - # image is considered a feature at this time, not a bug. |
|
384 - if size and size < disk.get_disk_size(base): |
|
385 - LOG.error('%s virtual size larger than flavor root disk size %s' % |
|
386 - (base, size)) |
|
387 - raise exception.ImageTooLarge() |
|
388 if not os.path.exists(self.path): |
|
389 with utils.remove_path_on_error(self.path): |
|
390 copy_qcow2_image(base, self.path, size) |
|
391 @@ -348,6 +374,7 @@ class Lvm(Image): |
|
392 lock_path=self.lock_path) |
|
393 def create_lvm_image(base, size): |
|
394 base_size = disk.get_disk_size(base) |
|
395 + self.verify_base_size(base, size, base_size=base_size) |
|
396 resize = size > base_size |
|
397 size = size if resize else base_size |
|
398 libvirt_utils.create_lvm_image(self.vg, self.lv, |
|
399 @@ -365,7 +392,7 @@ class Lvm(Image): |
|
400 with self.remove_volume_on_error(self.path): |
|
401 prepare_template(target=self.path, *args, **kwargs) |
|
402 else: |
|
403 - prepare_template(target=base, *args, **kwargs) |
|
404 + prepare_template(target=base, max_size=size, *args, **kwargs) |
|
405 with self.remove_volume_on_error(self.path): |
|
406 create_lvm_image(base, size) |
|
407 |
|
408 diff --git a/nova/virt/libvirt/utils.py b/nova/virt/libvirt/utils.py |
|
409 index 6972243..4c31fcb 100755 |
|
410 --- a/nova/virt/libvirt/utils.py |
|
411 +++ b/nova/virt/libvirt/utils.py |
|
412 @@ -592,9 +592,10 @@ def get_fs_info(path): |
|
413 'used': used} |
|
414 |
|
415 |
|
416 -def fetch_image(context, target, image_id, user_id, project_id): |
|
417 +def fetch_image(context, target, image_id, user_id, project_id, max_size=0): |
|
418 """Grab image.""" |
|
419 - images.fetch_to_raw(context, image_id, target, user_id, project_id) |
|
420 + images.fetch_to_raw(context, image_id, target, user_id, project_id, |
|
421 + max_size=max_size) |
|
422 |
|
423 |
|
424 def get_instance_path(instance, forceold=False, relative=False): |
|