From patchwork Sat Mar 9 22:22:25 2013 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Stefan Hajnoczi X-Patchwork-Id: 226399 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.gnu.org (lists.gnu.org [208.118.235.17]) (using TLSv1 with cipher AES256-SHA (256/256 bits)) (Client did not present a certificate) by ozlabs.org (Postfix) with ESMTPS id 995E22C0337 for ; Sun, 10 Mar 2013 09:26:59 +1100 (EST) Received: from localhost ([::1]:35131 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1UESED-0001gY-Qq for incoming@patchwork.ozlabs.org; Sat, 09 Mar 2013 17:26:57 -0500 Received: from eggs.gnu.org ([208.118.235.92]:43528) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1UESB8-0005Dw-FI for qemu-devel@nongnu.org; Sat, 09 Mar 2013 17:23:49 -0500 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1UESB2-0008QO-Nc for qemu-devel@nongnu.org; Sat, 09 Mar 2013 17:23:46 -0500 Received: from mx1.redhat.com ([209.132.183.28]:54121) by eggs.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1UESB2-0008QI-FV for qemu-devel@nongnu.org; Sat, 09 Mar 2013 17:23:40 -0500 Received: from int-mx10.intmail.prod.int.phx2.redhat.com (int-mx10.intmail.prod.int.phx2.redhat.com [10.5.11.23]) by mx1.redhat.com (8.14.4/8.14.4) with ESMTP id r29MNdl6010995 (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK); Sat, 9 Mar 2013 17:23:39 -0500 Received: from localhost (ovpn-112-17.ams2.redhat.com [10.36.112.17]) by int-mx10.intmail.prod.int.phx2.redhat.com (8.14.4/8.14.4) with ESMTP id r29MNcQK024222; Sat, 9 Mar 2013 17:23:39 -0500 From: Stefan Hajnoczi To: Date: Sat, 9 Mar 2013 23:22:25 +0100 Message-Id: <1362867748-30528-6-git-send-email-stefanha@redhat.com> In-Reply-To: <1362867748-30528-1-git-send-email-stefanha@redhat.com> References: <1362867748-30528-1-git-send-email-stefanha@redhat.com> X-Scanned-By: MIMEDefang 2.68 on 10.5.11.23 X-detected-operating-system: by eggs.gnu.org: GNU/Linux 3.x X-Received-From: 209.132.183.28 Cc: Kevin Wolf , dietmar@proxmox.com, Stefan Hajnoczi , Markus Armbruster Subject: [Qemu-devel] [RFC 5/8] Add nbd server Python module X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.14 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org Sender: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org The nbd module works like this: server = nbd.Server(sock) server.add_export('drive0', handler0) server.add_export('drive1', handler1) server.run() The user must provide a handler object which defines the behavior of an export: class MyNBDHandler(nbd.ExportHandler): def write(self, offset, data): pass # do something def size(self): return 10 * 1024 * 1024 * 1024 Note that the handler is invoked from a thread. Signed-off-by: Stefan Hajnoczi --- nbd.py | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 nbd.py diff --git a/nbd.py b/nbd.py new file mode 100644 index 0000000..2528531 --- /dev/null +++ b/nbd.py @@ -0,0 +1,124 @@ +# NBD server module +# +# Copyright 2013 Red Hat, Inc. and/or its affiliates +# +# Authors: +# Stefan Hajnoczi +# +# 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 struct +import collections +import threading + +__all__ = ['ExportHandler', 'Server'] + +NBD_CMD_WRITE = 1 +NBD_CMD_DISC = 2 +NBD_REQUEST_MAGIC = 0x25609513 +NBD_REPLY_MAGIC = 0x67446698 +NBD_PASSWD = 0x4e42444d41474943 +NBD_OPTS_MAGIC = 0x49484156454F5054 +NBD_OPT_EXPORT_NAME = 1 << 0 + +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 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 ''.join(chunks) + +class ExportHandler(object): + def write(self, offset, data): + pass + + def size(self): + return 0 + +def negotiate(conn, exports): + '''Negotiate export with client''' + # Send negotiation part 1 + buf = neg1_struct.pack(NBD_PASSWD, NBD_OPTS_MAGIC, 0) + conn.sendall(buf) + + # Receive export option + buf = recvall(conn, export_struct.size) + export = export_tuple._make(export_struct.unpack(buf)) + assert export.magic == NBD_OPTS_MAGIC + assert export.opt == NBD_OPT_EXPORT_NAME + name = recvall(conn, export.len) + + if name not in exports: + print 'name "%s" not in exports' % name + return None + handler = exports[name] + + # Send negotiation part 2 + buf = neg2_struct.pack(handler.size(), 0) + conn.sendall(buf) + return handler + +def read_request(conn): + '''Parse NBD request from client''' + buf = recvall(conn, request_struct.size) + 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_REPLY_MAGIC, error, handle) + conn.sendall(buf) + +def server_connection_thread(conn, exports): + handler = negotiate(conn, exports) + if handler is None: + conn.close() + return + + while True: + req = read_request(conn) + if req.type == NBD_CMD_WRITE: + # Reply immediately, don't propagate internal errors to client + write_reply(conn, 0, req.handle) + + data = recvall(conn, req.len) + handler.write(req.from_, data) + elif req.type == NBD_CMD_DISC: + break + else: + print 'unrecognized command type %#02x' % req.type + break + conn.close() + +class Server(object): + def __init__(self, sock): + self.sock = sock + self.exports = {} + + def add_export(self, name, handler): + self.exports[name] = handler + + def run(self): + threads = [] + for i in range(len(self.exports)): + conn, _ = self.sock.accept() + t = threading.Thread(target=server_connection_thread, + args=(conn, self.exports)) + t.daemon = True + t.start() + threads.append(t) + for t in threads: + t.join()