1903cb1bfSPhilippe 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 461e8ece0dSStefan Hajnocziimport sys 471e8ece0dSStefan Hajnocziimport socket 481e8ece0dSStefan Hajnocziimport struct 491e8ece0dSStefan Hajnocziimport collections 502d894beeSMax Reitzimport configparser 511e8ece0dSStefan Hajnoczi 521e8ece0dSStefan HajnocziFAKE_DISK_SIZE = 8 * 1024 * 1024 * 1024 # 8 GB 531e8ece0dSStefan Hajnoczi 541e8ece0dSStefan Hajnoczi# Protocol constants 551e8ece0dSStefan HajnocziNBD_CMD_READ = 0 561e8ece0dSStefan HajnocziNBD_CMD_WRITE = 1 571e8ece0dSStefan HajnocziNBD_CMD_DISC = 2 581e8ece0dSStefan HajnocziNBD_REQUEST_MAGIC = 0x25609513 597b3158f9SVladimir Sementsov-OgievskiyNBD_SIMPLE_REPLY_MAGIC = 0x67446698 601e8ece0dSStefan HajnocziNBD_PASSWD = 0x4e42444d41474943 611e8ece0dSStefan HajnocziNBD_OPTS_MAGIC = 0x49484156454F5054 621e8ece0dSStefan HajnocziNBD_CLIENT_MAGIC = 0x0000420281861253 631e8ece0dSStefan HajnocziNBD_OPT_EXPORT_NAME = 1 << 0 641e8ece0dSStefan Hajnoczi 651e8ece0dSStefan Hajnoczi# Protocol structs 661e8ece0dSStefan Hajnoczineg_classic_struct = struct.Struct('>QQQI124x') 671e8ece0dSStefan Hajnoczineg1_struct = struct.Struct('>QQH') 681e8ece0dSStefan Hajnocziexport_tuple = collections.namedtuple('Export', 'reserved magic opt len') 691e8ece0dSStefan Hajnocziexport_struct = struct.Struct('>IQII') 701e8ece0dSStefan Hajnoczineg2_struct = struct.Struct('>QH124x') 711e8ece0dSStefan Hajnoczirequest_tuple = collections.namedtuple('Request', 'magic type handle from_ len') 721e8ece0dSStefan Hajnoczirequest_struct = struct.Struct('>IIQQI') 731e8ece0dSStefan Hajnoczireply_struct = struct.Struct('>IIQ') 741e8ece0dSStefan Hajnoczi 751e8ece0dSStefan Hajnoczidef err(msg): 761e8ece0dSStefan Hajnoczi sys.stderr.write(msg + '\n') 771e8ece0dSStefan Hajnoczi sys.exit(1) 781e8ece0dSStefan Hajnoczi 791e8ece0dSStefan Hajnoczidef recvall(sock, bufsize): 801e8ece0dSStefan Hajnoczi received = 0 811e8ece0dSStefan Hajnoczi chunks = [] 821e8ece0dSStefan Hajnoczi while received < bufsize: 831e8ece0dSStefan Hajnoczi chunk = sock.recv(bufsize - received) 841e8ece0dSStefan Hajnoczi if len(chunk) == 0: 851e8ece0dSStefan Hajnoczi raise Exception('unexpected disconnect') 861e8ece0dSStefan Hajnoczi chunks.append(chunk) 871e8ece0dSStefan Hajnoczi received += len(chunk) 888eb5e674SMax Reitz return b''.join(chunks) 891e8ece0dSStefan Hajnoczi 901e8ece0dSStefan Hajnocziclass Rule(object): 911e8ece0dSStefan Hajnoczi def __init__(self, name, event, io, when): 921e8ece0dSStefan Hajnoczi self.name = name 931e8ece0dSStefan Hajnoczi self.event = event 941e8ece0dSStefan Hajnoczi self.io = io 951e8ece0dSStefan Hajnoczi self.when = when 961e8ece0dSStefan Hajnoczi 971e8ece0dSStefan Hajnoczi def match(self, event, io): 981e8ece0dSStefan Hajnoczi if event != self.event: 991e8ece0dSStefan Hajnoczi return False 1001e8ece0dSStefan Hajnoczi if io != self.io and self.io != 'readwrite': 1011e8ece0dSStefan Hajnoczi return False 1021e8ece0dSStefan Hajnoczi return True 1031e8ece0dSStefan Hajnoczi 1041e8ece0dSStefan Hajnocziclass FaultInjectionSocket(object): 1051e8ece0dSStefan Hajnoczi def __init__(self, sock, rules): 1061e8ece0dSStefan Hajnoczi self.sock = sock 1071e8ece0dSStefan Hajnoczi self.rules = rules 1081e8ece0dSStefan Hajnoczi 1091e8ece0dSStefan Hajnoczi def check(self, event, io, bufsize=None): 1101e8ece0dSStefan Hajnoczi for rule in self.rules: 1111e8ece0dSStefan Hajnoczi if rule.match(event, io): 1121e8ece0dSStefan Hajnoczi if rule.when == 0 or bufsize is None: 113f03868bdSEduardo Habkost print('Closing connection on rule match %s' % rule.name) 114a4d925f8SAndrey Shinkevich self.sock.close() 115a4d925f8SAndrey Shinkevich sys.stdout.flush() 1161e8ece0dSStefan Hajnoczi sys.exit(0) 1171e8ece0dSStefan Hajnoczi if rule.when != -1: 1181e8ece0dSStefan Hajnoczi return rule.when 1191e8ece0dSStefan Hajnoczi return bufsize 1201e8ece0dSStefan Hajnoczi 1211e8ece0dSStefan Hajnoczi def send(self, buf, event): 1221e8ece0dSStefan Hajnoczi bufsize = self.check(event, 'write', bufsize=len(buf)) 1231e8ece0dSStefan Hajnoczi self.sock.sendall(buf[:bufsize]) 1241e8ece0dSStefan Hajnoczi self.check(event, 'write') 1251e8ece0dSStefan Hajnoczi 1261e8ece0dSStefan Hajnoczi def recv(self, bufsize, event): 1271e8ece0dSStefan Hajnoczi bufsize = self.check(event, 'read', bufsize=bufsize) 1281e8ece0dSStefan Hajnoczi data = recvall(self.sock, bufsize) 1291e8ece0dSStefan Hajnoczi self.check(event, 'read') 1301e8ece0dSStefan Hajnoczi return data 1311e8ece0dSStefan Hajnoczi 1321e8ece0dSStefan Hajnoczi def close(self): 1331e8ece0dSStefan Hajnoczi self.sock.close() 1341e8ece0dSStefan Hajnoczi 1351e8ece0dSStefan Hajnoczidef negotiate_classic(conn): 1361e8ece0dSStefan Hajnoczi buf = neg_classic_struct.pack(NBD_PASSWD, NBD_CLIENT_MAGIC, 1371e8ece0dSStefan Hajnoczi FAKE_DISK_SIZE, 0) 1381e8ece0dSStefan Hajnoczi conn.send(buf, event='neg-classic') 1391e8ece0dSStefan Hajnoczi 1401e8ece0dSStefan Hajnoczidef negotiate_export(conn): 1411e8ece0dSStefan Hajnoczi # Send negotiation part 1 1421e8ece0dSStefan Hajnoczi buf = neg1_struct.pack(NBD_PASSWD, NBD_OPTS_MAGIC, 0) 1431e8ece0dSStefan Hajnoczi conn.send(buf, event='neg1') 1441e8ece0dSStefan Hajnoczi 1451e8ece0dSStefan Hajnoczi # Receive export option 1461e8ece0dSStefan Hajnoczi buf = conn.recv(export_struct.size, event='export') 1471e8ece0dSStefan Hajnoczi export = export_tuple._make(export_struct.unpack(buf)) 1481e8ece0dSStefan Hajnoczi assert export.magic == NBD_OPTS_MAGIC 1491e8ece0dSStefan Hajnoczi assert export.opt == NBD_OPT_EXPORT_NAME 1501e8ece0dSStefan Hajnoczi name = conn.recv(export.len, event='export-name') 1511e8ece0dSStefan Hajnoczi 1521e8ece0dSStefan Hajnoczi # Send negotiation part 2 1531e8ece0dSStefan Hajnoczi buf = neg2_struct.pack(FAKE_DISK_SIZE, 0) 1541e8ece0dSStefan Hajnoczi conn.send(buf, event='neg2') 1551e8ece0dSStefan Hajnoczi 1561e8ece0dSStefan Hajnoczidef negotiate(conn, use_export): 1571e8ece0dSStefan Hajnoczi '''Negotiate export with client''' 1581e8ece0dSStefan Hajnoczi if use_export: 1591e8ece0dSStefan Hajnoczi negotiate_export(conn) 1601e8ece0dSStefan Hajnoczi else: 1611e8ece0dSStefan Hajnoczi negotiate_classic(conn) 1621e8ece0dSStefan Hajnoczi 1631e8ece0dSStefan Hajnoczidef read_request(conn): 1641e8ece0dSStefan Hajnoczi '''Parse NBD request from client''' 1651e8ece0dSStefan Hajnoczi buf = conn.recv(request_struct.size, event='request') 1661e8ece0dSStefan Hajnoczi req = request_tuple._make(request_struct.unpack(buf)) 1671e8ece0dSStefan Hajnoczi assert req.magic == NBD_REQUEST_MAGIC 1681e8ece0dSStefan Hajnoczi return req 1691e8ece0dSStefan Hajnoczi 1701e8ece0dSStefan Hajnoczidef write_reply(conn, error, handle): 1717b3158f9SVladimir Sementsov-Ogievskiy buf = reply_struct.pack(NBD_SIMPLE_REPLY_MAGIC, error, handle) 1721e8ece0dSStefan Hajnoczi conn.send(buf, event='reply') 1731e8ece0dSStefan Hajnoczi 1741e8ece0dSStefan Hajnoczidef handle_connection(conn, use_export): 1751e8ece0dSStefan Hajnoczi negotiate(conn, use_export) 1761e8ece0dSStefan Hajnoczi while True: 1771e8ece0dSStefan Hajnoczi req = read_request(conn) 1781e8ece0dSStefan Hajnoczi if req.type == NBD_CMD_READ: 1791e8ece0dSStefan Hajnoczi write_reply(conn, 0, req.handle) 1808eb5e674SMax Reitz conn.send(b'\0' * req.len, event='data') 1811e8ece0dSStefan Hajnoczi elif req.type == NBD_CMD_WRITE: 1821e8ece0dSStefan Hajnoczi _ = conn.recv(req.len, event='data') 1831e8ece0dSStefan Hajnoczi write_reply(conn, 0, req.handle) 1841e8ece0dSStefan Hajnoczi elif req.type == NBD_CMD_DISC: 1851e8ece0dSStefan Hajnoczi break 1861e8ece0dSStefan Hajnoczi else: 187f03868bdSEduardo Habkost print('unrecognized command type %#02x' % req.type) 1881e8ece0dSStefan Hajnoczi break 1891e8ece0dSStefan Hajnoczi conn.close() 1901e8ece0dSStefan Hajnoczi 1911e8ece0dSStefan Hajnoczidef run_server(sock, rules, use_export): 1921e8ece0dSStefan Hajnoczi while True: 1931e8ece0dSStefan Hajnoczi conn, _ = sock.accept() 1941e8ece0dSStefan Hajnoczi handle_connection(FaultInjectionSocket(conn, rules), use_export) 1951e8ece0dSStefan Hajnoczi 1961e8ece0dSStefan Hajnoczidef parse_inject_error(name, options): 1971e8ece0dSStefan Hajnoczi if 'event' not in options: 1981e8ece0dSStefan Hajnoczi err('missing \"event\" option in %s' % name) 1991e8ece0dSStefan Hajnoczi event = options['event'] 2001e8ece0dSStefan Hajnoczi if event not in ('neg-classic', 'neg1', 'export', 'neg2', 'request', 'reply', 'data'): 2011e8ece0dSStefan Hajnoczi err('invalid \"event\" option value \"%s\" in %s' % (event, name)) 2021e8ece0dSStefan Hajnoczi io = options.get('io', 'readwrite') 2031e8ece0dSStefan Hajnoczi if io not in ('read', 'write', 'readwrite'): 2041e8ece0dSStefan Hajnoczi err('invalid \"io\" option value \"%s\" in %s' % (io, name)) 2051e8ece0dSStefan Hajnoczi when = options.get('when', 'before') 2061e8ece0dSStefan Hajnoczi try: 2071e8ece0dSStefan Hajnoczi when = int(when) 2081e8ece0dSStefan Hajnoczi except ValueError: 2091e8ece0dSStefan Hajnoczi if when == 'before': 2101e8ece0dSStefan Hajnoczi when = 0 2111e8ece0dSStefan Hajnoczi elif when == 'after': 2121e8ece0dSStefan Hajnoczi when = -1 2131e8ece0dSStefan Hajnoczi else: 2141e8ece0dSStefan Hajnoczi err('invalid \"when\" option value \"%s\" in %s' % (when, name)) 2151e8ece0dSStefan Hajnoczi return Rule(name, event, io, when) 2161e8ece0dSStefan Hajnoczi 2171e8ece0dSStefan Hajnoczidef parse_config(config): 2181e8ece0dSStefan Hajnoczi rules = [] 2191e8ece0dSStefan Hajnoczi for name in config.sections(): 2201e8ece0dSStefan Hajnoczi if name.startswith('inject-error'): 2211e8ece0dSStefan Hajnoczi options = dict(config.items(name)) 2221e8ece0dSStefan Hajnoczi rules.append(parse_inject_error(name, options)) 2231e8ece0dSStefan Hajnoczi else: 2241e8ece0dSStefan Hajnoczi err('invalid config section name: %s' % name) 2251e8ece0dSStefan Hajnoczi return rules 2261e8ece0dSStefan Hajnoczi 2271e8ece0dSStefan Hajnoczidef load_rules(filename): 2282d894beeSMax Reitz config = configparser.RawConfigParser() 2291e8ece0dSStefan Hajnoczi with open(filename, 'rt') as f: 230*5aaabf91SKevin Wolf config.read_file(f, filename) 2311e8ece0dSStefan Hajnoczi return parse_config(config) 2321e8ece0dSStefan Hajnoczi 2331e8ece0dSStefan Hajnoczidef open_socket(path): 2341e8ece0dSStefan Hajnoczi '''Open a TCP or UNIX domain listen socket''' 2351e8ece0dSStefan Hajnoczi if ':' in path: 2361e8ece0dSStefan Hajnoczi host, port = path.split(':', 1) 2371e8ece0dSStefan Hajnoczi sock = socket.socket() 2381e8ece0dSStefan Hajnoczi sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 2391e8ece0dSStefan Hajnoczi sock.bind((host, int(port))) 2406e592fc9SStefan Hajnoczi 2416e592fc9SStefan Hajnoczi # If given port was 0 the final port number is now available 2426e592fc9SStefan Hajnoczi path = '%s:%d' % sock.getsockname() 2431e8ece0dSStefan Hajnoczi else: 2441e8ece0dSStefan Hajnoczi sock = socket.socket(socket.AF_UNIX) 2451e8ece0dSStefan Hajnoczi sock.bind(path) 2461e8ece0dSStefan Hajnoczi sock.listen(0) 247f03868bdSEduardo Habkost print('Listening on %s' % path) 2486e592fc9SStefan Hajnoczi sys.stdout.flush() # another process may be waiting, show message now 2491e8ece0dSStefan Hajnoczi return sock 2501e8ece0dSStefan Hajnoczi 2511e8ece0dSStefan Hajnoczidef usage(args): 2521e8ece0dSStefan Hajnoczi sys.stderr.write('usage: %s [--classic-negotiation] <tcp-port>|<unix-path> <config-file>\n' % args[0]) 2531e8ece0dSStefan Hajnoczi sys.stderr.write('Run an fault injector NBD server with rules defined in a config file.\n') 2541e8ece0dSStefan Hajnoczi sys.exit(1) 2551e8ece0dSStefan Hajnoczi 2561e8ece0dSStefan Hajnoczidef main(args): 2571e8ece0dSStefan Hajnoczi if len(args) != 3 and len(args) != 4: 2581e8ece0dSStefan Hajnoczi usage(args) 2591e8ece0dSStefan Hajnoczi use_export = True 2601e8ece0dSStefan Hajnoczi if args[1] == '--classic-negotiation': 2611e8ece0dSStefan Hajnoczi use_export = False 2621e8ece0dSStefan Hajnoczi elif len(args) == 4: 2631e8ece0dSStefan Hajnoczi usage(args) 2641e8ece0dSStefan Hajnoczi sock = open_socket(args[1 if use_export else 2]) 2651e8ece0dSStefan Hajnoczi rules = load_rules(args[2 if use_export else 3]) 2661e8ece0dSStefan Hajnoczi run_server(sock, rules, use_export) 2671e8ece0dSStefan Hajnoczi return 0 2681e8ece0dSStefan Hajnoczi 2691e8ece0dSStefan Hajnocziif __name__ == '__main__': 2701e8ece0dSStefan Hajnoczi sys.exit(main(sys.argv)) 271