Add pecan transport driver and tests

This commit is contained in:
tonytan4ever 2014-07-28 11:55:51 -04:00
parent 7ced7423a2
commit 1737ae877d
23 changed files with 767 additions and 0 deletions

29
cdn/manager/base/v1.py Normal file
View File

@ -0,0 +1,29 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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 abc
import six
from cdn.manager.base import controller
@six.add_metaclass(abc.ABCMeta)
class V1ControllerBase(controller.ManagerControllerBase):
def __init__(self, manager):
super(V1ControllerBase, self).__init__(manager)
@abc.abstractmethod
def get(self):
raise NotImplementedError

31
cdn/manager/default/v1.py Normal file
View File

@ -0,0 +1,31 @@
from cdn.manager import base
JSON_HOME = {
"resources": {
"rel/cdn": {
"href-template": "services{?marker,limit}",
"href-vars": {
"marker": "param/marker",
"limit": "param/limit"
},
"hints": {
"allow": [
"GET"
],
"formats": {
"application/json": {}
}
}
}
}
}
class DefaultV1Controller(base.V1Controller):
def __init__(self, manager):
super(DefaultV1Controller, self).__init__(manager)
self.JSON_HOME = JSON_HOME
def get(self):
return self.JSON_HOME

View File

@ -0,0 +1,126 @@
# Copyright 2011 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed 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.
"""
Simple class that stores security context information in the web request.
Projects should subclass this class if they wish to enhance the request
context or provide additional information in their specific WSGI pipeline.
"""
import itertools
import uuid
def generate_request_id():
return b'req-' + str(uuid.uuid4()).encode('ascii')
class RequestContext(object):
"""Helper class to represent useful information about a request context.
Stores information about the security context under which the user
accesses the system, as well as additional request information.
"""
user_idt_format = '{user} {tenant} {domain} {user_domain} {p_domain}'
def __init__(self, auth_token=None, user=None, tenant=None, domain=None,
user_domain=None, project_domain=None, is_admin=False,
read_only=False, show_deleted=False, request_id=None,
instance_uuid=None):
self.auth_token = auth_token
self.user = user
self.tenant = tenant
self.domain = domain
self.user_domain = user_domain
self.project_domain = project_domain
self.is_admin = is_admin
self.read_only = read_only
self.show_deleted = show_deleted
self.instance_uuid = instance_uuid
if not request_id:
request_id = generate_request_id()
self.request_id = request_id
def to_dict(self):
user_idt = (
self.user_idt_format.format(user=self.user or '-',
tenant=self.tenant or '-',
domain=self.domain or '-',
user_domain=self.user_domain or '-',
p_domain=self.project_domain or '-'))
return {'user': self.user,
'tenant': self.tenant,
'domain': self.domain,
'user_domain': self.user_domain,
'project_domain': self.project_domain,
'is_admin': self.is_admin,
'read_only': self.read_only,
'show_deleted': self.show_deleted,
'auth_token': self.auth_token,
'request_id': self.request_id,
'instance_uuid': self.instance_uuid,
'user_identity': user_idt}
@classmethod
def from_dict(cls, ctx):
return cls(
auth_token=ctx.get("auth_token"),
user=ctx.get("user"),
tenant=ctx.get("tenant"),
domain=ctx.get("domain"),
user_domain=ctx.get("user_domain"),
project_domain=ctx.get("project_domain"),
is_admin=ctx.get("is_admin", False),
read_only=ctx.get("read_only", False),
show_deleted=ctx.get("show_deleted", False),
request_id=ctx.get("request_id"),
instance_uuid=ctx.get("instance_uuid"))
def get_admin_context(show_deleted=False):
context = RequestContext(None,
tenant=None,
is_admin=True,
show_deleted=show_deleted)
return context
def get_context_from_function_and_args(function, args, kwargs):
"""Find an arg of type RequestContext and return it.
This is useful in a couple of decorators where we don't
know much about the function we're wrapping.
"""
for arg in itertools.chain(kwargs.values(), args):
if isinstance(arg, RequestContext):
return arg
return None
def is_user_context(context):
"""Indicates if the request context is a normal user."""
if not context:
return False
if context.is_admin:
return False
if not context.user_id or not context.project_id:
return False
return True

37
cdn/transport/app.py Normal file
View File

@ -0,0 +1,37 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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.
"""WSGI callable for WSGI containers
This app should be used by external WSGI
containers. For example:
$ gunicorn dory.transport.app:app
NOTE: As for external containers, it is necessary
to put config files in the standard paths. There's
no common way to specify / pass configuration files
to the WSGI app when it is called from other apps.
"""
from oslo.config import cfg
from dory import bootstrap
conf = cfg.CONF
conf(project='cdn', prog='cdn', args=[])
app = bootstrap.Bootstrap(conf).transport.app

