iotests.py 18.2 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
# Common utilities and Python wrappers for qemu-iotests
#
# 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/>.
#

19
import errno
20 21 22
import os
import re
import subprocess
23
import string
24
import unittest
25 26 27
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'scripts'))
import qtest
28
import struct
F
Fam Zheng 已提交
29
import json
30
import signal
E
Eduardo Habkost 已提交
31
import logging
32 33


34
# This will not work if arguments contain spaces but is necessary if we
35
# want to support the override options that ./check supports.
36 37 38 39 40 41 42 43
qemu_img_args = [os.environ.get('QEMU_IMG_PROG', 'qemu-img')]
if os.environ.get('QEMU_IMG_OPTIONS'):
    qemu_img_args += os.environ['QEMU_IMG_OPTIONS'].strip().split(' ')

qemu_io_args = [os.environ.get('QEMU_IO_PROG', 'qemu-io')]
if os.environ.get('QEMU_IO_OPTIONS'):
    qemu_io_args += os.environ['QEMU_IO_OPTIONS'].strip().split(' ')

M
Max Reitz 已提交
44 45 46 47
qemu_nbd_args = [os.environ.get('QEMU_NBD_PROG', 'qemu-nbd')]
if os.environ.get('QEMU_NBD_OPTIONS'):
    qemu_nbd_args += os.environ['QEMU_NBD_OPTIONS'].strip().split(' ')

48
qemu_prog = os.environ.get('QEMU_PROG', 'qemu')
49
qemu_opts = os.environ.get('QEMU_OPTIONS', '').strip().split(' ')
50 51 52

imgfmt = os.environ.get('IMGFMT', 'raw')
imgproto = os.environ.get('IMGPROTO', 'file')
53
test_dir = os.environ.get('TEST_DIR')
M
Max Reitz 已提交
54
output_dir = os.environ.get('OUTPUT_DIR', '.')
55
cachemode = os.environ.get('CACHEMODE')
B
Bo Tu 已提交
56
qemu_default_machine = os.environ.get('QEMU_DEFAULT_MACHINE')
57

58
socket_scm_helper = os.environ.get('SOCKET_SCM_HELPER', 'socket_scm_helper')
59
debug = False
60

61 62 63
def qemu_img(*args):
    '''Run qemu-img and return the exit code'''
    devnull = open('/dev/null', 'r+')
64 65 66 67
    exitcode = subprocess.call(qemu_img_args + list(args), stdin=devnull, stdout=devnull)
    if exitcode < 0:
        sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args))))
    return exitcode
68

69
def qemu_img_verbose(*args):
70
    '''Run qemu-img without suppressing its output and return the exit code'''
71 72 73 74
    exitcode = subprocess.call(qemu_img_args + list(args))
    if exitcode < 0:
        sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args))))
    return exitcode
75

76 77
def qemu_img_pipe(*args):
    '''Run qemu-img and return its output'''
78 79 80
    subp = subprocess.Popen(qemu_img_args + list(args),
                            stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT)
81 82 83 84
    exitcode = subp.wait()
    if exitcode < 0:
        sys.stderr.write('qemu-img received signal %i: %s\n' % (-exitcode, ' '.join(qemu_img_args + list(args))))
    return subp.communicate()[0]
85

86 87 88
def qemu_io(*args):
    '''Run qemu-io and return the stdout data'''
    args = qemu_io_args + list(args)
89 90
    subp = subprocess.Popen(args, stdout=subprocess.PIPE,
                            stderr=subprocess.STDOUT)
91 92 93 94
    exitcode = subp.wait()
    if exitcode < 0:
        sys.stderr.write('qemu-io received signal %i: %s\n' % (-exitcode, ' '.join(args)))
    return subp.communicate()[0]
95

96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133

