basic API service
Implement the basic boilerplate for the smaug APIs, using WSGI and OSLO services. Smaug-api service now can respond the request of the resource plans. Follow these steps to using smaug API service: 1 download the source code form github. 2 install dependency libraries pip install -r requirements.txt 3 install smaug API service python setup.py install 4 start smaug API service python /usr/local/bin/smaug-api --config-file /etc/smaug/smaug.conf 4 using the resource plans RESTAPI (now only available with auth_strategy noauth ) http://10.229.46.128:8799/v1/55daed3cc6da42c6aa25e55d15bb1757/plans Closes-Bug: #1514745 Change-Id: Id27c624714c1839818937b94c1ecd33f393ec282
This commit is contained in:
parent
409b447de1
commit
b2a624c783
|
@ -0,0 +1,34 @@
|
|||
#############
|
||||
# OpenStack #
|
||||
#############
|
||||
|
||||
[composite:osapi_smaug]
|
||||
use = egg:Paste#urlmap
|
||||
/: apiversions
|
||||
/v1: openstack_smaug_api_v1
|
||||
|
||||
[composite:openstack_smaug_api_v1]
|
||||
use = call:smaug.api.middleware.auth:pipeline_factory
|
||||
noauth = request_id catch_errors noauth apiv1
|
||||
keystone = request_id catch_errors authtoken keystonecontext apiv1
|
||||
|
||||
[filter:request_id]
|
||||
paste.filter_factory = oslo_middleware:RequestId.factory
|
||||
|
||||
[filter:catch_errors]
|
||||
paste.filter_factory = oslo_middleware:CatchErrors.factory
|
||||
|
||||
[filter:noauth]
|
||||
paste.filter_factory = smaug.api.middleware.auth:NoAuthMiddleware.factory
|
||||
|
||||
[filter:keystonecontext]
|
||||
paste.filter_factory = smaug.api.middleware.auth:SmaugKeystoneContext.factory
|
||||
|
||||
[filter:authtoken]
|
||||
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
|
||||
|
||||
[app:apiversions]
|
||||
paste.app_factory = smaug.api.versions:Versions.factory
|
||||
|
||||
[app:apiv1]
|
||||
paste.app_factory = smaug.api.v1.router:APIRouter.factory
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"context_is_admin": "role:admin",
|
||||
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
|
||||
"default": "rule:admin_or_owner",
|
||||
|
||||
"admin_api": "is_admin:True",
|
||||
|
||||
"plan:create": "",
|
||||
"plan:delete": "rule:admin_or_owner"
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
#[keystone_authtoken]
|
||||
#signing_dir = /var/cache/smaug
|
||||
#cafile = /opt/stack/data/ca-bundle.pem
|
||||
#auth_uri = http://192.168.1.102:5000
|
||||
#project_domain_id = default
|
||||
#project_name = service
|
||||
#user_domain_id = default
|
||||
#password = nomoresecrete
|
||||
#username = smaug
|
||||
#auth_url = http://http://192.168.1.102:35357
|
||||
#auth_plugin = password
|
||||
|
||||
[DEFAULT]
|
||||
api_paste_config = /etc/smaug/api-paste.ini
|
||||
logging_context_format_string = %(asctime)s.%(msecs)03d %(color)s%(levelname)s %(name)s [%(request_id)s %(user_id)s %(project_id)s%(color)s] %(instance)s%(color)s%(message)s
|
||||
logging_debug_format_suffix = from (pid=%(process)d) %(funcName)s %(pathname)s:%(lineno)d
|
||||
logging_default_format_string = %(asctime)s.%(msecs)03d %(color)s%(levelname)s %(name)s [-%(color)s] %(instance)s%(color)s%(message)s
|
||||
logging_exception_prefix = %(color)s%(asctime)s.%(msecs)03d TRACE %(name)s %(instance)s
|
||||
verbose = True
|
||||
debug = True
|
||||
auth_strategy = noauth
|
||||
log_dir = /var/log/
|
||||
rpc_backend = rabbit
|
|
@ -4,3 +4,27 @@
|
|||
|
||||
pbr>=1.6
|
||||
Babel>=1.3
|
||||
eventlet>=0.17.4
|
||||
greenlet>=0.3.2
|
||||
keystonemiddleware>=4.0.0
|
||||
kombu>=3.0.7
|
||||
oslo.config>=2.7.0 # Apache-2.0
|
||||
oslo.concurrency>=2.3.0 # Apache-2.0
|
||||
oslo.context>=0.2.0 # Apache-2.0
|
||||
oslo.db>=3.2.0 # Apache-2.0
|
||||
oslo.log>=1.12.0 # Apache-2.0
|
||||
oslo.messaging>2.6.1,!=2.8.0 # Apache-2.0
|
||||
oslo.middleware>=3.0.0 # Apache-2.0
|
||||
oslo.policy>=0.5.0 # Apache-2.0
|
||||
oslo.serialization>=1.10.0 # Apache-2.0
|
||||
oslo.service>=1.0.0 # Apache-2.0
|
||||
Paste
|
||||
PasteDeploy>=1.5.0
|
||||
requests>=2.8.1
|
||||
Routes!=2.0,!=2.1,>=1.12.3;python_version=='2.7'
|
||||
Routes!=2.0,>=1.12.3;python_version!='2.7'
|
||||
six>=1.9.0
|
||||
SQLAlchemy<1.1.0,>=0.9.9
|
||||
sqlalchemy-migrate>=0.9.6
|
||||
WebOb>=1.2.3
|
||||
oslo.i18n>=1.5.0 # Apache-2.0
|
||||
|
|
10
setup.cfg
10
setup.cfg
|
@ -1,5 +1,6 @@
|
|||
[metadata]
|
||||
name = smaug
|
||||
version = 0.0.1
|
||||
summary = Application Data Protection as a Service for OpenStack
|
||||
description-file =
|
||||
README.rst
|
||||
|
@ -22,6 +23,15 @@ classifier =
|
|||
[files]
|
||||
packages =
|
||||
smaug
|
||||
data_files =
|
||||
/etc/smaug =
|
||||
etc/api-paste.ini
|
||||
etc/policy.json
|
||||
etc/smaug.conf
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
smaug-api = smaug.cmd.api:main
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
|
|
|
@ -0,0 +1,158 @@
|
|||
# Copyright 2010 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.
|
||||
"""
|
||||
Common Auth Middleware.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_middleware import request_id
|
||||
from oslo_serialization import jsonutils
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from smaug.api.openstack import wsgi
|
||||
from smaug import context
|
||||
from smaug.i18n import _
|
||||
from smaug.wsgi import common as base_wsgi
|
||||
|
||||
|
||||
use_forwarded_for_opt = cfg.BoolOpt(
|
||||
'use_forwarded_for',
|
||||
default=False,
|
||||
help='Treat X-Forwarded-For as the canonical remote address. '
|
||||
'Only enable this if you have a sanitizing proxy.')
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opt(use_forwarded_for_opt)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def pipeline_factory(loader, global_conf, **local_conf):
|
||||
"""A paste pipeline replica that keys off of auth_strategy."""
|
||||
pipeline = local_conf[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 InjectContext(base_wsgi.Middleware):
|
||||
"""Add a 'smaug.context' to WSGI environ."""
|
||||
|
||||
def __init__(self, context, *args, **kwargs):
|
||||
self.context = context
|
||||
super(InjectContext, self).__init__(*args, **kwargs)
|
||||
|
||||
@webob.dec.wsgify(RequestClass=base_wsgi.Request)
|
||||
def __call__(self, req):
|
||||
req.environ['smaug.context'] = self.context
|
||||
return self.application
|
||||
|
||||
|
||||
class SmaugKeystoneContext(base_wsgi.Middleware):
|
||||
"""Make a request context from keystone headers."""
|
||||
|
||||
@webob.dec.wsgify(RequestClass=base_wsgi.Request)
|
||||
def __call__(self, req):
|
||||
user_id = req.headers.get('X_USER')
|
||||
user_id = req.headers.get('X_USER_ID', user_id)
|
||||
if user_id is None:
|
||||
LOG.debug("Neither X_USER_ID nor X_USER found in request")
|
||||
return webob.exc.HTTPUnauthorized()
|
||||
# get the roles
|
||||
roles = [r.strip() for r in req.headers.get('X_ROLE', '').split(',')]
|
||||
if 'X_TENANT_ID' in req.headers:
|
||||
# This is the new header since Keystone went to ID/Name
|
||||
project_id = req.headers['X_TENANT_ID']
|
||||
else:
|
||||
# This is for legacy compatibility
|
||||
project_id = req.headers['X_TENANT']
|
||||
|
||||
project_name = req.headers.get('X_TENANT_NAME')
|
||||
|
||||
req_id = req.environ.get(request_id.ENV_REQUEST_ID)
|
||||
|
||||
# Get the auth token
|
||||
auth_token = req.headers.get('X_AUTH_TOKEN',
|
||||
req.headers.get('X_STORAGE_TOKEN'))
|
||||
|
||||
# Build a context, including the auth_token...
|
||||
remote_address = req.remote_addr
|
||||
|
||||
service_catalog = None
|
||||
if req.headers.get('X_SERVICE_CATALOG') is not None:
|
||||
try:
|
||||
catalog_header = req.headers.get('X_SERVICE_CATALOG')
|
||||
service_catalog = jsonutils.loads(catalog_header)
|
||||
except ValueError:
|
||||
raise webob.exc.HTTPInternalServerError(
|
||||
explanation=_('Invalid service catalog json.'))
|
||||
|
||||
if CONF.use_forwarded_for:
|
||||
remote_address = req.headers.get('X-Forwarded-For', remote_address)
|
||||
ctx = context.RequestContext(user_id,
|
||||
project_id,
|
||||
project_name=project_name,
|
||||
roles=roles,
|
||||
auth_token=auth_token,
|
||||
remote_address=remote_address,
|
||||
service_catalog=service_catalog,
|
||||
request_id=req_id)
|
||||
|
||||
req.environ['smaug.context'] = ctx
|
||||
return self.application
|
||||
|
||||
|
||||
class NoAuthMiddleware(base_wsgi.Middleware):
|
||||
"""Return a fake token if one isn't specified."""
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
if 'X-Auth-Token' not in req.headers:
|
||||
user_id = req.headers.get('X-Auth-User', 'admin')
|
||||
project_id = req.headers.get('X-Auth-Project-Id', 'admin')
|
||||
os_url = os.path.join(req.url, project_id)
|
||||
res = webob.Response()
|
||||
# NOTE(vish): This is expecting and returning Auth(1.1), whereas
|
||||
# keystone uses 2.0 auth. We should probably allow
|
||||
# 2.0 auth here as well.
|
||||
res.headers['X-Auth-Token'] = '%s:%s' % (user_id, project_id)
|
||||
res.headers['X-Server-Management-Url'] = os_url
|
||||
res.content_type = 'text/plain'
|
||||
res.status = '204'
|
||||
return res
|
||||
|
||||
token = req.headers['X-Auth-Token']
|
||||
user_id, _sep, project_id = token.partition(':')
|
||||
project_id = project_id or user_id
|
||||
remote_address = getattr(req, 'remote_address', '127.0.0.1')
|
||||
if CONF.use_forwarded_for:
|
||||
remote_address = req.headers.get('X-Forwarded-For', remote_address)
|
||||
ctx = context.RequestContext(user_id,
|
||||
project_id,
|
||||
is_admin=True,
|
||||
remote_address=remote_address)
|
||||
|
||||
req.environ['smaug.context'] = ctx
|
||||
return self.application
|
|
@ -0,0 +1,58 @@
|
|||
# Copyright (c) 2013 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.
|
||||
|
||||
"""
|
||||
WSGI middleware for OpenStack API controllers.
|
||||
"""
|
||||
|
||||
from oslo_log import log as logging
|
||||
import routes
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class APIMapper(routes.Mapper):
|
||||
def routematch(self, url=None, environ=None):
|
||||
if url is "":
|
||||
result = self._match("", environ)
|
||||
return result[0], result[1]
|
||||
return routes.Mapper.routematch(self, url, environ)
|
||||
|
||||
def connect(self, *args, **kwargs):
|
||||
# NOTE(inhye): Default the format part of a route to only accept json
|
||||
# and xml so it doesn't eat all characters after a '.'
|
||||
# in the url.
|
||||
kwargs.setdefault('requirements', {})
|
||||
if not kwargs['requirements'].get('format'):
|
||||
kwargs['requirements']['format'] = 'json|xml'
|
||||
return routes.Mapper.connect(self, *args, **kwargs)
|
||||
|
||||
|
||||
class ProjectMapper(APIMapper):
|
||||
def resource(self, member_name, collection_name, **kwargs):
|
||||
if 'parent_resource' not in kwargs:
|
||||
kwargs['path_prefix'] = '{project_id}/'
|
||||
else:
|
||||
parent_resource = kwargs['parent_resource']
|
||||
p_collection = parent_resource['collection_name']
|
||||
p_member = parent_resource['member_name']
|
||||
kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection,
|
||||
p_member)
|
||||
routes.Mapper.resource(self,
|
||||
member_name,
|
||||
collection_name,
|
||||
**kwargs)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,95 @@
|
|||
# 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.
|
||||
|
||||
"""The plans api."""
|
||||
|
||||
|
||||
from oslo_log import log as logging
|
||||
import webob
|
||||
from webob import exc
|
||||
|
||||
from smaug.api.openstack import wsgi
|
||||
from smaug.i18n import _LI
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlansController(wsgi.Controller):
|
||||
"""The Plans API controller for the OpenStack API."""
|
||||
|
||||
def __init__(self):
|
||||
super(PlansController, self).__init__()
|
||||
|
||||
def show(self, req, id):
|
||||
"""Return data about the given plan."""
|
||||
context = req.environ['smaug.context']
|
||||
LOG.info(_LI("Show plan with id: %s"), id, context=context)
|
||||
# TODO(chenying)
|
||||
return {'Smaug': "Plans show test."}
|
||||
|
||||
def delete(self, req, id):
|
||||
"""Delete a plan."""
|
||||
context = req.environ['smaug.context']
|
||||
|
||||
LOG.info(_LI("Delete plan with id: %s"), id, context=context)
|
||||
|
||||
# TODO(chenying)
|
||||
return webob.Response(status_int=202)
|
||||
|
||||
def index(self, req):
|
||||
"""Returns a summary list of plans."""
|
||||
|
||||
# TODO(chenying)
|
||||
|
||||
return {'plan': "Plans index test."}
|
||||
|
||||
def detail(self, req):
|
||||
"""Returns a detailed list of plans."""
|
||||
|
||||
# TODO(chenying)
|
||||
|
||||
return {'plan': "Plans detail test."}
|
||||
|
||||
def create(self, req, body):
|
||||
"""Creates a new plan."""
|
||||
if not self.is_valid_body(body, 'plan'):
|
||||
raise exc.HTTPUnprocessableEntity()
|
||||
|
||||
LOG.debug('Create plans request body: %s', body)
|
||||
context = req.environ['smaug.context']
|
||||
|
||||
LOG.debug('Create plans request context: %s', context)
|
||||
|
||||
# TODO(chenying)
|
||||
|
||||
return {'plan': "Create a plan test."}
|
||||
|
||||
def update(self, req, id, body):
|
||||
"""Update a plan."""
|
||||
context = req.environ['smaug.context']
|
||||
|
||||
if not body:
|
||||
raise exc.HTTPUnprocessableEntity()
|
||||
|
||||
if 'plan' not in body:
|
||||
raise exc.HTTPUnprocessableEntity()
|
||||
|
||||
plan = body['plan']
|
||||
|
||||
LOG.info(_LI("Update plan : %s"), plan, context=context)
|
||||
|
||||
return {'plan': "Update a plan test."}
|
||||
|
||||
|
||||
def create_resource():
|
||||
return wsgi.Resource(PlansController())
|
|
@ -0,0 +1,30 @@
|
|||
# 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 smaug.api.openstack import ProjectMapper
|
||||
from smaug.api.v1 import plans
|
||||
from smaug.wsgi import common as wsgi_common
|
||||
|
||||
|
||||
class APIRouter(wsgi_common.Router):
|
||||
@classmethod
|
||||
def factory(cls, global_conf, **local_conf):
|
||||
return cls(ProjectMapper())
|
||||
|
||||
def __init__(self, mapper):
|
||||
plans_resources = plans.create_resource()
|
||||
mapper.resource("plan", "plans",
|
||||
controller=plans_resources,
|
||||
collection={'detail': 'GET'},
|
||||
member={'action': 'POST'})
|
||||
|
||||
super(APIRouter, self).__init__(mapper)
|
|
@ -0,0 +1,54 @@
|
|||
# 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 httplib
|
||||
from oslo_log import log as logging
|
||||
import webob.dec
|
||||
|
||||
from oslo_serialization import jsonutils
|
||||
from smaug.api.openstack import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Versions(object):
|
||||
|
||||
@classmethod
|
||||
def factory(cls, global_config, **local_config):
|
||||
return cls()
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
"""Respond to a request for all OpenStack API versions."""
|
||||
def build_version_object(version, path, status):
|
||||
return {
|
||||
'id': 'v%s' % version,
|
||||
'status': status,
|
||||
'links': [
|
||||
{
|
||||
'rel': 'self',
|
||||
'href': '%s/%s/' % (req.host_url, path),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
version_objs = []
|
||||
version_objs.extend([
|
||||
build_version_object(1.0, 'v1', 'CURRENT'),
|
||||
])
|
||||
|
||||
response = webob.Response(request=req,
|
||||
status=httplib.MULTIPLE_CHOICES,
|
||||
content_type='application/json')
|
||||
response.body = jsonutils.dumps(dict(versions=version_objs))
|
||||
return response
|
|
@ -0,0 +1,47 @@
|
|||
#!/usr/bin/env python
|
||||
# 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.
|
||||
|
||||
"""Starter script for smaug OS API."""
|
||||
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
# Need to register global_opts
|
||||
from smaug.common import config # noqa
|
||||
from smaug import i18n
|
||||
i18n.enable_lazy()
|
||||
|
||||
from smaug import rpc
|
||||
from smaug import service
|
||||
from smaug import version
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
CONF(sys.argv[1:], project='smaug',
|
||||
version=version.version_string())
|
||||
logging.setup(CONF, "smaug")
|
||||
|
||||
rpc.init(CONF)
|
||||
launcher = service.process_launcher()
|
||||
server = service.WSGIService('osapi_smaug')
|
||||
launcher.launch_service(server, workers=server.workers)
|
||||
launcher.wait()
|
|
@ -0,0 +1,61 @@
|
|||
# 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.
|
||||
|
||||
"""Command-line flag library.
|
||||
|
||||
Emulates gflags by wrapping cfg.ConfigOpts.
|
||||
|
||||
The idea is to move fully to cfg eventually, and this wrapper is a
|
||||
stepping stone.
|
||||
|
||||
"""
|
||||
|
||||
import socket
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
logging.register_options(CONF)
|
||||
|
||||
core_opts = [
|
||||
cfg.StrOpt('api_paste_config',
|
||||
default="api-paste.ini",
|
||||
help='File name for the paste.deploy config for smaug-api')
|
||||
]
|
||||
|
||||
debug_opts = [
|
||||
]
|
||||
|
||||
CONF.register_cli_opts(core_opts)
|
||||
CONF.register_cli_opts(debug_opts)
|
||||
|
||||
global_opts = [
|
||||
cfg.StrOpt('scheduler_topic',
|
||||
default='Smaug-scheduler',
|
||||
help='The topic that scheduler nodes listen on'),
|
||||
cfg.StrOpt('scheduler_manager',
|
||||
default='Smaug.scheduler.manager.SchedulerManager',
|
||||
help='Full class name for the Manager for scheduler'),
|
||||
cfg.StrOpt('host',
|
||||
default=socket.gethostname(),
|
||||
help='Name of this node. This can be an opaque identifier. '
|
||||
'It is not necessarily a host name, FQDN, or IP address.'),
|
||||
cfg.StrOpt('auth_strategy',
|
||||
default='keystone',
|
||||
choices=['noauth', 'keystone'],
|
||||
help='The strategy to use for auth. Supports noauth or '
|
||||
'keystone.'),
|
||||
]
|
||||
|
||||
CONF.register_opts(global_opts)
|
|
@ -0,0 +1,166 @@
|
|||
# Copyright 2011 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""RequestContext: context for requests that persist through all of smaug."""
|
||||
|
||||
import copy
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_context import context
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import timeutils
|
||||
import six
|
||||
|
||||
from smaug.i18n import _
|
||||
from smaug import policy
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RequestContext(context.RequestContext):
|
||||
"""Security context and request information.
|
||||
|
||||
Represents the user taking a given action within the system.
|
||||
|
||||
"""
|
||||
def __init__(self, user_id, project_id, is_admin=None, read_deleted="no",
|
||||
roles=None, project_name=None, remote_address=None,
|
||||
timestamp=None, request_id=None, auth_token=None,
|
||||
overwrite=True, quota_class=None, service_catalog=None,
|
||||
domain=None, user_domain=None, project_domain=None,
|
||||
**kwargs):
|
||||
"""Initialize RequestContext.
|
||||
|
||||
:param read_deleted: 'no' indicates deleted records are hidden, 'yes'
|
||||
indicates deleted records are visible, 'only' indicates that
|
||||
*only* deleted records are visible.
|
||||
|
||||
:param overwrite: Set to False to ensure that the greenthread local
|
||||
copy of the index is not overwritten.
|
||||
|
||||
:param kwargs: Extra arguments that might be present, but we ignore
|
||||
because they possibly came in from older rpc messages.
|
||||
"""
|
||||
|
||||
super(RequestContext, self).__init__(auth_token=auth_token,
|
||||
user=user_id,
|
||||
tenant=project_id,
|
||||
domain=domain,
|
||||
user_domain=user_domain,
|
||||
project_domain=project_domain,
|
||||
is_admin=is_admin,
|
||||
request_id=request_id)
|
||||
self.roles = roles or []
|
||||
self.project_name = project_name
|
||||
self.read_deleted = read_deleted
|
||||
self.remote_address = remote_address
|
||||
if not timestamp:
|
||||
timestamp = timeutils.utcnow()
|
||||
elif isinstance(timestamp, six.string_types):
|
||||
timestamp = timeutils.parse_isotime(timestamp)
|
||||
self.timestamp = timestamp
|
||||
self.quota_class = quota_class
|
||||
|
||||
if service_catalog:
|
||||
# Only include required parts of service_catalog
|
||||
self.service_catalog = [s for s in service_catalog
|
||||
if s.get('type') in
|
||||
('identity', 'compute', 'object-store')]
|
||||
else:
|
||||
# if list is empty or none
|
||||
self.service_catalog = []
|
||||
|
||||
# We need to have RequestContext attributes defined
|
||||
# when policy.check_is_admin invokes request logging
|
||||
# to make it loggable.
|
||||
if self.is_admin is None:
|
||||
self.is_admin = policy.check_is_admin(self.roles, self)
|
||||
elif self.is_admin and 'admin' not in self.roles:
|
||||
self.roles.append('admin')
|
||||
|
||||
def _get_read_deleted(self):
|
||||
return self._read_deleted
|
||||
|
||||
def _set_read_deleted(self, read_deleted):
|
||||
if read_deleted not in ('no', 'yes', 'only'):
|
||||
raise ValueError(_("read_deleted can only be one of 'no', "
|
||||
"'yes' or 'only', not %r") % read_deleted)
|
||||
self._read_deleted = read_deleted
|
||||
|
||||
def _del_read_deleted(self):
|
||||
del self._read_deleted
|
||||
|
||||
read_deleted = property(_get_read_deleted, _set_read_deleted,
|
||||
_del_read_deleted)
|
||||
|
||||
def to_dict(self):
|
||||
result = super(RequestContext, self).to_dict()
|
||||
result['user_id'] = self.user_id
|
||||
result['project_id'] = self.project_id
|
||||
result['project_name'] = self.project_name
|
||||
result['domain'] = self.domain
|
||||
result['read_deleted'] = self.read_deleted
|
||||
result['roles'] = self.roles
|
||||
result['remote_address'] = self.remote_address
|
||||
result['timestamp'] = self.timestamp.isoformat()
|
||||
result['quota_class'] = self.quota_class
|
||||
result['service_catalog'] = self.service_catalog
|
||||
result['request_id'] = self.request_id
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, values):
|
||||
return cls(**values)
|
||||
|
||||
def elevated(self, read_deleted=None, overwrite=False):
|
||||
"""Return a version of this context with admin flag set."""
|
||||
context = self.deepcopy()
|
||||
context.is_admin = True
|
||||
|
||||
if 'admin' not in context.roles:
|
||||
context.roles.append('admin')
|
||||
|
||||
if read_deleted is not None:
|
||||
context.read_deleted = read_deleted
|
||||
|
||||
return context
|
||||
|
||||
def deepcopy(self):
|
||||
return copy.deepcopy(self)
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
return self.tenant
|
||||
|
||||
@project_id.setter
|
||||
def project_id(self, value):
|
||||
self.tenant = value
|
||||
|
||||
@property
|
||||
def user_id(self):
|
||||
return self.user
|
||||
|
||||
@user_id.setter
|
||||
def user_id(self, value):
|
||||
self.user = value
|
||||
|
||||
|
||||
def get_admin_context(read_deleted="no"):
|
||||
return RequestContext(user_id=None,
|
||||
project_id=None,
|
||||
is_admin=True,
|
||||
read_deleted=read_deleted,
|
||||
overwrite=False)
|
|
@ -0,0 +1,171 @@
|
|||
# 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.
|
||||
|
||||
"""Smaug base exception handling.
|
||||
|
||||
Includes decorator for re-raising Smaug-type exceptions.
|
||||
|
||||
SHOULD include dedicated exception logging.
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
import webob.exc
|
||||
from webob.util import status_generic_reasons
|
||||
from webob.util import status_reasons
|
||||
|
||||
from smaug.i18n import _, _LE
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
exc_log_opts = [
|
||||
cfg.BoolOpt('fatal_exception_format_errors',
|
||||
default=False,
|
||||
help='Make exception message format errors fatal.'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(exc_log_opts)
|
||||
|
||||
|
||||
class ConvertedException(webob.exc.WSGIHTTPException):
|
||||
def __init__(self, code=500, title="", explanation=""):
|
||||
self.code = code
|
||||
# There is a strict rule about constructing status line for HTTP:
|
||||
# '...Status-Line, consisting of the protocol version followed by a
|
||||
# numeric status code and its associated textual phrase, with each
|
||||
# element separated by SP characters'
|
||||
# (http://www.faqs.org/rfcs/rfc2616.html)
|
||||
# 'code' and 'title' can not be empty because they correspond
|
||||
# to numeric status code and its associated text
|
||||
if title:
|
||||
self.title = title
|
||||
else:
|
||||
try:
|
||||
self.title = status_reasons[self.code]
|
||||
except KeyError:
|
||||
generic_code = self.code // 100
|
||||
self.title = status_generic_reasons[generic_code]
|
||||
self.explanation = explanation
|
||||
super(ConvertedException, self).__init__()
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SmaugException(Exception):
|
||||
"""Base Smaug Exception
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a 'message' property. That message will get printf'd
|
||||
with the keyword arguments provided to the constructor.
|
||||
|
||||
"""
|
||||
message = _("An unknown exception occurred.")
|
||||
code = 500
|
||||
headers = {}
|
||||
safe = False
|
||||
|
||||
def __init__(self, message=None, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
self.kwargs['message'] = message
|
||||
|
||||
if 'code' not in self.kwargs:
|
||||
try:
|
||||
self.kwargs['code'] = self.code
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
for k, v in self.kwargs.items():
|
||||
if isinstance(v, Exception):
|
||||
self.kwargs[k] = six.text_type(v)
|
||||
|
||||
if self._should_format():
|
||||
try:
|
||||
message = self.message % kwargs
|
||||
|
||||
except Exception:
|
||||
exc_info = sys.exc_info()
|
||||
# kwargs doesn't match a variable in the message
|
||||
# log the issue and the kwargs
|
||||
LOG.exception(_LE('Exception in string format operation'))
|
||||
for name, value in kwargs.items():
|
||||
LOG.error(_LE("%(name)s: %(value)s"),
|
||||
{'name': name, 'value': value})
|
||||
if CONF.fatal_exception_format_errors:
|
||||
six.reraise(*exc_info)
|
||||
# at least get the core message out if something happened
|
||||
message = self.message
|
||||
elif isinstance(message, Exception):
|
||||
message = six.text_type(message)
|
||||
|
||||
# NOTE(luisg): We put the actual message in 'msg' so that we can access
|
||||
# it, because if we try to access the message via 'message' it will be
|
||||
# overshadowed by the class' message attribute
|
||||
self.msg = message
|
||||
super(SmaugException, self).__init__(message)
|
||||
|
||||
def _should_format(self):
|
||||
return self.kwargs['message'] is None or '%(message)' in self.message
|
||||
|
||||
def __unicode__(self):
|
||||
return six.text_type(self.msg)
|
||||
|
||||
|
||||
class NotAuthorized(SmaugException):
|
||||
message = _("Not authorized.")
|
||||
code = 403
|
||||
|
||||
|
||||
class AdminRequired(NotAuthorized):
|
||||
message = _("User does not have admin privileges")
|
||||
|
||||
|
||||
class PolicyNotAuthorized(NotAuthorized):
|
||||
message = _("Policy doesn't allow %(action)s to be performed.")
|
||||
|
||||
|
||||
class Invalid(SmaugException):
|
||||
message = _("Unacceptable parameters.")
|
||||
code = 400
|
||||
|
||||
|
||||
class InvalidInput(Invalid):
|
||||
message = _("Invalid input received: %(reason)s")
|
||||
|
||||
|
||||
class NotFound(SmaugException):
|
||||
message = _("Resource could not be found.")
|
||||
code = 404
|
||||
safe = True
|
||||
|
||||
|
||||
class ConfigNotFound(NotFound):
|
||||
message = _("Could not find config at %(path)s")
|
||||
|
||||
|
||||
class MalformedRequestBody(SmaugException):
|
||||
message = _("Malformed message body: %(reason)s")
|
||||
|
||||
|
||||
class InvalidContentType(Invalid):
|
||||
message = _("Invalid content type %(content_type)s.")
|
||||
|
||||
|
||||
class PasteAppNotFound(NotFound):
|
||||
message = _("Could not load paste app '%(name)s' from %(path)s")
|
|
@ -0,0 +1,48 @@
|
|||
# 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.
|
||||
|
||||
"""oslo.i18n integration module.
|
||||
|
||||
See http://docs.openstack.org/developer/oslo.i18n/usage.html .
|
||||
|
||||
"""
|
||||
|
||||
import oslo_i18n as i18n
|
||||
|
||||
DOMAIN = 'smaug'
|
||||
|
||||
_translators = i18n.TranslatorFactory(domain=DOMAIN)
|
||||
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = _translators.primary
|
||||
|
||||
# Translators for log levels.
|
||||
#
|
||||
# The abbreviated names are meant to reflect the usual use of a short
|
||||
# name like '_'. The "L" is for "log" and the other letter comes from
|
||||
# the level.
|
||||
_LI = _translators.log_info
|
||||
_LW = _translators.log_warning
|
||||
_LE = _translators.log_error
|
||||
_LC = _translators.log_critical
|
||||
|
||||
|
||||
def enable_lazy(enable=True):
|
||||
return i18n.enable_lazy(enable)
|
||||
|
||||
|
||||
def translate(value, user_locale=None):
|
||||
return i18n.translate(value, user_locale)
|
||||
|
||||
|
||||
def get_available_languages():
|
||||
return i18n.get_available_languages(DOMAIN)
|
|
@ -0,0 +1,93 @@
|
|||
# Copyright (c) 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.
|
||||
|
||||
"""Policy Engine For Smaug"""
|
||||
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_policy import opts as policy_opts
|
||||
from oslo_policy import policy
|
||||
|
||||
from smaug import exception
|
||||
|
||||
CONF = cfg.CONF
|
||||
policy_opts.set_defaults(cfg.CONF, 'policy.json')
|
||||
|
||||
_ENFORCER = None
|
||||
|
||||
|
||||
def init():
|
||||
global _ENFORCER
|
||||
if not _ENFORCER:
|
||||
_ENFORCER = policy.Enforcer(CONF)
|
||||
|
||||
|
||||
def enforce_action(context, action):
|
||||
"""Checks that the action can be done by the given context.
|
||||
|
||||
Applies a check to ensure the context's project_id and user_id can be
|
||||
applied to the given action using the policy enforcement api.
|
||||
"""
|
||||
|
||||
return enforce(context, action, {'project_id': context.project_id,
|
||||
'user_id': context.user_id})
|
||||
|
||||
|
||||
def enforce(context, action, target):
|
||||
"""Verifies that the action is valid on the target in this context.
|
||||
|
||||
:param context: smaug context
|
||||
:param action: string representing the action to be checked
|
||||
this should be colon separated for clarity.
|
||||
i.e. ``compute:create_instance``,
|
||||
``compute:attach_volume``,
|
||||
``volume:attach_volume``
|
||||
|
||||
:param object: dictionary representing the object of the action
|
||||
for object creation this should be a dictionary representing the
|
||||
location of the object e.g. ``{'project_id': context.project_id}``
|
||||
|
||||
:raises PolicyNotAuthorized: if verification fails.
|
||||
|
||||
"""
|
||||
init()
|
||||
|
||||
return _ENFORCER.enforce(action, target, context.to_dict(),
|
||||
do_raise=True,
|
||||
exc=exception.PolicyNotAuthorized,
|
||||
action=action)
|
||||
|
||||
|
||||
def check_is_admin(roles, context=None):
|
||||
"""Whether or not user is admin according to policy setting.
|
||||
|
||||
Can use roles or user_id from context to determine if user is admin.
|
||||
In a multi-domain configuration, roles alone may not be sufficient.
|
||||
"""
|
||||
init()
|
||||
|
||||
# include project_id on target to avoid KeyError if context_is_admin
|
||||
# policy definition is missing, and default admin_or_owner rule
|
||||
# attempts to apply. Since our credentials dict does not include a
|
||||
# project_id, this target can never match as a generic rule.
|
||||
target = {'project_id': ''}
|
||||
if context is None:
|
||||
credentials = {'roles': roles}
|
||||
else:
|
||||
credentials = {'roles': context.roles,
|
||||
'user_id': context.user_id
|
||||
}
|
||||
|
||||
return _ENFORCER.enforce('context_is_admin', target, credentials)
|
|
@ -0,0 +1,137 @@
|
|||
# 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.
|
||||
|
||||
__all__ = [
|
||||
'init',
|
||||
'cleanup',
|
||||
'set_defaults',
|
||||
'add_extra_exmods',
|
||||
'clear_extra_exmods',
|
||||
'get_allowed_exmods',
|
||||
'RequestContextSerializer',
|
||||
'get_client',
|
||||
'get_server',
|
||||
'get_notifier',
|
||||
'TRANSPORT_ALIASES',
|
||||
]
|
||||
|
||||
from oslo_config import cfg
|
||||
import oslo_messaging as messaging
|
||||
from oslo_serialization import jsonutils
|
||||
|
||||
import smaug.context
|
||||
import smaug.exception
|
||||
|
||||
CONF = cfg.CONF
|
||||
TRANSPORT = None
|
||||
NOTIFIER = None
|
||||
|
||||
ALLOWED_EXMODS = [
|
||||
smaug.exception.__name__,
|
||||
]
|
||||
EXTRA_EXMODS = []
|
||||
|
||||
TRANSPORT_ALIASES = {}
|
||||
|
||||
|
||||
def init(conf):
|
||||
global TRANSPORT, NOTIFIER
|
||||
exmods = get_allowed_exmods()
|
||||
TRANSPORT = messaging.get_transport(conf,
|
||||
allowed_remote_exmods=exmods,
|
||||
aliases=TRANSPORT_ALIASES)
|
||||
|
||||
serializer = RequestContextSerializer(JsonPayloadSerializer())
|
||||
NOTIFIER = messaging.Notifier(TRANSPORT, serializer=serializer)
|
||||
|
||||
|
||||
def initialized():
|
||||
return None not in [TRANSPORT, NOTIFIER]
|
||||
|
||||
|
||||
def cleanup():
|
||||
global TRANSPORT, NOTIFIER
|
||||
assert TRANSPORT is not None
|
||||
assert NOTIFIER is not None
|
||||
TRANSPORT.cleanup()
|
||||
TRANSPORT = NOTIFIER = None
|
||||
|
||||
|
||||
def set_defaults(control_exchange):
|
||||
messaging.set_transport_defaults(control_exchange)
|
||||
|
||||
|
||||
def add_extra_exmods(*args):
|
||||
EXTRA_EXMODS.extend(args)
|
||||
|
||||
|
||||
def clear_extra_exmods():
|
||||
del EXTRA_EXMODS[:]
|
||||
|
||||
|
||||
def get_allowed_exmods():
|
||||
return ALLOWED_EXMODS + EXTRA_EXMODS
|
||||
|
||||
|
||||
class JsonPayloadSerializer(messaging.NoOpSerializer):
|
||||
@staticmethod
|
||||
def serialize_entity(context, entity):
|
||||
return jsonutils.to_primitive(entity, convert_instances=True)
|
||||
|
||||
|
||||
class RequestContextSerializer(messaging.Serializer):
|
||||
|
||||
def __init__(self, base):
|
||||
self._base = base
|
||||
|
||||
def serialize_entity(self, context, entity):
|
||||
if not self._base:
|
||||
return entity
|
||||
return self._base.serialize_entity(context, entity)
|
||||
|
||||
def deserialize_entity(self, context, entity):
|
||||
if not self._base:
|
||||
return entity
|
||||
return self._base.deserialize_entity(context, entity)
|
||||
|
||||
def serialize_context(self, context):
|
||||
_context = context.to_dict()
|
||||
return _context
|
||||
|
||||
def deserialize_context(self, context):
|
||||
return smaug.context.RequestContext.from_dict(context)
|
||||
|
||||
|
||||
def get_client(target, version_cap=None, serializer=None):
|
||||
assert TRANSPORT is not None
|
||||
serializer = RequestContextSerializer(serializer)
|
||||
return messaging.RPCClient(TRANSPORT,
|
||||
target,
|
||||
version_cap=version_cap,
|
||||
serializer=serializer)
|
||||
|
||||
|
||||
def get_server(target, endpoints, serializer=None):
|
||||
assert TRANSPORT is not None
|
||||
serializer = RequestContextSerializer(serializer)
|
||||
return messaging.get_rpc_server(TRANSPORT,
|
||||
target,
|
||||
endpoints,
|
||||
executor='eventlet',
|
||||
serializer=serializer)
|
||||
|
||||
|
||||
def get_notifier(service=None, host=None, publisher_id=None):
|
||||
assert NOTIFIER is not None
|
||||
if not publisher_id:
|
||||
publisher_id = "%s.%s" % (service, host or CONF.host)
|
||||
return NOTIFIER.prepare(publisher_id=publisher_id)
|
|
@ -0,0 +1,191 @@
|
|||
# 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.
|
||||
|
||||
"""Generic Node base class for all workers that run on hosts."""
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_service import service
|
||||
from oslo_utils import importutils
|
||||
|
||||
from smaug import exception
|
||||
from smaug.i18n import _
|
||||
from smaug import rpc
|
||||
from smaug.wsgi import common as wsgi_common
|
||||
from smaug.wsgi import eventlet_server as wsgi
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
service_opts = [
|
||||
cfg.StrOpt('osapi_smaug_listen',
|
||||
default="0.0.0.0",
|
||||
help='IP address on which OpenStack Smaug API listens'),
|
||||
cfg.PortOpt('osapi_smaug_listen_port',
|
||||
default=8799,
|
||||
help='Port on which OpenStack Smaug API listens'),
|
||||
cfg.IntOpt('osapi_smaug_workers',
|
||||
help='Number of workers for OpenStack Smaug API service. '
|
||||
'The default is equal to the number of CPUs available.'), ]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(service_opts)
|
||||
|
||||
|
||||
class WSGIService(service.ServiceBase):
|
||||
"""Provides ability to launch API from a 'paste' configuration."""
|
||||
|
||||
def __init__(self, name, loader=None):
|
||||
"""Initialize, but do not start the WSGI server.
|
||||
|
||||
:param name: The name of the WSGI server given to the loader.
|
||||
:param loader: Loads the WSGI application using the given name.
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
self.name = name
|
||||
self.manager = self._get_manager()
|
||||
self.loader = loader or wsgi_common.Loader()
|
||||
self.app = self.loader.load_app(name)
|
||||
self.host = getattr(CONF, '%s_listen' % name, "0.0.0.0")
|
||||
self.port = getattr(CONF, '%s_listen_port' % name, 0)
|
||||
self.workers = (getattr(CONF, '%s_workers' % name, None) or
|
||||
processutils.get_worker_count())
|
||||
if self.workers and self.workers < 1:
|
||||
worker_name = '%s_workers' % name
|
||||
msg = (_("%(worker_name)s value of %(workers)d is invalid, "
|
||||
"must be greater than 0.") %
|
||||
{'worker_name': worker_name,
|
||||
'workers': self.workers})
|
||||
raise exception.InvalidInput(msg)
|
||||
|
||||
self.server = wsgi.Server(name,
|
||||
self.app,
|
||||
host=self.host,
|
||||
port=self.port)
|
||||
|
||||
def _get_manager(self):
|
||||
"""Initialize a Manager object appropriate for this service.
|
||||
|
||||
Use the service name to look up a Manager subclass from the
|
||||
configuration and initialize an instance. If no class name
|
||||
is configured, just return None.
|
||||
|
||||
:returns: a Manager instance, or None.
|
||||
|
||||
"""
|
||||
fl = '%s_manager' % self.name
|
||||
if fl not in CONF:
|
||||
return None
|
||||
|
||||
manager_class_name = CONF.get(fl, None)
|
||||
if not manager_class_name:
|
||||
return None
|
||||
|
||||
manager_class = importutils.import_class(manager_class_name)
|
||||
return manager_class()
|
||||
|
||||
def start(self):
|
||||
"""Start serving this service using loaded configuration.
|
||||
|
||||
Also, retrieve updated port number in case '0' was passed in, which
|
||||
indicates a random port should be used.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
if self.manager:
|
||||
self.manager.init_host()
|
||||
self.server.start()
|
||||
self.port = self.server.port
|
||||
|
||||
def stop(self):
|
||||
"""Stop serving this API.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
self.server.stop()
|
||||
|
||||
def wait(self):
|
||||
"""Wait for the service to stop serving this API.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
self.server.wait()
|
||||
|
||||
def reset(self):
|
||||
"""Reset server greenpool size to default.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
self.server.reset()
|
||||
|
||||
|
||||
def process_launcher():
|
||||
return service.ProcessLauncher(CONF)
|
||||
|
||||
|
||||
# NOTE(vish): the global launcher is to maintain the existing
|
||||
# functionality of calling service.serve +
|
||||
# service.wait
|
||||
_launcher = None
|
||||
|
||||
|
||||
def serve(server, workers=None):
|
||||
global _launcher
|
||||
if _launcher:
|
||||
raise RuntimeError(_('serve() can only be called once'))
|
||||
|
||||
_launcher = service.launch(CONF, server, workers=workers)
|
||||
|
||||
|
||||
def wait():
|
||||
LOG.debug('Full set of CONF:')
|
||||
for flag in CONF:
|
||||
flag_get = CONF.get(flag, None)
|
||||
# hide flag contents from log if contains a password
|
||||
# should use secret flag when switch over to openstack-common
|
||||
if ("_password" in flag or "_key" in flag or
|
||||
(flag == "sql_connection" and
|
||||
("mysql:" in flag_get or "postgresql:" in flag_get))):
|
||||
LOG.debug('%s : FLAG SET ', flag)
|
||||
else:
|
||||
LOG.debug('%(flag)s : %(flag_get)s',
|
||||
{'flag': flag, 'flag_get': flag_get})
|
||||
try:
|
||||
_launcher.wait()
|
||||
except KeyboardInterrupt:
|
||||
_launcher.stop()
|
||||
rpc.cleanup()
|
||||
|
||||
|
||||
class Launcher(object):
|
||||
def __init__(self):
|
||||
self.launch_service = serve
|
||||
self.wait = wait
|
||||
|
||||
|
||||
def get_launcher():
|
||||
# Note(lpetrut): ProcessLauncher uses green pipes which fail on Windows
|
||||
# due to missing support of non-blocking I/O pipes. For this reason, the
|
||||
# service must be spawned differently on Windows, using the ServiceLauncher
|
||||
# class instead.
|
||||
if os.name == 'nt':
|
||||
return Launcher()
|
||||
else:
|
||||
return process_launcher()
|
|
@ -13,10 +13,53 @@
|
|||
# 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
|
||||
from oslo_log import log
|
||||
from oslotest import base
|
||||
|
||||
from smaug.common import config # noqa Need to register global_opts
|
||||
from smaug.tests.unit import conf_fixture
|
||||
|
||||
test_opts = [
|
||||
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(test_opts)
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
|
||||
class TestCase(base.BaseTestCase):
|
||||
|
||||
"""Test case base class for all unit tests."""
|
||||
|
||||
def setUp(self):
|
||||
"""Run before each test method to initialize test environment."""
|
||||
super(TestCase, self).setUp()
|
||||
|
||||
conf_fixture.set_defaults(CONF)
|
||||
CONF([], default_config_files=[])
|
||||
|
||||
self.override_config('policy_file',
|
||||
os.path.join(
|
||||
os.path.abspath(
|
||||
os.path.join(
|
||||
os.path.dirname(__file__),
|
||||
'..',
|
||||
)
|
||||
),
|
||||
'tests/unit/policy.json'),
|
||||
group='oslo_policy')
|
||||
|
||||
def override_config(self, name, override, group=None):
|
||||
"""Cleanly override CONF variables."""
|
||||
CONF.set_override(name, override, group)
|
||||
self.addCleanup(CONF.clear_override, name, group)
|
||||
|
||||
def flags(self, **kw):
|
||||
"""Override CONF variables for a test."""
|
||||
for k, v in kw.items():
|
||||
self.override_config(k, v)
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
# Copyright 2010 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.
|
||||
|
||||
import uuid
|
||||
|
||||
import routes
|
||||
import webob
|
||||
import webob.dec
|
||||
import webob.request
|
||||
|
||||
from smaug.api.openstack import wsgi as os_wsgi
|
||||
from smaug import context
|
||||
from smaug.wsgi import common as wsgi
|
||||
|
||||
|
||||
FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
|
||||
FAKE_UUIDS = {}
|
||||
|
||||
|
||||
class Context(object):
|
||||
pass
|
||||
|
||||
|
||||
class FakeRouter(wsgi.Router):
|
||||
def __init__(self, ext_mgr=None):
|
||||
pass
|
||||
|
||||
@webob.dec.wsgify
|
||||
def __call__(self, req):
|
||||
res = webob.Response()
|
||||
res.status = '200'
|
||||
res.headers['X-Test-Success'] = 'True'
|
||||
return res
|
||||
|
||||
|
||||
@webob.dec.wsgify
|
||||
def fake_wsgi(self, req):
|
||||
return self.application
|
||||
|
||||
|
||||
class FakeToken(object):
|
||||
id_count = 0
|
||||
|
||||
def __getitem__(self, key):
|
||||
return getattr(self, key)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
FakeToken.id_count += 1
|
||||
self.id = FakeToken.id_count
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
class FakeRequestContext(context.RequestContext):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs['auth_token'] = kwargs.get('auth_token', 'fake_auth_token')
|
||||
super(FakeRequestContext, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class HTTPRequest(webob.Request):
|
||||
|
||||
@classmethod
|
||||
def blank(cls, *args, **kwargs):
|
||||
if args is not None:
|
||||
if args[0].find('v1') == 0:
|
||||
kwargs['base_url'] = 'http://localhost/v1'
|
||||
else:
|
||||
kwargs['base_url'] = 'http://localhost/v2'
|
||||
|
||||
use_admin_context = kwargs.pop('use_admin_context', False)
|
||||
out = os_wsgi.Request.blank(*args, **kwargs)
|
||||
out.environ['smaug.context'] = FakeRequestContext(
|
||||
'fake_user',
|
||||
'fakeproject',
|
||||
is_admin=use_admin_context)
|
||||
return out
|
||||
|
||||
|
||||
class TestRouter(wsgi.Router):
|
||||
def __init__(self, controller):
|
||||
mapper = routes.Mapper()
|
||||
mapper.resource("test", "tests",
|
||||
controller=os_wsgi.Resource(controller))
|
||||
super(TestRouter, self).__init__(mapper)
|
||||
|
||||
|
||||
def get_fake_uuid(token=0):
|
||||
if token not in FAKE_UUIDS:
|
||||
FAKE_UUIDS[token] = str(uuid.uuid4())
|
||||
return FAKE_UUIDS[token]
|
|
@ -0,0 +1,76 @@
|
|||
# Copyright (c) 2012 OpenStack Foundation
|
||||
#
|
||||
# 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 oslo_middleware import request_id
|
||||
import webob
|
||||
|
||||
import smaug.api.middleware.auth
|
||||
from smaug.tests import base
|
||||
|
||||
|
||||
class TestSmaugKeystoneContextMiddleware(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSmaugKeystoneContextMiddleware, self).setUp()
|
||||
|
||||
@webob.dec.wsgify()
|
||||
def fake_app(req):
|
||||
self.context = req.environ['smaug.context']
|
||||
return webob.Response()
|
||||
|
||||
self.context = None
|
||||
self.middleware = (smaug.api.middleware.auth
|
||||
.SmaugKeystoneContext(fake_app))
|
||||
self.request = webob.Request.blank('/')
|
||||
self.request.headers['X_TENANT_ID'] = 'testtenantid'
|
||||
self.request.headers['X_AUTH_TOKEN'] = 'testauthtoken'
|
||||
|
||||
def test_no_user_or_user_id(self):
|
||||
response = self.request.get_response(self.middleware)
|
||||
self.assertEqual('401 Unauthorized', response.status)
|
||||
|
||||
def test_user_only(self):
|
||||
self.request.headers['X_USER'] = 'testuser'
|
||||
response = self.request.get_response(self.middleware)
|
||||
self.assertEqual('200 OK', response.status)
|
||||
self.assertEqual('testuser', self.context.user_id)
|
||||
|
||||
def test_user_id_only(self):
|
||||
self.request.headers['X_USER_ID'] = 'testuserid'
|
||||
response = self.request.get_response(self.middleware)
|
||||
self.assertEqual('200 OK', response.status)
|
||||
self.assertEqual('testuserid', self.context.user_id)
|
||||
|
||||
def test_user_id_trumps_user(self):
|
||||
self.request.headers['X_USER_ID'] = 'testuserid'
|
||||
self.request.headers['X_USER'] = 'testuser'
|
||||
response = self.request.get_response(self.middleware)
|
||||
self.assertEqual('200 OK', response.status)
|
||||
self.assertEqual('testuserid', self.context.user_id)
|
||||
|
||||
def test_tenant_id_name(self):
|
||||
self.request.headers['X_USER_ID'] = 'testuserid'
|
||||
self.request.headers['X_TENANT_NAME'] = 'testtenantname'
|
||||
response = self.request.get_response(self.middleware)
|
||||
self.assertEqual('200 OK', response.status)
|
||||
self.assertEqual('testtenantid', self.context.project_id)
|
||||
self.assertEqual('testtenantname', self.context.project_name)
|
||||
|
||||
def test_request_id_extracted_from_env(self):
|
||||
req_id = 'dummy-request-id'
|
||||
self.request.headers['X_PROJECT_ID'] = 'testtenantid'
|
||||
self.request.headers['X_USER_ID'] = 'testuserid'
|
||||
self.request.environ[request_id.ENV_REQUEST_ID] = req_id
|
||||
self.request.get_response(self.middleware)
|
||||
self.assertEqual(req_id, self.context.request_id)
|
|
@ -0,0 +1,125 @@
|
|||
# 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 smaug.api.openstack import wsgi
|
||||
from smaug.tests import base
|
||||
|
||||
|
||||
class RequestTest(base.TestCase):
|
||||
def test_content_type_missing(self):
|
||||
request = wsgi.Request.blank('/tests/123', method='POST')
|
||||
request.body = b"<body />"
|
||||
self.assertIsNone(request.get_content_type())
|
||||
|
||||
def test_content_type_unsupported(self):
|
||||
request = wsgi.Request.blank('/tests/123', method='POST')
|
||||
request.headers["Content-Type"] = "text/html"
|
||||
request.body = b"asdf<br />"
|
||||
self.assertIsNone(request.get_content_type())
|
||||
|
||||
def test_content_type_with_charset(self):
|
||||
request = wsgi.Request.blank('/tests/123')
|
||||
request.headers["Content-Type"] = "application/json; charset=UTF-8"
|
||||
result = request.get_content_type()
|
||||
self.assertEqual("application/json", result)
|
||||
|
||||
def test_content_type_from_accept(self):
|
||||
for content_type in ('application/json',):
|
||||
request = wsgi.Request.blank('/tests/123')
|
||||
request.headers["Accept"] = content_type
|
||||
result = request.best_match_content_type()
|
||||
self.assertEqual(content_type, result)
|
||||
|
||||
def test_content_type_from_accept_best(self):
|
||||
request = wsgi.Request.blank('/tests/123')
|
||||
request.headers["Accept"] = "application/xml, application/json"
|
||||
result = request.best_match_content_type()
|
||||
self.assertEqual("application/json", result)
|
||||
|
||||
def test_content_type_from_query_extension(self):
|
||||
request = wsgi.Request.blank('/tests/123.json')
|
||||
result = request.best_match_content_type()
|
||||
self.assertEqual("application/json", result)
|
||||
|
||||
request = wsgi.Request.blank('/tests/123.invalid')
|
||||
result = request.best_match_content_type()
|
||||
self.assertEqual("application/json", result)
|
||||
|
||||
def test_content_type_accept_default(self):
|
||||
request = wsgi.Request.blank('/tests/123.unsupported')
|
||||
request.headers["Accept"] = "application/unsupported1"
|
||||
result = request.best_match_content_type()
|
||||
self.assertEqual("application/json", result)
|
||||
|
||||
|
||||
class ActionDispatcherTest(base.TestCase):
|
||||
def test_dispatch(self):
|
||||
serializer = wsgi.ActionDispatcher()
|
||||
serializer.create = lambda x: 'pants'
|
||||
self.assertEqual('pants', serializer.dispatch({}, action='create'))
|
||||
|
||||
def test_dispatch_action_none(self):
|
||||
serializer = wsgi.ActionDispatcher()
|
||||
serializer.create = lambda x: 'pants'
|
||||
serializer.default = lambda x: 'trousers'
|
||||
self.assertEqual('trousers', serializer.dispatch({}, action=None))
|
||||
|
||||
def test_dispatch_default(self):
|
||||
serializer = wsgi.ActionDispatcher()
|
||||
serializer.create = lambda x: 'pants'
|
||||
serializer.default = lambda x: 'trousers'
|
||||
self.assertEqual('trousers', serializer.dispatch({}, action='update'))
|
||||
|
||||
|
||||
class DictSerializerTest(base.TestCase):
|
||||
def test_dispatch_default(self):
|
||||
serializer = wsgi.DictSerializer()
|
||||
self.assertEqual('', serializer.serialize({}, 'update'))
|
||||
|
||||
|
||||
class JSONDictSerializerTest(base.TestCase):
|
||||
def test_json(self):
|
||||
input_dict = dict(servers=dict(a=(2, 3)))
|
||||
expected_json = '{"servers":{"a":[2,3]}}'
|
||||
serializer = wsgi.JSONDictSerializer()
|
||||
result = serializer.serialize(input_dict)
|
||||
result = result.replace('\n', '').replace(' ', '')
|
||||
self.assertEqual(expected_json, result)
|
||||
|
||||
|
||||
class TextDeserializerTest(base.TestCase):
|
||||
def test_dispatch_default(self):
|
||||
deserializer = wsgi.TextDeserializer()
|
||||
self.assertEqual({}, deserializer.deserialize({}, 'update'))
|
||||
|
||||
|
||||
class JSONDeserializerTest(base.TestCase):
|
||||
def test_json(self):
|
||||
data = """{"a": {
|
||||
"a1": "1",
|
||||
"a2": "2",
|
||||
"bs": ["1", "2", "3", {"c": {"c1": "1"}}],
|
||||
"d": {"e": "1"},
|
||||
"f": "1"}}"""
|
||||
as_dict = {
|
||||
'body': {
|
||||
'a': {
|
||||
'a1': '1',
|
||||
'a2': '2',
|
||||
'bs': ['1', '2', '3', {'c': {'c1': '1'}}],
|
||||
'd': {'e': '1'},
|
||||
'f': '1',
|
||||
},
|
||||
},
|
||||
}
|
||||
deserializer = wsgi.JSONDeserializer()
|
||||
self.assertEqual(as_dict, deserializer.deserialize(data))
|
|
@ -0,0 +1,37 @@
|
|||
# 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 smaug.api.openstack import ProjectMapper
|
||||
from smaug.api.v1 import router
|
||||
from smaug.tests import base
|
||||
from smaug.tests.unit.api import fakes
|
||||
|
||||
|
||||
class PlansRouterTestCase(base.TestCase):
|
||||
def setUp(self):
|
||||
super(PlansRouterTestCase, self).setUp()
|
||||
mapper = ProjectMapper()
|
||||
self.app = router.APIRouter(mapper)
|
||||
|
||||
def test_plans(self):
|
||||
req = fakes.HTTPRequest.blank('/fakeproject/plans')
|
||||
req.method = 'GET'
|
||||
req.content_type = 'application/json'
|
||||
response = req.get_response(self.app)
|
||||
self.assertEqual(200, response.status_int)
|
||||
|
||||
def test_plans_detail(self):
|
||||
req = fakes.HTTPRequest.blank('/fakeproject/plans/detail')
|
||||
req.method = 'GET'
|
||||
req.content_type = 'application/json'
|
||||
response = req.get_response(self.app)
|
||||
self.assertEqual(200, response.status_int)
|
|
@ -0,0 +1,26 @@
|
|||
# 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 oslo_config import cfg
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
CONF.import_opt('policy_file', 'smaug.policy', group='oslo_policy')
|
||||
|
||||
|
||||
def set_defaults(conf):
|
||||
conf.set_default('policy_file', 'smaug.tests.unit/policy.json',
|
||||
group='oslo_policy')
|
||||
conf.set_default('policy_dirs', [], group='oslo_policy')
|
||||
conf.set_default('auth_strategy', 'noauth')
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"context_is_admin": "role:admin",
|
||||
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
|
||||
"default": "rule:admin_or_owner",
|
||||
|
||||
"admin_api": "is_admin:True",
|
||||
|
||||
"plan:create": "",
|
||||
"plan:delete": "rule:admin_or_owner"
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
# 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 sys
|
||||
|
||||
try:
|
||||
from unittest import mock
|
||||
except ImportError:
|
||||
import mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from smaug.cmd import api as smaug_api
|
||||
from smaug.tests import base
|
||||
from smaug import version
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class TestSmaugApiCmd(base.TestCase):
|
||||
"""Unit test cases for python modules under smaug/cmd."""
|
||||
|
||||
def setUp(self):
|
||||
super(TestSmaugApiCmd, self).setUp()
|
||||
sys.argv = ['smaug-api']
|
||||
CONF(sys.argv[1:], project='smaug', version=version.version_string())
|
||||
|
||||
def tearDown(self):
|
||||
super(TestSmaugApiCmd, self).tearDown()
|
||||
|
||||
@mock.patch('smaug.service.WSGIService')
|
||||
@mock.patch('smaug.service.process_launcher')
|
||||
@mock.patch('smaug.rpc.init')
|
||||
@mock.patch('oslo_log.log.setup')
|
||||
def test_main(self, log_setup, rpc_init, process_launcher,
|
||||
wsgi_service):
|
||||
launcher = process_launcher.return_value
|
||||
server = wsgi_service.return_value
|
||||
server.workers = mock.sentinel.worker_count
|
||||
|
||||
smaug_api.main()
|
||||
|
||||
self.assertEqual('smaug', CONF.project)
|
||||
self.assertEqual(CONF.version, version.version_string())
|
||||
log_setup.assert_called_once_with(CONF, "smaug")
|
||||
rpc_init.assert_called_once_with(CONF)
|
||||
process_launcher.assert_called_once_with()
|
||||
wsgi_service.assert_called_once_with('osapi_smaug')
|
||||
launcher.launch_service.assert_called_once_with(server,
|
||||
workers=server.workers)
|
||||
launcher.wait.assert_called_once_with()
|
|
@ -0,0 +1,94 @@
|
|||
# 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 smaug import context
|
||||
from smaug.tests import base
|
||||
|
||||
|
||||
class ContextTestCase(base.TestCase):
|
||||
|
||||
def test_request_context_sets_is_admin(self):
|
||||
ctxt = context.RequestContext('111',
|
||||
'222',
|
||||
roles=['admin', 'weasel'])
|
||||
self.assertEqual(True, ctxt.is_admin)
|
||||
|
||||
def test_request_context_sets_is_admin_upcase(self):
|
||||
ctxt = context.RequestContext('111',
|
||||
'222',
|
||||
roles=['Admin', 'weasel'])
|
||||
self.assertEqual(True, ctxt.is_admin)
|
||||
|
||||
def test_request_context_read_deleted(self):
|
||||
ctxt = context.RequestContext('111',
|
||||
'222',
|
||||
read_deleted='yes')
|
||||
self.assertEqual('yes', ctxt.read_deleted)
|
||||
|
||||
ctxt.read_deleted = 'no'
|
||||
self.assertEqual('no', ctxt.read_deleted)
|
||||
|
||||
def test_request_context_read_deleted_invalid(self):
|
||||
self.assertRaises(ValueError,
|
||||
context.RequestContext,
|
||||
'111',
|
||||
'222',
|
||||
read_deleted=True)
|
||||
|
||||
ctxt = context.RequestContext('111', '222')
|
||||
self.assertRaises(ValueError,
|
||||
setattr,
|
||||
ctxt,
|
||||
'read_deleted',
|
||||
True)
|
||||
|
||||
def test_request_context_elevated(self):
|
||||
user_context = context.RequestContext(
|
||||
'fake_user', 'fake_project', admin=False)
|
||||
self.assertFalse(user_context.is_admin)
|
||||
admin_context = user_context.elevated()
|
||||
self.assertFalse(user_context.is_admin)
|
||||
self.assertTrue(admin_context.is_admin)
|
||||
self.assertFalse('admin' in user_context.roles)
|
||||
self.assertTrue('admin' in admin_context.roles)
|
||||
|
||||
def test_service_catalog_nova_and_swift(self):
|
||||
service_catalog = [
|
||||
{u'type': u'compute', u'name': u'nova'},
|
||||
{u'type': u's3', u'name': u's3'},
|
||||
{u'type': u'image', u'name': u'glance'},
|
||||
{u'type': u'volume', u'name': u'cinder'},
|
||||
{u'type': u'ec2', u'name': u'ec2'},
|
||||
{u'type': u'object-store', u'name': u'swift'},
|
||||
{u'type': u'identity', u'name': u'keystone'},
|
||||
{u'type': None, u'name': u'S_withtypeNone'},
|
||||
{u'type': u'co', u'name': u'S_partofcompute'}]
|
||||
|
||||
compute_catalog = [{u'type': u'compute', u'name': u'nova'}]
|
||||
object_catalog = [{u'name': u'swift', u'type': u'object-store'}]
|
||||
ctxt = context.RequestContext('111', '222',
|
||||
service_catalog=service_catalog)
|
||||
self.assertEqual(3, len(ctxt.service_catalog))
|
||||
return_compute = [v for v in ctxt.service_catalog if
|
||||
v['type'] == u'compute']
|
||||
return_object = [v for v in ctxt.service_catalog if
|
||||
v['type'] == u'object-store']
|
||||
self.assertEqual(compute_catalog, return_compute)
|
||||
self.assertEqual(object_catalog, return_object)
|
||||
|
||||
def test_user_identity(self):
|
||||
ctx = context.RequestContext("user", "tenant",
|
||||
domain="domain",
|
||||
user_domain="user-domain",
|
||||
project_domain="project-domain")
|
||||
self.assertEqual('user tenant domain user-domain project-domain',
|
||||
ctx.to_dict()["user_identity"])
|
|
@ -0,0 +1,118 @@
|
|||
# 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 smaug import exception
|
||||
from smaug.tests import base
|
||||
|
||||
import mock
|
||||
import six
|
||||
import webob.util
|
||||
|
||||
|
||||
class SmaugExceptionTestCase(base.TestCase):
|
||||
def test_default_error_msg(self):
|
||||
class FakeSmaugException(exception.SmaugException):
|
||||
message = "default message"
|
||||
|
||||
exc = FakeSmaugException()
|
||||
self.assertEqual('default message', six.text_type(exc))
|
||||
|
||||
def test_error_msg(self):
|
||||
self.assertEqual('test',
|
||||
six.text_type(exception.SmaugException('test')))
|
||||
|
||||
def test_default_error_msg_with_kwargs(self):
|
||||
class FakeSmaugException(exception.SmaugException):
|
||||
message = "default message: %(code)s"
|
||||
|
||||
exc = FakeSmaugException(code=500)
|
||||
self.assertEqual('default message: 500', six.text_type(exc))
|
||||
|
||||
def test_error_msg_exception_with_kwargs(self):
|
||||
# NOTE(dprince): disable format errors for this test
|
||||
self.flags(fatal_exception_format_errors=False)
|
||||
|
||||
class FakeSmaugException(exception.SmaugException):
|
||||
message = "default message: %(misspelled_code)s"
|
||||
|
||||
exc = FakeSmaugException(code=500)
|
||||
self.assertEqual('default message: %(misspelled_code)s',
|
||||
six.text_type(exc))
|
||||
|
||||
def test_default_error_code(self):
|
||||
class FakeSmaugException(exception.SmaugException):
|
||||
code = 404
|
||||
|
||||
exc = FakeSmaugException()
|
||||
self.assertEqual(404, exc.kwargs['code'])
|
||||
|
||||
def test_error_code_from_kwarg(self):
|
||||
class FakeSmaugException(exception.SmaugException):
|
||||
code = 500
|
||||
|
||||
exc = FakeSmaugException(code=404)
|
||||
self.assertEqual(404, exc.kwargs['code'])
|
||||
|
||||
def test_error_msg_is_exception_to_string(self):
|
||||
msg = 'test message'
|
||||
exc1 = Exception(msg)
|
||||
exc2 = exception.SmaugException(exc1)
|
||||
self.assertEqual(msg, exc2.msg)
|
||||
|
||||
def test_exception_kwargs_to_string(self):
|
||||
msg = 'test message'
|
||||
exc1 = Exception(msg)
|
||||
exc2 = exception.SmaugException(kwarg1=exc1)
|
||||
self.assertEqual(msg, exc2.kwargs['kwarg1'])
|
||||
|
||||
def test_message_in_format_string(self):
|
||||
class FakeSmaugException(exception.SmaugException):
|
||||
message = 'FakeSmaugException: %(message)s'
|
||||
|
||||
exc = FakeSmaugException(message='message')
|
||||
self.assertEqual('FakeSmaugException: message', six.text_type(exc))
|
||||
|
||||
def test_message_and_kwarg_in_format_string(self):
|
||||
class FakeSmaugException(exception.SmaugException):
|
||||
message = 'Error %(code)d: %(message)s'
|
||||
|
||||
exc = FakeSmaugException(message='message', code=404)
|
||||
self.assertEqual('Error 404: message', six.text_type(exc))
|
||||
|
||||
def test_message_is_exception_in_format_string(self):
|
||||
class FakeSmaugException(exception.SmaugException):
|
||||
message = 'Exception: %(message)s'
|
||||
|
||||
msg = 'test message'
|
||||
exc1 = Exception(msg)
|
||||
exc2 = FakeSmaugException(message=exc1)
|
||||
self.assertEqual('Exception: test message', six.text_type(exc2))
|
||||
|
||||
|
||||
class SmaugConvertedExceptionTestCase(base.TestCase):
|
||||
def test_default_args(self):
|
||||
exc = exception.ConvertedException()
|
||||
self.assertNotEqual('', exc.title)
|
||||
self.assertEqual(500, exc.code)
|
||||
self.assertEqual('', exc.explanation)
|
||||
|
||||
def test_standard_status_code(self):
|
||||
with mock.patch.dict(webob.util.status_reasons, {200: 'reason'}):
|
||||
exc = exception.ConvertedException(code=200)
|
||||
self.assertEqual('reason', exc.title)
|
||||
|
||||
@mock.patch.dict(webob.util.status_reasons, {500: 'reason'})
|
||||
def test_generic_status_code(self):
|
||||
with mock.patch.dict(webob.util.status_generic_reasons,
|
||||
{5: 'generic_reason'}):
|
||||
exc = exception.ConvertedException(code=599)
|
||||
self.assertEqual('generic_reason', exc.title)
|
|
@ -0,0 +1,100 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
Unit Tests for remote procedure calls using queue
|
||||
"""
|
||||
|
||||
import mock
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_config import cfg
|
||||
|
||||
from smaug import exception
|
||||
from smaug import service
|
||||
from smaug.tests import base
|
||||
from smaug.wsgi import common as wsgi
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class TestWSGIService(base.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestWSGIService, self).setUp()
|
||||
|
||||
@mock.patch('smaug.utils.find_config')
|
||||
def test_service_random_port(self, mock_find_config):
|
||||
with mock.patch.object(wsgi.Loader, 'load_app') as mock_load_app:
|
||||
test_service = service.WSGIService("test_service")
|
||||
self.assertEqual(0, test_service.port)
|
||||
test_service.start()
|
||||
self.assertNotEqual(0, test_service.port)
|
||||
test_service.stop()
|
||||
self.assertTrue(mock_load_app.called)
|
||||
|
||||
@mock.patch('smaug.utils.find_config')
|
||||
def test_reset_pool_size_to_default(self, mock_find_config):
|
||||
with mock.patch.object(wsgi.Loader, 'load_app') as mock_load_app:
|
||||
test_service = service.WSGIService("test_service")
|
||||
test_service.start()
|
||||
|
||||
# Stopping the service, which in turn sets pool size to 0
|
||||
test_service.stop()
|
||||
self.assertEqual(0, test_service.server._pool.size)
|
||||
|
||||
# Resetting pool size to default
|
||||
test_service.reset()
|
||||
test_service.start()
|
||||
self.assertEqual(1000, test_service.server._pool.size)
|
||||
self.assertTrue(mock_load_app.called)
|
||||
|
||||
@mock.patch('smaug.utils.find_config')
|
||||
@mock.patch('smaug.wsgi.common.Loader.load_app')
|
||||
@mock.patch('smaug.wsgi.eventlet_server.Server')
|
||||
def test_workers_set_default(self, wsgi_server, mock_load_app,
|
||||
mock_find_config):
|
||||
test_service = service.WSGIService("osapi_smaug")
|
||||
self.assertEqual(processutils.get_worker_count(), test_service.workers)
|
||||
|
||||
@mock.patch('smaug.utils.find_config')
|
||||
@mock.patch('smaug.wsgi.common.Loader.load_app')
|
||||
@mock.patch('smaug.wsgi.eventlet_server.Server')
|
||||
def test_workers_set_good_user_setting(self, wsgi_server,
|
||||
mock_load_app,
|
||||
mock_find_config):
|
||||
self.override_config('osapi_smaug_workers', 8)
|
||||
test_service = service.WSGIService("osapi_smaug")
|
||||
self.assertEqual(8, test_service.workers)
|
||||
|
||||
@mock.patch('smaug.utils.find_config')
|
||||
@mock.patch('smaug.wsgi.common.Loader.load_app')
|
||||
@mock.patch('smaug.wsgi.eventlet_server.Server')
|
||||
def test_workers_set_zero_user_setting(self, wsgi_server,
|
||||
mock_load_app,
|
||||
mock_find_config):
|
||||
self.override_config('osapi_smaug_workers', 0)
|
||||
test_service = service.WSGIService("osapi_smaug")
|
||||
# If a value less than 1 is used, defaults to number of procs available
|
||||
self.assertEqual(processutils.get_worker_count(), test_service.workers)
|
||||
|
||||
@mock.patch('smaug.utils.find_config')
|
||||
@mock.patch('smaug.wsgi.common.Loader.load_app')
|
||||
@mock.patch('smaug.wsgi.eventlet_server.Server')
|
||||
def test_workers_set_negative_user_setting(self, wsgi_server,
|
||||
mock_load_app,
|
||||
mock_find_config):
|
||||
self.override_config('osapi_smaug_workers', -1)
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
service.WSGIService,
|
||||
"osapi_smaug")
|
||||
self.assertFalse(wsgi_server.called)
|
|
@ -0,0 +1,91 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
Test WSGI basics and provide some helper functions for other WSGI tests.
|
||||
"""
|
||||
|
||||
import mock
|
||||
import six
|
||||
import tempfile
|
||||
|
||||
import routes
|
||||
import webob
|
||||
|
||||
from smaug.tests import base
|
||||
from smaug.wsgi import common as wsgi_common
|
||||
|
||||
|
||||
class CommonTest(base.TestCase):
|
||||
|
||||
def test_debug(self):
|
||||
|
||||
class Application(wsgi_common.Application):
|
||||
"""Dummy application to test debug."""
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
start_response("200", [("X-Test", "checking")])
|
||||
return [b'Test result']
|
||||
|
||||
with mock.patch('sys.stdout', new=six.StringIO()) as mock_stdout:
|
||||
mock_stdout.buffer = six.BytesIO()
|
||||
application = wsgi_common.Debug(Application())
|
||||
result = webob.Request.blank('/').get_response(application)
|
||||
self.assertEqual(b"Test result", result.body)
|
||||
|
||||
def test_router(self):
|
||||
|
||||
class Application(wsgi_common.Application):
|
||||
"""Test application to call from router."""
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
start_response("200", [])
|
||||
return [b'Router result']
|
||||
|
||||
class Router(wsgi_common.Router):
|
||||
"""Test router."""
|
||||
|
||||
def __init__(self):
|
||||
mapper = routes.Mapper()
|
||||
mapper.connect("/test", controller=Application())
|
||||
super(Router, self).__init__(mapper)
|
||||
|
||||
result = webob.Request.blank('/test').get_response(Router())
|
||||
self.assertEqual(b"Router result", result.body)
|
||||
result = webob.Request.blank('/bad').get_response(Router())
|
||||
self.assertNotEqual(b"Router result", result.body)
|
||||
|
||||
|
||||
class LoaderNormalFilesystemTest(base.TestCase):
|
||||
"""Loader tests with normal filesystem (unmodified os.path module)."""
|
||||
|
||||
_paste_config = """
|
||||
[app:test_app]
|
||||
use = egg:Paste#static
|
||||
document_root = /tmp
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(LoaderNormalFilesystemTest, self).setUp()
|
||||
self.config = tempfile.NamedTemporaryFile(mode="w+t")
|
||||
self.config.write(self._paste_config.lstrip())
|
||||
self.config.seek(0)
|
||||
self.config.flush()
|
||||
self.loader = wsgi_common.Loader(self.config.name)
|
||||
self.addCleanup(self.config.close)
|
||||
|
||||
def test_config_found(self):
|
||||
self.assertEqual(self.config.name, self.loader.config_path)
|
||||
|
||||
def test_app_found(self):
|
||||
url_parser = self.loader.load_app("test_app")
|
||||
self.assertEqual("/tmp", url_parser.directory)
|
|
@ -0,0 +1,81 @@
|
|||
# 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.
|
||||
|
||||
"""Unit tests for `smaug.wsgi`."""
|
||||
|
||||
import mock
|
||||
|
||||
from oslo_config import cfg
|
||||
import testtools
|
||||
|
||||
from smaug.tests import base
|
||||
from smaug.wsgi import eventlet_server as wsgi
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class WSGIServerTest(base.TestCase):
|
||||
"""WSGI server tests."""
|
||||
def _ipv6_configured():
|
||||
try:
|
||||
with open('/proc/net/if_inet6') as f:
|
||||
return len(f.read()) > 0
|
||||
except IOError:
|
||||
return False
|
||||
|
||||
def test_no_app(self):
|
||||
server = wsgi.Server("test_app", None,
|
||||
host="127.0.0.1", port=0)
|
||||
self.assertEqual("test_app", server.name)
|
||||
|
||||
def test_start_random_port(self):
|
||||
server = wsgi.Server("test_random_port", None, host="127.0.0.1")
|
||||
server.start()
|
||||
self.assertNotEqual(0, server.port)
|
||||
server.stop()
|
||||
server.wait()
|
||||
|
||||
@testtools.skipIf(not _ipv6_configured(),
|
||||
"Test requires an IPV6 configured interface")
|
||||
def test_start_random_port_with_ipv6(self):
|
||||
server = wsgi.Server("test_random_port",
|
||||
None,
|
||||
host="::1")
|
||||
server.start()
|
||||
self.assertEqual("::1", server.host)
|
||||
self.assertNotEqual(0, server.port)
|
||||
server.stop()
|
||||
server.wait()
|
||||
|
||||
def test_server_pool_waitall(self):
|
||||
# test pools waitall method gets called while stopping server
|
||||
server = wsgi.Server("test_server", None,
|
||||
host="127.0.0.1")
|
||||
server.start()
|
||||
with mock.patch.object(server._pool,
|
||||
'waitall') as mock_waitall:
|
||||
server.stop()
|
||||
server.wait()
|
||||
mock_waitall.assert_called_once_with()
|
||||
|
||||
def test_reset_pool_size_to_default(self):
|
||||
server = wsgi.Server("test_resize", None, host="127.0.0.1")
|
||||
server.start()
|
||||
|
||||
# Stopping the server, which in turn sets pool size to 0
|
||||
server.stop()
|
||||
self.assertEqual(0, server._pool.size)
|
||||
|
||||
# Resetting pool size to default
|
||||
server.reset()
|
||||
server.start()
|
||||
self.assertEqual(1000, server._pool.size)
|
|
@ -0,0 +1,70 @@
|
|||
# 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.
|
||||
|
||||
"""Utilities and helper functions."""
|
||||
import os
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
|
||||
from smaug import exception
|
||||
from smaug.i18n import _
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def find_config(config_path):
|
||||
"""Find a configuration file using the given hint.
|
||||
|
||||
:param config_path: Full or relative path to the config.
|
||||
:returns: Full path of the config, if it exists.
|
||||
:raises: `smaug.exception.ConfigNotFound`
|
||||
|
||||
"""
|
||||
possible_locations = [
|
||||
config_path,
|
||||
os.path.join("/var/lib/smaug", "etc", "smaug", config_path),
|
||||
os.path.join("/var/lib/smaug", "etc", config_path),
|
||||
os.path.join("/var/lib/smaug", config_path),
|
||||
"/etc/smaug/%s" % config_path,
|
||||
]
|
||||
|
||||
for path in possible_locations:
|
||||
if os.path.exists(path):
|
||||
return os.path.abspath(path)
|
||||
|
||||
raise exception.ConfigNotFound(path=os.path.abspath(config_path))
|
||||
|
||||
|
||||
def check_string_length(value, name, min_length=0, max_length=None):
|
||||
"""Check the length of specified string.
|
||||
|
||||
:param value: the value of the string
|
||||
:param name: the name of the string
|
||||
:param min_length: the min_length of the string
|
||||
:param max_length: the max_length of the string
|
||||
"""
|
||||
if not isinstance(value, six.string_types):
|
||||
msg = _("%s is not a string or unicode") % name
|
||||
raise exception.InvalidInput(message=msg)
|
||||
|
||||
if len(value) < min_length:
|
||||
msg = _("%(name)s has a minimum character requirement of "
|
||||
"%(min_length)s.") % {'name': name, 'min_length': min_length}
|
||||
raise exception.InvalidInput(message=msg)
|
||||
|
||||
if max_length and len(value) > max_length:
|
||||
msg = _("%(name)s has more than %(max_length)s "
|
||||
"characters.") % {'name': name, 'max_length': max_length}
|
||||
raise exception.InvalidInput(message=msg)
|
|
@ -0,0 +1,23 @@
|
|||
# Copyright 2011 OpenStack Foundation
|
||||
#
|
||||
# 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 pbr import version as pbr_version
|
||||
|
||||
SMAUG_VENDOR = "OpenStack Foundation"
|
||||
SMAUG_PRODUCT = "OpenStack Smaug"
|
||||
SMAUG_PACKAGE = None # OS distro package version suffix
|
||||
|
||||
loaded = False
|
||||
version_info = pbr_version.VersionInfo('Smaug')
|
||||
version_string = version_info.version_string
|
|
@ -0,0 +1,290 @@
|
|||
# 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.
|
||||
|
||||
"""Utility methods for working with WSGI servers."""
|
||||
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from paste import deploy
|
||||
import routes.middleware
|
||||
import six
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from smaug import exception
|
||||
from smaug.i18n import _, _LE
|
||||
from smaug import utils
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Request(webob.Request):
|
||||
pass
|
||||
|
||||
|
||||
class Application(object):
|
||||
"""Base WSGI application wrapper. Subclasses need to implement __call__."""
|
||||
|
||||
@classmethod
|
||||
def factory(cls, global_config, **local_config):
|
||||
"""Used for paste app factories in paste.deploy config files.
|
||||
|
||||
Any local configuration (that is, values under the [app:APPNAME]
|
||||
section of the paste config) will be passed into the `__init__` method
|
||||
as kwargs.
|
||||
|
||||
A hypothetical configuration would look like:
|
||||
|
||||
[app:wadl]
|
||||
latest_version = 1.3
|
||||
paste.app_factory = smaug.api.fancy_api:Wadl.factory
|
||||
|
||||
which would result in a call to the `Wadl` class as
|
||||
|
||||
import smaug.api.fancy_api
|
||||
fancy_api.Wadl(latest_version='1.3')
|
||||
|
||||
You could of course re-implement the `factory` method in subclasses,
|
||||
but using the kwarg passing it shouldn't be necessary.
|
||||
|
||||
"""
|
||||
return cls(**local_config)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""Subclasses will probably want to implement __call__ like this:
|
||||
|
||||
@webob.dec.wsgify(RequestClass=Request)
|
||||
def __call__(self, req):
|
||||
# Any of the following objects work as responses:
|
||||
|
||||
# Option 1: simple string
|
||||
res = 'message\n'
|
||||
|
||||
# Option 2: a nicely formatted HTTP exception page
|
||||
res = exc.HTTPForbidden(explanation='Nice try')
|
||||
|
||||
# Option 3: a webob Response object (in case you need to play with
|
||||
# headers, or you want to be treated like an iterable)
|
||||
res = Response();
|
||||
res.app_iter = open('somefile')
|
||||
|
||||
# Option 4: any wsgi app to be run next
|
||||
res = self.application
|
||||
|
||||
# Option 5: you can get a Response object for a wsgi app, too, to
|
||||
# play with headers etc
|
||||
res = req.get_response(self.application)
|
||||
|
||||
# You can then just return your response...
|
||||
return res
|
||||
# ... or set req.response and return None.
|
||||
req.response = res
|
||||
|
||||
See the end of http://pythonpaste.org/webob/modules/dec.html
|
||||
for more info.
|
||||
|
||||
"""
|
||||
raise NotImplementedError(_('You must implement __call__'))
|
||||
|
||||
|
||||
class Middleware(Application):
|
||||
"""Base WSGI middleware.
|
||||
|
||||
These classes require an application to be
|
||||
initialized that will be called next. By default the middleware will
|
||||
simply call its wrapped app, or you can override __call__ to customize its
|
||||
behavior.
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def factory(cls, global_config, **local_config):
|
||||
"""Used for paste app factories in paste.deploy config files.
|
||||
|
||||
Any local configuration (that is, values under the [filter:APPNAME]
|
||||
section of the paste config) will be passed into the `__init__` method
|
||||
as kwargs.
|
||||
|
||||
A hypothetical configuration would look like:
|
||||
|
||||
[filter:analytics]
|
||||
redis_host = 127.0.0.1
|
||||
paste.filter_factory = smaug.api.analytics:Analytics.factory
|
||||
|
||||
which would result in a call to the `Analytics` class as
|
||||
|
||||
import smaug.api.analytics
|
||||
analytics.Analytics(app_from_paste, redis_host='127.0.0.1')
|
||||
|
||||
You could of course re-implement the `factory` method in subclasses,
|
||||
but using the kwarg passing it shouldn't be necessary.
|
||||
|
||||
"""
|
||||
def _factory(app):
|
||||
return cls(app, **local_config)
|
||||
return _factory
|
||||
|
||||
def __init__(self, application):
|
||||
self.application = application
|
||||
|
||||
def process_request(self, req):
|
||||
"""Called on each request.
|
||||
|
||||
If this returns None, the next application down the stack will be
|
||||
executed. If it returns a response then that response will be returned
|
||||
and execution will stop here.
|
||||
|
||||
"""
|
||||
return None
|
||||
|
||||
def process_response(self, response):
|
||||
"""Do whatever you'd like to the response."""
|
||||
return response
|
||||
|
||||
@webob.dec.wsgify(RequestClass=Request)
|
||||
def __call__(self, req):
|
||||
response = self.process_request(req)
|
||||
if response:
|
||||
return response
|
||||
response = req.get_response(self.application)
|
||||
return self.process_response(response)
|
||||
|
||||
|
||||
class Debug(Middleware):
|
||||
"""Helper class for debugging a WSGI application.
|
||||
|
||||
Can be inserted into any WSGI application chain to get information
|
||||
about the request and response.
|
||||
|
||||
"""
|
||||
|
||||
@webob.dec.wsgify(RequestClass=Request)
|
||||
def __call__(self, req):
|
||||
print(('*' * 40) + ' REQUEST ENVIRON') # noqa
|
||||
for key, value in req.environ.items():
|
||||
print(key, '=', value) # noqa
|
||||
print() # noqa
|
||||
resp = req.get_response(self.application)
|
||||
|
||||
print(('*' * 40) + ' RESPONSE HEADERS') # noqa
|
||||
for (key, value) in resp.headers.items():
|
||||
print(key, '=', value) # noqa
|
||||
print() # noqa
|
||||
|
||||
resp.app_iter = self.print_generator(resp.app_iter)
|
||||
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def print_generator(app_iter):
|
||||
"""Iterator that prints the contents of a wrapper string."""
|
||||
print(('*' * 40) + ' BODY') # noqa
|
||||
for part in app_iter:
|
||||
if six.PY3:
|
||||
sys.stdout.flush()
|
||||
sys.stdout.buffer.write(part) # pylint: disable=E1101
|
||||
sys.stdout.buffer.flush() # pylint: disable=E1101
|
||||
else:
|
||||
sys.stdout.write(part)
|
||||
sys.stdout.flush()
|
||||
yield part
|
||||
print() # noqa
|
||||
|
||||
|
||||
class Router(object):
|
||||
"""WSGI middleware that maps incoming requests to WSGI apps."""
|
||||
|
||||
def __init__(self, mapper):
|
||||
"""Create a router for the given routes.Mapper.
|
||||
|
||||
Each route in `mapper` must specify a 'controller', which is a
|
||||
WSGI app to call. You'll probably want to specify an 'action' as
|
||||
well and have your controller be an object that can route
|
||||
the request to the action-specific method.
|
||||
|
||||
Examples:
|
||||
mapper = routes.Mapper()
|
||||
sc = ServerController()
|
||||
|
||||
# Explicit mapping of one route to a controller+action
|
||||
mapper.connect(None, '/svrlist', controller=sc, action='list')
|
||||
|
||||
# Actions are all implicitly defined
|
||||
mapper.resource('server', 'servers', controller=sc)
|
||||
|
||||
# Pointing to an arbitrary WSGI app. You can specify the
|
||||
# {path_info:.*} parameter so the target app can be handed just that
|
||||
# section of the URL.
|
||||
mapper.connect(None, '/v1.0/{path_info:.*}', controller=BlogApp())
|
||||
|
||||
"""
|
||||
self.map = mapper
|
||||
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
|
||||
self.map)
|
||||
|
||||
@webob.dec.wsgify(RequestClass=Request)
|
||||
def __call__(self, req):
|
||||
"""Route the incoming request to a controller based on self.map.
|
||||
|
||||
If no match, return a 404.
|
||||
|
||||
"""
|
||||
return self._router
|
||||
|
||||
@staticmethod
|
||||
@webob.dec.wsgify(RequestClass=Request)
|
||||
def _dispatch(req):
|
||||
"""Dispatch the request to the appropriate controller.
|
||||
|
||||
Called by self._router after matching the incoming request to a route
|
||||
and putting the information into req.environ. Either returns 404
|
||||
or the routed WSGI app's response.
|
||||
|
||||
"""
|
||||
match = req.environ['wsgiorg.routing_args'][1]
|
||||
if not match:
|
||||
return webob.exc.HTTPNotFound()
|
||||
app = match['controller']
|
||||
return app
|
||||
|
||||
|
||||
class Loader(object):
|
||||
"""Used to load WSGI applications from paste configurations."""
|
||||
|
||||
def __init__(self, config_path=None):
|
||||
"""Initialize the loader, and attempt to find the config.
|
||||
|
||||
:param config_path: Full or relative path to the paste config.
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
config_path = config_path or CONF.api_paste_config
|
||||
self.config_path = utils.find_config(config_path)
|
||||
|
||||
def load_app(self, name):
|
||||
"""Return the paste URLMap wrapped WSGI application.
|
||||
|
||||
:param name: Name of the application to load.
|
||||
:returns: Paste URLMap object wrapping the requested application.
|
||||
:raises: `smaug.exception.PasteAppNotFound`
|
||||
|
||||
"""
|
||||
try:
|
||||
return deploy.loadapp("config:%s" % self.config_path, name=name)
|
||||
except LookupError:
|
||||
LOG.exception(_LE("Error loading app %s"), name)
|
||||
raise exception.PasteAppNotFound(name=name, path=self.config_path)
|
|
@ -0,0 +1,282 @@
|
|||
# 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.
|
||||
|
||||
"""Methods for working with eventlet WSGI servers."""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import errno
|
||||
import os
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
|
||||
import eventlet
|
||||
import eventlet.wsgi
|
||||
import greenlet
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_service import service
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import netutils
|
||||
|
||||
|
||||
from smaug import exception
|
||||
from smaug.i18n import _, _LE, _LI
|
||||
|
||||
|
||||
socket_opts = [
|
||||
cfg.BoolOpt('tcp_keepalive',
|
||||
default=True,
|
||||
help="Sets the value of TCP_KEEPALIVE (True/False) for each "
|
||||
"server socket."),
|
||||
cfg.IntOpt('tcp_keepidle',
|
||||
default=600,
|
||||
help="Sets the value of TCP_KEEPIDLE in seconds for each "
|
||||
"server socket. Not supported on OS X."),
|
||||
cfg.IntOpt('tcp_keepalive_interval',
|
||||
help="Sets the value of TCP_KEEPINTVL in seconds for each "
|
||||
"server socket. Not supported on OS X."),
|
||||
cfg.IntOpt('tcp_keepalive_count',
|
||||
help="Sets the value of TCP_KEEPCNT for each "
|
||||
"server socket. Not supported on OS X."),
|
||||
cfg.StrOpt('ssl_ca_file',
|
||||
help="CA certificate file to use to verify "
|
||||
"connecting clients"),
|
||||
cfg.StrOpt('ssl_cert_file',
|
||||
help="Certificate file to use when starting "
|
||||
"the server securely"),
|
||||
cfg.StrOpt('ssl_key_file',
|
||||
help="Private key file to use when starting "
|
||||
"the server securely"),
|
||||
]
|
||||
|
||||
eventlet_opts = [
|
||||
cfg.IntOpt('max_header_line',
|
||||
default=16384,
|
||||
help="Maximum line size of message headers to be accepted. "
|
||||
"max_header_line may need to be increased when using "
|
||||
"large tokens (typically those generated by the "
|
||||
"Keystone v3 API with big service catalogs)."),
|
||||
cfg.IntOpt('client_socket_timeout', default=900,
|
||||
help="Timeout for client connections\' socket operations. "
|
||||
"If an incoming connection is idle for this number of "
|
||||
"seconds it will be closed. A value of \'0\' means "
|
||||
"wait forever."),
|
||||
cfg.BoolOpt('wsgi_keep_alive',
|
||||
default=True,
|
||||
help='If False, closes the client socket connection '
|
||||
'explicitly. Setting it to True to maintain backward '
|
||||
'compatibility. Recommended setting is set it to False.'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(socket_opts)
|
||||
CONF.register_opts(eventlet_opts)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Server(service.ServiceBase):
|
||||
"""Server class to manage a WSGI server, serving a WSGI application."""
|
||||
|
||||
default_pool_size = 1000
|
||||
|
||||
def __init__(self, name, app, host=None, port=None, pool_size=None,
|
||||
protocol=eventlet.wsgi.HttpProtocol, backlog=128):
|
||||
"""Initialize, but do not start, a WSGI server.
|
||||
|
||||
:param name: Pretty name for logging.
|
||||
:param app: The WSGI application to serve.
|
||||
:param host: IP address to serve the application.
|
||||
:param port: Port number to server the application.
|
||||
:param pool_size: Maximum number of eventlets to spawn concurrently.
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
# Allow operators to customize http requests max header line size.
|
||||
eventlet.wsgi.MAX_HEADER_LINE = CONF.max_header_line
|
||||
self.client_socket_timeout = CONF.client_socket_timeout or None
|
||||
self.name = name
|
||||
self.app = app
|
||||
self._host = host or "0.0.0.0"
|
||||
self._port = port or 0
|
||||
self._server = None
|
||||
self._socket = None
|
||||
self._protocol = protocol
|
||||
self.pool_size = pool_size or self.default_pool_size
|
||||
self._pool = eventlet.GreenPool(self.pool_size)
|
||||
self._logger = logging.getLogger("eventlet.wsgi.server")
|
||||
|
||||
if backlog < 1:
|
||||
raise exception.InvalidInput(
|
||||
reason='The backlog must be more than 1')
|
||||
|
||||
bind_addr = (host, port)
|
||||
# TODO(dims): eventlet's green dns/socket module does not actually
|
||||
# support IPv6 in getaddrinfo(). We need to get around this in the
|
||||
# future or monitor upstream for a fix
|
||||
try:
|
||||
info = socket.getaddrinfo(bind_addr[0],
|
||||
bind_addr[1],
|
||||
socket.AF_UNSPEC,
|
||||
socket.SOCK_STREAM)[0]
|
||||
family = info[0]
|
||||
bind_addr = info[-1]
|
||||
except Exception:
|
||||
family = socket.AF_INET
|
||||
|
||||
cert_file = CONF.ssl_cert_file
|
||||
key_file = CONF.ssl_key_file
|
||||
ca_file = CONF.ssl_ca_file
|
||||
self._use_ssl = cert_file or key_file
|
||||
|
||||
if cert_file and not os.path.exists(cert_file):
|
||||
raise RuntimeError(_("Unable to find cert_file : %s")
|
||||
% cert_file)
|
||||
|
||||
if ca_file and not os.path.exists(ca_file):
|
||||
raise RuntimeError(_("Unable to find ca_file : %s") % ca_file)
|
||||
|
||||
if key_file and not os.path.exists(key_file):
|
||||
raise RuntimeError(_("Unable to find key_file : %s")
|
||||
% key_file)
|
||||
|
||||
if self._use_ssl and (not cert_file or not key_file):
|
||||
raise RuntimeError(_("When running server in SSL mode, you "
|
||||
"must specify both a cert_file and "
|
||||
"key_file option value in your "
|
||||
"configuration file."))
|
||||
|
||||
retry_until = time.time() + 30
|
||||
while not self._socket and time.time() < retry_until:
|
||||
try:
|
||||
self._socket = eventlet.listen(bind_addr, backlog=backlog,
|
||||
family=family)
|
||||
except socket.error as err:
|
||||
if err.args[0] != errno.EADDRINUSE:
|
||||
raise
|
||||
eventlet.sleep(0.1)
|
||||
|
||||
if not self._socket:
|
||||
raise RuntimeError(_("Could not bind to %(host)s:%(port)s "
|
||||
"after trying for 30 seconds") %
|
||||
{'host': host, 'port': port})
|
||||
|
||||
(self._host, self._port) = self._socket.getsockname()[0:2]
|
||||
LOG.info(_LI("%(name)s listening on %(_host)s:%(_port)s"),
|
||||
{'name': self.name, '_host': self._host, '_port': self._port})
|
||||
|
||||
def start(self):
|
||||
"""Start serving a WSGI application.
|
||||
|
||||
:returns: None
|
||||
:raises: smaug.exception.InvalidInput
|
||||
|
||||
"""
|
||||
# The server socket object will be closed after server exits,
|
||||
# but the underlying file descriptor will remain open, and will
|
||||
# give bad file descriptor error. So duplicating the socket object,
|
||||
# to keep file descriptor usable.
|
||||
|
||||
dup_socket = self._socket.dup()
|
||||
dup_socket.setsockopt(socket.SOL_SOCKET,
|
||||
socket.SO_REUSEADDR, 1)
|
||||
|
||||
# NOTE(praneshp): Call set_tcp_keepalive in oslo to set
|
||||
# tcp keepalive parameters. Sockets can hang around forever
|
||||
# without keepalive
|
||||
netutils.set_tcp_keepalive(dup_socket,
|
||||
CONF.tcp_keepalive,
|
||||
CONF.tcp_keepidle,
|
||||
CONF.tcp_keepalive_count,
|
||||
CONF.tcp_keepalive_interval)
|
||||
|
||||
if self._use_ssl:
|
||||
try:
|
||||
ssl_kwargs = {
|
||||
'server_side': True,
|
||||
'certfile': CONF.ssl_cert_file,
|
||||
'keyfile': CONF.ssl_key_file,
|
||||
'cert_reqs': ssl.CERT_NONE,
|
||||
}
|
||||
|
||||
if CONF.ssl_ca_file:
|
||||
ssl_kwargs['ca_certs'] = CONF.ssl_ca_file
|
||||
ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED
|
||||
|
||||
dup_socket = ssl.wrap_socket(dup_socket,
|
||||
**ssl_kwargs)
|
||||
except Exception:
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.error(_LE("Failed to start %(name)s on %(_host)s: "
|
||||
"%(_port)s with SSL "
|
||||
"support."), self.__dict__)
|
||||
|
||||
wsgi_kwargs = {
|
||||
'func': eventlet.wsgi.server,
|
||||
'sock': dup_socket,
|
||||
'site': self.app,
|
||||
'protocol': self._protocol,
|
||||
'custom_pool': self._pool,
|
||||
'log': self._logger,
|
||||
'socket_timeout': self.client_socket_timeout,
|
||||
'keepalive': CONF.wsgi_keep_alive
|
||||
}
|
||||
|
||||
self._server = eventlet.spawn(**wsgi_kwargs)
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self._host
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return self._port
|
||||
|
||||
def stop(self):
|
||||
"""Stop this server.
|
||||
|
||||
This is not a very nice action, as currently the method by which a
|
||||
server is stopped is by killing its eventlet.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
LOG.info(_LI("Stopping WSGI server."))
|
||||
if self._server is not None:
|
||||
# Resize pool to stop new requests from being processed
|
||||
self._pool.resize(0)
|
||||
self._server.kill()
|
||||
|
||||
def wait(self):
|
||||
"""Block, until the server has stopped.
|
||||
|
||||
Waits on the server's eventlet to finish, then returns.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
try:
|
||||
if self._server is not None:
|
||||
self._pool.waitall()
|
||||
self._server.wait()
|
||||
except greenlet.GreenletExit:
|
||||
LOG.info(_LI("WSGI server has stopped."))
|
||||
|
||||
def reset(self):
|
||||
"""Reset server greenpool size to default.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
self._pool.resize(self.pool_size)
|
Loading…
Reference in New Issue