diff mbox

[v14,12/21] option: allow qemu_opts_to_qdict to merge repeated options

Message ID 1475246744-29302-13-git-send-email-berrange@redhat.com
State New
Headers show

Commit Message

Daniel P. Berrangé Sept. 30, 2016, 2:45 p.m. UTC
If given an option string such as

  size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind

the qemu_opts_to_qdict() method will currently overwrite
the values for repeated option keys, so only the last
value is in the returned dict:

    size=QString("1024")
    nodes=QString("1-2")
    policy=QString("bind")

With this change the caller can optionally ask for all
the repeated values to be stored in a QList. In the
above example that would result in 'nodes' being a
QList, so the returned dict would contain

    size=QString("1024")
    nodes=QList([QString("10"),
                 QString("4-5"),
                 QString("1-2")])
    policy=QString("bind")

Note that the conversion has no way of knowing whether
any given key is expected to be a list upfront - it can
only figure that out when seeing the first duplicated
key. Thus the caller has to be prepared to deal with the
fact that if a key 'foo' is a list, then the returned
qdict may contain either a QString or a QList for the
key 'foo'.

In a third mode, it is possible to ask for repeated
options to be reported as an error, rather than silently
dropping all but the last one.

All existing callers are all converted to explicitly
request the historical behaviour of only reporting the
last key. Later patches will make use of the new modes.

Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
---
 blockdev.c                   |   7 ++-
 include/qemu/option.h        |  13 ++++-
 monitor.c                    |   3 +-
 qapi/qobject-input-visitor.c |   4 +-
 qemu-img.c                   |   4 +-
 qemu-io-cmds.c               |   3 +-
 qemu-io.c                    |   6 +-
 qemu-nbd.c                   |   3 +-
 qom/object_interfaces.c      |   3 +-
 tests/test-qemu-opts.c       | 132 +++++++++++++++++++++++++++++++++++++++++++
 tests/test-replication.c     |   9 ++-
 util/qemu-option.c           |  64 ++++++++++++++++++---
 12 files changed, 229 insertions(+), 22 deletions(-)

Comments

Eric Blake Sept. 30, 2016, 8:08 p.m. UTC | #1
On 09/30/2016 09:45 AM, Daniel P. Berrange wrote:
> If given an option string such as
> 
>   size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind
> 
> the qemu_opts_to_qdict() method will currently overwrite
> the values for repeated option keys, so only the last
> value is in the returned dict:
> 
>     size=QString("1024")
>     nodes=QString("1-2")
>     policy=QString("bind")
> 
> With this change the caller can optionally ask for all
> the repeated values to be stored in a QList. In the
> above example that would result in 'nodes' being a
> QList, so the returned dict would contain
> 
>     size=QString("1024")
>     nodes=QList([QString("10"),
>                  QString("4-5"),
>                  QString("1-2")])
>     policy=QString("bind")

I think the last time around you were converting keys (turning "nodes"
into "nodes.0"; I think your idea here of keeping a single key tied to
QList makes more sense.

> 
> Note that the conversion has no way of knowing whether
> any given key is expected to be a list upfront - it can
> only figure that out when seeing the first duplicated
> key. Thus the caller has to be prepared to deal with the
> fact that if a key 'foo' is a list, then the returned
> qdict may contain either a QString or a QList for the
> key 'foo'.
> 
> In a third mode, it is possible to ask for repeated
> options to be reported as an error, rather than silently
> dropping all but the last one.
> 
> All existing callers are all converted to explicitly
> request the historical behaviour of only reporting the
> last key. Later patches will make use of the new modes.
> 
> Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
> ---

> -QDict *qemu_opts_to_qdict(const QemuOpts *opts, QDict *qdict);
> +typedef enum {
> +    /* Last occurrence of a duplicate option silently replaces earlier */
> +    QEMU_OPTS_REPEAT_POLICY_LAST,
> +    /* Each occurrence of a duplicate option converts value to a QList */
> +    QEMU_OPTS_REPEAT_POLICY_ALL,
> +    /* First occurrence of a duplicate option causes an error */
> +    QEMU_OPTS_REPEAT_POLICY_ERROR,
> +} QemuOptsRepeatPolicy;
> +

Nicer.  Thanks!

> +++ b/tests/test-qemu-opts.c
> @@ -421,6 +421,130 @@ static void test_qemu_opts_set(void)
>      g_assert(opts == NULL);
>  }
>  

> +static void test_qemu_opts_to_qdict_repeat_all(void)
> +{
> +    QemuOpts *opts;
> +    QDict *dict;
> +    QList *list;
> +    const QListEntry *ent;
> +    QString *str;
> +
> +    /* dynamically initialized (parsed) opts */
> +    opts = qemu_opts_parse(&opts_list_03,
> +                           "size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind",
> +                           false, NULL);
> +    g_assert(opts);
> +
> +    dict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_ALL,
> +                              &error_abort);
> +    g_assert(dict);
> +
> +    g_assert_cmpstr(qdict_get_str(dict, "size"), ==, "1024");
> +    g_assert(qdict_haskey(dict, "nodes"));
> +    list = qobject_to_qlist(qdict_get(dict, "nodes"));
> +    g_assert(list);
> +
> +    ent = qlist_first(list);
> +    g_assert(ent);
> +    str = qobject_to_qstring(ent->value);
> +    g_assert(str);
> +    g_assert_cmpstr(qstring_get_str(str), ==, "10");
> +
> +    ent = qlist_next(ent);
> +    g_assert(ent);
> +    str = qobject_to_qstring(ent->value);
> +    g_assert(str);
> +    g_assert_cmpstr(qstring_get_str(str), ==, "4-5");
> +
> +    ent = qlist_next(ent);
> +    g_assert(ent);
> +    str = qobject_to_qstring(ent->value);
> +    g_assert(str);
> +    g_assert_cmpstr(qstring_get_str(str), ==, "1-2");

Yes, this is different from v13, but does look nicer with the value
turning into QList instead of the key being rewritten into dotted form.

> +++ b/util/qemu-option.c
> @@ -1057,23 +1057,73 @@ void qemu_opts_absorb_qdict(QemuOpts *opts, QDict *qdict, Error **errp)
>   * The QDict values are of type QString.
>   * TODO We'll want to use types appropriate for opt->desc->type, but
>   * this is enough for now.
> + *
> + * If the @opts contains multiple occurrences of the same key,
> + * then the @repeatPolicy parameter determines how they are to
> + * be handled. Traditional behaviour was to only store the
> + * last occurrence, but if @repeatPolicy is set to
> + * QEMU_OPTS_REPEAT_POLICY_ALL, then a QList will be returned
> + * containing all values, for any key with multiple occurrences.
> + * The QEMU_OPTS_REPEAT_POLICY_ERROR value can be used to fail
> + * conversion with an error if multiple occurrences of a key
> + * are seen.
>   */
> -QDict *qemu_opts_to_qdict(const QemuOpts *opts, QDict *qdict)
> +QDict *qemu_opts_to_qdict(const QemuOpts *opts, QDict *qdict,
> +                          QemuOptsRepeatPolicy repeatPolicy,
> +                          Error **errp)

Reviewed-by: Eric Blake <eblake@redhat.com>
Markus Armbruster Oct. 12, 2016, 5:46 p.m. UTC | #2
"Daniel P. Berrange" <berrange@redhat.com> writes:

> If given an option string such as
>
>   size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind
>
> the qemu_opts_to_qdict() method will currently overwrite
> the values for repeated option keys, so only the last
> value is in the returned dict:
>
>     size=QString("1024")
>     nodes=QString("1-2")
>     policy=QString("bind")
>
> With this change the caller can optionally ask for all
> the repeated values to be stored in a QList. In the
> above example that would result in 'nodes' being a
> QList, so the returned dict would contain
>
>     size=QString("1024")
>     nodes=QList([QString("10"),
>                  QString("4-5"),
>                  QString("1-2")])
>     policy=QString("bind")
>
> Note that the conversion has no way of knowing whether
> any given key is expected to be a list upfront - it can
> only figure that out when seeing the first duplicated
> key. Thus the caller has to be prepared to deal with the
> fact that if a key 'foo' is a list, then the returned
> qdict may contain either a QString or a QList for the
> key 'foo'.
>
> In a third mode, it is possible to ask for repeated
> options to be reported as an error, rather than silently
> dropping all but the last one.

To serve as a replacement for the options visitor, this needs to be able
to behave exactly the same together with a suitably hacked up QObject
input visitor.  Before I dive into the actual patch, let me summarize
QemuOpts and options visitor behavior.

Warning, this is going to get ugly.

QemuOpts faithfully represents a key=value,... string as a list of
QemuOpt.  Each QemuOpt represents one key=value.  They are in the same
order.  If key occurs multiple times in the string, it occurs just the
same in the list.

*Except* key "id" is special: it's stored outside the list, and all but
the first one are silently ignored.

Most users only ever get the last value of a key.  Any non-last
key=value are silently ignored.

We actually exploit this behavior to do defaults, by *prepending* them
to the list.  See the use of qemu_opts_set_defaults() in main().

A few users get all values of keys (other than key "id"):

* -device, in qdev_device_add() with callback set_property().

  We first get "driver" and "bus" normally (silently ignoring non-last
  values, as usual).  All other keys are device properties.  To set
  them, we get all (key, value), ignore keys "driver" and "bus", and set
  the rest.  If a key occurs multiple times, it gets set multiple times.
  This effectively ignores all but the last one, silently.

* -semihosting-config, in main() with callback add_semihosting_arg().

  We first get a bunch of keys normally.  Key "arg" is special: it may
  be repeated to build a list.  To implement that, we get all (key,
  value), ignore keys other than "arg", and accumulate the values.

