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