xref: /qemu/tests/qemu-iotests/nbd-fault-injector.py (revision 1e8ece0db3e8604d3a17bbd2bd1277161851a44a)
1*1e8ece0dSStefan Hajnoczi#!/usr/bin/env python
2*1e8ece0dSStefan Hajnoczi# NBD server - fault injection utility
3*1e8ece0dSStefan Hajnoczi#
4*1e8ece0dSStefan Hajnoczi# Configuration file syntax:
5*1e8ece0dSStefan Hajnoczi#   [inject-error "disconnect-neg1"]
6*1e8ece0dSStefan Hajnoczi#   event=neg1
7*1e8ece0dSStefan Hajnoczi#   io=readwrite
8*1e8ece0dSStefan Hajnoczi#   when=before
9*1e8ece0dSStefan Hajnoczi#
10*1e8ece0dSStefan Hajnoczi# Note that Python's ConfigParser squashes together all sections with the same
11*1e8ece0dSStefan Hajnoczi# name, so give each [inject-error] a unique name.
12*1e8ece0dSStefan Hajnoczi#
13*1e8ece0dSStefan Hajnoczi# inject-error options:
14*1e8ece0dSStefan Hajnoczi#   event - name of the trigger event
15*1e8ece0dSStefan Hajnoczi#           "neg1" - first part of negotiation struct
16*1e8ece0dSStefan Hajnoczi#           "export" - export struct
17*1e8ece0dSStefan Hajnoczi#           "neg2" - second part of negotiation struct
18*1e8ece0dSStefan Hajnoczi#           "request" - NBD request struct
19*1e8ece0dSStefan Hajnoczi#           "reply" - NBD reply struct
20*1e8ece0dSStefan Hajnoczi#           "data" - request/reply data
21*1e8ece0dSStefan Hajnoczi#   io    - I/O direction that triggers this rule:
22*1e8ece0dSStefan Hajnoczi#           "read", "write", or "readwrite"
23*1e8ece0dSStefan Hajnoczi#           default: readwrite
24*1e8ece0dSStefan Hajnoczi#   when  - after how many bytes to inject the fault
25*1e8ece0dSStefan Hajnoczi#           -1 - inject error after I/O
26*1e8ece0dSStefan Hajnoczi#           0 - inject error before I/O
27*1e8ece0dSStefan Hajnoczi#           integer - inject error after integer bytes
28*1e8ece0dSStefan Hajnoczi#           "before" - alias for 0
29*1e8ece0dSStefan Hajnoczi#           "after" - alias for -1
30*1e8ece0dSStefan Hajnoczi#           default: before
31*1e8ece0dSStefan Hajnoczi#
32*1e8ece0dSStefan Hajnoczi# Currently the only error injection action is to terminate the server process.
33*1e8ece0dSStefan Hajnoczi# This resets the TCP connection and thus forces the client to handle
34*1e8ece0dSStefan Hajnoczi# unexpected connection termination.
35*1e8ece0dSStefan Hajnoczi#
36*1e8ece0dSStefan Hajnoczi# Other error injection actions could be added in the future.
37*1e8ece0dSStefan Hajnoczi#
38*1e8ece0dSStefan Hajnoczi# Copyright Red Hat, Inc. 2014
39*1e8ece0dSStefan Hajnoczi#
40*1e8ece0dSStefan Hajnoczi# Authors:
41*1e8ece0dSStefan Hajnoczi#   Stefan Hajnoczi <stefanha@redhat.com>
42*1e8ece0dSStefan Hajnoczi#
43*1e8ece0dSStefan Hajnoczi# This work is licensed under the terms of the GNU GPL, version 2 or later.
44*1e8ece0dSStefan Hajnoczi# See the COPYING file in the top-level directory.
45*1e8ece0dSStefan Hajnoczi
46*1e8ece0dSStefan Hajnocziimport sys
47*1e8ece0dSStefan Hajnocziimport socket
48*1e8ece0dSStefan Hajnocziimport struct
49*1e8ece0dSStefan Hajnocziimport collections
50*1e8ece0dSStefan Hajnocziimport ConfigParser
51*1e8ece0dSStefan Hajnoczi
52*1e8ece0dSStefan HajnocziFAKE_DISK_SIZE = 8 * 1024 * 1024 * 1024 # 8 GB
53*1e8ece0dSStefan Hajnoczi
54*1e8ece0dSStefan Hajnoczi# Protocol constants
55*1e8ece0dSStefan HajnocziNBD_CMD_READ = 0
56*1e8ece0dSStefan HajnocziNBD_CMD_WRITE = 1
57*1e8ece0dSStefan HajnocziNBD_CMD_DISC = 2
58*1e8ece0dSStefan HajnocziNBD_REQUEST_MAGIC = 0x25609513
59*1e8ece0dSStefan HajnocziNBD_REPLY_MAGIC = 0x67446698
60*1e8ece0dSStefan HajnocziNBD_PASSWD = 0x4e42444d41474943
61*1e8ece0dSStefan HajnocziNBD_OPTS_MAGIC = 0x49484156454F5054
62*1e8ece0dSStefan HajnocziNBD_CLIENT_MAGIC = 0x0000420281861253
63*1e8ece0dSStefan HajnocziNBD_OPT_EXPORT_NAME = 1 << 0
64*1e8ece0dSStefan Hajnoczi
65*1e8ece0dSStefan Hajnoczi# Protocol structs
66*1e8ece0dSStefan Hajnoczineg_classic_struct = struct.Struct('>QQQI124x')
67*1e8ece0dSStefan Hajnoczineg1_struct = struct.Struct('>QQH')
68*1e8ece0dSStefan Hajnocziexport_tuple = collections.namedtuple('Export', 'reserved magic opt len')
69*1e8ece0dSStefan Hajnocziexport_struct = struct.Struct('>IQII')
70*1e8ece0dSStefan Hajnoczineg2_struct = struct.Struct('>QH124x')
71*1e8ece0dSStefan Hajnoczirequest_tuple = collections.namedtuple('Request', 'magic type handle from_ len')
72*1e8ece0dSStefan Hajnoczirequest_struct = struct.Struct('>IIQQI')
73*1e8ece0dSStefan Hajnoczireply_struct = struct.Struct('>IIQ')
74*1e8ece0dSStefan Hajnoczi
75*1e8ece0dSStefan Hajnoczidef err(msg):
76*1e8ece0dSStefan Hajnoczi    sys.stderr.write(msg + '\n')
77*1e8ece0dSStefan Hajnoczi    sys.exit(1)
78*1e8ece0dSStefan Hajnoczi
79*1e8ece0dSStefan Hajnoczidef recvall(sock, bufsize):
80*1e8ece0dSStefan Hajnoczi    received = 0
81*1e8ece0dSStefan Hajnoczi    chunks = []
82*1e8ece0dSStefan Hajnoczi    while received < bufsize:
83*1e8ece0dSStefan Hajnoczi        chunk = sock.recv(bufsize - received)
84*1e8ece0dSStefan Hajnoczi        if len(chunk) == 0:
85*1e8ece0dSStefan Hajnoczi            raise Exception('unexpected disconnect')
86*1e8ece0dSStefan Hajnoczi        chunks.append(chunk)
87*1e8ece0dSStefan Hajnoczi        received += len(chunk)
88*1e8ece0dSStefan Hajnoczi    return ''.join(chunks)
89*1e8ece0dSStefan Hajnoczi
90*1e8ece0dSStefan Hajnocziclass Rule(object):
91*1e8ece0dSStefan Hajnoczi    def __init__(self, name, event, io, when):
92*1e8ece0dSStefan Hajnoczi        self.name = name
93*1e8ece0dSStefan Hajnoczi        self.event = event
94*1e8ece0dSStefan Hajnoczi        self.io = io
95*1e8ece0dSStefan Hajnoczi        self.when = when
96*1e8ece0dSStefan Hajnoczi
97*1e8ece0dSStefan Hajnoczi    def match(self, event, io):
98*1e8ece0dSStefan Hajnoczi        if event != self.event:
99*1e8ece0dSStefan Hajnoczi            return False
100*1e8ece0dSStefan Hajnoczi        if io != self.io and self.io != 'readwrite':
101*1e8ece0dSStefan Hajnoczi            return False
102*1e8ece0dSStefan Hajnoczi        return True
103*1e8ece0dSStefan Hajnoczi
104*1e8ece0dSStefan Hajnocziclass FaultInjectionSocket(object):
105*1e8ece0dSStefan Hajnoczi    def __init__(self, sock, rules):
106*1e8ece0dSStefan Hajnoczi        self.sock = sock
107*1e8ece0dSStefan Hajnoczi        self.rules = rules
108*1e8ece0dSStefan Hajnoczi
109*1e8ece0dSStefan Hajnoczi    def check(self, event, io, bufsize=None):
110*1e8ece0dSStefan Hajnoczi        for rule in self.rules:
111*1e8ece0dSStefan Hajnoczi            if rule.match(event, io):
112*1e8ece0dSStefan Hajnoczi                if rule.when == 0 or bufsize is None:
113*1e8ece0dSStefan Hajnoczi                    print 'Closing connection on rule match %s' % rule.name
114*1e8ece0dSStefan Hajnoczi                    sys.exit(0)
115*1e8ece0dSStefan Hajnoczi                if rule.when != -1:
116*1e8ece0dSStefan Hajnoczi                    return rule.when
117*1e8ece0dSStefan Hajnoczi        return bufsize
118*1e8ece0dSStefan Hajnoczi
119*1e8ece0dSStefan Hajnoczi    def send(self, buf, event):
120*1e8ece0dSStefan Hajnoczi        bufsize = self.check(event, 'write', bufsize=len(buf))
121*1e8ece0dSStefan Hajnoczi        self.sock.sendall(buf[:bufsize])
122*1e8ece0dSStefan Hajnoczi        self.check(event, 'write')
123*1e8ece0dSStefan Hajnoczi
124*1e8ece0dSStefan Hajnoczi    def recv(self, bufsize, event):
125*1e8ece0dSStefan Hajnoczi        bufsize = self.check(event, 'read', bufsize=bufsize)
126*1e8ece0dSStefan Hajnoczi        data = recvall(self.sock, bufsize)
127*1e8ece0dSStefan Hajnoczi        self.check(event, 'read')
128*1e8ece0dSStefan Hajnoczi        return data
129*1e8ece0dSStefan Hajnoczi
130*1e8ece0dSStefan Hajnoczi    def close(self):
131*1e8ece0dSStefan Hajnoczi        self.sock.close()
132*1e8ece0dSStefan Hajnoczi
133*1e8ece0dSStefan Hajnoczidef negotiate_classic(conn):
134*1e8ece0dSStefan Hajnoczi    buf = neg_classic_struct.pack(NBD_PASSWD, NBD_CLIENT_MAGIC,
135*1e8ece0dSStefan Hajnoczi                                  FAKE_DISK_SIZE, 0)
136*1e8ece0dSStefan Hajnoczi    conn.send(buf, event='neg-classic')
137*1e8ece0dSStefan Hajnoczi
138*1e8ece0dSStefan Hajnoczidef negotiate_export(conn):
139*1e8ece0dSStefan Hajnoczi    # Send negotiation part 1
140*1e8ece0dSStefan Hajnoczi    buf = neg1_struct.pack(NBD_PASSWD, NBD_OPTS_MAGIC, 0)
141*1e8ece0dSStefan Hajnoczi    conn.send(buf, event='neg1')
142*1e8ece0dSStefan Hajnoczi
143*1e8ece0dSStefan Hajnoczi    # Receive export option
144*1e8ece0dSStefan Hajnoczi    buf = conn.recv(export_struct.size, event='export')
145*1e8ece0dSStefan Hajnoczi    export = export_tuple._make(export_struct.unpack(buf))
146*1e8ece0dSStefan Hajnoczi    assert export.magic == NBD_OPTS_MAGIC
147*1e8ece0dSStefan Hajnoczi    assert export.opt == NBD_OPT_EXPORT_NAME
148*1e8ece0dSStefan Hajnoczi    name = conn.recv(export.len, event='export-name')
149*1e8ece0dSStefan Hajnoczi
150*1e8ece0dSStefan Hajnoczi    # Send negotiation part 2
151*1e8ece0dSStefan Hajnoczi    buf = neg2_struct.pack(FAKE_DISK_SIZE, 0)
152*1e8ece0dSStefan Hajnoczi    conn.send(buf, event='neg2')
153*1e8ece0dSStefan Hajnoczi
154*1e8ece0dSStefan Hajnoczidef negotiate(conn, use_export):
155*1e8ece0dSStefan Hajnoczi    '''Negotiate export with client'''
156*1e8ece0dSStefan Hajnoczi    if use_export:
157*1e8ece0dSStefan Hajnoczi        negotiate_export(conn)
158*1e8ece0dSStefan Hajnoczi    else:
159*1e8ece0dSStefan Hajnoczi        negotiate_classic(conn)
160*1e8ece0dSStefan Hajnoczi
161*1e8ece0dSStefan Hajnoczidef read_request(conn):
162*1e8ece0dSStefan Hajnoczi    '''Parse NBD request from client'''
163*1e8ece0dSStefan Hajnoczi    buf = conn.recv(request_struct.size, event='request')
164*1e8ece0dSStefan Hajnoczi    req = request_tuple._make(request_struct.unpack(buf))
165*1e8ece0dSStefan Hajnoczi    assert req.magic == NBD_REQUEST_MAGIC
166*1e8ece0dSStefan Hajnoczi    return req
167*1e8ece0dSStefan Hajnoczi
168*1e8ece0dSStefan Hajnoczidef write_reply(conn, error, handle):
169*1e8ece0dSStefan Hajnoczi    buf = reply_struct.pack(NBD_REPLY_MAGIC, error, handle)
170*1e8ece0dSStefan Hajnoczi    conn.send(buf, event='reply')
171*1e8ece0dSStefan Hajnoczi
172*1e8ece0dSStefan Hajnoczidef handle_connection(conn, use_export):
173*1e8ece0dSStefan Hajnoczi    negotiate(conn, use_export)
174*1e8ece0dSStefan Hajnoczi    while True:
175*1e8ece0dSStefan Hajnoczi        req = read_request(conn)
176*1e8ece0dSStefan Hajnoczi        if req.type == NBD_CMD_READ:
177*1e8ece0dSStefan Hajnoczi            write_reply(conn, 0, req.handle)
178*1e8ece0dSStefan Hajnoczi            conn.send('\0' * req.len, event='data')
179*1e8ece0dSStefan Hajnoczi        elif req.type == NBD_CMD_WRITE:
180*1e8ece0dSStefan Hajnoczi            _ = conn.recv(req.len, event='data')
181*1e8ece0dSStefan Hajnoczi            write_reply(conn, 0, req.handle)
182*1e8ece0dSStefan Hajnoczi        elif req.type == NBD_CMD_DISC:
183*1e8ece0dSStefan Hajnoczi            break
184*1e8ece0dSStefan Hajnoczi        else:
185*1e8ece0dSStefan Hajnoczi            print 'unrecognized command type %#02x' % req.type
186*1e8ece0dSStefan Hajnoczi            break
187*1e8ece0dSStefan Hajnoczi    conn.close()
188*1e8ece0dSStefan Hajnoczi
189*1e8ece0dSStefan Hajnoczidef run_server(sock, rules, use_export):
190*1e8ece0dSStefan Hajnoczi    while True:
191*1e8ece0dSStefan Hajnoczi        conn, _ = sock.accept()
192*1e8ece0dSStefan Hajnoczi        handle_connection(FaultInjectionSocket(conn, rules), use_export)
193*1e8ece0dSStefan Hajnoczi
194*1e8ece0dSStefan Hajnoczidef parse_inject_error(name, options):
195*1e8ece0dSStefan Hajnoczi    if 'event' not in options:
196*1e8ece0dSStefan Hajnoczi        err('missing \"event\" option in %s' % name)
197*1e8ece0dSStefan Hajnoczi    event = options['event']
198*1e8ece0dSStefan Hajnoczi    if event not in ('neg-classic', 'neg1', 'export', 'neg2', 'request', 'reply', 'data'):
199*1e8ece0dSStefan Hajnoczi        err('invalid \"event\" option value \"%s\" in %s' % (event, name))
200*1e8ece0dSStefan Hajnoczi    io = options.get('io', 'readwrite')
201*1e8ece0dSStefan Hajnoczi    if io not in ('read', 'write', 'readwrite'):
202*1e8ece0dSStefan Hajnoczi        err('invalid \"io\" option value \"%s\" in %s' % (io, name))
203*1e8ece0dSStefan Hajnoczi    when = options.get('when', 'before')
204*1e8ece0dSStefan Hajnoczi    try:
205*1e8ece0dSStefan Hajnoczi        when = int(when)
206*1e8ece0dSStefan Hajnoczi    except ValueError:
207*1e8ece0dSStefan Hajnoczi        if when == 'before':
208*1e8ece0dSStefan Hajnoczi            when = 0
209*1e8ece0dSStefan Hajnoczi        elif when == 'after':
210*1e8ece0dSStefan Hajnoczi            when = -1
211*1e8ece0dSStefan Hajnoczi        else:
212*1e8ece0dSStefan Hajnoczi            err('invalid \"when\" option value \"%s\" in %s' % (when, name))
213*1e8ece0dSStefan Hajnoczi    return Rule(name, event, io, when)
214*1e8ece0dSStefan Hajnoczi
215*1e8ece0dSStefan Hajnoczidef parse_config(config):
216*1e8ece0dSStefan Hajnoczi    rules = []
217*1e8ece0dSStefan Hajnoczi    for name in config.sections():
218*1e8ece0dSStefan Hajnoczi        if name.startswith('inject-error'):
219*1e8ece0dSStefan Hajnoczi            options = dict(config.items(name))
220*1e8ece0dSStefan Hajnoczi            rules.append(parse_inject_error(name, options))
221*1e8ece0dSStefan Hajnoczi        else:
222*1e8ece0dSStefan Hajnoczi            err('invalid config section name: %s' % name)
223*1e8ece0dSStefan Hajnoczi    return rules
224*1e8ece0dSStefan Hajnoczi
225*1e8ece0dSStefan Hajnoczidef load_rules(filename):
226*1e8ece0dSStefan Hajnoczi    config = ConfigParser.RawConfigParser()
227*1e8ece0dSStefan Hajnoczi    with open(filename, 'rt') as f:
228*1e8ece0dSStefan Hajnoczi        config.readfp(f, filename)
229*1e8ece0dSStefan Hajnoczi    return parse_config(config)
230*1e8ece0dSStefan Hajnoczi
231*1e8ece0dSStefan Hajnoczidef open_socket(path):
232*1e8ece0dSStefan Hajnoczi    '''Open a TCP or UNIX domain listen socket'''
233*1e8ece0dSStefan Hajnoczi    if ':' in path:
234*1e8ece0dSStefan Hajnoczi        host, port = path.split(':', 1)
235*1e8ece0dSStefan Hajnoczi        sock = socket.socket()
236*1e8ece0dSStefan Hajnoczi        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
237*1e8ece0dSStefan Hajnoczi        sock.bind((host, int(port)))
238*1e8ece0dSStefan Hajnoczi    else:
239*1e8ece0dSStefan Hajnoczi        sock = socket.socket(socket.AF_UNIX)
240*1e8ece0dSStefan Hajnoczi        sock.bind(path)
241*1e8ece0dSStefan Hajnoczi    sock.listen(0)
242*1e8ece0dSStefan Hajnoczi    print 'Listening on %s' % path
243*1e8ece0dSStefan Hajnoczi    return sock
244*1e8ece0dSStefan Hajnoczi
245*1e8ece0dSStefan Hajnoczidef usage(args):
246*1e8ece0dSStefan Hajnoczi    sys.stderr.write('usage: %s [--classic-negotiation] <tcp-port>|<unix-path> <config-file>\n' % args[0])
247*1e8ece0dSStefan Hajnoczi    sys.stderr.write('Run an fault injector NBD server with rules defined in a config file.\n')
248*1e8ece0dSStefan Hajnoczi    sys.exit(1)
249*1e8ece0dSStefan Hajnoczi
250*1e8ece0dSStefan Hajnoczidef main(args):
251*1e8ece0dSStefan Hajnoczi    if len(args) != 3 and len(args) != 4:
252*1e8ece0dSStefan Hajnoczi        usage(args)
253*1e8ece0dSStefan Hajnoczi    use_export = True
254*1e8ece0dSStefan Hajnoczi    if args[1] == '--classic-negotiation':
255*1e8ece0dSStefan Hajnoczi        use_export = False
256*1e8ece0dSStefan Hajnoczi    elif len(args) == 4:
257*1e8ece0dSStefan Hajnoczi        usage(args)
258*1e8ece0dSStefan Hajnoczi    sock = open_socket(args[1 if use_export else 2])
259*1e8ece0dSStefan Hajnoczi    rules = load_rules(args[2 if use_export else 3])
260*1e8ece0dSStefan Hajnoczi    run_server(sock, rules, use_export)
261*1e8ece0dSStefan Hajnoczi    return 0
262*1e8ece0dSStefan Hajnoczi
263*1e8ece0dSStefan Hajnocziif __name__ == '__main__':
264*1e8ece0dSStefan Hajnoczi    sys.exit(main(sys.argv))
265