* -machine & friends, in main() with callback machine_set_property()

  Similar to -device, only for machines, with "type" instead of "driver"
  and "bus".

* -spice, in qemu_spice_init() with callback add_channel()

  Keys "tls-channel" and "plaintext-channel" may be used repeated to
  specify multiple channels.  To implement that, we get all (key,
  value), ignore keys other than "tls-channel" and "plaintext-channel",
  and set up a channel for each of the others.

* -writeconfig, in config_write_opts() with callback config_write_opt()

  We write out all keys in order.

* The options visitor, in opts_start_struct()

  We convert the list of (key, value) to a hash table of (key, list of
  values).  Most of the time, the list of values has exactly one
  element.

  When the visitor's user asks for a scalar, we return the last element
  of the list of values, in lookup_scalar().

  When the user asks for list elements, we return the elements of the
  list of values in order, in opts_next_list(), or if there are none,
  the empty list in opts_start_list().

Unlike the options visitor, this patch (judging from your description)
makes a list only when keys are repeated.  The QObject visitor will have
to cope with finding both scalars and lists.  When it finds a scalar,
but needs a list, it'll have to wrap it in a list (PATCH 09, I think).
When it finds a list, but needs a scalar, it'll have to fish it out of
the list (where is that?).

> All existing callers are all converted to explicitly
> request the historical behaviour of only reporting the
> last key. Later patches will make use of the new modes.
>
> Signed-off-by: Daniel P. Berrange <berrange@redhat.com>

Out of steam for today.
Markus Armbruster Oct. 13, 2016, 8:31 a.m. UTC | #3
"Daniel P. Berrange" <berrange@redhat.com> writes:

> If given an option string such as
>
>   size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind
>
> the qemu_opts_to_qdict() method will currently overwrite
> the values for repeated option keys, so only the last
> value is in the returned dict:
>
>     size=QString("1024")
>     nodes=QString("1-2")
>     policy=QString("bind")
>
> With this change the caller can optionally ask for all
> the repeated values to be stored in a QList. In the
> above example that would result in 'nodes' being a
> QList, so the returned dict would contain
>
>     size=QString("1024")
>     nodes=QList([QString("10"),
>                  QString("4-5"),
>                  QString("1-2")])
>     policy=QString("bind")
>
> Note that the conversion has no way of knowing whether
> any given key is expected to be a list upfront - it can
> only figure that out when seeing the first duplicated
> key. Thus the caller has to be prepared to deal with the
> fact that if a key 'foo' is a list, then the returned
> qdict may contain either a QString or a QList for the
> key 'foo'.

Actually three cases, not two:

0. qdict does not contain the key means empty list.

1. qdict contains the key with a QString value means list of one
element.

2. qdict contains the key with a QList value means list of more than one
element.

I consider this weird.  However, it's usefully weird with at least your
QObject input visitor.

> In a third mode, it is possible to ask for repeated
> options to be reported as an error, rather than silently
> dropping all but the last one.

Got users for this policy in the pipeline?

> All existing callers are all converted to explicitly
> request the historical behaviour of only reporting the
> last key. Later patches will make use of the new modes.
>
> Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
> ---
>  blockdev.c                   |   7 ++-
>  include/qemu/option.h        |  13 ++++-
>  monitor.c                    |   3 +-
>  qapi/qobject-input-visitor.c |   4 +-
>  qemu-img.c                   |   4 +-
>  qemu-io-cmds.c               |   3 +-
>  qemu-io.c                    |   6 +-
>  qemu-nbd.c                   |   3 +-
>  qom/object_interfaces.c      |   3 +-
>  tests/test-qemu-opts.c       | 132 +++++++++++++++++++++++++++++++++++++++++++
>  tests/test-replication.c     |   9 ++-
>  util/qemu-option.c           |  64 ++++++++++++++++++---
>  12 files changed, 229 insertions(+), 22 deletions(-)
>
> diff --git a/blockdev.c b/blockdev.c
> index 814d49f..a999d46 100644
> --- a/blockdev.c
> +++ b/blockdev.c
> @@ -914,7 +914,8 @@ DriveInfo *drive_new(QemuOpts *all_opts, BlockInterfaceType block_default_type)
>  
>      /* Get a QDict for processing the options */
>      bs_opts = qdict_new();
> -    qemu_opts_to_qdict(all_opts, bs_opts);
> +    qemu_opts_to_qdict(all_opts, bs_opts, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                       &error_abort);
>  
>      legacy_opts = qemu_opts_create(&qemu_legacy_drive_opts, NULL, 0,
>                                     &error_abort);
> @@ -3804,8 +3805,8 @@ void hmp_drive_add_node(Monitor *mon, const char *optstr)
>          return;
>      }
>  
> -    qdict = qemu_opts_to_qdict(opts, NULL);
> -
> +    qdict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                               &error_abort);
>      if (!qdict_get_try_str(qdict, "node-name")) {
>          QDECREF(qdict);
>          error_report("'node-name' needs to be specified");
> diff --git a/include/qemu/option.h b/include/qemu/option.h
> index 29f3f18..ffd841d 100644
> --- a/include/qemu/option.h
> +++ b/include/qemu/option.h
> @@ -125,7 +125,18 @@ void qemu_opts_set_defaults(QemuOptsList *list, const char *params,
>                              int permit_abbrev);
>  QemuOpts *qemu_opts_from_qdict(QemuOptsList *list, const QDict *qdict,
>                                 Error **errp);
> -QDict *qemu_opts_to_qdict(const QemuOpts *opts, QDict *qdict);

Blank line here, please.

> +typedef enum {
> +    /* Last occurrence of a duplicate option silently replaces earlier */
> +    QEMU_OPTS_REPEAT_POLICY_LAST,
> +    /* Each occurrence of a duplicate option converts value to a QList */
> +    QEMU_OPTS_REPEAT_POLICY_ALL,
> +    /* First occurrence of a duplicate option causes an error */
> +    QEMU_OPTS_REPEAT_POLICY_ERROR,
> +} QemuOptsRepeatPolicy;
> +
> +QDict *qemu_opts_to_qdict(const QemuOpts *opts, QDict *qdict,
> +                          QemuOptsRepeatPolicy repeatPolicy,
> +                          Error **errp);
>  void qemu_opts_absorb_qdict(QemuOpts *opts, QDict *qdict, Error **errp);
>  
>  typedef int (*qemu_opts_loopfunc)(void *opaque, QemuOpts *opts, Error **errp);
> diff --git a/monitor.c b/monitor.c
> index 14089cc..84e79d4 100644
> --- a/monitor.c
> +++ b/monitor.c
> @@ -2642,7 +2642,8 @@ static QDict *monitor_parse_arguments(Monitor *mon,
>                  if (!opts) {
>                      goto fail;
>                  }
> -                qemu_opts_to_qdict(opts, qdict);
> +                qemu_opts_to_qdict(opts, qdict, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                                   &error_abort);
>                  qemu_opts_del(opts);
>              }
>              break;
> diff --git a/qapi/qobject-input-visitor.c b/qapi/qobject-input-visitor.c
> index 6b3d0f2..2287d11 100644
> --- a/qapi/qobject-input-visitor.c
> +++ b/qapi/qobject-input-visitor.c
> @@ -759,7 +759,9 @@ Visitor *qobject_input_visitor_new_opts(const QemuOpts *opts,
>      QObject *pobj = NULL;
>      Visitor *v = NULL;
>  
> -    pdict = qemu_opts_to_qdict(opts, NULL);
> +    pdict = qemu_opts_to_qdict(opts, NULL,
> +                               QEMU_OPTS_REPEAT_POLICY_LAST,

Join the previous two lines, please.

> +                               errp);

Unlike the other callers, this one doesn't pass &error_abort.  The error
handling is actually dead code.  Let's pass &error_abort and be done
with it.

>      if (!pdict) {
>          goto cleanup;
>      }
> diff --git a/qemu-img.c b/qemu-img.c
> index 851422a..f47ea75 100644
> --- a/qemu-img.c
> +++ b/qemu-img.c
> @@ -273,7 +273,9 @@ static BlockBackend *img_open_opts(const char *optstr,
>      QDict *options;
>      Error *local_err = NULL;
>      BlockBackend *blk;
> -    options = qemu_opts_to_qdict(opts, NULL);
> +    options = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                                 &error_abort);
> +
>      blk = blk_new_open(NULL, NULL, options, flags, &local_err);
>      if (!blk) {
>          error_reportf_err(local_err, "Could not open '%s': ", optstr);
> diff --git a/qemu-io-cmds.c b/qemu-io-cmds.c
> index 3a3838a..ce654f4 100644
> --- a/qemu-io-cmds.c
> +++ b/qemu-io-cmds.c
> @@ -1952,7 +1952,8 @@ static int reopen_f(BlockBackend *blk, int argc, char **argv)
>      }
>  
>      qopts = qemu_opts_find(&reopen_opts, NULL);
> -    opts = qopts ? qemu_opts_to_qdict(qopts, NULL) : NULL;
> +    opts = qopts ? qemu_opts_to_qdict(qopts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                                      &error_abort) : NULL;

Short ternaries are more legible than if statements, but this one is
pushing the limit now.

>      qemu_opts_reset(&reopen_opts);
>  
>      brq = bdrv_reopen_queue(NULL, bs, opts, flags);
> diff --git a/qemu-io.c b/qemu-io.c
> index db129ea..bb374a6 100644
> --- a/qemu-io.c
> +++ b/qemu-io.c
> @@ -207,7 +207,8 @@ static int open_f(BlockBackend *blk, int argc, char **argv)
>      }
>  
>      qopts = qemu_opts_find(&empty_opts, NULL);
> -    opts = qopts ? qemu_opts_to_qdict(qopts, NULL) : NULL;
> +    opts = qopts ? qemu_opts_to_qdict(qopts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                                      &error_abort) : NULL;

Likewise.

>      qemu_opts_reset(&empty_opts);
>  
>      if (optind == argc - 1) {
> @@ -593,7 +594,8 @@ int main(int argc, char **argv)
>              if (!qopts) {
>                  exit(1);
>              }
> -            opts = qemu_opts_to_qdict(qopts, NULL);
> +            opts = qemu_opts_to_qdict(qopts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                                      &error_abort);

Even this line's getting ugly.  Can we find a prefix that's less
loquacious than QEMU_OPTS_REPEAT_POLICY_, but still self-explaining?

>              openfile(NULL, flags, writethrough, opts);
>          } else {
>              if (format) {
> diff --git a/qemu-nbd.c b/qemu-nbd.c
> index 99297a5..73b40b1 100644
> --- a/qemu-nbd.c
> +++ b/qemu-nbd.c
> @@ -859,7 +859,8 @@ int main(int argc, char **argv)
>              qemu_opts_reset(&file_opts);
>              exit(EXIT_FAILURE);
>          }
> -        options = qemu_opts_to_qdict(opts, NULL);
> +        options = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                                     &error_abort);
>          qemu_opts_reset(&file_opts);
>          blk = blk_new_open(NULL, NULL, options, flags, &local_err);
>      } else {
> diff --git a/qom/object_interfaces.c b/qom/object_interfaces.c
> index ded4d84..fdc406b 100644
> --- a/qom/object_interfaces.c
> +++ b/qom/object_interfaces.c
> @@ -161,7 +161,8 @@ Object *user_creatable_add_opts(QemuOpts *opts, Error **errp)
>      Object *obj = NULL;
>  
>      v = opts_visitor_new(opts);
> -    pdict = qemu_opts_to_qdict(opts, NULL);
> +    pdict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                               &error_abort);
>  
>      obj = user_creatable_add(pdict, v, errp);
>      visit_free(v);
> diff --git a/tests/test-qemu-opts.c b/tests/test-qemu-opts.c
> index a505a3e..3b59978 100644
> --- a/tests/test-qemu-opts.c
> +++ b/tests/test-qemu-opts.c
> @@ -421,6 +421,130 @@ static void test_qemu_opts_set(void)
>      g_assert(opts == NULL);
>  }
>  
> +
> +static void test_qemu_opts_to_qdict_repeat_last(void)
> +{
> +    QemuOpts *opts;
> +    QDict *dict;
> +
> +    /* dynamically initialized (parsed) opts */

What are you trying to say with this comment?  Hmm, you merely copied it
from the existing tests.  I'd drop the existing instances of this
comment instead.

> +    opts = qemu_opts_parse(&opts_list_03,
> +                           "size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind",
> +                           false, NULL);
> +    g_assert(opts);

You could pass &error_abort.

> +
> +    dict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                              &error_abort);
> +    g_assert(dict);
> +
> +

