This is an automated email from the ASF dual-hosted git repository.

kgiusti pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/qpid-dispatch.git


The following commit(s) were added to refs/heads/master by this push:
     new a1e59d2  DISPATCH-1656: Add openProperties attribute to listener and 
connector
a1e59d2 is described below

commit a1e59d27e00d4902935421e6788a2d65d55f3ce8
Author: Kenneth Giusti <kgiu...@apache.org>
AuthorDate: Tue May 19 15:21:25 2020 -0400

    DISPATCH-1656: Add openProperties attribute to listener and connector
    
    This closes #746
---
 include/qpid/dispatch/compose.h                    |   9 +
 include/qpid/dispatch/proton_utils.h               |  10 +
 include/qpid/dispatch/python_embedded.h            |   9 +
 include/qpid/dispatch/server.h                     |   5 +
 python/qpid_dispatch/management/qdrouter.json      |  12 +
 python/qpid_dispatch_internal/management/agent.py  |  43 ++-
 python/qpid_dispatch_internal/management/config.py | 132 ++++++--
 python/qpid_dispatch_internal/management/schema.py |  22 ++
 src/compose.c                                      |  14 +
 src/connection_manager.c                           |   3 +-
 src/entity.c                                       |  35 ++
 src/entity.h                                       |   4 +
 src/proton_utils.c                                 | 147 +++++++++
 src/python_embedded.c                              | 117 +++++++
 src/server.c                                       |  26 ++
 tests/CMakeLists.txt                               |   1 +
 tests/system_test.py                               |  62 +++-
 tests/system_tests_open_properties.py              | 362 +++++++++++++++++++++
 tests/system_tests_policy.py                       |  94 +++---
 tests/system_tests_policy_oversize_basic.py        |  29 +-
 tests/system_tests_policy_oversize_compound.py     |  28 +-
 21 files changed, 1049 insertions(+), 115 deletions(-)

diff --git a/include/qpid/dispatch/compose.h b/include/qpid/dispatch/compose.h
index 78bb8d2..9abadae 100644
--- a/include/qpid/dispatch/compose.h
+++ b/include/qpid/dispatch/compose.h
@@ -254,6 +254,15 @@ void qd_compose_insert_opaque_elements(qd_composed_field_t 
*field,
                                        uint32_t             count,
                                        uint32_t             size);
 
+/**
+ * Insert a double floating point (64 bits) into the field.
+ *
+ * @param field A field created by qd_compose.
+ * @param value The double value to be inserted.
+ */
+void qd_compose_insert_double(qd_composed_field_t *field, double value);
+
+
 ///@}
 
 #endif
diff --git a/include/qpid/dispatch/proton_utils.h 
b/include/qpid/dispatch/proton_utils.h
index e128d5a..bb69dd4 100644
--- a/include/qpid/dispatch/proton_utils.h
+++ b/include/qpid/dispatch/proton_utils.h
@@ -30,5 +30,15 @@
  */
 char *qdpn_data_as_string(pn_data_t *data);
 
+
+/**
+ * Copy the data node at src to dest.  Both src and dest are advanced after 
this call.
+ *
+ * @param src A proton data field
+ * @param dest A proton data field to which src will be added
+ * @return 0 on success
+ */
+int qdpn_data_insert(pn_data_t *dest, pn_data_t *src);
+
 #endif
 
diff --git a/include/qpid/dispatch/python_embedded.h 
b/include/qpid/dispatch/python_embedded.h
index 4221832..c690fa1 100644
--- a/include/qpid/dispatch/python_embedded.h
+++ b/include/qpid/dispatch/python_embedded.h
@@ -24,6 +24,7 @@
  */
 
 #include <Python.h>
+#include <proton/codec.h>
 #include <qpid/dispatch/dispatch.h>
 #include <qpid/dispatch/compose.h>
 #include <qpid/dispatch/parse.h>
@@ -77,6 +78,14 @@ qd_error_t qd_py_to_composed(PyObject *value, 
qd_composed_field_t *field);
 PyObject *qd_field_to_py(qd_parsed_field_t *field);
 
 /**
+ * Convert a Python object to a proton pn_data_t object
+ *
+ * @param value A Python Object
+ * @param data A proton pn_data_t object
+ */
+qd_error_t qd_py_to_pn_data(PyObject *value, pn_data_t *data);
+
+/**
  * These are temporary and will eventually be replaced by having an internal 
python
  * work queue that feeds a dedicated embedded-python thread.
  */
diff --git a/include/qpid/dispatch/server.h b/include/qpid/dispatch/server.h
index 98df6f7..aa8fcc4 100644
--- a/include/qpid/dispatch/server.h
+++ b/include/qpid/dispatch/server.h
@@ -434,6 +434,11 @@ typedef struct qd_server_config_t {
     qd_failover_list_t *failover_list;
 
     /**
+     * Extra connection properties to include in the outgoing Open frame.  
Stored as a map.
+     */
+    pn_data_t *conn_props;
+
+    /**
      * @name These fields are not primary configuration, they are computed.
      * @{
      */
diff --git a/python/qpid_dispatch/management/qdrouter.json 
b/python/qpid_dispatch/management/qdrouter.json
index 688bcc4..d1ce29a 100644
--- a/python/qpid_dispatch/management/qdrouter.json
+++ b/python/qpid_dispatch/management/qdrouter.json
@@ -911,6 +911,12 @@
                     "required": false,
                     "description": "A listener may optionally define a virtual 
host to index to a specific policy to restrict the remote container to access 
only specific resources. This attribute defines the name of the policy vhost 
for this listener.  If multi-tenancy is enabled for the listener, this vhost 
will override the peer-supplied vhost for the purposes of identifying the 
desired policy settings for the connections.",
                     "create": true
+                },
+                "openProperties": {
+                    "description": "A JSON map containing connection 
properties.  These will be sent to the peer on connection open.  All map keys 
are restricted to strings containing only valid ASCII characters, Keys must not 
start with prefixes 'qd.' or 'x-opt-qd.'.  The following key values are also 
reserved: 'product', 'version', 'failover-server-list', 'network-host', 'port', 
'scheme' 'hostname'",
+                    "type": "properties",
+                    "required": false,
+                    "create": true
                 }
             }
         },
@@ -1061,6 +1067,12 @@
                     "required": false,
                     "description": "A connector may optionally define a policy 
to restrict the remote container to access only specific resources. This 
attribute defines the name of the policy vhost for this connector. Within the 
vhost the connector will use the vhost policy settings from user group 
'$connector'. If the vhost policy is absent or if the user group '$connector' 
within that policy is absent then the connector will fail to start.  In policy 
specified via connector attribute  [...]
                     "create": true
+                },
+                "openProperties": {
+                    "description": "A JSON map containing connection 
properties.  These will be sent to the peer on connection open.  All map keys 
are restricted to strings containing only valid ASCII characters, Keys must not 
start with prefixes 'qd.' or 'x-opt-qd.'.  The following key values are also 
reserved: 'product', 'version', 'failover-server-list', 'network-host', 'port', 
'scheme' 'hostname'",
+                    "type": "properties",
+                    "required": false,
+                    "create": true
                 }
             }
         },
