aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--block/iscsi.c353
-rw-r--r--block/mirror.c9
-rw-r--r--qapi/block-core.json125
-rw-r--r--qemu-options.hx2
4 files changed, 317 insertions, 172 deletions
diff --git a/block/iscsi.c b/block/iscsi.c
index 2561be9..c4f813b 100644
--- a/block/iscsi.c
+++ b/block/iscsi.c
@@ -1245,29 +1245,14 @@ retry:
return 0;
}
-static void parse_chap(struct iscsi_context *iscsi, const char *target,
+static void apply_chap(struct iscsi_context *iscsi, QemuOpts *opts,
Error **errp)
{
- QemuOptsList *list;
- QemuOpts *opts;
const char *user = NULL;
const char *password = NULL;
const char *secretid;
char *secret = NULL;
- list = qemu_find_opts("iscsi");
- if (!list) {
- return;
- }
-
- opts = qemu_opts_find(list, target);
- if (opts == NULL) {
- opts = QTAILQ_FIRST(&list->head);
- if (!opts) {
- return;
- }
- }
-
user = qemu_opt_get(opts, "user");
if (!user) {
return;
@@ -1298,64 +1283,36 @@ static void parse_chap(struct iscsi_context *iscsi, const char *target,
g_free(secret);
}
-static void parse_header_digest(struct iscsi_context *iscsi, const char *target,
+static void apply_header_digest(struct iscsi_context *iscsi, QemuOpts *opts,
Error **errp)
{
- QemuOptsList *list;
- QemuOpts *opts;
const char *digest = NULL;
- list = qemu_find_opts("iscsi");
- if (!list) {
- return;
- }
-
- opts = qemu_opts_find(list, target);
- if (opts == NULL) {
- opts = QTAILQ_FIRST(&list->head);
- if (!opts) {
- return;
- }
- }
-
digest = qemu_opt_get(opts, "header-digest");
if (!digest) {
- return;
- }
-
- if (!strcmp(digest, "CRC32C")) {
+ iscsi_set_header_digest(iscsi, ISCSI_HEADER_DIGEST_NONE_CRC32C);
+ } else if (!strcmp(digest, "crc32c")) {
iscsi_set_header_digest(iscsi, ISCSI_HEADER_DIGEST_CRC32C);
- } else if (!strcmp(digest, "NONE")) {
+ } else if (!strcmp(digest, "none")) {
iscsi_set_header_digest(iscsi, ISCSI_HEADER_DIGEST_NONE);
- } else if (!strcmp(digest, "CRC32C-NONE")) {
+ } else if (!strcmp(digest, "crc32c-none")) {
iscsi_set_header_digest(iscsi, ISCSI_HEADER_DIGEST_CRC32C_NONE);
- } else if (!strcmp(digest, "NONE-CRC32C")) {
+ } else if (!strcmp(digest, "none-crc32c")) {
iscsi_set_header_digest(iscsi, ISCSI_HEADER_DIGEST_NONE_CRC32C);
} else {
error_setg(errp, "Invalid header-digest setting : %s", digest);
}
}
-static char *parse_initiator_name(const char *target)
+static char *get_initiator_name(QemuOpts *opts)
{
- QemuOptsList *list;
- QemuOpts *opts;
const char *name;
char *iscsi_name;
UuidInfo *uuid_info;
- list = qemu_find_opts("iscsi");
- if (list) {
- opts = qemu_opts_find(list, target);
- if (!opts) {
- opts = QTAILQ_FIRST(&list->head);
- }
- if (opts) {
- name = qemu_opt_get(opts, "initiator-name");
- if (name) {
- return g_strdup(name);
- }
- }
+ name = qemu_opt_get(opts, "initiator-name");
+ if (name) {
+ return g_strdup(name);
}
uuid_info = qmp_query_uuid(NULL);
@@ -1370,29 +1327,6 @@ static char *parse_initiator_name(const char *target)
return iscsi_name;
}
-static int parse_timeout(const char *target)
-{
- QemuOptsList *list;
- QemuOpts *opts;
- const char *timeout;
-
- list = qemu_find_opts("iscsi");
- if (list) {
- opts = qemu_opts_find(list, target);
- if (!opts) {
- opts = QTAILQ_FIRST(&list->head);
- }
- if (opts) {
- timeout = qemu_opt_get(opts, "timeout");
- if (timeout) {
- return atoi(timeout);
- }
- }
- }
-
- return 0;
-}
-
static void iscsi_nop_timed_event(void *opaque)
{
IscsiLun *iscsilun = opaque;
@@ -1483,20 +1417,6 @@ static void iscsi_readcapacity_sync(IscsiLun *iscsilun, Error **errp)
}
}
-/* TODO Convert to fine grained options */
-static QemuOptsList runtime_opts = {
- .name = "iscsi",
- .head = QTAILQ_HEAD_INITIALIZER(runtime_opts.head),
- .desc = {
- {
- .name = "filename",
- .type = QEMU_OPT_STRING,
- .help = "URL to the iscsi image",
- },
- { /* end of list */ }
- },
-};
-
static struct scsi_task *iscsi_do_inquiry(struct iscsi_context *iscsi, int lun,
int evpd, int pc, void **inq, Error **errp)
{
@@ -1614,24 +1534,178 @@ out:
}
}
+static void iscsi_parse_iscsi_option(const char *target, QDict *options)
+{
+ QemuOptsList *list;
+ QemuOpts *opts;
+ const char *user, *password, *password_secret, *initiator_name,
+ *header_digest, *timeout;
+
+ list = qemu_find_opts("iscsi");
+ if (!list) {
+ return;
+ }
+
+ opts = qemu_opts_find(list, target);
+ if (opts == NULL) {
+ opts = QTAILQ_FIRST(&list->head);
+ if (!opts) {
+ return;
+ }
+ }
+
+ user = qemu_opt_get(opts, "user");
+ if (user) {
+ qdict_set_default_str(options, "user", user);
+ }
+
+ password = qemu_opt_get(opts, "password");
+ if (password) {
+ qdict_set_default_str(options, "password", password);
+ }
+
+ password_secret = qemu_opt_get(opts, "password-secret");
+ if (password_secret) {
+ qdict_set_default_str(options, "password-secret", password_secret);
+ }
+
+ initiator_name = qemu_opt_get(opts, "initiator-name");
+ if (initiator_name) {
+ qdict_set_default_str(options, "initiator-name", initiator_name);
+ }
+
+ header_digest = qemu_opt_get(opts, "header-digest");
+ if (header_digest) {
+ /* -iscsi takes upper case values, but QAPI only supports lower case
+ * enum constant names, so we have to convert here. */
+ char *qapi_value = g_ascii_strdown(header_digest, -1);
+ qdict_set_default_str(options, "header-digest", qapi_value);
+ g_free(qapi_value);
+ }
+
+ timeout = qemu_opt_get(opts, "timeout");
+ if (timeout) {
+ qdict_set_default_str(options, "timeout", timeout);
+ }
+}
+
/*
* We support iscsi url's on the form
* iscsi://[<username>%<password>@]<host>[:<port>]/<targetname>/<lun>
*/
+static void iscsi_parse_filename(const char *filename, QDict *options,
+ Error **errp)
+{
+ struct iscsi_url *iscsi_url;
+ const char *transport_name;
+ char *lun_str;
+
+ iscsi_url = iscsi_parse_full_url(NULL, filename);
+ if (iscsi_url == NULL) {
+ error_setg(errp, "Failed to parse URL : %s", filename);
+ return;
+ }
+
+#if LIBISCSI_API_VERSION >= (20160603)
+ switch (iscsi_url->transport) {
+ case TCP_TRANSPORT:
+ transport_name = "tcp";
+ break;
+ case ISER_TRANSPORT:
+ transport_name = "iser";
+ break;
+ default:
+ error_setg(errp, "Unknown transport type (%d)",
+ iscsi_url->transport);
+ return;
+ }
+#else
+ transport_name = "tcp";
+#endif
+
+ qdict_set_default_str(options, "transport", transport_name);
+ qdict_set_default_str(options, "portal", iscsi_url->portal);
+ qdict_set_default_str(options, "target", iscsi_url->target);
+
+ lun_str = g_strdup_printf("%d", iscsi_url->lun);
+ qdict_set_default_str(options, "lun", lun_str);
+ g_free(lun_str);
+
+ /* User/password from -iscsi take precedence over those from the URL */
+ iscsi_parse_iscsi_option(iscsi_url->target, options);
+
+ if (iscsi_url->user[0] != '\0') {
+ qdict_set_default_str(options, "user", iscsi_url->user);
+ qdict_set_default_str(options, "password", iscsi_url->passwd);
+ }
+
+ iscsi_destroy_url(iscsi_url);
+}
+
+static QemuOptsList runtime_opts = {
+ .name = "iscsi",
+ .head = QTAILQ_HEAD_INITIALIZER(runtime_opts.head),
+ .desc = {
+ {
+ .name = "transport",
+ .type = QEMU_OPT_STRING,
+ },
+ {
+ .name = "portal",
+ .type = QEMU_OPT_STRING,
+ },
+ {
+ .name = "target",
+ .type = QEMU_OPT_STRING,
+ },
+ {
+ .name = "user",
+ .type = QEMU_OPT_STRING,
+ },
+ {
+ .name = "password",
+ .type = QEMU_OPT_STRING,
+ },
+ {
+ .name = "password-secret",
+ .type = QEMU_OPT_STRING,
+ },
+ {
+ .name = "lun",
+ .type = QEMU_OPT_NUMBER,
+ },
+ {
+ .name = "initiator-name",
+ .type = QEMU_OPT_STRING,
+ },
+ {
+ .name = "header-digest",
+ .type = QEMU_OPT_STRING,
+ },
+ {
+ .name = "timeout",
+ .type = QEMU_OPT_NUMBER,
+ },
+ { /* end of list */ }
+ },
+};
+
static int iscsi_open(BlockDriverState *bs, QDict *options, int flags,
Error **errp)
{
IscsiLun *iscsilun = bs->opaque;
struct iscsi_context *iscsi = NULL;
- struct iscsi_url *iscsi_url = NULL;
struct scsi_task *task = NULL;
struct scsi_inquiry_standard *inq = NULL;
struct scsi_inquiry_supported_pages *inq_vpd;
char *initiator_name = NULL;
QemuOpts *opts;
Error *local_err = NULL;
- const char *filename;
- int i, ret = 0, timeout = 0;
+ const char *transport_name, *portal, *target;
+#if LIBISCSI_API_VERSION >= (20160603)
+ enum iscsi_transport_type transport;
+#endif
+ int i, ret = 0, timeout = 0, lun;
opts = qemu_opts_create(&runtime_opts, NULL, 0, &error_abort);
qemu_opts_absorb_qdict(opts, options, &local_err);
@@ -1641,18 +1715,34 @@ static int iscsi_open(BlockDriverState *bs, QDict *options, int flags,
goto out;
}
- filename = qemu_opt_get(opts, "filename");
+ transport_name = qemu_opt_get(opts, "transport");
+ portal = qemu_opt_get(opts, "portal");
+ target = qemu_opt_get(opts, "target");
+ lun = qemu_opt_get_number(opts, "lun", 0);
- iscsi_url = iscsi_parse_full_url(iscsi, filename);
- if (iscsi_url == NULL) {
- error_setg(errp, "Failed to parse URL : %s", filename);
+ if (!transport_name || !portal || !target) {
+ error_setg(errp, "Need all of transport, portal and target options");
+ ret = -EINVAL;
+ goto out;
+ }
+
+ if (!strcmp(transport_name, "tcp")) {
+#if LIBISCSI_API_VERSION >= (20160603)
+ transport = TCP_TRANSPORT;
+ } else if (!strcmp(transport_name, "iser")) {
+ transport = ISER_TRANSPORT;
+#else
+ /* TCP is what older libiscsi versions always use */
+#endif
+ } else {
+ error_setg(errp, "Unknown transport: %s", transport_name);
ret = -EINVAL;
goto out;
}
memset(iscsilun, 0, sizeof(IscsiLun));
- initiator_name = parse_initiator_name(iscsi_url->target);
+ initiator_name = get_initiator_name(opts);
iscsi = iscsi_create_context(initiator_name);
if (iscsi == NULL) {
@@ -1661,30 +1751,20 @@ static int iscsi_open(BlockDriverState *bs, QDict *options, int flags,
goto out;
}
#if LIBISCSI_API_VERSION >= (20160603)
- if (iscsi_init_transport(iscsi, iscsi_url->transport)) {
+ if (iscsi_init_transport(iscsi, transport)) {
error_setg(errp, ("Error initializing transport."));
ret = -EINVAL;
goto out;
}
#endif
- if (iscsi_set_targetname(iscsi, iscsi_url->target)) {
+ if (iscsi_set_targetname(iscsi, target)) {
error_setg(errp, "iSCSI: Failed to set target name.");
ret = -EINVAL;
goto out;
}
- if (iscsi_url->user[0] != '\0') {
- ret = iscsi_set_initiator_username_pwd(iscsi, iscsi_url->user,
- iscsi_url->passwd);
- if (ret != 0) {
- error_setg(errp, "Failed to set initiator username and password");
- ret = -EINVAL;
- goto out;
- }
- }
-
/* check if we got CHAP username/password via the options */
- parse_chap(iscsi, iscsi_url->target, &local_err);
+ apply_chap(iscsi, opts, &local_err);
if (local_err != NULL) {
error_propagate(errp, local_err);
ret = -EINVAL;
@@ -1697,10 +1777,8 @@ static int iscsi_open(BlockDriverState *bs, QDict *options, int flags,
goto out;
}
- iscsi_set_header_digest(iscsi, ISCSI_HEADER_DIGEST_NONE_CRC32C);
-
/* check if we got HEADER_DIGEST via the options */
- parse_header_digest(iscsi, iscsi_url->target, &local_err);
+ apply_header_digest(iscsi, opts, &local_err);
if (local_err != NULL) {
error_propagate(errp, local_err);
ret = -EINVAL;
@@ -1708,7 +1786,7 @@ static int iscsi_open(BlockDriverState *bs, QDict *options, int flags,
}
/* timeout handling is broken in libiscsi before 1.15.0 */
- timeout = parse_timeout(iscsi_url->target);
+ timeout = qemu_opt_get_number(opts, "timeout", 0);
#if LIBISCSI_API_VERSION >= 20150621
iscsi_set_timeout(iscsi, timeout);
#else
@@ -1717,7 +1795,7 @@ static int iscsi_open(BlockDriverState *bs, QDict *options, int flags,
}
#endif
- if (iscsi_full_connect_sync(iscsi, iscsi_url->portal, iscsi_url->lun) != 0) {
+ if (iscsi_full_connect_sync(iscsi, portal, lun) != 0) {
error_setg(errp, "iSCSI: Failed to connect to LUN : %s",
iscsi_get_error(iscsi));
ret = -EINVAL;
@@ -1726,7 +1804,7 @@ static int iscsi_open(BlockDriverState *bs, QDict *options, int flags,
iscsilun->iscsi = iscsi;
iscsilun->aio_context = bdrv_get_aio_context(bs);
- iscsilun->lun = iscsi_url->lun;
+ iscsilun->lun = lun;
iscsilun->has_write_same = true;
task = iscsi_do_inquiry(iscsilun->iscsi, iscsilun->lun, 0, 0,
@@ -1829,9 +1907,6 @@ static int iscsi_open(BlockDriverState *bs, QDict *options, int flags,
out:
qemu_opts_del(opts);
g_free(initiator_name);
- if (iscsi_url != NULL) {
- iscsi_destroy_url(iscsi_url);
- }
if (task != NULL) {
scsi_free_scsi_task(task);
}
@@ -2040,15 +2115,15 @@ static BlockDriver bdrv_iscsi = {
.format_name = "iscsi",
.protocol_name = "iscsi",
- .instance_size = sizeof(IscsiLun),
- .bdrv_needs_filename = true,
- .bdrv_file_open = iscsi_open,
- .bdrv_close = iscsi_close,
- .bdrv_create = iscsi_create,
- .create_opts = &iscsi_create_opts,
- .bdrv_reopen_prepare = iscsi_reopen_prepare,
- .bdrv_reopen_commit = iscsi_reopen_commit,
- .bdrv_invalidate_cache = iscsi_invalidate_cache,
+ .instance_size = sizeof(IscsiLun),
+ .bdrv_parse_filename = iscsi_parse_filename,
+ .bdrv_file_open = iscsi_open,
+ .bdrv_close = iscsi_close,
+ .bdrv_create = iscsi_create,
+ .create_opts = &iscsi_create_opts,
+ .bdrv_reopen_prepare = iscsi_reopen_prepare,
+ .bdrv_reopen_commit = iscsi_reopen_commit,
+ .bdrv_invalidate_cache = iscsi_invalidate_cache,
.bdrv_getlength = iscsi_getlength,
.bdrv_get_info = iscsi_get_info,
@@ -2075,15 +2150,15 @@ static BlockDriver bdrv_iser = {
.format_name = "iser",
.protocol_name = "iser",
- .instance_size = sizeof(IscsiLun),
- .bdrv_needs_filename = true,
- .bdrv_file_open = iscsi_open,
- .bdrv_close = iscsi_close,
- .bdrv_create = iscsi_create,
- .create_opts = &iscsi_create_opts,
- .bdrv_reopen_prepare = iscsi_reopen_prepare,
- .bdrv_reopen_commit = iscsi_reopen_commit,
- .bdrv_invalidate_cache = iscsi_invalidate_cache,
+ .instance_size = sizeof(IscsiLun),
+ .bdrv_parse_filename = iscsi_parse_filename,
+ .bdrv_file_open = iscsi_open,
+ .bdrv_close = iscsi_close,
+ .bdrv_create = iscsi_create,
+ .create_opts = &iscsi_create_opts,
+ .bdrv_reopen_prepare = iscsi_reopen_prepare,
+ .bdrv_reopen_commit = iscsi_reopen_commit,
+ .bdrv_invalidate_cache = iscsi_invalidate_cache,
.bdrv_getlength = iscsi_getlength,
.bdrv_get_info = iscsi_get_info,
diff --git a/block/mirror.c b/block/mirror.c
index 698a54e..ca8547b 100644
--- a/block/mirror.c
+++ b/block/mirror.c
@@ -69,6 +69,7 @@ typedef struct MirrorBlockJob {
bool waiting_for_io;
int target_cluster_sectors;
int max_iov;
+ bool initial_zeroing_ongoing;
} MirrorBlockJob;
typedef struct MirrorOp {
@@ -117,9 +118,10 @@ static void mirror_iteration_done(MirrorOp *op, int ret)
if (s->cow_bitmap) {
bitmap_set(s->cow_bitmap, chunk_num, nb_chunks);
}
- s->common.offset += (uint64_t)op->nb_sectors * BDRV_SECTOR_SIZE;
+ if (!s->initial_zeroing_ongoing) {
+ s->common.offset += (uint64_t)op->nb_sectors * BDRV_SECTOR_SIZE;
+ }
}
-
qemu_iovec_destroy(&op->qiov);
g_free(op);
@@ -572,6 +574,7 @@ static int coroutine_fn mirror_dirty_init(MirrorBlockJob *s)
return 0;
}
+ s->initial_zeroing_ongoing = true;
for (sector_num = 0; sector_num < end; ) {
int nb_sectors = MIN(end - sector_num,
QEMU_ALIGN_DOWN(INT_MAX, s->granularity) >> BDRV_SECTOR_BITS);
@@ -579,6 +582,7 @@ static int coroutine_fn mirror_dirty_init(MirrorBlockJob *s)
mirror_throttle(s);
if (block_job_is_cancelled(&s->common)) {
+ s->initial_zeroing_ongoing = false;
return 0;
}
@@ -593,6 +597,7 @@ static int coroutine_fn mirror_dirty_init(MirrorBlockJob *s)
}
mirror_wait_for_all_io(s);
+ s->initial_zeroing_ongoing = false;
}
/* First part, loop on the sectors and initialize the dirty bitmap. */
diff --git a/qapi/block-core.json b/qapi/block-core.json
index 932f5bb..5f82d35 100644
--- a/qapi/block-core.json
+++ b/qapi/block-core.json
@@ -2110,16 +2110,17 @@
# @nfs: Since 2.8
# @replication: Since 2.8
# @ssh: Since 2.8
+# @iscsi: Since 2.9
#
# Since: 2.0
##
{ 'enum': 'BlockdevDriver',
'data': [ 'archipelago', 'blkdebug', 'blkverify', 'bochs', 'cloop',
'dmg', 'file', 'ftp', 'ftps', 'gluster', 'host_cdrom',
- 'host_device', 'http', 'https', 'luks', 'nbd', 'nfs', 'null-aio',
- 'null-co', 'parallels', 'qcow', 'qcow2', 'qed', 'quorum', 'raw',
- 'replication', 'ssh', 'vdi', 'vhdx', 'vmdk', 'vpc',
- 'vvfat' ] }
+ 'host_device', 'http', 'https', 'iscsi', 'luks', 'nbd', 'nfs',
+ 'null-aio', 'null-co', 'parallels', 'qcow', 'qcow2', 'qed',
+ 'quorum', 'raw', 'replication', 'ssh', 'vdi', 'vhdx', 'vmdk',
+ 'vpc', 'vvfat' ] }
##
# @BlockdevOptionsFile:
@@ -2601,6 +2602,70 @@
'*logfile': 'str' } }
##
+# @IscsiTransport:
+#
+# An enumeration of libiscsi transport types
+#
+# Since: 2.9
+##
+{ 'enum': 'IscsiTransport',
+ 'data': [ 'tcp', 'iser' ] }
+
+##
+# @IscsiHeaderDigest:
+#
+# An enumeration of header digests supported by libiscsi
+#
+# Since: 2.9
+##
+{ 'enum': 'IscsiHeaderDigest',
+ 'prefix': 'QAPI_ISCSI_HEADER_DIGEST',
+ 'data': [ 'crc32c', 'none', 'crc32c-none', 'none-crc32c' ] }
+
+##
+# @BlockdevOptionsIscsi:
+#
+# @transport The iscsi transport type
+#
+# @portal The address of the iscsi portal
+#
+# @target The target iqn name
+#
+# @lun #optional LUN to connect to. Defaults to 0.
+#
+# @user #optional User name to log in with. If omitted, no CHAP
+# authentication is performed.
+#
+# @password-secret #optional The ID of a QCryptoSecret object providing
+# the password for the login. This option is required if
+# @user is specified.
+#
+# @initiator-name #optional The iqn name we want to identify to the target
+# as. If this option is not specified, an initiator name is
+# generated automatically.
+#
+# @header-digest #optional The desired header digest. Defaults to
+# none-crc32c.
+#
+# @timeout #optional Timeout in seconds after which a request will
+# timeout. 0 means no timeout and is the default.
+#
+# Driver specific block device options for iscsi
+#
+# Since: 2.9
+##
+{ 'struct': 'BlockdevOptionsIscsi',
+ 'data': { 'transport': 'IscsiTransport',
+ 'portal': 'str',
+ 'target': 'str',
+ '*lun': 'int',
+ '*user': 'str',
+ '*password-secret': 'str',
+ '*initiator-name': 'str',
+ '*header-digest': 'IscsiHeaderDigest',
+ '*timeout': 'int' } }
+
+##
# @ReplicationMode:
#
# An enumeration of replication modes.
@@ -2786,7 +2851,7 @@
'host_device':'BlockdevOptionsFile',
'http': 'BlockdevOptionsCurl',
'https': 'BlockdevOptionsCurl',
-# TODO iscsi: Wait for structured options
+ 'iscsi': 'BlockdevOptionsIscsi',
'luks': 'BlockdevOptionsLUKS',
'nbd': 'BlockdevOptionsNbd',
'nfs': 'BlockdevOptionsNfs',
@@ -2845,34 +2910,36 @@
# 1.
# -> { "execute": "blockdev-add",
# "arguments": {
-# "options" : { "driver": "qcow2",
-# "file": { "driver": "file",
-# "filename": "test.qcow2" } } } }
+# "driver": "qcow2",
+# "node-name": "test1",
+# "file": {
+# "driver": "file",
+# "filename": "test.qcow2"
+# }
+# }
+# }
# <- { "return": {} }
#
# 2.
# -> { "execute": "blockdev-add",
# "arguments": {
-# "options": {
-# "driver": "qcow2",
-# "node-name": "node0",
-# "discard": "unmap",
-# "cache": {
-# "direct": true,
-# "writeback": true
+# "driver": "qcow2",
+# "node-name": "node0",
+# "discard": "unmap",
+# "cache": {
+# "direct": true
# },
# "file": {
-# "driver": "file",
-# "filename": "/tmp/test.qcow2"
+# "driver": "file",
+# "filename": "/tmp/test.qcow2"
# },
# "backing": {
-# "driver": "raw",
-# "file": {
-# "driver": "file",
-# "filename": "/dev/fdset/4"
+# "driver": "raw",
+# "file": {
+# "driver": "file",
+# "filename": "/dev/fdset/4"
# }
# }
-# }
# }
# }
#
@@ -2900,14 +2967,12 @@
#
# -> { "execute": "blockdev-add",
# "arguments": {
-# "options": {
-# "driver": "qcow2",
-# "node-name": "node0",
-# "file": {
-# "driver": "file",
-# "filename": "test.qcow2"
-# }
-# }
+# "driver": "qcow2",
+# "node-name": "node0",
+# "file": {
+# "driver": "file",
+# "filename": "test.qcow2"
+# }
# }
# }
# <- { "return": {} }
diff --git a/qemu-options.hx b/qemu-options.hx
index 1e815ef..9936cf3 100644
--- a/qemu-options.hx
+++ b/qemu-options.hx
@@ -2586,7 +2586,7 @@ Example
qemu-system-i386 --drive file=sheepdog://192.0.2.1:30000/MyVirtualMachine
@end example
-See also @url{http://http://www.osrg.net/sheepdog/}.
+See also @url{https://sheepdog.github.io/sheepdog/}.
@item GlusterFS
GlusterFS is a user space distributed file system.