summaryrefslogtreecommitdiff
path: root/tests/qemu-iotests/nbd-fault-injector.py
blob: 78f42c421432bcc0cd10173697fd07b2a17b8aaa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
#!/usr/bin/env python3
# NBD server - fault injection utility
#
# Configuration file syntax:
#   [inject-error "disconnect-neg1"]
#   event=neg1
#   io=readwrite
#   when=before
#
# Note that Python's ConfigParser squashes together all sections with the same
# name, so give each [inject-error] a unique name.
#
# inject-error options:
#   event - name of the trigger event
#           "neg1" - first part of negotiation struct
#           "export" - export struct
#           "neg2" - second part of negotiation struct
#           "request" - NBD request struct
#           "reply" - NBD reply struct
#           "data" - request/reply data
#   io    - I/O direction that triggers this rule:
#           "read", "write", or "readwrite"
#           default: readwrite
#   when  - after how many bytes to inject the fault
#           -1 - inject error after I/O
#           0 - inject error before I/O
#           integer - inject error after integer bytes
#           "before" - alias for 0
#           "after" - alias for -1
#           default: before
#
# Currently the only error injection action is to terminate the server process.
# This resets the TCP connection and thus forces the client to handle
# unexpected connection termination.
#
# Other error injection actions could be added in the future.
#
# Copyright Red Hat, Inc. 2014
#
# Authors:
#   Stefan Hajnoczi <stefanha@redhat.com>
#
# This work is licensed under the terms of the GNU GPL, version 2 or later.
# See the COPYING file in the top-level directory.

import sys
import socket
import struct
import collections
import configparser

FAKE_DISK_SIZE = 8 * 1024 * 1024 * 1024 # 8 GB

# Protocol constants
NBD_CMD_READ = 0
NBD_CMD_WRITE = 1
NBD_CMD_DISC = 2
NBD_REQUEST_MAGIC = 0x25609513
NBD_SIMPLE_REPLY_MAGIC = 0x67446698
NBD_PASSWD = 0x4e42444d41474943
NBD_OPTS_MAGIC = 0x49484156454F5054
NBD_CLIENT_MAGIC = 0x0000420281861253
NBD_OPT_EXPORT_NAME = 1 << 0

# Protocol structs
neg_classic_struct = struct.Struct('>QQQI124x')
neg1_struct = struct.Struct('>QQH')
export_tuple = collections.namedtuple('Export', 'reserved magic opt len')
export_struct = struct.Struct('>IQII')
neg2_struct = struct.Struct('>QH124x')
request_tuple = collections.namedtuple('Request', 'magic type handle from_ len')
request_struct = struct.Struct('>IIQQI')
reply_struct = struct.Struct('>IIQ')

def err(msg):
    sys.stderr.write(msg + '\n')
    sys.exit(1)

def recvall(sock, bufsize):
    received = 0
    chunks = []
    while received < bufsize:
        chunk = sock.recv(bufsize - received)
        if len(chunk) == 0:
            raise Exception('unexpected disconnect')
        chunks.append(chunk)
        received += len(chunk)
    return b''.join(chunks)

class Rule(object):
    def __init__(self, name, event, io, when):
        self.name = name
        self.event = event
        self.io = io
        self.when = when

    def match(self, event, io):
        if event != self.event:
            return False
        if io != self.io and self.io != 'readwrite':
            return False
        return True

class FaultInjectionSocket(object):
    def __init__(self, sock, rules):
        self.sock = sock
        self.rules = rules

    def check(self, event, io, bufsize=None):
        for rule in self.rules:
            if rule.match(event, io):
                if rule.when == 0 or bufsize is None:
                    print('Closing connection on rule match %s' % rule.name)
                    self.sock.close()
                    sys.stdout.flush()
                    sys.exit(0)
                if rule.when != -1:
                    return rule.when
        return bufsize

    def send(self, buf, event):
        bufsize = self.check(event, 'write', bufsize=len(buf))
        self.sock.sendall(buf[:bufsize])
        self.check(event, 'write')

    def recv(self, bufsize, event):
        bufsize = self.check(event, 'read', bufsize=bufsize)
        data = recvall(self.sock, bufsize)
        self.check(event, 'read')
        return data

    def close(self):
        self.sock.close()

def negotiate_classic(conn):
    buf = neg_classic_struct.pack(NBD_PASSWD, NBD_CLIENT_MAGIC,
                                  FAKE_DISK_SIZE, 0)
    conn.send(buf, event='neg-classic')

