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