You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ac...@apache.org on 2014/05/30 18:48:56 UTC

svn commit: r1598667 - in /qpid/dispatch/trunk: python/qpid_dispatch_internal/management/ tests/management/

Author: aconway
Date: Fri May 30 16:48:56 2014
New Revision: 1598667

URL: http://svn.apache.org/r1598667
Log:
DISPATCH-56: Introduce Json/AMQP friendly schema and config file parsing.

Added qpid_dispatch_internal.management package with the following:
- schema.py: manipulate general purpose json schema for management entities.
- Schema.validate(): verifies attribute values, adds default values, and enforces
  required/unique/singleton constraints in the schema.
- entity.py: represent management entity instances with attribute values
- qdrouter.json: json schema for the dispatch router.
- qdrouter.py: parse qdrouterd.conf into a Schema.

These classes are intended to replace config.schema and config.parser,
they are more general and more JSON/AMQP friendly.

They are not yet wired into the C router.

The config file parser introduces some minor change to config file format to
accomodate the AMQP requirement that all entities have name and identity
attributes:
- all sections (entities) can have name and identity attributes as per AMQP spec.
- sections without explicit name/identity attributes are given defaults.
- section router: "router-id" replaced with "identity"
- section log: renamed "logging"
- section log: "module" replaced by "name"
- section listener/connector: "label" replaced by "name"

Added:
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py   (with props)
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/entity.py   (with props)
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.json
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.py   (with props)
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/schema.py   (with props)
    qpid/dispatch/trunk/tests/management/
    qpid/dispatch/trunk/tests/management/__init__.py   (with props)
    qpid/dispatch/trunk/tests/management/entity.py   (with props)
    qpid/dispatch/trunk/tests/management/qdrouter.py   (with props)
    qpid/dispatch/trunk/tests/management/schema.py   (with props)

Added: qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py?rev=1598667&view=auto
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py (added)
+++ qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py Fri May 30 16:48:56 2014
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+"""Management package"""

Propchange: qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py
------------------------------------------------------------------------------
    svn:eol-style = native

Added: qpid/dispatch/trunk/python/qpid_dispatch_internal/management/entity.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch_internal/management/entity.py?rev=1598667&view=auto
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/entity.py (added)
+++ qpid/dispatch/trunk/python/qpid_dispatch_internal/management/entity.py Fri May 30 16:48:56 2014
@@ -0,0 +1,158 @@
+##
+## 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
+##
+
+"""
+Representation of AMQP management entities.
+
+An entity has a set of named attributes and an L{EntityType} defined by a L{Schema}.
+"""
+
+from schema import EntityType
+from copy import copy
+
+class Entity(dict):
+    """
+    A management entity: a set of attributes with an associated entity-type.
+
+    @ivar entity_type: The type of the entity.
+    @type entitytype: L{EntityType}
+    @ivar attributes: The attribute values.
+    @type attribute: dict.
+    @ivar I{attribute-name}: Access an entity attribute as a python attribute.
+    """
+
+    def __init__(self, entity_type, attributes=None, schema=None, **kw_attributes):
+        """
+        @param entity_type: An L{EntityType} or the name of an entity type in the schema.
+        @param schema: The L{Schema} defining entity_type.
+        @param attributes: An attribute mapping.
+        @param kw_attributes: Attributes as keyword arguments.
+        """
+        super(Entity, self).__init__()
+        if schema and entity_type in schema.entity_types:
+            self.entity_type = schema.entity_types[entity_type]
+        else:
+            assert isinstance(entity_type, EntityType), "'%s' is not an entity type"%entity_type
+            self.entity_type = entity_type
+        self.attributes = attributes or {}
+        self.attributes.update(kw_attributes)
+
+    def validate(self, **kwargs):
+        """
+        Calls self.entity_type.validate(self). See L{Schema.validate}
+        """
+        self.entity_type.validate(self.attributes, **kwargs)
+
+    def __getattr__(self, name):
+        if not name in self.attributes:
+            raise AttributeError("'%s' object has no attribute '%s'"%(self.__class__.__name__, name))
+        return self.attributes[name]
+
+    def dump(self, as_map=False):
+        """
+        Dump as a json-friendly tuple or map.
+        @keyword as_map:
+            If true dump as a map: { "entity_type":"<type>", "attributes":{"<name>":"<value>", ...}}
+            Otherwise dump as a tuple: ("<type>", {"<name>":"<value", ...})
+        """
+        if as_map:
+            return {'entity_type':self.entity_type.name, 'attributes':self.attributes}
+        else:
+            return (self.entity_type.name, self.attributes)
+
+class EntityList(list):
+    """
+    A list of entities with some convenience methods for finding entities
+    by type or attribute value.
+
+    @ivar schema: The ${schema.Schema}
+    @ivar <singleton-entity>: Python attribute shortcut to
+        self.get(entity_type=<singleton-entity>, single=True)
+    """
+
+    def __init__(self, schema, contents=None):
+        """
+        @param schema: The L{Schema} for this entity list.
+        @param contents: A list of L{Entity} or tuple (entity-type-name, { attributes...})
+        """
+        self.schema = schema
+        super(EntityList, self).__init__()
+        self.replace(contents or [])
+
+    def entity(self, entity):
+        """
+        Make an L{Entity}. If entity is already an L{Entity} return it unchanged.
+        Otherwise entity should be a tuple (entity-type-name, { attributes...})
+        @param entity: An L{Entity} or a tuple
+        """
+        if isinstance(entity, Entity):
+            return entity
+        else:
+            return Entity(entity[0], entity[1], self.schema)
+
+    def validate(self):
+        """
+        Calls self.schema.validate(self). See L{Schema.validate}.
+        """
+        self.schema.validate(self)
+
+    def get(self, single=False, entity_type=None, **kwargs):
+        """
+        Get a list of entities matching the criteria defined by keyword arguments.
+        @keyword single: If True return a single value. Raise an exception if the result is not a single value.
+        @keyword entity_type: An entity type name, return instances of that type.
+        @param kwargs: Set of attribute-name:value keywords. Return instances of entities where all the attributes match.
+        @return: a list of entities or a single entity if single=True.
+        """
+        result = self
+        def match(e):
+            """True if e matches the criteria"""
+            if entity_type and e.entity_type.name != entity_type:
+                return False
+            for name, value in kwargs.iteritems():
+                if name not in e.attributes or e.attributes[name] != value:
+                    return False
+            return True
+        result = [e for e in self if match(e)]
+        if single:
+            if len(result) != 1:
+                criteria = copy(kwargs)
+                if entity_type: criteria['entity_type'] = entity_type
+                raise ValueError("Expecting single value for %s, got %s"%(criteria, result))
+            return result[0]
+        return result
+
+    def __getattr__(self, name):
+        if name in self.schema.entity_types:
+            return self.get(entity_type=name, single=self.schema.entity_types[name].singleton)
+        raise AttributeError("'%s' object has no attribute '%s'"%(self.__class__.__name__, name))
+
+    def dump(self, as_map=False):
+        """
+        Dump as a json-friendly list of entities. See L{Entity.dump}
+        @keyword as_map: If true dump entities as maps, else as tuples.
+        """
+        return [e.dump(as_map) for e in self]
+
+    def replace(self, contents):
+        """
+        Replace the contents of the list.
+        @param contents: A list of L{Entity} or tuple (entity-type-name, { attributes...})
+        """
+        self[:] = [self.entity(c) for c in contents]

