52ea799e96
We can turn logging on/off globally instead of per-function. Remove use_log from run_job, and use python logging to turn on diffable output when we run through a script entry point. iotest 245 changes output order due to buffering reasons. An extended note on python logging: A NullHandler is added to `qemu.iotests` to stop output from being generated if this code is used as a library without configuring logging. A NullHandler is only needed at the root, so a duplicate handler is not needed for `qemu.iotests.diff_io`. When logging is not configured, messages at the 'WARNING' levels or above are printed with default settings. The NullHandler stops this from occurring, which is considered good hygiene for code used as a library. See https://docs.python.org/3/howto/logging.html#library-config When logging is actually enabled (always at the behest of an explicit call by a client script), a root logger is implicitly created at the root, which allows messages to propagate upwards and be handled/emitted from the root logger with default settings. When we want iotest logging, we attach a handler to the qemu.iotests.diff_io logger and disable propagation to avoid possible double-printing. For more information on python logging infrastructure, I highly recommend downloading the pip package `logging_tree`, which provides convenient visualizations of the hierarchical logging configuration under different circumstances. See https://pypi.org/project/logging_tree/ for more information. Signed-off-by: John Snow <jsnow@redhat.com> Reviewed-by: Max Reitz <mreitz@redhat.com> Message-Id: <20200331000014.11581-15-jsnow@redhat.com> Reviewed-by: Kevin Wolf <kwolf@redhat.com> Signed-off-by: Max Reitz <mreitz@redhat.com>
960 lines
40 KiB
Python
Executable File
960 lines
40 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Tests for image streaming.
|
|
#
|
|
# Copyright (C) 2012 IBM Corp.
|
|
#
|
|
# 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/>.
|
|
#
|
|
|
|
import time
|
|
import os
|
|
import iotests
|
|
from iotests import qemu_img, qemu_io
|
|
|
|
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 TestSingleDrive(iotests.QMPTestCase):
|
|
image_len = 1 * 1024 * 1024 # MB
|
|
|
|
def setUp(self):
|
|
iotests.create_image(backing_img, TestSingleDrive.image_len)
|
|
qemu_img('create', '-f', iotests.imgfmt, '-o', 'backing_file=%s' % backing_img, mid_img)
|
|
qemu_img('create', '-f', iotests.imgfmt, '-o', 'backing_file=%s' % mid_img, test_img)
|
|
qemu_io('-f', 'raw', '-c', 'write -P 0x1 0 512', backing_img)
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0x1 524288 512', mid_img)
|
|
self.vm = iotests.VM().add_drive("blkdebug::" + test_img,
|
|
"backing.node-name=mid," +
|
|
"backing.backing.node-name=base")
|
|
self.vm.launch()
|
|
|
|
def tearDown(self):
|
|
self.vm.shutdown()
|
|
os.remove(test_img)
|
|
os.remove(mid_img)
|
|
os.remove(backing_img)
|
|
|
|
def test_stream(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('block-stream', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed()
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
self.assertEqual(qemu_io('-f', 'raw', '-c', 'map', backing_img),
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'map', test_img),
|
|
'image file map does not match backing file after streaming')
|
|
|
|
def test_stream_intermediate(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
self.assertNotEqual(qemu_io('-f', 'raw', '-rU', '-c', 'map', backing_img),
|
|
qemu_io('-f', iotests.imgfmt, '-rU', '-c', 'map', mid_img),
|
|
'image file map matches backing file before streaming')
|
|
|
|
result = self.vm.qmp('block-stream', device='mid', job_id='stream-mid')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed(drive='stream-mid')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
self.assertEqual(qemu_io('-f', 'raw', '-c', 'map', backing_img),
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'map', mid_img),
|
|
'image file map does not match backing file after streaming')
|
|
|
|
def test_stream_pause(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
self.vm.pause_drive('drive0')
|
|
result = self.vm.qmp('block-stream', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.pause_job('drive0', wait=False)
|
|
self.vm.resume_drive('drive0')
|
|
self.pause_wait('drive0')
|
|
|
|
result = self.vm.qmp('query-block-jobs')
|
|
offset = self.dictpath(result, 'return[0]/offset')
|
|
|
|
time.sleep(0.5)
|
|
result = self.vm.qmp('query-block-jobs')
|
|
self.assert_qmp(result, 'return[0]/offset', offset)
|
|
|
|
result = self.vm.qmp('block-job-resume', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed()
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
self.assertEqual(qemu_io('-f', 'raw', '-c', 'map', backing_img),
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'map', test_img),
|
|
'image file map does not match backing file after streaming')
|
|
|
|
def test_stream_no_op(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# The image map is empty before the operation
|
|
empty_map = qemu_io('-f', iotests.imgfmt, '-rU', '-c', 'map', test_img)
|
|
|
|
# This is a no-op: no data should ever be copied from the base image
|
|
result = self.vm.qmp('block-stream', device='drive0', base=mid_img)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed()
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
self.assertEqual(qemu_io('-f', iotests.imgfmt, '-c', 'map', test_img),
|
|
empty_map, 'image file map changed after a no-op')
|
|
|
|
def test_stream_partial(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('block-stream', device='drive0', base=backing_img)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed()
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
self.assertEqual(qemu_io('-f', iotests.imgfmt, '-c', 'map', mid_img),
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'map', test_img),
|
|
'image file map does not match backing file after streaming')
|
|
|
|
def test_device_not_found(self):
|
|
result = self.vm.qmp('block-stream', device='nonexistent')
|
|
self.assert_qmp(result, 'error/desc',
|
|
'Cannot find device=nonexistent nor node_name=nonexistent')
|
|
|
|
def test_job_id_missing(self):
|
|
result = self.vm.qmp('block-stream', device='mid')
|
|
self.assert_qmp(result, 'error/desc', "Invalid job ID ''")
|
|
|
|
def test_read_only(self):
|
|
# Create a new file that we can attach (we need a read-only top)
|
|
with iotests.FilePath('ro-top.img') as ro_top_path:
|
|
qemu_img('create', '-f', iotests.imgfmt, ro_top_path,
|
|
str(self.image_len))
|
|
|
|
result = self.vm.qmp('blockdev-add',
|
|
node_name='ro-top',
|
|
driver=iotests.imgfmt,
|
|
read_only=True,
|
|
file={
|
|
'driver': 'file',
|
|
'filename': ro_top_path,
|
|
'read-only': True
|
|
},
|
|
backing='mid')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('block-stream', job_id='stream',
|
|
device='ro-top', base_node='base')
|
|
self.assert_qmp(result, 'error/desc', 'Block node is read-only')
|
|
|
|
result = self.vm.qmp('blockdev-del', node_name='ro-top')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
|
|
class TestParallelOps(iotests.QMPTestCase):
|
|
num_ops = 4 # Number of parallel block-stream operations
|
|
num_imgs = num_ops * 2 + 1
|
|
image_len = num_ops * 4 * 1024 * 1024
|
|
imgs = []
|
|
|
|
def setUp(self):
|
|
opts = []
|
|
self.imgs = []
|
|
|
|
# Initialize file names and command-line options
|
|
for i in range(self.num_imgs):
|
|
img_depth = self.num_imgs - i - 1
|
|
opts.append("backing." * img_depth + "node-name=node%d" % i)
|
|
self.imgs.append(os.path.join(iotests.test_dir, 'img-%d.img' % i))
|
|
|
|
# Create all images
|
|
iotests.create_image(self.imgs[0], self.image_len)
|
|
for i in range(1, self.num_imgs):
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % self.imgs[i-1], self.imgs[i])
|
|
|
|
# Put data into the images we are copying data from
|
|
odd_img_indexes = [x for x in reversed(range(self.num_imgs)) if x % 2 == 1]
|
|
for i in range(len(odd_img_indexes)):
|
|
# Alternate between 2MB and 4MB.
|
|
# This way jobs will not finish in the same order they were created
|
|
num_mb = 2 + 2 * (i % 2)
|
|
qemu_io('-f', iotests.imgfmt,
|
|
'-c', 'write -P 0xFF %dM %dM' % (i * 4, num_mb),
|
|
self.imgs[odd_img_indexes[i]])
|
|
|
|
# Attach the drive to the VM
|
|
self.vm = iotests.VM()
|
|
self.vm.add_drive(self.imgs[-1], ','.join(opts))
|
|
self.vm.launch()
|
|
|
|
def tearDown(self):
|
|
self.vm.shutdown()
|
|
for img in self.imgs:
|
|
os.remove(img)
|
|
|
|
# Test that it's possible to run several block-stream operations
|
|
# in parallel in the same snapshot chain
|
|
def test_stream_parallel(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Check that the maps don't match before the streaming operations
|
|
for i in range(2, self.num_imgs, 2):
|
|
self.assertNotEqual(qemu_io('-f', iotests.imgfmt, '-rU', '-c', 'map', self.imgs[i]),
|
|
qemu_io('-f', iotests.imgfmt, '-rU', '-c', 'map', self.imgs[i-1]),
|
|
'image file map matches backing file before streaming')
|
|
|
|
# Create all streaming jobs
|
|
pending_jobs = []
|
|
for i in range(2, self.num_imgs, 2):
|
|
node_name = 'node%d' % i
|
|
job_id = 'stream-%s' % node_name
|
|
pending_jobs.append(job_id)
|
|
result = self.vm.qmp('block-stream', device=node_name, job_id=job_id, base=self.imgs[i-2], speed=512*1024)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
for job in pending_jobs:
|
|
result = self.vm.qmp('block-job-set-speed', device=job, speed=0)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Wait for all jobs to be finished.
|
|
while len(pending_jobs) > 0:
|
|
for event in self.vm.get_qmp_events(wait=True):
|
|
if event['event'] == 'BLOCK_JOB_COMPLETED':
|
|
job_id = self.dictpath(event, 'data/device')
|
|
self.assertTrue(job_id in pending_jobs)
|
|
self.assert_qmp_absent(event, 'data/error')
|
|
pending_jobs.remove(job_id)
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
# Check that all maps match now
|
|
for i in range(2, self.num_imgs, 2):
|
|
self.assertEqual(qemu_io('-f', iotests.imgfmt, '-c', 'map', self.imgs[i]),
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'map', self.imgs[i-1]),
|
|
'image file map does not match backing file after streaming')
|
|
|
|
# Test that it's not possible to perform two block-stream
|
|
# operations if there are nodes involved in both.
|
|
def test_overlapping_1(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Set a speed limit to make sure that this job blocks the rest
|
|
result = self.vm.qmp('block-stream', device='node4', job_id='stream-node4', base=self.imgs[1], speed=1024*1024)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('block-stream', device='node5', job_id='stream-node5', base=self.imgs[2])
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node4' is busy: block device is in use by block job: stream")
|
|
|
|
result = self.vm.qmp('block-stream', device='node3', job_id='stream-node3', base=self.imgs[2])
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node3' is busy: block device is in use by block job: stream")
|
|
|
|
result = self.vm.qmp('block-stream', device='node4', job_id='stream-node4-v2')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node4' is busy: block device is in use by block job: stream")
|
|
|
|
# block-commit should also fail if it touches nodes used by the stream job
|
|
result = self.vm.qmp('block-commit', device='drive0', base=self.imgs[4], job_id='commit-node4')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node4' is busy: block device is in use by block job: stream")
|
|
|
|
result = self.vm.qmp('block-commit', device='drive0', base=self.imgs[1], top=self.imgs[3], job_id='commit-node1')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node3' is busy: block device is in use by block job: stream")
|
|
|
|
# This fails because it needs to modify the backing string in node2, which is blocked
|
|
result = self.vm.qmp('block-commit', device='drive0', base=self.imgs[0], top=self.imgs[1], job_id='commit-node0')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node2' is busy: block device is in use by block job: stream")
|
|
|
|
result = self.vm.qmp('block-job-set-speed', device='stream-node4', speed=0)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed(drive='stream-node4')
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Similar to test_overlapping_1, but with block-commit
|
|
# blocking the other jobs
|
|
def test_overlapping_2(self):
|
|
self.assertLessEqual(9, self.num_imgs)
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Set a speed limit to make sure that this job blocks the rest
|
|
result = self.vm.qmp('block-commit', device='drive0', top=self.imgs[5], base=self.imgs[3], job_id='commit-node3', speed=1024*1024)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('block-stream', device='node3', job_id='stream-node3')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node3' is busy: block device is in use by block job: commit")
|
|
|
|
result = self.vm.qmp('block-stream', device='node6', base=self.imgs[2], job_id='stream-node6')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node5' is busy: block device is in use by block job: commit")
|
|
|
|
result = self.vm.qmp('block-stream', device='node4', base=self.imgs[2], job_id='stream-node4')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node4' is busy: block device is in use by block job: commit")
|
|
|
|
result = self.vm.qmp('block-stream', device='node6', base=self.imgs[4], job_id='stream-node6-v2')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node5' is busy: block device is in use by block job: commit")
|
|
|
|
# This fails because block-commit currently blocks the active layer even if it's not used
|
|
result = self.vm.qmp('block-stream', device='drive0', base=self.imgs[5], job_id='stream-drive0')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'drive0' is busy: block device is in use by block job: commit")
|
|
|
|
result = self.vm.qmp('block-job-set-speed', device='commit-node3', speed=0)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed(drive='commit-node3')
|
|
|
|
# Similar to test_overlapping_2, but here block-commit doesn't use the 'top' parameter.
|
|
# Internally this uses a mirror block job, hence the separate test case.
|
|
def test_overlapping_3(self):
|
|
self.assertLessEqual(8, self.num_imgs)
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Set a speed limit to make sure that this job blocks the rest
|
|
result = self.vm.qmp('block-commit', device='drive0', base=self.imgs[3], job_id='commit-drive0', speed=1024*1024)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('block-stream', device='node5', base=self.imgs[3], job_id='stream-node6')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node5' is busy: block device is in use by block job: commit")
|
|
|
|
event = self.vm.event_wait(name='BLOCK_JOB_READY')
|
|
self.assert_qmp(event, 'data/device', 'commit-drive0')
|
|
self.assert_qmp(event, 'data/type', 'commit')
|
|
self.assert_qmp_absent(event, 'data/error')
|
|
|
|
result = self.vm.qmp('block-job-set-speed', device='commit-drive0', speed=0)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('block-job-complete', device='commit-drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed(drive='commit-drive0')
|
|
|
|
# In this case the base node of the stream job is the same as the
|
|
# top node of commit job. Since this results in the commit filter
|
|
# node being part of the stream chain, this is not allowed.
|
|
def test_overlapping_4(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Commit from node2 into node0
|
|
result = self.vm.qmp('block-commit', device='drive0',
|
|
top=self.imgs[2], base=self.imgs[0],
|
|
filter_node_name='commit-filter', speed=1024*1024)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Stream from node2 into node4
|
|
result = self.vm.qmp('block-stream', device='node4', base_node='node2', job_id='node4')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Cannot freeze 'backing' link to 'commit-filter'")
|
|
|
|
result = self.vm.qmp('block-job-set-speed', device='drive0', speed=0)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed()
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# In this case the base node of the stream job is the commit job's
|
|
# filter node. stream does not have a real dependency on its base
|
|
# node, so even though commit removes it when it is done, there is
|
|
# no conflict.
|
|
def test_overlapping_5(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Commit from node2 into node0
|
|
result = self.vm.qmp('block-commit', device='drive0',
|
|
top_node='node2', base_node='node0',
|
|
filter_node_name='commit-filter', speed=1024*1024)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Stream from node2 into node4
|
|
result = self.vm.qmp('block-stream', device='node4',
|
|
base_node='commit-filter', job_id='node4')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('block-job-set-speed', device='drive0', speed=0)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.vm.run_job(job='drive0', auto_dismiss=True)
|
|
self.vm.run_job(job='node4', auto_dismiss=True)
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Test a block-stream and a block-commit job in parallel
|
|
# Here the stream job is supposed to finish quickly in order to reproduce
|
|
# the scenario that triggers the bug fixed in 3d5d319e1221 and 1a63a907507
|
|
def test_stream_commit_1(self):
|
|
self.assertLessEqual(8, self.num_imgs)
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Stream from node0 into node2
|
|
result = self.vm.qmp('block-stream', device='node2', base_node='node0', job_id='node2')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Commit from the active layer into node3
|
|
result = self.vm.qmp('block-commit', device='drive0', base=self.imgs[3])
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Wait for all jobs to be finished.
|
|
pending_jobs = ['node2', 'drive0']
|
|
while len(pending_jobs) > 0:
|
|
for event in self.vm.get_qmp_events(wait=True):
|
|
if event['event'] == 'BLOCK_JOB_COMPLETED':
|
|
node_name = self.dictpath(event, 'data/device')
|
|
self.assertTrue(node_name in pending_jobs)
|
|
self.assert_qmp_absent(event, 'data/error')
|
|
pending_jobs.remove(node_name)
|
|
if event['event'] == 'BLOCK_JOB_READY':
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp(event, 'data/type', 'commit')
|
|
self.assert_qmp_absent(event, 'data/error')
|
|
self.assertTrue('drive0' in pending_jobs)
|
|
self.vm.qmp('block-job-complete', device='drive0')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# This is similar to test_stream_commit_1 but both jobs are slowed
|
|
# down so they can run in parallel for a little while.
|
|
def test_stream_commit_2(self):
|
|
self.assertLessEqual(8, self.num_imgs)
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Stream from node0 into node4
|
|
result = self.vm.qmp('block-stream', device='node4', base_node='node0', job_id='node4', speed=1024*1024)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Commit from the active layer into node5
|
|
result = self.vm.qmp('block-commit', device='drive0', base=self.imgs[5], speed=1024*1024)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
for job in ['drive0', 'node4']:
|
|
result = self.vm.qmp('block-job-set-speed', device=job, speed=0)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Wait for all jobs to be finished.
|
|
pending_jobs = ['node4', 'drive0']
|
|
while len(pending_jobs) > 0:
|
|
for event in self.vm.get_qmp_events(wait=True):
|
|
if event['event'] == 'BLOCK_JOB_COMPLETED':
|
|
node_name = self.dictpath(event, 'data/device')
|
|
self.assertTrue(node_name in pending_jobs)
|
|
self.assert_qmp_absent(event, 'data/error')
|
|
pending_jobs.remove(node_name)
|
|
if event['event'] == 'BLOCK_JOB_READY':
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp(event, 'data/type', 'commit')
|
|
self.assert_qmp_absent(event, 'data/error')
|
|
self.assertTrue('drive0' in pending_jobs)
|
|
self.vm.qmp('block-job-complete', device='drive0')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
|
|
# Test the base_node parameter
|
|
def test_stream_base_node_name(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
self.assertNotEqual(qemu_io('-f', iotests.imgfmt, '-rU', '-c', 'map', self.imgs[4]),
|
|
qemu_io('-f', iotests.imgfmt, '-rU', '-c', 'map', self.imgs[3]),
|
|
'image file map matches backing file before streaming')
|
|
|
|
# Error: the base node does not exist
|
|
result = self.vm.qmp('block-stream', device='node4', base_node='none', job_id='stream')
|
|
self.assert_qmp(result, 'error/desc',
|
|
'Cannot find device= nor node_name=none')
|
|
|
|
# Error: the base node is not a backing file of the top node
|
|
result = self.vm.qmp('block-stream', device='node4', base_node='node6', job_id='stream')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node6' is not a backing image of 'node4'")
|
|
|
|
# Error: the base node is the same as the top node
|
|
result = self.vm.qmp('block-stream', device='node4', base_node='node4', job_id='stream')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"Node 'node4' is not a backing image of 'node4'")
|
|
|
|
# Error: cannot specify 'base' and 'base-node' at the same time
|
|
result = self.vm.qmp('block-stream', device='node4', base=self.imgs[2], base_node='node2', job_id='stream')
|
|
self.assert_qmp(result, 'error/desc',
|
|
"'base' and 'base-node' cannot be specified at the same time")
|
|
|
|
# Success: the base node is a backing file of the top node
|
|
result = self.vm.qmp('block-stream', device='node4', base_node='node2', job_id='stream')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed(drive='stream')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
self.assertEqual(qemu_io('-f', iotests.imgfmt, '-c', 'map', self.imgs[4]),
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'map', self.imgs[3]),
|
|
'image file map matches backing file after streaming')
|
|
|
|
class TestQuorum(iotests.QMPTestCase):
|
|
num_children = 3
|
|
children = []
|
|
backing = []
|
|
|
|
@iotests.skip_if_unsupported(['quorum'])
|
|
def setUp(self):
|
|
opts = ['driver=quorum', 'vote-threshold=2']
|
|
|
|
# Initialize file names and command-line options
|
|
for i in range(self.num_children):
|
|
child_img = os.path.join(iotests.test_dir, 'img-%d.img' % i)
|
|
backing_img = os.path.join(iotests.test_dir, 'backing-%d.img' % i)
|
|
self.children.append(child_img)
|
|
self.backing.append(backing_img)
|
|
qemu_img('create', '-f', iotests.imgfmt, backing_img, '1M')
|
|
qemu_io('-f', iotests.imgfmt,
|
|
'-c', 'write -P 0x55 0 1024', backing_img)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=%s' % backing_img, child_img)
|
|
opts.append("children.%d.file.filename=%s" % (i, child_img))
|
|
opts.append("children.%d.node-name=node%d" % (i, i))
|
|
|
|
# Attach the drive to the VM
|
|
self.vm = iotests.VM()
|
|
self.vm.add_drive(path = None, opts = ','.join(opts))
|
|
self.vm.launch()
|
|
|
|
def tearDown(self):
|
|
self.vm.shutdown()
|
|
for img in self.children:
|
|
os.remove(img)
|
|
for img in self.backing:
|
|
os.remove(img)
|
|
|
|
def test_stream_quorum(self):
|
|
self.assertNotEqual(qemu_io('-f', iotests.imgfmt, '-rU', '-c', 'map', self.children[0]),
|
|
qemu_io('-f', iotests.imgfmt, '-rU', '-c', 'map', self.backing[0]),
|
|
'image file map matches backing file before streaming')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('block-stream', device='node0', job_id='stream-node0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed(drive='stream-node0')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
self.assertEqual(qemu_io('-f', iotests.imgfmt, '-c', 'map', self.children[0]),
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'map', self.backing[0]),
|
|
'image file map does not match backing file after streaming')
|
|
|
|
class TestSmallerBackingFile(iotests.QMPTestCase):
|
|
backing_len = 1 * 1024 * 1024 # MB
|
|
image_len = 2 * backing_len
|
|
|
|
def setUp(self):
|
|
iotests.create_image(backing_img, self.backing_len)
|
|
qemu_img('create', '-f', iotests.imgfmt, '-o', 'backing_file=%s' % backing_img, test_img, str(self.image_len))
|
|
self.vm = iotests.VM().add_drive(test_img)
|
|
self.vm.launch()
|
|
|
|
# If this hangs, then you are missing a fix to complete streaming when the
|
|
# end of the backing file is reached.
|
|
def test_stream(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('block-stream', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed()
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
class TestErrors(iotests.QMPTestCase):
|
|
image_len = 2 * 1024 * 1024 # MB
|
|
|
|
# this should match STREAM_BUFFER_SIZE/512 in block/stream.c
|
|
STREAM_BUFFER_SIZE = 512 * 1024
|
|
|
|
def create_blkdebug_file(self, name, event, errno):
|
|
file = open(name, 'w')
|
|
file.write('''
|
|
[inject-error]
|
|
state = "1"
|
|
event = "%s"
|
|
errno = "%d"
|
|
immediately = "off"
|
|
once = "on"
|
|
sector = "%d"
|
|
|
|
[set-state]
|
|
state = "1"
|
|
event = "%s"
|
|
new_state = "2"
|
|
|
|
[set-state]
|
|
state = "2"
|
|
event = "%s"
|
|
new_state = "1"
|
|
''' % (event, errno, self.STREAM_BUFFER_SIZE // 512, event, event))
|
|
file.close()
|
|
|
|
class TestEIO(TestErrors):
|
|
def setUp(self):
|
|
self.blkdebug_file = backing_img + ".blkdebug"
|
|
iotests.create_image(backing_img, TestErrors.image_len)
|
|
self.create_blkdebug_file(self.blkdebug_file, "read_aio", 5)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=blkdebug:%s:%s,backing_fmt=raw'
|
|
% (self.blkdebug_file, backing_img),
|
|
test_img)
|
|
self.vm = iotests.VM().add_drive(test_img)
|
|
self.vm.launch()
|
|
|
|
def tearDown(self):
|
|
self.vm.shutdown()
|
|
os.remove(test_img)
|
|
os.remove(backing_img)
|
|
os.remove(self.blkdebug_file)
|
|
|
|
def test_report(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('block-stream', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
completed = False
|
|
error = False
|
|
while not completed:
|
|
for event in self.vm.get_qmp_events(wait=True):
|
|
if event['event'] == 'BLOCK_JOB_ERROR':
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp(event, 'data/operation', 'read')
|
|
error = True
|
|
elif event['event'] == 'BLOCK_JOB_COMPLETED':
|
|
self.assertTrue(error, 'job completed unexpectedly')
|
|
self.assert_qmp(event, 'data/type', 'stream')
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp(event, 'data/error', 'Input/output error')
|
|
self.assert_qmp(event, 'data/offset', self.STREAM_BUFFER_SIZE)
|
|
self.assert_qmp(event, 'data/len', self.image_len)
|
|
completed = True
|
|
elif event['event'] == 'JOB_STATUS_CHANGE':
|
|
self.assert_qmp(event, 'data/id', 'drive0')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
def test_ignore(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('block-stream', device='drive0', on_error='ignore')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
error = False
|
|
completed = False
|
|
while not completed:
|
|
for event in self.vm.get_qmp_events(wait=True):
|
|
if event['event'] == 'BLOCK_JOB_ERROR':
|
|
error = True
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp(event, 'data/operation', 'read')
|
|
result = self.vm.qmp('query-block-jobs')
|
|
if result == {'return': []}:
|
|
# Job finished too quickly
|
|
continue
|
|
self.assert_qmp(result, 'return[0]/paused', False)
|
|
elif event['event'] == 'BLOCK_JOB_COMPLETED':
|
|
self.assertTrue(error, 'job completed unexpectedly')
|
|
self.assert_qmp(event, 'data/type', 'stream')
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp(event, 'data/error', 'Input/output error')
|
|
self.assert_qmp(event, 'data/offset', self.image_len)
|
|
self.assert_qmp(event, 'data/len', self.image_len)
|
|
completed = True
|
|
elif event['event'] == 'JOB_STATUS_CHANGE':
|
|
self.assert_qmp(event, 'data/id', 'drive0')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
def test_stop(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('block-stream', device='drive0', on_error='stop')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
error = False
|
|
completed = False
|
|
while not completed:
|
|
for event in self.vm.get_qmp_events(wait=True):
|
|
if event['event'] == 'BLOCK_JOB_ERROR':
|
|
error = True
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp(event, 'data/operation', 'read')
|
|
|
|
result = self.vm.qmp('query-block-jobs')
|
|
self.assert_qmp(result, 'return[0]/paused', True)
|
|
self.assert_qmp(result, 'return[0]/offset', self.STREAM_BUFFER_SIZE)
|
|
self.assert_qmp(result, 'return[0]/io-status', 'failed')
|
|
|
|
result = self.vm.qmp('block-job-resume', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('query-block-jobs')
|
|
if result == {'return': []}:
|
|
# Race; likely already finished. Check.
|
|
continue
|
|
self.assert_qmp(result, 'return[0]/paused', False)
|
|
self.assert_qmp(result, 'return[0]/io-status', 'ok')
|
|
elif event['event'] == 'BLOCK_JOB_COMPLETED':
|
|
self.assertTrue(error, 'job completed unexpectedly')
|
|
self.assert_qmp(event, 'data/type', 'stream')
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp_absent(event, 'data/error')
|
|
self.assert_qmp(event, 'data/offset', self.image_len)
|
|
self.assert_qmp(event, 'data/len', self.image_len)
|
|
completed = True
|
|
elif event['event'] == 'JOB_STATUS_CHANGE':
|
|
self.assert_qmp(event, 'data/id', 'drive0')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
def test_enospc(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('block-stream', device='drive0', on_error='enospc')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
completed = False
|
|
error = False
|
|
while not completed:
|
|
for event in self.vm.get_qmp_events(wait=True):
|
|
if event['event'] == 'BLOCK_JOB_ERROR':
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp(event, 'data/operation', 'read')
|
|
error = True
|
|
elif event['event'] == 'BLOCK_JOB_COMPLETED':
|
|
self.assertTrue(error, 'job completed unexpectedly')
|
|
self.assert_qmp(event, 'data/type', 'stream')
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp(event, 'data/error', 'Input/output error')
|
|
self.assert_qmp(event, 'data/offset', self.STREAM_BUFFER_SIZE)
|
|
self.assert_qmp(event, 'data/len', self.image_len)
|
|
completed = True
|
|
elif event['event'] == 'JOB_STATUS_CHANGE':
|
|
self.assert_qmp(event, 'data/id', 'drive0')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
class TestENOSPC(TestErrors):
|
|
def setUp(self):
|
|
self.blkdebug_file = backing_img + ".blkdebug"
|
|
iotests.create_image(backing_img, TestErrors.image_len)
|
|
self.create_blkdebug_file(self.blkdebug_file, "read_aio", 28)
|
|
qemu_img('create', '-f', iotests.imgfmt,
|
|
'-o', 'backing_file=blkdebug:%s:%s,backing_fmt=raw'
|
|
% (self.blkdebug_file, backing_img),
|
|
test_img)
|
|
self.vm = iotests.VM().add_drive(test_img)
|
|
self.vm.launch()
|
|
|
|
def tearDown(self):
|
|
self.vm.shutdown()
|
|
os.remove(test_img)
|
|
os.remove(backing_img)
|
|
os.remove(self.blkdebug_file)
|
|
|
|
def test_enospc(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('block-stream', device='drive0', on_error='enospc')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
error = False
|
|
completed = False
|
|
while not completed:
|
|
for event in self.vm.get_qmp_events(wait=True):
|
|
if event['event'] == 'BLOCK_JOB_ERROR':
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp(event, 'data/operation', 'read')
|
|
error = True
|
|
|
|
result = self.vm.qmp('query-block-jobs')
|
|
self.assert_qmp(result, 'return[0]/paused', True)
|
|
self.assert_qmp(result, 'return[0]/offset', self.STREAM_BUFFER_SIZE)
|
|
self.assert_qmp(result, 'return[0]/io-status', 'nospace')
|
|
|
|
result = self.vm.qmp('block-job-resume', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('query-block-jobs')
|
|
if result == {'return': []}:
|
|
# Race; likely already finished. Check.
|
|
continue
|
|
self.assert_qmp(result, 'return[0]/paused', False)
|
|
self.assert_qmp(result, 'return[0]/io-status', 'ok')
|
|
elif event['event'] == 'BLOCK_JOB_COMPLETED':
|
|
self.assertTrue(error, 'job completed unexpectedly')
|
|
self.assert_qmp(event, 'data/type', 'stream')
|
|
self.assert_qmp(event, 'data/device', 'drive0')
|
|
self.assert_qmp_absent(event, 'data/error')
|
|
self.assert_qmp(event, 'data/offset', self.image_len)
|
|
self.assert_qmp(event, 'data/len', self.image_len)
|
|
completed = True
|
|
elif event['event'] == 'JOB_STATUS_CHANGE':
|
|
self.assert_qmp(event, 'data/id', 'drive0')
|
|
|
|
self.assert_no_active_block_jobs()
|
|
self.vm.shutdown()
|
|
|
|
class TestStreamStop(iotests.QMPTestCase):
|
|
image_len = 8 * 1024 * 1024 * 1024 # GB
|
|
|
|
def setUp(self):
|
|
qemu_img('create', backing_img, str(TestStreamStop.image_len))
|
|
qemu_io('-f', 'raw', '-c', 'write -P 0x1 0 32M', backing_img)
|
|
qemu_img('create', '-f', iotests.imgfmt, '-o', 'backing_file=%s' % backing_img, test_img)
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0x1 32M 32M', test_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(backing_img)
|
|
|
|
def test_stream_stop(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
self.vm.pause_drive('drive0')
|
|
result = self.vm.qmp('block-stream', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
time.sleep(0.1)
|
|
events = self.vm.get_qmp_events(wait=False)
|
|
for e in events:
|
|
self.assert_qmp(e, 'event', 'JOB_STATUS_CHANGE')
|
|
self.assert_qmp(e, 'data/id', 'drive0')
|
|
|
|
self.cancel_and_wait(resume=True)
|
|
|
|
class TestSetSpeed(iotests.QMPTestCase):
|
|
image_len = 80 * 1024 * 1024 # MB
|
|
|
|
def setUp(self):
|
|
qemu_img('create', backing_img, str(TestSetSpeed.image_len))
|
|
qemu_io('-f', 'raw', '-c', 'write -P 0x1 0 32M', backing_img)
|
|
qemu_img('create', '-f', iotests.imgfmt, '-o', 'backing_file=%s' % backing_img, test_img)
|
|
qemu_io('-f', iotests.imgfmt, '-c', 'write -P 0x1 32M 32M', test_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(backing_img)
|
|
|
|
# This is a short performance test which is not run by default.
|
|
# Invoke "IMGFMT=qed ./030 TestSetSpeed.perf_test_throughput"
|
|
def perf_test_throughput(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('block-stream', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('block-job-set-speed', device='drive0', speed=8 * 1024 * 1024)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
self.wait_until_completed()
|
|
|
|
self.assert_no_active_block_jobs()
|
|
|
|
def test_set_speed(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
self.vm.pause_drive('drive0')
|
|
result = self.vm.qmp('block-stream', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
# Default speed is 0
|
|
result = self.vm.qmp('query-block-jobs')
|
|
self.assert_qmp(result, 'return[0]/device', 'drive0')
|
|
self.assert_qmp(result, 'return[0]/speed', 0)
|
|
|
|
result = self.vm.qmp('block-job-set-speed', device='drive0', speed=8 * 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', 8 * 1024 * 1024)
|
|
|
|
self.cancel_and_wait(resume=True)
|
|
self.vm.pause_drive('drive0')
|
|
|
|
# Check setting speed in block-stream works
|
|
result = self.vm.qmp('block-stream', device='drive0', speed=4 * 1024 * 1024)
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('query-block-jobs')
|
|
self.assert_qmp(result, 'return[0]/device', 'drive0')
|
|
self.assert_qmp(result, 'return[0]/speed', 4 * 1024 * 1024)
|
|
|
|
self.cancel_and_wait(resume=True)
|
|
|
|
def test_set_speed_invalid(self):
|
|
self.assert_no_active_block_jobs()
|
|
|
|
result = self.vm.qmp('block-stream', device='drive0', speed=-1)
|
|
self.assert_qmp(result, 'error/desc', "Parameter 'speed' expects a non-negative value")
|
|
|
|
self.assert_no_active_block_jobs()
|
|
|
|
self.vm.pause_drive('drive0')
|
|
result = self.vm.qmp('block-stream', device='drive0')
|
|
self.assert_qmp(result, 'return', {})
|
|
|
|
result = self.vm.qmp('block-job-set-speed', device='drive0', speed=-1)
|
|
self.assert_qmp(result, 'error/desc', "Parameter 'speed' expects a non-negative value")
|
|
|
|
self.cancel_and_wait(resume=True)
|
|
|
|
if __name__ == '__main__':
|
|
iotests.main(supported_fmts=['qcow2', 'qed'],
|
|
supported_protocols=['file'])
|