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