View File

@ -0,0 +1,22 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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.
"""Pecan Transport Driver"""
from cdn.transport.pecan import driver
# Hoist into package namespace
Driver = driver.PecanTransportDriver

View File

@ -0,0 +1,26 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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.
"""Pecan Controllers"""
from cdn.transport.pecan.controllers import root
from cdn.transport.pecan.controllers import services
from cdn.transport.pecan.controllers import v1
# Hoist into package namespace
Root = root.RootController
Services = services.ServicesController
V1 = v1.V1Controller

View File

@ -0,0 +1,25 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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.
from pecan import rest
class Controller(rest.RestController):
def __init__(self, driver):
self._driver = driver
def add_controller(self, path, controller):
setattr(self, path, controller)

View File

@ -0,0 +1,51 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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 re
import pecan
from cdn.transport.pecan.controllers import base
class RootController(base.Controller):
def __init__(self, driver):
super(RootController, self).__init__(driver)
self.paths = []
def add_controller(self, path, controller):
super(RootController, self).add_controller(path, controller)
self.paths.append(path)
@pecan.expose()
def _route(self, args, request=None):
# Optionally allow OpenStack project ID in the URL
# Remove it from the URL if it's present
# ['v1', 'todos'] or ['v1', '123', 'todos']
if (
len(args) >= 2
and args[0] in self.paths
and re.match('^[0-9]+$', args[1])
):
args.pop(1)
return super(RootController, self)._route(args, request)
@pecan.expose('json')
def get_all(self):
return {
'status': 'up',
}

View File

@ -0,0 +1,35 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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 uuid
import pecan
from cdn.transport.pecan.controllers import base
class ServicesController(base.Controller):
@pecan.expose('json')
def get_all(self):
tenant_id = pecan.request.context.to_dict()['tenant']
services_controller = self._driver.manager.services_controller
return services_controller.list(tenant_id)
@pecan.expose('json')
def get_one(self):
tenant_id = pecan.request.context.to_dict()['tenant']
services_controller = self._driver.manager.services_controller
return services_controller.list(tenant_id)

View File

@ -0,0 +1,26 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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 pecan
from cdn.transport.pecan.controllers import base
class V1Controller(base.Controller):
@pecan.expose('json')
def get(self):
v1_controller = self._driver.manager.v1_controller
return v1_controller.get()

View File

@ -0,0 +1,74 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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.
from wsgiref import simple_server
import pecan
from oslo.config import cfg
from cdn.openstack.common import log
from cdn import transport
from cdn.transport.pecan import controllers
from cdn.transport.pecan import hooks
_PECAN_OPTIONS = [
cfg.StrOpt('bind', default='127.0.0.1',
help='Address on which the self-hosting server will listen'),
cfg.IntOpt('port', default=8888,
help='Port on which the self-hosting server will listen'),
]
_PECAN_GROUP = 'drivers:transport:pecan'
LOG = log.getLogger(__name__)
class PecanTransportDriver(transport.Driver):
def __init__(self, conf, manager):
super(PecanTransportDriver, self).__init__(conf, manager)
self._conf.register_opts(_PECAN_OPTIONS, group=_PECAN_GROUP)
self._pecan_conf = self._conf[_PECAN_GROUP]
self._setup_app()
def _setup_app(self):
root_controller = controllers.Root(self)
pecan_hooks = [hooks.Context()]
self._app = pecan.make_app(root_controller, hooks=pecan_hooks)
v1_controller = controllers.V1(self)
root_controller.add_controller('v1.0', v1_controller)
services_controller = controllers.Services(self)
v1_controller.add_controller('services', services_controller)
def listen(self):
LOG.info(
'Serving on host %(bind)s:%(port)s',
{
'bind': self._pecan_conf.bind,
'port': self._pecan_conf.port,
},
)
httpd = simple_server.make_server(self._pecan_conf.bind,
self._pecan_conf.port,
self.app)
httpd.serve_forever()

View File

@ -0,0 +1,22 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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.
"""Pecan Hooks"""
from cdn.transport.pecan.hooks import context
# Hoist into package namespace
Context = context.ContextHook

View File