One blank line, not two, please.

> +    g_assert_cmpstr(qdict_get_str(dict, "size"), ==, "1024");
> +    g_assert_cmpstr(qdict_get_str(dict, "nodes"), ==, "1-2");
> +    g_assert(!qdict_haskey(dict, "nodes.0"));
> +    g_assert(!qdict_haskey(dict, "nodes.1"));
> +    g_assert(!qdict_haskey(dict, "nodes.2"));
> +    g_assert_cmpstr(qdict_get_str(dict, "policy"), ==, "bind");

Checking for absence of "nodes.0"... feels odd.  A better idea is to
check we got exactly the keys we expect, and no more.  Here's a simple
way to do that:

       g_assert(qdict_size(dict) == 3);

> +    QDECREF(dict);
> +
> +    qemu_opts_del(opts);
> +    qemu_opts_reset(&opts_list_03);
> +}
> +
> +
> +static void test_qemu_opts_to_qdict_repeat_all(void)
> +{
> +    QemuOpts *opts;
> +    QDict *dict;
> +    QList *list;
> +    const QListEntry *ent;
> +    QString *str;
> +
> +    /* dynamically initialized (parsed) opts */
> +    opts = qemu_opts_parse(&opts_list_03,
> +                           "size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind",
> +                           false, NULL);
> +    g_assert(opts);
> +
> +    dict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_ALL,
> +                              &error_abort);
> +    g_assert(dict);
> +
> +    g_assert_cmpstr(qdict_get_str(dict, "size"), ==, "1024");
> +    g_assert(qdict_haskey(dict, "nodes"));
> +    list = qobject_to_qlist(qdict_get(dict, "nodes"));
> +    g_assert(list);

Works, but I'd do

       obj = qdict_get(dict, "nodes");
       g_assert(obj);
       list = qobject_to_qlist(obj);
       g_assert(list);

> +
> +    ent = qlist_first(list);
> +    g_assert(ent);
> +    str = qobject_to_qstring(ent->value);
> +    g_assert(str);
> +    g_assert_cmpstr(qstring_get_str(str), ==, "10");

Since g_assert_cmpstr() does the right thing when @str is null,
g_assert(str) is redundant.  More of the same below.

> +
> +    ent = qlist_next(ent);
> +    g_assert(ent);
> +    str = qobject_to_qstring(ent->value);
> +    g_assert(str);
> +    g_assert_cmpstr(qstring_get_str(str), ==, "4-5");
> +
> +    ent = qlist_next(ent);
> +    g_assert(ent);
> +    str = qobject_to_qstring(ent->value);
> +    g_assert(str);
> +    g_assert_cmpstr(qstring_get_str(str), ==, "1-2");
> +
> +    ent = qlist_next(ent);
> +    g_assert(!ent);
> +
> +    g_assert_cmpstr(qdict_get_str(dict, "policy"), ==, "bind");

       g_assert(qdict_size(dict) == 3);

> +    QDECREF(dict);
> +
> +    qemu_opts_del(opts);
> +    qemu_opts_reset(&opts_list_03);
> +}
> +
> +static void test_qemu_opts_to_qdict_repeat_err_fail(void)
> +{
> +    QemuOpts *opts;
> +    QDict *dict;
> +    Error *err = NULL;
> +
> +    /* dynamically initialized (parsed) opts */
> +    opts = qemu_opts_parse(&opts_list_03,
> +                           "size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind",
> +                           false, NULL);
> +    g_assert(opts);
> +
> +    dict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_ERROR,
> +                              &err);
> +    error_free_or_abort(&err);
> +    g_assert(!dict);
> +
> +    qemu_opts_del(opts);
> +    qemu_opts_reset(&opts_list_03);
> +}
> +
> +static void test_qemu_opts_to_qdict_repeat_err_ok(void)
> +{
> +    QemuOpts *opts;
> +    QDict *dict;
> +
> +    /* dynamically initialized (parsed) opts */
> +    opts = qemu_opts_parse(&opts_list_03,
> +                           "size=1024,nodes=10,policy=bind",
> +                           false, NULL);
> +    g_assert(opts);
> +
> +    dict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_ERROR,
> +                              &error_abort);
> +    g_assert_cmpstr(qdict_get_str(dict, "size"), ==, "1024");
> +    g_assert_cmpstr(qdict_get_str(dict, "nodes"), ==, "10");
> +    g_assert_cmpstr(qdict_get_str(dict, "policy"), ==, "bind");

       g_assert(qdict_size(dict) == 3);

> +
> +    QDECREF(dict);
> +    qemu_opts_del(opts);
> +    qemu_opts_reset(&opts_list_03);
> +}
> +
>  int main(int argc, char *argv[])
>  {
>      register_opts();
> @@ -435,6 +559,14 @@ int main(int argc, char *argv[])
>      g_test_add_func("/qemu-opts/opt_unset", test_qemu_opt_unset);
>      g_test_add_func("/qemu-opts/opts_reset", test_qemu_opts_reset);
>      g_test_add_func("/qemu-opts/opts_set", test_qemu_opts_set);
> +    g_test_add_func("/qemu-opts/to_qdict/repeat-last",
> +                    test_qemu_opts_to_qdict_repeat_last);
> +    g_test_add_func("/qemu-opts/to_qdict/repeat-all",
> +                    test_qemu_opts_to_qdict_repeat_all);
> +    g_test_add_func("/qemu-opts/to_qdict/repeat-err-fail",
> +                    test_qemu_opts_to_qdict_repeat_err_fail);
> +    g_test_add_func("/qemu-opts/to_qdict/repeat-err-ok",
> +                    test_qemu_opts_to_qdict_repeat_err_ok);
>      g_test_run();
>      return 0;
>  }
> diff --git a/tests/test-replication.c b/tests/test-replication.c
> index 0997bd8..e267f9a 100644
> --- a/tests/test-replication.c
> +++ b/tests/test-replication.c
> @@ -181,7 +181,8 @@ static BlockBackend *start_primary(void)
>      opts = qemu_opts_parse_noisily(&qemu_drive_opts, cmdline, false);
>      g_free(cmdline);
>  
> -    qdict = qemu_opts_to_qdict(opts, NULL);
> +    qdict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                               &error_abort);
>      qdict_set_default_str(qdict, BDRV_OPT_CACHE_DIRECT, "off");
>      qdict_set_default_str(qdict, BDRV_OPT_CACHE_NO_FLUSH, "off");
>  
> @@ -311,7 +312,8 @@ static BlockBackend *start_secondary(void)
>      opts = qemu_opts_parse_noisily(&qemu_drive_opts, cmdline, false);
>      g_free(cmdline);
>  
> -    qdict = qemu_opts_to_qdict(opts, NULL);
> +    qdict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                               &error_abort);
>      qdict_set_default_str(qdict, BDRV_OPT_CACHE_DIRECT, "off");
>      qdict_set_default_str(qdict, BDRV_OPT_CACHE_NO_FLUSH, "off");
>  
> @@ -336,7 +338,8 @@ static BlockBackend *start_secondary(void)
>      opts = qemu_opts_parse_noisily(&qemu_drive_opts, cmdline, false);
>      g_free(cmdline);
>  
> -    qdict = qemu_opts_to_qdict(opts, NULL);
> +    qdict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
> +                               &error_abort);
>      qdict_set_default_str(qdict, BDRV_OPT_CACHE_DIRECT, "off");
>      qdict_set_default_str(qdict, BDRV_OPT_CACHE_NO_FLUSH, "off");
>  
> diff --git a/util/qemu-option.c b/util/qemu-option.c
> index 418cbb6..0129ede 100644
> --- a/util/qemu-option.c
> +++ b/util/qemu-option.c
> @@ -1057,23 +1057,73 @@ void qemu_opts_absorb_qdict(QemuOpts *opts, QDict *qdict, Error **errp)
>   * The QDict values are of type QString.
>   * TODO We'll want to use types appropriate for opt->desc->type, but
>   * this is enough for now.