def negotiate_export(conn):
    # Send negotiation part 1
    buf = neg1_struct.pack(NBD_PASSWD, NBD_OPTS_MAGIC, 0)
    conn.send(buf, event='neg1')

    # Receive export option
    buf = conn.recv(export_struct.size, event='export')
    export = export_tuple._make(export_struct.unpack(buf))
    assert export.magic == NBD_OPTS_MAGIC
    assert export.opt == NBD_OPT_EXPORT_NAME
    name = conn.recv(export.len, event='export-name')

    # Send negotiation part 2
    buf = neg2_struct.pack(FAKE_DISK_SIZE, 0)
    conn.send(buf, event='neg2')

def negotiate(conn, use_export):
    '''Negotiate export with client'''
    if use_export:
        negotiate_export(conn)
    else:
        negotiate_classic(conn)

def read_request(conn):
    '''Parse NBD request from client'''
    buf = conn.recv(request_struct.size, event='request')
    req = request_tuple._make(request_struct.unpack(buf))
    assert req.magic == NBD_REQUEST_MAGIC
    return req

def write_reply(conn, error, handle):
    buf = reply_struct.pack(NBD_SIMPLE_REPLY_MAGIC, error, handle)
    conn.send(buf, event='reply')

def handle_connection(conn, use_export):
    negotiate(conn, use_export)
    while True:
        req = read_request(conn)
        if req.type == NBD_CMD_READ:
            write_reply(conn, 0, req.handle)
            conn.send(b'\0' * req.len, event='data')
        elif req.type == NBD_CMD_WRITE:
            _ = conn.recv(req.len, event='data')
            write_reply(conn, 0, req.handle)
        elif req.type == NBD_CMD_DISC:
            break
        else:
            print('unrecognized command type %#02x' % req.type)
            break
    conn.close()

def run_server(sock, rules, use_export):
    while True:
        conn, _ = sock.accept()
        handle_connection(FaultInjectionSocket(conn, rules), use_export)

def parse_inject_error(name, options):
    if 'event' not in options:
        err('missing \"event\" option in %s' % name)
    event = options['event']
    if event not in ('neg-classic', 'neg1', 'export', 'neg2', 'request', 'reply', 'data'):
        err('invalid \"event\" option value \"%s\" in %s' % (event, name))
    io = options.get('io', 'readwrite')
    if io not in ('read', 'write', 'readwrite'):
        err('invalid \"io\" option value \"%s\" in %s' % (io, name))
    when = options.get('when', 'before')
    try:
        when = int(when)
    except ValueError:
        if when == 'before':
            when = 0
        elif when == 'after':
            when = -1
        else:
            err('invalid \"when\" option value \"%s\" in %s' % (when, name))
    return Rule(name, event, io, when)

def parse_config(config):
    rules = []
    for name in config.sections():
        if name.startswith('inject-error'):
            options = dict(config.items(name))
            rules.append(parse_inject_error(name, options))
        else:
            err('invalid config section name: %s' % name)
    return rules

def load_rules(filename):
    config = configparser.RawConfigParser()
    with open(filename, 'rt') as f:
        config.readfp(f, filename)
    return parse_config(config)

def open_socket(path):
    '''Open a TCP or UNIX domain listen socket'''
    if ':' in path:
        host, port = path.split(':', 1)
        sock = socket.socket()
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        sock.bind((host, int(port)))

        # If given port was 0 the final port number is now available
        path = '%s:%d' % sock.getsockname()
    else:
        sock = socket.socket(socket.AF_UNIX)
        sock.bind(path)
    sock.listen(0)
    print('Listening on %s' % path)
    sys.stdout.flush() # another process may be waiting, show message now
    return sock

def usage(args):
    sys.stderr.write('usage: %s [--classic-negotiation] <tcp-port>|<unix-path> <config-file>\n' % args[0])
    sys.stderr.write('Run an fault injector NBD server with rules defined in a config file.\n')
    sys.exit(1)

def main(args):
    if len(args) != 3 and len(args) != 4:
        usage(args)
    use_export = True
    if args[1] == '--classic-negotiation':
        use_export = False
    elif len(args) == 4:
        usage(args)
    sock = open_socket(args[1 if use_export else 2])
    rules = load_rules(args[2 if use_export else 3])
    run_server(sock, rules, use_export)
    return 0

if __name__ == '__main__':
    sys.exit(main(sys.argv))