There are many existing qcow2 images that specify a backing file but no format. This has been the source of CVEs in the past, but has become more prominent of a problem now that libvirt has switched to -blockdev. With older -drive, at least the probing was always done by qemu (so the only risk of a changed format between successive boots of a guest was if qemu was upgraded and probed differently). But with newer -blockdev, libvirt must specify a format; if libvirt guesses raw where the image was formatted, this results in data corruption visible to the guest; conversely, if libvirt guesses qcow2 where qemu was using raw, this can result in potential security holes, so modern libvirt instead refuses to use images without explicit backing format. The change in libvirt to reject images without explicit backing format has pointed out that a number of tools have been far too reliant on probing in the past. It's time to set a better example in our own iotests of properly setting this parameter. iotest calls to create, rebase, and convert are all impacted to some degree. It's a bit annoying that we are inconsistent on command line - while all of those accept -o backing_file=...,backing_fmt=..., the shortcuts are different: create and rebase have -b and -F, while convert has -B but no -F. (amend has no shortcuts, but the previous patch just deprecated the use of amend to change backing chains). Signed-off-by: Eric Blake <eblake@redhat.com> Message-Id: <20200706203954.341758-9-eblake@redhat.com> Signed-off-by: Kevin Wolf <kwolf@redhat.com>
740 lines
31 KiB
Python
Executable file
740 lines
31 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
#
|
|
# Tests for image block commit.
|
|
#
|
|
# Copyright (C) 2012 IBM, Corp.
|
|
# Copyright (C) 2012 Red Hat, Inc.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
# Test for live block commit
|
|
# Derived from Image Streaming Test 030
|
|
|
|
import time
|
|
import os
|
|
import iotests
|
|
from iotests import qemu_img, qemu_io
|
|
import struct
|
|
import errno
|
|
|
|
backing_img = os.path.join(iotests.test_dir, 'backing.img')
|
|
mid_img = os.path.join(iotests.test_dir, 'mid.img')
|
|
test_img = os.path.join(iotests.test_dir, 'test.img')
|
|
|
|
class ImageCommitTestCase(iotests.QMPTestCase):
|
|
'''Abstract base class for image commit test cases'''
|
|
|
|
def wait_for_complete(self, need_ready=False):
|
|
completed = False
|
|
ready = False
|
|
while not completed:
|
|
for event in self.vm.get_qmp_events(wait=True):
|
|
if event['event'] == 'BLOCK_JOB_COMPLETED':
|
|
self.assert_qmp_absent(event, 'data/error')
|
|
self.assert_qmp(event, 'data/type', 'commit')
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp(event, 'data/offset', event['data']['len'])
|
|
if need_ready:
|
|
self.assertTrue(ready, "Expecting BLOCK_JOB_COMPLETED event")
|
|
completed = True
|
|
elif event['event'] == 'BLOCK_JOB_READY':
|
|
ready = True
|
|
self.assert_qmp(event, 'data/type', 'commit')
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.vm.qmp('block-job-complete', device='drive0')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
def run_commit_test(self, top, base, need_ready=False, node_names=False):
|
|
self.assert_no_active_block_jobs()
|
|
if node_names:
|
|
result = self.vm.qmp('block-commit', device='drive0', top_node=top, base_node=base)
|
|
else:
|
|
result = self.vm.qmp('block-commit', device='drive0', top=top, base=base)
|
|
self.assert_qmp(result, 'return', {})
|
|
self.wait_for_complete(need_ready)
|
|
|
|
def run_default_commit_test(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
self.wait_for_complete()
|
|
|
|
class TestSingleDrive(ImageCommitTestCase):
|
|
# Need some space after the copied data so that throttling is effective in
|
|
# tests that use it rather than just completing the job immediately
|
|
image_len = 2 * 1024 * 1024
|
|
test_len = 1 * 1024 * 256
|
|
|
|
def setUp(self):
|
|
iotests.create_image(backing_img, self.image_len)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % backing_img, '-F', 'raw', mid_img)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % mid_img,
|
|
'-F', iotests.imgfmt, test_img)
|
|
qemu_io('-f', 'raw', '-c', 'write -P 0xab 0 524288', backing_img)
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0xef 524288 524288', mid_img)
|
|
self.vm = iotests.VM().add_drive(test_img, "node-name=top,backing.node-name=mid,backing.backing.node-name=base", interface="none")
|
|
self.vm.add_device(iotests.get_virtio_scsi_device())
|
|
self.vm.add_device("scsi-hd,id=scsi0,drive=drive0")
|
|
self.vm.launch()
|
|
self.has_quit = False
|
|
|
|
def tearDown(self):
|
|
self.vm.shutdown(has_quit=self.has_quit)
|
|
os.remove(test_img)
|
|
os.remove(mid_img)
|
|
os.remove(backing_img)
|
|
|
|
def test_commit(self):
|
|
self.run_commit_test(mid_img, backing_img)
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img).find("verification failed"))
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img).find("verification failed"))
|
|
|
|
def test_commit_node(self):
|
|
self.run_commit_test("mid", "base", node_names=True)
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img).find("verification failed"))
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img).find("verification failed"))
|
|
|
|
@iotests.skip_if_unsupported(['throttle'])
|
|
def test_commit_with_filter_and_quit(self):
|
|
result = self.vm.qmp('object-add', qom_type='throttle-group', id='tg')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Add a filter outside of the backing chain
|
|
result = self.vm.qmp('blockdev-add', driver='throttle', node_name='filter', throttle_group='tg', file='mid')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('block-commit', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Quit immediately, thus forcing a simultaneous cancel of the
|
|
# block job and a bdrv_drain_all()
|
|
result = self.vm.qmp('quit')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.has_quit = True
|
|
|
|
# Same as above, but this time we add the filter after starting the job
|
|
@iotests.skip_if_unsupported(['throttle'])
|
|
def test_commit_plus_filter_and_quit(self):
|
|
result = self.vm.qmp('object-add', qom_type='throttle-group', id='tg')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('block-commit', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Add a filter outside of the backing chain
|
|
result = self.vm.qmp('blockdev-add', driver='throttle', node_name='filter', throttle_group='tg', file='mid')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Quit immediately, thus forcing a simultaneous cancel of the
|
|
# block job and a bdrv_drain_all()
|
|
result = self.vm.qmp('quit')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.has_quit = True
|
|
|
|
def test_device_not_found(self):
|
|
result = self.vm.qmp('block-commit', device='nonexistent', top='%s' % mid_img)
|
|
self.assert_qmp(result, 'error/class', 'DeviceNotFound')
|
|
|
|
def test_top_same_base(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top='%s' % backing_img, base='%s' % backing_img)
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', 'Base \'%s\' not found' % backing_img)
|
|
|
|
def test_top_invalid(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top='badfile', base='%s' % backing_img)
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', 'Top image file badfile not found')
|
|
|
|
def test_base_invalid(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top='%s' % mid_img, base='badfile')
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', 'Base \'badfile\' not found')
|
|
|
|
def test_top_node_invalid(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top_node='badfile', base_node='base')
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', "Cannot find device= nor node_name=badfile")
|
|
|
|
def test_base_node_invalid(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top_node='mid', base_node='badfile')
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', "Cannot find device= nor node_name=badfile")
|
|
|
|
def test_top_path_and_node(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top_node='mid', base_node='base', top='%s' % mid_img)
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', "'top-node' and 'top' are mutually exclusive")
|
|
|
|
def test_base_path_and_node(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top_node='mid', base_node='base', base='%s' % backing_img)
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', "'base-node' and 'base' are mutually exclusive")
|
|
|
|
def test_top_is_active(self):
|
|
self.run_commit_test(test_img, backing_img, need_ready=True)
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img).find("verification failed"))
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img).find("verification failed"))
|
|
|
|
def test_top_is_default_active(self):
|
|
self.run_default_commit_test()
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img).find("verification failed"))
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img).find("verification failed"))
|
|
|
|
def test_top_and_base_reversed(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top='%s' % backing_img, base='%s' % mid_img)
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', 'Base \'%s\' not found' % mid_img)
|
|
|
|
def test_top_and_base_node_reversed(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top_node='base', base_node='top')
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', "'top' is not in this backing file chain")
|
|
|
|
def test_top_node_in_wrong_chain(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('blockdev-add', driver='null-co', node_name='null')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('block-commit', device='drive0', top_node='null', base_node='base')
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', "'null' is not in this backing file chain")
|
|
|
|
# When the job is running on a BB that is automatically deleted on hot
|
|
# unplug, the job is cancelled when the device disappears
|
|
def test_hot_unplug(self):
|
|
if self.image_len == 0:
|
|
return
|
|
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top=mid_img,
|
|
base=backing_img, speed=(self.image_len // 4))
|
|
self.assert_qmp(result, 'return', {})
|
|
result = self.vm.qmp('device_del', id='scsi0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
cancelled = False
|
|
deleted = False
|
|
while not cancelled or not deleted:
|
|
for event in self.vm.get_qmp_events(wait=True):
|
|
if event['event'] == 'DEVICE_DELETED':
|
|
self.assert_qmp(event, 'data/device', 'scsi0')
|
|
deleted = True
|
|
elif event['event'] == 'BLOCK_JOB_CANCELLED':
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
cancelled = True
|
|
elif event['event'] == 'JOB_STATUS_CHANGE':
|
|
self.assert_qmp(event, 'data/id', 'drive0')
|
|
else:
|
|
self.fail("Unexpected event %s" % (event['event']))
|
|
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Tests that the insertion of the commit_top filter node doesn't make a
|
|
# difference to query-blockstat
|
|
def test_implicit_node(self):
|
|
if self.image_len == 0:
|
|
return
|
|
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top=mid_img,
|
|
base=backing_img, speed=(self.image_len // 4))
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('query-block')
|
|
self.assert_qmp(result, 'return[0]/inserted/file', test_img)
|
|
self.assert_qmp(result, 'return[0]/inserted/drv', iotests.imgfmt)
|
|
self.assert_qmp(result, 'return[0]/inserted/backing_file', mid_img)
|
|
self.assert_qmp(result, 'return[0]/inserted/backing_file_depth', 2)
|
|
self.assert_qmp(result, 'return[0]/inserted/image/filename', test_img)
|
|
self.assert_qmp(result, 'return[0]/inserted/image/backing-image/filename', mid_img)
|
|
self.assert_qmp(result, 'return[0]/inserted/image/backing-image/backing-image/filename', backing_img)
|
|
|
|
result = self.vm.qmp('query-blockstats')
|
|
self.assert_qmp(result, 'return[0]/node-name', 'top')
|
|
self.assert_qmp(result, 'return[0]/backing/node-name', 'mid')
|
|
self.assert_qmp(result, 'return[0]/backing/backing/node-name', 'base')
|
|
|
|
self.cancel_and_wait()
|
|
self.assert_no_active_block_jobs()
|
|
|
|
class TestRelativePaths(ImageCommitTestCase):
|
|
image_len = 1 * 1024 * 1024
|
|
test_len = 1 * 1024 * 256
|
|
|
|
dir1 = "dir1"
|
|
dir2 = "dir2/"
|
|
dir3 = "dir2/dir3/"
|
|
|
|
test_img = os.path.join(iotests.test_dir, dir3, 'test.img')
|
|
mid_img = "../mid.img"
|
|
backing_img = "../dir1/backing.img"
|
|
|
|
backing_img_abs = os.path.join(iotests.test_dir, dir1, 'backing.img')
|
|
mid_img_abs = os.path.join(iotests.test_dir, dir2, 'mid.img')
|
|
|
|
def setUp(self):
|
|
try:
|
|
os.mkdir(os.path.join(iotests.test_dir, self.dir1))
|
|
os.mkdir(os.path.join(iotests.test_dir, self.dir2))
|
|
os.mkdir(os.path.join(iotests.test_dir, self.dir3))
|
|
except OSError as exception:
|
|
if exception.errno != errno.EEXIST:
|
|
raise
|
|
iotests.create_image(self.backing_img_abs, TestRelativePaths.image_len)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % self.backing_img_abs,
|
|
'-F', 'raw', self.mid_img_abs)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % self.mid_img_abs,
|
|
'-F', iotests.imgfmt, self.test_img)
|
|
qemu_img('rebase', '-u', '-b', self.backing_img,
|
|
'-F', 'raw', self.mid_img_abs)
|
|
qemu_img('rebase', '-u', '-b', self.mid_img,
|
|
'-F', iotests.imgfmt, self.test_img)
|
|
qemu_io('-f', 'raw', '-c', 'write -P 0xab 0 524288', self.backing_img_abs)
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0xef 524288 524288', self.mid_img_abs)
|
|
self.vm = iotests.VM().add_drive(self.test_img)
|
|
self.vm.launch()
|
|
|
|
def tearDown(self):
|
|
self.vm.shutdown()
|
|
os.remove(self.test_img)
|
|
os.remove(self.mid_img_abs)
|
|
os.remove(self.backing_img_abs)
|
|
try:
|
|
os.rmdir(os.path.join(iotests.test_dir, self.dir1))
|
|
os.rmdir(os.path.join(iotests.test_dir, self.dir3))
|
|
os.rmdir(os.path.join(iotests.test_dir, self.dir2))
|
|
except OSError as exception:
|
|
if exception.errno != errno.EEXIST and exception.errno != errno.ENOTEMPTY:
|
|
raise
|
|
|
|
def test_commit(self):
|
|
self.run_commit_test(self.mid_img, self.backing_img)
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', self.backing_img_abs).find("verification failed"))
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', self.backing_img_abs).find("verification failed"))
|
|
|
|
def test_device_not_found(self):
|
|
result = self.vm.qmp('block-commit', device='nonexistent', top='%s' % self.mid_img)
|
|
self.assert_qmp(result, 'error/class', 'DeviceNotFound')
|
|
|
|
def test_top_same_base(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top='%s' % self.mid_img, base='%s' % self.mid_img)
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', 'Base \'%s\' not found' % self.mid_img)
|
|
|
|
def test_top_invalid(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top='badfile', base='%s' % self.backing_img)
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', 'Top image file badfile not found')
|
|
|
|
def test_base_invalid(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top='%s' % self.mid_img, base='badfile')
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', 'Base \'badfile\' not found')
|
|
|
|
def test_top_is_active(self):
|
|
self.run_commit_test(self.test_img, self.backing_img)
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', self.backing_img_abs).find("verification failed"))
|
|
self.assertEqual(-1, qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', self.backing_img_abs).find("verification failed"))
|
|
|
|
def test_top_and_base_reversed(self):
|
|
self.assert_no_active_block_jobs()
|
|
result = self.vm.qmp('block-commit', device='drive0', top='%s' % self.backing_img, base='%s' % self.mid_img)
|
|
self.assert_qmp(result, 'error/class', 'GenericError')
|
|
self.assert_qmp(result, 'error/desc', 'Base \'%s\' not found' % self.mid_img)
|
|
|
|
|
|
class TestSetSpeed(ImageCommitTestCase):
|
|
image_len = 80 * 1024 * 1024 # MB
|
|
|
|
def setUp(self):
|
|
qemu_img('create', backing_img, str(TestSetSpeed.image_len))
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % backing_img, '-F', 'raw', mid_img)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % mid_img,
|
|
'-F', iotests.imgfmt, test_img)
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0x1 0 512', test_img)
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0xef 524288 524288', mid_img)
|
|
self.vm = iotests.VM().add_drive('blkdebug::' + test_img)
|
|
self.vm.launch()
|
|
|
|
def tearDown(self):
|
|
self.vm.shutdown()
|
|
os.remove(test_img)
|
|
os.remove(mid_img)
|
|
os.remove(backing_img)
|
|
|
|
def test_set_speed(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
self.vm.pause_drive('drive0')
|
|
result = self.vm.qmp('block-commit', device='drive0', top=mid_img, speed=1024 * 1024)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Ensure the speed we set was accepted
|
|
result = self.vm.qmp('query-block-jobs')
|
|
self.assert_qmp(result, 'return[0]/device', 'drive0')
|
|
self.assert_qmp(result, 'return[0]/speed', 1024 * 1024)
|
|
|
|
self.cancel_and_wait(resume=True)
|
|
|
|
class TestActiveZeroLengthImage(TestSingleDrive):
|
|
image_len = 0
|
|
|
|
class TestReopenOverlay(ImageCommitTestCase):
|
|
image_len = 1024 * 1024
|
|
img0 = os.path.join(iotests.test_dir, '0.img')
|
|
img1 = os.path.join(iotests.test_dir, '1.img')
|
|
img2 = os.path.join(iotests.test_dir, '2.img')
|
|
img3 = os.path.join(iotests.test_dir, '3.img')
|
|
|
|
def setUp(self):
|
|
iotests.create_image(self.img0, self.image_len)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % self.img0, '-F', 'raw', self.img1)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % self.img1,
|
|
'-F', iotests.imgfmt, self.img2)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % self.img2,
|
|
'-F', iotests.imgfmt, self.img3)
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0xab 0 128K', self.img1)
|
|
self.vm = iotests.VM().add_drive(self.img3)
|
|
self.vm.launch()
|
|
|
|
def tearDown(self):
|
|
self.vm.shutdown()
|
|
os.remove(self.img0)
|
|
os.remove(self.img1)
|
|
os.remove(self.img2)
|
|
os.remove(self.img3)
|
|
|
|
# This tests what happens when the overlay image of the 'top' node
|
|
# needs to be reopened in read-write mode in order to update the
|
|
# backing image string.
|
|
def test_reopen_overlay(self):
|
|
self.run_commit_test(self.img1, self.img0)
|
|
|
|
class TestErrorHandling(iotests.QMPTestCase):
|
|
image_len = 2 * 1024 * 1024
|
|
|
|
def setUp(self):
|
|
iotests.create_image(backing_img, self.image_len)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % backing_img,
|
|
'-F', 'raw', mid_img)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % mid_img,
|
|
'-F', iotests.imgfmt, test_img)
|
|
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0x11 0 512k', mid_img)
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0x22 0 512k', test_img)
|
|
|
|
self.vm = iotests.VM()
|
|
self.vm.launch()
|
|
|
|
self.blkdebug_file = iotests.file_path("blkdebug.conf")
|
|
|
|
def tearDown(self):
|
|
self.vm.shutdown()
|
|
os.remove(test_img)
|
|
os.remove(mid_img)
|
|
os.remove(backing_img)
|
|
|
|
def blockdev_add(self, **kwargs):
|
|
result = self.vm.qmp('blockdev-add', **kwargs)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
def add_block_nodes(self, base_debug=None, mid_debug=None, top_debug=None):
|
|
self.blockdev_add(node_name='base-file', driver='file',
|
|
filename=backing_img)
|
|
self.blockdev_add(node_name='mid-file', driver='file',
|
|
filename=mid_img)
|
|
self.blockdev_add(node_name='top-file', driver='file',
|
|
filename=test_img)
|
|
|
|
if base_debug:
|
|
self.blockdev_add(node_name='base-dbg', driver='blkdebug',
|
|
image='base-file', inject_error=base_debug)
|
|
if mid_debug:
|
|
self.blockdev_add(node_name='mid-dbg', driver='blkdebug',
|
|
image='mid-file', inject_error=mid_debug)
|
|
if top_debug:
|
|
self.blockdev_add(node_name='top-dbg', driver='blkdebug',
|
|
image='top-file', inject_error=top_debug)
|
|
|
|
self.blockdev_add(node_name='base-fmt', driver='raw',
|
|
file=('base-dbg' if base_debug else 'base-file'))
|
|
self.blockdev_add(node_name='mid-fmt', driver=iotests.imgfmt,
|
|
file=('mid-dbg' if mid_debug else 'mid-file'),
|
|
backing='base-fmt')
|
|
self.blockdev_add(node_name='top-fmt', driver=iotests.imgfmt,
|
|
file=('top-dbg' if top_debug else 'top-file'),
|
|
backing='mid-fmt')
|
|
|
|
def run_job(self, expected_events, error_pauses_job=False):
|
|
match_device = {'data': {'device': 'job0'}}
|
|
events = [
|
|
('BLOCK_JOB_COMPLETED', match_device),
|
|
('BLOCK_JOB_CANCELLED', match_device),
|
|
('BLOCK_JOB_ERROR', match_device),
|
|
('BLOCK_JOB_READY', match_device),
|
|
]
|
|
|
|
completed = False
|
|
log = []
|
|
while not completed:
|
|
ev = self.vm.events_wait(events, timeout=5.0)
|
|
if ev['event'] == 'BLOCK_JOB_COMPLETED':
|
|
completed = True
|
|
elif ev['event'] == 'BLOCK_JOB_ERROR':
|
|
if error_pauses_job:
|
|
result = self.vm.qmp('block-job-resume', device='job0')
|
|
self.assert_qmp(result, 'return', {})
|
|
elif ev['event'] == 'BLOCK_JOB_READY':
|
|
result = self.vm.qmp('block-job-complete', device='job0')
|
|
self.assert_qmp(result, 'return', {})
|
|
else:
|
|
self.fail("Unexpected event: %s" % ev)
|
|
log.append(iotests.filter_qmp_event(ev))
|
|
|
|
self.maxDiff = None
|
|
self.assertEqual(expected_events, log)
|
|
|
|
def event_error(self, op, action):
|
|
return {
|
|
'event': 'BLOCK_JOB_ERROR',
|
|
'data': {'action': action, 'device': 'job0', 'operation': op},
|
|
'timestamp': {'microseconds': 'USECS', 'seconds': 'SECS'}
|
|
}
|
|
|
|
def event_ready(self):
|
|
return {
|
|
'event': 'BLOCK_JOB_READY',
|
|
'data': {'device': 'job0',
|
|
'len': 524288,
|
|
'offset': 524288,
|
|
'speed': 0,
|
|
'type': 'commit'},
|
|
'timestamp': {'microseconds': 'USECS', 'seconds': 'SECS'},
|
|
}
|
|
|
|
def event_completed(self, errmsg=None, active=True):
|
|
max_len = 524288 if active else self.image_len
|
|
data = {
|
|
'device': 'job0',
|
|
'len': max_len,
|
|
'offset': 0 if errmsg else max_len,
|
|
'speed': 0,
|
|
'type': 'commit'
|
|
}
|
|
if errmsg:
|
|
data['error'] = errmsg
|
|
|
|
return {
|
|
'event': 'BLOCK_JOB_COMPLETED',
|
|
'data': data,
|
|
'timestamp': {'microseconds': 'USECS', 'seconds': 'SECS'},
|
|
}
|
|
|
|
def blkdebug_event(self, event, is_raw=False):
|
|
if event:
|
|
return [{
|
|
'event': event,
|
|
'sector': 512 if is_raw else 1024,
|
|
'once': True,
|
|
}]
|
|
return None
|
|
|
|
def prepare_and_start_job(self, on_error, active=True,
|
|
top_event=None, mid_event=None, base_event=None):
|
|
|
|
top_debug = self.blkdebug_event(top_event)
|
|
mid_debug = self.blkdebug_event(mid_event)
|
|
base_debug = self.blkdebug_event(base_event, True)
|
|
|
|
self.add_block_nodes(top_debug=top_debug, mid_debug=mid_debug,
|
|
base_debug=base_debug)
|
|
|
|
result = self.vm.qmp('block-commit', job_id='job0', device='top-fmt',
|
|
top_node='top-fmt' if active else 'mid-fmt',
|
|
base_node='mid-fmt' if active else 'base-fmt',
|
|
on_error=on_error)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
def testActiveReadErrorReport(self):
|
|
self.prepare_and_start_job('report', top_event='read_aio')
|
|
self.run_job([
|
|
self.event_error('read', 'report'),
|
|
self.event_completed('Input/output error')
|
|
])
|
|
|
|
self.vm.shutdown()
|
|
self.assertFalse(iotests.compare_images(test_img, mid_img),
|
|
'target image matches source after error')
|
|
|
|
def testActiveReadErrorStop(self):
|
|
self.prepare_and_start_job('stop', top_event='read_aio')
|
|
self.run_job([
|
|
self.event_error('read', 'stop'),
|
|
self.event_ready(),
|
|
self.event_completed()
|
|
], error_pauses_job=True)
|
|
|
|
self.vm.shutdown()
|
|
self.assertTrue(iotests.compare_images(test_img, mid_img),
|
|
'target image does not match source after commit')
|
|
|
|
def testActiveReadErrorIgnore(self):
|
|
self.prepare_and_start_job('ignore', top_event='read_aio')
|
|
self.run_job([
|
|
self.event_error('read', 'ignore'),
|
|
self.event_ready(),
|
|
self.event_completed()
|
|
])
|
|
|
|
# For commit, 'ignore' actually means retry, so this will succeed
|
|
self.vm.shutdown()
|
|
self.assertTrue(iotests.compare_images(test_img, mid_img),
|
|
'target image does not match source after commit')
|
|
|
|
def testActiveWriteErrorReport(self):
|
|
self.prepare_and_start_job('report', mid_event='write_aio')
|
|
self.run_job([
|
|
self.event_error('write', 'report'),
|
|
self.event_completed('Input/output error')
|
|
])
|
|
|
|
self.vm.shutdown()
|
|
self.assertFalse(iotests.compare_images(test_img, mid_img),
|
|
'target image matches source after error')
|
|
|
|
def testActiveWriteErrorStop(self):
|
|
self.prepare_and_start_job('stop', mid_event='write_aio')
|
|
self.run_job([
|
|
self.event_error('write', 'stop'),
|
|
self.event_ready(),
|
|
self.event_completed()
|
|
], error_pauses_job=True)
|
|
|
|
self.vm.shutdown()
|
|
self.assertTrue(iotests.compare_images(test_img, mid_img),
|
|
'target image does not match source after commit')
|
|
|
|
def testActiveWriteErrorIgnore(self):
|
|
self.prepare_and_start_job('ignore', mid_event='write_aio')
|
|
self.run_job([
|
|
self.event_error('write', 'ignore'),
|
|
self.event_ready(),
|
|
self.event_completed()
|
|
])
|
|
|
|
# For commit, 'ignore' actually means retry, so this will succeed
|
|
self.vm.shutdown()
|
|
self.assertTrue(iotests.compare_images(test_img, mid_img),
|
|
'target image does not match source after commit')
|
|
|
|
def testIntermediateReadErrorReport(self):
|
|
self.prepare_and_start_job('report', active=False, mid_event='read_aio')
|
|
self.run_job([
|
|
self.event_error('read', 'report'),
|
|
self.event_completed('Input/output error', active=False)
|
|
])
|
|
|
|
self.vm.shutdown()
|
|
self.assertFalse(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
|
|
'target image matches source after error')
|
|
|
|
def testIntermediateReadErrorStop(self):
|
|
self.prepare_and_start_job('stop', active=False, mid_event='read_aio')
|
|
self.run_job([
|
|
self.event_error('read', 'stop'),
|
|
self.event_completed(active=False)
|
|
], error_pauses_job=True)
|
|
|
|
self.vm.shutdown()
|
|
self.assertTrue(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
|
|
'target image does not match source after commit')
|
|
|
|
def testIntermediateReadErrorIgnore(self):
|
|
self.prepare_and_start_job('ignore', active=False, mid_event='read_aio')
|
|
self.run_job([
|
|
self.event_error('read', 'ignore'),
|
|
self.event_completed(active=False)
|
|
])
|
|
|
|
# For commit, 'ignore' actually means retry, so this will succeed
|
|
self.vm.shutdown()
|
|
self.assertTrue(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
|
|
'target image does not match source after commit')
|
|
|
|
def testIntermediateWriteErrorReport(self):
|
|
self.prepare_and_start_job('report', active=False, base_event='write_aio')
|
|
self.run_job([
|
|
self.event_error('write', 'report'),
|
|
self.event_completed('Input/output error', active=False)
|
|
])
|
|
|
|
self.vm.shutdown()
|
|
self.assertFalse(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
|
|
'target image matches source after error')
|
|
|
|
def testIntermediateWriteErrorStop(self):
|
|
self.prepare_and_start_job('stop', active=False, base_event='write_aio')
|
|
self.run_job([
|
|
self.event_error('write', 'stop'),
|
|
self.event_completed(active=False)
|
|
], error_pauses_job=True)
|
|
|
|
self.vm.shutdown()
|
|
self.assertTrue(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
|
|
'target image does not match source after commit')
|
|
|
|
def testIntermediateWriteErrorIgnore(self):
|
|
self.prepare_and_start_job('ignore', active=False, base_event='write_aio')
|
|
self.run_job([
|
|
self.event_error('write', 'ignore'),
|
|
self.event_completed(active=False)
|
|
])
|
|
|
|
# For commit, 'ignore' actually means retry, so this will succeed
|
|
self.vm.shutdown()
|
|
self.assertTrue(iotests.compare_images(mid_img, backing_img, fmt2='raw'),
|
|
'target image does not match source after commit')
|
|
|
|
if __name__ == '__main__':
|
|
iotests.main(supported_fmts=['qcow2', 'qed'],
|
|
supported_protocols=['file'])
|