Propchange: qpid/dispatch/trunk/python/qpid_dispatch_internal/management/entity.py
------------------------------------------------------------------------------
    svn:eol-style = native

Added: qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.json
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.json?rev=1598667&view=auto
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.json (added)
+++ qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.json Fri May 30 16:48:56 2014
@@ -0,0 +1,80 @@
+{
+  "prefix": "org.apache.qpid.dispatch",
+  
+  "includes": {
+    "entity-id": {
+      "name": {"type":"String", "required":true, "unique":true},
+      "identity": {"type":"String", "required":true, "unique":true}
+    },
+    
+    "ssl-profile": {
+      "cert-db" : {"type":"String"},
+      "cert-file" : {"type":"String"},
+      "key-file" : {"type":"String"},
+      "password-file" : {"type":"String"},
+      "password" : {"type":"String"}
+    },
+
+    "ip-addr": {
+      "addr" : {"type":"String", "default":"0.0.0.0"},
+      "port" : {"type":"String", "default":"amqp"}
+    }
+  },
+
+  "entity_types": {
+    "container": {
+      "singleton": true,
+      "include" : ["entity-id"],
+      "attributes": {
+	"worker-threads" : {"type":"Integer", "default":"1"}
+      }
+    },
+    
+    "router": {
+      "singleton": true,
+      "include" : ["entity-id"],
+      "attributes": {
+	"mode" : {"type": ["standalone", "interior"], "default":"standalone"},
+	"area" : {"type": "String"},
+	"hello-interval" : {"type": "Integer", "default": 1},
+	"hello-max-age" : {"type": "Integer", "default": 3},
+	"ra-Integererval" : {"type": "Integer", "default": 30},
+	"remote-ls-max-age" : {"type": "Integer", "default": 60},
+	"mobile-addr-max-age" : {"type": "Integer", "default": 60}
+      }
+    },
+    
+    "listener": {
+      "include" : ["entity-id", "ssl-profile", "ip-addr"],
+      "attributes": {
+	"role" : {"type":["normal", "inter-router"], "default":"normal"},
+	"sasl-mechanisms" : {"type":"String", "required":true},
+	"require-peer-auth" : {"type": "Boolean", "default":true},
+	"trusted-certs" : {"type": "String"},
+	"allow-unsecured" : {"type": "Boolean", "default":false},
+	"max-frame-size" : {"type": "Integer", "default":65536}
+      }
+    },
+    
+    "connector": {
+      "include" : ["entity-id", "ssl-profile", "ip-addr"],
+      "attributes": {
+	"role" : {"type": ["normal", "inter-router", "on-demand"], "default":"normal"},
+	"sasl-mechanisms" : {"type":"String", "required":true},
+	"allow-redirect" : {"type": "Boolean", "default":true},
+	"max-frame-size" : {"type": "Integer", "default":65536}
+      }
+    },
+
+    "logging": {
+      "include": ["entity-id"],
+      "attributes": {
+	"level" : {"type": ["none", "trace", "debug", "info", "notice", "warning", "error", "critical"], "default":"info"},
+	"timestamp" : {"type": "Boolean"},
+	"output" : {"type": "String"}
+      }
+    }
+  }
+}
+
+

