2 # NBD server - fault injection utility
4 # Configuration file syntax:
5 # [inject-error "disconnect-neg1"]
10 # Note that Python's ConfigParser squashes together all sections with the same
11 # name, so give each [inject-error] a unique name.
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"
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
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.
36 # Other error injection actions could be added in the future.
38 # Copyright Red Hat, Inc. 2014
41 # Stefan Hajnoczi <stefanha@redhat.com>
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.
46 from __future__
import print_function
51 if sys
.version_info
.major
>= 3:
54 import ConfigParser
as configparser
56 FAKE_DISK_SIZE
= 8 * 1024 * 1024 * 1024 # 8 GB
62 NBD_REQUEST_MAGIC
= 0x25609513
63 NBD_SIMPLE_REPLY_MAGIC
= 0x67446698
64 NBD_PASSWD
= 0x4e42444d41474943
65 NBD_OPTS_MAGIC
= 0x49484156454F5054
66 NBD_CLIENT_MAGIC
= 0x0000420281861253
67 NBD_OPT_EXPORT_NAME
= 1 << 0
70 neg_classic_struct
= struct
.Struct('>QQQI124x')
71 neg1_struct
= struct
.Struct('>QQH')
72 export_tuple
= collections
.namedtuple('Export', 'reserved magic opt len')
73 export_struct
= struct
.Struct('>IQII')
74 neg2_struct
= struct
.Struct('>QH124x')
75 request_tuple
= collections
.namedtuple('Request', 'magic type handle from_ len')
76 request_struct
= struct
.Struct('>IIQQI')
77 reply_struct
= struct
.Struct('>IIQ')
80 sys
.stderr
.write(msg
+ '\n')
83 def recvall(sock
, bufsize
):
86 while received
< bufsize
:
87 chunk
= sock
.recv(bufsize
- received
)
89 raise Exception('unexpected disconnect')
91 received
+= len(chunk
)
92 return b
''.join(chunks
)
95 def __init__(self
, name
, event
, io
, when
):
101 def match(self
, event
, io
):
102 if event
!= self
.event
:
104 if io
!= self
.io
and self
.io
!= 'readwrite':
108 class FaultInjectionSocket(object):
109 def __init__(self
, sock
, rules
):
113 def check(self
, event
, io
, bufsize
=None):
114 for rule
in self
.rules
:
115 if rule
.match(event
, io
):
116 if rule
.when
== 0 or bufsize
is None:
117 print('Closing connection on rule match %s' % rule
.name
)
124 def send(self
, buf
, event
):
125 bufsize
= self
.check(event
, 'write', bufsize
=len(buf
))
126 self
.sock
.sendall(buf
[:bufsize
])
127 self
.check(event
, 'write')
129 def recv(self
, bufsize
, event
):
130 bufsize
= self
.check(event
, 'read', bufsize
=bufsize
)
131 data
= recvall(self
.sock
, bufsize
)
132 self
.check(event
, 'read')
138 def negotiate_classic(conn
):
139 buf
= neg_classic_struct
.pack(NBD_PASSWD
, NBD_CLIENT_MAGIC
,
141 conn
.send(buf
, event
='neg-classic')
143 def negotiate_export(conn
):
144 # Send negotiation part 1
145 buf
= neg1_struct
.pack(NBD_PASSWD
, NBD_OPTS_MAGIC
, 0)
146 conn
.send(buf
, event
='neg1')
148 # Receive export option
149 buf
= conn
.recv(export_struct
.size
, event
='export')
150 export
= export_tuple
._make
(export_struct
.unpack(buf
))
151 assert export
.magic
== NBD_OPTS_MAGIC
152 assert export
.opt
== NBD_OPT_EXPORT_NAME
153 name
= conn
.recv(export
.len, event
='export-name')
155 # Send negotiation part 2
156 buf
= neg2_struct
.pack(FAKE_DISK_SIZE
, 0)
157 conn
.send(buf
, event
='neg2')
159 def negotiate(conn
, use_export
):
160 '''Negotiate export with client'''
162 negotiate_export(conn
)
164 negotiate_classic(conn
)
166 def read_request(conn
):
167 '''Parse NBD request from client'''
168 buf
= conn
.recv(request_struct
.size
, event
='request')
169 req
= request_tuple
._make
(request_struct
.unpack(buf
))
170 assert req
.magic
== NBD_REQUEST_MAGIC
173 def write_reply(conn
, error
, handle
):
174 buf
= reply_struct
.pack(NBD_SIMPLE_REPLY_MAGIC
, error
, handle
)
175 conn
.send(buf
, event
='reply')
177 def handle_connection(conn
, use_export
):
178 negotiate(conn
, use_export
)
180 req
= read_request(conn
)
181 if req
.type == NBD_CMD_READ
:
182 write_reply(conn
, 0, req
.handle
)
183 conn
.send(b
'\0' * req
.len, event
='data')
184 elif req
.type == NBD_CMD_WRITE
:
185 _
= conn
.recv(req
.len, event
='data')
186 write_reply(conn
, 0, req
.handle
)
187 elif req
.type == NBD_CMD_DISC
:
190 print('unrecognized command type %#02x' % req
.type)
194 def run_server(sock
, rules
, use_export
):
196 conn
, _
= sock
.accept()
197 handle_connection(FaultInjectionSocket(conn
, rules
), use_export
)
199 def parse_inject_error(name
, options
):
200 if 'event' not in options
:
201 err('missing \"event\" option in %s' % name
)
202 event
= options
['event']
203 if event
not in ('neg-classic', 'neg1', 'export', 'neg2', 'request', 'reply', 'data'):
204 err('invalid \"event\" option value \"%s\" in %s' % (event
, name
))
205 io
= options
.get('io', 'readwrite')
206 if io
not in ('read', 'write', 'readwrite'):
207 err('invalid \"io\" option value \"%s\" in %s' % (io
, name
))
208 when
= options
.get('when', 'before')
214 elif when
== 'after':
217 err('invalid \"when\" option value \"%s\" in %s' % (when
, name
))
218 return Rule(name
, event
, io
, when
)
220 def parse_config(config
):
222 for name
in config
.sections():
223 if name
.startswith('inject-error'):
224 options
= dict(config
.items(name
))
225 rules
.append(parse_inject_error(name
, options
))
227 err('invalid config section name: %s' % name
)
230 def load_rules(filename
):
231 config
= configparser
.RawConfigParser()
232 with
open(filename
, 'rt') as f
:
233 config
.readfp(f
, filename
)
234 return parse_config(config
)
236 def open_socket(path
):
237 '''Open a TCP or UNIX domain listen socket'''
239 host
, port
= path
.split(':', 1)
240 sock
= socket
.socket()
241 sock
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEADDR
, 1)
242 sock
.bind((host
, int(port
)))
244 # If given port was 0 the final port number is now available
245 path
= '%s:%d' % sock
.getsockname()
247 sock
= socket
.socket(socket
.AF_UNIX
)
250 print('Listening on %s' % path
)
251 sys
.stdout
.flush() # another process may be waiting, show message now
255 sys
.stderr
.write('usage: %s [--classic-negotiation] <tcp-port>|<unix-path> <config-file>\n' % args
[0])
256 sys
.stderr
.write('Run an fault injector NBD server with rules defined in a config file.\n')
260 if len(args
) != 3 and len(args
) != 4:
263 if args
[1] == '--classic-negotiation':
267 sock
= open_socket(args
[1 if use_export
else 2])
268 rules
= load_rules(args
[2 if use_export
else 3])
269 run_server(sock
, rules
, use_export
)
272 if __name__
== '__main__':
273 sys
.exit(main(sys
.argv
))