While there, we could document @obj and @dict properly.  But see below
on @dict.

> + *
> + * If the @opts contains multiple occurrences of the same key,
> + * then the @repeatPolicy parameter determines how they are to
> + * be handled. Traditional behaviour was to only store the
> + * last occurrence, but if @repeatPolicy is set to
> + * QEMU_OPTS_REPEAT_POLICY_ALL, then a QList will be returned
> + * containing all values, for any key with multiple occurrences.
> + * The QEMU_OPTS_REPEAT_POLICY_ERROR value can be used to fail
> + * conversion with an error if multiple occurrences of a key
> + * are seen.

What the traditional behavior was is quite interesting in a commit
message.  Less so in a comment.  Suggest:

    * be handled.  With @repeatPolicy QEMU_OPTS_REPEAT_POLICY_LAST,
    * all values but the last are silently ignored.  With
    * QEMU_OPTS_REPEAT_POLICY_ALL, a QList will be returned
    * containing all values, for any key with multiple occurrences.
    * With QEMU_OPTS_REPEAT_POLICY_ERROR, the conversion fails.

Let's spell out that the function can only fail with
QEMU_OPTS_REPEAT_POLICY_ERROR:

    * This is the only way this function can fail.

>   */
> -QDict *qemu_opts_to_qdict(const QemuOpts *opts, QDict *qdict)
> +QDict *qemu_opts_to_qdict(const QemuOpts *opts, QDict *qdict,
> +                          QemuOptsRepeatPolicy repeatPolicy,
> +                          Error **errp)
>  {
>      QemuOpt *opt;
> -    QObject *val;
> +    QObject *val, *prevval;

I'd prefer prev_val.

> +    QList *list;
> +    QDict *ret;
>  
> -    if (!qdict) {
> -        qdict = qdict_new();
> +    if (qdict) {
> +        ret = qdict;
> +    } else {
> +        ret = qdict_new();
>      }

You need this change to get the reference counting on failure right.
This makes me question the qdict parameter.  I can see two non-null
arguments for it outside tests.  The one in drive_new() is basically
stupid:

    bs_opts = qdict_new();
    qemu_opts_to_qdict(all_opts, bs_opts, QEMU_OPTS_REPEAT_POLICY_LAST,
                       &error_abort);

Easily replaced by

    bs_opts = qemu_opts_to_qdict(all_opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
                                 &error_abort);

The one in monitor_parse_arguments()

                qemu_opts_to_qdict(opts, qdict, QEMU_OPTS_REPEAT_POLICY_LAST,
                                   &error_abort);

could be replaced by

                QDECREF(qdict);
                qdict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
                                   &error_abort);

at a negligible loss of efficiency.  We could then drop the parameter,
simplifying the function's contract.

Let's insert a blank line here...

>      if (opts->id) {
> -        qdict_put(qdict, "id", qstring_from_str(opts->id));
> +        qdict_put(ret, "id", qstring_from_str(opts->id));
>      }

... and here.

>      QTAILQ_FOREACH(opt, &opts->head, next) {
>          val = QOBJECT(qstring_from_str(opt->str));
> -        qdict_put_obj(qdict, opt->name, val);
> +
> +        if (qdict_haskey(ret, opt->name)) {
> +            switch (repeatPolicy) {
> +            case QEMU_OPTS_REPEAT_POLICY_ERROR:
> +                error_setg(errp, "Option '%s' appears more than once",
> +                           opt->name);
> +                qobject_decref(val);
> +                if (!qdict) {
> +                    QDECREF(ret);
> +                }
> +                return NULL;
> +
> +            case QEMU_OPTS_REPEAT_POLICY_ALL:
> +                prevval = qdict_get(ret, opt->name);
> +                if (qobject_type(prevval) == QTYPE_QLIST) {
> +                    /* Already identified this key as a list */
> +                    list = qobject_to_qlist(prevval);
> +                } else {
> +                    /* Replace current scalar with a list */
> +                    list = qlist_new();
> +                    qobject_incref(prevval);

Where is this reference decremented?

> +                    qlist_append_obj(list, prevval);
> +                    qdict_put_obj(ret, opt->name, QOBJECT(list));
> +                }
> +                qlist_append_obj(list, val);
> +                break;
> +
> +            case QEMU_OPTS_REPEAT_POLICY_LAST:
> +                /* Just discard previously set value */
> +                qdict_put_obj(ret, opt->name, val);
> +                break;
> +            }
> +        } else {
> +            qdict_put_obj(ret, opt->name, val);
> +        }

A possible alternative:

           val = QOBJECT(qstring_from_str(opt->str));

           if (qdict_haskey(ret, opt->name)) {
               switch (repeatPolicy) {
               case QEMU_OPTS_REPEAT_POLICY_ERROR:
                   error_setg(errp, "Option '%s' appears more than once",
                              opt->name);
                   qobject_decref(val);
                   if (!qdict) {
                       QDECREF(ret);
                   }
                   return NULL;

               case QEMU_OPTS_REPEAT_POLICY_ALL:
                   prevval = qdict_get(ret, opt->name);
                   if (qobject_type(prevval) == QTYPE_QLIST) {
                       /* Already identified this key as a list */
                       list = qobject_to_qlist(prevval);
                   } else {
                       /* Replace current scalar with a list */
                       list = qlist_new();
                       qobject_incref(prevval);
                       qlist_append_obj(list, prevval);
                   }
                   qlist_append_obj(list, val);
                   val = QOBJECT(list);
                   break;

               case QEMU_OPTS_REPEAT_POLICY_LAST:
                   break;
               }
           }
           qdict_put_obj(ret, opt->name, val);

This shows the common part of the behavior more clearly.  Matter of
taste, you get to use your artistic license.

>      }
> -    return qdict;
> +    return ret;
>  }
>  
>  /* Validate parsed opts against descriptions where no
Markus Armbruster Oct. 13, 2016, 9:21 a.m. UTC | #4
Markus Armbruster <armbru@redhat.com> writes:

> "Daniel P. Berrange" <berrange@redhat.com> writes:
>
>> If given an option string such as
>>
>>   size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind
>>
>> the qemu_opts_to_qdict() method will currently overwrite
>> the values for repeated option keys, so only the last
>> value is in the returned dict:
>>
>>     size=QString("1024")
>>     nodes=QString("1-2")
>>     policy=QString("bind")
>>
>> With this change the caller can optionally ask for all
>> the repeated values to be stored in a QList. In the
>> above example that would result in 'nodes' being a
>> QList, so the returned dict would contain
>>
>>     size=QString("1024")
>>     nodes=QList([QString("10"),
>>                  QString("4-5"),
>>                  QString("1-2")])
>>     policy=QString("bind")
>>
>> Note that the conversion has no way of knowing whether
>> any given key is expected to be a list upfront - it can
>> only figure that out when seeing the first duplicated
>> key. Thus the caller has to be prepared to deal with the
>> fact that if a key 'foo' is a list, then the returned
>> qdict may contain either a QString or a QList for the
>> key 'foo'.
>>
>> In a third mode, it is possible to ask for repeated
>> options to be reported as an error, rather than silently
>> dropping all but the last one.
>
> To serve as a replacement for the options visitor, this needs to be able
> to behave exactly the same together with a suitably hacked up QObject
> input visitor.  Before I dive into the actual patch, let me summarize
> QemuOpts and options visitor behavior.
>
> Warning, this is going to get ugly.
>
> QemuOpts faithfully represents a key=value,... string as a list of
> QemuOpt.  Each QemuOpt represents one key=value.  They are in the same
> order.  If key occurs multiple times in the string, it occurs just the
> same in the list.
>
> *Except* key "id" is special: it's stored outside the list, and all but
> the first one are silently ignored.
>
> Most users only ever get the last value of a key.  Any non-last
> key=value are silently ignored.
>
> We actually exploit this behavior to do defaults, by *prepending* them
> to the list.  See the use of qemu_opts_set_defaults() in main().

This prepending of defaults assumes all users ignore values other than
the last.  It breaks if any user gets non-last values.

> A few users get all values of keys (other than key "id"):
>
> * -device, in qdev_device_add() with callback set_property().
>
>   We first get "driver" and "bus" normally (silently ignoring non-last
>   values, as usual).  All other keys are device properties.  To set
>   them, we get all (key, value), ignore keys "driver" and "bus", and set
>   the rest.  If a key occurs multiple times, it gets set multiple times.
>   This effectively ignores all but the last one, silently.
>
> * -semihosting-config, in main() with callback add_semihosting_arg().
>
>   We first get a bunch of keys normally.  Key "arg" is special: it may
>   be repeated to build a list.  To implement that, we get all (key,
>   value), ignore keys other than "arg", and accumulate the values.
>
> * -machine & friends, in main() with callback machine_set_property()
>
>   Similar to -device, only for machines, with "type" instead of "driver"
>   and "bus".
>
> * -spice, in qemu_spice_init() with callback add_channel()
>
>   Keys "tls-channel" and "plaintext-channel" may be used repeated to
>   specify multiple channels.  To implement that, we get all (key,
>   value), ignore keys other than "tls-channel" and "plaintext-channel",
>   and set up a channel for each of the others.
>
> * -writeconfig, in config_write_opts() with callback config_write_opt()
>
>   We write out all keys in order.
>
> * The options visitor, in opts_start_struct()
>
>   We convert the list of (key, value) to a hash table of (key, list of
>   values).  Most of the time, the list of values has exactly one
>   element.
>
>   When the visitor's user asks for a scalar, we return the last element
>   of the list of values, in lookup_scalar().
>
>   When the user asks for list elements, we return the elements of the
>   list of values in order, in opts_next_list(), or if there are none,
>   the empty list in opts_start_list().

Note that the only way to get non-last values is to iterate over all
(key, value).  The combination of "getting a specific key's value gets
the last one" and "iterating over all keys gets all values" is poor
interface design.  The latter feature got pressed into service to do
list-valued keys.  When qemu_opts_set_defaults() got added (commit
4f6dd9a), the bad interaction with the list-valued keys hack wasn't
considered, probably because the whole thing had become too byzantine to
fully understand.

> Unlike the options visitor, this patch (judging from your description)
> makes a list only when keys are repeated.  The QObject visitor will have
> to cope with finding both scalars and lists.  When it finds a scalar,
> but needs a list, it'll have to wrap it in a list (PATCH 09, I think).
> When it finds a list, but needs a scalar, it'll have to fish it out of
> the list (where is that?).
>
>> All existing callers are all converted to explicitly
>> request the historical behaviour of only reporting the
>> last key. Later patches will make use of the new modes.
>>
>> Signed-off-by: Daniel P. Berrange <berrange@redhat.com>
>
> Out of steam for today.
Daniel P. Berrangé Oct. 20, 2016, 2:29 p.m. UTC | #5
On Wed, Oct 12, 2016 at 07:46:00PM +0200, Markus Armbruster wrote:
> "Daniel P. Berrange" <berrange@redhat.com> writes:
> 
> > If given an option string such as
> >
> >   size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind
> >
> > the qemu_opts_to_qdict() method will currently overwrite
> > the values for repeated option keys, so only the last
> > value is in the returned dict:
> >
> >     size=QString("1024")
> >     nodes=QString("1-2")
> >     policy=QString("bind")
> >
> > With this change the caller can optionally ask for all
> > the repeated values to be stored in a QList. In the
> > above example that would result in 'nodes' being a
> > QList, so the returned dict would contain
> >
> >     size=QString("1024")
> >     nodes=QList([QString("10"),
> >                  QString("4-5"),
> >                  QString("1-2")])
> >     policy=QString("bind")
> >
> > Note that the conversion has no way of knowing whether
> > any given key is expected to be a list upfront - it can
> > only figure that out when seeing the first duplicated
> > key. Thus the caller has to be prepared to deal with the
> > fact that if a key 'foo' is a list, then the returned
> > qdict may contain either a QString or a QList for the
> > key 'foo'.
> >
> > In a third mode, it is possible to ask for repeated
> > options to be reported as an error, rather than silently
> > dropping all but the last one.
> 
> To serve as a replacement for the options visitor, this needs to be able
> to behave exactly the same together with a suitably hacked up QObject
> input visitor.  Before I dive into the actual patch, let me summarize
> QemuOpts and options visitor behavior.
> 
> Warning, this is going to get ugly.
> 
> QemuOpts faithfully represents a key=value,... string as a list of
> QemuOpt.  Each QemuOpt represents one key=value.  They are in the same
> order.  If key occurs multiple times in the string, it occurs just the
> same in the list.
> 
> *Except* key "id" is special: it's stored outside the list, and all but
> the first one are silently ignored.
> 
> Most users only ever get the last value of a key.  Any non-last
> key=value are silently ignored.
> 
> We actually exploit this behavior to do defaults, by *prepending* them
> to the list.  See the use of qemu_opts_set_defaults() in main().
> 
> A few users get all values of keys (other than key "id"):
> 
> * -device, in qdev_device_add() with callback set_property().
> 
>   We first get "driver" and "bus" normally (silently ignoring non-last
>   values, as usual).  All other keys are device properties.  To set
>   them, we get all (key, value), ignore keys "driver" and "bus", and set
>   the rest.  If a key occurs multiple times, it gets set multiple times.
>   This effectively ignores all but the last one, silently.
> 
> * -semihosting-config, in main() with callback add_semihosting_arg().
> 
>   We first get a bunch of keys normally.  Key "arg" is special: it may
>   be repeated to build a list.  To implement that, we get all (key,
>   value), ignore keys other than "arg", and accumulate the values.
> 
> * -machine & friends, in main() with callback machine_set_property()
> 
>   Similar to -device, only for machines, with "type" instead of "driver"
>   and "bus".
> 
> * -spice, in qemu_spice_init() with callback add_channel()
> 
>   Keys "tls-channel" and "plaintext-channel" may be used repeated to
>   specify multiple channels.  To implement that, we get all (key,
>   value), ignore keys other than "tls-channel" and "plaintext-channel",
>   and set up a channel for each of the others.
> 
> * -writeconfig, in config_write_opts() with callback config_write_opt()
> 
>   We write out all keys in order.
> 
> * The options visitor, in opts_start_struct()
> 
>   We convert the list of (key, value) to a hash table of (key, list of
>   values).  Most of the time, the list of values has exactly one
>   element.
> 
>   When the visitor's user asks for a scalar, we return the last element
>   of the list of values, in lookup_scalar().
> 
>   When the user asks for list elements, we return the elements of the
>   list of values in order, in opts_next_list(), or if there are none,
>   the empty list in opts_start_list().
> 
> Unlike the options visitor, this patch (judging from your description)
> makes a list only when keys are repeated.  The QObject visitor will have
> to cope with finding both scalars and lists.  When it finds a scalar,
> but needs a list, it'll have to wrap it in a list (PATCH 09, I think).
> When it finds a list, but needs a scalar, it'll have to fish it out of
> the list (where is that?).

If my code finds a list but wants a scalar, it is reporting an
error.

Regards,
Daniel
Daniel P. Berrangé Oct. 20, 2016, 2:37 p.m. UTC | #6
On Thu, Oct 13, 2016 at 10:31:37AM +0200, Markus Armbruster wrote:
> "Daniel P. Berrange" <berrange@redhat.com> writes:
> 
> > If given an option string such as
> >
> >   size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind
> >
> > the qemu_opts_to_qdict() method will currently overwrite
> > the values for repeated option keys, so only the last
> > value is in the returned dict:
> >
> >     size=QString("1024")
> >     nodes=QString("1-2")
> >     policy=QString("bind")
> >
> > With this change the caller can optionally ask for all
> > the repeated values to be stored in a QList. In the
> > above example that would result in 'nodes' being a
> > QList, so the returned dict would contain
> >
> >     size=QString("1024")
> >     nodes=QList([QString("10"),
> >                  QString("4-5"),
> >                  QString("1-2")])
> >     policy=QString("bind")
> >
> > Note that the conversion has no way of knowing whether
> > any given key is expected to be a list upfront - it can
> > only figure that out when seeing the first duplicated
> > key. Thus the caller has to be prepared to deal with the
> > fact that if a key 'foo' is a list, then the returned
> > qdict may contain either a QString or a QList for the
> > key 'foo'.
> 
> Actually three cases, not two:
> 
> 0. qdict does not contain the key means empty list.
> 
> 1. qdict contains the key with a QString value means list of one
> element.
> 
> 2. qdict contains the key with a QList value means list of more than one
> element.
> 
> I consider this weird.  However, it's usefully weird with at least your
> QObject input visitor.
> 
> > In a third mode, it is possible to ask for repeated
> > options to be reported as an error, rather than silently
> > dropping all but the last one.
> 
> Got users for this policy in the pipeline?

I in fact used it in the QObjectInputVisitor, when the
autocreate_list is not set.

I guess strictly speaking this is not back-compatible
if someone is passing repeated keys, but I judged that
rather than silently ignoring this incorrect usage, it
was acceptable to report an error.




> >      QTAILQ_FOREACH(opt, &opts->head, next) {
> >          val = QOBJECT(qstring_from_str(opt->str));
> > -        qdict_put_obj(qdict, opt->name, val);
> > +
> > +        if (qdict_haskey(ret, opt->name)) {
> > +            switch (repeatPolicy) {
> > +            case QEMU_OPTS_REPEAT_POLICY_ERROR:
> > +                error_setg(errp, "Option '%s' appears more than once",
> > +                           opt->name);
> > +                qobject_decref(val);
> > +                if (!qdict) {
> > +                    QDECREF(ret);
> > +                }
> > +                return NULL;
> > +
> > +            case QEMU_OPTS_REPEAT_POLICY_ALL:
> > +                prevval = qdict_get(ret, opt->name);
> > +                if (qobject_type(prevval) == QTYPE_QLIST) {
> > +                    /* Already identified this key as a list */
> > +                    list = qobject_to_qlist(prevval);
> > +                } else {
> > +                    /* Replace current scalar with a list */
> > +                    list = qlist_new();
> > +                    qobject_incref(prevval);
> 
> Where is this reference decremented?

'prevval' is a borrowed reference from 'ret', against the
key opt->name.

qdict_put_obj decrements the reference we borrowed
from ret against the key opt->name. 

qlist_append_obj() takes ownership of the reference
it is passed, so we must qobject_incref() to avoid
qdict_put_obj free'ing prevval.

When we call qdict_put_obj() we're replacing the value
currently associted

> 
> > +                    qlist_append_obj(list, prevval);
> > +                    qdict_put_obj(ret, opt->name, QOBJECT(list));
> > +                }
> > +                qlist_append_obj(list, val);
> > +                break;
> > +
> > +            case QEMU_OPTS_REPEAT_POLICY_LAST:
> > +                /* Just discard previously set value */
> > +                qdict_put_obj(ret, opt->name, val);
> > +                break;
> > +            }
> > +        } else {
> > +            qdict_put_obj(ret, opt->name, val);
> > +        }
> 
> A possible alternative:
> 
>            val = QOBJECT(qstring_from_str(opt->str));
> 
>            if (qdict_haskey(ret, opt->name)) {
>                switch (repeatPolicy) {
>                case QEMU_OPTS_REPEAT_POLICY_ERROR:
>                    error_setg(errp, "Option '%s' appears more than once",
>                               opt->name);
>                    qobject_decref(val);
>                    if (!qdict) {
>                        QDECREF(ret);
>                    }
>                    return NULL;
> 
>                case QEMU_OPTS_REPEAT_POLICY_ALL:
>                    prevval = qdict_get(ret, opt->name);
>                    if (qobject_type(prevval) == QTYPE_QLIST) {
>                        /* Already identified this key as a list */
>                        list = qobject_to_qlist(prevval);
>                    } else {
>                        /* Replace current scalar with a list */
>                        list = qlist_new();
>                        qobject_incref(prevval);
>                        qlist_append_obj(list, prevval);
>                    }
>                    qlist_append_obj(list, val);
>                    val = QOBJECT(list);
>                    break;
> 
>                case QEMU_OPTS_REPEAT_POLICY_LAST:
>                    break;
>                }
>            }
>            qdict_put_obj(ret, opt->name, val);
> 
> This shows the common part of the behavior more clearly.  Matter of
> taste, you get to use your artistic license.


