Switch from Flask's WSGI Server to OpenStack Commons + Use PasteDeploy for easier Keystone integration.

This commit is contained in:
Kiall Mac Innes 2012-10-14 18:04:25 +01:00
parent 79dda034de
commit 5d9513349e
16 changed files with 256 additions and 97 deletions

1
.gitignore vendored
View File

@ -11,5 +11,6 @@ venv
*.sqlite
var/*
etc/*.conf
etc/*.ini
AUTHORS
ChangeLog

View File

@ -15,29 +15,24 @@
# License for the specific language governing permissions and limitations
# under the License.
import sys
import eventlet
from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging
from moniker.api.app import app
from moniker.openstack.common import service
from moniker.api import service as api_service
config_files = cfg.find_config_files(project='moniker', prog='moniker-api')
eventlet.monkey_patch()
config_files = cfg.find_config_files(project='moniker',
prog='moniker-api')
config_files.append('./etc/moniker-api.conf')
# Jerry Rig the keystone middleware.
from keystone.middleware import auth_token
auth_token.CONF = cfg.CONF
cfg.CONF.register_opts(auth_token.opts, group='keystone_authtoken')
cfg.CONF(sys.argv[1:], project='moniker', prog='moniker-api',
default_config_files=config_files)
logging.setup('moniker')
if cfg.CONF.verbose or cfg.CONF.debug:
app.debug = True
serv = api_service.Service(host=cfg.CONF.api_host, port=cfg.CONF.api_port)
if cfg.CONF.enable_keystone:
# Add Keystone Middleware
middleware_conf = {'delay_auth_decision': False}
app.wsgi_app = auth_token.AuthProtocol(app.wsgi_app, middleware_conf)
app.run(host=cfg.CONF.api_host, port=cfg.CONF.api_port)
launcher = service.launch(serv)
launcher.wait()

View File

@ -0,0 +1,29 @@
[composite:osapi_dns]
use = egg:Paste#urlmap
/v1: osapi_dns_api_v1
[composite:osapi_dns_api_v1]
use = call:moniker.api.auth:pipeline_factory
noauth = noauth osapi_dns_app_v1
keystone = authtoken keystonecontext osapi_dns_app_v1
[app:osapi_dns_app_v1]
paste.app_factory = moniker.api.v1:factory
[filter:noauth]
paste.filter_factory = moniker.api.auth:NoAuthMiddleware.factory
[filter:keystonecontext]
paste.filter_factory = moniker.api.auth:KeystoneContextMiddleware.factory
[filter:authtoken]
paste.filter_factory = keystone.middleware.auth_token:filter_factory
service_protocol = http
service_host = 127.0.0.1
service_port = 5000
auth_host = 127.0.0.1
auth_port = 35357
auth_protocol = http
admin_tenant_name = %SERVICE_TENANT_NAME%
admin_user = %SERVICE_USER%
admin_password = %SERVICE_PASSWORD%

View File

@ -20,10 +20,5 @@ control_exchange = moniker
#
allowed_rpc_exception_modules = moniker.exceptions, moniker.openstack.common.exception
[keystone_authtoken]
auth_host = 127.0.0.1
auth_port = 35357
auth_protocol = http
admin_tenant_name = admin
admin_user = admin
admin_password = password
#
auth_strategy = noauth

View File

@ -0,0 +1,35 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 flask
from moniker.openstack.common import cfg
from moniker.openstack.common import jsonutils
cfg.CONF.register_opts([
cfg.StrOpt('api_host', default='0.0.0.0',
help='API Host'),
cfg.IntOpt('api_port', default=9001,
help='API Port Number'),
cfg.StrOpt('api_paste_config', default='moniker-api-paste.ini',
help='File name for the paste.deploy config for moniker-api'),
cfg.StrOpt('auth_strategy', default='noauth',
help='The strategy to use for auth. Supports noauth or '
'keystone'),
])
# Allows us to serialize datetime's etc
flask.helpers.json = jsonutils

View File

@ -1,52 +0,0 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 flask
from moniker.openstack.common import cfg
from moniker.openstack.common import jsonutils
from moniker.openstack.common.context import RequestContext
from moniker import central
from moniker.api import v1
from moniker.api import debug
# Allows us to serialize datetime's etc
flask.helpers.json = jsonutils
cfg.CONF.register_opts([
cfg.StrOpt('api_host', default='0.0.0.0',
help='API Host'),
cfg.IntOpt('api_port', default=9001,
help='API Port Number'),
])
app = flask.Flask('moniker.api')
# Blueprints
app.register_blueprint(v1.blueprint, url_prefix='/v1')
app.register_blueprint(debug.blueprint, url_prefix='/debug')
@app.before_request
def attach_context():
request = flask.request
headers = request.headers
if cfg.CONF.enable_keystone:
request.context = RequestContext(auth_tok=headers.get('X-Auth-Token'),
user=headers.get('X-User-ID'),
tenant=headers.get('X-Tenant-ID'))
else:
request.context = RequestContext(user=cfg.CONF.default_user,
tenant=cfg.CONF.default_tenant)

53
moniker/api/auth.py Normal file
View File

@ -0,0 +1,53 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 moniker.openstack.common.context import RequestContext
from moniker.openstack.common import cfg
from moniker.openstack.common import log as logging
from moniker import wsgi
LOG = logging.getLogger(__name__)
def pipeline_factory(loader, global_conf, **local_conf):
"""
A paste pipeline replica that keys off of auth_strategy.
Code nabbed from cinder.
"""
pipeline = local_conf[cfg.CONF.auth_strategy]
pipeline = pipeline.split()
filters = [loader.get_filter(n) for n in pipeline[:-1]]
app = loader.get_app(pipeline[-1])
filters.reverse()
for filter in filters:
app = filter(app)
return app
class KeystoneContextMiddleware(wsgi.Middleware):
def process_request(self, request):
headers = request.headers
context = RequestContext(auth_tok=headers.get('X-Auth-Token'),
user=headers.get('X-User-ID'),
tenant=headers.get('X-Tenant-ID'))
request.environ['context'] = context
class NoAuthMiddleware(wsgi.Middleware):
def process_request(self, request):
context = RequestContext(user=cfg.CONF.default_user,
tenant=cfg.CONF.default_tenant)
request.environ['context'] = context

44
moniker/api/service.py Normal file
View File

@ -0,0 +1,44 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 paste import deploy
from moniker.openstack.common import log as logging
from moniker.openstack.common import wsgi
from moniker.openstack.common import cfg
from moniker import utils
LOG = logging.getLogger(__name__)
class Service(wsgi.Service):
def __init__(self, port, host='0.0.0.0', backlog=128, threads=1000):
super(Service, self).__init__(threads)
self.host = host
self.port = port
self.backlog = backlog
config_path = cfg.CONF.api_paste_config
config_path = utils.find_config(config_path)
self.application = deploy.loadapp("config:%s" % config_path,
name='osapi_dns')
def start(self):
return super(Service, self).start(application=self.application,
port=self.port, host=self.host,
backlog=self.backlog)

View File

@ -20,3 +20,11 @@ blueprint = flask.Blueprint('v1', __name__)
import moniker.api.v1.servers
import moniker.api.v1.domains
import moniker.api.v1.records
def factory(global_config, **local_conf):
import flask
app = flask.Flask('moniker.api.v1')
app.register_blueprint(blueprint)
return app

View File

@ -43,7 +43,7 @@ def get_domains_schema():
@blueprint.route('/domains', methods=['POST'])
def create_domain():
context = flask.request.context
context = flask.request.environ.get('context')
values = flask.request.json
try:
@ -67,7 +67,7 @@ def create_domain():
@blueprint.route('/domains', methods=['GET'])
def get_domains():
context = flask.request.context
context = flask.request.environ.get('context')
domains = central_api.get_domains(context)
@ -78,7 +78,7 @@ def get_domains():
@blueprint.route('/domains/<domain_id>', methods=['GET'])
def get_domain(domain_id):
context = flask.request.context
context = flask.request.environ.get('context')
try:
domain = central_api.get_domain(context, domain_id)
@ -94,7 +94,7 @@ def get_domain(domain_id):
@blueprint.route('/domains/<domain_id>', methods=['PUT'])
def update_domain(domain_id):
context = flask.request.context
context = flask.request.environ.get('context')
values = flask.request.json
try:
@ -116,7 +116,7 @@ def update_domain(domain_id):
@blueprint.route('/domains/<domain_id>', methods=['DELETE'])
def delete_domain(domain_id):
context = flask.request.context
context = flask.request.environ.get('context')
try:
central_api.delete_domain(context, domain_id)

View File

@ -44,7 +44,7 @@ def get_records_schema():
@blueprint.route('/domains/<domain_id>/records', methods=['POST'])
def create_record(domain_id):
context = flask.request.context
context = flask.request.environ.get('context')
values = flask.request.json
try:
@ -69,7 +69,7 @@ def create_record(domain_id):
@blueprint.route('/domains/<domain_id>/records', methods=['GET'])
def get_records(domain_id):
context = flask.request.context
context = flask.request.environ.get('context')
records = central_api.get_records(context, domain_id)
@ -78,7 +78,7 @@ def get_records(domain_id):
@blueprint.route('/domains/<domain_id>/records/<record_id>', methods=['GET'])
def get_record(domain_id, record_id):
context = flask.request.context
context = flask.request.environ.get('context')
try:
record = central_api.get_record(context, domain_id, record_id)
@ -94,7 +94,7 @@ def get_record(domain_id, record_id):
@blueprint.route('/domains/<domain_id>/records/<record_id>', methods=['PUT'])
def update_record(domain_id, record_id):
context = flask.request.context
context = flask.request.environ.get('context')
values = flask.request.json
try:
@ -118,7 +118,7 @@ def update_record(domain_id, record_id):
@blueprint.route('/domains/<domain_id>/records/<record_id>',
methods=['DELETE'])
def delete_record(domain_id, record_id):
context = flask.request.context
context = flask.request.environ.get('context')
try:
central_api.delete_record(context, domain_id, record_id)

View File

@ -42,7 +42,7 @@ def get_servers_schema():
@blueprint.route('/servers', methods=['POST'])
def create_server():
context = flask.request.context
context = flask.request.environ.get('context')
values = flask.request.json
try:
@ -66,7 +66,7 @@ def create_server():
@blueprint.route('/servers', methods=['GET'])
def get_servers():
context = flask.request.context
context = flask.request.environ.get('context')
servers = central_api.get_servers(context)
@ -77,7 +77,7 @@ def get_servers():
@blueprint.route('/servers/<server_id>', methods=['GET'])
def get_server(server_id):
context = flask.request.context
context = flask.request.environ.get('context')
try:
server = central_api.get_server(context, server_id)
@ -93,7 +93,7 @@ def get_server(server_id):
@blueprint.route('/servers/<server_id>', methods=['PUT'])
def update_server(server_id):
context = flask.request.context
context = flask.request.environ.get('context')
values = flask.request.json
try:
@ -115,7 +115,7 @@ def update_server(server_id):
@blueprint.route('/servers/<server_id>', methods=['DELETE'])
def delete_server(server_id):
context = flask.request.context
context = flask.request.environ.get('context')
try:
central_api.delete_server(context, server_id)

View File

@ -19,6 +19,10 @@ class Base(Exception):
pass
class ConfigNotFound(Base):
pass
class InvalidObject(Base):
pass

View File

@ -56,7 +56,7 @@ class Service(service.Service):
"""
def __init__(self, threads=1000):
super(Service, self).start()
super(Service, self).__init__()
self.pool = eventlet.GreenPool(threads)
def start(self, application, port, host='0.0.0.0', backlog=128):

View File

@ -13,8 +13,10 @@
# 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 moniker.openstack.common import rpc
import os
from moniker.openstack.common import cfg
from moniker.openstack.common.notifier import api as notifier_api
from moniker import exceptions
def notify(context, service, event_type, payload):
@ -24,10 +26,28 @@ def notify(context, service, event_type, payload):
notifier_api.notify(context, publisher_id, event_type, priority, payload)
def fanout_cast(context, topic, method, **kwargs):
msg = {
'method': method,
'args': kwargs
}
def find_config(config_path):
""" Find a configuration file using the given hint.
rpc.fanout_cast(context, topic, msg)
Code nabbed from cinder.
:param config_path: Full or relative path to the config.
:returns: Full path of the config, if it exists.
:raises: `moniker.exceptions.ConfigNotFound`
"""
possible_locations = [
config_path,
os.path.join("etc", "moniker", config_path),
os.path.join("etc", config_path),
os.path.join(cfg.CONF.state_path, "etc", "moniker", config_path),
os.path.join(cfg.CONF.state_path, "etc", config_path),
os.path.join(cfg.CONF.state_path, config_path),
"/etc/moniker/%s" % config_path,
]
for path in possible_locations:
if os.path.exists(path):
return os.path.abspath(path)
raise exceptions.ConfigNotFound(os.path.abspath(config_path))

27
moniker/wsgi.py Normal file
View File

@ -0,0 +1,27 @@
# Copyright 2012 Managed I.T.
#
# Author: Kiall Mac Innes <kiall@managedit.ie>
#
# 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 moniker.openstack.common import wsgi
class Middleware(wsgi.Middleware):
@classmethod
def factory(cls, global_config, **local_conf):
""" Used for paste app factories in paste.deploy config files """
def _factory(app):
return cls(app, **local_conf)
return _factory