class QemuIoInteractive:
    def __init__(self, *args):
        self.args = qemu_io_args + list(args)
        self._p = subprocess.Popen(self.args, stdin=subprocess.PIPE,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.STDOUT)
        assert self._p.stdout.read(9) == 'qemu-io> '

    def close(self):
        self._p.communicate('q\n')

    def _read_output(self):
        pattern = 'qemu-io> '
        n = len(pattern)
        pos = 0
        s = []
        while pos != n:
            c = self._p.stdout.read(1)
            # check unexpected EOF
            assert c != ''
            s.append(c)
            if c == pattern[pos]:
                pos += 1
            else:
                pos = 0

        return ''.join(s[:-n])

    def cmd(self, cmd):
        # quit command is in close(), '\n' is added automatically
        assert '\n' not in cmd
        cmd = cmd.strip()
        assert cmd != 'q' and cmd != 'quit'
        self._p.stdin.write(cmd + '\n')
        return self._read_output()


M
Max Reitz 已提交
134 135 136 137
def qemu_nbd(*args):
    '''Run qemu-nbd in daemon mode and return the parent's exit code'''
    return subprocess.call(qemu_nbd_args + ['--fork'] + list(args))

138
def compare_images(img1, img2, fmt1=imgfmt, fmt2=imgfmt):
139
    '''Return True if two image files are identical'''
140 141
    return qemu_img('compare', '-f', fmt1,
                    '-F', fmt2, img1, img2) == 0
142

143 144 145 146 147 148 149 150 151 152
def create_image(name, size):
    '''Create a fully-allocated raw image with sector markers'''
    file = open(name, 'w')
    i = 0
    while i < size:
        sector = struct.pack('>l504xl', i / 512, i / 512)
        file.write(sector)
        i = i + 512
    file.close()

F
Fam Zheng 已提交
153 154 155 156 157
def image_size(img):
    '''Return image's virtual size'''
    r = qemu_img_pipe('info', '--output=json', '-f', imgfmt, img)
    return json.loads(r)['virtual-size']

158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
test_dir_re = re.compile(r"%s" % test_dir)
def filter_test_dir(msg):
    return test_dir_re.sub("TEST_DIR", msg)

win32_re = re.compile(r"\r")
def filter_win32(msg):
    return win32_re.sub("", msg)

qemu_io_re = re.compile(r"[0-9]* ops; [0-9\/:. sec]* \([0-9\/.inf]* [EPTGMKiBbytes]*\/sec and [0-9\/.inf]* ops\/sec\)")
def filter_qemu_io(msg):
    msg = filter_win32(msg)
    return qemu_io_re.sub("X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)", msg)

chown_re = re.compile(r"chown [0-9]+:[0-9]+")
def filter_chown(msg):
    return chown_re.sub("chown UID:GID", msg)

175 176 177 178 179 180 181 182
def filter_qmp_event(event):
    '''Filter a QMP event dict'''
    event = dict(event)
    if 'timestamp' in event:
        event['timestamp']['seconds'] = 'SECS'
        event['timestamp']['microseconds'] = 'USECS'
    return event

183 184 185 186 187
def log(msg, filters=[]):
    for flt in filters:
        msg = flt(msg)
    print msg

188 189 190 191 192 193 194 195 196 197 198 199 200 201
class Timeout:
    def __init__(self, seconds, errmsg = "Timeout"):
        self.seconds = seconds
        self.errmsg = errmsg
    def __enter__(self):
        signal.signal(signal.SIGALRM, self.timeout)
        signal.setitimer(signal.ITIMER_REAL, self.seconds)
        return self
    def __exit__(self, type, value, traceback):
        signal.setitimer(signal.ITIMER_REAL, 0)
        return False
    def timeout(self, signum, frame):
        raise Exception(self.errmsg)

202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227

class FilePath(object):
    '''An auto-generated filename that cleans itself up.

    Use this context manager to generate filenames and ensure that the file
    gets deleted::

        with TestFilePath('test.img') as img_path:
            qemu_img('create', img_path, '1G')
        # migration_sock_path is automatically deleted
    '''
    def __init__(self, name):
        filename = '{0}-{1}'.format(os.getpid(), name)
        self.path = os.path.join(test_dir, filename)

    def __enter__(self):
        return self.path

    def __exit__(self, exc_type, exc_val, exc_tb):
        try:
            os.remove(self.path)
        except OSError:
            pass
        return False