Regards,
Daniel
Markus Armbruster Oct. 21, 2016, 11:09 a.m. UTC | #7
"Daniel P. Berrange" <berrange@redhat.com> writes:

> On Wed, Oct 12, 2016 at 07:46:00PM +0200, Markus Armbruster wrote:
>> "Daniel P. Berrange" <berrange@redhat.com> writes:
>> 
>> > If given an option string such as
>> >
>> >   size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind
>> >
>> > the qemu_opts_to_qdict() method will currently overwrite
>> > the values for repeated option keys, so only the last
>> > value is in the returned dict:
>> >
>> >     size=QString("1024")
>> >     nodes=QString("1-2")
>> >     policy=QString("bind")
>> >
>> > With this change the caller can optionally ask for all
>> > the repeated values to be stored in a QList. In the
>> > above example that would result in 'nodes' being a
>> > QList, so the returned dict would contain
>> >
>> >     size=QString("1024")
>> >     nodes=QList([QString("10"),
>> >                  QString("4-5"),
>> >                  QString("1-2")])
>> >     policy=QString("bind")
>> >
>> > Note that the conversion has no way of knowing whether
>> > any given key is expected to be a list upfront - it can
>> > only figure that out when seeing the first duplicated
>> > key. Thus the caller has to be prepared to deal with the
>> > fact that if a key 'foo' is a list, then the returned
>> > qdict may contain either a QString or a QList for the
>> > key 'foo'.
>> >
>> > In a third mode, it is possible to ask for repeated
>> > options to be reported as an error, rather than silently
>> > dropping all but the last one.
>> 
>> To serve as a replacement for the options visitor, this needs to be able
>> to behave exactly the same together with a suitably hacked up QObject
>> input visitor.  Before I dive into the actual patch, let me summarize
>> QemuOpts and options visitor behavior.
[...]
>> Unlike the options visitor, this patch (judging from your description)
>> makes a list only when keys are repeated.  The QObject visitor will have
>> to cope with finding both scalars and lists.  When it finds a scalar,
>> but needs a list, it'll have to wrap it in a list (PATCH 09, I think).
>> When it finds a list, but needs a scalar, it'll have to fish it out of
>> the list (where is that?).
>
> If my code finds a list but wants a scalar, it is reporting an
> error.

