commit 7492b50d2848e25b41d489664207fcaefbf8c276
Author: Mark McClain <mark.mcclain@dreamhost.com>
Date:   Wed Aug 29 18:56:07 2012 -0400

    Finish repo reorg
    
    Moved files into the correct component dir for Horizon and Quantum
    extensions.  Update module and class import paths to match the new
    organization format.

diff --git a/akanda/__init__.py b/akanda/__init__.py
new file mode 100644
index 0000000..de40ea7
--- /dev/null
+++ b/akanda/__init__.py
@@ -0,0 +1 @@
+__import__('pkg_resources').declare_namespace(__name__)
diff --git a/akanda/quantum/README.rst b/akanda/quantum/README.rst
new file mode 100644
index 0000000..a28b970
--- /dev/null
+++ b/akanda/quantum/README.rst
@@ -0,0 +1,48 @@
+Akanda User-facing API implemented as a Quantum Extension
+==========================================================
+
+Portforward
+-----------
+
+portfoward.py implemented under quantum/extensions allows... 
+
+Firewall
+----------
+
+firewall.py implemented under quantum/extensions allows...
+
+AddressBook
+---------
+addressbook.py implemented under quantum/extensions allows...
+
+Info
+----
+
+This is the home for the REST API that users will be calling directly with
+their preferred REST tool (curl, Python wrapper, etc.).
+
+This code will eventually become part of OpenStack or act as a source or
+inspiration that will. As such, this API should be constructed entirely with
+standard OpenStack tools.
+
+
+Exploratory Dev Work
+--------------------
+
+You also have to update Quantum's policy file for the extension to work with
+authZ.
+
+    "create_portforward": [],
+    "get_portforward": [["rule:admin_or_owner"]],
+    "update_portforward": [["rule:admin_or_owner"]],
+    "delete_portforward": [["rule:admin_or_owner"]]
+
+
+If you want to use quotas:
+
+add to the QUOTAS section of quantum.conf
+
+quota_portforward = 10
+
+=======
+
diff --git a/akanda/quantum/__init__.py b/akanda/quantum/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/akanda/quantum/_authzbase.py b/akanda/quantum/_authzbase.py
new file mode 100644
index 0000000..98795e1
--- /dev/null
+++ b/akanda/quantum/_authzbase.py
@@ -0,0 +1,199 @@
+import abc
+
+from sqlalchemy.orm import exc as sa_exc
+
+from quantum import quota
+#from quantum.api.v2 import attributes
+from quantum.api.v2 import base
+from quantum.api.v2 import resource as api_resource
+from quantum.common import exceptions as q_exc
+from quantum.openstack.common import cfg
+
+
+class ResourcePlugin(object):
+    """
+    This is a class does some of what the Quantum plugin does, managing
+    resources in a way very similar to what Quantum does. It differ from
+    Quantum is that this provides a base plugin infrastructure, and doesn't
+    manage any resources.
+
+    Quantum doesn't split infrastructure and implementation.
+    """
+    JOINS = ()
+
+    def __init__(self, delegate):
+        # synthesize the hooks because Quantum's base class uses the
+        # resource name as part of the method name
+        setattr(self, 'get_%s' % delegate.collection_name,
+                self._get_collection)
+        setattr(self, 'get_%s' % delegate.resource_name, self._get_item)
+        setattr(self, 'update_%s' % delegate.resource_name, self._update_item)
+        setattr(self, 'create_%s' % delegate.resource_name, self._create_item)
+        setattr(self, 'delete_%s' % delegate.resource_name, self._delete_item)
+        self.delegate = delegate
+
+    def _get_tenant_id_for_create(self, context, resource):
+        if context.is_admin and 'tenant_id' in resource:
+            tenant_id = resource['tenant_id']
+        elif ('tenant_id' in resource and
+              resource['tenant_id'] != context.tenant_id):
+            reason = _('Cannot create resource for another tenant')
+            raise q_exc.AdminRequired(reason=reason)
+        else:
+            tenant_id = context.tenant_id
+        return tenant_id
+
+    def _model_query(self, context):
+        query = context.session.query(self.delegate.model)
+
+    # NOTE(jkoelker) non-admin queries are scoped to their tenant_id
+        if not context.is_admin and hasattr(self.delegate.model, 'tenant_id'):
+            query = query.filter(
+                self.delegate.model.tenant_id == context.tenant_id)
+        return query
+
+    def _get_collection(self, context, filters=None, fields=None,
+                        verbose=None):
+        collection = self._model_query(context)
+        if filters:
+            for key, value in filters.iteritems():
+                column = getattr(self.delegate.model, key, None)
+                if column:
+                    collection = collection.filter(column.in_(value))
+        return [self._fields(self.delegate.make_dict(c), fields) for c in
+                collection.all()]
+
+    def _get_by_id(self, context, id, verbose=None):
+        try:
+            query = self._model_query(context)
+            if verbose:
+                if verbose and isinstance(verbose, list):
+                    # XXX orm is undefined; please fix
+                    options = [orm.joinedload(join) for join in
+                               self.delegate.joins if join in verbose]
+                else:
+                    # XXX orm is undefined; please fix
+                    options = [orm.joinedload(join) for join in
+                               self.delegate.joins]
+                query = query.options(*options)
+            return query.filter_by(id=id).one()
+        except sa_exc.NoResultFound:
+            raise q_exc.NotFound()
+
+    def _get_item(self, context, id, fields=None, verbose=None):
+        obj = self._get_by_id(context, id, verbose=verbose)
+        return self._fields(self.delegate.make_dict(obj), fields)
+
+    def _update_item(self, id, **kwargs):
+        key = self.delegate.resource_name
+        resource_dict = kwargs[key][key]
+        # XXX context and verbase are not defined here, probably missing in the
+        # method signature; please fix
+        obj = self._get_by_id(context, id, verbose=verbose)
+        return self.delegate.update(obj, resource_dict)
+
+    def _create_item(self, context, **kwargs):
+        key = self.delegate.resource_name
+        resource_dict = kwargs[key][key]
+        tenant_id = self._get_tenant_id_for_create(context, resource_dict)
+        return self.delegate.create(tenant_id, resource_dict)
+
+    def _delete_item(self, context, id):
+        # XXX verbose is missing a definition, probably missing from the method
+        # signature; please fix
+        obj = self._get_by_id(context, id, verbose=verbose)
+        with context.session.begin():
+            self.delegate.before_delete(obj)
+            context.session.delete(obj)
+
+    def _fields(self, resource, fields):
+        if fields:
+            return dict([(key, item) for key, item in resource.iteritems()
+                        if key in fields])
+        return resource
+
+
+class ResourceDelegateInterface(object):
+    """
+    An abstract marker class defines the interface of RESTful resources.
+    """
+    __metaclass__ = abc.ABCMeta
+
+    def before_delete(self, resource):
+        pass
+
+    @abc.abstractproperty
+    def model(self):
+        pass
+
+    @abc.abstractproperty
+    def resource_name(self):
+        pass
+
+    @abc.abstractproperty
+    def collection_name(self):
+        pass
+
+    @property
+    def joins(self):
+        return ()
+
+    @abc.abstractmethod
+    def update(self, tenant_id, resource, body):
+        pass
+
+    @abc.abstractmethod
+    def create(self, tenant_id, body):
+        pass
+
+    @abc.abstractmethod
+    def make_dict(self, obj):
+        pass
+
+
+class ResourceDelegate(ResourceDelegateInterface):
+    """
+    This class partially implemnts the ResourceDelegateInterface, providing
+    common code for use by child classes that inherit from it.
+    """
+    def create(self, tenant_id, body):
+        with context.session.begin(subtransactions=True):
+            item = self.model(**body)
+            context.session.add(item)
+        return self.make_dict(item)
+
+    def update(self, tenant_id, resource, resource_dict):
+        with context.session.begin(subtransactions=True):
+            item = self.model(**resource)
+            context.session.update(item)
+        return self.make_dict(item)
+
+    def delete(self, tenant_id, resource, resource_dict):
+        pass
+
+
+def create_extension(delegate):
+    """
+    """
+    #for key, value in delegate.ATTRIBUTE_MAP.iteritems():
+    #    if key in attributes.RESOURCE_ATTRIBUTE_MAP:
+    #        pass # TODO(mark): should log that we're doing this
+    #    attributes.RESOURCE_ATTRIBUTE_MAP[key] = value
+    return api_resource.Resource(base.Controller(ResourcePlugin(delegate),
+                                                 delegate.collection_name,
+                                                 delegate.resource_name,
+                                                 delegate.ATTRIBUTE_MAP))
+
+
+def register_quota(resource_name, config_key_name, default=-1):
+    """
+    """
+    quota_opt = cfg.IntOpt(config_key_name,
+                           default=default,
+                           help=('number of %s allowed per tenant, -1 for '
+                                 'unlimited' % resource_name))
+    cfg.CONF.register_opts([quota_opt], 'QUOTAS')
+    quota.QUOTAS.register_resource(
+        quota.CountableResource(resource_name,
+                                quota._count_resource,
+                                config_key_name))
diff --git a/akanda/quantum/addressbook.py b/akanda/quantum/addressbook.py
new file mode 100644
index 0000000..1325b1b
--- /dev/null
+++ b/akanda/quantum/addressbook.py
@@ -0,0 +1,77 @@
+from quantum.api.v2 import attributes
+from quantum.db import models_v2
+from quantum.extensions import extensions
+
+from akanda.quantum import _authzbase
+from akanda.quantum.db import models
+
+
+# XXX: I used Network as an existing model for testing.  Need to change to
+# use an actual PortForward model.
+#
+# Duncan: cool, we'll get a PortForward model in place ASAP, so that this code
+# can be updated to use it.
+
+
+class AddressBookResource(_authzbase.ResourceDelegate):
+    """
+    """
+    model = models.AddressBook
+    resource_name = 'addressbook'
+    collection_name = 'addressbookgroup'
+
+    ATTRIBUTE_MAP = {
+        'id': {'allow_post': False, 'allow_put': False,
+               'validate': {'type:regex': attributes.UUID_PATTERN},
+               'is_visible': True},
+        'name': {'allow_post': True, 'allow_put': True,
+                 'default': '', 'is_visible': True},
+        'tenant_id': {'allow_post': True, 'allow_put': False,
+                      'required_by_policy': True,
+                      'is_visible': True},
+    }
+
+    def make_dict(self, addressbook):
+        """
+        Convert a addressbook model object to a dictionary.
+        """
+        res = {'id': addressbook['id'],
+               'name': addressbook['name'],
+               'groups': [group['id']
+                           for group in addressbook['groups']]}
+
+        return res
+
+
+_authzbase.register_quota('portforward', 'quota_portforward')
+
+
+class Portforward(object):
+    """
+    """
+    def get_name(self):
+        return "port forward"
+
+    def get_alias(self):
+        return "dhportforward"
+
+    def get_description(self):
+        return "A port forwarding extension"
+
+    def get_namespace(self):
+        return 'http://docs.dreamcompute.com/api/ext/v1.0'
+
+    def get_updated(self):
+        return "2012-08-02T16:00:00-05:00"
+
+    def get_resources(self):
+        return [extensions.ResourceExtension(
+            'dhportforward',
+            _authzbase.create_extension(PortforwardResource()))]
+            #_authzbase.ResourceController(PortforwardResource()))]
+
+    def get_actions(self):
+        return []
+
+    def get_request_extensions(self):
+        return []
diff --git a/akanda/quantum/db/__init__.py b/akanda/quantum/db/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/akanda/quantum/db/models.py b/akanda/quantum/db/models.py
new file mode 100644
index 0000000..9364afe
--- /dev/null
+++ b/akanda/quantum/db/models.py
@@ -0,0 +1,54 @@
+import sqlalchemy as sa
+from sqlalchemy import orm
+
+from quantum.db import model_base
+from quantum.db import models_v2 as models
+from quantum.openstack.common import timeutils
+
+
+class PortForward(model_base.BASEV2, models.HasId, models.HasTenant):
+    name = sa.Column(sa.String(255))
+    public_port = sa.Column(sa.Integer, nullable=False)
+    instance_id = sa.Column(sa.String(36), nullable=False)
+    private_port = sa.Column(sa.Integer, nullable=True)
+    # Quantum port address are stored in ipallocation which are internally
+    # referred to as fixed_id, thus the name below.
+    # XXX can we add a docsting to this model that explains how fixed_id is
+    # used?
+    fixed_id = sa.Column(
+        sa.String(36), sa.ForeignKey('ipallocation.id', ondelete="CASCADE"),
+        nullable=True)
+
+
+class AddressBookEntry(model_base.BASEV2, models.HasId, models.HasTenant):
+    group_id = sa.Column(sa.String(36), sa.ForeignKey('addressbookgroup.id'),
+                         nullable=False)
+    cidr = sa.Column(sa.String(64), nullable=False)
+
+
+class AddressBookGroup(model_base.BASEV2, models.HasId, models.HasTenant):
+    name = sa.Column(sa.String(255), nullable=False, primary_key=True)
+    table_id = sa.Column(sa.String(36), sa.ForeignKey('addressbook.id'),
+                         nullable=False)
+    entries = orm.relationship(AddressBookEntry, backref='groups')
+
+
+class AddressBook(model_base.BASEV2, models.HasId, models.HasTenant):
+    name = sa.Column(sa.String(255), nullable=False, primary_key=True)
+    groups = orm.relationship(AddressBookGroup, backref='book')
+
+
+class FilterRule(model_base.BASEV2, models.HasId, models.HasTenant):
+    action = sa.Column(sa.String(6), nullable=False, primary_key=True)
+    ip_version = sa.Column(sa.Integer, nullable=True)
+    protocol = sa.Column(sa.String(4), nullable=False)
+    source_alias = sa.Column(sa.String(36),
+                             sa.ForeignKey('addressbookentry.id'),
+                             nullable=False)
+    source_port = sa.Column(sa.Integer, nullable=True)
+    destination_alias = sa.Column(sa.String(36),
+                                  sa.ForeignKey('addressbookentry.id'),
+                                  nullable=False)
+    destination_port = sa.Column(sa.Integer, nullable=True)
+    created_at = sa.Column(sa.DateTime, default=timeutils.utcnow,
+                           nullable=False)
diff --git a/akanda/quantum/firewall.py b/akanda/quantum/firewall.py
new file mode 100644
index 0000000..06efeca
--- /dev/null
+++ b/akanda/quantum/firewall.py
@@ -0,0 +1,80 @@
+from quantum.api.v2 import attributes
+from quantum.db import models_v2
+from quantum.extensions import extensions
+
+from akanda.quantum import _authzbase
+from akanda.quantum.db import models
+
+
+# XXX: I used Network as an existing model for testing.  Need to change to
+# use an actual PortForward model.
+#
+# Duncan: cool, we'll get a PortForward model in place ASAP, so that this code
+# can be updated to use it.
+
+
+class FirewallResource(_authzbase.ResourceDelegate):
+    """
+    """
+    model = models.Firewall
+    resource_name = 'firewall'
+    collection_name = 'firewalls'
+
+    ATTRIBUTE_MAP = {
+        'id': {'allow_post': False, 'allow_put': False,
+               'validate': {'type:regex': attributes.UUID_PATTERN},
+               'is_visible': True},
+        'name': {'allow_post': True, 'allow_put': True,
+                 'default': '', 'is_visible': True},
+        'tenant_id': {'allow_post': True, 'allow_put': False,
+                      'required_by_policy': True,
+                      'is_visible': True},
+    }
+
+    def make_dict(self, firewall):
+        """
+        Convert a firewall model object to a dictionary.
+        """
+        res = {'id': firewall['id'],
+               'action': firewall['action'],
+               'protocol': firewall['protocol'],
+               'source_alias': firewall['source_alias'],
+               'source_port': firewall['source_port'],
+               'destination_alias': firewall['destination_alias'],
+               'destination_port': firewall['destination_port'],
+               'created_at': firewall['created_at']}
+        return res
+
+
+_authzbase.register_quota('portforward', 'quota_portforward')
+
+
+class Portforward(object):
+    """
+    """
+    def get_name(self):
+        return "port forward"
+
+    def get_alias(self):
+        return "dhportforward"
+
+    def get_description(self):
+        return "A port forwarding extension"
+
+    def get_namespace(self):
+        return 'http://docs.dreamcompute.com/api/ext/v1.0'
+
+    def get_updated(self):
+        return "2012-08-02T16:00:00-05:00"
+
+    def get_resources(self):
+        return [extensions.ResourceExtension(
+            'dhportforward',
+            _authzbase.create_extension(PortforwardResource()))]
+            #_authzbase.ResourceController(PortforwardResource()))]
+
+    def get_actions(self):
+        return []
+
+    def get_request_extensions(self):
+        return []
diff --git a/akanda/quantum/portforward.py b/akanda/quantum/portforward.py
new file mode 100644
index 0000000..ebd227b
--- /dev/null
+++ b/akanda/quantum/portforward.py
@@ -0,0 +1,78 @@
+from quantum.api.v2 import attributes
+from quantum.extensions import extensions
+
+from akanda.quantum import _authzbase
+from akanda.quantum.db import models
+
+
+# XXX: I used Network as an existing model for testing.  Need to change to
+# use an actual PortForward model.
+#
+# Duncan: cool, we'll get a PortForward model in place ASAP, so that this code
+# can be updated to use it.
+
+
+class PortforwardResource(_authzbase.ResourceDelegate):
+    """
+    This class is responsible for receiving REST requests and operating on the
+    defined data model to create, update, or delete portforward-related data.
+    """
+    model = models.PortForward
+    resource_name = 'portforward'
+    collection_name = 'portforwards'
+
+    ATTRIBUTE_MAP = {
+        'id': {'allow_post': False, 'allow_put': False,
+               'validate': {'type:regex': attributes.UUID_PATTERN},
+               'is_visible': True},
+        'name': {'allow_post': True, 'allow_put': True,
+                 'default': '', 'is_visible': True},
+        'tenant_id': {'allow_post': True, 'allow_put': False,
+                      'required_by_policy': True,
+                      'is_visible': True},
+    }
+
+    def make_dict(self, portforward):
+        """
+        Convert a portforward model object to a dictionary.
+        """
+        res = {'id': portforward['id'],
+               'name': portforward['name'],
+               'instance_id': portforward['instance_id'],
+               'private_port': portforward['private_port'],
+               'fixed_id': portforward['fixed_id']}
+        return res
+
+
+_authzbase.register_quota('portforward', 'quota_portforward')
+
+
+class Portforward(object):
+    """
+    """
+    def get_name(self):
+        return "port forward"
+
+    def get_alias(self):
+        return "dhportforward"
+
+    def get_description(self):
+        return "A port forwarding extension"
+
+    def get_namespace(self):
+        return 'http://docs.dreamcompute.com/api/ext/v1.0'
+
+    def get_updated(self):
+        return "2012-08-02T16:00:00-05:00"
+
+    def get_resources(self):
+        return [extensions.ResourceExtension(
+            'dhportforward',
+            _authzbase.create_extension(PortforwardResource()))]
+            #_authzbase.ResourceController(PortforwardResource()))]
+
+    def get_actions(self):
+        return []
+
+    def get_request_extensions(self):
+        return []
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..1fb7f5d
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,5 @@
+[nosetests]
+where = test
+verbosity = 2
+detailed-errors = 1
+cover-package = akanda
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..47f0a8d
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,21 @@
+import os
+
+from setuptools import setup, find_packages
+
+
+setup(
+    name='Akanda Horizon Dashboard Plugin',
+    version='0.1.0',
+    description='OpenStack Horizon dashboards for manipulating L3 extensions',
+    author='DreamHost',
+    author_email='dev-community@dreamhost.com',
+    url='http://github.com/dreamhost/akanda',
+    license='BSD',
+    install_requires=[
+    ],
+    namespace_packages=['akanda'],
+    packages=find_packages(),
+    include_package_data=True,
+    zip_safe=False,
+)
+
diff --git a/test_requirements.txt b/test_requirements.txt
new file mode 100644
index 0000000..12d57b1
--- /dev/null
+++ b/test_requirements.txt
@@ -0,0 +1,7 @@
+tox
+unittest2
+nose
+coverage
+mock>=0.8.0
+pep8
+pyflakes
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..2bc9a2a
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,26 @@
+[tox]
+envlist = py26,py27,pep8,pyflakes
+
+[testenv]
+setenv = VIRTUAL_ENV={envdir}
+deps = -r{toxinidir}/test_requirements.txt
+commands = nosetests {posargs}
+sitepackages = True
+
+[tox:jenkins]
+
+[testenv:pep8]
+deps = pep8
+       setuptools_git>=0.4
+commands = pep8 --repeat --show-source --ignore=E125 --exclude=.venv,.tox,dist,doc,*egg .
+
+[testenv:cover]
+setenv = NOSE_WITH_COVERAGE=1
+
+[testenv:venv]
+commands = {posargs}
+
+[testenv:pyflakes]
+deps = pyflakes
+commands = pyflakes akanda
+