@ -0,0 +1,40 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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.
from pecan import hooks
from cdn.openstack.common import context
from cdn.openstack.common import local
class ContextHook(hooks.PecanHook):
def on_route(self, state):
context_kwargs = {}
if 'X-Project-ID' in state.request.headers:
context_kwargs['tenant'] = state.request.headers['X-Project-ID']
if 'tenant' not in context_kwargs:
# Didn't find the X-Project-Id header, pull from URL instead
# Expects form /v1/{project_id}/path
context_kwargs['tenant'] = state.request.path.split('/')[2]
if 'X-Auth-Token' in state.request.headers:
context_kwargs['auth_token'] = state.request.headers['X-Auth-Token']
request_context = context.RequestContext(**context_kwargs)
state.request.context = request_context
local.store.context = request_context

View File

@ -0,0 +1,41 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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 os
from oslo.config import cfg
import testtools
import webtest
from cdn import bootstrap
class BaseFunctionalTest(testtools.TestCase):
def setUp(self):
super(BaseFunctionalTest, self).setUp()
tests_path = os.path.abspath(os.path.dirname(
os.path.dirname(
os.path.dirname(os.path.dirname(__file__)
))))
conf_path = os.path.join(tests_path, 'etc', 'default_functional.conf')
cfg.CONF(args=[], default_config_files=[conf_path])
cdn_wsgi = bootstrap.Bootstrap(cfg.CONF).transport.app
self.app = webtest.TestApp(cdn_wsgi)
FunctionalTest = BaseFunctionalTest

View File

@ -0,0 +1,10 @@
from tests.functional.transport.pecan import base
class ServiceControllerTest(base.FunctionalTest):
def test_get_all(self):
pass
#response = self.app.get('/health')
#self.assertEqual(204, response.status_code)

View File

@ -0,0 +1,24 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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.
from tests.functional.transport.pecan import base
class ServicesControllerTest(base.FunctionalTest):
def test_get_all(self):
response = self.app.get('/v1.0/00001/services')
self.assertEqual(200, response.status_code)

View File

@ -0,0 +1,28 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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.
from cdn.manager.default import v1
from tests.functional.transport.pecan import base
class V1ControllerTest(base.FunctionalTest):
def test_get_all(self):
response = self.app.get('/v1.0/00001')
self.assertEqual(200, response.status_code)
# Temporary until actual implementation
self.assertEqual(v1.JSON_HOME, response.json)

View File

@ -0,0 +1,43 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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 uuid
from cdn.manager.default import v1
from tests.functional.transport.pecan import base
class ContextHookTest(base.FunctionalTest):
def setUp(self):
super(ContextHookTest, self).setUp()
self.headers = {'X-Auth-Token': str(uuid.uuid4())}
def test_project_id_in_header(self):
self.headers['X-Project-Id'] = '000001'
response = self.app.get('/v1.0', headers=self.headers)
self.assertEqual(200, response.status_code)
# Temporary until actual implementation
self.assertEqual(v1.JSON_HOME, response.json)
def test_project_id_in_url(self):
response = self.app.get('/v1.0/000001', headers=self.headers)
self.assertEqual(200, response.status_code)
# Temporary until actual implementation
self.assertEqual(v1.JSON_HOME, response.json)

View File

View File

@ -0,0 +1,77 @@
# Copyright (c) 2014 Rackspace, Inc.
#
# Licensed 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
import threading
import ctypes
import requests
from oslo.config import cfg
from cdn.transport.pecan import driver
def terminate_thread(thread):
"""Terminates a python thread from another thread.
:param thread: a threading.Thread instance
"""
if not thread.isAlive():
return
exc = ctypes.py_object(SystemExit)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
ctypes.c_long(thread.ident), exc)
if res == 0:
raise ValueError("nonexistent thread id")
elif res > 1:
# """if it returns a number greater than one, you're in trouble,
# and you should call it again with exc=NULL to revert the effect"""
ctypes.pythonapi.PyThreadState_SetAsyncExc(thread.ident, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
class StoppableThread(threading.Thread):
"""Thread class with a stop() method. The thread itself has to check
regularly for the stopped() condition."""
def __init__(self, **kwargs):
super(StoppableThread, self).__init__(**kwargs)
self._stop = threading.Event()
def stop(self):
self._stop.set()
def stopped(self):
return self._stop.isSet()
class TestPecanDriver(unittest.TestCase):
def setUp(self):
# Let manager = None for now
self.pecan_driver = driver.PecanTransportDriver(cfg.CONF, None)
def test_app_created(self):
self.assertEquals(self.pecan_driver.app is not None, True)
t = StoppableThread(target = self.pecan_driver.listen)
t.start()
#r = requests.get('http://127.0.0.1:8888')
#print r
#assertR
t.stop()
terminate_thread(t)