Skip to content

Commit 4c9a7c8

Browse files
kk7dsm-bull
authored andcommitted
Change force_format strategy to catch mismatches
When we moved the qemu-img command in fetch_to_raw() to force the format to what we expect, we lost the ability to identify and react to situations where qemu-img detected a file as a format that is not supported by us (i.e. identfied and safety-checked by format_inspector). In the case of some of the other VMDK variants that we don't support, we need to be sure to catch any case where qemu-img thinks it's something other than raw when we think it is, which will be the case for those formats we don't support. Note this also moves us from explicitly using the format_inspector that we're told by glance is appropriate, to using our own detection. We assert that we agree with glance and as above, qemu agrees with us. This helps us avoid cases where the uploader lies about the image format, causing us to not run the appropriate safety check. AMI formats are a liability here since we have a very hard time asserting what they are and what they will be detected as later in the pipeline, so there is still special-casing for those. Closes-Bug: #2071734 Change-Id: I4b792c5bc959a904854c21565682ed3a687baa1a (cherry picked from commit 8b4c522)
1 parent b69ae07 commit 4c9a7c8

File tree

3 files changed

+133
-65
lines changed

3 files changed

+133
-65
lines changed

nova/tests/unit/virt/libvirt/test_utils.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -405,12 +405,12 @@ def test_fetch_initrd_image(self, mock_images):
405405
_context, image_id, target, trusted_certs)
406406

407407
@mock.patch.object(images, 'IMAGE_API')
408-
@mock.patch.object(format_inspector, 'get_inspector')
408+
@mock.patch.object(format_inspector, 'detect_file_format')
409409
@mock.patch.object(compute_utils, 'disk_ops_semaphore')
410410
@mock.patch('nova.privsep.utils.supports_direct_io', return_value=True)
411411
@mock.patch('nova.privsep.qemu.unprivileged_convert_image')
412412
def test_fetch_raw_image(self, mock_convert_image, mock_direct_io,
413-
mock_disk_op_sema, mock_gi, mock_glance):
413+
mock_disk_op_sema, mock_detect, mock_glance):
414414

415415
def fake_rename(old, new):
416416
self.executes.append(('mv', old, new))
@@ -450,7 +450,7 @@ class FakeImgInfo(object):
450450
self.stub_out('oslo_utils.fileutils.delete_if_exists',
451451
fake_rm_on_error)
452452

453-
mock_inspector = mock_gi.return_value.from_file.return_value
453+
mock_inspector = mock_detect.return_value
454454

455455
# Since the remove param of fileutils.remove_path_on_error()
456456
# is initialized at load time, we must provide a wrapper
@@ -464,6 +464,7 @@ class FakeImgInfo(object):
464464

465465
# Make sure qcow2 gets converted to raw
466466
mock_inspector.safety_check.return_value = True
467+
mock_inspector.__str__.return_value = 'qcow2'
467468
mock_glance.get.return_value = {'disk_format': 'qcow2'}
468469
target = 't.qcow2'
469470
self.executes = []
@@ -477,12 +478,13 @@ class FakeImgInfo(object):
477478
CONF.instances_path, False)
478479
mock_convert_image.reset_mock()
479480
mock_inspector.safety_check.assert_called_once_with()
480-
mock_gi.assert_called_once_with('qcow2')
481+
mock_detect.assert_called_once_with('t.qcow2.part')
481482

482483
# Make sure raw does not get converted
483-
mock_gi.reset_mock()
484+
mock_detect.reset_mock()
484485
mock_inspector.safety_check.reset_mock()
485486
mock_inspector.safety_check.return_value = True
487+
mock_inspector.__str__.return_value = 'raw'
486488
mock_glance.get.return_value = {'disk_format': 'raw'}
487489
target = 't.raw'
488490
self.executes = []
@@ -491,12 +493,13 @@ class FakeImgInfo(object):
491493
self.assertEqual(self.executes, expected_commands)
492494
mock_convert_image.assert_not_called()
493495
mock_inspector.safety_check.assert_called_once_with()
494-
mock_gi.assert_called_once_with('raw')
496+
mock_detect.assert_called_once_with('t.raw.part')
495497

