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/09/10 05:34:42 UTC

svn commit: r1623899 - in /qpid/dispatch/trunk: ./ doc/man/ python/qpid_dispatch/ python/qpid_dispatch/management/ python/qpid_dispatch_internal/management/ src/ tests/ tests/management/ tools/

Author: aconway
Date: Wed Sep 10 03:34:41 2014
New Revision: 1623899

URL: http://svn.apache.org/r1623899
Log:
DISPATCH-56: Make dispatch management client library public.

The dispatch management client library is now installed as a normal python package
qpid_dispatch.management

- Added management.Entity with read, update, delete operations.
- Added setup.py.in to install python packages to standard site-directory for CMAKE_INSTALL_PREFIX
- Added VERSION.txt with project version, available in cmake as QPID_DISPATCH_VERSION

- Separate proton.Messenger code from client.Node.
- Add CRUD permissions to schema. Fix status return codes for CRUD ops.
- Added DummyEntity to test CRUD, implements all 4 operations.

Added:
    qpid/dispatch/trunk/VERSION.txt
    qpid/dispatch/trunk/python/qpid_dispatch/
    qpid/dispatch/trunk/python/qpid_dispatch/__init__.py
      - copied, changed from r1618265, qpid/dispatch/trunk/config.sh
    qpid/dispatch/trunk/python/qpid_dispatch/management/
    qpid/dispatch/trunk/python/qpid_dispatch/management/__init__.py
      - copied, changed from r1618265, qpid/dispatch/trunk/config.sh
    qpid/dispatch/trunk/python/qpid_dispatch/management/client.py
      - copied, changed from r1618265, qpid/dispatch/trunk/python/qpid_dispatch_internal/management/client.py
    qpid/dispatch/trunk/python/qpid_dispatch/management/entity.py
    qpid/dispatch/trunk/python/qpid_dispatch/management/error.py
      - copied, changed from r1618265, qpid/dispatch/trunk/python/qpid_dispatch_internal/management/error.py
    qpid/dispatch/trunk/setup.py.in   (contents, props changed)
      - copied, changed from r1618265, qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py
Removed:
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/client.py
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/entity.py
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/error.py
Modified:
    qpid/dispatch/trunk/CMakeLists.txt
    qpid/dispatch/trunk/config.sh
    qpid/dispatch/trunk/doc/man/qdrouterd_conf_man.py
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/agent.py
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/config.py
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.json
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.py
    qpid/dispatch/trunk/python/qpid_dispatch_internal/management/schema.py
    qpid/dispatch/trunk/src/dispatch.c
    qpid/dispatch/trunk/src/schema_c.py
    qpid/dispatch/trunk/tests/management/__init__.py
    qpid/dispatch/trunk/tests/management/client.py
    qpid/dispatch/trunk/tests/management/entity.py
    qpid/dispatch/trunk/tests/management/qdrouter.py
    qpid/dispatch/trunk/tests/management/schema.py
    qpid/dispatch/trunk/tests/run.py.in
    qpid/dispatch/trunk/tests/system_test.py
    qpid/dispatch/trunk/tests/system_tests_management.py
    qpid/dispatch/trunk/tests/system_tests_two_routers.py
    qpid/dispatch/trunk/tools/qdstat