228
class VM(qtest.QEMUQtestMachine):
229 230
    '''A QEMU VM'''

231 232 233 234
    def __init__(self, path_suffix=''):
        name = "qemu%s-%d" % (path_suffix, os.getpid())
        super(VM, self).__init__(qemu_prog, qemu_opts, name=name,
                                 test_dir=test_dir,
235
                                 socket_scm_helper=socket_scm_helper)
236
        self._num_drives = 0
237

S
Stefan Hajnoczi 已提交
238 239 240 241 242
    def add_object(self, opts):
        self._args.append('-object')
        self._args.append(opts)
        return self

243 244 245 246 247
    def add_device(self, opts):
        self._args.append('-device')
        self._args.append(opts)
        return self

F
Fam Zheng 已提交
248 249 250 251 252
    def add_drive_raw(self, opts):
        self._args.append('-drive')
        self._args.append(opts)
        return self

253
    def add_drive(self, path, opts='', interface='virtio', format=imgfmt):
254
        '''Add a virtio-blk drive to the VM'''
255
        options = ['if=%s' % interface,
256
                   'id=drive%d' % self._num_drives]
257 258 259

        if path is not None:
            options.append('file=%s' % path)
260
            options.append('format=%s' % format)
261
            options.append('cache=%s' % cachemode)
262

263 264 265 266 267 268 269 270
        if opts:
            options.append(opts)

        self._args.append('-drive')
        self._args.append(','.join(options))
        self._num_drives += 1
        return self

271 272 273 274 275 276 277 278
    def add_blockdev(self, opts):
        self._args.append('-blockdev')
        if isinstance(opts, str):
            self._args.append(opts)
        else:
            self._args.append(','.join(opts))
        return self

279 280 281 282 283
    def add_incoming(self, addr):
        self._args.append('-incoming')
        self._args.append(addr)
        return self

284 285 286 287 288 289 290 291 292 293 294 295 296
    def pause_drive(self, drive, event=None):
        '''Pause drive r/w operations'''
        if not event:
            self.pause_drive(drive, "read_aio")
            self.pause_drive(drive, "write_aio")
            return
        self.qmp('human-monitor-command',
                    command_line='qemu-io %s "break %s bp_%s"' % (drive, event, drive))

    def resume_drive(self, drive):
        self.qmp('human-monitor-command',
                    command_line='qemu-io %s "remove_break bp_%s"' % (drive, drive))

297 298 299 300 301
    def hmp_qemu_io(self, drive, cmd):
        '''Write to a given drive using an HMP command'''
        return self.qmp('human-monitor-command',
                        command_line='qemu-io %s "%s"' % (drive, cmd))

J
John Snow 已提交
302

303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328
index_re = re.compile(r'([^\[]+)\[([^\]]+)\]')

class QMPTestCase(unittest.TestCase):
    '''Abstract base class for QMP test cases'''

    def dictpath(self, d, path):
        '''Traverse a path in a nested dict'''
        for component in path.split('/'):
            m = index_re.match(component)
            if m:
                component, idx = m.groups()
                idx = int(idx)

            if not isinstance(d, dict) or component not in d:
                self.fail('failed path traversal for "%s" in "%s"' % (path, str(d)))
            d = d[component]

            if m:
                if not isinstance(d, list):
                    self.fail('path component "%s" in "%s" is not a list in "%s"' % (component, path, str(d)))
                try:
                    d = d[idx]
                except IndexError:
                    self.fail('invalid index "%s" in path "%s" in "%s"' % (idx, path, str(d)))
        return d

329 330 331 332 333 334 335 336 337 338 339 340 341
    def flatten_qmp_object(self, obj, output=None, basestr=''):
        if output is None:
            output = dict()
        if isinstance(obj, list):
            for i in range(len(obj)):
                self.flatten_qmp_object(obj[i], output, basestr + str(i) + '.')
        elif isinstance(obj, dict):
            for key in obj:
                self.flatten_qmp_object(obj[key], output, basestr + key + '.')
        else:
            output[basestr[:-1]] = obj # Strip trailing '.'
        return output