Added: qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.py?rev=1598667&view=auto
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.py (added)
+++ qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.py Fri May 30 16:48:56 2014
@@ -0,0 +1,133 @@
+##
+## 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
+##
+
+"""
+Qpid Dispatch Router management schema and config file parsing.
+"""
+
+import schema, json, re
+from entity import EntityList
+from copy import copy
+
+class Schema(schema.Schema):
+    """
+    Qpid Dispatch Router management schema.
+    """
+    SCHEMA_FILE = schema.schema_file("qdrouter.json")
+
+    def __init__(self):
+        """Load schema."""
+        with open(self.SCHEMA_FILE) as f:
+            schema.Schema.__init__(self, **json.load(f))
+
+    def validate(self, entities, **kwargs):
+        """
+        In addition to L{schema.Schema.validate}, check the following:
+
+        If the operating mode of the router is not 'interior', then the only
+        permitted roles for listeners and connectors is 'normal'.
+
+        @param entities: An L{EntityList}
+        @param kwargs: See L{schema.Schema.validate}
+        """
+        schema.Schema.validate(self, entities, **kwargs)
+
+        if entities.router.mode != 'interior':
+            for connect in entities.get(entity_type='listeners') + entities.get(entity_type='connector'):
+                if connect['role'] != 'normal':
+                    raise schema.SchemaError("Role '%s' for entity '%s' only permitted with 'interior' mode % (entity['role'], connect.name)")
+
+SCHEMA = Schema()
+"""Instance of the Qpid Dispatch Router management schema""" # pylint: disable=pointless-string-statement
+
+class Configuration(EntityList):
+    """An L{EntityList} loaded from a qdrouterd.conf and validated against L{SCHEMA}."""
+
+    def __init__(self):
+        super(Configuration, self).__init__(SCHEMA)
+
+    @staticmethod
+    def _parse(lines):
+        """Parse config file format into a section list"""
+        begin = re.compile(r'([\w-]+)[ \t]*{') # WORD {
+        end = re.compile(r'}')                 # }
+        attr = re.compile(r'([\w-]+)[ \t]*:[ \t]*([\w-]+)') # WORD1: WORD2
+
+        def sub(line):
+            """Do substitutions to make line json-friendly"""
+            line = line.split('#')[0].strip() # Strip comments
+            line = re.sub(begin, r'["\1", {', line)
+            line = re.sub(end, r'}],', line)
+            line = re.sub(attr, r'"\1": "\2",', line)
+            return line
+
+        js_text = "[%s]"%("".join([sub(l) for l in lines]))
+        spare_comma = re.compile(r',\s*([]}])') # Strip spare commas
+        return json.loads(re.sub(spare_comma, r'\1', js_text))
+
+    def _expand(self, content):
+        """
+        Find include sections (defined by schema) in the content,
+        expand references and remove the include sections.
+        """
+        def _expand_section(section, includes):
+            """Expand one section"""
+            attrs = section[1]
+            for k in attrs.keys(): # Iterate over keys() because we will modify attr
+                inc = [i[1] for i in includes if i[0] == k and i[1]['name'] == attrs[k]]
+                if inc:
+                    assert len(inc) == 1
+                    inc = copy(inc[0])
+                    del inc['name'] # Not a real attribute, just an include id.
+                    attrs.update(inc)
+                    del attrs[k] # Delete the include attribute.
+            return section
+        includes = [s for s in content if s[0] in self.schema.includes]
+        return [_expand_section(s, includes) for s in content if s[0] not in self.schema.includes]
+
+    def _default_ids(self, content):
+        """
+        Set default name and identity where missing.
+        - If entity has no name/identity, set both to "<entity-type>-<i>"
+        - If entity has one of name/identity set the other to be the same.
+        - If entity has both, do nothing
+        """
+        counts = dict((e, 0) for e in self.schema.entity_types)
+        for section in content:
+            entity_type, attrs = section
+            count = counts[entity_type]
+            counts[entity_type] += 1
+            if 'name' in attrs and 'identity' in attrs:
+                continue
+            elif 'name' in attrs:
+                attrs['identity'] = attrs['name']
+            elif 'identity' in attrs:
+                attrs['name'] = attrs['identity']
+            else:
+                identity = "%s%d"%(entity_type, count)
+                attrs['name'] = attrs['identity'] = identity
+        return content
+
+    def load(self, lines):
+        """
+        Load a configuration file.
+        @param lines: A list of lines, or an open file object.
+        """
+        self.replace(self._default_ids(self._expand(self._parse(lines))))
+        self.validate()

