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