I'm afraid that breaks things like

    --numa node,cpus=0,cpus=2,mem=512,nodeid=0,nodeid=0

The options visitor inteprets cpus=0,cpus=2 as a list [0,2] (because
NumaNodeOptions member @cpus has a list type) and nodeid=0,nodid=0 as
integer 0 (because member @nodeid has a scalar type).
Daniel P. Berrangé Oct. 21, 2016, 11:14 a.m. UTC | #8
On Fri, Oct 21, 2016 at 01:09:07PM +0200, Markus Armbruster wrote:
> "Daniel P. Berrange" <berrange@redhat.com> writes:
> 
> > On Wed, Oct 12, 2016 at 07:46:00PM +0200, Markus Armbruster wrote:
> >> "Daniel P. Berrange" <berrange@redhat.com> writes:
> >> 
> >> > If given an option string such as
> >> >
> >> >   size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind
> >> >
> >> > the qemu_opts_to_qdict() method will currently overwrite
> >> > the values for repeated option keys, so only the last
> >> > value is in the returned dict:
> >> >
> >> >     size=QString("1024")
> >> >     nodes=QString("1-2")
> >> >     policy=QString("bind")
> >> >
> >> > With this change the caller can optionally ask for all
> >> > the repeated values to be stored in a QList. In the
> >> > above example that would result in 'nodes' being a
> >> > QList, so the returned dict would contain
> >> >
> >> >     size=QString("1024")
> >> >     nodes=QList([QString("10"),
> >> >                  QString("4-5"),
> >> >                  QString("1-2")])
> >> >     policy=QString("bind")
> >> >
> >> > Note that the conversion has no way of knowing whether
> >> > any given key is expected to be a list upfront - it can
> >> > only figure that out when seeing the first duplicated
> >> > key. Thus the caller has to be prepared to deal with the
> >> > fact that if a key 'foo' is a list, then the returned
> >> > qdict may contain either a QString or a QList for the
> >> > key 'foo'.
> >> >
> >> > In a third mode, it is possible to ask for repeated
> >> > options to be reported as an error, rather than silently
> >> > dropping all but the last one.
> >> 
> >> To serve as a replacement for the options visitor, this needs to be able
> >> to behave exactly the same together with a suitably hacked up QObject
> >> input visitor.  Before I dive into the actual patch, let me summarize
> >> QemuOpts and options visitor behavior.
> [...]
> >> Unlike the options visitor, this patch (judging from your description)
> >> makes a list only when keys are repeated.  The QObject visitor will have
> >> to cope with finding both scalars and lists.  When it finds a scalar,
> >> but needs a list, it'll have to wrap it in a list (PATCH 09, I think).
> >> When it finds a list, but needs a scalar, it'll have to fish it out of
> >> the list (where is that?).
> >
> > If my code finds a list but wants a scalar, it is reporting an
> > error.
> 
> I'm afraid that breaks things like
> 
>     --numa node,cpus=0,cpus=2,mem=512,nodeid=0,nodeid=0
> 
> The options visitor inteprets cpus=0,cpus=2 as a list [0,2] (because
> NumaNodeOptions member @cpus has a list type) and nodeid=0,nodid=0 as
> integer 0 (because member @nodeid has a scalar type).

Right, but that command line syntax is arguably broken already - the
app/user invoking QEMU is buggy, so I felt it was justifiable to
change from silently ignoring the app error to telling them of their
mistake.

Regards,
Daniel
Markus Armbruster Oct. 21, 2016, 11:28 a.m. UTC | #9
"Daniel P. Berrange" <berrange@redhat.com> writes:

> On Thu, Oct 13, 2016 at 10:31:37AM +0200, Markus Armbruster wrote:
>> "Daniel P. Berrange" <berrange@redhat.com> writes:
>> 
>> > If given an option string such as
>> >
>> >   size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind
>> >
>> > the qemu_opts_to_qdict() method will currently overwrite
>> > the values for repeated option keys, so only the last
>> > value is in the returned dict:
>> >
>> >     size=QString("1024")
>> >     nodes=QString("1-2")
>> >     policy=QString("bind")
>> >
>> > With this change the caller can optionally ask for all
>> > the repeated values to be stored in a QList. In the
>> > above example that would result in 'nodes' being a
>> > QList, so the returned dict would contain
>> >
>> >     size=QString("1024")
>> >     nodes=QList([QString("10"),
>> >                  QString("4-5"),
>> >                  QString("1-2")])
>> >     policy=QString("bind")
>> >
>> > Note that the conversion has no way of knowing whether
>> > any given key is expected to be a list upfront - it can
>> > only figure that out when seeing the first duplicated
>> > key. Thus the caller has to be prepared to deal with the
>> > fact that if a key 'foo' is a list, then the returned
>> > qdict may contain either a QString or a QList for the
>> > key 'foo'.
>> 
>> Actually three cases, not two:
>> 
>> 0. qdict does not contain the key means empty list.
>> 
>> 1. qdict contains the key with a QString value means list of one
>> element.
>> 
>> 2. qdict contains the key with a QList value means list of more than one
>> element.
>> 
>> I consider this weird.  However, it's usefully weird with at least your
>> QObject input visitor.
>> 
>> > In a third mode, it is possible to ask for repeated
>> > options to be reported as an error, rather than silently
>> > dropping all but the last one.
>> 
>> Got users for this policy in the pipeline?
>
> I in fact used it in the QObjectInputVisitor, when the
> autocreate_list is not set.
>
> I guess strictly speaking this is not back-compatible
> if someone is passing repeated keys, but I judged that
> rather than silently ignoring this incorrect usage, it
> was acceptable to report an error.

