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:
commit
7492b50d28
1
akanda/__init__.py
Normal file
1
akanda/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
__import__('pkg_resources').declare_namespace(__name__)
|
48
akanda/quantum/README.rst
Normal file
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
akanda/quantum/__init__.py
Normal file
0
akanda/quantum/__init__.py
Normal file
199
akanda/quantum/_authzbase.py
Normal file
199
akanda/quantum/_authzbase.py
Normal file
@ -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))
|
77
akanda/quantum/addressbook.py
Normal file
77
akanda/quantum/addressbook.py
Normal file
@ -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
akanda/quantum/db/__init__.py
Normal file
0
akanda/quantum/db/__init__.py
Normal file
54
akanda/quantum/db/models.py
Normal file
54
akanda/quantum/db/models.py
Normal file
@ -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)
|
80
akanda/quantum/firewall.py
Normal file
80
akanda/quantum/firewall.py
Normal file
@ -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 []
|
78
akanda/quantum/portforward.py
Normal file
78
akanda/quantum/portforward.py
Normal file
@ -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
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
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
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
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user