Propchange: qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.py
------------------------------------------------------------------------------
    svn:eol-style = native

Added: qpid/dispatch/trunk/python/qpid_dispatch_internal/management/schema.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch_internal/management/schema.py?rev=1598667&view=auto
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/schema.py (added)
+++ qpid/dispatch/trunk/python/qpid_dispatch_internal/management/schema.py Fri May 30 16:48:56 2014
@@ -0,0 +1,371 @@
+##*
+## 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
+##
+
+"""
+Schema for AMQP management entity types.
+
+Schema validation will validate and transform values, add default values and
+check for uniqueness of enties/attributes that are specified to be unique.
+
+A Schema can be loaded/dumped to a json file.
+"""
+
+import os
+
+class SchemaError(Exception):
+    """Class for schema errors"""
+    pass
+
+def schema_file(name):
+    """Return a file name relative to the directory from which this module was loaded."""
+    return os.path.join(os.path.dirname(__file__), name)
+
+class Type(object):
+    """Base class for schema types.
+
+    @ivar name: The type name.
+    @ivar pytype: The python type for this schema type.
+    """
+    def __init__(self, name, pytype):
+        """
+        @param name: The type name.
+        @param pytype: The python type for this schema type.
+        """
+        self.name, self.pytype = name, pytype
+
+    def validate(self, value, **kwargs): # pylint: disable=unused-argument
+        """
+        Convert value to the correct python type.
+
+        @param kwargs: See L{Schema.validate}
+        """
+        return self.pytype(value)
+
+    def dump(self):
+        """
+        @return: Representation of the type to dump to json. Normally the type name,
+            EnumType.dump is the exception.
+        """
+        return self.name
+
+    def __repr__(self):
+        return str(self.dump())
+
+class BooleanType(Type):
+    """A boolean schema type"""
+
+    def __init__(self):
+        super(BooleanType, self).__init__("Boolean", bool)
+
+    VALUES = {"yes":1, "true":1, "on":1, "no":0, "false":0, "off":0}
+
+    def validate(self, value, **kwargs):
+        """
+        @param value: A string such as "yes", "false" etc. is converted appropriately.
+            Any other type is converted using python's bool()
+        @param kwargs: See L{Schema.validate}
+        @return A python bool.
+        """
+        try:
+            if isinstance(value, basestring):
+                return self.VALUES[value.lower()]
+            return bool(value)
+        except:
+            raise ValueError("Invalid Boolean value '%r'"%value)
+
+class EnumType(Type):
+    """An enumerated type"""
+
+    def __init__(self, tags):
+        """
+        @param tags: A list of string values for the enumerated type.
+        """
+        assert isinstance(tags, list)
+        super(EnumType, self).__init__("enum%s"%([str(t) for t in tags]), int)
+        self.tags = tags
+
+    def validate(self, value, enum_as_int=False, **kwargs):
+        """
+        @param value: May be a string from the set of enum tag strings or anything
+            that can convert to an int - in which case it must be in the enum range.
+        @keyword enum_as_int: If true the return value will be an int.
+        @param kwargs: See L{Schema.validate}
+        @return: If enum_as_int is True the int value of the enum, othewise the enum tag string.
+        """
+        if value in self.tags:
+            if enum_as_int:
+                return self.tags.index(value)
+            else:
+                return value
+        else:
+            try:
+                i = int(value)
+                if 0 <= i and i < len(self.tags):
+                    if enum_as_int:
+                        return i
+                    else:
+                        return self.tags[i]
+            except (ValueError, IndexError):
+                pass
+        raise ValueError("Invalid value for %s: '%r'"%(self.name, value))
+
+    def dump(self):
+        """
+        @return: A list of the enum tags.
+        """
+        return self.tags
+
+BUILTIN_TYPES = dict((t.name, t) for t in [Type("String", str), Type("Integer", int), BooleanType()])
+
+def get_type(rep):
+    """
+    Get a schema type.
+    @param rep: json representation of the type.
+    """
+    if isinstance(rep, list):
+        return EnumType(rep)
+    if rep in BUILTIN_TYPES:
+        return BUILTIN_TYPES[rep]
+    raise SchemaError("No such schema type: %s"%rep)
+
+def _dump_dict(items):
+    """
+    Remove all items with None value from a mapping.
+    @return: Map of non-None items.
+    """
+    return dict((k, v) for k, v in items if v)
+
+def _is_unique(found, category, value):
+    """
+    Check if value has already been found in category.
+
+    @param found: Map of values found in each category { category:set(value,...), ...}
+        Modified: value is added to the category.
+        If found is None then _is_unique simply returns True.
+    @param category: category to check for value.
+    @param value: value to check.
+    @return: True if value is unique so far (i.e. was not already found)
+    """
+    if found is None:
+        return True
+    if not category in found:
+        found[category] = set()
+    if value in found[category]:
+        return False
+    else:
+        found[category].add(value)
+        return True
+
+
+class AttributeDef(object):
+    """
+    Definition of an attribute.
+
+    @ivar name: Attribute name.
+    @ivar atype: Attribute L{Type}
+    @ivar required: True if the attribute is reqiured.
+    @ivar default: Default value for the attribute or None if no default.
+    @ivar unique: True if the attribute value is unique.
+    """
+
+    def __init__(self, name, type=None, default=None, required=False, unique=False): # pylint: disable=redefined-builtin
+        """
+        See L{AttributeDef} instance variables.
+        """
+        self.name = name
+        self.atype = get_type(type)
+        self.required = required
+        self.default = default
+        self.unique = unique
+        if default is not None:
+            self.default = self.atype.validate(default)
+
+    def validate(self, value, check_required=True, add_default=True, check_unique=None, **kwargs):
+        """
+        Validate value for this attribute definition.
+        @keyword check_required: Raise an exception if required attributes are misssing.
+        @keyword add_default:  Add a default value for missing attributes.
+        @keyword check_unique: A dict to collect values to check for uniqueness.
+            None means don't check for uniqueness.
+        @param kwargs: See L{Schema.validate}
+        @return: value converted to the correct python type. Rais exception if any check fails.
+        """
+        if value is None and add_default:
+            value = self.default
+        if value is None:
+            if self.required and check_required:
+                raise SchemaError("Missing value for attribute '%s'"%self.name)
+            else:
+                return None
+        else:
+            if self.unique and not _is_unique(check_unique, self.name, value):
+                raise SchemaError("Multiple instances of unique attribute '%s'"%self.name)
+            return self.atype.validate(value, **kwargs)
+
+    def dump(self):
+        """
+        @return: Json-friendly representation of an attribute type
+        """
+        return _dump_dict([
+            ('type', self.atype.dump()), ('default', self.default), ('required', self.required)])
+
+    def __str__(self):
+        return "AttributeDef%s"%(self.__dict__)
+
+class EntityType(object):
+    """
+    An entity type defines a set of attributes for an entity.
+
+    @ivar name: Entity type name.
+    @ivar attributes: Map of L{AttributeDef} for entity.
+    @ivar singleton: If true only one entity of this type is allowed.
+    """
+    def __init__(self, name, schema, singleton=False, include=None, attributes=None):
+        """
+        @param name: name of the entity type.
+        @param schema: schema for this type.
+        @param singleton: True if entity type is a singleton.
+        @param include: List of names of include types for this entity.
+        @param attributes: Map of attributes {name: {type:, default:, required:, unique:}}
+        """
+        self.name = name
+        self.schema = schema
+        self.singleton = singleton
+        self.attributes = {}
+        if attributes:
+            self.add_attributes(attributes)
+        if include and self.schema.includes:
+            for i in include:
+                self.add_attributes(schema.includes[i])
+
+    def add_attributes(self, attributes):
+        """
+        Add attributes.
+        @param attributes: Map of attributes {name: {type:, default:, required:, unique:}}
+        """
+        for k, v in attributes.iteritems():
+            if k in self.attributes:
+                raise SchemaError("Attribute '%s' duplicated in '%s'"%(k, self.name))
+            self.attributes[k] = AttributeDef(k, **v)
+
+    def dump(self):
+        """Json friendly representation"""
+        return _dump_dict([
+            ('singleton', self.singleton),
+            ('attributes', dict((k, v.dump()) for k, v in self.attributes.iteritems()))])
+
+    def validate(self, attributes, check_singleton=None, **kwargs):
+        """
+        Validate attributes.
+        @param attributes: Map of attribute values {name:value}
+            Modifies attributes: adds defaults, converts values.
+        @param check_singleton: dict to enable singleton checking or None to disable.
+        @param kwargs: See L{Schema.validate}
+        """
+        if self.singleton and not _is_unique(check_singleton, self.name, True):
+            raise SchemaError("Found multiple instances of singleton entity type '%s'"%self.name)
+        # Validate
+        for name, value in attributes.iteritems():
+            attributes[name] = self.attributes[name].validate(value, **kwargs)
+        # Set defaults, check for missing required values
+        for attr in self.attributes.itervalues():
+            if attr.name not in attributes:
+                value = attr.validate(None, **kwargs)
+                if not value is None:
+                    attributes[attr.name] = value
+        # Drop null items
+        for name in attributes.keys():
+            if attributes[name] is None:
+                del attributes[name]
+        return attributes
+
+
+class Schema(object):
+    """
+    Schema defining entity types.
+
+    @ivar prefix: Prefix to prepend to short entity names.
+    @ivar entity_types: Map of L{EntityType} by name.
+    """
+    def __init__(self, prefix="", includes=None, entity_types=None):
+        """
+        @param prefix: Prefix for entity names.
+        @param includes: Map of  { include-name: {attribute-name:value, ... }}
+        @param entity_types: Map of  { entity-type-name: { singleton:, include:[...], attributes:{...}}}
+        """
+        self.prefix = self.prefixdot = prefix
+        if not prefix.endswith('.'):
+            self.prefixdot += '.'
+        self.includes = includes or {}
+        self.entity_types = {}
+        if entity_types:
+            for k, v in entity_types.iteritems():
+                self.add_entity_type(k, **v)
+
+    def add_entity_type(self, name, singleton=False, include=None, attributes=None):
+        """
+        Add an entity type to the schema.
+        @param name: Entity type name.
+        @param singleton: True if this is a singleton.
+        @param include: List of names of include sections for this entity.
+        @param attributes: Map of attributes {name: {type:, default:, required:, unique:}}
+        """
+        self.entity_types[name] = EntityType(name, self, singleton, include, attributes)
+
+    def short_name(self, name):
+        """Remove prefix from name if present"""
+        if name.startswith(self.prefixdot):
+            return name[len(self.prefixdot):]
+        else: return name
+
+    def long_name(self, name):
+        """Add prefix to name if absent"""
+        if not name.startswith(self.prefixdot):
+            return self.prefixdot + name
+        else: return name
+
+    def dump(self):
+        """Return json-friendly representation"""
+        return {'prefix':self.prefix,
+                'includes':self.includes,
+                'entity_types':dict((k, v.dump()) for k, v in self.entity_types.iteritems())}
+
+    def validate(self, entities, enum_as_int=False, check_required=True, add_default=True, check_unique=True, check_singleton=True):
+        """
+        Validate entities, verify singleton entities and unique attributes are unique.
+
+        @param entities: List of L{Entity}
+        @keyword enum_as_int: Represent enums as int rather than string.
+        @keyword check_required: Raise exception if required attributes are missing.
+        @keyword add_default: Add defaults for missing attributes.
+        @keyword check_unique: Raise exception if unique attributes are duplicated.
+        @keyword check_singleton: Raise exception if singleton entities are duplicated.
+
+        """
+        if check_singleton: check_singleton = {}
+        if check_unique: check_unique = {}
+        for e in entities:
+            assert e.entity_type.schema is self, "Entity '%s' from wrong schema"%e
+            e.validate(
+                enum_as_int=enum_as_int,
+                check_required=check_required,
+                add_default=add_default,
+                check_unique=check_unique,
+                check_singleton=check_singleton)
+        return entities

