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.
This commit is contained in:
Mark McClain 2012-08-29 18:56:07 -04:00
commit 7492b50d28
13 changed files with 596 additions and 0 deletions

1
akanda/__init__.py Normal file

@ -0,0 +1 @@
__import__('pkg_resources').declare_namespace(__name__)

48
akanda/quantum/README.rst Normal file

@ -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
=======

@ -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))

@ -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 []

@ -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)

@ -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 []

@ -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 []

5
setup.cfg Normal file

@ -0,0 +1,5 @@
[nosetests]
where = test
verbosity = 2
detailed-errors = 1
cover-package = akanda

21
setup.py Normal file

@ -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,
)

7
test_requirements.txt Normal file

@ -0,0 +1,7 @@
tox
unittest2
nose
coverage
mock>=0.8.0
pep8
pyflakes

26
tox.ini Normal file

@ -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