xref: /qemu/tests/qemu-iotests/nbd-fault-injector.py (revision 903cb1bf398666014180d00711e2c1a9ffdadd5a)
1*903cb1bfSPhilippe Mathieu-Daudé#!/usr/bin/env python3
21e8ece0dSStefan Hajnoczi# NBD server - fault injection utility
31e8ece0dSStefan Hajnoczi#
41e8ece0dSStefan Hajnoczi# Configuration file syntax:
51e8ece0dSStefan Hajnoczi#   [inject-error "disconnect-neg1"]
61e8ece0dSStefan Hajnoczi#   event=neg1
71e8ece0dSStefan Hajnoczi#   io=readwrite
81e8ece0dSStefan Hajnoczi#   when=before
91e8ece0dSStefan Hajnoczi#
101e8ece0dSStefan Hajnoczi# Note that Python's ConfigParser squashes together all sections with the same
111e8ece0dSStefan Hajnoczi# name, so give each [inject-error] a unique name.
121e8ece0dSStefan Hajnoczi#
131e8ece0dSStefan Hajnoczi# inject-error options:
141e8ece0dSStefan Hajnoczi#   event - name of the trigger event
151e8ece0dSStefan Hajnoczi#           "neg1" - first part of negotiation struct
161e8ece0dSStefan Hajnoczi#           "export" - export struct
171e8ece0dSStefan Hajnoczi#           "neg2" - second part of negotiation struct
181e8ece0dSStefan Hajnoczi#           "request" - NBD request struct
191e8ece0dSStefan Hajnoczi#           "reply" - NBD reply struct
201e8ece0dSStefan Hajnoczi#           "data" - request/reply data
211e8ece0dSStefan Hajnoczi#   io    - I/O direction that triggers this rule:
221e8ece0dSStefan Hajnoczi#           "read", "write", or "readwrite"
231e8ece0dSStefan Hajnoczi#           default: readwrite
241e8ece0dSStefan Hajnoczi#   when  - after how many bytes to inject the fault
251e8ece0dSStefan Hajnoczi#           -1 - inject error after I/O
261e8ece0dSStefan Hajnoczi#           0 - inject error before I/O
271e8ece0dSStefan Hajnoczi#           integer - inject error after integer bytes
281e8ece0dSStefan Hajnoczi#           "before" - alias for 0
291e8ece0dSStefan Hajnoczi#           "after" - alias for -1
301e8ece0dSStefan Hajnoczi#           default: before
311e8ece0dSStefan Hajnoczi#
321e8ece0dSStefan Hajnoczi# Currently the only error injection action is to terminate the server process.
331e8ece0dSStefan Hajnoczi# This resets the TCP connection and thus forces the client to handle
341e8ece0dSStefan Hajnoczi# unexpected connection termination.
351e8ece0dSStefan Hajnoczi#
361e8ece0dSStefan Hajnoczi# Other error injection actions could be added in the future.
371e8ece0dSStefan Hajnoczi#
381e8ece0dSStefan Hajnoczi# Copyright Red Hat, Inc. 2014
391e8ece0dSStefan Hajnoczi#
401e8ece0dSStefan Hajnoczi# Authors:
411e8ece0dSStefan Hajnoczi#   Stefan Hajnoczi <stefanha@redhat.com>
421e8ece0dSStefan Hajnoczi#
431e8ece0dSStefan Hajnoczi# This work is licensed under the terms of the GNU GPL, version 2 or later.
441e8ece0dSStefan Hajnoczi# See the COPYING file in the top-level directory.
451e8ece0dSStefan Hajnoczi
46f03868bdSEduardo Habkostfrom __future__ import print_function
471e8ece0dSStefan Hajnocziimport sys
481e8ece0dSStefan Hajnocziimport socket
491e8ece0dSStefan Hajnocziimport struct
501e8ece0dSStefan Hajnocziimport collections
512d894beeSMax Reitzif sys.version_info.major >= 3:
522d894beeSMax Reitz    import configparser
532d894beeSMax Reitzelse:
542d894beeSMax Reitz    import ConfigParser as configparser
551e8ece0dSStefan Hajnoczi
561e8ece0dSStefan HajnocziFAKE_DISK_SIZE = 8 * 1024 * 1024 * 1024 # 8 GB
571e8ece0dSStefan Hajnoczi
581e8ece0dSStefan Hajnoczi# Protocol constants
591e8ece0dSStefan HajnocziNBD_CMD_READ = 0
601e8ece0dSStefan HajnocziNBD_CMD_WRITE = 1
611e8ece0dSStefan HajnocziNBD_CMD_DISC = 2
621e8ece0dSStefan HajnocziNBD_REQUEST_MAGIC = 0x25609513
637b3158f9SVladimir Sementsov-OgievskiyNBD_SIMPLE_REPLY_MAGIC = 0x67446698
641e8ece0dSStefan HajnocziNBD_PASSWD = 0x4e42444d41474943
651e8ece0dSStefan HajnocziNBD_OPTS_MAGIC = 0x49484156454F5054
661e8ece0dSStefan HajnocziNBD_CLIENT_MAGIC = 0x0000420281861253
671e8ece0dSStefan HajnocziNBD_OPT_EXPORT_NAME = 1 << 0
681e8ece0dSStefan Hajnoczi
691e8ece0dSStefan Hajnoczi# Protocol structs
701e8ece0dSStefan Hajnoczineg_classic_struct = struct.Struct('>QQQI124x')
711e8ece0dSStefan Hajnoczineg1_struct = struct.Struct('>QQH')
721e8ece0dSStefan Hajnocziexport_tuple = collections.namedtuple('Export', 'reserved magic opt len')
731e8ece0dSStefan Hajnocziexport_struct = struct.Struct('>IQII')
741e8ece0dSStefan Hajnoczineg2_struct = struct.Struct('>QH124x')
751e8ece0dSStefan Hajnoczirequest_tuple = collections.namedtuple('Request', 'magic type handle from_ len')
761e8ece0dSStefan Hajnoczirequest_struct = struct.Struct('>IIQQI')
771e8ece0dSStefan Hajnoczireply_struct = struct.Struct('>IIQ')
781e8ece0dSStefan Hajnoczi
791e8ece0dSStefan Hajnoczidef err(msg):
801e8ece0dSStefan Hajnoczi    sys.stderr.write(msg + '\n')
811e8ece0dSStefan Hajnoczi    sys.exit(1)
821e8ece0dSStefan Hajnoczi
831e8ece0dSStefan Hajnoczidef recvall(sock, bufsize):
841e8ece0dSStefan Hajnoczi    received = 0
851e8ece0dSStefan Hajnoczi    chunks = []
861e8ece0dSStefan Hajnoczi    while received < bufsize:
871e8ece0dSStefan Hajnoczi        chunk = sock.recv(bufsize - received)
881e8ece0dSStefan Hajnoczi        if len(chunk) == 0:
891e8ece0dSStefan Hajnoczi            raise Exception('unexpected disconnect')
901e8ece0dSStefan Hajnoczi        chunks.append(chunk)
911e8ece0dSStefan Hajnoczi        received += len(chunk)
928eb5e674SMax Reitz    return b''.join(chunks)
931e8ece0dSStefan Hajnoczi
941e8ece0dSStefan Hajnocziclass Rule(object):
951e8ece0dSStefan Hajnoczi    def __init__(self, name, event, io, when):
961e8ece0dSStefan Hajnoczi        self.name = name
971e8ece0dSStefan Hajnoczi        self.event = event
981e8ece0dSStefan Hajnoczi        self.io = io
991e8ece0dSStefan Hajnoczi        self.when = when
1001e8ece0dSStefan Hajnoczi
1011e8ece0dSStefan Hajnoczi    def match(self, event, io):
1021e8ece0dSStefan Hajnoczi        if event != self.event:
1031e8ece0dSStefan Hajnoczi            return False
1041e8ece0dSStefan Hajnoczi        if io != self.io and self.io != 'readwrite':
1051e8ece0dSStefan Hajnoczi            return False
1061e8ece0dSStefan Hajnoczi        return True
1071e8ece0dSStefan Hajnoczi
1081e8ece0dSStefan Hajnocziclass FaultInjectionSocket(object):
1091e8ece0dSStefan Hajnoczi    def __init__(self, sock, rules):
1101e8ece0dSStefan Hajnoczi        self.sock = sock
1111e8ece0dSStefan Hajnoczi        self.rules = rules
1121e8ece0dSStefan Hajnoczi
1131e8ece0dSStefan Hajnoczi    def check(self, event, io, bufsize=None):
1141e8ece0dSStefan Hajnoczi        for rule in self.rules:
1151e8ece0dSStefan Hajnoczi            if rule.match(event, io):
1161e8ece0dSStefan Hajnoczi                if rule.when == 0 or bufsize is None:
117f03868bdSEduardo Habkost                    print('Closing connection on rule match %s' % rule.name)
118a4d925f8SAndrey Shinkevich                    self.sock.close()
119a4d925f8SAndrey Shinkevich                    sys.stdout.flush()
1201e8ece0dSStefan Hajnoczi                    sys.exit(0)
1211e8ece0dSStefan Hajnoczi                if rule.when != -1:
1221e8ece0dSStefan Hajnoczi                    return rule.when
1231e8ece0dSStefan Hajnoczi        return bufsize
1241e8ece0dSStefan Hajnoczi
1251e8ece0dSStefan Hajnoczi    def send(self, buf, event):
1261e8ece0dSStefan Hajnoczi        bufsize = self.check(event, 'write', bufsize=len(buf))
1271e8ece0dSStefan Hajnoczi        self.sock.sendall(buf[:bufsize])
1281e8ece0dSStefan Hajnoczi        self.check(event, 'write')
1291e8ece0dSStefan Hajnoczi
1301e8ece0dSStefan Hajnoczi    def recv(self, bufsize, event):
1311e8ece0dSStefan Hajnoczi        bufsize = self.check(event, 'read', bufsize=bufsize)
1321e8ece0dSStefan Hajnoczi        data = recvall(self.sock, bufsize)
1331e8ece0dSStefan Hajnoczi        self.check(event, 'read')
1341e8ece0dSStefan Hajnoczi        return data
1351e8ece0dSStefan Hajnoczi
1361e8ece0dSStefan Hajnoczi    def close(self):
1371e8ece0dSStefan Hajnoczi        self.sock.close()
1381e8ece0dSStefan Hajnoczi
1391e8ece0dSStefan Hajnoczidef negotiate_classic(conn):
1401e8ece0dSStefan Hajnoczi    buf = neg_classic_struct.pack(NBD_PASSWD, NBD_CLIENT_MAGIC,
1411e8ece0dSStefan Hajnoczi                                  FAKE_DISK_SIZE, 0)
1421e8ece0dSStefan Hajnoczi    conn.send(buf, event='neg-classic')
1431e8ece0dSStefan Hajnoczi
1441e8ece0dSStefan Hajnoczidef negotiate_export(conn):
1451e8ece0dSStefan Hajnoczi    # Send negotiation part 1
1461e8ece0dSStefan Hajnoczi    buf = neg1_struct.pack(NBD_PASSWD, NBD_OPTS_MAGIC, 0)
1471e8ece0dSStefan Hajnoczi    conn.send(buf, event='neg1')
1481e8ece0dSStefan Hajnoczi
1491e8ece0dSStefan Hajnoczi    # Receive export option
1501e8ece0dSStefan Hajnoczi    buf = conn.recv(export_struct.size, event='export')
1511e8ece0dSStefan Hajnoczi    export = export_tuple._make(export_struct.unpack(buf))
1521e8ece0dSStefan Hajnoczi    assert export.magic == NBD_OPTS_MAGIC
1531e8ece0dSStefan Hajnoczi    assert export.opt == NBD_OPT_EXPORT_NAME
1541e8ece0dSStefan Hajnoczi    name = conn.recv(export.len, event='export-name')
1551e8ece0dSStefan Hajnoczi
1561e8ece0dSStefan Hajnoczi    # Send negotiation part 2
1571e8ece0dSStefan Hajnoczi    buf = neg2_struct.pack(FAKE_DISK_SIZE, 0)
1581e8ece0dSStefan Hajnoczi    conn.send(buf, event='neg2')
1591e8ece0dSStefan Hajnoczi
1601e8ece0dSStefan Hajnoczidef negotiate(conn, use_export):
1611e8ece0dSStefan Hajnoczi    '''Negotiate export with client'''
1621e8ece0dSStefan Hajnoczi    if use_export:
1631e8ece0dSStefan Hajnoczi        negotiate_export(conn)
1641e8ece0dSStefan Hajnoczi    else:
1651e8ece0dSStefan Hajnoczi        negotiate_classic(conn)
1661e8ece0dSStefan Hajnoczi
1671e8ece0dSStefan Hajnoczidef read_request(conn):
1681e8ece0dSStefan Hajnoczi    '''Parse NBD request from client'''
1691e8ece0dSStefan Hajnoczi    buf = conn.recv(request_struct.size, event='request')
1701e8ece0dSStefan Hajnoczi    req = request_tuple._make(request_struct.unpack(buf))
1711e8ece0dSStefan Hajnoczi    assert req.magic == NBD_REQUEST_MAGIC
1721e8ece0dSStefan Hajnoczi    return req
1731e8ece0dSStefan Hajnoczi
1741e8ece0dSStefan Hajnoczidef write_reply(conn, error, handle):
1757b3158f9SVladimir Sementsov-Ogievskiy    buf = reply_struct.pack(NBD_SIMPLE_REPLY_MAGIC, error, handle)
1761e8ece0dSStefan Hajnoczi    conn.send(buf, event='reply')
1771e8ece0dSStefan Hajnoczi
1781e8ece0dSStefan Hajnoczidef handle_connection(conn, use_export):
1791e8ece0dSStefan Hajnoczi    negotiate(conn, use_export)
1801e8ece0dSStefan Hajnoczi    while True:
1811e8ece0dSStefan Hajnoczi        req = read_request(conn)
1821e8ece0dSStefan Hajnoczi        if req.type == NBD_CMD_READ:
1831e8ece0dSStefan Hajnoczi            write_reply(conn, 0, req.handle)
1848eb5e674SMax Reitz            conn.send(b'\0' * req.len, event='data')
1851e8ece0dSStefan Hajnoczi        elif req.type == NBD_CMD_WRITE:
1861e8ece0dSStefan Hajnoczi            _ = conn.recv(req.len, event='data')
1871e8ece0dSStefan Hajnoczi            write_reply(conn, 0, req.handle)
1881e8ece0dSStefan Hajnoczi        elif req.type == NBD_CMD_DISC:
1891e8ece0dSStefan Hajnoczi            break
1901e8ece0dSStefan Hajnoczi        else:
191f03868bdSEduardo Habkost            print('unrecognized command type %#02x' % req.type)
1921e8ece0dSStefan Hajnoczi            break
1931e8ece0dSStefan Hajnoczi    conn.close()
1941e8ece0dSStefan Hajnoczi
1951e8ece0dSStefan Hajnoczidef run_server(sock, rules, use_export):
1961e8ece0dSStefan Hajnoczi    while True:
1971e8ece0dSStefan Hajnoczi        conn, _ = sock.accept()
1981e8ece0dSStefan Hajnoczi        handle_connection(FaultInjectionSocket(conn, rules), use_export)
1991e8ece0dSStefan Hajnoczi
2001e8ece0dSStefan Hajnoczidef parse_inject_error(name, options):
2011e8ece0dSStefan Hajnoczi    if 'event' not in options:
2021e8ece0dSStefan Hajnoczi        err('missing \"event\" option in %s' % name)
2031e8ece0dSStefan Hajnoczi    event = options['event']
2041e8ece0dSStefan Hajnoczi    if event not in ('neg-classic', 'neg1', 'export', 'neg2', 'request', 'reply', 'data'):
2051e8ece0dSStefan Hajnoczi        err('invalid \"event\" option value \"%s\" in %s' % (event, name))
2061e8ece0dSStefan Hajnoczi    io = options.get('io', 'readwrite')
2071e8ece0dSStefan Hajnoczi    if io not in ('read', 'write', 'readwrite'):
2081e8ece0dSStefan Hajnoczi        err('invalid \"io\" option value \"%s\" in %s' % (io, name))
2091e8ece0dSStefan Hajnoczi    when = options.get('when', 'before')
2101e8ece0dSStefan Hajnoczi    try:
2111e8ece0dSStefan Hajnoczi        when = int(when)
2121e8ece0dSStefan Hajnoczi    except ValueError:
2131e8ece0dSStefan Hajnoczi        if when == 'before':
2141e8ece0dSStefan Hajnoczi            when = 0
2151e8ece0dSStefan Hajnoczi        elif when == 'after':
2161e8ece0dSStefan Hajnoczi            when = -1
2171e8ece0dSStefan Hajnoczi        else:
2181e8ece0dSStefan Hajnoczi            err('invalid \"when\" option value \"%s\" in %s' % (when, name))
2191e8ece0dSStefan Hajnoczi    return Rule(name, event, io, when)
2201e8ece0dSStefan Hajnoczi
2211e8ece0dSStefan Hajnoczidef parse_config(config):
2221e8ece0dSStefan Hajnoczi    rules = []
2231e8ece0dSStefan Hajnoczi    for name in config.sections():
2241e8ece0dSStefan Hajnoczi        if name.startswith('inject-error'):
2251e8ece0dSStefan Hajnoczi            options = dict(config.items(name))
2261e8ece0dSStefan Hajnoczi            rules.append(parse_inject_error(name, options))
2271e8ece0dSStefan Hajnoczi        else:
2281e8ece0dSStefan Hajnoczi            err('invalid config section name: %s' % name)
2291e8ece0dSStefan Hajnoczi    return rules
2301e8ece0dSStefan Hajnoczi
2311e8ece0dSStefan Hajnoczidef load_rules(filename):
2322d894beeSMax Reitz    config = configparser.RawConfigParser()
2331e8ece0dSStefan Hajnoczi    with open(filename, 'rt') as f:
2341e8ece0dSStefan Hajnoczi        config.readfp(f, filename)
2351e8ece0dSStefan Hajnoczi    return parse_config(config)
2361e8ece0dSStefan Hajnoczi
2371e8ece0dSStefan Hajnoczidef open_socket(path):
2381e8ece0dSStefan Hajnoczi    '''Open a TCP or UNIX domain listen socket'''
2391e8ece0dSStefan Hajnoczi    if ':' in path:
2401e8ece0dSStefan Hajnoczi        host, port = path.split(':', 1)
2411e8ece0dSStefan Hajnoczi        sock = socket.socket()
2421e8ece0dSStefan Hajnoczi        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
2431e8ece0dSStefan Hajnoczi        sock.bind((host, int(port)))
2446e592fc9SStefan Hajnoczi
2456e592fc9SStefan Hajnoczi        # If given port was 0 the final port number is now available
2466e592fc9SStefan Hajnoczi        path = '%s:%d' % sock.getsockname()
2471e8ece0dSStefan Hajnoczi    else:
2481e8ece0dSStefan Hajnoczi        sock = socket.socket(socket.AF_UNIX)
2491e8ece0dSStefan Hajnoczi        sock.bind(path)
2501e8ece0dSStefan Hajnoczi    sock.listen(0)
251f03868bdSEduardo Habkost    print('Listening on %s' % path)
2526e592fc9SStefan Hajnoczi    sys.stdout.flush() # another process may be waiting, show message now
2531e8ece0dSStefan Hajnoczi    return sock
2541e8ece0dSStefan Hajnoczi
2551e8ece0dSStefan Hajnoczidef usage(args):
2561e8ece0dSStefan Hajnoczi    sys.stderr.write('usage: %s [--classic-negotiation] <tcp-port>|<unix-path> <config-file>\n' % args[0])
2571e8ece0dSStefan Hajnoczi    sys.stderr.write('Run an fault injector NBD server with rules defined in a config file.\n')
2581e8ece0dSStefan Hajnoczi    sys.exit(1)
2591e8ece0dSStefan Hajnoczi
2601e8ece0dSStefan Hajnoczidef main(args):
2611e8ece0dSStefan Hajnoczi    if len(args) != 3 and len(args) != 4:
2621e8ece0dSStefan Hajnoczi        usage(args)
2631e8ece0dSStefan Hajnoczi    use_export = True
2641e8ece0dSStefan Hajnoczi    if args[1] == '--classic-negotiation':
2651e8ece0dSStefan Hajnoczi        use_export = False
2661e8ece0dSStefan Hajnoczi    elif len(args) == 4:
2671e8ece0dSStefan Hajnoczi        usage(args)
2681e8ece0dSStefan Hajnoczi    sock = open_socket(args[1 if use_export else 2])
2691e8ece0dSStefan Hajnoczi    rules = load_rules(args[2 if use_export else 3])
2701e8ece0dSStefan Hajnoczi    run_server(sock, rules, use_export)
2711e8ece0dSStefan Hajnoczi    return 0
2721e8ece0dSStefan Hajnoczi
2731e8ece0dSStefan Hajnocziif __name__ == '__main__':
2741e8ece0dSStefan Hajnoczi    sys.exit(main(sys.argv))
275