diff --git a/python/qpid_dispatch_internal/management/agent.py 
b/python/qpid_dispatch_internal/management/agent.py
index 6167de5..9585975 100644
--- a/python/qpid_dispatch_internal/management/agent.py
+++ b/python/qpid_dispatch_internal/management/agent.py
@@ -380,7 +380,41 @@ class AuthServicePluginEntity(EntityAdapter):
     def __str__(self):
         return super(AuthServicePluginEntity, 
self).__str__().replace("Entity(", "AuthServicePluginEntity(")
 
-class ListenerEntity(EntityAdapter):
+class ConnectionBaseEntity(EntityAdapter):
+    """
+    Provides validation of the openProperties attribute shared by Listener and
+    Connector entities
+    """
+    # qdrouterd reserves a set of connection-property keys as well as any key
+    # that starts with certain prefixes
+    _RESERVED_KEYS=['product',
+                    'version',
+                    'failover-server-list',
+                    'network-host',
+                    'port',
+                    'scheme'
+                    'hostname']
+    _RESERVED_PREFIXES=['qd.', 'x-opt-qd.']
+
+    def validate(self, **kwargs):
+        super(ConnectionBaseEntity, self).validate(**kwargs)
+        op = self.attributes.get('openProperties')
+        if op:
+            msg = "Reserved key '%s' not allowed in openProperties"
+            try:
+                for key in op.keys():
+                    if key in self._RESERVED_KEYS:
+                        raise ValidationError(msg % key)
+                    for prefix in self._RESERVED_PREFIXES:
+                        if key.startswith(prefix):
+                            raise ValidationError(msg % key)
+            except ValidationError:
+                raise
+            except Exception as exc:
+                raise ValidationError(str(exc))
+
+
+class ListenerEntity(ConnectionBaseEntity):
     def create(self):
         config_listener = 
self._qd.qd_dispatch_configure_listener(self._dispatch, self)
         self._qd.qd_connection_manager_start(self._dispatch)
@@ -395,7 +429,12 @@ class ListenerEntity(EntityAdapter):
     def _delete(self):
         self._qd.qd_connection_manager_delete_listener(self._dispatch, 
self._implementations[0].key)
 
-class ConnectorEntity(EntityAdapter):
+
+class ConnectorEntity(ConnectionBaseEntity):
+    def __init__(self, agent, entity_type, attributes=None, validate=True):
+        super(ConnectorEntity, self).__init__(agent, entity_type, attributes,
+                                              validate)
+
     def create(self):
         config_connector = 
self._qd.qd_dispatch_configure_connector(self._dispatch, self)
         self._qd.qd_connection_manager_start(self._dispatch)
diff --git a/python/qpid_dispatch_internal/management/config.py 
b/python/qpid_dispatch_internal/management/config.py
index bd1595f..4c08360 100644
--- a/python/qpid_dispatch_internal/management/config.py
+++ b/python/qpid_dispatch_internal/management/config.py
@@ -28,6 +28,7 @@ from __future__ import print_function
 
 import json, re, sys
 import os
+import traceback
 from copy import copy
 from qpid_dispatch.management.entity import camelcase
 
@@ -38,16 +39,23 @@ from qpid_dispatch_internal.compat import dict_iteritems
 from qpid_dispatch_internal.compat import PY_STRING_TYPE
 from qpid_dispatch_internal.compat import PY_TEXT_TYPE
 
+try:
+    from ..dispatch import LogAdapter, LOG_WARNING
+    _log_imported = True
+except ImportError:
+    # unit test cannot import since LogAdapter not set up
+    _log_imported = False
+
+
 class Config(object):
     """Load config entities from qdrouterd.conf and validated against 
L{QdSchema}."""
 
-    # static property to control depth level while reading the entities
-    child_level = 0
-
     def __init__(self, filename=None, schema=QdSchema(), raw_json=False):
         self.schema = schema
         self.config_types = [et for et in dict_itervalues(schema.entity_types)
                              if schema.is_configuration(et)]
+        self._log_adapter = LogAdapter("AGENT") if _log_imported else None
+
         if filename:
             try:
                 self.load(filename, raw_json)
@@ -57,6 +65,11 @@ class Config(object):
         else:
             self.entities = []
 
+    def _log(self, level, text):
+        if self._log_adapter is not None:
+            info = traceback.extract_stack(limit=2)[0] # Caller frame info
+            self._log_adapter.log(level, text, info[0], info[1])
+
     @staticmethod
     def transform_sections(sections):
         for s in sections:
@@ -68,13 +81,37 @@ class Config(object):
             if s[0] == "exchange":  s[0] = "router.config.exchange"
             if s[0] == "binding":   s[0] = "router.config.binding"
 
