xemu/tests/qemu-iotests/040
John Snow 4897629173 iotests/040: Fix TestCommitWithFilters test
Without this change, asserting that qemu_io always returns 0 causes this
test to fail in a way we happened not to be catching previously:

 qemu.utils.VerboseProcessError: Command
  '('/home/jsnow/src/qemu/bin/git/tests/qemu-iotests/../../qemu-io',
  '--cache', 'writeback', '--aio', 'threads', '-f', 'qcow2', '-c',
  'read -P 4 3M 1M',
  '/home/jsnow/src/qemu/bin/git/tests/qemu-iotests/scratch/3.img')'
  returned non-zero exit status 1.
  ┏━ output ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ┃ qemu-io: can't open device
  ┃ /home/jsnow/src/qemu/bin/git/tests/qemu-iotests/scratch/3.img:
  ┃ Could not open backing file: Could not open backing file: Throttle
  ┃ group 'tg' does not exist
  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

The commit jobs changes the backing file string stored in the image file
header belonging to the node above the commit’s top node to point to the
commit target (the base node).  QEMU tries to be as accurate as
possible, and so in these test cases will include the filter that is
part of the block graph in that backing file string (by virtue of making
it a json:{} description of the post-commit subgraph).  This makes
little sense outside of QEMU, though: Specifically, the throttle node in
that subgraph will dearly miss its supposedly associated throttle group
object.

When starting the commit job, we can specify a custom backing file
string to write into said image file, so let’s use that feature to write
the plain filename of the backing chain’s next actual image file there.

Explicitly provide the backing file so that opening the file outside of
QEMU (Where we will not have throttle groups) will succeed.

Signed-off-by: Hanna Reitz <hreitz@redhat.com>
Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Eric Blake <eblake@redhat.com>
Message-Id: <20220418211504.943969-6-jsnow@redhat.com>
2022-04-25 14:30:09 +02:00

986 lines
41 KiB
Python
Executable File