Propchange: qpid/dispatch/trunk/python/qpid_dispatch_internal/management/schema.py
------------------------------------------------------------------------------
    svn:eol-style = native

Added: qpid/dispatch/trunk/tests/management/__init__.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/management/__init__.py?rev=1598667&view=auto
==============================================================================
--- qpid/dispatch/trunk/tests/management/__init__.py (added)
+++ qpid/dispatch/trunk/tests/management/__init__.py Fri May 30 16:48:56 2014
@@ -0,0 +1,23 @@
+##
+## 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
+##
+
+#pylint: disable=wildcard-import,missing-docstring
+from entity import *
+from schema import *
+from qdrouter import *

Propchange: qpid/dispatch/trunk/tests/management/__init__.py
------------------------------------------------------------------------------
    svn:eol-style = native

Added: qpid/dispatch/trunk/tests/management/entity.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/management/entity.py?rev=1598667&view=auto
==============================================================================
--- qpid/dispatch/trunk/tests/management/entity.py (added)
+++ qpid/dispatch/trunk/tests/management/entity.py Fri May 30 16:48:56 2014
@@ -0,0 +1,60 @@
+##
+## 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
+##
+
+#pylint: disable=wildcard-import,missing-docstring,too-many-public-methods
+
+import unittest
+from qpid_dispatch_internal.management.schema import Schema
+from qpid_dispatch_internal.management.entity import Entity, EntityList
+from schema import SCHEMA_1
+
+class EntityTest(unittest.TestCase):
+
+    def test_entity(self):
+        s = Schema(**SCHEMA_1)
+        e = Entity('container', name='x', schema=s)
+        self.assertEqual(e.name, 'x')
+        self.assertEqual(e.attributes, {'name':'x'})
+        e.validate()
+        attrs = {'name':'x', 'worker-threads':1}
+        self.assertEqual(e.attributes, attrs)
+        self.assertEqual(e.dump(as_map=True), {'entity_type':'container', 'attributes':attrs})
+        self.assertEqual(e.dump(), ('container', attrs))
+
+
+    def test_entity_list(self):
+        s = Schema(**SCHEMA_1)
+        contents = [('container', {'name':'x'}),
+             ('listener', {'name':'y', 'addr':'1'}),
+             ('listener', {'name':'z', 'addr':'2'}),
+             ('listener', {'name':'q', 'addr':'2'}),
+             ('connector', {'name':'c1', 'addr':'1'})]
+        l = EntityList(s, contents)
+
+        self.assertEqual(l.dump(), contents)
+        self.assertEqual([e.name for e in l.get(entity_type='listener')], ['y', 'z', 'q'])
+        self.assertEqual([e.name for e in l.get(entity_type='listener', addr='1')], ['y'])
+        self.assertEqual([e.name for e in l.get(addr='1')], ['y', 'c1'])
+        self.assertEqual(l.get(name='x', single=True).name, 'x')
+
+        self.assertRaises(ValueError, l.get, entity_type='listener', single=True)
+        self.assertRaises(ValueError, l.get, name='nosuch', single=True)
+
+if __name__ == '__main__':
+    unittest.main()

