#!/usr/bin/python
import struct, socket, sys
# network block device server, substitute for nbd-server.  Probably slower.
# But it works!  And it's probably a lot easier to improve the
# performance of this Python version than of the C version.  This
# Python version is 14% of the size and perhaps 20% of the features of
# the C version.  Hmm, that's not so great after all...
# Working:
# - nbd protocol
# - read/write serving up files
# - error handling
# - file size detection
# - in theory, large file support... not really
# - so_reuseaddr
# - nonforking
# Missing:
# - reporting errors to client (in particular writing and reading past end)
# - multiple clients (this probably requires copy-on-write or read-only)
# - copy on write
# - read-only
# - permission tracking
# - idle timeouts
# - running from inetd
# - filename substitution
# - partial file exports
# - exports of large files (bigger than 1/4 of RAM)
# - manual exportsize specification
# - so_keepalive
# - that "split an export file into multiple files" thing that sticks the .0
#   on the end of your filename
# - backgrounding
# - daemonizing

class Error(Exception): pass

class buffsock:
    "Buffered socket wrapper; always returns the amount of data you want."
    def __init__(self, sock): self.sock = sock
    def recv(self, nbytes):
        rv = ''
        while len(rv) < nbytes:
            more = self.sock.recv(nbytes - len(rv))
            if more == '': raise Error(nbytes)
            rv += more
        return rv
    def send(self, astring): self.sock.send(astring)
    def close(self): self.sock.close()
            

class debugsock:
    "Debugging socket wrapper."
    def __init__(self, sock): self.sock = sock
    def recv(self, nbytes):
        print "recv(%d) =" % nbytes,
        rv = self.sock.recv(nbytes)
        print `rv`
        return rv
    def send(self, astring):
        print "send(%r) =" % astring,
        rv = self.sock.send(astring)
        print `rv`
        return rv
    def close(self):
        print "close()"
        self.sock.close()

def negotiation(exportsize):
    "Returns initial NBD negotiation sequence for exportsize in bytes."
    return ('NBDMAGIC' + '\x00\x00\x42\x02\x81\x86\x12\x53' +
        struct.pack('>Q', exportsize) + '\0' * 128);

def nbd_reply(error=0, handle=1, data=''):
    "Construct an NBD reply."
    assert type(handle) is type('') and len(handle) == 8
    return ('\x67\x44\x66\x98' + struct.pack('>L', error) + handle + data)
     
# possible request types
read_request = 0
write_request = 1
disconnect_request = 2

class nbd_request:
    "Decodes an NBD request off the TCP socket."
    def __init__(self, conn):
        conn = buffsock(conn)
        template = '>LL8sQL'
        header = conn.recv(struct.calcsize(template))
        (self.magic, self.type, self.handle, self.offset, 
            self.len) = struct.unpack(template, header)
        if self.magic != 0x25609513: raise Error(self.magic)
        if self.type == write_request: 
            self.data = conn.recv(self.len)
            assert len(self.data) == self.len
    def reply(self, error, data=''):
        return nbd_reply(error=error, handle=self.handle, data=data)
    def range(self):
        return slice(self.offset, self.offset + self.len)

def serveclient(asock, afile):
    "Serves a single client until it exits."
    afile.seek(0)
    abuf = list(afile.read())
    asock.send(negotiation(len(abuf)))
    while 1:
        req = nbd_request(asock)
        if req.type == read_request:
            asock.send(req.reply(error=0, 
                data=''.join(abuf[req.range()])))
        elif req.type == write_request:
            abuf[req.range()] = req.data
            afile.seek(req.offset)
            afile.write(req.data)
            afile.flush()
            asock.send(req.reply(error=0))
        elif req.type == disconnect_request:
            asock.close()
            return

def mainloop(listensock, afile):
    "Serves clients forever."
    while 1:
        (sock, addr) = listensock.accept()
        print "got conn on", addr
        serveclient(sock, afile)

def main(argv):
    "Given a port and a filename, serves up the file."
    afile = file(argv[2], 'rb+')
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind(('', int(argv[1])))
    sock.listen(5)
    mainloop(sock, afile)

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

Reply via email to