While usage like

    -machine usb=on,usb=off

could perhaps be declared erroneous post hoc, the "last one wins"
feature also has genuinely useful applications.  For instance, tack a
correction to a long command line:

    -machine usb=on [... lots of other stuff ...] -machine usb=off

Or modify canned configuration:

    -readconfig vm1.config -machine usb=off
    # vm1.config sets usb=on, -machine overrides

    -readconfig vm1.config -set drive.disk0.file=tmp.qcow2
    # vm1.config defines drive "disk0", -set overrides its image file

I don't think we can break them all.

[...]
diff mbox

Patch

diff --git a/blockdev.c b/blockdev.c
index 814d49f..a999d46 100644
--- a/blockdev.c
+++ b/blockdev.c
@@ -914,7 +914,8 @@  DriveInfo *drive_new(QemuOpts *all_opts, BlockInterfaceType block_default_type)
 
     /* Get a QDict for processing the options */
     bs_opts = qdict_new();
-    qemu_opts_to_qdict(all_opts, bs_opts);
+    qemu_opts_to_qdict(all_opts, bs_opts, QEMU_OPTS_REPEAT_POLICY_LAST,
+                       &error_abort);
 
     legacy_opts = qemu_opts_create(&qemu_legacy_drive_opts, NULL, 0,
                                    &error_abort);
@@ -3804,8 +3805,8 @@  void hmp_drive_add_node(Monitor *mon, const char *optstr)
         return;
     }
 
-    qdict = qemu_opts_to_qdict(opts, NULL);
-
+    qdict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
+                               &error_abort);
     if (!qdict_get_try_str(qdict, "node-name")) {
         QDECREF(qdict);
         error_report("'node-name' needs to be specified");
diff --git a/include/qemu/option.h b/include/qemu/option.h
index 29f3f18..ffd841d 100644
--- a/include/qemu/option.h
+++ b/include/qemu/option.h
@@ -125,7 +125,18 @@  void qemu_opts_set_defaults(QemuOptsList *list, const char *params,
                             int permit_abbrev);
 QemuOpts *qemu_opts_from_qdict(QemuOptsList *list, const QDict *qdict,
                                Error **errp);
-QDict *qemu_opts_to_qdict(const QemuOpts *opts, QDict *qdict);
+typedef enum {
+    /* Last occurrence of a duplicate option silently replaces earlier */
+    QEMU_OPTS_REPEAT_POLICY_LAST,
+    /* Each occurrence of a duplicate option converts value to a QList */
+    QEMU_OPTS_REPEAT_POLICY_ALL,
+    /* First occurrence of a duplicate option causes an error */
+    QEMU_OPTS_REPEAT_POLICY_ERROR,
+} QemuOptsRepeatPolicy;
+
+QDict *qemu_opts_to_qdict(const QemuOpts *opts, QDict *qdict,
+                          QemuOptsRepeatPolicy repeatPolicy,
+                          Error **errp);
 void qemu_opts_absorb_qdict(QemuOpts *opts, QDict *qdict, Error **errp);
 
 typedef int (*qemu_opts_loopfunc)(void *opaque, QemuOpts *opts, Error **errp);
diff --git a/monitor.c b/monitor.c
index 14089cc..84e79d4 100644
--- a/monitor.c
+++ b/monitor.c
@@ -2642,7 +2642,8 @@  static QDict *monitor_parse_arguments(Monitor *mon,
                 if (!opts) {
                     goto fail;
                 }
-                qemu_opts_to_qdict(opts, qdict);
+                qemu_opts_to_qdict(opts, qdict, QEMU_OPTS_REPEAT_POLICY_LAST,
+                                   &error_abort);
                 qemu_opts_del(opts);
             }
             break;
diff --git a/qapi/qobject-input-visitor.c b/qapi/qobject-input-visitor.c
index 6b3d0f2..2287d11 100644
--- a/qapi/qobject-input-visitor.c
+++ b/qapi/qobject-input-visitor.c
@@ -759,7 +759,9 @@  Visitor *qobject_input_visitor_new_opts(const QemuOpts *opts,
     QObject *pobj = NULL;
     Visitor *v = NULL;
 
-    pdict = qemu_opts_to_qdict(opts, NULL);
+    pdict = qemu_opts_to_qdict(opts, NULL,
+                               QEMU_OPTS_REPEAT_POLICY_LAST,
+                               errp);
     if (!pdict) {
         goto cleanup;
     }
diff --git a/qemu-img.c b/qemu-img.c
index 851422a..f47ea75 100644
--- a/qemu-img.c
+++ b/qemu-img.c
@@ -273,7 +273,9 @@  static BlockBackend *img_open_opts(const char *optstr,
     QDict *options;
     Error *local_err = NULL;
     BlockBackend *blk;
-    options = qemu_opts_to_qdict(opts, NULL);
+    options = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
+                                 &error_abort);
+
     blk = blk_new_open(NULL, NULL, options, flags, &local_err);
     if (!blk) {
         error_reportf_err(local_err, "Could not open '%s': ", optstr);
diff --git a/qemu-io-cmds.c b/qemu-io-cmds.c
index 3a3838a..ce654f4 100644
--- a/qemu-io-cmds.c
+++ b/qemu-io-cmds.c
@@ -1952,7 +1952,8 @@  static int reopen_f(BlockBackend *blk, int argc, char **argv)
     }
 
     qopts = qemu_opts_find(&reopen_opts, NULL);
-    opts = qopts ? qemu_opts_to_qdict(qopts, NULL) : NULL;
+    opts = qopts ? qemu_opts_to_qdict(qopts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
+                                      &error_abort) : NULL;
     qemu_opts_reset(&reopen_opts);
 
     brq = bdrv_reopen_queue(NULL, bs, opts, flags);
diff --git a/qemu-io.c b/qemu-io.c
index db129ea..bb374a6 100644
--- a/qemu-io.c
+++ b/qemu-io.c
@@ -207,7 +207,8 @@  static int open_f(BlockBackend *blk, int argc, char **argv)
     }
 
     qopts = qemu_opts_find(&empty_opts, NULL);
-    opts = qopts ? qemu_opts_to_qdict(qopts, NULL) : NULL;
+    opts = qopts ? qemu_opts_to_qdict(qopts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
+                                      &error_abort) : NULL;
     qemu_opts_reset(&empty_opts);
 
     if (optind == argc - 1) {
@@ -593,7 +594,8 @@  int main(int argc, char **argv)
             if (!qopts) {
                 exit(1);
             }
-            opts = qemu_opts_to_qdict(qopts, NULL);
+            opts = qemu_opts_to_qdict(qopts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
+                                      &error_abort);
             openfile(NULL, flags, writethrough, opts);
         } else {
             if (format) {
diff --git a/qemu-nbd.c b/qemu-nbd.c
index 99297a5..73b40b1 100644
--- a/qemu-nbd.c
+++ b/qemu-nbd.c
@@ -859,7 +859,8 @@  int main(int argc, char **argv)
             qemu_opts_reset(&file_opts);
             exit(EXIT_FAILURE);
         }
-        options = qemu_opts_to_qdict(opts, NULL);
+        options = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
+                                     &error_abort);
         qemu_opts_reset(&file_opts);
         blk = blk_new_open(NULL, NULL, options, flags, &local_err);
     } else {
diff --git a/qom/object_interfaces.c b/qom/object_interfaces.c
index ded4d84..fdc406b 100644
--- a/qom/object_interfaces.c
+++ b/qom/object_interfaces.c
@@ -161,7 +161,8 @@  Object *user_creatable_add_opts(QemuOpts *opts, Error **errp)
     Object *obj = NULL;
 
     v = opts_visitor_new(opts);
-    pdict = qemu_opts_to_qdict(opts, NULL);
+    pdict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
+                               &error_abort);
 
     obj = user_creatable_add(pdict, v, errp);
     visit_free(v);
diff --git a/tests/test-qemu-opts.c b/tests/test-qemu-opts.c
index a505a3e..3b59978 100644
--- a/tests/test-qemu-opts.c
+++ b/tests/test-qemu-opts.c
@@ -421,6 +421,130 @@  static void test_qemu_opts_set(void)
     g_assert(opts == NULL);
 }
 
