Initial Kingbird framework code base ( part5:test )

Fisrt patch to implement the framework of Kingbird, this is
the part5 for api test with readme files added.

Replace the Werkzeug wsgi server to oslo.service.wsgi to
reduce the requirements

Correct some invalid field in common/context.py

Blueprint: https://blueprints.launchpad.net/kingbird/+spec/kingbird-framework

Change-Id: Iebab2f554ac13ab653bf30adf021193b82a5e84b
Signed-off-by: Chaoyi Huang <joehuang@huawei.com>
This commit is contained in:
Chaoyi Huang 2015-11-27 17:33:01 +08:00
parent 9ae8bfc2fe
commit 06c20a15fd
17 changed files with 550 additions and 32 deletions

17
cmd/README.rst Executable file
View File

@ -0,0 +1,17 @@
===============================
cmd
===============================
Scripts to start the API, JobDaemon and JobWorker service
api.py:
start API service
python api.py --config-file=../etc/api.conf
jobdaemon.py:
start JobDaemon service
python jobdaemon.py --config-file=../etc/jobdaemon.conf
jobworker.py:
start JobWorker service
python jobworker.py --config-file=../etc/jobworker.conf

View File

@ -21,11 +21,10 @@ import sys
from oslo_config import cfg
from oslo_log import log as logging
from oslo_service import wsgi
import logging as std_logging
from werkzeug import serving
from kingbird.api import apicfg
from kingbird.api import app
@ -52,13 +51,15 @@ def main():
LOG.info(_LI("Server on http://%(host)s:%(port)s with %(workers)s"),
{'host': host, 'port': port, 'workers': workers})
serving.run_simple(host, port,
application,
processes=workers)
service = wsgi.Server(CONF, "Kingbird", application, host, port)
app.serve(service, CONF, workers)
LOG.info(_LI("Configuration:"))
CONF.log_opt_values(LOG, std_logging.INFO)
app.wait()
if __name__ == '__main__':
main()

18
devstack/README.rst Executable file
View File

@ -0,0 +1,18 @@
===============================
devstack
===============================
Scripts to integrate the API, JobDaemon and JobWorker service to OpenStack
devstack development environment
local.conf.sample:
sample configruation to integrate kingbird into devstack
cuntomize the configuration file to tell devstack which OpenStack services
will be launched
plugin.sh: plugin to the devstack
devstack will automaticly search the devstack folder, and load, exeucte
the plugin.sh in different environment establishment phase
settins: configuration for kingbird in the devstack
devstack will automaticly load the settings to be used in the plugin.sh

14
etc/README.rst Executable file
View File

@ -0,0 +1,14 @@
===============================
etc
===============================
configuration sample for the API, JobDaemon and JobWorker service
api.conf:
configuration sample for API service
jobdaemon.conf:
configuration sample for JobDaemon service
jobworker.conf:
configuration sample for JobWorker service

29
kingbird/api/README.rst Executable file
View File

@ -0,0 +1,29 @@
===============================
api
===============================
Kingbird API is Web Server Gateway Interface (WSGI) applications to receive
and process API calls, including keystonemiddleware to do the authentication,
parameter check and validation, convert API calls to job rpc message, and
then send the job to Kingbird Job Daemon through the queue. If the job will
be processed by Kingbird Job Daemon in synchronous way, the Kingbird API will
wait for the response from the Kingbird Job Daemon. Otherwise, the Kingbird
API will send response to the API caller first, and then send the job to
Kingbird Job Daemon in asynchronous way. One of the Kingbird Job Daemons
will be the owner of the job.
Multiple Kingbird API could run in parallel, and also can work in multi-worker
mode.
Multiple Kingbird API will be designed and run in stateless mode, persistent
data will be accessed (read and write) from the Kingbird Database through the
DAL module.
Setup and encapsulate the API WSGI app
app.py:
Setup and encapsulate the API WSGI app, including integrate the
keystonemiddleware app
apicfg.py:
API configuration loading and init