342 343 344 345 346 347 348
    def qmp_to_opts(self, obj):
        obj = self.flatten_qmp_object(obj)
        output_list = list()
        for key in obj:
            output_list += [key + '=' + obj[key]]
        return ','.join(output_list)

349 350 351 352 353 354 355
    def assert_qmp_absent(self, d, path):
        try:
            result = self.dictpath(d, path)
        except AssertionError:
            return
        self.fail('path "%s" has value "%s"' % (path, str(result)))

356 357 358 359 360
    def assert_qmp(self, d, path, value):
        '''Assert that the value for a specific path in a QMP dict matches'''
        result = self.dictpath(d, path)
        self.assertEqual(result, value, 'values not equal "%s" and "%s"' % (str(result), str(value)))

361 362 363 364
    def assert_no_active_block_jobs(self):
        result = self.vm.qmp('query-block-jobs')
        self.assert_qmp(result, 'return', [])

365 366 367 368 369 370 371 372 373 374 375 376 377 378
    def assert_has_block_node(self, node_name=None, file_name=None):
        """Issue a query-named-block-nodes and assert node_name and/or
        file_name is present in the result"""
        def check_equal_or_none(a, b):
            return a == None or b == None or a == b
        assert node_name or file_name
        result = self.vm.qmp('query-named-block-nodes')
        for x in result["return"]:
            if check_equal_or_none(x.get("node-name"), node_name) and \
                    check_equal_or_none(x.get("file"), file_name):
                return
        self.assertTrue(False, "Cannot find %s %s in result:\n%s" % \
                (node_name, file_name, result))

379 380 381 382 383 384 385
    def assert_json_filename_equal(self, json_filename, reference):
        '''Asserts that the given filename is a json: filename and that its
           content is equal to the given reference object'''
        self.assertEqual(json_filename[:5], 'json:')
        self.assertEqual(self.flatten_qmp_object(json.loads(json_filename[5:])),
                         self.flatten_qmp_object(reference))

386
    def cancel_and_wait(self, drive='drive0', force=False, resume=False):
387 388 389 390
        '''Cancel a block job and wait for it to finish, returning the event'''
        result = self.vm.qmp('block-job-cancel', device=drive, force=force)
        self.assert_qmp(result, 'return', {})

391 392 393
        if resume:
            self.vm.resume_drive(drive)

394 395 396 397 398 399 400 401 402 403 404 405 406
        cancelled = False
        result = None
        while not cancelled:
            for event in self.vm.get_qmp_events(wait=True):
                if event['event'] == 'BLOCK_JOB_COMPLETED' or \
                   event['event'] == 'BLOCK_JOB_CANCELLED':
                    self.assert_qmp(event, 'data/device', drive)
                    result = event
                    cancelled = True

        self.assert_no_active_block_jobs()
        return result

407
    def wait_until_completed(self, drive='drive0', check_offset=True):
408 409 410 411 412 413 414
        '''Wait for a block job to finish, returning the event'''
        completed = False
        while not completed:
            for event in self.vm.get_qmp_events(wait=True):
                if event['event'] == 'BLOCK_JOB_COMPLETED':
                    self.assert_qmp(event, 'data/device', drive)
                    self.assert_qmp_absent(event, 'data/error')
415
                    if check_offset:
416
                        self.assert_qmp(event, 'data/offset', event['data']['len'])
417 418 419 420 421
                    completed = True

        self.assert_no_active_block_jobs()
        return event

422 423
    def wait_ready(self, drive='drive0'):
        '''Wait until a block job BLOCK_JOB_READY event'''