496498
# Make sure safety check failure prevents us from proceeding
497-
mock_gi.reset_mock()
499+
mock_detect.reset_mock()
498500
mock_inspector.safety_check.reset_mock()
499501
mock_inspector.safety_check.return_value = False
502+
mock_inspector.__str__.return_value = 'qcow2'
500503
mock_glance.get.return_value = {'disk_format': 'qcow2'}
501504
target = 'backing.qcow2'
502505
self.executes = []
@@ -506,10 +509,10 @@ class FakeImgInfo(object):
506509
self.assertEqual(self.executes, expected_commands)
507510
mock_convert_image.assert_not_called()
508511
mock_inspector.safety_check.assert_called_once_with()
509-
mock_gi.assert_called_once_with('qcow2')
512+
mock_detect.assert_called_once_with('backing.qcow2.part')
510513

511514
# Make sure a format mismatch prevents us from proceeding
512-
mock_gi.reset_mock()
515+
mock_detect.reset_mock()
513516
mock_inspector.safety_check.reset_mock()
514517
mock_inspector.safety_check.side_effect = (
515518
format_inspector.ImageFormatError)
@@ -522,7 +525,7 @@ class FakeImgInfo(object):
522525
self.assertEqual(self.executes, expected_commands)
523526
mock_convert_image.assert_not_called()
524527
mock_inspector.safety_check.assert_called_once_with()
525-
mock_gi.assert_called_once_with('qcow2')
528+
mock_detect.assert_called_once_with('backing.qcow2.part')
526529

527530
del self.executes
528531

nova/tests/unit/virt/test_images.py

Lines changed: 79 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121

2222
from nova.compute import utils as compute_utils
2323
from nova import exception
24-
from nova.image import format_inspector
2524
from nova import test
2625
from nova.virt import images
2726

@@ -101,15 +100,16 @@ def test_qemu_img_info_with_disk_not_found(self, exists, mocked_execute):
101100
mocked_execute.assert_called_once()
102101

103102
@mock.patch.object(images, 'IMAGE_API')
104-
@mock.patch('nova.image.format_inspector.get_inspector')
103+
@mock.patch('nova.image.format_inspector.detect_file_format')
105104
@mock.patch.object(images, 'convert_image',
106105
side_effect=exception.ImageUnacceptable)
107106
@mock.patch.object(images, 'qemu_img_info')
108107
@mock.patch.object(images, 'fetch')
109108
def test_fetch_to_raw_errors(self, convert_image, qemu_img_info, fetch,
110-
get_inspector, glance):
111-
inspector = get_inspector.return_value.from_file.return_value
109+
mock_detect, glance):
110+
inspector = mock_detect.return_value
112111
inspector.safety_check.return_value = True
112+
inspector.__str__.return_value = 'qcow2'
113113
glance.get.return_value = {'disk_format': 'qcow2'}
114114
qemu_img_info.backing_file = None
115115
qemu_img_info.file_format = 'qcow2'
@@ -120,16 +120,17 @@ def test_fetch_to_raw_errors(self, convert_image, qemu_img_info, fetch,
120120
None, 'href123', '/no/path')
121121

122122
@mock.patch.object(images, 'IMAGE_API')
123-
@mock.patch('nova.image.format_inspector.get_inspector')
123+
@mock.patch('nova.image.format_inspector.detect_file_format')
124124
@mock.patch.object(images, 'convert_image',
125125
side_effect=exception.ImageUnacceptable)
126126
@mock.patch.object(images, 'qemu_img_info')
127127
@mock.patch.object(images, 'fetch')
128128
def test_fetch_to_raw_data_file(self, convert_image, qemu_img_info_fn,
129-
fetch, mock_gi, mock_glance):
129+
fetch, mock_detect, mock_glance):
130130
mock_glance.get.return_value = {'disk_format': 'qcow2'}
131-
inspector = mock_gi.return_value.from_file.return_value
131+
inspector = mock_detect.return_value
132132
inspector.safety_check.return_value = True
133+
inspector.__str__.return_value = 'qcow2'
133134
# NOTE(danms): the above test needs the following line as well, as it
134135
# is broken without it.
135136
qemu_img_info = qemu_img_info_fn.return_value
@@ -142,16 +143,17 @@ def test_fetch_to_raw_data_file(self, convert_image, qemu_img_info_fn,
142143
images.fetch_to_raw,
143144
None, 'href123', '/no/path')
144145