Propchange: qpid/dispatch/trunk/tests/management/entity.py
------------------------------------------------------------------------------
    svn:eol-style = native

Added: qpid/dispatch/trunk/tests/management/qdrouter.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/management/qdrouter.py?rev=1598667&view=auto
==============================================================================
--- qpid/dispatch/trunk/tests/management/qdrouter.py (added)
+++ qpid/dispatch/trunk/tests/management/qdrouter.py Fri May 30 16:48:56 2014
@@ -0,0 +1,91 @@
+##
+## 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
+##
+
+#pylint: disable=wildcard-import,unused-wildcard-import,missing-docstring,too-many-public-methods
+
+import unittest
+from qpid_dispatch_internal.management.qdrouter import *
+
+class QdrouterTest(unittest.TestCase):
+    """Tests for qpid_dispatch_internal.config.qdrouter"""
+
+    def test_qdrouter_parse(self):
+        conf = Configuration()
+        conf_text = """
+# Line comment
+router {
+    mode: standalone            # End of line comment
+}
+ssl-profile {
+    name: test-profile
+    password: secret
+}
+listener {
+    name: l0
+    sasl-mechanisms: ANONYMOUS
+    ssl-profile: test-profile
+}
+listener {
+    identity: l1
+    sasl-mechanisms: ANONYMOUS
+    port: 1234
+}
+listener {
+    sasl-mechanisms: ANONYMOUS
+    port: 4567
+}
+        """
+        #pylint: disable=protected-access
+        content = conf._parse(conf_text.split("\n"))
+
+        self.maxDiff = None     # pylint: disable=invalid-name
+        self.assertEqual(content, [
+            ["router", {"mode":"standalone"}],
+            ["ssl-profile", {"name":"test-profile", "password":"secret"}],
+            ["listener", {"name":"l0", "sasl-mechanisms":"ANONYMOUS", "ssl-profile":"test-profile"}],
+            ["listener", {"identity":"l1", "sasl-mechanisms":"ANONYMOUS", "port":"1234"}],
+            ["listener", {"sasl-mechanisms":"ANONYMOUS", "port":"4567"}]
+        ])
+
+        content = conf._expand(content)
+        self.assertEqual(content, [
+            ["router", {"mode":"standalone"}],
+            ["listener", {"name":"l0", "sasl-mechanisms":"ANONYMOUS", "password":"secret"}],
+            ["listener", {"identity":"l1", "sasl-mechanisms":"ANONYMOUS", "port":"1234"}],
+            ["listener", {"sasl-mechanisms":"ANONYMOUS", "port":"4567"}]
+        ])
+
+        content = conf._default_ids(content)
+        self.assertEqual(content, [
+            ["router", {"mode":"standalone", "name":"router0", "identity":"router0"}],
+            ["listener", {"name":"l0", "identity":"l0", "sasl-mechanisms":"ANONYMOUS", "password":"secret"}],
+            ["listener", {"name":"l1", "identity":"l1", "sasl-mechanisms":"ANONYMOUS", "port":"1234"}],
+            ["listener", {"name":"listener2", "identity":"listener2", "sasl-mechanisms":"ANONYMOUS", "port":"4567"}]
+        ])
+
+        conf.load(conf_text.split("\n"))
+        self.assertEqual(conf.router.name, 'router0')
+        self.assertEqual(conf.router.identity, 'router0')
+        self.assertEqual(len(conf.listener), 3)
+        self.assertEqual(conf.listener[0].name, 'l0')
+        self.assertEqual(conf.listener[2].name, 'listener2')
+        self.assertEqual(conf.listener[2].identity, 'listener2')
+
+if __name__ == '__main__':
+    unittest.main()