Modified: qpid/dispatch/trunk/CMakeLists.txt
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/CMakeLists.txt?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/CMakeLists.txt (original)
+++ qpid/dispatch/trunk/CMakeLists.txt Wed Sep 10 03:34:41 2014
@@ -27,6 +27,8 @@ message("Build type is \"${CMAKE_BUILD_T
 
 project(qpid-dispatch C)
 
+file(STRINGS "${CMAKE_SOURCE_DIR}/VERSION.txt" QPID_DISPATCH_VERSION)
+
 cmake_minimum_required(VERSION 2.6)
 include(CheckLibraryExists)
 include(CheckSymbolExists)
@@ -136,6 +138,12 @@ install(PROGRAMS ${TOOLS_EXECUTABLES}
 install(FILES ${DOC_FILES}
         DESTINATION ${DOC_INSTALL_DIR}/qpid-dispatch)
 
+configure_file(${CMAKE_CURRENT_SOURCE_DIR}/setup.py.in
+               ${CMAKE_CURRENT_BINARY_DIR}/setup.py)
+
+install(CODE "execute_process(COMMAND ${PYTHON_EXECUTABLE} setup.py install --prefix=${CMAKE_INSTALL_PREFIX} WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})")
+
+
 ## Build Tests first as other subdirs use tests/run.py
 ##
 add_subdirectory(tests)

Added: qpid/dispatch/trunk/VERSION.txt
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/VERSION.txt?rev=1623899&view=auto
==============================================================================
--- qpid/dispatch/trunk/VERSION.txt (added)
+++ qpid/dispatch/trunk/VERSION.txt Wed Sep 10 03:34:41 2014
@@ -0,0 +1 @@
+0.3

Modified: qpid/dispatch/trunk/config.sh
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/config.sh?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/config.sh (original)
+++ qpid/dispatch/trunk/config.sh Wed Sep 10 03:34:41 2014
@@ -26,7 +26,9 @@ export SOURCE_DIR=$(pwd)
 export BUILD_DIR=$SOURCE_DIR/build
 export INSTALL_DIR=$SOURCE_DIR/install
 
+PYTHON_LIB=$(python -c "from distutils.sysconfig import get_python_lib; print get_python_lib(prefix='$INSTALL_DIR')")
+
 export QPID_DISPATCH_HOME=$INSTALL_DIR/lib/qpid-dispatch
 export LD_LIBRARY_PATH=$INSTALL_DIR/lib64:$INSTALL_DIR/lib:$LD_LIBRARY_PATH
-export PYTHONPATH=$QPID_DISPATCH_HOME/python:$PYTHONPATH
+export PYTHONPATH=$QPID_DISPATCH_HOME/python:$PYTHON_LIB:$PYTHONPATH
 export PATH=$INSTALL_DIR/sbin:$INSTALL_DIR/bin:$SOURCE_DIR/bin:$PATH

Modified: qpid/dispatch/trunk/doc/man/qdrouterd_conf_man.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/doc/man/qdrouterd_conf_man.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/doc/man/qdrouterd_conf_man.py (original)
+++ qpid/dispatch/trunk/doc/man/qdrouterd_conf_man.py Wed Sep 10 03:34:41 2014
@@ -21,7 +21,7 @@
 Generate the qdrouterd.conf man page from the qdrouterd management schema."""
 
 import sys
-from qpid_dispatch_internal.management import QdSchema
+from qpid_dispatch_internal.management.qdrouter import QdSchema
 
 def make_man_page(filename):
     """Generate a man page for the configuration file from L{QdSchema} descriptions"""

Copied: qpid/dispatch/trunk/python/qpid_dispatch/__init__.py (from r1618265, qpid/dispatch/trunk/config.sh)
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch/__init__.py?p2=qpid/dispatch/trunk/python/qpid_dispatch/__init__.py&p1=qpid/dispatch/trunk/config.sh&r1=1618265&r2=1623899&rev=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/config.sh (original)
+++ qpid/dispatch/trunk/python/qpid_dispatch/__init__.py Wed Sep 10 03:34:41 2014
@@ -14,19 +14,6 @@
 # "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.
+# under the License
 #
-
-if [[ ! -f config.sh ]]; then
-    echo "You must source config.sh from within its own directory"
-    return
-fi
-
-export SOURCE_DIR=$(pwd)
-export BUILD_DIR=$SOURCE_DIR/build
-export INSTALL_DIR=$SOURCE_DIR/install
-
-export QPID_DISPATCH_HOME=$INSTALL_DIR/lib/qpid-dispatch
-export LD_LIBRARY_PATH=$INSTALL_DIR/lib64:$INSTALL_DIR/lib:$LD_LIBRARY_PATH
-export PYTHONPATH=$QPID_DISPATCH_HOME/python:$PYTHONPATH
-export PATH=$INSTALL_DIR/sbin:$INSTALL_DIR/bin:$SOURCE_DIR/bin:$PATH
+"""Qpid dispatch package"""

Copied: qpid/dispatch/trunk/python/qpid_dispatch/management/__init__.py (from r1618265, qpid/dispatch/trunk/config.sh)
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch/management/__init__.py?p2=qpid/dispatch/trunk/python/qpid_dispatch/management/__init__.py&p1=qpid/dispatch/trunk/config.sh&r1=1618265&r2=1623899&rev=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/config.sh (original)
+++ qpid/dispatch/trunk/python/qpid_dispatch/management/__init__.py Wed Sep 10 03:34:41 2014
@@ -14,19 +14,8 @@
 # "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.
+# under the License
 #
+"""AMQP management client package."""
 
-if [[ ! -f config.sh ]]; then
-    echo "You must source config.sh from within its own directory"
-    return
-fi
-
-export SOURCE_DIR=$(pwd)
-export BUILD_DIR=$SOURCE_DIR/build
-export INSTALL_DIR=$SOURCE_DIR/install
-
-export QPID_DISPATCH_HOME=$INSTALL_DIR/lib/qpid-dispatch
-export LD_LIBRARY_PATH=$INSTALL_DIR/lib64:$INSTALL_DIR/lib:$LD_LIBRARY_PATH
-export PYTHONPATH=$QPID_DISPATCH_HOME/python:$PYTHONPATH
-export PATH=$INSTALL_DIR/sbin:$INSTALL_DIR/bin:$SOURCE_DIR/bin:$PATH
+from .client import *

Copied: qpid/dispatch/trunk/python/qpid_dispatch/management/client.py (from r1618265, qpid/dispatch/trunk/python/qpid_dispatch_internal/management/client.py)
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch/management/client.py?p2=qpid/dispatch/trunk/python/qpid_dispatch/management/client.py&p1=qpid/dispatch/trunk/python/qpid_dispatch_internal/management/client.py&r1=1618265&r2=1623899&rev=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/client.py (original)
+++ qpid/dispatch/trunk/python/qpid_dispatch/management/client.py Wed Sep 10 03:34:41 2014
@@ -22,13 +22,13 @@ AMQP management client for Qpid dispatch
 """
 
 import proton, re, threading
-from error import ManagementError, OK, NOT_FOUND
-from schema import Schema
+from .error import *
+from . import entity
 
-class Url:
+class Url(object):
     """Simple AMQP URL parser/constructor"""
 
-    RE = re.compile(r"""
+    URL_RE = re.compile(r"""
     # [   <scheme>://  ] [   <user>   [    : <password> ]  @]      ( <host4>   | \[    <host6>    \] ) [   :<port>   ]   [ / path]
     ^ (?: ([^:/@]+)://)? (?: ([^:/@]+) (?: : ([^:/@]+)  )? @)? (?: ([^@:/\[]+) | \[ ([a-f0-9:.]+) \] ) (?: :([0-9]+))? (?: / (.*))? $
 """, re.X | re.I)
@@ -51,7 +51,7 @@ class Url:
             self.__dict__.update(url.__dict__)
 
         elif url is not None:   # Parse from url
-            match = Url.RE.match(url)
+            match = Url.URL_RE.match(url)
             if match is None:
                 raise ValueError("Invalid AMQP URL: %s"%url)
             self.scheme, self.user, self.password, host4, host6, port, self.path = match.groups()
@@ -84,33 +84,115 @@ class Url:
         return s
 
     def __eq__(self, url):
-         if isinstance(url, basestring):
-             url = Url(url)
-         return \
-             self.scheme==url.scheme and \
-             self.user==url.user and self.password==url.password and \
-             self.host==url.host and self.port==url.port and \
-             self.path==url.path
+        if isinstance(url, basestring):
+            url = Url(url)
+        return \
+            self.scheme == url.scheme and \
+            self.user == url.user and self.password == url.password and \
+            self.host == url.host and self.port == url.port and \
+            self.path == url.path
 
     def __ne__(self, url):
-         return not self.__eq__(url)
+        return not self.__eq__(url)
 
     def defaults(self):
         """"Fill in defaults for scheme and port if missing """
         if not self.scheme: self.scheme = self.AMQP
         if not self.port:
-            if self.scheme==self.AMQP: self.port = 5672
-            elif self.scheme==self.AMQPS: self.port = 5671
+            if self.scheme == self.AMQP: self.port = 5672
+            elif self.scheme == self.AMQPS: self.port = 5671
             else: raise ValueError("Invalid URL scheme: %s"%self.scheme)
         return self
 
 def remove_none(d):
     """@return: A copy of d without any None values. Does not modify d."""
-    return dict((k, v) for k, v in d.iteritems() if v is not None)
+    return dict((unicode(k), v) for k, v in d.iteritems() if v is not None)
+
+
+class MessengerImpl(object):
+    """
+    Messaging implementation for L{Node} based on proton.Messenger
+    @ivar reply_to: address for replies to the node.
+    """
+
+    def __init__(self, address, timeout=None):
+        self.messenger = proton.Messenger()
+        self.messenger.start()
+        self.messenger.timeout = timeout
+        subscribe_address = Url(address)
+        subscribe_address.path = "#"
+        self.subscription = self.messenger.subscribe(str(subscribe_address))
+        self._flush()
+        self.reply_to = self.subscription.address
+        if not self.reply_to:
+            raise ValueError("Failed to subscribe to %s"%subscribe_address)
+
+    def send(self, request):
+        """Send a message"""
+        self.messenger.put(request)
+        self.messenger.send()
+        self._flush()
+
+    def fetch(self):
+        """Wait for a single message."""
+        self.messenger.recv(1)
+        response = proton.Message()
+        self.messenger.get(response)
+        return response
+
+    def _flush(self):
+        """Call self.messenger.work() till there is no work left."""
+        while self.messenger.work(0.1): pass
+
+    def stop(self):
+        """Stop the messaging implementation"""
+        self.messenger.stop()
+
+
+class Entity(entity.Entity):
+    """
+    Proxy for an AMQP manageable entity.
+
+    Modifying local attributes dict will not change the remote entity. Call
+    update() to send the local attributes to the remote entity.
+
+    The standard AMQP requests read, update and delete are defined
+    here. create() is defined on L{Node}.
+
+    Attribute access:
+    - via index operator: entity['foo']
+    - as python attributes: entity.foo (only if attribute name is a legal python identitfier)
+
+    @ivar attributes: Map of attribute values for this entity.
+    """
+
+    def __init__(self, node, attributes=None, **kwattrs):
+        super(Entity, self).__init__(attributes, **kwattrs)
+        self.__dict__['_node'] = node # Avoid getattr recursion
+
+    def call(self, operation, expect=OK, **arguments):
+        """Call an arbitrary management method on this entity"""
+        request = self._node.request(
+            operation=operation, type=self.type, identity=self.identity, **arguments)
+        return self._node.call(request, expect=expect).body
+
+    def read(self):
+        """Read the remote entity attributes into the local attributes."""
+        self.attributes = self.call('READ', expect=OK)
+
+    def update(self):
+        """Update the remote entity attributes from the local attributes."""
+        self.attributes = self.call('UPDATE', expect=OK, body=self.attributes)
+
+    def delete(self):
+        """Delete the remote entity"""
+        self.call('DELETE', expect=NO_CONTENT)
+
 
 class Node(object):
+    """Client proxy for an AMQP management node"""
 
-    def __init__(self, address=None, router=None, locales=None, timeout=10):
+    def __init__(self, address=None, router=None, locales=None, timeout=10, message_impl=None):
         """
         @param address: AMQP address of the management node.
         @param router: If address does not contain a path, use the management node for this router ID.
@@ -118,7 +200,7 @@ class Node(object):
         @param locales: Default list of locales for management operations.
         """
         self.name = self.identity = 'self'
-        self.type='org.amqp.management' # AMQP management node type
+        self.type = 'org.amqp.management' # AMQP management node type
 
         self.address = Url(address).defaults()
         self.locales = locales
@@ -128,33 +210,20 @@ class Node(object):
             else:
                 self.address.path = '$management'
         self.responses = {}
-
-        self.messenger = proton.Messenger()
-        self.messenger.start()
-        self.messenger.timeout = timeout
-        subscribe_address = Url(address)
-        subscribe_address.path = "#"
-        self.subscription = self.messenger.subscribe(str(subscribe_address))
-        self._flush()
-        self.reply_to = self.subscription.address
-        if not self.reply_to:
-            raise ValueError("Failed to subscribe to %s"%subscribe_address)
+        self.message_impl = message_impl or MessengerImpl(self.address, timeout=timeout)
+        self.reply_to = self.message_impl.reply_to
 
     def stop(self):
-        if not self.messenger: return
-        self.messenger.stop()
-        self.messenger = None
+        """Shut down the node"""
+        if not self.message_impl: return
+        self.message_impl.stop()
+        self.message_impl = None
 
     def __del__(self):
-        if hasattr(self, 'messenger'):
+        if hasattr(self, 'message_impl'):
             try: self.stop()
             except: pass
 
-    def _flush(self):
-        """Call self.messenger.work() till there is no work left."""
-        while self.messenger.work(0.1):
-            pass
-
     def __repr__(self):
         return "%s(%s)"%(self.__class__.__name__, self.address)
 
@@ -167,16 +236,21 @@ class Node(object):
             self.CORRELATION_ID += 1
             return self.CORRELATION_ID
 
-    def check_response(self, response, request):
+    def check_response(self, response, request, expect=OK):
         """
-        Check a manaement response message for errors and correlation ID.
+        Check a management response message for errors and correlation ID.
         """
         code = response.properties.get('statusCode')
-        if code != OK:
-            raise ManagementError(code, response.properties.get('statusDescription'))
+        if code != expect:
+            if 200 <= code <= 299:
+                raise ValueError("Response was %s(%s) but expected %s(%s): %s" % (
+                    code, STATUS_TEXT[code], expect, STATUS_TEXT[expect],
+                    response.properties.get('statusDescription')))
+            else:
+                raise ManagementError.create(code, response.properties.get('statusDescription'))
         if response.correlation_id != request.correlation_id:
-            raise ManagementError(NOT_FOUND, "Bad correlation id request=%s, response=%s"%(
-                request.correlation_id, response.correlation_id))
+            raise NotFound("Bad correlation id request=%s, response=%s" %
+                               (request.correlation_id, response.correlation_id))
 
 
     def request(self, body=None, **properties):
@@ -188,18 +262,18 @@ class Node(object):
         """
         if self.locales: properties.setdefault('locales', self.locales)
         request = proton.Message()
-        request.address=str(self.address)
-        request.reply_to=self.reply_to
-        request.correlation_id=self.correlation_id()
-        request.properties=remove_none(properties)
-        request.body=body or {}
+        request.address = str(self.address)
+        request.reply_to = self.reply_to
+        request.correlation_id = self.correlation_id()
+        request.properties = remove_none(properties)
+        request.body = body or {}
         return request
 
     def node_request(self, body=None, **properties):
+        """Construct a request for the managment node itself"""
         return self.request(body, name=self.name, type=self.type, **properties)
 
-    # TODO aconway 2014-06-03: async send/receive
-    def call(self, request):
+    def call(self, request, expect=OK):
         """
         Send a management request message, wait for a response.
         @return: Response message.
@@ -208,13 +282,9 @@ class Node(object):
             raise ValueError("Message must have an address")
         if not request.reply_to:
             raise ValueError("Message must have reply_to %s", request)
-        self.messenger.put(request)
-        self.messenger.send()
-        self._flush()
-        self.messenger.recv(1)
-        response = proton.Message()
-        self.messenger.get(response)
-        self.check_response(response, request)
+        self.message_impl.send(request)
+        response = self.message_impl.fetch()
+        self.check_response(response, request, expect=expect)
         return response
 
     class QueryResponse(object):
@@ -222,18 +292,20 @@ class Node(object):
         Result returned by L{query}.
         @ivar attribute_names: List of attribute names for the results.
         @ivar results: list of lists of attribute values in same order as attribute_names
-        @ivar result_maps: Results as list of attributes maps {name: value}.
+        @ivar entities: Results as list of L{Entity}
         """
-        def __init__(self, response):
+        def __init__(self, node, response):
             """
             @param response: the respose message to a query.
             """
+            self.node = node
             self.attribute_names = response.body['attributeNames']
             self.results = response.body['results']
 
         @property
-        def result_maps(self):
-            return [dict(zip(self.attribute_names, r)) for r in self.results]
+        def entities(self):
+            """Results as list of entities."""
+            return [Entity(self.node, dict(zip(self.attribute_names, r))) for r in self.results]
 
         def __str__(self):
             return "QueryResponse(attribute_names=%s, results=%s"%(
@@ -254,7 +326,7 @@ class Node(object):
             operation='QUERY', entityType=type, offset=offset, count=count)
 
         response = self.call(request)
-        return Node.QueryResponse(response)
+        return Node.QueryResponse(self, response)
 
     def create(self, type, name, attributes=None):
         """
@@ -262,20 +334,30 @@ class Node(object):
         @param type: Type of entity to create.
         @param name: Name for the new entity.
         @param attributes: Map of attribute name:value for the new entity.
-        @return: Map of attributes returned by create (with defaults etc.)
+        @return: Entity proxy for the new entity.
         """
         request = self.request(operation='CREATE', type=type, name=name, body=attributes or {})
-        response = self.call(request)
-        return response.body
+        return Entity(self, self.call(request, expect=CREATED).body)
+
 
     def read(self, type, name=None, identity=None):
         """
-        Send an AMQP management read request. Must specify one of name or identity.
+        Read an AMQP entity, specify exactly one of name or identity.
         @param type: Type of entity to read.
-        @param name: Name of entity to read.
-        @param identity: Identity of entity to read.
-        @return: Map of attributes for the requested entity.
+        @return: An L{Entity}
         """
         request = self.request(operation='READ', type=type, name=name, identity=identity)
-        response = self.call(request)
-        return response.body
+        return Entity(self, self.call(request).body)
+
+    def get_types(self, type=None):
+        return self.call(self.node_request(operation="GET-TYPES", entityType=type)).body
+
+    def get_attributes(self, type=None):
+        return self.call(self.node_request(operation="GET-ATTRIBUTES", entityType=type)).body
+
+    def get_operations(self, type=None):
+        return self.call(self.node_request(operation="GET-OPERATIONS", entityType=type)).body
+
+    def get_other_nodes(self):
+        return self.call(self.node_request(operation="GET-MGMT-NODES")).body
+

Added: qpid/dispatch/trunk/python/qpid_dispatch/management/entity.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch/management/entity.py?rev=1623899&view=auto
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch/management/entity.py (added)
+++ qpid/dispatch/trunk/python/qpid_dispatch/management/entity.py Wed Sep 10 03:34:41 2014
@@ -0,0 +1,63 @@
+#
+# 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
+#
+
+"""
+AMQP Managment Entity
+"""
+
+class Entity(object):
+    """
+    A collection of named attributes.
+
+    Attribute access:
+    - via index operator: entity['foo']
+    - as python attributes: entity.foo (only if attribute name is a legal python identitfier)
+
+    @ivar attributes: Map of attribute values for this entity.
+
+    NOTE: Entity does not itself implement the python map protocol because map
+    methods (in particular 'update') can clash with AMQP methods and attributes.
+    """
+
+    def __init__(self, attributes=None, **kwargs):
+        self.__dict__['attributes'] = dict(attributes or [], **kwargs)
+
+    def __getitem__(self, name): return self.attributes[name]
+
+    def __setitem__(self, name, value): self.attributes[name] = value
+
+    def __getattr__(self, name):
+        try:
+            return self.attributes[name]
+        except KeyError:
+            raise AttributeError("'%s' object has no attribute '%s'" % (type(self).__name__, name))
+
+    def __setattr__(self, name, value):
+        if name in self.__dict__:
+            super(Entity, self).__setattr__(name, value)
+        else:
+            self.attributes[name] = value
+
+    def __delattr__(self, name):
+        try:
+            del self.attributes[name]
+        except KeyError:
+            super(Entity, self).__delattr__(name)
+
+    def __repr__(self): return "Entity(%r)" % self.attributes

Copied: qpid/dispatch/trunk/python/qpid_dispatch/management/error.py (from r1618265, qpid/dispatch/trunk/python/qpid_dispatch_internal/management/error.py)
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch/management/error.py?p2=qpid/dispatch/trunk/python/qpid_dispatch/management/error.py&p1=qpid/dispatch/trunk/python/qpid_dispatch_internal/management/error.py&r1=1618265&r2=1623899&rev=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/error.py (original)
+++ qpid/dispatch/trunk/python/qpid_dispatch/management/error.py Wed Sep 10 03:34:41 2014
@@ -17,13 +17,13 @@
 # under the License
 #
 
-# HTTP status codes used by AMQP (extend as needed)
+"""
+ManagementError exception class and subclasses, with status codes used by AMQP.
+"""
 
 from httplib import responses as STATUS_TEXT
-
-STATUS_CODES=['OK', 'BAD_REQUEST', 'UNAUTHORIZED', 'FORBIDDEN', 'NOT_FOUND', 'REQUEST_TIMEOUT', 'INTERNAL_SERVER_ERROR', 'NOT_IMPLEMENTED']
-
-for code in STATUS_CODES: exec("from httplib import %s" % code)
+from httplib import OK, NO_CONTENT, CREATED, \
+    BAD_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, INTERNAL_SERVER_ERROR, NOT_IMPLEMENTED
 
 class ManagementError(Exception):
     """
@@ -34,10 +34,25 @@ class ManagementError(Exception):
     """
     def __init__(self, status, description):
         self.status, self.description = status, description
+        super(ManagementError, self).__init__(description)
 
-    def __str__(self):
-        text = STATUS_TEXT.get(self.status) or "Unknown status %s"%self.status
-        if text in self.description: return self.description
-        else: return "%s: %s"%(text, self.description)
-
-__all__ = STATUS_CODES + ['ManagementError', 'STATUS_TEXT']
+    @staticmethod
+    def create(status, description):
+        """Create the appropriate ManagementError subclass for status"""
+        try:
+            class_name = '' + (STATUS_TEXT[status].replace(' ',''))
+            return globals()[class_name](description)
+        except KeyError, e:
+            return ManagementError(status, description)
+
+def _error_class(code):
+    class Error(ManagementError):
+        def __init__(self, description): ManagementError.__init__(self, code, description)
+    return Error
+
+class BadRequest(_error_class(BAD_REQUEST)): pass
+class Unauthorized(_error_class(UNAUTHORIZED)): pass
+class Forbidden(_error_class(FORBIDDEN)): pass
+class NotFound(_error_class(NOT_FOUND)): pass
+class InternalServerError(_error_class(INTERNAL_SERVER_ERROR)): pass
+class NotImplemented(_error_class(NOT_IMPLEMENTED)): pass

Modified: 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=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py (original)
+++ qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py Wed Sep 10 03:34:41 2014
@@ -16,16 +16,5 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-"""Management package"""
+"""Qpid Dispatch internal management package."""
 
-
-from .client import Node, Url
-from .qdrouter import QdSchema
-from .config import Config, configure_dispatch
-from .schema import Type, BooleanType, EnumType, EnumValue, AttributeType, EntityType, Schema, schema_file, ValidationError
-from .entity import Entity
-
-from  .error import ManagementError, STATUS_CODES, STATUS_TEXT
-for code in STATUS_CODES: exec("from .error import %s"%code)
-
-__all__ = ["ManagementError", "Node", "Url", "QdSchema", "Config", "configure_dispatch", "Type", "BooleanType", "EnumType", "EnumValue", "AttributeType", "EntityType", "Schema", "schema_file", "ValidationError", "ManagementError", "STATUS_CODES", "STATUS_TEXT"] + STATUS_CODES

Modified: qpid/dispatch/trunk/python/qpid_dispatch_internal/management/agent.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch_internal/management/agent.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/agent.py (original)
+++ qpid/dispatch/trunk/python/qpid_dispatch_internal/management/agent.py Wed Sep 10 03:34:41 2014
@@ -17,39 +17,55 @@
 # under the License
 #
 
-import re
-from qpid_dispatch_internal import dispatch_c
+"""
+Pyton agent for dispatch router.
+
+Implements the server side of the AMQP management protocol for the dispatch router.
+Manages a set of manageable Entities that can be Created, Read, Updated and Deleted.
+Entity types are described by the schema in qdrouter.json.
+"""
+
+from traceback import format_exc
+from threading import Lock
 from dispatch import IoAdapter, LogAdapter, LOG_DEBUG, LOG_ERROR
-from error import ManagementError, OK, BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_IMPLEMENTED, NOT_FOUND
-from schema import ValidationError, Entity as SchemaEntity
-from qdrouter import QdSchema
+from qpid_dispatch.management.error import ManagementError, OK, CREATED, NO_CONTENT, STATUS_TEXT, \
+    BadRequest, InternalServerError, NotImplemented, NotFound, Forbidden
+from .. import dispatch_c
+from .schema import ValidationError, Entity as SchemaEntity
+from .qdrouter import QdSchema
 from ..router.message import Message
-from traceback import format_exc
-from ..compat import OrderedDict
 
 def dictstr(d):
     """Stringify a dict in the form 'k=v, k=v ...' instead of '{k:v, ...}'"""
     return ", ".join("%s=%r" % (k, v) for k, v in d.iteritems())
 
 def required_property(prop, request):
+    """Raise exception if required property is missing"""
     if prop not in request.properties:
-        raise ManagementError(BAD_REQUEST, "No '%s' property: %s"%(prop,request))
+        raise BadRequest("No '%s' property: %s"%(prop, request))
     return request.properties[prop]
 
-def not_implemented(operation, type):
-    raise ManagementError(NOT_IMPLEMENTED,
-                          "Operation '%s' not implemented on %s"  % (operation, type))
-
+def not_implemented(operation, entity_type):
+    """Raise NOT_IMPLEMENTED exception"""
+    raise NotImplemented("Operation '%s' not implemented on %s"  % (operation, entity_type))
+
+class AtomicCount(object):
+    """Simple atomic counter"""
+    def __init__(self, count=0):
+        self.count = count
+        self.lock = Lock()
+
+    def next(self):
+        with self.lock:
+            self.count  += 1
+            return self.count
 
 class Entity(SchemaEntity):
     """
     Base class for agent entities with operations as well as attributes.
-
-    Implements "read" operation to return attributes, other basic
-    operations throw NOT_IMPLEMENTED, they must be implemented by subclasses.
     """
 
-    def __init__(self, qd, dispatch, entity_type, attributes):
+    def __init__(self, agent, entity_type, attributes):
         """
         @para qd: Dispatch C library.
         @param dispatch: Pointer to qd_dispatch C object.
@@ -58,16 +74,29 @@ class Entity(SchemaEntity):
         """
         super(Entity, self).__init__(entity_type, attributes)
         # Direct __dict__ access to avoid validation as schema attributes
-        self.__dict__['_qd'] = qd
-        self.__dict__['_dispatch'] = dispatch
+        self.__dict__['_agent'] = agent
+        self.__dict__['_qd'] = agent.qd
+        self.__dict__['_dispatch'] = agent.dispatch
 
-    def create(self, request): not_implemented('create', self.type)
 
-    def read(self, request): return dict(self.attributes)
-
-    def update(self, request): not_implemented('update', self.type)
+    def create(self, request):
+        """Subclasses can add extra create actions here"""
+        pass
 
-    def delete(self, request): not_implemented('delete', self.type)
+    def read(self, request):
+        """Handle read request, default is to return attributes."""
+        return (OK, self.attributes)
+
+    def update(self, request):
+        self.entity_type.allowed('update')
+        self.attributes.update(request.body)
+        self.validate()
+        return (OK, self.attributes)
+
+    def delete(self, request):
+        self.entity_type.allowed('delete')
+        self._agent.delete(self)
+        return (NO_CONTENT, {})
 
 
 class ContainerEntity(Entity):
@@ -106,23 +135,24 @@ class FixedAddressEntity(Entity):
 class WaypointEntity(Entity):
     def create(self, request):
         self._qd.qd_dispatch_configure_waypoint(self._dispatch, self)
-        self._qd.qd_waypoint_activate_all(self._dispatch);
+        self._qd.qd_waypoint_activate_all(self._dispatch)
 
+class DummyEntity(Entity):
 
-class Agent(object):
+    id_count = AtomicCount()
+
+    def create(self, request):
+        self['identity'] = self.next_id()
+        return (OK, self.attributes)
+
+    def next_id(self): return self.type+str(self.id_count.next())
+
+    def callme(self, request):
+        return (OK, dict(**request.properties))
 
-    def entity(self, attributes):
-        """Create an instance of the implementation class for an entity"""
-        if not 'type' in attributes:
-            raise ManagementError(BAD_REQUEST, "No 'type' attributre in %s" % attributes)
-        entity_type = self.schema.entity_type(attributes['type'])
-        class_name = ''.join([n.capitalize() for n in entity_type.short_name.split('-')])
-        class_name += 'Entity'
-        entity_class = globals().get(class_name)
-        if not entity_class:
-            raise ManagementError(INTERNAL_SERVER_ERROR,
-                                  "Can't find implementation for %s" % entity_type)
-        return entity_class(self.qd, self.dispatch, entity_type, attributes)
+
+class Agent(object):
+    """AMQP managment agent"""
 
     def __init__(self, dispatch, attribute_maps=None):
         self.qd = dispatch_c.instance()
@@ -130,58 +160,73 @@ class Agent(object):
         # FIXME aconway 2014-06-26: merge with $management
         self.io = [IoAdapter(self.receive, "$management2"),
                    IoAdapter(self.receive, "$management2", True)] # Global
-        self.log = LogAdapter("AGENT").log
+        self.log = LogAdapter("PYAGENT").log                      # FIXME aconway 2014-09-08: AGENT
         self.schema = QdSchema()
-        self.entities = [self.entity(attributes) for attributes in attribute_maps or []]
+        self.entities = [self.create_entity(attributes) for attributes in attribute_maps or []]
         self.name = self.identity = 'self'
-        self.type='org.amqp.management' # AMQP management node type
+        self.type = 'org.amqp.management' # AMQP management node type
+
+    def create_entity(self, attributes):
+        """Create an instance of the implementation class for an entity"""
+        if not 'type' in attributes:
+            raise BadRequest("No 'type' attribute in %s" % attributes)
+        entity_type = self.schema.entity_type(attributes['type'])
+        class_name = ''.join([n.capitalize() for n in entity_type.short_name.split('-')])
+        class_name += 'Entity'
+        entity_class = globals().get(class_name)
+        if not entity_class:
+            raise InternalServerError("Can't find implementation for %s" % entity_type)
+        return entity_class(self, entity_type, attributes)
 
-    def respond(self, request, status=OK, description="OK", body=None):
+    def respond(self, request, status=OK, description=None, body=None):
+        """Send a response to the client"""
+        description = description or STATUS_TEXT[status]
         response = Message(
             address=request.reply_to,
             correlation_id=request.correlation_id,
-            properties={'statusCode': status, 'statusDescription': description },
+            properties={'statusCode': status, 'statusDescription': description},
             body=body or {})
-        self.log(LOG_DEBUG, "Agent response %s (to %s)"%(response, request))
+        self.log(LOG_DEBUG, "Agent response:\n  %s\n  Responding to: \n  %s"%(response, request))
         try:
             self.io[0].send(response)
         except:
             self.log(LOG_ERROR, "Can't respond to %s: %s"%(request, format_exc()))
 
     def receive(self, request, link_id):
+        """Called when a management request is received."""
         self.log(LOG_DEBUG, "Agent request %s on link %s"%(request, link_id))
         def error(e, trace):
+            """Raise an error"""
             self.log(LOG_ERROR, "Error dispatching %s: %s\n%s"%(request, e, trace))
             self.respond(request, e.status, e.description)
         try:
-            self.respond(request, body=self.handle(request))
+            status, body = self.handle(request)
+            self.respond(request, status=status, body=body)
         except ManagementError, e:
             error(e, format_exc())
         except ValidationError, e:
-            error(ManagementError(BAD_REQUEST, str(e)), format_exc())
+            error(BadRequest(str(e)), format_exc())
         except Exception, e:
-            error(ManagementError(INTERNAL_SERVER_ERROR,
-                                  "%s: %s"%(type(e).__name__ , e)), format_exc())
+            error(InternalServerError("%s: %s"%(type(e).__name__, e)), format_exc())
 
     def handle(self, request):
         """
         Handle a request.
         Dispatch management node requests to self, entity requests to the entity.
+        @return: (response-code, body)
         """
         operation = required_property('operation', request)
         type = required_property('type', request)
+        if type == self.type or operation.lower() == 'create':
+            target = self
+        else:
+            target = self.find_entity(request)
+            target.entity_type.allowed(operation)
         try:
-            if type == self.type or operation.lower() == 'create':
-                # This is a management node request, handle it myself.
-                method = getattr(self, operation.lower())
-                return method(request)
-            else:
-                # This is an entity request, find the entity to handle it.
-                entity = self.find_entity(request)
-                method = getattr(entity, operation.lower())
-                return method(request)
+            method = getattr(target, operation.lower())
         except AttributeError:
-            not_implemented(operation, self.type)
+            not_implemented(operation, type)
+        return method(request)
 
     def query(self, request):
         """Management node query operation"""
@@ -190,7 +235,7 @@ class Agent(object):
             try:
                 entity_type = self.schema.entity_type(entity_type)
             except:
-                raise ManagementError(BAD_REQUEST, "Unknown entity type '%s'" % entity_type)
+                raise BadRequest("Unknown entity type '%s'" % entity_type)
         attribute_names = request.body.get('attributeNames')
         if not attribute_names:
             if entity_type:
@@ -204,7 +249,7 @@ class Agent(object):
         results = [[e.attributes.get(a) for a in attribute_names]
                    for e in self.entities
                    if not entity_type or e.type == entity_type.name]
-        return { 'attributeNames': attribute_names, 'results': results }
+        return (OK, {'attributeNames': attribute_names, 'results': results})
 
     def create(self, request):
         """
@@ -216,29 +261,35 @@ class Agent(object):
         for a in ['type', 'name']:
             value = required_property(a, request)
             if a in attributes and attributes[a] != value:
-                raise ManagementError(BAD_REQUEST, "Conflicting values for '%s'"%a)
+                raise BadRequest("Conflicting values for '%s'"%a)
             attributes[a] = value
-        entity = self.entity(attributes)
+        entity = self.create_entity(attributes)
+        entity.entity_type.allowed('create')
         # Validate in the context of the existing entities for uniqueness
         self.schema.validate_all([entity]+self.entities)
         entity.create(request)  # Send the create request to the entity
         self.entities.append(entity)
-        return dict(entity.attributes)
+        return (CREATED, entity.attributes)
 
     def find_entity(self, request):
+        """Find the entity addressed by request"""
         attr = [(name, value) for name, value in request.properties.iteritems()
                 if name in ['name', 'identity'] and value is not None]
         if len(attr) != 1:
-            raise ManagementError(
-                BAD_REQUEST, "Specify exactly one of 'name' or 'identity': %s" % request)
+            raise BadRequest("Specify exactly one of 'name' or 'identity': %s" % request)
         name, value = attr[0]
         try:
             entity = (e for e in self.entities if e.attributes.get(name) == value).next()
         except StopIteration:
-            raise ManagementError(NOT_FOUND, "No entity with %s='%s'" % (name, value))
+            raise NotFound("No entity with %s='%s'" % (name, value))
         expect_type = request.properties.get('type')
         if expect_type and entity.type != expect_type:
-            raise ManagementError(
-                NOT_FOUND, "Type mismatch, expected '%s', found '%s'" %
-                (expect_type, entity.type))
+            raise NotFound("Type mismatch, expected '%s', found '%s'" %
+                               (expect_type, entity.type))
         return entity
+
+    def delete(self, entity):
+        try:
+            self.entities.remove(entity)
+        except ValueError:
+            raise NotFound("Cannot delete, entity not found: %s"%entity)

Modified: qpid/dispatch/trunk/python/qpid_dispatch_internal/management/config.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/python/qpid_dispatch_internal/management/config.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/config.py (original)
+++ qpid/dispatch/trunk/python/qpid_dispatch_internal/management/config.py Wed Sep 10 03:34:41 2014
@@ -22,9 +22,9 @@ Configuration file parsing
 """
 
 import json, re, sys
-import schema
 from copy import copy
-from qpid_dispatch_internal import dispatch_c
+from .schema import ValidationError
+from .. import dispatch_c
 from .qdrouter import QdSchema
 from ..compat import json_load_kwargs
 
@@ -34,7 +34,7 @@ class Config(object):
     def __init__(self, filename=None, schema=QdSchema()):
         self.schema = schema
         if filename:
-            e = self.load(filename)
+            self.load(filename)
         else:
             self.entities = []
 
@@ -113,9 +113,9 @@ class Config(object):
                     self.load(f)
                 except:
                     ex_type, ex_value, ex_trace = sys.exc_info()
-                    raise schema.ValidationError, "Loading '%s', %s: %s"%(source, ex_type.__name__, ex_value), ex_trace
+                    raise ValidationError, "Loading '%s', %s: %s"%(source, ex_type.__name__, ex_value), ex_trace
         else:
-            sections = self._parse(source);
+            sections = self._parse(source)
             # Add missing singleton sections
             for et in self.schema.entity_types.itervalues():
                 if et.singleton and not [s for s in sections if s[0] == et.short_name]:
@@ -152,9 +152,9 @@ def configure_dispatch(dispatch, filenam
     for w in config.by_type('waypoint'): qd.qd_dispatch_configure_waypoint(dispatch, w)
     for l in config.by_type('listener'): qd.qd_dispatch_configure_listener(dispatch, l)
     for c in config.by_type('connector'): qd.qd_dispatch_configure_connector(dispatch, c)
-    qd.qd_connection_manager_start(dispatch);
-    qd.qd_waypoint_activate_all(dispatch);
+    qd.qd_connection_manager_start(dispatch)
+    qd.qd_waypoint_activate_all(dispatch)
 
-    # TODO aconway 2014-07-09: messy dependency, can't import till after prepare
+    # NOTE: Can't import agent till after qd_dispatch_prepare
     from .agent import Agent
     qd.qd_dispatch_set_agent(dispatch, Agent(dispatch, config.entities))

Modified: 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=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.json (original)
+++ qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.json Wed Sep 10 03:34:41 2014
@@ -85,6 +85,7 @@
   },
   "entity_types": {
     "container": {
+      "allows": "CR",
       "singleton": true,
       "include": [
         "common"
@@ -103,6 +104,7 @@
     },
 
     "router": {
+      "allows": "CR",
       "singleton": true,
       "include": [
         "common"
@@ -155,6 +157,7 @@
 
     "listener": {
       "description": "Listens for incoming connections to the router",
+      "allows": "CR",
       "include": [
         "common",
         "ssl-profile",
@@ -190,6 +193,7 @@
     },
     "connector": {
       "description": "Establishes an outgoing connections from the router",
+      "allows": "CR",
       "include": [
         "common",
         "ssl-profile",
@@ -215,6 +219,7 @@
       "include": [
         "common"
       ],
+      "allows": "CR",
       "attributes": {
         "module": {
           "type":[
@@ -222,6 +227,7 @@
             "MESSAGE",
             "SERVER",
             "AGENT",
+	    "PYAGENT",
             "CONTAINER",
             "CONFIG",
             "DEFAULT",
@@ -257,6 +263,7 @@
     },
 
     "fixed-address": {
+      "allows": "CR",
       "include": [
         "common"
       ],
@@ -290,6 +297,7 @@
     },
 
     "waypoint": {
+      "allows": "CR",
       "include": [
         "common"
       ],
@@ -311,6 +319,18 @@
           "default": -1
         }
       }
+    },
+
+    "dummy": {
+      "description": "Dummy entity for test purposes.",
+      "allows": "CRUD",
+      "include": ["common"],
+      "attributes": {
+        "arg1": {"type": "String"},
+        "arg2": {"type": "String"},
+	"num1": {"type": "Integer"},
+	"num2": {"type": "Integer"}
+      }
     }
   }
 }

Modified: 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=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.py (original)
+++ qpid/dispatch/trunk/python/qpid_dispatch_internal/management/qdrouter.py Wed Sep 10 03:34:41 2014
@@ -22,7 +22,7 @@ Qpid Dispatch Router management schema a
 """
 
 import json
-import schema
+from . import schema
 from ..compat import json_load_kwargs
 
 class QdSchema(schema.Schema):

Modified: 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=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/schema.py (original)
+++ qpid/dispatch/trunk/python/qpid_dispatch_internal/management/schema.py Wed Sep 10 03:34:41 2014
@@ -26,10 +26,10 @@ check for uniqueness of enties/attribute
 A Schema can be loaded/dumped to a json file.
 """
 
-import os, sys, re
+import os, sys
+from qpid_dispatch.management import entity
+from qpid_dispatch.management.error import Forbidden
 from ..compat import OrderedDict
-import entity
-from collections import MutableMapping
 
 class ValidationError(Exception):
     """Error raised if schema validation fails"""
@@ -294,7 +294,7 @@ class AttributeTypeHolder(object):
         return self.name
 
 class IncludeType(AttributeTypeHolder):
-
+    """An include type defines a set of attributes that can be re-used by multiple EntityTypes"""
     def __init__(self, name, schema, attributes=None, description=""):
         super(IncludeType, self).__init__(name, schema, attributes, description)
         attributes = attributes or {}
@@ -313,7 +313,7 @@ class EntityType(AttributeTypeHolder):
     #ivar include: List of names of sections included by this entity.
     """
     def __init__(self, name, schema, singleton=False, include=None, attributes=None,
-                 description=""):
+                 description="", allows=""):
         """
         @param name: name of the entity type.
         @param schema: schema for this type.
@@ -321,12 +321,14 @@ class EntityType(AttributeTypeHolder):
         @param include: List of names of include types for this entity.
         @param attributes: Map of attributes {name: {type:, default:, required:, unique:}}
         @param description: Human readable description.
+        @param allows: Allowed operations, string of "CRUD"
         """
         super(EntityType, self).__init__(name, schema, attributes, description)
         self.short_name = schema.short_name(name)
         self.refs = {'entity-type': name}
         self.singleton = singleton
         self.include = include
+        self.allows = allows.upper()
         if include and self.schema.includes:
             for i in include:
                 if not i in schema.includes:
@@ -397,10 +399,17 @@ class EntityType(AttributeTypeHolder):
 
         return attributes
 
+    def allowed(self, operation):
+        """Raise excepiton if op is not a valid operation on entity."""
+        op = operation.upper()
+        if op[0] not in self.allows:
+            raise Forbidden("Operation '%s' not allowed for '%s'" % (op, self.name))
+
     def __repr__(self): return "%s(%s)" % (type(self).__name__, self.name)
 
     def __str__(self): return self.name
 
+
 class Schema(object):
     """
     Schema defining entity types.
@@ -454,6 +463,7 @@ class Schema(object):
         ])
 
     def entity_type(self, name):
+        """Look up an EntityType by name"""
         try:
             return self.entity_types[self.long_name(name)]
         except KeyError:
@@ -512,39 +522,19 @@ class Schema(object):
 
     def entities(self, attribute_maps):
         """Convert a list of attribute maps into a list of L{Entity}"""
-        return map(self.entity, attribute_maps)
+        return [self.entity(m) for m in attribute_maps]
 
 
 class Entity(entity.Entity):
-    """
-    An L{entity.Entity} with schema enforced entity type.
-    @ivar _entity_type: not private, _ prefix to avoid clash with attribute names.
-    """
-
-    def __init__(self, entity_type, attributes):
-        """
-        @param entity_type: L{EntityType}
-        @param attributes: map of atrribute name:value or list of [(name, value)]
-        """
-        super(Entity, self).__init__(attributes)
-        self.__dict__['_entity_type'] = entity_type # Avoid __getattr__ recursion
-        entity_type.validate(self)
-
-    @staticmethod
-    def from_schema(schema, attributes):
-        """
-        Look up attributes['type'] in schema and create an Entity.
-        """
-        entity_type = schema.entity_type.get(attributes['type'])
-        if not entity_type:
-            raise ValidationError("No 'type' attributre in %s" % attributes)
-        return Entity(entity_type, attributes)
-
-
-    def __setitem__(self, attrname, value):
-        if attrname not in self._entity_type.attributes:
-            raise ValidationError("Illegal attribute '%s' for %s" % (attrname, self._entity_type))
-        super(Entity, self).__setitem__(attrname, value);
+    """A map of attributes associated with an L{EntityType}"""
+    def __init__(self, entity_type, attributes=None, **kwattrs):
+        super(Entity, self).__init__(attributes, **kwattrs)
+        self.__dict__['entity_type'] = entity_type
+        self.validate()
+
+    def __setitem__(self, name, value):
+        super(Entity, self).__setitem__(name, value)
+        self.validate()
 
-    def __delitem__(self, attrname):
-        raise TypeError("Can't delete attributes from %s" % self)
+    def validate(self):
+        self.entity_type.validate(self)

Copied: qpid/dispatch/trunk/setup.py.in (from r1618265, qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py)
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/setup.py.in?p2=qpid/dispatch/trunk/setup.py.in&p1=qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py&r1=1618265&r2=1623899&rev=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/python/qpid_dispatch_internal/management/__init__.py (original)
+++ qpid/dispatch/trunk/setup.py.in Wed Sep 10 03:34:41 2014
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
 #
 # Licensed to the Apache Software Foundation (ASF) under one
 # or more contributor license agreements.  See the NOTICE file
@@ -16,16 +17,30 @@
 # specific language governing permissions and limitations
 # under the License.
 #
-"""Management package"""
+from distutils.core import setup
 
+pypi_long_description = """
 
-from .client import Node, Url
-from .qdrouter import QdSchema
-from .config import Config, configure_dispatch
-from .schema import Type, BooleanType, EnumType, EnumValue, AttributeType, EntityType, Schema, schema_file, ValidationError
-from .entity import Entity
+============
+qpid_dispatch
+=============
 
-from  .error import ManagementError, STATUS_CODES, STATUS_TEXT
-for code in STATUS_CODES: exec("from .error import %s"%code)
+qpid_dispatch contains the following sub-packages:
+- qpid_dispatch.management: AMQP management client library.
 
-__all__ = ["ManagementError", "Node", "Url", "QdSchema", "Config", "configure_dispatch", "Type", "BooleanType", "EnumType", "EnumValue", "AttributeType", "EntityType", "Schema", "schema_file", "ValidationError", "ManagementError", "STATUS_CODES", "STATUS_TEXT"] + STATUS_CODES
+"""
+
+setup(
+    name="qpid_dispatch",
+    version="${QPID_DISPATCH_VERSION}",
+    author="Apache Qpid",
+    author_email="dev@qpid.apache.org",
+    package_dir={'' : '${CMAKE_SOURCE_DIR}/python'},
+    packages=["qpid_dispatch", "qpid_dispatch.management"],
+    scripts=[],                # Installed by cmake
+    data_files=[],             # Installed by cmake
+    url="http://qpid.apache.org/",
+    license="Apache Software License",
+    description="Management libraries for Apache Qpid Dispatch.",
+    long_description=pypi_long_description
+)

Propchange: qpid/dispatch/trunk/setup.py.in
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: qpid/dispatch/trunk/setup.py.in
------------------------------------------------------------------------------
    svn:executable = *

Modified: qpid/dispatch/trunk/src/dispatch.c
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/src/dispatch.c?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/src/dispatch.c (original)
+++ qpid/dispatch/trunk/src/dispatch.c Wed Sep 10 03:34:41 2014
@@ -78,7 +78,7 @@ qd_error_t qd_dispatch_load_config(qd_di
 {
     PyObject *module=0, *configure_dispatch=0, *result=0;
     bool ok =
-        (module = PyImport_ImportModule("qpid_dispatch_internal.management")) &&
+        (module = PyImport_ImportModule("qpid_dispatch_internal.management.config")) &&
 	(configure_dispatch = PyObject_GetAttrString(module, "configure_dispatch")) &&
 	(result = PyObject_CallFunction(configure_dispatch, "(ls)", (long)qd, config_path));
     Py_XDECREF(module);

Modified: qpid/dispatch/trunk/src/schema_c.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/src/schema_c.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/src/schema_c.py (original)
+++ qpid/dispatch/trunk/src/schema_c.py Wed Sep 10 03:34:41 2014
@@ -22,7 +22,8 @@ Generate C code from the router schema.
 """
 
 import re
-from qpid_dispatch_internal.management import EnumType, QdSchema
+from qpid_dispatch_internal.management.schema import EnumType
+from qpid_dispatch_internal.management.qdrouter import QdSchema
 
 copyright="""/*
  * Licensed to the Apache Software Foundation (ASF) under one

Modified: qpid/dispatch/trunk/tests/management/__init__.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/management/__init__.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/tests/management/__init__.py (original)
+++ qpid/dispatch/trunk/tests/management/__init__.py Wed Sep 10 03:34:41 2014
@@ -17,7 +17,9 @@
 ## under the License
 ##
 
-from schema import SchemaTest
-from client import UrlTest
-from qdrouter import QdrouterTest
-from entity import EntityTest
+"""Management unit test package"""
+
+from client import *
+from schema import *
+from qdrouter import *
+from entity import *

Modified: qpid/dispatch/trunk/tests/management/client.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/management/client.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/tests/management/client.py (original)
+++ qpid/dispatch/trunk/tests/management/client.py Wed Sep 10 03:34:41 2014
@@ -20,7 +20,7 @@
 """Tests for management.amqp"""
 
 import unittest
-from qpid_dispatch_internal.management import Url
+from qpid_dispatch.management import Url
 
 class UrlTest(unittest.TestCase):
 

Modified: qpid/dispatch/trunk/tests/management/entity.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/management/entity.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/tests/management/entity.py (original)
+++ qpid/dispatch/trunk/tests/management/entity.py Wed Sep 10 03:34:41 2014
@@ -1,49 +1,43 @@
-##
-## 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
+#
+# 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
+#
 
 import unittest
-from qpid_dispatch_internal.management import Entity
+from qpid_dispatch.management.entity import Entity
 
 class EntityTest(unittest.TestCase):
 
     def test_entity(self):
         e = Entity({'foo-bar': 'baz'}, type='container', name='x')
-        self.assertEqual(e.name, 'x')
         self.assertEqual(e.attributes, {'type': 'container', 'name':'x', 'foo-bar': 'baz'})
-        self.assertEqual(e.foo_bar, 'baz')
-        self.assertEqual(e.attributes['foo-bar'], 'baz')
+        self.assertEqual(e.name, 'x')
+        self.assertEqual(e['name'], 'x')
 
-        e.foo_bar = 'x'
-        self.assertEqual(e.foo_bar, 'x')
-        self.assertEqual(e.attributes['foo-bar'], 'x')
+        e.name = 'y'
+        self.assertEqual(e.name, 'y')
+        self.assertEqual(e['name'], 'y')
+        self.assertEqual(e.attributes['name'], 'y')
 
         e.xx = 'xx'
         self.assertEqual(e.xx, 'xx')
+        self.assertEqual(e['xx'], 'xx')
         self.assertEqual(e.attributes['xx'], 'xx')
 
-        e.attributes['y-y'] = 'yy'
-        self.assertEqual(e.y_y, 'yy')
-        self.assertEqual(e.attributes['y-y'], 'yy')
-        def assign(): e.attributes['foo/bar'] = 1
-        self.assertRaises(KeyError, assign)
 
 if __name__ == '__main__':
     unittest.main()

Modified: qpid/dispatch/trunk/tests/management/qdrouter.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/management/qdrouter.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/tests/management/qdrouter.py (original)
+++ qpid/dispatch/trunk/tests/management/qdrouter.py Wed Sep 10 03:34:41 2014
@@ -20,7 +20,7 @@
 #pylint: disable=wildcard-import,unused-wildcard-import,missing-docstring,too-many-public-methods
 
 import unittest
-from qpid_dispatch_internal.management import Config
+from qpid_dispatch_internal.management.config import Config
 
 class QdrouterTest(unittest.TestCase):
     """Tests for qpid_dispatch_internal.config.qdrouter"""

Modified: qpid/dispatch/trunk/tests/management/schema.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/management/schema.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/tests/management/schema.py (original)
+++ qpid/dispatch/trunk/tests/management/schema.py Wed Sep 10 03:34:41 2014
@@ -21,7 +21,7 @@
 #pylint: disable=wildcard-import,missing-docstring,too-many-public-methods
 
 import unittest, json
-from qpid_dispatch_internal.management import Schema, BooleanType, EnumType, AttributeType, schema_file, ValidationError, EnumValue, EntityType
+from qpid_dispatch_internal.management.schema import Schema, BooleanType, EnumType, AttributeType, schema_file, ValidationError, EnumValue, EntityType
 import collections
 
 def replace_od(thing):
@@ -228,10 +228,10 @@ class SchemaTest(unittest.TestCase):
         self.assertRaises(ValidationError, s.entity, {'type': 'listener', 'nosuch': 'x'})
         e = s.entity({'type': 'listener', 'name':'x', 'addr':'foo'})
         self.assertEqual(e.attributes, {'type': 'org.example.listener', 'name':'x', 'addr':'foo'})
-        self.assertEqual(e.addr, 'foo')
+        self.assertEqual(e['addr'], 'foo')
         self.assertRaises(ValidationError, e.__setitem__, 'nosuch', 'x')
         try:
-            e.nosuch = x
+            e.nosuch = 'x'
             self.fail("Expected exception")
         except: pass
 

Modified: qpid/dispatch/trunk/tests/run.py.in
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/run.py.in?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/tests/run.py.in (original)
+++ qpid/dispatch/trunk/tests/run.py.in Wed Sep 10 03:34:41 2014
@@ -146,5 +146,5 @@ if __name__ == "__main__":
             os.execvp(args[0], args)
     except Exception, e:
         print "%s%s: %s"%(error_prefix, type(e).__name__, e)
-        sys.exit(1)
+        raise
 

Modified: qpid/dispatch/trunk/tests/system_test.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/system_test.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/tests/system_test.py (original)
+++ qpid/dispatch/trunk/tests/system_test.py Wed Sep 10 03:34:41 2014
@@ -56,7 +56,7 @@ import os, time, socket, random, subproc
 from copy import copy
 import proton
 from proton import Message
-from qpid_dispatch_internal.management import Node
+from qpid_dispatch.management import Node
 from run import with_valgrind
 
 # Optional modules
@@ -352,7 +352,7 @@ class Qdrouterd(Process):
     def is_connected(self, port, host='0.0.0.0'):
         """If router has a connection to host:port return the management info.
         Otherwise return None"""
-        connections = self.management.query('org.apache.qpid.dispatch.connection').result_maps
+        connections = self.management.query('org.apache.qpid.dispatch.connection').entities
         for c in connections:
             if c['name'] == '%s:%s'%(host, port):
                 return c
@@ -369,7 +369,7 @@ class Qdrouterd(Process):
             # FIXME aconway 2014-06-12: this should be a request by name, not a query.
             addrs = self.management.query(
                 type='org.apache.qpid.dispatch.router.address',
-                attribute_names=['name', 'subscriberCount', 'remoteCount']).result_maps
+                attribute_names=['name', 'subscriberCount', 'remoteCount']).entities
             # FIXME aconway 2014-06-12: endswith check is because of M0/L prefixes
             addrs = [a for a in addrs if a['name'].endswith(address)]
             return addrs and addrs[0]['subscriberCount'] >= subscribers and addrs[0]['remoteCount'] >= remotes

Modified: qpid/dispatch/trunk/tests/system_tests_management.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/system_tests_management.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/tests/system_tests_management.py (original)
+++ qpid/dispatch/trunk/tests/system_tests_management.py Wed Sep 10 03:34:41 2014
@@ -20,13 +20,14 @@
 """System tests for management of qdrouter"""
 
 import unittest, system_test, re, os
-from qpid_dispatch_internal.management import Node, ManagementError, Url, BAD_REQUEST, NOT_IMPLEMENTED, STATUS_TEXT, NOT_FOUND
+from qpid_dispatch.management import Node, ManagementError, Url, BadRequest, NotImplemented, NotFound, Forbidden
 from system_test import Qdrouterd, message, retry
 
 LISTENER = 'org.apache.qpid.dispatch.listener'
 CONNECTOR = 'org.apache.qpid.dispatch.connector'
 FIXED_ADDRESS = 'org.apache.qpid.dispatch.fixed-address'
 WAYPOINT = 'org.apache.qpid.dispatch.waypoint'
+DUMMY = 'org.apache.qpid.dispatch.dummy'
 
 ADDRESS = 'org.apache.qpid.dispatch.router.address'
 
@@ -53,46 +54,29 @@ class ManagementTest(system_test.TestCas
         self.node = self.cleanup(Node(self.router.addresses[0]))
         # Temporary access to separate new management address.
         self.node2 = self.cleanup(Node(Url(self.router.addresses[0], path='$management2')))
-
-    def assertRaisesManagement(self, status, call, *args, **kwargs):
-        """Assert that call(*args, **kwargs) raises a ManagementError with status"""
-        try:
-            call(*args, **kwargs)
-            self.fail("Expected ManagementError(%r)"%(STATUS_TEXT[status]))
-        except ManagementError, e:
-            self.assertEqual(status, e.status,
-                             msg="Expected status %r, got '%s'"%(STATUS_TEXT[status], e))
+        self.maxDiff = None
+        self.longMessage = True
+        unittest.util._MAX_LENGTH = 256 # Monkey patch unittest truncation
 
     def test_bad_query(self):
         """Test that various badly formed queries get the proper response"""
         # No operation attribute
-        self.assertRaisesManagement(BAD_REQUEST, self.node.call, self.node.request())
+        self.assertRaises(BadRequest, self.node.call, self.node.request())
         # Unknown operation
-        self.assertRaisesManagement(NOT_IMPLEMENTED, self.node.call,
-                                    self.node.request(operation="nosuch"))
+        self.assertRaises(NotImplemented, self.node.call, self.node.request(operation="nosuch"))
         # No entityType or attributeList
-        self.assertRaisesManagement(BAD_REQUEST, self.node.query)
-
-    def test_query_type(self):
-        address = 'org.apache.qpid.dispatch.router.address'
-        response = self.node.query(type=address)
-        self.assertEqual(response.attribute_names[0:3], ['type', 'name', 'identity'])
-        for r in response.result_maps:  # Check types
-            self.assertEqual(r['type'], address)
-        names = [r['name'] for r in response.result_maps]
-        self.assertTrue('L$management' in names)
-        self.assertTrue('M0$management' in names)
+        self.assertRaises(BadRequest, self.node.query)
 
     def test_query_type(self):
         """Query with type only"""
         response = self.node2.query(type=LISTENER)
         for attr in ['type', 'name', 'identity', 'addr', 'port']:
             self.assertTrue(attr in response.attribute_names)
-        for r in response.result_maps:           # Check types
-            self.assertEqual(len(response.attribute_names), len(r))
+        for r in response.entities: # Check types
+            self.assertEqual(len(response.attribute_names), len(r.attributes))
             self.assertEqual(r['type'], LISTENER)
         self.assertTrue(
-            set(['l0', 'l1', 'l2']) <= set(r['name'] for r in response.result_maps))
+            set(['l0', 'l1', 'l2']) <= set(r['name'] for r in response.entities))
 
     def test_query_type_attributes(self):
         """Query with type and attribute names"""
@@ -102,7 +86,7 @@ class ManagementTest(system_test.TestCas
         expect = [[LISTENER, 'l%s' % i, str(self.router.ports[i])] for i in xrange(3)]
         for r in expect: # We might have extras in results due to create tests
             self.assertTrue(r in response.results)
-            self.assertTrue(dict(zip(attribute_names, r)) in response.result_maps)
+            self.assertTrue(dict(zip(attribute_names, r)) in [e.attributes for e in response.entities])
 
     def test_query_attributes(self):
         """Query with attributes only"""
@@ -113,18 +97,18 @@ class ManagementTest(system_test.TestCas
         for r in expect: # We might have extras in results due to create tests
             self.assertTrue(r in response.results)
         for name in ['router0', 'log0']:
-            self.assertTrue([r for r in response.result_maps if r['name'] == name],
+            self.assertTrue([r for r in response.entities if r['name'] == name],
                             msg="Can't find result with name '%s'" % name)
 
-    def assertMapSubset(self, sub, super):
-        """Assert that mapping sub is a subset of mapping super"""
-        missing = [(k, v) for k, v in sub.items() if (k, v) not in super.items()]
-        assert not missing, "Not a subset, missing %s, sub=%s, super=%s"%(missing, sub, super)
+    def assertMapSubset(self, small, big):
+        """Assert that mapping small is a subset of mapping big"""
+        missing = [(k, v) for k, v in small.items() if (k, v) not in big.items()]
+        assert not missing, "Not a subset, missing %s, sub=%s, super=%s"%(missing, small, big)
 
     def assert_create_ok(self, type, name, attributes):
-        result = self.node2.create(type, name, attributes)
-        self.assertMapSubset(attributes, result)
-        return result
+        entity = self.node2.create(type, name, attributes)
+        self.assertMapSubset(attributes, entity.attributes)
+        return entity
 
     def test_create_listener(self):
         """Create a new listener on a running router"""
@@ -132,14 +116,14 @@ class ManagementTest(system_test.TestCas
         port = self.get_port()
         # Note qdrouter schema defines port as string not int, since it can be a service name.
         attributes = {'name':'foo', 'port':str(port), 'role':'normal', 'sasl-mechanisms': 'ANONYMOUS'}
-        result = self.assert_create_ok(LISTENER, 'foo', attributes)
-        self.assertEqual(result['identity'], attributes['name'])
-        self.assertEqual(result['addr'], '0.0.0.0')
+        entity = self.assert_create_ok(LISTENER, 'foo', attributes)
+        self.assertEqual(entity['identity'], attributes['name'])
+        self.assertEqual(entity['addr'], '0.0.0.0')
 
         # Connect via the new listener
         node3 = self.cleanup(Node(Url(port=port, path='$management')))
-        router = node3.query(type='org.apache.qpid.dispatch.router')
-        self.assertEqual(self.__class__.router.name, router.result_maps[0]['name'])
+        router = node3.query(type='org.apache.qpid.dispatch.router').entities
+        self.assertEqual(self.__class__.router.name, router[0]['name'])
 
     def test_create_log(self):
         """Create a log entity"""
@@ -147,7 +131,8 @@ class ManagementTest(system_test.TestCas
         # - allow auto-assigned name/identity? Use module as name/identity?
         # - 1 entity with full log state, allow updates.
         log = os.path.abspath("test_create_log.log")
-        self.assert_create_ok('log', 'log.1', dict(module='AGENT', level="error", output=log))
+        # FIXME aconway 2014-09-08: PYAGENT->AGENT
+        self.assert_create_ok('log', 'log.1', dict(module='PYAGENT', level="error", output=log))
         # Cause an error and verify it shows up in the log file.
         self.assertRaises(ManagementError, self.node2.create, type='nosuch', name='nosuch')
         f = self.cleanup(open(log))
@@ -206,28 +191,79 @@ class ManagementTest(system_test.TestCas
         finally:
             f.close()
 
-    def test_read(self):
-        attributes = self.node2.read(type=LISTENER, name='l0')
-        self.assertEqual('l0', attributes['name'])
-        self.assertEqual('l0', attributes['identity'])
-        self.assertEqual(str(self.router.ports[0]), attributes['port'])
-
-        attributes = self.node2.read(type=LISTENER, identity='l1')
-        self.assertEqual('l1', attributes['name'])
-        self.assertEqual('l1', attributes['identity'])
-        self.assertEqual(str(self.router.ports[1]), attributes['port'])
+    def test_entity(self):
+        entity = self.node2.read(type=LISTENER, name='l0')
+        self.assertEqual('l0', entity.name)
+        self.assertEqual('l0', entity.identity)
+        self.assertEqual(str(self.router.ports[0]), entity.port)
+
+        entity = self.node2.read(type=LISTENER, identity='l1')
+        self.assertEqual('l1', entity.name)
+        self.assertEqual('l1', entity.identity)
+        self.assertEqual(str(self.router.ports[1]), entity.port)
 
         # Can't specify both name and identity
-        self.assertRaisesManagement(
-            BAD_REQUEST, self.node2.read, type=LISTENER, name='l0', identity='l1')
+        self.assertRaises(BadRequest, self.node2.read, type=LISTENER, name='l0', identity='l1')
 
         # Bad type
-        self.assertRaisesManagement(
-            NOT_FOUND, self.node2.read, type=CONNECTOR, name='l0')
+        self.assertRaises(NotFound, self.node2.read, type=CONNECTOR, name='l0')
 
         # Unknown entity
-        self.assertRaisesManagement(
-            NOT_FOUND, self.node2.read, type=LISTENER, name='nosuch')
+        self.assertRaises(NotFound, self.node2.read, type=LISTENER, name='nosuch')
+
+        # Update and delete are not allowed by the schema
+        self.assertRaises(Forbidden, entity.update)
+        self.assertRaises(Forbidden, entity.delete)
+
+        # Non-standard request is not allowed by schema.
+        self.assertRaises(Forbidden, entity.call, 'nosuchop', foo="bar")
+
+        # Dummy entity supports all CRUD operations
+        dummy = self.node2.create(type=DUMMY, name='MyDummy', attributes={'arg1': 'START'})
+        self.assertEqual(dummy.type, DUMMY)
+        self.assertEqual(dummy.name, 'MyDummy')
+        self.assertEqual(dummy.arg1, 'START')
+        identity = dummy.identity
+        self.assertEqual(
+            dict(type=DUMMY, identity=identity, name='MyDummy', arg1='START'),
+            dummy.attributes)
+
+        dummy.attributes['num1'] = 42
+        dummy.arg1 = 'one'
+        self.assertEqual(
+            dict(type=DUMMY, identity=identity, name='MyDummy', arg1='one', num1=42),
+            dummy.attributes)
+        dummy.update()
+
+        dummy.attributes.update(dict(arg1='x', num1=0))
+        dummy.read()
+        self.assertEqual(
+            dict(type=DUMMY, name='MyDummy', identity=identity, arg1='one', num1=42),
+            dummy.attributes)
+
+        dummy2 = self.node2.read(type=DUMMY, name='MyDummy')
+        self.assertEqual(dummy.attributes, dummy2.attributes)
+
+        self.assertEqual({'operation': 'callme', 'foo': 'bar', 'type': DUMMY, 'identity': identity},
+                         dummy.call('callme', foo='bar'))
+
+        dummy.badattribute = 'Bad'
+        self.assertRaises(BadRequest, dummy.update)
+
+        dummy.delete()
+        self.assertRaises(NotFound, self.node2.read, type=DUMMY, name='MyDummy')
+
+    def test_get_types(self):
+        self.assertRaises(NotImplemented, self.node2.get_types)
+
+    def test_get_attributes(self):
+        self.assertRaises(NotImplemented, self.node2.get_attributes)
+
+    def test_get_operations(self):
+        self.assertRaises(NotImplemented, self.node2.get_operations)
+
+    def test_get_other_nodes(self):
+        self.assertRaises(NotImplemented, self.node2.get_other_nodes)
 
 if __name__ == '__main__':
     unittest.main()

Modified: qpid/dispatch/trunk/tests/system_tests_two_routers.py
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tests/system_tests_two_routers.py?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/tests/system_tests_two_routers.py (original)
+++ qpid/dispatch/trunk/tests/system_tests_two_routers.py Wed Sep 10 03:34:41 2014
@@ -20,7 +20,7 @@
 import unittest, os
 from proton import Message, PENDING, ACCEPTED, REJECTED, RELEASED, SSLDomain, SSLUnavailable
 from system_test import TestCase, Qdrouterd, retry_exception
-from qpid_dispatch_internal.management import Node
+from qpid_dispatch.management import Node
 
 class RouterTest(TestCase):
     @classmethod

Modified: qpid/dispatch/trunk/tools/qdstat
URL: http://svn.apache.org/viewvc/qpid/dispatch/trunk/tools/qdstat?rev=1623899&r1=1623898&r2=1623899&view=diff
==============================================================================
--- qpid/dispatch/trunk/tools/qdstat (original)
+++ qpid/dispatch/trunk/tools/qdstat Wed Sep 10 03:34:41 2014
@@ -32,7 +32,8 @@ if not home:
     home = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'lib', 'qpid-dispatch')
 sys.path.insert(0, os.path.join(home, "python"))
 
-from qpid_dispatch_internal.management import Url, Node, QdSchema, Entity
+from qpid_dispatch.management import Url, Node, Entity
+from qpid_dispatch_internal.management.qdrouter import QdSchema
 from qpid_dispatch_internal.tools import Display, Header, Sorter, YN, Commas, TimeLong
 
 
@@ -103,8 +104,7 @@ class BusManager(Node):
     schema = QdSchema()
 
     def query(self, entity_type):
-        return [Entity(m) for m in
-                super(BusManager, self).query(entity_type).result_maps]
+        return super(BusManager, self).query(entity_type).entities
 
     def displayConnections(self):
         disp = Display(prefix="  ")



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