-    @staticmethod
-    def _parse(lines):
-        """Parse config file format into a section list"""
-        begin = re.compile(r'([\w-]+)[ \t]*{[ \t]*($|#)')             # WORD {
-        end = re.compile(r'^}')                                       # }
-        attr = re.compile(r'([\w-]+)[ \t]*:[ \t]*(.+)')               # WORD1: 
VALUE
-        child = re.compile(r'([\$]*[\w-]+)[ \t]*:[ \t]*{[ \t]*($|#)') # WORD: {
+    def _parse(self, lines):
+        """
+        Parse config file format into a section list
+
+        The config file format is a text file in JSON-ish syntax.  It allows
+        the user to define a set of Entities which contain Attributes.
+        Attributes may be either a single item or a map of nested attributes.
+
+        Entities and map Attributes start with a single open brace on a line by
+        itself (no non-comment text after the opening brace!)
+
+        Entities and map Attributes are terminated by a single closing brace
+        that appears on a line by itself (no trailing comma and no non-comment
+        trailing text!)
+
+        Entity names and Attribute names and items are NOT enclosed in quotes
+        nor are they terminated with commas, however some select Attributes
+        have values which are expected to be valid JSON (double quoted
+        strings, etc)
+
+        Unlike JSON the config file also allows comments.  A comment begins
+        with the '#' character and is terminated at the end of line.
+        """
+
+        # note: these regexes expect that trailing comment and leading and
+        # trailing whitespace has been removed
+        #
+        entity = re.compile(r'([\w-]+)[ \t]*{[ \t]*$')                    # 
WORD {
+        attr_map = re.compile(r'([\$]*[\w-]+)[ \t]*:[ \t]*{[ \t]*$')      # 
WORD: {
+        attr_item = re.compile(r'([\w-]+)[ \t]*:[ \t]*([^ \t{]+.*)$')     # 
WORD1: VALUE
+        end = re.compile(r'^}$')                                          # }
 
         # The 'pattern:' and 'bindingKey:' attributes in the schema are special
         # snowflakes. They allow '#' characters in their value, so they cannot
@@ -82,29 +119,74 @@ class Config(object):
         special_snowflakes = ['pattern', 'bindingKey']
         hash_ok = re.compile(r'([\w-]+)[ \t]*:[ \t]*([\S]+).*')
 
+        # the 'openProperties' and 'groups' attributes are also special
+        # snowflakes in that their value is expected to be valid JSON.  These
+        # values do allow single line comments which are stripped out, but the
+        # remaining content is expected to be valid JSON.
+        json_snowflakes = ['openProperties', 'groups']
+
+        self._line_num = 1
+        self._child_level = 0
+        self._in_json = False
+
         def sub(line):
             """Do substitutions to make line json-friendly"""
             line = line.strip()
-            if line.startswith("#"):
+
+            # ignore empty and comment lines
+            if not line or line.startswith("#"):
+                self._line_num += 1
                 return ""
-            if line.split(':')[0].strip() in special_snowflakes:
-                line = re.sub(hash_ok, r'"\1": "\2",', line)
-            elif child.search(line):
-                line = line.split('#')[0].strip()
-                line = re.sub(child, r'"\1": {', line)
-                Config.child_level += 1
-            elif end.search(line) and Config.child_level > 0:
-                line = line.split('#')[0].strip()
-                line = re.sub(end, r'},', line)
-                Config.child_level -= 1
+
+            # just pass JSON values along
+            if self._in_json and not end.search(line.split('#')[0].strip()):
+                self._line_num += 1
+                return line
+
+            # filter off pattern items before stripping comments
+            if attr_item.search(line):
+                if re.sub(attr_item, r'\1', line) in special_snowflakes:
+                    self._line_num += 1
+                    return re.sub(hash_ok, r'"\1": "\2",', line)
+
+            # now trim trailing comment
+            line = line.split('#')[0].strip()
+
+            if entity.search(line):
+                # WORD {  --> ["WORD", {
+                line = re.sub(entity, r'["\1", {', line)
+            elif attr_map.search(line):
+                # WORD: {  --> ["WORD": {
+                key = re.sub(attr_map, r'\1', line)
+                line = re.sub(attr_map, r'"\1": {', line)
+                self._child_level += 1
+                if key in json_snowflakes:
+                    self._in_json = True
+            elif attr_item.search(line):
+                # WORD: VALUE --> "WORD": "VALUE"
+                line = re.sub(attr_item, r'"\1": "\2",', line)
+            elif end.search(line):
+                # }  --> "}," or "}]," depending on nesting level
+                if self._child_level > 0:
+                    line = re.sub(end, r'},', line)
+                    self._child_level -= 1
+                    self._in_json = False
+                else:
+                    # end top level entity list item
+                    line = re.sub(end, r'}],', line)
             else:
-                line = line.split('#')[0].strip()
-                line = re.sub(begin, r'["\1", {', line)
-                line = re.sub(end, r'}],', line)
-                line = re.sub(attr, r'"\1": "\2",', line)
+                # unexpected syntax, let json parser figure it out
+                self._log(LOG_WARNING,
+                          "Invalid config file syntax (line %d):\n"
+                          ">>> %s"
+                          % (self._line_num, line))
+            self._line_num += 1
             return line
 
         js_text = "[%s]"%("\n".join([sub(l) for l in lines]))
+        if self._in_json or self._child_level != 0:
+            self._log(LOG_WARNING,
+                      "Configuration file: invalid entity nesting detected.")
         spare_comma = re.compile(r',\s*([]}])') # Strip spare commas
         js_text = re.sub(spare_comma, r'\1', js_text)
         # Convert dictionary keys to camelCase
diff --git a/python/qpid_dispatch_internal/management/schema.py 
b/python/qpid_dispatch_internal/management/schema.py
index f42cf10..069c140 100644
--- a/python/qpid_dispatch_internal/management/schema.py
+++ b/python/qpid_dispatch_internal/management/schema.py
@@ -159,6 +159,27 @@ class EnumType(Type):
         """String description of enum type."""
         return "One of [%s]" % ', '.join([("'%s'" %tag) for tag in self.tags])
 
+
+class PropertiesType(Type):
+    """
+    A PropertiesType is a restricted map: keys must be AMQP 1.0 Symbol types.
+    See the "fields" type in:
+    
http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#type-fields
+    """
+    def __init__(self):
+        super(PropertiesType, self).__init__("properties", dict)
+
+    def validate(self, value):
+        if not isinstance(value, dict):
+            raise ValidationError("Properties must be a map");
+
+        for key in value.keys():
+            if (not isinstance(key, PY_STRING_TYPE)
+                or any(ord(x) > 127 for x in key)):
+                raise ValidationError("Property keys must be ASCII encoded")
+        return value
+
+
 BUILTIN_TYPES = OrderedDict(
     (t.name, t) for t in [Type("string", str),
                           Type("path", str),
@@ -167,6 +188,7 @@ BUILTIN_TYPES = OrderedDict(
                           Type("list", list),
                           Type("map", dict),
                           Type("dict", dict),
+                          PropertiesType(),
                           BooleanType()])
 
 def get_type(rep):
diff --git a/src/compose.c b/src/compose.c
index 5f5270b..3495ed6 100644
--- a/src/compose.c
+++ b/src/compose.c
@@ -559,3 +559,17 @@ void qd_compose_insert_opaque_elements(qd_composed_field_t 
*field,
     bump_count_by_n(field, count);
     bump_length(field, size);
 }
+
+
+void qd_compose_insert_double(qd_composed_field_t *field, double value)
+{
+    union {
+        uint64_t l;
+        double d;
+    } converter;
+    converter.d = value;
+
+    qd_insert_8(field, QD_AMQP_DOUBLE);
+    qd_insert_64(field, converter.l);
+    bump_count(field);
+}
diff --git a/src/connection_manager.c b/src/connection_manager.c
index c08a5f4..ef27bca 100644
--- a/src/connection_manager.c
+++ b/src/connection_manager.c
@@ -390,10 +390,11 @@ static qd_error_t load_server_config(qd_dispatch_t *qd, 
qd_server_config_t *conf
     config->sasl_password        = qd_entity_opt_string(entity, 
"saslPassword", 0);   CHECK();
     config->sasl_mechanisms      = qd_entity_opt_string(entity, 
"saslMechanisms", 0); CHECK();
     config->ssl_profile          = qd_entity_opt_string(entity, "sslProfile", 
0);     CHECK();
-    config->sasl_plugin          = qd_entity_opt_string(entity, "saslPlugin", 
0);   CHECK();
+    config->sasl_plugin          = qd_entity_opt_string(entity, "saslPlugin", 
0);     CHECK();
     config->link_capacity        = qd_entity_opt_long(entity, "linkCapacity", 
0);     CHECK();
     config->multi_tenant         = qd_entity_opt_bool(entity, "multiTenant", 
false);  CHECK();
     config->policy_vhost         = qd_entity_opt_string(entity, "policyVhost", 
0);    CHECK();
+    config->conn_props           = qd_entity_opt_map(entity, 
"openProperties");       CHECK();
     set_config_host(config, entity);
 
     if (config->sasl_password) {
diff --git a/src/entity.c b/src/entity.c
index 9bcf6fa..c1fb9b7 100644
--- a/src/entity.c
+++ b/src/entity.c
@@ -20,6 +20,7 @@
 #include "python_private.h"  // must be first!
 
 #include <qpid/dispatch/error.h>
+#include <qpid/dispatch/python_embedded.h>
 #include "dispatch_private.h"
 #include "entity.h"
 
@@ -104,6 +105,40 @@ bool qd_entity_opt_bool(qd_entity_t *entity, const char* 
attribute, bool default
 }
 
 
+pn_data_t *qd_entity_opt_map(qd_entity_t *entity, const char *attribute)
+{
+    if (!qd_entity_has(entity, attribute))
+        return NULL;
+
+    PyObject *py_obj = qd_entity_get_py(entity, attribute);
+    assert(py_obj); // qd_entity_has() indicates py_obj != NULL
+
+    if (!PyDict_Check(py_obj)) {
+        qd_error(QD_ERROR_CONFIG, "Invalid type: map expected");
+        Py_XDECREF(py_obj);
+        return NULL;
+    }
+
+    pn_data_t *pn_map = pn_data(0);
+    if (!pn_map) {
+        qd_error(QD_ERROR_ALLOC, "Map allocation failure");
+        Py_XDECREF(py_obj);
+        return NULL;
+    }
+
+    qd_error_t rc = qd_py_to_pn_data(py_obj, pn_map);
+    Py_XDECREF(py_obj);
+
+    if (rc != QD_ERROR_NONE) {
+        qd_error(QD_ERROR_ALLOC, "Failed to convert python map");
+        pn_data_free(pn_map);
+        return NULL;
+    }
+
+    return pn_map;
+}
+
+
 /**
  * Set a value for an entity attribute. If py_value == NULL then clear the 
attribute.
  * If the attribute exists and is a list, append this value to the list.
diff --git a/src/entity.h b/src/entity.h
index fda51c6..6e22ef1 100644
--- a/src/entity.h
+++ b/src/entity.h
@@ -66,6 +66,10 @@ long qd_entity_opt_long(qd_entity_t *entity, const char 
*attribute, long default
  */
 bool qd_entity_opt_bool(qd_entity_t *entity, const char *attribute, bool 
default_value);
 
+/** Get a map in pn_data_t format.  Return NULL if map not present or error 
parsing out map
+ * Caller must free pn_data_t when no longer in use.
+ */
+struct pn_data_t *qd_entity_opt_map(qd_entity_t *entity, const char 
*attribute);
 
 /** Set a string valued attribute, entity makes a copy.
  * If value is NULL clear the attribute.
diff --git a/src/proton_utils.c b/src/proton_utils.c
index edd86ba..81b06ca 100644
--- a/src/proton_utils.c
+++ b/src/proton_utils.c
@@ -23,6 +23,7 @@
 #include <time.h>
 #include <ctype.h>
 #include <stdio.h>
+#include <assert.h>
 
 char *qdpn_data_as_string(pn_data_t *data)
 {
@@ -150,3 +151,149 @@ char *qdpn_data_as_string(pn_data_t *data)
 
 
 
+int qdpn_data_insert(pn_data_t *dest, pn_data_t *src)
+{
+    assert(dest && src);
+
+    switch (pn_data_type(src)) {
+
+        // simple scalar types
+
+    case PN_NULL:
+        pn_data_put_null(dest);
+        break;
+    case PN_BOOL:
+        pn_data_put_bool(dest, pn_data_get_bool(src));
+        break;
+    case PN_UBYTE:
+        pn_data_put_ubyte(dest, pn_data_get_ubyte(src));
+        break;
+    case PN_BYTE:
+        pn_data_put_byte(dest, pn_data_get_byte(src));
+        break;
+    case PN_USHORT:
+        pn_data_put_ushort(dest, pn_data_get_ushort(src));
+        break;
+    case PN_SHORT:
+        pn_data_put_short(dest, pn_data_get_short(src));
+        break;
+    case PN_UINT:
+        pn_data_put_uint(dest, pn_data_get_uint(src));
+        break;
+    case PN_INT:
+        pn_data_put_int(dest, pn_data_get_int(src));
+        break;
+    case PN_CHAR:
+        pn_data_put_char(dest, pn_data_get_char(src));
+        break;
+    case PN_ULONG:
+        pn_data_put_ulong(dest, pn_data_get_ulong(src));
+        break;
+    case PN_LONG:
+        pn_data_put_long(dest, pn_data_get_long(src));
+        break;
+    case PN_TIMESTAMP:
+        pn_data_put_timestamp(dest, pn_data_get_timestamp(src));
+        break;
+    case PN_FLOAT:
+        pn_data_put_float(dest, pn_data_get_float(src));
+        break;
+    case PN_DOUBLE:
+        pn_data_put_double(dest, pn_data_get_double(src));
+        break;
+    case PN_DECIMAL32:
+        pn_data_put_decimal32(dest, pn_data_get_decimal32(src));
+        break;
+    case PN_DECIMAL64:
+        pn_data_put_decimal64(dest, pn_data_get_decimal64(src));
+        break;
+    case PN_DECIMAL128:
+        pn_data_put_decimal128(dest, pn_data_get_decimal128(src));
+        break;
+    case PN_UUID:
+        pn_data_put_uuid(dest, pn_data_get_uuid(src));
+        break;
+    case PN_BINARY:
+        pn_data_put_binary(dest, pn_data_get_binary(src));
+        break;
+    case PN_STRING:
+        pn_data_put_string(dest, pn_data_get_string(src));
+        break;
+    case PN_SYMBOL:
+        pn_data_put_symbol(dest, pn_data_get_symbol(src));
+        break;
+
+        // complex types
+
+    case PN_DESCRIBED:
+        // two children: descriptor and value:
+        pn_data_put_described(dest);
+        pn_data_enter(dest);
+        pn_data_enter(src);
+        pn_data_next(src);
+        qdpn_data_insert(dest, src);
+        pn_data_next(src);
+        qdpn_data_insert(dest, src);
+        pn_data_exit(src);
+        pn_data_exit(dest);
+        break;
+
+    case PN_ARRAY: {
+        const size_t count = pn_data_get_array(src);
+        const bool described = pn_data_is_array_described(src);
+        const pn_type_t atype = pn_data_get_array_type(src);
+
+        pn_data_put_array(dest, described, atype);
+        pn_data_enter(dest);
+        pn_data_enter(src);
+        if (described) {
+            pn_data_next(src);
+            qdpn_data_insert(dest, src);
+        }
+
+        for (size_t i = 0; i < count; ++i) {
+            pn_data_next(src);
+            qdpn_data_insert(dest, src);
+        }
+        pn_data_exit(src);
+        pn_data_exit(dest);
+    } break;
+
+    case PN_LIST: {
+        const size_t count = pn_data_get_list(src);
+
+        pn_data_put_list(dest);
+        pn_data_enter(dest);
+        pn_data_enter(src);
+        for (size_t i = 0; i < count; ++i) {
+            pn_data_next(src);
+            qdpn_data_insert(dest, src);
+        }
+        pn_data_exit(src);
+        pn_data_exit(dest);
+    } break;
+
+    case PN_MAP: {
+        const size_t count = pn_data_get_map(src);
+
+        pn_data_put_map(dest);
+        pn_data_enter(dest);
+        pn_data_enter(src);
+        for (size_t i = 0; i < count / 2; ++i) {
+            // key
+            pn_data_next(src);
+            qdpn_data_insert(dest, src);
+            // value
+            pn_data_next(src);
+            qdpn_data_insert(dest, src);
+        }
+        pn_data_exit(src);
+        pn_data_exit(dest);
+
+    } break;
+
+    default:
+        break;
+    }
+    return 0;
+}
diff --git a/src/python_embedded.c b/src/python_embedded.c
index 972a235..7265eb2 100644
--- a/src/python_embedded.c
+++ b/src/python_embedded.c
@@ -178,6 +178,9 @@ qd_error_t qd_py_to_composed(PyObject *value, 
qd_composed_field_t *field)
             qd_compose_insert_long(field, ival);
         }
     }
+    else if (PyFloat_Check(value)) {
+        qd_compose_insert_double(field, PyFloat_AS_DOUBLE(value));
+    }
     else if (PyUnicode_Check(value)) {
         char *data = py_string_2_c(value);
         if (data) {
@@ -267,6 +270,120 @@ qd_error_t qd_py_to_composed(PyObject *value, 
qd_composed_field_t *field)
     return qd_error_code();
 }
 
+
+// like qd_py_to_composed, but output to a pn_data_t
+//
+qd_error_t qd_py_to_pn_data(PyObject *value, pn_data_t *data)
+{
+    qd_python_check_lock();
+    qd_error_clear();
+    if (value == Py_None) {
+        pn_data_put_null(data);
+    }
+    else if (PyBool_Check(value)) {
+        pn_data_put_bool(data, !!PyLong_AsLong(value));
+    }
+    else if (QD_PY_INT_CHECK(value)) {
+        // We are now sure that the value is an integer type
+        int64_t ival = QD_PY_INT_2_INT64(value);
+        if (INT32_MIN <= ival && ival <= INT32_MAX) {
+            pn_data_put_int(data, (int32_t) ival);
+        } else {
+            pn_data_put_long(data, ival);
+        }
+    }
+    else if (PyFloat_Check(value)) {
+        pn_data_put_double(data, PyFloat_AS_DOUBLE(value));
+    }
+    else if (PyUnicode_Check(value)) {
+        char *str = py_string_2_c(value);
+        if (str) {
+            pn_bytes_t pb = {.size = strlen(str),
+                             .start = str};
+            pn_data_put_string(data, pb);
+            free(str);
+        } else {
+            QD_ERROR_PY_RET();
+        }
+    }
+    else if (PyBytes_Check(value)) {
+        // Note: In python 2.X PyBytes is simply an alias for the PyString
+        // type. In python 3.x PyBytes is a distinct type (may contain zeros),
+        // and all strings are PyUnicode types.  Historically
+        // this code has just assumed this data is always a null terminated
+        // UTF8 string. We continue that tradition for Python2, but ending up
+        // here in Python3 means this is actually binary data which may have
+        // embedded zeros.
+        pn_bytes_t pb;
+        char *str;
+        Py_ssize_t p_size;
+        PyBytes_AsStringAndSize(value, &str, &p_size); QD_ERROR_PY_RET();
+        pb.start = str;
+        pb.size = p_size;
+        if (PY_MAJOR_VERSION <= 2) {
+            pn_data_put_string(data, pb);
+        } else {
+            pn_data_put_binary(data, pb);
+        }
+    }
+    else if (PyDict_Check(value)) {
+        Py_ssize_t  iter = 0;
+        PyObject   *key;
+        PyObject   *val;
+        pn_data_put_map(data);
+        pn_data_enter(data);
+        while (PyDict_Next(value, &iter, &key, &val)) {
+            qd_py_to_pn_data(key, data); QD_ERROR_RET();
+            qd_py_to_pn_data(val, data); QD_ERROR_RET();
+        }
+        QD_ERROR_PY_RET();
+        pn_data_exit(data);
+    }
+    else if (PyList_Check(value)) {
+        pn_data_put_list(data);
+        pn_data_enter(data);
+        Py_ssize_t count = PyList_Size(value);
+        for (Py_ssize_t idx = 0; idx < count; idx++) {
+            PyObject *item = PyList_GetItem(value, idx); QD_ERROR_PY_RET();
+            qd_py_to_pn_data(item, data); QD_ERROR_RET();
+        }
+        pn_data_exit(data);
+    }
+    else if (PyTuple_Check(value)) {
+        pn_data_put_list(data);
+        pn_data_enter(data);
+        Py_ssize_t count = PyTuple_Size(value);
+        for (Py_ssize_t idx = 0; idx < count; idx++) {
+            PyObject *item = PyTuple_GetItem(value, idx); QD_ERROR_PY_RET();
+            qd_py_to_pn_data(item, data); QD_ERROR_RET();
+        }
+        pn_data_exit(data);
+    }
+    else {
+        PyObject *type=0, *typestr=0, *repr=0;
+        if ((type = PyObject_Type(value)) &&
+            (typestr = PyObject_Str(type)) &&
+            (repr = PyObject_Repr(value))) {
+            char *t_str = py_string_2_c(typestr);
+            char *r_str = py_string_2_c(repr);
+            qd_error(QD_ERROR_TYPE, "Can't compose object of type %s: %s",
+                     t_str ? t_str : "Unknown",
+                     r_str ? r_str : "Unknown");
+            free(t_str);
+            free(r_str);
+        } else
+            qd_error(QD_ERROR_TYPE, "Can't compose python object of unknown 
type");
+
+        Py_XDECREF(type);
+        Py_XDECREF(typestr);
+        Py_XDECREF(repr);
+
+        pn_data_put_null(data);
+    }
+    return qd_error_code();
+}
+
+
 void qd_py_attr_to_composed(PyObject *object, const char *attr, 
qd_composed_field_t *field)
 {
     qd_python_check_lock();
diff --git a/src/server.c b/src/server.c
index 3005517..234d2ab 100644
--- a/src/server.c
+++ b/src/server.c
@@ -29,6 +29,7 @@
 #include <qpid/dispatch/failoverlist.h>
 #include <qpid/dispatch/alloc.h>
 #include <qpid/dispatch/platform.h>
+#include <qpid/dispatch/proton_utils.h>
 
 #include <proton/event.h>
 #include <proton/listener.h>
@@ -511,6 +512,31 @@ static void decorate_connection(qd_server_t *qd_server, 
pn_connection_t *conn, c
         }
     }
 
+    // Append any user-configured properties. conn_props is a pn_data_t PN_MAP
+    // type. Append the map elements - not the map itself!
+    //
+    if (config->conn_props) {
+        pn_data_t *outp = pn_connection_properties(conn);
+
+        pn_data_rewind(config->conn_props);
+        pn_data_next(config->conn_props);
+        assert(pn_data_type(config->conn_props) == PN_MAP);
+        const size_t count = pn_data_get_map(config->conn_props);
+        pn_data_enter(config->conn_props);
+        for (size_t i = 0; i < count / 2; ++i) {
+            // key: the key must be of type Symbol.  The python agent has
+            // validated the keys as ASCII strings, but the JSON converter does
+            // not provide a Symbol type so all the keys in conn_props are
+            // PN_STRING.
+            pn_data_next(config->conn_props);
+            assert(pn_data_type(config->conn_props) == PN_STRING);
+            pn_data_put_symbol(outp, pn_data_get_string(config->conn_props));
+            // put value
+            pn_data_next(config->conn_props);
+            qdpn_data_insert(outp, config->conn_props);
+        }
+    }
+
     pn_data_exit(pn_connection_properties(conn));
 }
 
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 47d6481..a12eb93 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -146,6 +146,7 @@ foreach(py_test_module
     system_tests_stuck_deliveries
     system_tests_log_level_update
     system_tests_routing_protocol
+    system_tests_open_properties
     )
 
   add_test(${py_test_module} ${TEST_WRAP} ${PYTHON_TEST_COMMAND} -v 
${py_test_module})
diff --git a/tests/system_test.py b/tests/system_test.py
index 079f6de..796d464 100755
--- a/tests/system_test.py
+++ b/tests/system_test.py
@@ -68,7 +68,9 @@ from proton.utils import BlockingConnection
 from proton.reactor import AtLeastOnce, Container
 from proton.reactor import AtMostOnce
 from qpid_dispatch.management.client import Node
-from qpid_dispatch_internal.compat import dict_iteritems
+from qpid_dispatch_internal.compat import dict_iteritems, PY_STRING_TYPE
+from qpid_dispatch_internal.compat import PY_TEXT_TYPE
+
 
 # Optional modules
 MISSING_MODULES = []
@@ -344,7 +346,25 @@ class Qdrouterd(Process):
 
     class Config(list, Config):
         """
-        List of ('section', {'name':'value', ...}).
+        A router configuration.
+
+        The Config class is a list of tuples in the following format:
+
+        [ ('section-name', {attribute-map}), ...]
+
+        where attribute-map is a dictionary of key+value pairs.  Key is an
+        attribute name (string), value can be any of [scalar | string | dict]
+
+        When written to a configuration file to be loaded by the router:
+        o) there is no ":' between the section-name and the opening brace
+        o) attribute keys are separated by a ":" from their values
+        o) attribute values that are scalar or string follow the ":" on the
+        same line.
+        o) attribute values do not have trailing commas
+        o) The section-name and attribute keywords are written
+        without enclosing quotes
+        o) string type attribute values are not enclosed in quotes
+        o) attribute values of type dict are written in their JSON 
representation.
 
         Fills in some default values automatically, see Qdrouterd.DEFAULTS
         """
@@ -373,21 +393,33 @@ class Qdrouterd(Process):
         def __str__(self):
             """Generate config file content. Calls default() first."""
             def tabs(level):
-                return "    " * level
-
-            def sub_elem(l, level):
-                return "".join(["%s%s: {\n%s%s}\n" % (tabs(level), n, props(p, 
level + 1), tabs(level)) for n, p in l])
-
-            def child(v, level):
-                return "{\n%s%s}" % (sub_elem(v, level), tabs(level - 1))
-
-            def props(p, level):
-                return "".join(
-                    ["%s%s: %s\n" % (tabs(level), k, v if not isinstance(v, 
list) else child(v, level + 1)) for k, v in
-                     dict_iteritems(p)])
+                if level:
+                    return "    " * level
+                return ""
+
+            def value(item, level):
+                if isinstance(item, dict):
+                    result = "{\n"
+                    result += "".join(["%s%s: %s,\n" % (tabs(level + 1),
+                                                        json.dumps(k),
+                                                        json.dumps(v))
+                                       for k,v in item.items()])
+                    result += "%s}" % tabs(level)
+                    return result
+                return "%s" %  item
+
+            def attributes(e, level):
+                assert(isinstance(e, dict))
+                # k = attribute name
+                # v = string | scalar | dict
+                return "".join(["%s%s: %s\n" % (tabs(level),
+                                                k,
+                                                value(v, level + 1))
+                                for k, v in dict_iteritems(e)])
 
             self.defaults()
-            return "".join(["%s {\n%s}\n"%(n, props(p, 1)) for n, p in self])
+            # top level list of tuples ('section-name', dict)
+            return "".join(["%s {\n%s}\n"%(n, attributes(p, 1)) for n, p in 
self])
 
     def __init__(self, name=None, config=Config(), pyinclude=None, wait=True,
                  perform_teardown=True, cl_args=None, expect=Process.RUNNING):
diff --git a/tests/system_tests_open_properties.py 
b/tests/system_tests_open_properties.py
new file mode 100644
index 0000000..ad53f90
--- /dev/null
+++ b/tests/system_tests_open_properties.py
@@ -0,0 +1,362 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+from __future__ import unicode_literals
+from __future__ import division
+from __future__ import absolute_import
+from __future__ import print_function
+
+import json
+
+from proton.handlers import MessagingHandler
+from proton.reactor import Container
+from test_broker import FakeBroker
+from system_test import TestCase, unittest, main_module, Qdrouterd
+from system_test import retry, TIMEOUT, wait_port, QdManager
+
+
+def strip_default_options(options):
+    # remove default connection properties added by router to all connections
+    defaults = [
+        "product",
+        "version",
+        "qd.conn-id"]
+
+    for key in defaults:
+        del options[key]
+
+    return options
+
+
+class OpenPropertiesBroker(FakeBroker):
+    """
+    For obtaining open properties on connector connections
+    """
+    wait = False  # do not block for broker connector setup
+    entity = 'connector'
+
+    def __init__(self, router):
+        self.open_properties = None
+        self._router = router
+
+    def on_connection_opened(self, event):
+        self.open_properties = 
strip_default_options(event.connection.remote_properties)
+        super(OpenPropertiesBroker, self).on_connection_opened(event)
+
+    def run(self, host=None, port=None, pf=None):
+        if port:
+            if pf and pf.lower() == 'ipv6':
+                addr = "amqp://[%s]:%s" % (host, port)
+            else:
+                addr = "amqp://%s:%s" % (host, port)
+        else:
+            addr = self._router.connector_addresses[0]
+        super(OpenPropertiesBroker, self).__init__(url=addr)
+        retry(lambda : self.open_properties is not None, delay=0.1)
+        self.join()
+
+
+class OpenPropertiesClient(MessagingHandler):
+    """
+    For obtaining open properties on listener connections
+    """
+    wait = True  # wait for broker setup to complete
+    entity = 'listener'
+
+    def __init__(self, router):
+        super(OpenPropertiesClient, self).__init__()
+        self.open_properties = None
+        self._router = router
+
+    def on_start(self, event):
+        self._conn = event.container.connect(self._addr)
+
+    def on_connection_opened(self, event):
+        self.open_properties = 
strip_default_options(event.connection.remote_properties)
+        event.connection.close()
+
+    def run(self, host=None, port=None, pf=None):
+        if port:
+            wait_port(port, protocol_family=pf)
+            if pf and pf.lower() == 'ipv6':
+                self._addr = "amqp://[%s]:%s" % (host, port)
+            else:
+                self._addr = "amqp://%s:%s" % (host, port)
+        else:
+            self._addr = self._router.addresses[0]
+        Container(self).run()
+
+
+class OpenPropertiesConfigTest(TestCase):
+    """
+    Test the openProperties configuration attribute of the Connector and
+    Listener configuration entities
+    """
+
+    def _valid_properties_check(self, client_class):
+        """
+        Test a few different valid property maps
+        """
+
+        valid_properties = [
+            {
+                "simple": "string",
+            },
+            {
+                "float": 0.0001,
+            },
+            {
+                "int": -3,
+            },
+            {
+                "bool": True,
+            },
+            {
+                "Null": None,
+            },
+            {
+                "list": [1, 2, "a", None, False, -0.01, "done"]
+            },
+            {
+                "map": {"key": "value"},
+            },
+            {
+                "empty1": {},
+            },
+            {
+                "empty2": [],
+            },
+            {
+                # empty
+            },
+            # compound + nested
+            {
+                "string": "string value",
+                "integer": 999,
+                "map" : {
+                    "map-float": 3.14,
+                    "map-list": [1, "A", 0.02],
+                    "map-map": {"key1": "string",
+                                "key2": 1,
+                                "key3": True,
+                                "key4": False,
+                                "key5": None,
+                                "key6": ["x", False, "z", None]
+                    },
+                },
+                "None": None,
+                "True": True,
+                "False": False,
+                "list": [1,
+                         2,
+                         {"a": 1,
+                          "b": None,
+                          "c": True,
+                          "d": "end"},
+                         "text",
+                         3]
+            }
+        ]
+
+        i = 0
+        for op in valid_properties:
+            name = "Router%d" % i
+            i += 1
+            config = [('router', {'id': name}),
+                      (client_class.entity, {
+                          'port': self.tester.get_port(),
+                          'openProperties': op
+                      })
+            ]
+
+            router = self.tester.qdrouterd(name, Qdrouterd.Config(config),
+                                           wait=client_class.wait)
+
+            client = client_class(router)
+            client.run()
+            self.assertEqual(op, client.open_properties)
+            router.teardown()
+
+    def test_01_verify_listener_properties(self):
+        self._valid_properties_check(OpenPropertiesClient)
+
+    def test_02_verify_connector_properties(self):
+        self._valid_properties_check(OpenPropertiesBroker)
+
+
+class OpenPropertiesQdManageTest(TestCase):
+    """
+    Tests creating openProperties via qdmanage tool
+    """
+    def _valid_properties_check(self, client_class):
+        """
+        Test a few different valid property maps
+        """
+
+        valid_properties = [
+            {
+                # empty
+            },
+            {
+                "simple": "string",
+                "int": -3,
+                "bool": True,
+                "Null": None,
+                "list": [1, 2, "a", None, False, "done"],
+                "map": {"key": "value"},
+            },
+            # compound + nested
+            {
+                "string": "string value",
+                "integer": 999,
+                "map" : {
+                    "map-bool": False,
+                    "map-list": [1, "A", None],
+                    "map-map": {"key1": "string",
+                                "key2": 1,
+                                "key3": True,
+                                "key4": False,
+                                "key5": None,
+                                "key6": ["x", False, "z", None]
+                    },
+                },
+                "None": None,
+                "True": True,
+                "False": False,
+                "list": [1,
+                         2,
+                         {"a": 1,
+                          "b": None,
+                          "c": True,
+                          "d": "end"},
+                         "text",
+                         3]
+            }
+        ]
+
+        i = 0
+        for op in valid_properties:
+            name = "Router%d" % i
+            i += 1
+            config = [('router', {'id': name}),
+                      ('listener', {
+                          'port': self.tester.get_port()})
+            ]
+
+            router = self.tester.qdrouterd(name,
+                                           Qdrouterd.Config(config),
+                                           wait=True)
+            new_port = self.tester.get_port()
+            input = json.dumps({'port': new_port,
+                                'name': "%s%d" % (client_class.entity, i),
+                                'openProperties':
+                                op})
+
+            cmd = "CREATE --type=org.apache.qpid.dispatch.%s --stdin" % 
client_class.entity
+            output = QdManager(tester=self)(cmd=cmd,
+                                            address=router.addresses[0],
+                                            input=input,
+                                            timeout=TIMEOUT)
+            rc = json.loads(output)
+            self.assertTrue("openProperties" in rc)
+            self.assertEqual(op, rc["openProperties"])
+
+            client = client_class(router)
+            client.run(host=rc.get("host"), port=new_port,
+                       pf=rc.get("protocolFamily", "IPv4"))
+            router.teardown()
+
+    def test_01_verify_listener_properties(self):
+        self._valid_properties_check(OpenPropertiesClient)
+
+    def test_02_verify_connector_properties(self):
+        self._valid_properties_check(OpenPropertiesBroker)
+
+
+class OpenPropertiesBadConfigTest(TestCase):
+    """
+    Ensure invalid open properties configurations are detected
+    """
+
+    def _find_in_output(self, filename, error_msg):
+        with open(filename, 'r') as out_file:
+            for line in out_file:
+                if error_msg in line:
+                    return True
+        return False
+
+    def test_01_invalid_properties_check(self):
+        """
+        Test a few different invalid property maps
+        """
+        invalid_properties = [
+            (
+                {9: "invalid key type"},
+                "Expecting property name enclosed in double quotes"
+            ),
+            (
+                [1, 2, "not a map"],
+                "Properties must be a map"
+            ),
+            (
+                "I am bad",
+                "Properties must be a map"
+            ),
+            (
+                {u"nonascii\u2588": 1},
+                "Property keys must be ASCII encoded"
+            ),
+            (
+                {None: None},
+                "Expecting property name enclosed in double quotes"
+            ),
+            (
+                {'product': "reserved keyword"},
+                "ValidationError: Reserved key 'product' not allowed in 
openProperties"
+            ),
+            (
+                {'qd.FOO': "reserved prefix"},
+                "ValidationError: Reserved key 'qd.FOO' not allowed in 
openProperties"
+            ),
+            (
+                {'x-opt-qd.BAR': "reserved prefix"},
+                "ValidationError: Reserved key 'x-opt-qd.BAR' not allowed in 
openProperties"
+            )
+        ]
+
+        i = 0
+        for op, err in invalid_properties:
+            name = "Router%d" % i
+            i += 1
+            config = [('router', {'id': name}),
+                      ('listener', {
+                          'port': self.tester.get_port(),
+                          'openProperties': op
+                      })
+            ]
+
+            router = self.tester.qdrouterd(name, Qdrouterd.Config(config), 
wait=False)
+            router.wait(timeout=TIMEOUT)
+            self.assertRaises(RuntimeError, router.teardown)
+            self.assertTrue(self._find_in_output(router.outfile + '.out', err))
+
+
+if __name__== '__main__':
+    unittest.main(main_module())
+
diff --git a/tests/system_tests_policy.py b/tests/system_tests_policy.py
index 9650b3e..e55eda4 100644
--- a/tests/system_tests_policy.py
+++ b/tests/system_tests_policy.py
@@ -1112,21 +1112,23 @@ class VhostPolicyFromRouterConfig(TestCase):
             ('vhost', {
                 'hostname': '0.0.0.0', 'maxConnections': 2,
                 'allowUnknownUser': 'true',
-                'groups': [(
-                    '$default', {
-                        'users': '*', 'remoteHosts': '*',
-                        'sources': '*', 'targets': '*',
-                        'allowDynamicSource': 'true'
-                    }
-                ), (
-                    'anonymous', {
-                        'users': 'anonymous', 'remoteHosts': '*',
+                'groups': {
+                    '$default': {
+                        'users': '*',
+                        'remoteHosts': '*',
+                        'sources': '*',
+                        'targets': '*',
+                        'allowDynamicSource': True
+                    },
+                    'anonymous': {
+                        'users': 'anonymous',
+                        'remoteHosts': '*',
                         'sourcePattern': 'addr/*/queue/*, simpleaddress, 
queue.${user}',
                         'targets': 'addr/*, simpleaddress, queue.${user}',
-                        'allowDynamicSource': 'true',
-                        'allowAnonymousSender': 'true'
+                        'allowDynamicSource': True,
+                        'allowAnonymousSender': True
                     }
-                )]
+                }
             })
         ])
 
@@ -1215,23 +1217,25 @@ class VhostPolicyConnLimit(TestCase):
                 'hostname': '0.0.0.0', 'maxConnections': 100,
                 'maxConnectionsPerUser': 2,
                 'allowUnknownUser': 'true',
-                'groups': [(
-                    '$default', {
-                        'users': '*', 'remoteHosts': '*',
-                        'sources': '*', 'targets': '*',
-                        'allowDynamicSource': 'true',
+                'groups': {
+                    '$default': {
+                        'users': '*',
+                        'remoteHosts': '*',
+                        'sources': '*',
+                        'targets': '*',
+                        'allowDynamicSource': True,
                         'maxConnectionsPerUser': 3
-                    }
-                ), (
-                    'anonymous', {
-                        'users': 'anonymous', 'remoteHosts': '*',
+                    },
+                    'anonymous': {
+                        'users': 'anonymous',
+                        'remoteHosts': '*',
                         'sourcePattern': 'addr/*/queue/*, simpleaddress, 
queue.${user}',
                         'targets': 'addr/*, simpleaddress, queue.${user}',
-                        'allowDynamicSource': 'true',
-                        'allowAnonymousSender': 'true',
+                        'allowDynamicSource': True,
+                        'allowAnonymousSender': True,
                         'maxConnectionsPerUser': 3
                     }
-                )]
+                }
             })
         ])
 
@@ -1439,21 +1443,23 @@ class ConnectorPolicyMisconfigured(TestCase):
             ('vhost', {
                 'hostname': '0.0.0.0', 'maxConnections': 2,
                 'allowUnknownUser': 'true',
-                'groups': [(
-                    '$default', {
-                        'users': '*', 'remoteHosts': '*',
-                        'sources': '*', 'targets': '*',
-                        'allowDynamicSource': 'true'
-                    }
-                ), (
-                    'anonymous', {
-                        'users': 'anonymous', 'remoteHosts': '*',
+                'groups': {
+                    '$default': {
+                        'users': '*',
+                        'remoteHosts': '*',
+                        'sources': '*',
+                        'targets': '*',
+                        'allowDynamicSource': True
+                    },
+                    'anonymous': {
+                        'users': 'anonymous',
+                        'remoteHosts': '*',
                         'sourcePattern': 'addr/*/queue/*, simpleaddress, 
queue.${user}',
                         'targets': 'addr/*, simpleaddress, queue.${user}',
-                        'allowDynamicSource': 'true',
-                        'allowAnonymousSender': 'true'
+                        'allowDynamicSource': True,
+                        'allowAnonymousSender': True
                     }
-                )]
+                }
             })
         ])
 
@@ -1634,12 +1640,12 @@ class ConnectorPolicySrcTgt(TestCase):
             ('autoLink', {'address': 'node.1', 'containerId': 'container.1', 
'direction': 'out'}),
             ('vhost', {
                 'hostname': 'test',
-                'groups': [(
-                    '$connector', {
+                'groups': {
+                    '$connector': {
                         'sources': 'test,examples,work*',
                         'targets': 'examples,$management,play*',
                     }
-                )]
+                }
             })
         ])
 
@@ -1731,16 +1737,16 @@ class ConnectorPolicyNSndrRcvr(TestCase):
             ('autoLink', {'address': 'node.1', 'containerId': 'container.1', 
'direction': 'out'}),
             ('vhost', {
                 'hostname': 'test',
-                'groups': [(
-                    '$connector', {
+                'groups': {
+                    '$connector': {
                         'sources': '*',
                         'targets': '*',
                         'maxSenders': cls.MAX_SENDERS,
                         'maxReceivers': cls.MAX_RECEIVERS,
-                        'allowAnonymousSender': 'true',
-                        'allowWaypointLinks': 'true'
+                        'allowAnonymousSender': True,
+                        'allowWaypointLinks': True
                     }
-                )]
+                }
             })
         ])
 
diff --git a/tests/system_tests_policy_oversize_basic.py 
b/tests/system_tests_policy_oversize_basic.py
index f77bef9..ce90e40 100644
--- a/tests/system_tests_policy_oversize_basic.py
+++ b/tests/system_tests_policy_oversize_basic.py
@@ -289,20 +289,21 @@ class MaxMessageSizeBlockOversize(TestCase):
                               'port': cls.tester.get_port()}),
                 ('address', {'prefix': 'multicast', 'distribution': 
'multicast'}),
                 ('policy', {'maxConnections': 100, 'enableVhostPolicy': 
'true', 'maxMessageSize': max_size, 'defaultVhost': '$default'}),
-                ('vhost', {'hostname': '$default', 'allowUnknownUser': 'true',
-                    'groups': [(
-                        '$default', {
-                            'users': '*',
-                            'maxConnections': 100,
-                            'remoteHosts': '*',
-                            'sources': '*',
-                            'targets': '*',
-                            'allowAnonymousSender': 'true',
-                            'allowWaypointLinks': 'true',
-                            'allowDynamicSource': 'true'
-                        }
-                    )]}
-                )
+                ('vhost', {'hostname': '$default',
+                           'allowUnknownUser': 'true',
+                           'groups': {
+                               '$default': {
+                                   'users': '*',
+                                   'maxConnections': 100,
+                                   'remoteHosts': '*',
+                                   'sources': '*',
+                                   'targets': '*',
+                                   'allowAnonymousSender': 'true',
+                                   'allowWaypointLinks': 'true',
+                                   'allowDynamicSource': 'true'
+                               }
+                           }
+                })
             ]
 
             if extra:
diff --git a/tests/system_tests_policy_oversize_compound.py 
b/tests/system_tests_policy_oversize_compound.py
index e8c4b5e..fb4f618 100644
--- a/tests/system_tests_policy_oversize_compound.py
+++ b/tests/system_tests_policy_oversize_compound.py
@@ -657,19 +657,19 @@ class MaxMessageSizeBlockOversize(TestCase):
                 ('policy', {'maxConnections': 100, 'enableVhostPolicy': 
'true', 'maxMessageSize': max_size, 'defaultVhost': '$default'}),
                 ('address', {'prefix': 'multicast', 'distribution': 
'multicast'}),
                 ('vhost', {'hostname': '$default', 'allowUnknownUser': 'true',
-                    'groups': [(
-                        '$default', {
+                    'groups': {
+                        '$default': {
                             'users': '*',
                             'maxConnections': 100,
                             'remoteHosts': '*',
                             'sources': '*',
                             'targets': '*',
-                            'allowAnonymousSender': 'true',
-                            'allowWaypointLinks': 'true',
-                            'allowDynamicSource': 'true'
+                            'allowAnonymousSender': True,
+                            'allowWaypointLinks': True,
+                            'allowDynamicSource': True
                         }
-                    )]}
-                )
+                    }
+                })
             ]
 
             if extra:
@@ -1096,19 +1096,19 @@ class MaxMessageSizeLinkRouteOversize(TestCase):
                 ('linkRoute', {'prefix': 'oversize', 'containerId': 
'FakeBroker', 'direction': 'in'}),
                 ('linkRoute', {'prefix': 'oversize', 'containerId': 
'FakeBroker', 'direction': 'out'}),
                 ('vhost', {'hostname': '$default', 'allowUnknownUser': 'true',
-                    'groups': [(
-                        '$default', {
+                    'groups': {
+                        '$default': {
                             'users': '*',
                             'maxConnections': 100,
                             'remoteHosts': '*',
                             'sources': '*',
                             'targets': '*',
-                            'allowAnonymousSender': 'true',
-                            'allowWaypointLinks': 'true',
-                            'allowDynamicSource': 'true'
+                            'allowAnonymousSender': True,
+                            'allowWaypointLinks': True,
+                            'allowDynamicSource': True
                         }
-                    )]}
-                )
+                    }
+                })
             ]
 
             if extra:


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org
For additional commands, e-mail: commits-h...@qpid.apache.org

Reply via email to