Propchange: qpid/dispatch/trunk/tests/management/qdrouter.py
------------------------------------------------------------------------------
    svn:eol-style = native

Added: qpid/dispatch/trunk/tests/management/schema.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/management/schema.py?rev=1598667&view=auto
==============================================================================
--- qpid/dispatch/trunk/tests/management/schema.py (added)
+++ qpid/dispatch/trunk/tests/management/schema.py Fri May 30 16:48:56 2014
@@ -0,0 +1,159 @@
+#
+# 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.
+#
+
+
+#pylint: disable=wildcard-import,missing-docstring,too-many-public-methods
+
+import unittest, json
+from qpid_dispatch_internal.management.schema import Schema, EntityType, BooleanType, EnumType, AttributeDef, SchemaError, schema_file
+from qpid_dispatch_internal.management.entity import Entity
+
+SCHEMA_1 = {
+    "prefix":"org.example",
+    "includes": {
+        "entity-id": {
+            "name": {"type":"String", "required": True, "unique":True}
+        },
+    },
+    "entity_types": {
+        "container": {
+            "singleton": True,
+            "include" : ["entity-id"],
+            "attributes": {
+                "worker-threads" : {"type":"Integer", "default": 1}
+            }
+        },
+        "listener": {
+            "include" : ["entity-id"],
+            "attributes": {
+                "addr" : {"type":"String"}
+            }
+        },
+        "connector": {
+            "include" : ["entity-id"],
+            "attributes": {
+                "addr" : {"type":"String"}
+            }
+        }
+    }
+}
+
+
+class SchemaTest(unittest.TestCase):
+
+    def test_bool(self):
+        b = BooleanType()
+        self.assertTrue(b.validate('on'))
+        self.assertTrue(b.validate(True))
+        self.assertFalse(b.validate(False))
+        self.assertFalse(b.validate('no'))
+        self.assertRaises(ValueError, b.validate, 'x')
+
+    def test_enum(self):
+        e = EnumType(['a', 'b', 'c'])
+        self.assertEqual(e.validate('a'), 'a')
+        self.assertEqual(e.validate(1), 'b')
+        self.assertEqual(e.validate('c', enum_as_int=True), 2)
+        self.assertEqual(e.validate(2, enum_as_int=True), 2)
+        self.assertRaises(ValueError, e.validate, 'foo')
+        self.assertRaises(ValueError, e.validate, 3)
+
+    def test_attribute_def(self):
+        a = AttributeDef('foo', 'String', 'FOO', False)
+        self.assertEqual(a.validate('x'), 'x')
+        self.assertEqual(a.validate(None), 'FOO')
+        a = AttributeDef('foo', 'String', 'FOO', True)
+        self.assertEqual('FOO', a.validate(None))
+        a = AttributeDef('foo', 'Integer', None, True)
+        self.assertRaises(SchemaError, a.validate, None) # Missing default
+
+    def test_entity_type(self):
+        s = Schema(includes={
+            'i1':{'foo1': {'type':'String', 'default':'FOO1'}},
+            'i2':{'foo2': {'type':'String', 'default':'FOO2'}}})
+
+        e = EntityType('MyEntity', s, attributes={
+            'foo': {'type':'String', 'default':'FOO'},
+            'req': {'type':'Integer', 'required':True},
+            'e': {'type':['x', 'y']}})
+        self.assertRaises(SchemaError, e.validate, {}) # Missing required 'req'
+        self.assertEqual(e.validate({'req':42, 'e':None}), {'foo': 'FOO', 'req': 42})
+        # Try with an include
+        e = EntityType('e2', s, attributes={'x':{'type':'Integer'}}, include=['i1', 'i2'])
+        self.assertEqual(e.validate({'x':1}), {'x':1, 'foo1': 'FOO1', 'foo2': 'FOO2'})
+
+    qdrouter_json = schema_file('qdrouter.json')
+
+
+    @staticmethod
+    def load_schema(fname=qdrouter_json):
+        with open(fname) as f:
+            j = json.load(f)
+            return Schema(**j)
+
+    def test_schema_dump(self):
+        s = Schema(**SCHEMA_1)
+        self.maxDiff = None     # pylint: disable=invalid-name
+        expect = {
+            "prefix":"org.example",
+            "includes": {"entity-id": {"name": {"required": True, "unique":True, "type": "String"}}},
+            "entity_types": {
+                "container": {
+                    "singleton": True,
+                    "attributes": {
+                        "name": {"type":"String", "required": True},
+                        "worker-threads": {"type":"Integer", "default": 1}
+                    }
+                    },
+                    "listener": {
+                        "attributes": {
+                            "name": {"type":"String", "required": True},
+                            "addr" : {"type":"String"}
+                        }
+                    },
+                "connector": {
+                    "attributes": {
+                        "name": {"type":"String", "required": True},
+                        "addr" : {"type":"String"}
+                    }
+                }
+            }
+        }
+        self.assertEquals(s.dump(), expect)
+
+        s2 = Schema(**s.dump())
+        self.assertEqual(s.dump(), s2.dump())
+
+    def test_schema_validate(self):
+        s = Schema(**SCHEMA_1)
+        # Duplicate unique attribute 'name'
+        m = [Entity('listener', {'name':'x'}, s),
+             Entity('listener', {'name':'x'}, s)]
+        self.assertRaises(SchemaError, s.validate, m)
+        # Duplicate singleton entity 'container'
+        m = [Entity('container', {'name':'x'}, s),
+             Entity('container', {'name':'y'}, s)]
+        self.assertRaises(SchemaError, s.validate, m)
+        # Valid model
+        m = [Entity('container', {'name':'x'}, s),
+             Entity('listener', {'name':'y'}, s)]
+        s.validate(m)
+
+if __name__ == '__main__':
+    unittest.main()

Propchange: qpid/dispatch/trunk/tests/management/schema.py
------------------------------------------------------------------------------
    svn:eol-style = native



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