145-
@mock.patch('nova.image.format_inspector.get_inspector')
146+
@mock.patch('nova.image.format_inspector.detect_file_format')
146147
@mock.patch.object(images, 'IMAGE_API')
147148
@mock.patch('os.rename')
148149
@mock.patch.object(images, 'qemu_img_info')
149150
@mock.patch.object(images, 'fetch')
150151
def test_fetch_to_raw_from_raw(self, fetch, qemu_img_info_fn, mock_rename,
151-
mock_glance, mock_gi):
152+
mock_glance, mock_detect):
152153
# Make sure we support a case where we fetch an already-raw image and
153154
# qemu-img returns None for "format_specific".
154155
mock_glance.get.return_value = {'disk_format': 'raw'}
156+
mock_detect.return_value.__str__.return_value = 'raw'
155157
qemu_img_info = qemu_img_info_fn.return_value
156158
qemu_img_info.file_format = 'raw'
157159
qemu_img_info.backing_file = None
@@ -215,14 +217,15 @@ def test_convert_image_vmdk_allowed_list_checking(self):
215217
format='json'))
216218

217219
@mock.patch.object(images, 'IMAGE_API')
218-
@mock.patch('nova.image.format_inspector.get_inspector')
220+
@mock.patch('nova.image.format_inspector.detect_file_format')
219221
@mock.patch.object(images, 'fetch')
220222
@mock.patch('nova.privsep.qemu.unprivileged_qemu_img_info')
221-
def test_fetch_checks_vmdk_rules(self, mock_info, mock_fetch, mock_gi,
223+
def test_fetch_checks_vmdk_rules(self, mock_info, mock_fetch, mock_detect,
222224
mock_glance):
223225
mock_glance.get.return_value = {'disk_format': 'vmdk'}
224-
inspector = mock_gi.return_value.from_file.return_value
226+
inspector = mock_detect.return_value
225227
inspector.safety_check.return_value = True
228+
inspector.__str__.return_value = 'vmdk'
226229
info = {'format': 'vmdk',
227230
'format-specific': {
228231
'type': 'vmdk',
@@ -235,22 +238,54 @@ def test_fetch_checks_vmdk_rules(self, mock_info, mock_fetch, mock_gi,
235238
images.fetch_to_raw, None, 'foo', 'anypath')
236239
self.assertIn('Invalid VMDK create-type specified', str(e))
237240

241+
@mock.patch('os.rename')
238242
@mock.patch.object(images, 'IMAGE_API')
239243
@mock.patch('nova.image.format_inspector.get_inspector')
244+
@mock.patch('nova.image.format_inspector.detect_file_format')
245+
@mock.patch.object(images, 'fetch')
246+
@mock.patch('nova.privsep.qemu.unprivileged_qemu_img_info')
247+
def test_fetch_iso_is_raw(
248+
self, mock_info, mock_fetch, mock_detect_file_format, mock_gi,
249+
mock_glance, mock_rename):
250+
mock_glance.get.return_value = {'disk_format': 'iso'}
251+
inspector = mock_gi.return_value.from_file.return_value
252+
inspector.safety_check.return_value = True
253+
inspector.__str__.return_value = 'iso'
254+
mock_detect_file_format.return_value = inspector
255+
# qemu-img does not have a parser for iso so it is treated as raw
256+
info = {
257+
"virtual-size": 356352,
258+
"filename": "foo.iso",
259+
"format": "raw",
260+
"actual-size": 356352,
261+
"dirty-flag": False
262+
}
263+
mock_info.return_value = jsonutils.dumps(info)
264+
with mock.patch('os.path.exists', return_value=True):
265+
images.fetch_to_raw(None, 'foo', 'anypath')
266+
# Make sure we called info with -f raw for an iso, since qemu-img does
267+
# not support iso
268+
mock_info.assert_called_once_with('anypath.part', format=None)
269+
# Make sure that since we considered this to be a raw file, we did the
270+
# just-rename-don't-convert path
271+
mock_rename.assert_called_once_with('anypath.part', 'anypath')
272+
273+
@mock.patch.object(images, 'IMAGE_API')
274+
@mock.patch('nova.image.format_inspector.detect_file_format')
240275
@mock.patch.object(images, 'qemu_img_info')
241276
@mock.patch.object(images, 'fetch')
242-
def test_fetch_to_raw_inspector(self, fetch, qemu_img_info, mock_gi,
277+
def test_fetch_to_raw_inspector(self, fetch, qemu_img_info, mock_detect,
243278
mock_glance):
244279
# Image claims to be qcow2, is qcow2, but fails safety check, so we
245280
# abort before qemu-img-info
246281
mock_glance.get.return_value = {'disk_format': 'qcow2'}
247-
inspector = mock_gi.return_value.from_file.return_value
282+
inspector = mock_detect.return_value
248283
inspector.safety_check.return_value = False
284+
inspector.__str__.return_value = 'qcow2'
249285
self.assertRaises(exception.ImageUnacceptable,
250286
images.fetch_to_raw, None, 'href123', '/no.path')
251287
qemu_img_info.assert_not_called()
252-
mock_gi.assert_called_once_with('qcow2')
253-
mock_gi.return_value.from_file.assert_called_once_with('/no.path.part')
288+
mock_detect.assert_called_once_with('/no.path.part')
254289
inspector.safety_check.assert_called_once_with()
255290
mock_glance.get.assert_called_once_with(None, 'href123')
256291

@@ -264,18 +299,17 @@ def test_fetch_to_raw_inspector(self, fetch, qemu_img_info, mock_gi,
264299
# Image claims to be qcow2 in glance, but the image is something else,
265300
# so we abort before qemu-img-info
266301
qemu_img_info.reset_mock()
267-
mock_gi.reset_mock()
302+
mock_detect.reset_mock()
268303
inspector.safety_check.reset_mock()
269-
mock_gi.return_value.from_file.side_effect = (
270-
format_inspector.ImageFormatError)
304+
mock_detect.return_value.__str__.return_value = 'vmdk'
271305
self.assertRaises(exception.ImageUnacceptable,
272306
images.fetch_to_raw, None, 'href123', '/no.path')
273-
mock_gi.assert_called_once_with('qcow2')
274-
inspector.safety_check.assert_not_called()
307+
mock_detect.assert_called_once_with('/no.path.part')
308+
inspector.safety_check.assert_called_once_with()
275309
qemu_img_info.assert_not_called()
276310

277311
@mock.patch.object(images, 'IMAGE_API')
278-
@mock.patch('nova.image.format_inspector.get_inspector')
312+
@mock.patch('nova.image.format_inspector.detect_file_format')
279313
@mock.patch.object(images, 'qemu_img_info')
280314
@mock.patch.object(images, 'fetch')
281315
def test_fetch_to_raw_inspector_disabled(self, fetch, qemu_img_info,
@@ -288,36 +322,41 @@ def test_fetch_to_raw_inspector_disabled(self, fetch, qemu_img_info,
288322
# If deep inspection is disabled, we should never call the inspector
289323
mock_gi.assert_not_called()
290324
# ... and we let qemu-img detect the format itself.
291-
qemu_img_info.assert_called_once_with('/no.path.part',
292-
format=None)
325+
qemu_img_info.assert_called_once_with('/no.path.part')
293326
mock_glance.get.assert_not_called()
294327

295328
@mock.patch.object(images, 'IMAGE_API')
296329
@mock.patch.object(images, 'qemu_img_info')
297-
def test_fetch_inspect_ami(self, imginfo, glance):
330+
@mock.patch('nova.image.format_inspector.detect_file_format')
331+
def test_fetch_inspect_ami(self, detect, imginfo, glance):
298332
glance.get.return_value = {'disk_format': 'ami'}
333+
detect.return_value.__str__.return_value = 'raw'
299334
self.assertRaises(exception.ImageUnacceptable,
300335
images.fetch_to_raw, None, 'href123', '/no.path')
301336
# Make sure 'ami was translated into 'raw' before we call qemu-img
302-
imginfo.assert_called_once_with('/no.path.part', format='raw')
337+
imginfo.assert_called_once_with('/no.path.part')
303338

304339
@mock.patch.object(images, 'IMAGE_API')
305340
@mock.patch.object(images, 'qemu_img_info')
306-
def test_fetch_inspect_aki(self, imginfo, glance):
341+
@mock.patch('nova.image.format_inspector.detect_file_format')
342+
def test_fetch_inspect_aki(self, detect, imginfo, glance):
307343
glance.get.return_value = {'disk_format': 'aki'}
344+
detect.return_value.__str__.return_value = 'raw'
308345
self.assertRaises(exception.ImageUnacceptable,
309346
images.fetch_to_raw, None, 'href123', '/no.path')
310347
# Make sure 'aki was translated into 'raw' before we call qemu-img
311-
imginfo.assert_called_once_with('/no.path.part', format='raw')
348+
imginfo.assert_called_once_with('/no.path.part')
312349

313350
@mock.patch.object(images, 'IMAGE_API')
314351
@mock.patch.object(images, 'qemu_img_info')
315-
def test_fetch_inspect_ari(self, imginfo, glance):
352+
@mock.patch('nova.image.format_inspector.detect_file_format')
353+
def test_fetch_inspect_ari(self, detect, imginfo, glance):
316354
glance.get.return_value = {'disk_format': 'ari'}
355+
detect.return_value.__str__.return_value = 'raw'
317356
self.assertRaises(exception.ImageUnacceptable,
318357
images.fetch_to_raw, None, 'href123', '/no.path')
319358
# Make sure 'aki was translated into 'raw' before we call qemu-img
320-
imginfo.assert_called_once_with('/no.path.part', format='raw')
359+
imginfo.assert_called_once_with('/no.path.part')
321360

322361
@mock.patch.object(images, 'IMAGE_API')
323362
@mock.patch.object(images, 'qemu_img_info')
@@ -330,13 +369,16 @@ def test_fetch_inspect_unknown_format(self, imginfo, glance):
330369

331370
@mock.patch.object(images, 'IMAGE_API')
332371
@mock.patch.object(images, 'qemu_img_info')
333-
@mock.patch('nova.image.format_inspector.get_inspector')
334-
def test_fetch_inspect_disagrees_qemu(self, mock_gi, imginfo, glance):
372+
@mock.patch('nova.image.format_inspector.detect_file_format')
373+
def test_fetch_inspect_disagrees_qemu(self, mock_detect, imginfo, glance):
335374
glance.get.return_value = {'disk_format': 'qcow2'}
375+
mock_detect.return_value.__str__.return_value = 'qcow2'
336376
# Glance and inspector think it is a qcow2 file, but qemu-img does not
337-
# agree. It was forced to interpret as a qcow2, but returned no
338-
# format information as a result.
377+
# agree.
339378
imginfo.return_value.data_file = None
340-
self.assertRaises(exception.ImageUnacceptable,
341-
images.fetch_to_raw, None, 'href123', '/no.path')
342-
imginfo.assert_called_once_with('/no.path.part', format='qcow2')
379+
imginfo.return_value.file_format = 'vmdk'
380+
ex = self.assertRaises(exception.ImageUnacceptable,
381+
images.fetch_to_raw,
382+
None, 'href123', '/no.path')
383+
self.assertIn('content does not match disk_format', str(ex))
384+
imginfo.assert_called_once_with('/no.path.part')

0 commit comments

Comments
 (0)