View File

@ -17,6 +17,7 @@
Routines for configuring kingbird, largely copy from Neutron
"""
import os
import sys
@ -40,8 +41,9 @@ common_opts = [
help=_("The port to bind to")),
cfg.IntOpt('api_workers', default=2,
help=_("number of api workers")),
cfg.StrOpt('api_paste_config', default="api-paste.ini",
help=_("The API paste config file to use")),
cfg.StrOpt('state_path',
default=os.path.join(os.path.dirname(__file__), '../'),
help='Top-level directory for maintaining kingbird state'),
cfg.StrOpt('api_extensions_path', default="",
help=_("The path for API extensions")),
cfg.StrOpt('auth_strategy', default='keystone',

View File

@ -13,12 +13,15 @@
# License for the specific language governing permissions and limitations
# under the License.
import pecan
from keystonemiddleware import auth_token
from oslo_config import cfg
from oslo_middleware import request_id
import pecan
from oslo_service import service
from kingbird.common import exceptions as k_exc
from kingbird.common.i18n import _
def setup_app(*args, **kwargs):
@ -64,3 +67,18 @@ def _wrap_app(app):
opt_name='auth_strategy', opt_value=cfg.CONF.auth_strategy)
return app
_launcher = None
def serve(api_service, conf, workers=1):
global _launcher
if _launcher:
raise RuntimeError(_('serve() can only be called once'))
_launcher = service.launch(conf, api_service, workers=workers)
def wait():
_launcher.wait()

View File

@ -0,0 +1,14 @@
===============================
controllers
===============================
API request processing
root.py:
API root request
helloworld.py:
sample for adding a new resource/controller for http request processing
restcomm.py:
common functionality used in API

View File

@ -13,14 +13,15 @@
# License for the specific language governing permissions and limitations
# under the License.
from kingbird.jobdaemon import jdrpcapi
import pecan
from pecan import expose
from pecan import request
from pecan import rest
import restcomm
from kingbird.jobdaemon import jdrpcapi
class HelloWorldController(rest.RestController):
@ -62,9 +63,15 @@ class HelloWorldController(rest.RestController):
# jdmanager, jwmanager instead
context = restcomm.extract_context_from_environ()
payload = '## delete call ##, request.body is null'
payload = '## delete cast ##, request.body is null'
payload = payload + request.body
self.jd_api.say_hello_world_cast(context, payload)
return self._delete_response(context)
def _delete_response(self, context):
context = context
return {'cast example': 'check the log produced by jobdaemon '
+ 'and jobworker, no value returned here'}

View File

@ -13,9 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import helloworld
import pecan
from kingbird.api.controllers import helloworld
class RootController(object):
@ -24,7 +26,7 @@ class RootController(object):
if version == 'v1.0':
return V1Controller(), remainder
@pecan.expose('json')
@pecan.expose(generic=True, template='json')
def index(self):
return {
"versions": [
@ -42,6 +44,14 @@ class RootController(object):
]
}
@index.when(method='POST')
@index.when(method='PUT')
@index.when(method='DELETE')
@index.when(method='HEAD')
@index.when(method='PATCH')
def not_supported(self):
pecan.abort(405)
class V1Controller(object):
@ -54,7 +64,7 @@ class V1Controller(object):
for name, ctrl in self.sub_controllers.items():
setattr(self, name, ctrl)
@pecan.expose('json')
@pecan.expose(generic=True, template='json')
def index(self):
return {
"version": "1.0",
@ -67,3 +77,11 @@ class V1Controller(object):
for name in sorted(self.sub_controllers)
]
}
@index.when(method='POST')
@index.when(method='PUT')
@index.when(method='DELETE')
@index.when(method='HEAD')
@index.when(method='PATCH')
def not_supported(self):
pecan.abort(405)

View File

@ -47,24 +47,9 @@ class ContextBase(oslo_ctx.RequestContext):
'user_name': self.user_name,
'tenant_name': self.tenant_name,
'auth_url': self.auth_url,
'auth_token': self.auth_token,
'auth_token_info': self.auth_token_info,
'user': self.user,
'user_domain': self.user_domain,
'user_domain_name': self.user_domain_name,
'project': self.project,
'project_name': self.project_name,
'project_domain': self.project_domain,
'project_domain_name': self.project_domain_name,
'domain': self.domain,
'domain_name': self.domain_name,
'trusts': self.trusts,
'region_name': self.region_name,
'roles': self.roles,
'show_deleted': self.show_deleted,
'is_admin': self.is_admin,
'request_id': self.request_id,
'password': self.password,
'default_name': self.default_name,
'region_name': self.region_name,
})
return ctx_dict

46
kingbird/jobdaemon/README.rst Executable file
View File

@ -0,0 +1,46 @@
===============================
jobdaemon
===============================
Kingbird Job Daemon has responsibility for:
Divid job from Kingbird API to smaller jobs, each smaller job will only
be involved with one specific region, and one smaller job will be
dispatched to one Kingbird Job Worker. Multiple smaller jobs may be
dispatched to the same Kingbird Job Worker, its up to the load balancing
policy and how many Kingbird Job Workers are running.
Some job from Kingbird API could not be divided, schedule and re-schedule
such kind of (periodically running, like quota enforcement, regular
event statistic collection task) job to a specific Kingbird Job Worker.
If some Kingbird Job Worker failed, re-balance the job to other Kingbird
Job Workers.
Monitoring the job/smaller jobs status, and return the result to Kingbird
API if needed.
Generate task to purge time-out jobs from Kingbird Database
Multiple Job Daemon could run in parallel, and also can work in
multi-worker mode. But for one job from Kingbird API, only one Kingbird
Job Daemon will be the owner. One Kingbird Job Daemon could be the owner
of multiple jobs from multiple Kingbird APIs
Multiple Kingbird Daemon will be designed and run in stateless mode,
persistent data will be accessed (read and write) from the Kingbird
Database through the DAL module.
jdrpcapi.py:
the client side RPC api for JobDaemon. Often the API service will
call the api provided in this file, and the RPC client will send the
request to message-bus, and then the JobDaemon can pickup the RPC message
from the message bus
jdservice.py:
run JobDaemon in multi-worker mode, and establish RPC server
jdmanager.py:
all rpc messages received by the jdservice RPC server will be processed
in the jdmanager's regarding function.
jdcfg.py:
configuration and initialization for JobDaemon

38
kingbird/jobworker/README.rst Executable file
View File

@ -0,0 +1,38 @@
===============================
jobworker
===============================
Kingbird Job Worker has responsibility for:
Concurrently process the divided smaller jobs from Kingbird Job Daemon.
Each smaller job will be a job to a specific OpenStack instance, i.e.,
one OpenStack region.
Periodically running background job which was assigned by the Kingbird
Job Daemon, Kingbird Job Worker will generate a new one-time job (for
example, for quota enforcement, generate a collecting resource usage job),
and send it to the Kingbird Job Daemon for further processing in each
cycle. Multiple Job Worker could run in parallel, and also can work in
multi-worker mode. But for one smaller job from Kingbird Job Daemon,
only one Kingbird Job Worker will be the owner. One Kingbird Job Worker
could be the owner of multiple smaller jobs from multiple Kingbird
JobDaemons.
Multiple Kingbird Job Workers will be designed and run in stateless mode,
persistent data will be accessed (read and write) from the Kingbird
Database through the DAL module.
jwrpcapi.py:
the client side RPC api for JobWoker. Often the JobDaemon service will
call the api provided in this file, and the RPC client will send the
request to message-bus, and then the JobWorker can pickup the RPC message
from the message bus
jwservice.py:
run JobWorker in multi-worker mode, and establish RPC server
jwmanager.py:
all rpc messages received by the jwservice RPC server will be processed
in the jwmanager's regarding function.
jwcfg.py:
configuration and initialization for JobWorker

View File

View File

View File

@ -0,0 +1,312 @@
# Copyright (c) 2015 Huawei Technologies Co., Ltd.
# 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.
import mock
from mock import patch
import pecan
from pecan.configuration import set_config
from pecan.testing import load_test_app
from oslo_config import cfg
from oslo_config import fixture as fixture_config
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from kingbird.api import apicfg
from kingbird.api.controllers import helloworld
from kingbird.common import rpc
from kingbird.jobdaemon import jdrpcapi
from kingbird.tests import base
OPT_GROUP_NAME = 'keystone_authtoken'
cfg.CONF.import_group(OPT_GROUP_NAME, "keystonemiddleware.auth_token")
def fake_say_hello_world_call(self, ctxt, payload):
info_text = "say_hello_world_call, payload: %s" % payload
return {'jobdaemon': info_text}
def fake_say_hello_world_cast(self, ctxt, payload):
info_text = "say_hello_world_cast, payload: %s" % payload
return {'jobdaemon': info_text}
def fake_delete_response(self, context):
resp = jsonutils.dumps(context.to_dict())
return resp
class KBFunctionalTest(base.KingbirdTestCase):
def setUp(self):
super(KBFunctionalTest, self).setUp()
self.addCleanup(set_config, {}, overwrite=True)
apicfg.test_init()
self.CONF = self.useFixture(fixture_config.Config()).conf
# self.setup_messaging(self.CONF)
self.CONF.set_override('auth_strategy', 'noauth')
self.app = self._make_app()
def _make_app(self, enable_acl=False):
self.config = {
'app': {
'root': 'kingbird.api.controllers.root.RootController',
'modules': ['kingbird.api'],
'enable_acl': enable_acl,
'errors': {
400: '/error',
'__force_dict__': True
}
},
}
return load_test_app(self.config)
def tearDown(self):
super(KBFunctionalTest, self).tearDown()
pecan.set_config({}, overwrite=True)
class TestRootController(KBFunctionalTest):
"""Test version listing on root URI."""
def test_get(self):
response = self.app.get('/')
self.assertEqual(response.status_int, 200)
json_body = jsonutils.loads(response.body)
versions = json_body.get('versions')
self.assertEqual(1, len(versions))
def _test_method_returns_405(self, method):
api_method = getattr(self.app, method)
response = api_method('/', expect_errors=True)
self.assertEqual(response.status_int, 405)
def test_post(self):
self._test_method_returns_405('post')
def test_put(self):
self._test_method_returns_405('put')
def test_patch(self):
self._test_method_returns_405('patch')
def test_delete(self):
self._test_method_returns_405('delete')
def test_head(self):
self._test_method_returns_405('head')
class TestV1Controller(KBFunctionalTest):
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
def test_get(self):
response = self.app.get('/v1.0')
self.assertEqual(response.status_int, 200)
json_body = jsonutils.loads(response.body)
version = json_body.get('version')
self.assertEqual('1.0', version)
links = json_body.get('links')
v1_link = links[0]
helloworld_link = links[1]
self.assertEqual('self', v1_link['rel'])
self.assertEqual('helloworld', helloworld_link['rel'])
def _test_method_returns_405(self, method):
api_method = getattr(self.app, method)
response = api_method('/v1.0', expect_errors=True)
self.assertEqual(response.status_int, 405)
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
def test_post(self):
self._test_method_returns_405('post')
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
def test_put(self):
self._test_method_returns_405('put')
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
def test_patch(self):
self._test_method_returns_405('patch')
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
def test_delete(self):
self._test_method_returns_405('delete')
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
def test_head(self):
self._test_method_returns_405('head')
class TestHelloworld(KBFunctionalTest):
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
def test_get(self):
response = self.app.get('/v1.0/helloworld')
self.assertEqual(response.status_int, 200)
self.assertIn('hello world message for', response)
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
def test_post(self):
response = self.app.post_json('/v1.0/helloworld',
headers={'X-Tenant-Id': 'tenid'})
self.assertEqual(response.status_int, 200)
self.assertIn('## post call ##', response)
self.assertIn('jobdaemon', response)
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
def test_put(self):
response = self.app.put_json('/v1.0/helloworld',
headers={'X-Tenant-Id': 'tenid'})
self.assertEqual(response.status_int, 200)
self.assertIn('## put call ##', response)
self.assertIn('jobdaemon', response)
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
def test_delete(self):
response = self.app.delete('/v1.0/helloworld',
headers={'X-Tenant-Id': 'tenid'})
self.assertEqual(response.status_int, 200)
self.assertIn('cast example', response)
self.assertIn('check the log produced by jobdaemon', response)
class TestHelloworldContext(KBFunctionalTest):
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
@patch.object(helloworld.HelloWorldController, '_delete_response',
new=fake_delete_response)
def test_context_set_in_request(self):
response = self.app.delete('/v1.0/helloworld',
headers={'X_Auth_Token': 'a-token',
'X_TENANT_ID': 't-id',
'X_USER_ID': 'u-id',
'X_USER_NAME': 'u-name',
'X_PROJECT_NAME': 't-name',
'X_DOMAIN_ID': 'domainx',
'X_USER_DOMAIN_ID': 'd-u',
'X_PROJECT_DOMAIN_ID': 'p_d',
})
json_body = jsonutils.loads(response.body)
self.assertIn('a-token', json_body)
self.assertIn('t-id', json_body)
self.assertIn('u-id', json_body)
self.assertIn('u-name', json_body)
self.assertIn('t-name', json_body)
self.assertIn('domainx', json_body)
self.assertIn('d-u', json_body)
self.assertIn('p_d', json_body)
class TestErrors(KBFunctionalTest):
def test_404(self):
response = self.app.get('/assert_called_once', expect_errors=True)
self.assertEqual(response.status_int, 404)
@patch.object(rpc, 'get_client', new=mock.Mock())
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_call',
new=fake_say_hello_world_call)
@patch.object(jdrpcapi.JobDaemonAPI, 'say_hello_world_cast',
new=fake_say_hello_world_cast)
def test_bad_method(self):
response = self.app.patch('/v1.0/helloworld/123.json',
expect_errors=True)
self.assertEqual(response.status_int, 405)
class TestRequestID(KBFunctionalTest):
def test_request_id(self):
response = self.app.get('/')
self.assertIn('x-openstack-request-id', response.headers)
self.assertTrue(
response.headers['x-openstack-request-id'].startswith('req-'))
id_part = response.headers['x-openstack-request-id'].split('req-')[1]
self.assertTrue(uuidutils.is_uuid_like(id_part))
class TestKeystoneAuth(KBFunctionalTest):
def setUp(self):
super(KBFunctionalTest, self).setUp()
self.addCleanup(set_config, {}, overwrite=True)
apicfg.test_init()
self.CONF = self.useFixture(fixture_config.Config()).conf
cfg.CONF.set_override('auth_strategy', 'keystone')
self.app = self._make_app()
def test_auth_enforced(self):
response = self.app.get('/', expect_errors=True)
self.assertEqual(response.status_int, 401)

View File

@ -14,7 +14,6 @@ pecan>=1.0.0
greenlet>=0.3.2
httplib2>=0.7.5
requests!=2.8.0,>=2.5.2
Werkzeug>=0.7 # BSD License
Jinja2>=2.8 # BSD License (3 clause)
keystonemiddleware!=2.4.0,>=2.0.0
netaddr!=0.7.16,>=0.7.12