nbd patches for 2018-01-26

- Vladimir Sementsov-Ogievskiy - nbd export qmp interface
 - Eric Blake - hmp: Add nbd_server_remove to mirror QMP command
 - Edgar Kaziakhmedov - nbd: implement bdrv_get_info callback
 -----BEGIN PGP SIGNATURE-----
 Comment: Public key at http://people.redhat.com/eblake/eblake.gpg
 
 iQEcBAABCAAGBQJaa1EaAAoJEKeha0olJ0NqmBUH+gPukp0LJUDd8GKgoTVLumys
 lRJ6+XPosN/z9mKuM4KFUDRnOV25N5Jq+iyYUHdOzL2WVaM1JWJXo45qPgNGtRPc
 feQ2oF3YfSZ+OvpyemDoV1CfQJdjc+/SfDeOrwR6diaxIvW9nezDlSDQxNnMVP4I
 yexNEFa92+e6lmjXxb+TVzwp2HGb1GQu+yV3pB2sVeAdU3ldWMqTMKjzGMDtvgrM
 C7WdVXGaE2geqMZKmbYtSGwmRcEgC62zn9luCnCD30SSfy4VkpdCcsBLWfyd0UC3
 ZIS2dDWuoyGAk1hZlNxzUxdWLrg79PstL4w5FlCqYGlklm31qvrqZj/4AcCeny8=
 =yaSE
 -----END PGP SIGNATURE-----

Merge remote-tracking branch 'remotes/ericb/tags/pull-nbd-2018-01-26' into staging

nbd patches for 2018-01-26

- Vladimir Sementsov-Ogievskiy - nbd export qmp interface
- Eric Blake - hmp: Add nbd_server_remove to mirror QMP command
- Edgar Kaziakhmedov - nbd: implement bdrv_get_info callback

# gpg: Signature made Fri 26 Jan 2018 16:02:34 GMT
# gpg:                using RSA key 0xA7A16B4A2527436A
# gpg: Good signature from "Eric Blake <eblake@redhat.com>"
# gpg:                 aka "Eric Blake (Free Software Programmer) <ebb9@byu.net>"
# gpg:                 aka "[jpeg image of size 6874]"
# Primary key fingerprint: 71C2 CC22 B1C4 6029 27D2  F3AA A7A1 6B4A 2527 436A

* remotes/ericb/tags/pull-nbd-2018-01-26:
  nbd: implement bdrv_get_info callback
  hmp: Add nbd_server_remove to mirror QMP command
  iotest 205: new test for qmp nbd-server-remove
  iotests: implement QemuIoInteractive class
  iotest 147: add cases to test new @name parameter of nbd-server-add
  qapi: add nbd-server-remove
  hmp: Add name parameter to nbd_server_add
  qapi: add name parameter to nbd-server-add

Signed-off-by: Peter Maydell <peter.maydell@linaro.org>
This commit is contained in:
Peter Maydell 2018-01-26 17:29:14 +00:00
commit 6233b4a8c2
14 changed files with 401 additions and 31 deletions

View File