+
+static void test_qemu_opts_to_qdict_repeat_last(void)
+{
+    QemuOpts *opts;
+    QDict *dict;
+
+    /* dynamically initialized (parsed) opts */
+    opts = qemu_opts_parse(&opts_list_03,
+                           "size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind",
+                           false, NULL);
+    g_assert(opts);
+
+    dict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
+                              &error_abort);
+    g_assert(dict);
+
+
+    g_assert_cmpstr(qdict_get_str(dict, "size"), ==, "1024");
+    g_assert_cmpstr(qdict_get_str(dict, "nodes"), ==, "1-2");
+    g_assert(!qdict_haskey(dict, "nodes.0"));
+    g_assert(!qdict_haskey(dict, "nodes.1"));
+    g_assert(!qdict_haskey(dict, "nodes.2"));
+    g_assert_cmpstr(qdict_get_str(dict, "policy"), ==, "bind");
+    QDECREF(dict);
+
+    qemu_opts_del(opts);
+    qemu_opts_reset(&opts_list_03);
+}
+
+
+static void test_qemu_opts_to_qdict_repeat_all(void)
+{
+    QemuOpts *opts;
+    QDict *dict;
+    QList *list;
+    const QListEntry *ent;
+    QString *str;
+
+    /* dynamically initialized (parsed) opts */
+    opts = qemu_opts_parse(&opts_list_03,
+                           "size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind",
+                           false, NULL);
+    g_assert(opts);
+
+    dict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_ALL,
+                              &error_abort);
+    g_assert(dict);
+
+    g_assert_cmpstr(qdict_get_str(dict, "size"), ==, "1024");
+    g_assert(qdict_haskey(dict, "nodes"));
+    list = qobject_to_qlist(qdict_get(dict, "nodes"));
+    g_assert(list);
+
+    ent = qlist_first(list);
+    g_assert(ent);
+    str = qobject_to_qstring(ent->value);
+    g_assert(str);
+    g_assert_cmpstr(qstring_get_str(str), ==, "10");
+
+    ent = qlist_next(ent);
+    g_assert(ent);
+    str = qobject_to_qstring(ent->value);
+    g_assert(str);
+    g_assert_cmpstr(qstring_get_str(str), ==, "4-5");
+
+    ent = qlist_next(ent);
+    g_assert(ent);
+    str = qobject_to_qstring(ent->value);
+    g_assert(str);
+    g_assert_cmpstr(qstring_get_str(str), ==, "1-2");
+
+    ent = qlist_next(ent);
+    g_assert(!ent);
+
+    g_assert_cmpstr(qdict_get_str(dict, "policy"), ==, "bind");
+    QDECREF(dict);
+
+    qemu_opts_del(opts);
+    qemu_opts_reset(&opts_list_03);
+}
+
+static void test_qemu_opts_to_qdict_repeat_err_fail(void)
+{
+    QemuOpts *opts;
+    QDict *dict;
+    Error *err = NULL;
+
+    /* dynamically initialized (parsed) opts */
+    opts = qemu_opts_parse(&opts_list_03,
+                           "size=1024,nodes=10,nodes=4-5,nodes=1-2,policy=bind",
+                           false, NULL);
+    g_assert(opts);
+
+    dict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_ERROR,
+                              &err);
+    error_free_or_abort(&err);
+    g_assert(!dict);
+
+    qemu_opts_del(opts);
+    qemu_opts_reset(&opts_list_03);
+}
+
+static void test_qemu_opts_to_qdict_repeat_err_ok(void)
+{
+    QemuOpts *opts;
+    QDict *dict;
+
+    /* dynamically initialized (parsed) opts */
+    opts = qemu_opts_parse(&opts_list_03,
+                           "size=1024,nodes=10,policy=bind",
+                           false, NULL);
+    g_assert(opts);
+
+    dict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_ERROR,
+                              &error_abort);
+    g_assert_cmpstr(qdict_get_str(dict, "size"), ==, "1024");
+    g_assert_cmpstr(qdict_get_str(dict, "nodes"), ==, "10");
+    g_assert_cmpstr(qdict_get_str(dict, "policy"), ==, "bind");
+
+    QDECREF(dict);
+    qemu_opts_del(opts);
+    qemu_opts_reset(&opts_list_03);
+}
+
 int main(int argc, char *argv[])
 {
     register_opts();
@@ -435,6 +559,14 @@  int main(int argc, char *argv[])
     g_test_add_func("/qemu-opts/opt_unset", test_qemu_opt_unset);
     g_test_add_func("/qemu-opts/opts_reset", test_qemu_opts_reset);
     g_test_add_func("/qemu-opts/opts_set", test_qemu_opts_set);
+    g_test_add_func("/qemu-opts/to_qdict/repeat-last",
+                    test_qemu_opts_to_qdict_repeat_last);
+    g_test_add_func("/qemu-opts/to_qdict/repeat-all",
+                    test_qemu_opts_to_qdict_repeat_all);
+    g_test_add_func("/qemu-opts/to_qdict/repeat-err-fail",
+                    test_qemu_opts_to_qdict_repeat_err_fail);
+    g_test_add_func("/qemu-opts/to_qdict/repeat-err-ok",
+                    test_qemu_opts_to_qdict_repeat_err_ok);
     g_test_run();
     return 0;
 }
diff --git a/tests/test-replication.c b/tests/test-replication.c
index 0997bd8..e267f9a 100644
--- a/tests/test-replication.c
+++ b/tests/test-replication.c
@@ -181,7 +181,8 @@  static BlockBackend *start_primary(void)
     opts = qemu_opts_parse_noisily(&qemu_drive_opts, cmdline, false);
     g_free(cmdline);
 
-    qdict = qemu_opts_to_qdict(opts, NULL);
+    qdict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
+                               &error_abort);
     qdict_set_default_str(qdict, BDRV_OPT_CACHE_DIRECT, "off");
     qdict_set_default_str(qdict, BDRV_OPT_CACHE_NO_FLUSH, "off");
 
@@ -311,7 +312,8 @@  static BlockBackend *start_secondary(void)
     opts = qemu_opts_parse_noisily(&qemu_drive_opts, cmdline, false);
     g_free(cmdline);
 
-    qdict = qemu_opts_to_qdict(opts, NULL);
+    qdict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
+                               &error_abort);
     qdict_set_default_str(qdict, BDRV_OPT_CACHE_DIRECT, "off");
     qdict_set_default_str(qdict, BDRV_OPT_CACHE_NO_FLUSH, "off");
 
@@ -336,7 +338,8 @@  static BlockBackend *start_secondary(void)
     opts = qemu_opts_parse_noisily(&qemu_drive_opts, cmdline, false);
     g_free(cmdline);
 
-    qdict = qemu_opts_to_qdict(opts, NULL);
+    qdict = qemu_opts_to_qdict(opts, NULL, QEMU_OPTS_REPEAT_POLICY_LAST,
+                               &error_abort);
     qdict_set_default_str(qdict, BDRV_OPT_CACHE_DIRECT, "off");
     qdict_set_default_str(qdict, BDRV_OPT_CACHE_NO_FLUSH, "off");
 
diff --git a/util/qemu-option.c b/util/qemu-option.c
index 418cbb6..0129ede 100644
--- a/util/qemu-option.c
+++ b/util/qemu-option.c
@@ -1057,23 +1057,73 @@  void qemu_opts_absorb_qdict(QemuOpts *opts, QDict *qdict, Error **errp)
  * The QDict values are of type QString.
  * TODO We'll want to use types appropriate for opt->desc->type, but
  * this is enough for now.
+ *
+ * If the @opts contains multiple occurrences of the same key,
+ * then the @repeatPolicy parameter determines how they are to
+ * be handled. Traditional behaviour was to only store the
+ * last occurrence, but if @repeatPolicy is set to
+ * QEMU_OPTS_REPEAT_POLICY_ALL, then a QList will be returned
+ * containing all values, for any key with multiple occurrences.
+ * The QEMU_OPTS_REPEAT_POLICY_ERROR value can be used to fail
+ * conversion with an error if multiple occurrences of a key
+ * are seen.
  */
-QDict *qemu_opts_to_qdict(const QemuOpts *opts, QDict *qdict)
+QDict *qemu_opts_to_qdict(const QemuOpts *opts, QDict *qdict,
+                          QemuOptsRepeatPolicy repeatPolicy,
+                          Error **errp)
 {
     QemuOpt *opt;
-    QObject *val;
+    QObject *val, *prevval;
+    QList *list;
+    QDict *ret;
 
-    if (!qdict) {
-        qdict = qdict_new();
+    if (qdict) {
+        ret = qdict;
+    } else {
+        ret = qdict_new();
     }
     if (opts->id) {
-        qdict_put(qdict, "id", qstring_from_str(opts->id));
+        qdict_put(ret, "id", qstring_from_str(opts->id));
     }
     QTAILQ_FOREACH(opt, &opts->head, next) {
         val = QOBJECT(qstring_from_str(opt->str));
-        qdict_put_obj(qdict, opt->name, val);
+
+        if (qdict_haskey(ret, opt->name)) {
+            switch (repeatPolicy) {
+            case QEMU_OPTS_REPEAT_POLICY_ERROR:
+                error_setg(errp, "Option '%s' appears more than once",
+                           opt->name);
+                qobject_decref(val);
+                if (!qdict) {
+                    QDECREF(ret);
+                }
+                return NULL;
+
+            case QEMU_OPTS_REPEAT_POLICY_ALL:
+                prevval = qdict_get(ret, opt->name);
+                if (qobject_type(prevval) == QTYPE_QLIST) {
+                    /* Already identified this key as a list */
+                    list = qobject_to_qlist(prevval);
+                } else {
+                    /* Replace current scalar with a list */
+                    list = qlist_new();
+                    qobject_incref(prevval);
+                    qlist_append_obj(list, prevval);
+                    qdict_put_obj(ret, opt->name, QOBJECT(list));
+                }
+                qlist_append_obj(list, val);
+                break;
+
+            case QEMU_OPTS_REPEAT_POLICY_LAST:
+                /* Just discard previously set value */
+                qdict_put_obj(ret, opt->name, val);
+                break;
+            }
+        } else {
+            qdict_put_obj(ret, opt->name, val);
+        }
     }
-    return qdict;
+    return ret;
 }
 
 /* Validate parsed opts against descriptions where no