424 425
        f = {'data': {'type': 'mirror', 'device': drive } }
        event = self.vm.event_wait(name='BLOCK_JOB_READY', match=f)
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444

    def wait_ready_and_cancel(self, drive='drive0'):
        self.wait_ready(drive=drive)
        event = self.cancel_and_wait(drive=drive)
        self.assertEquals(event['event'], 'BLOCK_JOB_COMPLETED')
        self.assert_qmp(event, 'data/type', 'mirror')
        self.assert_qmp(event, 'data/offset', event['data']['len'])

    def complete_and_wait(self, drive='drive0', wait_ready=True):
        '''Complete a block job and wait for it to finish'''
        if wait_ready:
            self.wait_ready(drive=drive)

        result = self.vm.qmp('block-job-complete', device=drive)
        self.assert_qmp(result, 'return', {})

        event = self.wait_until_completed(drive=drive)
        self.assert_qmp(event, 'data/type', 'mirror')

445 446 447 448 449 450 451 452 453 454 455 456
    def pause_job(self, job_id='job0'):
        result = self.vm.qmp('block-job-pause', device=job_id)
        self.assert_qmp(result, 'return', {})

        with Timeout(1, "Timeout waiting for job to pause"):
            while True:
                result = self.vm.qmp('query-block-jobs')
                for job in result['return']:
                    if job['device'] == job_id and job['paused'] == True and job['busy'] == False:
                        return job


457 458 459 460 461
def notrun(reason):
    '''Skip this test suite'''
    # Each test in qemu-iotests has a number ("seq")
    seq = os.path.basename(sys.argv[0])

M
Max Reitz 已提交
462
    open('%s/%s.notrun' % (output_dir, seq), 'wb').write(reason + '\n')
463 464 465
    print '%s not run: %s' % (seq, reason)
    sys.exit(0)

466
def verify_image_format(supported_fmts=[], unsupported_fmts=[]):
467 468
    if supported_fmts and (imgfmt not in supported_fmts):
        notrun('not suitable for this image format: %s' % imgfmt)
469 470
    if unsupported_fmts and (imgfmt in unsupported_fmts):
        notrun('not suitable for this image format: %s' % imgfmt)
471

472
def verify_platform(supported_oses=['linux']):
473
    if True not in [sys.platform.startswith(x) for x in supported_oses]:
474 475
        notrun('not suitable for this OS: %s' % sys.platform)

476 477 478
def supports_quorum():
    return 'quorum' in qemu_img_pipe('--help')

479 480
def verify_quorum():
    '''Skip test suite if quorum support is not available'''
481
    if not supports_quorum():
482 483
        notrun('quorum support missing')

484 485 486
def main(supported_fmts=[], supported_oses=['linux']):
    '''Run tests'''

487 488
    global debug

489 490 491 492 493 494 495 496
    # We are using TEST_DIR and QEMU_DEFAULT_MACHINE as proxies to
    # indicate that we're not being run via "check". There may be
    # other things set up by "check" that individual test cases rely
    # on.
    if test_dir is None or qemu_default_machine is None:
        sys.stderr.write('Please run this test via the "check" script\n')
        sys.exit(os.EX_USAGE)

497 498 499 500 501
    debug = '-d' in sys.argv
    verbosity = 1
    verify_image_format(supported_fmts)
    verify_platform(supported_oses)

502 503 504
    # We need to filter out the time taken from the output so that qemu-iotest
    # can reliably diff the results against master output.
    import StringIO
505 506 507 508 509 510
    if debug:
        output = sys.stdout
        verbosity = 2
        sys.argv.remove('-d')
    else:
        output = StringIO.StringIO()
511

E
Eduardo Habkost 已提交
512 513
    logging.basicConfig(level=(logging.DEBUG if debug else logging.WARN))

514
    class MyTestRunner(unittest.TextTestRunner):
515
        def __init__(self, stream=output, descriptions=True, verbosity=verbosity):
516 517 518 519 520 521
            unittest.TextTestRunner.__init__(self, stream, descriptions, verbosity)

    # unittest.main() will use sys.exit() so expect a SystemExit exception
    try:
        unittest.main(testRunner=MyTestRunner)
    finally:
522 523
        if not debug:
            sys.stderr.write(re.sub(r'Ran (\d+) tests? in [\d.]+s', r'Ran \1 tests', output.getvalue()))