#!/usr/bin/env python3
# group: rw auto
#
# 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)
if self.image_len:
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('virtio-scsi')
self.vm.add_device("scsi-hd,id=scsi0,drive=drive0")
self.vm.launch()
def tearDown(self):
self.vm.shutdown()
os.remove(test_img)
os.remove(mid_img)
os.remove(backing_img)
def test_commit(self):
self.run_commit_test(mid_img, backing_img)
if not self.image_len:
return
qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img)
qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img)
def test_commit_node(self):
self.run_commit_test("mid", "base", node_names=True)
if not self.image_len:
return
qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img)
qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img)
@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', {})
# 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', {})
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', "Can't find '%s' in the backing chain" % 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', "Can't find 'badfile' in the backing chain")
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)
if not self.image_len:
return
qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img)
qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img)
def test_top_is_default_active(self):
self.run_default_commit_test()
if not self.image_len:
return
qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', backing_img)
qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', backing_img)
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', "Can't find '%s' in the backing chain" % 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)
qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', self.backing_img_abs)
qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', self.backing_img_abs)
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', "Can't find '%s' in the backing chain" % 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', "Can't find 'badfile' in the backing chain")
def test_top_is_active(self):
self.run_commit_test(self.test_img, self.backing_img)
qemu_io('-f', 'raw', '-c', 'read -P 0xab 0 524288', self.backing_img_abs)
qemu_io('-f', 'raw', '-c', 'read -P 0xef 524288 524288', self.backing_img_abs)
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', "Can't find '%s' in the backing chain" % 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')
class TestCommitWithFilters(iotests.QMPTestCase):
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 do_test_io(self, read_or_write):
for index, pattern_file in enumerate(self.pattern_files):
qemu_io('-f', iotests.imgfmt,
'-c',
f'{read_or_write} -P {index + 1} {index}M 1M',
pattern_file)
@iotests.skip_if_unsupported(['throttle'])
def setUp(self):
qemu_img('create', '-f', iotests.imgfmt, self.img0, '64M')
qemu_img('create', '-f', iotests.imgfmt, self.img1, '64M')
qemu_img('create', '-f', iotests.imgfmt, self.img2, '64M')
qemu_img('create', '-f', iotests.imgfmt, self.img3, '64M')
# Distributions of the patterns in the files; this is checked
# by tearDown() and should be changed by the test cases as is
# necessary
self.pattern_files = [self.img0, self.img1, self.img2, self.img3]
self.do_test_io('write')
self.vm = iotests.VM().add_device('virtio-scsi,id=vio-scsi')
self.vm.launch()
result = self.vm.qmp('object-add', qom_type='throttle-group', id='tg')
self.assert_qmp(result, 'return', {})
result = self.vm.qmp('blockdev-add', **{
'node-name': 'top-filter',
'driver': 'throttle',
'throttle-group': 'tg',
'file': {
'node-name': 'cow-3',
'driver': iotests.imgfmt,
'file': {
'driver': 'file',
'filename': self.img3
},
'backing': {
'node-name': 'cow-2',
'driver': iotests.imgfmt,
'file': {
'driver': 'file',
'filename': self.img2
},
'backing': {
'node-name': 'cow-1',
'driver': iotests.imgfmt,
'file': {
'driver': 'file',
'filename': self.img1
},
'backing': {
'node-name': 'bottom-filter',
'driver': 'throttle',
'throttle-group': 'tg',
'file': {
'node-name': 'cow-0',
'driver': iotests.imgfmt,
'file': {
'driver': 'file',
'filename': self.img0
}
}
}
}
}
}
})
self.assert_qmp(result, 'return', {})
def tearDown(self):
self.vm.shutdown()
self.do_test_io('read')
os.remove(self.img3)
os.remove(self.img2)
os.remove(self.img1)
os.remove(self.img0)
# Filters make for funny filenames, so we cannot just use
# self.imgX to get them
def get_filename(self, node):
return self.vm.node_info(node)['image']['filename']
def test_filterless_commit(self):
result = self.vm.qmp('block-commit',
job_id='commit',
device='top-filter',
top_node='cow-2',
base_node='cow-1',
backing_file=self.img1)
self.assert_qmp(result, 'return', {})
self.wait_until_completed(drive='commit')
self.assertIsNotNone(self.vm.node_info('cow-3'))
self.assertIsNone(self.vm.node_info('cow-2'))
self.assertIsNotNone(self.vm.node_info('cow-1'))
# 2 has been comitted into 1
self.pattern_files[2] = self.img1
def test_commit_through_filter(self):
result = self.vm.qmp('block-commit',
job_id='commit',
device='top-filter',
top_node='cow-1',
base_node='cow-0',
backing_file=self.img0)
self.assert_qmp(result, 'return', {})
self.wait_until_completed(drive='commit')
self.assertIsNotNone(self.vm.node_info('cow-2'))
self.assertIsNone(self.vm.node_info('cow-1'))
self.assertIsNone(self.vm.node_info('bottom-filter'))
self.assertIsNotNone(self.vm.node_info('cow-0'))
# 1 has been comitted into 0
self.pattern_files[1] = self.img0
def test_filtered_active_commit_with_filter(self):
# Add a device, so the commit job finds a parent it can change
# to point to the base node (so we can test that top-filter is
# dropped from the graph)
result = self.vm.qmp('device_add', id='drv0', driver='scsi-hd',
bus='vio-scsi.0', drive='top-filter')
self.assert_qmp(result, 'return', {})
# Try to release our reference to top-filter; that should not
# work because drv0 uses it
result = self.vm.qmp('blockdev-del', node_name='top-filter')
self.assert_qmp(result, 'error/class', 'GenericError')
self.assert_qmp(result, 'error/desc', 'Node top-filter is in use')
result = self.vm.qmp('block-commit',
job_id='commit',
device='top-filter',
base_node='cow-2')
self.assert_qmp(result, 'return', {})
self.complete_and_wait(drive='commit')
# Try to release our reference to top-filter again
result = self.vm.qmp('blockdev-del', node_name='top-filter')
self.assert_qmp(result, 'return', {})
self.assertIsNone(self.vm.node_info('top-filter'))
self.assertIsNone(self.vm.node_info('cow-3'))
self.assertIsNotNone(self.vm.node_info('cow-2'))
# Check that drv0 is now connected to cow-2
blockdevs = self.vm.qmp('query-block')['return']
drv0 = next(dev for dev in blockdevs if dev['qdev'] == 'drv0')
self.assertEqual(drv0['inserted']['node-name'], 'cow-2')
# 3 has been comitted into 2
self.pattern_files[3] = self.img2
def test_filtered_active_commit_without_filter(self):
result = self.vm.qmp('block-commit',
job_id='commit',
device='top-filter',
top_node='cow-3',
base_node='cow-2')
self.assert_qmp(result, 'return', {})
self.complete_and_wait(drive='commit')
self.assertIsNotNone(self.vm.node_info('top-filter'))
self.assertIsNone(self.vm.node_info('cow-3'))
self.assertIsNotNone(self.vm.node_info('cow-2'))
# 3 has been comitted into 2
self.pattern_files[3] = self.img2
class TestCommitWithOverriddenBacking(iotests.QMPTestCase):
img_base_a = os.path.join(iotests.test_dir, 'base_a.img')
img_base_b = os.path.join(iotests.test_dir, 'base_b.img')
img_top = os.path.join(iotests.test_dir, 'top.img')
def setUp(self):
qemu_img('create', '-f', iotests.imgfmt, self.img_base_a, '1M')
qemu_img('create', '-f', iotests.imgfmt, self.img_base_b, '1M')
qemu_img('create', '-f', iotests.imgfmt, '-b', self.img_base_a,
'-F', iotests.imgfmt, self.img_top)
self.vm = iotests.VM()
self.vm.launch()
# Use base_b instead of base_a as the backing of top
result = self.vm.qmp('blockdev-add', **{
'node-name': 'top',
'driver': iotests.imgfmt,
'file': {
'driver': 'file',
'filename': self.img_top
},
'backing': {
'node-name': 'base',
'driver': iotests.imgfmt,
'file': {
'driver': 'file',
'filename': self.img_base_b
}
}
})
self.assert_qmp(result, 'return', {})
def tearDown(self):
self.vm.shutdown()
os.remove(self.img_top)
os.remove(self.img_base_a)
os.remove(self.img_base_b)
def test_commit_to_a(self):
# Try committing to base_a (which should fail, as top's
# backing image is base_b instead)
result = self.vm.qmp('block-commit',
job_id='commit',
device='top',
base=self.img_base_a)
self.assert_qmp(result, 'error/class', 'GenericError')
def test_commit_to_b(self):
# Try committing to base_b (which should work, since that is
# actually top's backing image)
result = self.vm.qmp('block-commit',
job_id='commit',
device='top',
base=self.img_base_b)
self.assert_qmp(result, 'return', {})
self.vm.event_wait('BLOCK_JOB_READY')
self.vm.qmp('block-job-complete', device='commit')
self.vm.event_wait('BLOCK_JOB_COMPLETED')
if __name__ == '__main__':
iotests.main(supported_fmts=['qcow2', 'qed'],
supported_protocols=['file'])