@ -566,6 +566,14 @@ static void nbd_refresh_filename(BlockDriverState *bs, QDict *options)
bs->full_open_options = opts;
}
static int nbd_get_info(BlockDriverState *bs, BlockDriverInfo *bdi)
{
if (bs->supported_zero_flags & BDRV_REQ_MAY_UNMAP) {
bdi->can_write_zeroes_with_unmap = true;
}
return 0;
}
static BlockDriver bdrv_nbd = {
.format_name = "nbd",
.protocol_name = "nbd",
@ -583,6 +591,7 @@ static BlockDriver bdrv_nbd = {
.bdrv_detach_aio_context = nbd_detach_aio_context,
.bdrv_attach_aio_context = nbd_attach_aio_context,
.bdrv_refresh_filename = nbd_refresh_filename,
.bdrv_get_info = nbd_get_info,
};
static BlockDriver bdrv_nbd_tcp = {
@ -602,6 +611,7 @@ static BlockDriver bdrv_nbd_tcp = {
.bdrv_detach_aio_context = nbd_detach_aio_context,
.bdrv_attach_aio_context = nbd_attach_aio_context,
.bdrv_refresh_filename = nbd_refresh_filename,
.bdrv_get_info = nbd_get_info,
};
static BlockDriver bdrv_nbd_unix = {
@ -621,6 +631,7 @@ static BlockDriver bdrv_nbd_unix = {
.bdrv_detach_aio_context = nbd_detach_aio_context,
.bdrv_attach_aio_context = nbd_attach_aio_context,
.bdrv_refresh_filename = nbd_refresh_filename,
.bdrv_get_info = nbd_get_info,
};
static void bdrv_nbd_init(void)

View File

@ -140,8 +140,8 @@ void qmp_nbd_server_start(SocketAddressLegacy *addr,
qapi_free_SocketAddress(addr_flat);
}
void qmp_nbd_server_add(const char *device, bool has_writable, bool writable,
Error **errp)
void qmp_nbd_server_add(const char *device, bool has_name, const char *name,
bool has_writable, bool writable, Error **errp)
{
BlockDriverState *bs = NULL;
BlockBackend *on_eject_blk;
@ -152,8 +152,12 @@ void qmp_nbd_server_add(const char *device, bool has_writable, bool writable,
return;
}
if (nbd_export_find(device)) {
error_setg(errp, "NBD server already exporting device '%s'", device);
if (!has_name) {
name = device;
}
if (nbd_export_find(name)) {
error_setg(errp, "NBD server already has export named '%s'", name);
return;
}
@ -177,7 +181,7 @@ void qmp_nbd_server_add(const char *device, bool has_writable, bool writable,
return;
}
nbd_export_set_name(exp, device);
nbd_export_set_name(exp, name);
/* The list of named exports has a strong reference to this export now and
* our only way of accessing it is through nbd_export_find(), so we can drop
@ -185,6 +189,30 @@ void qmp_nbd_server_add(const char *device, bool has_writable, bool writable,
nbd_export_put(exp);
}
void qmp_nbd_server_remove(const char *name,
bool has_mode, NbdServerRemoveMode mode,
Error **errp)
{
NBDExport *exp;
if (!nbd_server) {
error_setg(errp, "NBD server not running");
return;
}
exp = nbd_export_find(name);
if (exp == NULL) {
error_setg(errp, "Export '%s' is not found", name);
return;
}
if (!has_mode) {
mode = NBD_SERVER_REMOVE_MODE_SAFE;
}
nbd_export_remove(exp, mode, errp);
}
void qmp_nbd_server_stop(Error **errp)
{
nbd_export_close_all();

View File

@ -1553,17 +1553,35 @@ ETEXI
{
.name = "nbd_server_add",
.args_type = "writable:-w,device:B",
.params = "nbd_server_add [-w] device",
.args_type = "writable:-w,device:B,name:s?",
.params = "nbd_server_add [-w] device [name]",
.help = "export a block device via NBD",
.cmd = hmp_nbd_server_add,
},
STEXI
@item nbd_server_add @var{device}
@item nbd_server_add @var{device} [ @var{name} ]
@findex nbd_server_add
Export a block device through QEMU's NBD server, which must be started
beforehand with @command{nbd_server_start}. The @option{-w} option makes the
exported device writable too.
exported device writable too. The export name is controlled by @var{name},
defaulting to @var{device}.
ETEXI
{
.name = "nbd_server_remove",
.args_type = "force:-f,name:s",
.params = "nbd_server_remove [-f] name",
.help = "remove an export previously exposed via NBD",
.cmd = hmp_nbd_server_remove,
},
STEXI
@item nbd_server_remove [-f] @var{name}
@findex nbd_server_remove
Stop exporting a block device through QEMU's NBD server, which was
previously started with @command{nbd_server_add}. The @option{-f}
option forces the server to drop the export immediately even if
clients are connected; otherwise the command fails unless there are no
clients.
ETEXI
{

20
hmp.c
View File

@ -2203,7 +2203,8 @@ void hmp_nbd_server_start(Monitor *mon, const QDict *qdict)
continue;
}
qmp_nbd_server_add(info->value->device, true, writable, &local_err);
qmp_nbd_server_add(info->value->device, false, NULL,
true, writable, &local_err);
if (local_err != NULL) {
qmp_nbd_server_stop(NULL);
@ -2220,14 +2221,23 @@ exit:
void hmp_nbd_server_add(Monitor *mon, const QDict *qdict)
{
const char *device = qdict_get_str(qdict, "device");
const char *name = qdict_get_try_str(qdict, "name");
bool writable = qdict_get_try_bool(qdict, "writable", false);
Error *local_err = NULL;
qmp_nbd_server_add(device, true, writable, &local_err);
qmp_nbd_server_add(device, !!name, name, true, writable, &local_err);
hmp_handle_error(mon, &local_err);
}
if (local_err != NULL) {
hmp_handle_error(mon, &local_err);
}
void hmp_nbd_server_remove(Monitor *mon, const QDict *qdict)
{
const char *name = qdict_get_str(qdict, "name");
bool force = qdict_get_try_bool(qdict, "force", false);
Error *err = NULL;
/* Rely on NBD_SERVER_REMOVE_MODE_SAFE being the default */
qmp_nbd_server_remove(name, force, NBD_SERVER_REMOVE_MODE_HARD, &err);
hmp_handle_error(mon, &err);
}
void hmp_nbd_server_stop(Monitor *mon, const QDict *qdict)

1
hmp.h
View File

@ -101,6 +101,7 @@ void hmp_sendkey(Monitor *mon, const QDict *qdict);
void hmp_screendump(Monitor *mon, const QDict *qdict);
void hmp_nbd_server_start(Monitor *mon, const QDict *qdict);
void hmp_nbd_server_add(Monitor *mon, const QDict *qdict);
void hmp_nbd_server_remove(Monitor *mon, const QDict *qdict);
void hmp_nbd_server_stop(Monitor *mon, const QDict *qdict);
void hmp_chardev_add(Monitor *mon, const QDict *qdict);
void hmp_chardev_change(Monitor *mon, const QDict *qdict);

View File

@ -261,6 +261,7 @@ NBDExport *nbd_export_new(BlockDriverState *bs, off_t dev_offset, off_t size,
bool writethrough, BlockBackend *on_eject_blk,
Error **errp);
void nbd_export_close(NBDExport *exp);
void nbd_export_remove(NBDExport *exp, NbdServerRemoveMode mode, Error **errp);
void nbd_export_get(NBDExport *exp);
void nbd_export_put(NBDExport *exp);

View File

@ -1177,6 +1177,19 @@ void nbd_export_close(NBDExport *exp)
nbd_export_put(exp);
}
void nbd_export_remove(NBDExport *exp, NbdServerRemoveMode mode, Error **errp)
{
if (mode == NBD_SERVER_REMOVE_MODE_HARD || QTAILQ_EMPTY(&exp->clients)) {
nbd_export_close(exp);
return;
}
assert(mode == NBD_SERVER_REMOVE_MODE_SAFE);
error_setg(errp, "export '%s' still in use", exp->name);
error_append_hint(errp, "Use mode='hard' to force client disconnect\n");
}
void nbd_export_get(NBDExport *exp)
{
assert(exp->refcount > 0);

View File

@ -213,14 +213,60 @@
#
# @device: The device name or node name of the node to be exported
#
# @name: Export name. If unspecified, the @device parameter is used as the
# export name. (Since 2.12)
#
# @writable: Whether clients should be able to write to the device via the
# NBD connection (default false).
#
# Returns: error if the device is already marked for export.
# Returns: error if the server is not running, or export with the same name
# already exists.
#
# Since: 1.3.0
##
{ 'command': 'nbd-server-add', 'data': {'device': 'str', '*writable': 'bool'} }
{ 'command': 'nbd-server-add',
'data': {'device': 'str', '*name': 'str', '*writable': 'bool'} }
##
# @NbdServerRemoveMode:
#
# Mode for removing an NBD export.
#
# @safe: Remove export if there are no existing connections, fail otherwise.
#
# @hard: Drop all connections immediately and remove export.
#
# Potential additional modes to be added in the future:
#
# hide: Just hide export from new clients, leave existing connections as is.
# Remove export after all clients are disconnected.
#
# soft: Hide export from new clients, answer with ESHUTDOWN for all further
# requests from existing clients.
#
# Since: 2.12
##
{'enum': 'NbdServerRemoveMode', 'data': ['safe', 'hard']}
##
# @nbd-server-remove:
#
# Remove NBD export by name.
#
# @name: Export name.
#
# @mode: Mode of command operation. See @NbdServerRemoveMode description.
# Default is 'safe'.
#
# Returns: error if
# - the server is not running
# - export is not found
# - mode is 'safe' and there are existing connections
#
# Since: 2.12
##
{ 'command': 'nbd-server-remove',
'data': {'name': 'str', '*mode': 'NbdServerRemoveMode'} }
##
# @nbd-server-stop:

View File

@ -38,8 +38,8 @@ def flatten_sock_addr(crumpled_address):
class NBDBlockdevAddBase(iotests.QMPTestCase):
def blockdev_add_options(self, address, export=None):
options = { 'node-name': 'nbd-blockdev',
def blockdev_add_options(self, address, export, node_name):
options = { 'node-name': node_name,
'driver': 'raw',
'file': {
'driver': 'nbd',
@ -50,23 +50,28 @@ class NBDBlockdevAddBase(iotests.QMPTestCase):
options['file']['export'] = export
return options
def client_test(self, filename, address, export=None):
bao = self.blockdev_add_options(address, export)
def client_test(self, filename, address, export=None,
node_name='nbd-blockdev', delete=True):
bao = self.blockdev_add_options(address, export, node_name)
result = self.vm.qmp('blockdev-add', **bao)
self.assert_qmp(result, 'return', {})
found = False
result = self.vm.qmp('query-named-block-nodes')
for node in result['return']:
if node['node-name'] == 'nbd-blockdev':
if node['node-name'] == node_name:
found = True
if isinstance(filename, str):
self.assert_qmp(node, 'image/filename', filename)
else:
self.assert_json_filename_equal(node['image']['filename'],
filename)
break
self.assertTrue(found)
result = self.vm.qmp('blockdev-del', node_name='nbd-blockdev')
self.assert_qmp(result, 'return', {})
if delete:
result = self.vm.qmp('blockdev-del', node_name=node_name)
self.assert_qmp(result, 'return', {})
class QemuNBD(NBDBlockdevAddBase):
@ -125,26 +130,63 @@ class BuiltinNBD(NBDBlockdevAddBase):
except OSError:
pass
def _server_up(self, address):
def _server_up(self, address, export_name=None, export_name2=None):
result = self.server.qmp('nbd-server-start', addr=address)
self.assert_qmp(result, 'return', {})
result = self.server.qmp('nbd-server-add', device='nbd-export')
if export_name is None:
result = self.server.qmp('nbd-server-add', device='nbd-export')
else:
result = self.server.qmp('nbd-server-add', device='nbd-export',
name=export_name)
self.assert_qmp(result, 'return', {})
if export_name2 is not None:
result = self.server.qmp('nbd-server-add', device='nbd-export',
name=export_name2)
self.assert_qmp(result, 'return', {})
def _server_down(self):
result = self.server.qmp('nbd-server-stop')
self.assert_qmp(result, 'return', {})
def test_inet(self):
def do_test_inet(self, export_name=None):
address = { 'type': 'inet',
'data': {
'host': 'localhost',
'port': str(NBD_PORT)
} }
self._server_up(address)
self.client_test('nbd://localhost:%i/nbd-export' % NBD_PORT,
flatten_sock_addr(address), 'nbd-export')
self._server_up(address, export_name)
export_name = export_name or 'nbd-export'
self.client_test('nbd://localhost:%i/%s' % (NBD_PORT, export_name),
flatten_sock_addr(address), export_name)
self._server_down()
def test_inet_default_export_name(self):
self.do_test_inet()
def test_inet_same_export_name(self):
self.do_test_inet('nbd-export')
def test_inet_different_export_name(self):
self.do_test_inet('shadow')
def test_inet_two_exports(self):
address = { 'type': 'inet',
'data': {
'host': 'localhost',
'port': str(NBD_PORT)
} }
self._server_up(address, 'exp1', 'exp2')
self.client_test('nbd://localhost:%i/%s' % (NBD_PORT, 'exp1'),
flatten_sock_addr(address), 'exp1', 'node1', False)
self.client_test('nbd://localhost:%i/%s' % (NBD_PORT, 'exp2'),
flatten_sock_addr(address), 'exp2', 'node2', False)
result = self.vm.qmp('blockdev-del', node_name='node1')
self.assert_qmp(result, 'return', {})
result = self.vm.qmp('blockdev-del', node_name='node2')
self.assert_qmp(result, 'return', {})
self._server_down()
def test_inet6(self):

View File

@ -1,5 +1,5 @@
......
.........
----------------------------------------------------------------------
Ran 6 tests
Ran 9 tests
OK

156
tests/qemu-iotests/205 Normal file
View File

@ -0,0 +1,156 @@
#!/usr/bin/env python
#
# Tests for qmp command nbd-server-remove.
#
# Copyright (c) 2017 Virtuozzo International GmbH
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import os
import sys
import iotests
import time
from iotests import qemu_img, qemu_io, filter_qemu_io, QemuIoInteractive
nbd_sock = 'nbd_sock'
nbd_uri = 'nbd+unix:///exp?socket=' + nbd_sock
disk = os.path.join(iotests.test_dir, 'disk')
class TestNbdServerRemove(iotests.QMPTestCase):
def setUp(self):
qemu_img('create', '-f', iotests.imgfmt, disk, '1M')
self.vm = iotests.VM().add_drive(disk)
self.vm.launch()
address = {
'type': 'unix',
'data': {
'path': nbd_sock
}
}
result = self.vm.qmp('nbd-server-start', addr=address)
self.assert_qmp(result, 'return', {})
result = self.vm.qmp('nbd-server-add', device='drive0', name='exp')
self.assert_qmp(result, 'return', {})
def tearDown(self):
self.vm.shutdown()
os.remove(nbd_sock)
os.remove(disk)
def remove_export(self, name, mode=None):
if mode is None:
return self.vm.qmp('nbd-server-remove', name=name)
else:
return self.vm.qmp('nbd-server-remove', name=name, mode=mode)
def assertExportNotFound(self, name):
result = self.vm.qmp('nbd-server-remove', name=name)
self.assert_qmp(result, 'error/desc', "Export 'exp' is not found")
def assertExistingClients(self, result):
self.assert_qmp(result, 'error/desc', "export 'exp' still in use")
def assertReadOk(self, qemu_io_output):
self.assertEqual(
filter_qemu_io(qemu_io_output).strip(),
'read 512/512 bytes at offset 0\n' +
'512 bytes, X ops; XX:XX:XX.X (XXX YYY/sec and XXX ops/sec)')
def assertReadFailed(self, qemu_io_output):
self.assertEqual(filter_qemu_io(qemu_io_output).strip(),
'read failed: Input/output error')
def assertConnectFailed(self, qemu_io_output):
self.assertEqual(filter_qemu_io(qemu_io_output).strip(),
"can't open device " + nbd_uri +
": Requested export not available\n"
"server reported: export 'exp' not present")
def do_test_connect_after_remove(self, mode=None):
args = ('-r', '-f', 'raw', '-c', 'read 0 512', nbd_uri)
self.assertReadOk(qemu_io(*args))
result = self.remove_export('exp', mode)
self.assert_qmp(result, 'return', {})
self.assertExportNotFound('exp')
self.assertConnectFailed(qemu_io(*args))
def test_connect_after_remove_default(self):
self.do_test_connect_after_remove()
def test_connect_after_remove_safe(self):
self.do_test_connect_after_remove('safe')
def test_connect_after_remove_force(self):
self.do_test_connect_after_remove('hard')
def do_test_remove_during_connect_safe(self, mode=None):
qio = QemuIoInteractive('-r', '-f', 'raw', nbd_uri)
self.assertReadOk(qio.cmd('read 0 512'))
result = self.remove_export('exp', mode)
self.assertExistingClients(result)
self.assertReadOk(qio.cmd('read 0 512'))
qio.close()
result = self.remove_export('exp', mode)
self.assert_qmp(result, 'return', {})
self.assertExportNotFound('exp')
def test_remove_during_connect_default(self):
self.do_test_remove_during_connect_safe()
def test_remove_during_connect_safe(self):
self.do_test_remove_during_connect_safe('safe')
def test_remove_during_connect_hard(self):
qio = QemuIoInteractive('-r', '-f', 'raw', nbd_uri)
self.assertReadOk(qio.cmd('read 0 512'))
result = self.remove_export('exp', 'hard')
self.assert_qmp(result, 'return', {})
self.assertReadFailed(qio.cmd('read 0 512'))
self.assertExportNotFound('exp')
qio.close()
def test_remove_during_connect_safe_hard(self):
qio = QemuIoInteractive('-r', '-f', 'raw', nbd_uri)
self.assertReadOk(qio.cmd('read 0 512'))
result = self.remove_export('exp', 'safe')
self.assertExistingClients(result)
self.assertReadOk(qio.cmd('read 0 512'))
result = self.remove_export('exp', 'hard')
self.assert_qmp(result, 'return', {})
self.assertExportNotFound('exp')
self.assertReadFailed(qio.cmd('read 0 512'))
qio.close()
if __name__ == '__main__':
iotests.main()

View File

@ -0,0 +1,5 @@
.......
----------------------------------------------------------------------
Ran 7 tests
OK

View File

@ -201,3 +201,4 @@
202 rw auto quick
203 rw auto
204 rw auto quick
205 rw auto quick

View File

@ -93,6 +93,44 @@ def qemu_io(*args):
sys.stderr.write('qemu-io received signal %i: %s\n' % (-exitcode, ' '.join(args)))
return subp.communicate()[0]
class QemuIoInteractive:
def __init__(self, *args):
self.args = qemu_io_args + list(args)
self._p = subprocess.Popen(self.args, stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
assert self._p.stdout.read(9) == 'qemu-io> '
def close(self):
self._p.communicate('q\n')
def _read_output(self):
pattern = 'qemu-io> '
n = len(pattern)
pos = 0
s = []
while pos != n:
c = self._p.stdout.read(1)
# check unexpected EOF
assert c != ''
s.append(c)
if c == pattern[pos]:
pos += 1
else:
pos = 0
return ''.join(s[:-n])
def cmd(self, cmd):
# quit command is in close(), '\n' is added automatically
assert '\n' not in cmd
cmd = cmd.strip()
assert cmd != 'q' and cmd != 'quit'
self._p.stdin.write(cmd + '\n')
return self._read_output()
def qemu_nbd(*args):
'''Run qemu-nbd in daemon mode and return the parent's exit code'''
return subprocess.call(qemu_nbd_args + ['--fork'] + list(args))