Add support for wsgi framework
Added wsgi framework support to create masakari-api service using oslo_service framework. Similar to core openstack projects, config items will be kept at centralized location. Refer README.rst to cofigure and run masakari-api service. Change-Id: Idb0120b8cf3b10642c51b286d82cd0944cad5ca3
This commit is contained in:
parent
336f6dc384
commit
d4f055262e
33
README.rst
33
README.rst
@ -19,13 +19,42 @@ Masakari is distributed under the terms of the Apache License,
|
||||
Version 2.0. The full terms and conditions of this license are
|
||||
detailed in the LICENSE file.
|
||||
|
||||
|
||||
|
||||
* Free software: Apache license 2.0
|
||||
* Documentation: http://docs.openstack.org/developer/masakari
|
||||
* Source: http://git.openstack.org/cgit/openstack/masakari
|
||||
* Bugs: http://bugs.launchpad.net/masakari
|
||||
|
||||
|
||||
Configure masakari-api
|
||||
----------------------
|
||||
|
||||
1. Create masakari user:
|
||||
$ openstack user create --password-prompt masakari
|
||||
(give password as masakari)
|
||||
|
||||
2. Add admin role to masakari user:
|
||||
$ openstack role add --project service --user masakari admin
|
||||
|
||||
3. Create new service:
|
||||
$ openstack service create --name masakari --description "masakari high availability" masakari
|
||||
|
||||
4. Create endpoint for masakari service:
|
||||
$ openstack endpoint create --region RegionOne masakari --publicurl http://<ip-address>:<port>/v1/%\(tenant_id\)s --adminurl http://<ip-address>:<port>/v1/%\(tenant_id\)s --internalurl http://<ip-address>:<port>/v1/%\(tenant_id\)s
|
||||
|
||||
5. Clone masakari using
|
||||
$ git clone https://github.com/openstack/masakari.git
|
||||
|
||||
6. Run setup.py from masakari
|
||||
$ sudo python setup.py install
|
||||
|
||||
7. Create masakari directory in /etc/
|
||||
|
||||
8. Copy masakari.conf, api-paste.ini and policy.json file from masakari/etc/ to
|
||||
/etc/masakari folder
|
||||
|
||||
9. To run masakari-api simply use following binary:
|
||||
$ masakari-api
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
|
45
etc/masakari/api-paste.ini
Normal file
45
etc/masakari/api-paste.ini
Normal file
@ -0,0 +1,45 @@
|
||||
[composite:masakari_api]
|
||||
use = call:masakari.api.urlmap:urlmap_factory
|
||||
/: apiversions
|
||||
/v1: masakari_api_v1
|
||||
|
||||
|
||||
[composite:masakari_api_v1]
|
||||
use = call:masakari.api.auth:pipeline_factory_v1
|
||||
keystone = cors http_proxy_to_wsgi request_id faultwrap sizelimit authtoken keystonecontext osapi_masakari_app_v1
|
||||
|
||||
# filters
|
||||
[filter:cors]
|
||||
paste.filter_factory = oslo_middleware.cors:filter_factory
|
||||
oslo_config_project = masakari
|
||||
|
||||
[filter:http_proxy_to_wsgi]
|
||||
paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory
|
||||
|
||||
[filter:request_id]
|
||||
paste.filter_factory = oslo_middleware:RequestId.factory
|
||||
|
||||
[filter:faultwrap]
|
||||
paste.filter_factory = masakari.api.openstack:FaultWrapper.factory
|
||||
|
||||
[filter:sizelimit]
|
||||
paste.filter_factory = oslo_middleware:RequestBodySizeLimiter.factory
|
||||
|
||||
[filter:authtoken]
|
||||
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
|
||||
|
||||
[filter:keystonecontext]
|
||||
paste.filter_factory = masakari.api.auth:MasakariKeystoneContext.factory
|
||||
|
||||
[filter:noauth2]
|
||||
paste.filter_factory = masakari.api.auth:NoAuthMiddleware.factory
|
||||
|
||||
# apps
|
||||
[app:osapi_masakari_app_v1]
|
||||
paste.app_factory = masakari.api.openstack.ha:APIRouterV1.factory
|
||||
|
||||
[pipeline:apiversions]
|
||||
pipeline = faultwrap http_proxy_to_wsgi apiversionsapp
|
||||
|
||||
[app:apiversionsapp]
|
||||
paste.app_factory = masakari.api.openstack.ha.versions:Versions.factory
|
26
etc/masakari/masakari.conf
Normal file
26
etc/masakari/masakari.conf
Normal file
@ -0,0 +1,26 @@
|
||||
[DEFAULT]
|
||||
enabled_apis = masakari_api
|
||||
|
||||
# Enable to specify listening IP other than default
|
||||
masakari_api_listen = 127.0.0.1
|
||||
# Enable to specify port other than default
|
||||
#masakari_api_listen_port = 15868
|
||||
debug = False
|
||||
auth_strategy=keystone
|
||||
|
||||
[wsgi]
|
||||
# The paste configuration file path
|
||||
api_paste_config = /etc/masakari/api-paste.ini
|
||||
|
||||
[keystone_authtoken]
|
||||
auth_uri = http://127.0.0.1:5000
|
||||
auth_url = http://127.0.0.1:35357
|
||||
auth_type = password
|
||||
project_domain_id = default
|
||||
user_domain_id = default
|
||||
project_name = service
|
||||
username = masakari
|
||||
password = masakari
|
||||
|
||||
[database]
|
||||
connection = mysql+pymysql://root:admin@127.0.0.1/masakari?charset=utf8
|
7
etc/masakari/policy.json
Normal file
7
etc/masakari/policy.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"admin_api": "is_admin:True",
|
||||
"context_is_admin": "role:admin",
|
||||
"admin_or_owner": "is_admin:True or project_id:%(project_id)s",
|
||||
"default": "rule:admin_or_owner",
|
||||
"os_masakari_api:extensions": "rule:admin_api"
|
||||
}
|
@ -1,19 +1,29 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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
|
||||
# Copyright (c) 2016 NTT Data
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
# 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
|
||||
#
|
||||
# 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.
|
||||
# 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 pbr.version
|
||||
"""
|
||||
:mod:`masakari` -- Cloud IaaS Platform
|
||||
===================================
|
||||
|
||||
.. automodule:: masakari
|
||||
:platform: Unix
|
||||
:synopsis: Infrastructure-as-a-Service Cloud platform.
|
||||
"""
|
||||
|
||||
__version__ = pbr.version.VersionInfo(
|
||||
'masakari').version_string()
|
||||
import os
|
||||
|
||||
os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
|
||||
|
||||
import eventlet # noqa
|
||||
|
0
masakari/api/__init__.py
Normal file
0
masakari/api/__init__.py
Normal file
182
masakari/api/api_version_request.py
Normal file
182
masakari/api/api_version_request.py
Normal file
@ -0,0 +1,182 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
|
||||
from masakari import exception
|
||||
from masakari.i18n import _
|
||||
|
||||
# Define the minimum and maximum version of the API across all of the
|
||||
# REST API. The format of the version is:
|
||||
# X.Y where:
|
||||
#
|
||||
# - X will only be changed if a significant backwards incompatible API
|
||||
# change is made which affects the API as whole. That is, something
|
||||
# that is only very very rarely incremented.
|
||||
#
|
||||
# - Y when you make any change to the API. Note that this includes
|
||||
# semantic changes which may not affect the input or output formats or
|
||||
# even originate in the API code layer. We are not distinguishing
|
||||
# between backwards compatible and backwards incompatible changes in
|
||||
# the versioning system. It must be made clear in the documentation as
|
||||
# to what is a backwards compatible change and what is a backwards
|
||||
# incompatible one.
|
||||
|
||||
#
|
||||
# You must update the API version history string below with a one or
|
||||
# two line description as well as update rest_api_version_history.rst
|
||||
REST_API_VERSION_HISTORY = """REST API Version History:
|
||||
|
||||
* 1.0 - Initial version.
|
||||
"""
|
||||
|
||||
# The minimum and maximum versions of the API supported
|
||||
# The default api version request is defined to be the
|
||||
# the minimum version of the API supported.
|
||||
# Note: This only applies for the v1 API once microversions
|
||||
# support is fully merged.
|
||||
_MIN_API_VERSION = "1.0"
|
||||
_MAX_API_VERSION = "1.0"
|
||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||
|
||||
|
||||
# NOTE: min and max versions declared as functions so we can
|
||||
# mock them for unittests. Do not use the constants directly anywhere
|
||||
# else.
|
||||
def min_api_version():
|
||||
return APIVersionRequest(_MIN_API_VERSION)
|
||||
|
||||
|
||||
def max_api_version():
|
||||
return APIVersionRequest(_MAX_API_VERSION)
|
||||
|
||||
|
||||
def is_supported(req, min_version=_MIN_API_VERSION,
|
||||
max_version=_MAX_API_VERSION):
|
||||
"""Check if API request version satisfies version restrictions.
|
||||
|
||||
:param req: request object
|
||||
:param min_version: minimal version of API needed for correct
|
||||
request processing
|
||||
:param max_version: maximum version of API needed for correct
|
||||
request processing
|
||||
|
||||
:returns True if request satisfies minimal and maximum API version
|
||||
requirements. False in other case.
|
||||
"""
|
||||
|
||||
return (APIVersionRequest(max_version) >= req.api_version_request >=
|
||||
APIVersionRequest(min_version))
|
||||
|
||||
|
||||
class APIVersionRequest(object):
|
||||
"""This class represents an API Version Request with convenience
|
||||
methods for manipulation and comparison of version
|
||||
numbers that we need to do to implement microversions.
|
||||
"""
|
||||
|
||||
def __init__(self, version_string=None):
|
||||
"""Create an API version request object.
|
||||
|
||||
:param version_string: String representation of APIVersionRequest.
|
||||
Correct format is 'X.Y', where 'X' and 'Y' are int values.
|
||||
None value should be used to create Null APIVersionRequest,
|
||||
which is equal to 0.0
|
||||
"""
|
||||
self.ver_major = 0
|
||||
self.ver_minor = 0
|
||||
|
||||
if version_string is not None:
|
||||
match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0)$",
|
||||
version_string)
|
||||
if match:
|
||||
self.ver_major = int(match.group(1))
|
||||
self.ver_minor = int(match.group(2))
|
||||
else:
|
||||
raise exception.InvalidAPIVersionString(version=version_string)
|
||||
|
||||
def __str__(self):
|
||||
"""Debug/Logging representation of object."""
|
||||
return ("API Version Request Major: %s, Minor: %s"
|
||||
% (self.ver_major, self.ver_minor))
|
||||
|
||||
def is_null(self):
|
||||
return self.ver_major == 0 and self.ver_minor == 0
|
||||
|
||||
def _format_type_error(self, other):
|
||||
return TypeError(_("'%(other)s' should be an instance of '%(cls)s'") %
|
||||
{"other": other, "cls": self.__class__})
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, APIVersionRequest):
|
||||
raise self._format_type_error(other)
|
||||
|
||||
return ((self.ver_major, self.ver_minor) <
|
||||
(other.ver_major, other.ver_minor))
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, APIVersionRequest):
|
||||
raise self._format_type_error(other)
|
||||
|
||||
return ((self.ver_major, self.ver_minor) ==
|
||||
(other.ver_major, other.ver_minor))
|
||||
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, APIVersionRequest):
|
||||
raise self._format_type_error(other)
|
||||
|
||||
return ((self.ver_major, self.ver_minor) >
|
||||
(other.ver_major, other.ver_minor))
|
||||
|
||||
def __le__(self, other):
|
||||
return self < other or self == other
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __ge__(self, other):
|
||||
return self > other or self == other
|
||||
|
||||
def matches(self, min_version, max_version):
|
||||
"""Returns whether the version object represents a version
|
||||
greater than or equal to the minimum version and less than
|
||||
or equal to the maximum version.
|
||||
|
||||
@param min_version: Minimum acceptable version.
|
||||
@param max_version: Maximum acceptable version.
|
||||
@returns: boolean
|
||||
|
||||
If min_version is null then there is no minimum limit.
|
||||
If max_version is null then there is no maximum limit.
|
||||
If self is null then raise ValueError
|
||||
"""
|
||||
|
||||
if self.is_null():
|
||||
raise ValueError
|
||||
if max_version.is_null() and min_version.is_null():
|
||||
return True
|
||||
elif max_version.is_null():
|
||||
return min_version <= self
|
||||
elif min_version.is_null():
|
||||
return self <= max_version
|
||||
else:
|
||||
return min_version <= self <= max_version
|
||||
|
||||
def get_string(self):
|
||||
"""Converts object to string representation which if used to create
|
||||
an APIVersionRequest object results in the same version request.
|
||||
"""
|
||||
if self.is_null():
|
||||
raise ValueError
|
||||
return "%s.%s" % (self.ver_major, self.ver_minor)
|
127
masakari/api/auth.py
Normal file
127
masakari/api/auth.py
Normal file
@ -0,0 +1,127 @@
|
||||
# Copyright (c) 2016 NTT DATA
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_middleware import request_id
|
||||
from oslo_serialization import jsonutils
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
import masakari.conf
|
||||
from masakari import context
|
||||
from masakari.i18n import _
|
||||
from masakari import wsgi
|
||||
|
||||
|
||||
CONF = masakari.conf.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _load_pipeline(loader, pipeline):
|
||||
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
|
||||
|
||||
|
||||
def pipeline_factory_v1(loader, global_conf, **local_conf):
|
||||
"""A paste pipeline replica that keys off of auth_strategy."""
|
||||
|
||||
return _load_pipeline(loader, local_conf[CONF.auth_strategy].split())
|
||||
|
||||
|
||||
class InjectContext(wsgi.Middleware):
|
||||
"""Add a 'masakari.context' to WSGI environ."""
|
||||
|
||||
def __init__(self, context, *args, **kwargs):
|
||||
self.context = context
|
||||
super(InjectContext, self).__init__(*args, **kwargs)
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
req.environ['masakari.context'] = self.context
|
||||
return self.application
|
||||
|
||||
|
||||
class MasakariKeystoneContext(wsgi.Middleware):
|
||||
"""Make a request context from keystone headers."""
|
||||
|
||||
@webob.dec.wsgify(RequestClass=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()
|
||||
|
||||
roles = self._get_roles(req)
|
||||
|
||||
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')
|
||||
user_name = req.headers.get('X_USER_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
|
||||
if CONF.use_forwarded_for:
|
||||
remote_address = req.headers.get('X-Forwarded-For', remote_address)
|
||||
|
||||
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(
|
||||
_('Invalid service catalog json.'))
|
||||
|
||||
# NOTE: This is a full auth plugin set by auth_token
|
||||
# middleware in newer versions.
|
||||
user_auth_plugin = req.environ.get('keystone.token_auth')
|
||||
|
||||
ctx = context.RequestContext(user_id,
|
||||
project_id,
|
||||
user_name=user_name,
|
||||
project_name=project_name,
|
||||
roles=roles,
|
||||
auth_token=auth_token,
|
||||
remote_address=remote_address,
|
||||
service_catalog=service_catalog,
|
||||
request_id=req_id,
|
||||
user_auth_plugin=user_auth_plugin)
|
||||
|
||||
req.environ['masakari.context'] = ctx
|
||||
return self.application
|
||||
|
||||
def _get_roles(self, req):
|
||||
"""Get the list of roles."""
|
||||
|
||||
roles = req.headers.get('X_ROLES', '')
|
||||
return [r.strip() for r in roles.split(',')]
|
285
masakari/api/openstack/__init__.py
Normal file
285
masakari/api/openstack/__init__.py
Normal file
@ -0,0 +1,285 @@
|
||||
# Copyright (c) 2016 NTT DATA
|
||||
#
|
||||
# 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
|
||||
import six
|
||||
import stevedore
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
from masakari.api.openstack import wsgi
|
||||
import masakari.conf
|
||||
from masakari.i18n import _LE
|
||||
from masakari.i18n import _LI
|
||||
from masakari.i18n import _LW
|
||||
from masakari.i18n import translate
|
||||
from masakari import utils
|
||||
from masakari import wsgi as base_wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = masakari.conf.CONF
|
||||
|
||||
|
||||
class FaultWrapper(base_wsgi.Middleware):
|
||||
"""Calls down the middleware stack, making exceptions into faults."""
|
||||
|
||||
_status_to_type = {}
|
||||
|
||||
@staticmethod
|
||||
def status_to_type(status):
|
||||
if not FaultWrapper._status_to_type:
|
||||
for clazz in utils.walk_class_hierarchy(webob.exc.HTTPError):
|
||||
FaultWrapper._status_to_type[clazz.code] = clazz
|
||||
return FaultWrapper._status_to_type.get(
|
||||
status, webob.exc.HTTPInternalServerError)()
|
||||
|
||||
def _error(self, inner, req):
|
||||
LOG.exception(_LE("Caught error: %s"), six.text_type(inner))
|
||||
|
||||
safe = getattr(inner, 'safe', False)
|
||||
headers = getattr(inner, 'headers', None)
|
||||
status = getattr(inner, 'code', 500)
|
||||
if status is None:
|
||||
status = 500
|
||||
|
||||
msg_dict = dict(url=req.url, status=status)
|
||||
LOG.info(_LI("%(url)s returned with HTTP %(status)d"), msg_dict)
|
||||
outer = self.status_to_type(status)
|
||||
if headers:
|
||||
outer.headers = headers
|
||||
|
||||
if safe:
|
||||
user_locale = req.best_match_language()
|
||||
inner_msg = translate(inner.message, user_locale)
|
||||
outer.explanation = '%s: %s' % (inner.__class__.__name__,
|
||||
inner_msg)
|
||||
|
||||
return wsgi.Fault(outer)
|
||||
|
||||
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
||||
def __call__(self, req):
|
||||
try:
|
||||
return req.get_response(self.application)
|
||||
except Exception as ex:
|
||||
return self._error(ex, req)
|
||||
|
||||
|
||||
class APIMapper(routes.Mapper):
|
||||
def routematch(self, url=None, environ=None):
|
||||
if url == "":
|
||||
result = self._match("", environ)
|
||||
return result[0], result[1]
|
||||
return routes.Mapper.routematch(self, url, environ)
|
||||
|
||||
def connect(self, *args, **kargs):
|
||||
kargs.setdefault('requirements', {})
|
||||
if not kargs['requirements'].get('format'):
|
||||
kargs['requirements']['format'] = 'json|xml'
|
||||
return routes.Mapper.connect(self, *args, **kargs)
|
||||
|
||||
|
||||
class ProjectMapper(APIMapper):
|
||||
def resource(self, member_name, collection_name, **kwargs):
|
||||
# NOTE(abhishekk): project_id parameter is only valid if its hex
|
||||
# or hex + dashes (note, integers are a subset of this). This
|
||||
# is required to hand our overlaping routes issues.
|
||||
project_id_regex = '[0-9a-f\-]+'
|
||||
if CONF.osapi_v1.project_id_regex:
|
||||
project_id_regex = CONF.osapi_v1.project_id_regex
|
||||
|
||||
project_id_token = '{project_id:%s}' % project_id_regex
|
||||
if 'parent_resource' not in kwargs:
|
||||
kwargs['path_prefix'] = '%s/' % project_id_token
|
||||
else:
|
||||
parent_resource = kwargs['parent_resource']
|
||||
p_collection = parent_resource['collection_name']
|
||||
p_member = parent_resource['member_name']
|
||||
kwargs['path_prefix'] = '%s/%s/:%s_id' % (
|
||||
project_id_token,
|
||||
p_collection,
|
||||
p_member)
|
||||
routes.Mapper.resource(
|
||||
self,
|
||||
member_name,
|
||||
collection_name,
|
||||
**kwargs)
|
||||
|
||||
# while we are in transition mode, create additional routes
|
||||
# for the resource that do not include project_id.
|
||||
if 'parent_resource' not in kwargs:
|
||||
del kwargs['path_prefix']
|
||||
else:
|
||||
parent_resource = kwargs['parent_resource']
|
||||
p_collection = parent_resource['collection_name']
|
||||
p_member = parent_resource['member_name']
|
||||
kwargs['path_prefix'] = '%s/:%s_id' % (p_collection,
|
||||
p_member)
|
||||
routes.Mapper.resource(self, member_name,
|
||||
collection_name,
|
||||
**kwargs)
|
||||
|
||||
|
||||
class PlainMapper(APIMapper):
|
||||
def resource(self, member_name, collection_name, **kwargs):
|
||||
if 'parent_resource' in kwargs:
|
||||
parent_resource = kwargs['parent_resource']
|
||||
p_collection = parent_resource['collection_name']
|
||||
p_member = parent_resource['member_name']
|
||||
kwargs['path_prefix'] = '%s/:%s_id' % (p_collection, p_member)
|
||||
routes.Mapper.resource(self, member_name,
|
||||
collection_name,
|
||||
**kwargs)
|
||||
|
||||
|
||||
class APIRouterV1(base_wsgi.Router):
|
||||
"""Routes requests on the OpenStack v1 API to the appropriate controller
|
||||
and method.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def factory(cls, global_config, **local_config):
|
||||
"""Simple paste factory
|
||||
|
||||
:class:`masakari.wsgi.Router` doesn't have one.
|
||||
"""
|
||||
return cls()
|
||||
|
||||
@staticmethod
|
||||
def api_extension_namespace():
|
||||
return 'masakari.api.v1.extensions'
|
||||
|
||||
def __init__(self, init_only=None):
|
||||
def _check_load_extension(ext):
|
||||
return self._register_extension(ext)
|
||||
|
||||
self.api_extension_manager = stevedore.enabled.EnabledExtensionManager(
|
||||
namespace=self.api_extension_namespace(),
|
||||
check_func=_check_load_extension,
|
||||
invoke_on_load=True,
|
||||
invoke_kwds={"extension_info": self.loaded_extension_info})
|
||||
|
||||
mapper = ProjectMapper()
|
||||
|
||||
self.resources = {}
|
||||
|
||||
if list(self.api_extension_manager):
|
||||
self._register_resources_check_inherits(mapper)
|
||||
self.api_extension_manager.map(self._register_controllers)
|
||||
|
||||
LOG.info(_LI("Loaded extensions: %s"),
|
||||
sorted(self.loaded_extension_info.get_extensions().keys()))
|
||||
super(APIRouterV1, self).__init__(mapper)
|
||||
|
||||
def _register_resources_list(self, ext_list, mapper):
|
||||
for ext in ext_list:
|
||||
self._register_resources(ext, mapper)
|
||||
|
||||
def _register_resources_check_inherits(self, mapper):
|
||||
ext_has_inherits = []
|
||||
ext_no_inherits = []
|
||||
|
||||
for ext in self.api_extension_manager:
|
||||
for resource in ext.obj.get_resources():
|
||||
if resource.inherits:
|
||||
ext_has_inherits.append(ext)
|
||||
break
|
||||
else:
|
||||
ext_no_inherits.append(ext)
|
||||
|
||||
self._register_resources_list(ext_no_inherits, mapper)
|
||||
self._register_resources_list(ext_has_inherits, mapper)
|
||||
|
||||
@property
|
||||
def loaded_extension_info(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _register_extension(self, ext):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _register_resources(self, ext, mapper):
|
||||
"""Register resources defined by the extensions
|
||||
|
||||
Extensions define what resources they want to add through a
|
||||
get_resources function
|
||||
"""
|
||||
|
||||
handler = ext.obj
|
||||
LOG.debug("Running _register_resources on %s", ext.obj)
|
||||
|
||||
for resource in handler.get_resources():
|
||||
LOG.debug('Extended resource: %s', resource.collection)
|
||||
|
||||
inherits = None
|
||||
if resource.inherits:
|
||||
inherits = self.resources.get(resource.inherits)
|
||||
if not resource.controller:
|
||||
resource.controller = inherits.controller
|
||||
wsgi_resource = wsgi.ResourceV1(resource.controller,
|
||||
inherits=inherits)
|
||||
self.resources[resource.collection] = wsgi_resource
|
||||
kargs = dict(
|
||||
controller=wsgi_resource,
|
||||
collection=resource.collection_actions,
|
||||
member=resource.member_actions)
|
||||
|
||||
if resource.parent:
|
||||
kargs['parent_resource'] = resource.parent
|
||||
|
||||
# non core-API plugins use the collection name as the
|
||||
# member name, but the core-API plugins use the
|
||||
# singular/plural convention for member/collection names
|
||||
if resource.member_name:
|
||||
member_name = resource.member_name
|
||||
else:
|
||||
member_name = resource.collection
|
||||
mapper.resource(member_name, resource.collection,
|
||||
**kargs)
|
||||
|
||||
if resource.custom_routes_fn:
|
||||
resource.custom_routes_fn(mapper, wsgi_resource)
|
||||
|
||||
def _register_controllers(self, ext):
|
||||
"""Register controllers defined by the extensions
|
||||
|
||||
Extensions define what resources they want to add through
|
||||
a get_controller_extensions function
|
||||
"""
|
||||
|
||||
handler = ext.obj
|
||||
LOG.debug("Running _register_controllers on %s", ext.obj)
|
||||
|
||||
for extension in handler.get_controller_extensions():
|
||||
ext_name = extension.extension.name
|
||||
collection = extension.collection
|
||||
controller = extension.controller
|
||||
|
||||
if collection not in self.resources:
|
||||
LOG.warning(_LW('Extension %(ext_name)s: Cannot extend '
|
||||
'resource %(collection)s: No such resource'),
|
||||
{'ext_name': ext_name, 'collection': collection})
|
||||
continue
|
||||
|
||||
LOG.debug('Extension %(ext_name)s extending resource: '
|
||||
'%(collection)s',
|
||||
{'ext_name': ext_name, 'collection': collection})
|
||||
|
||||
resource = self.resources[collection]
|
||||
resource.register_actions(controller)
|
||||
resource.register_extensions(controller)
|
164
masakari/api/openstack/common.py
Normal file
164
masakari/api/openstack/common.py
Normal file
@ -0,0 +1,164 @@
|
||||
# Copyright (c) 2016 NTT DATA
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
|
||||
from oslo_log import log as logging
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
import masakari.conf
|
||||
|
||||
CONF = masakari.conf.CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def remove_trailing_version_from_href(href):
|
||||
"""Removes the api version from the href.
|
||||
|
||||
Given: 'http://www.masakari.com/ha/v1.1'
|
||||
Returns: 'http://www.masakari.com/ha'
|
||||
|
||||
Given: 'http://www.masakari.com/v1.1'
|
||||
Returns: 'http://www.masakari.com'
|
||||
|
||||
"""
|
||||
parsed_url = urlparse.urlsplit(href)
|
||||
url_parts = parsed_url.path.rsplit('/', 1)
|
||||
|
||||
# NOTE: this should match vX.X or vX
|
||||
expression = re.compile(r'^v([0-9]+|[0-9]+\.[0-9]+)(/.*|$)')
|
||||
if not expression.match(url_parts.pop()):
|
||||
LOG.debug('href %s does not contain version', href)
|
||||
raise ValueError(_('href %s does not contain version') % href)
|
||||
|
||||
new_path = url_join(*url_parts)
|
||||
parsed_url = list(parsed_url)
|
||||
parsed_url[2] = new_path
|
||||
return urlparse.urlunsplit(parsed_url)
|
||||
|
||||
|
||||
def url_join(*parts):
|
||||
"""Convenience method for joining parts of a URL
|
||||
|
||||
Any leading and trailing '/' characters are removed, and the parts joined
|
||||
together with '/' as a separator. If last element of 'parts' is an empty
|
||||
string, the returned URL will have a trailing slash.
|
||||
"""
|
||||
parts = parts or [""]
|
||||
clean_parts = [part.strip("/") for part in parts if part]
|
||||
if not parts[-1]:
|
||||
# Empty last element should add a trailing slash
|
||||
clean_parts.append("")
|
||||
return "/".join(clean_parts)
|
||||
|
||||
|
||||
class ViewBuilder(object):
|
||||
"""Model API responses as dictionaries."""
|
||||
|
||||
def _get_project_id(self, request):
|
||||
"""Get project id from request url if present or empty string
|
||||
otherwise
|
||||
"""
|
||||
project_id = request.environ["masakari.context"].project_id
|
||||
if project_id in request.url:
|
||||
return project_id
|
||||
return ''
|
||||
|
||||
def _get_links(self, request, identifier, collection_name):
|
||||
return [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": self._get_href_link(request, identifier,
|
||||
collection_name),
|
||||
},
|
||||
{
|
||||
"rel": "bookmark",
|
||||
"href": self._get_bookmark_link(request,
|
||||
identifier,
|
||||
collection_name),
|
||||
}
|
||||
]
|
||||
|
||||
def _get_next_link(self, request, identifier, collection_name):
|
||||
"""Return href string with proper limit and marker params."""
|
||||
params = request.params.copy()
|
||||
params["marker"] = identifier
|
||||
prefix = self._update_masakari_link_prefix(request.application_url)
|
||||
url = url_join(prefix,
|
||||
self._get_project_id(request),
|
||||
collection_name)
|
||||
return "%s?%s" % (url, urlparse.urlencode(params))
|
||||
|
||||
def _get_href_link(self, request, identifier, collection_name):
|
||||
"""Return an href string pointing to this object."""
|
||||
prefix = self._update_masakari_link_prefix(request.application_url)
|
||||
return url_join(prefix,
|
||||
self._get_project_id(request),
|
||||
collection_name,
|
||||
str(identifier))
|
||||
|
||||
def _get_bookmark_link(self, request, identifier, collection_name):
|
||||
"""Create a URL that refers to a specific resource."""
|
||||
base_url = remove_trailing_version_from_href(request.application_url)
|
||||
base_url = self._update_masakari_link_prefix(base_url)
|
||||
return url_join(base_url,
|
||||
self._get_project_id(request),
|
||||
collection_name,
|
||||
str(identifier))
|
||||
|
||||
def _get_collection_links(self,
|
||||
request,
|
||||
items,
|
||||
collection_name,
|
||||
id_key="uuid"):
|
||||
"""Retrieve 'next' link, if applicable. This is included if:
|
||||
1) 'limit' param is specified and equals the number of items.
|
||||
2) 'limit' param is specified but it exceeds CONF.osapi_max_limit,
|
||||
in this case the number of items is CONF.osapi_max_limit.
|
||||
3) 'limit' param is NOT specified but the number of items is
|
||||
CONF.osapi_max_limit.
|
||||
"""
|
||||
links = []
|
||||
max_items = min(
|
||||
int(request.params.get("limit", CONF.osapi_max_limit)),
|
||||
CONF.osapi_max_limit)
|
||||
if max_items and max_items == len(items):
|
||||
last_item = items[-1]
|
||||
if id_key in last_item:
|
||||
last_item_id = last_item[id_key]
|
||||
elif 'id' in last_item:
|
||||
last_item_id = last_item["id"]
|
||||
else:
|
||||
last_item_id = last_item["flavorid"]
|
||||
links.append({
|
||||
"rel": "next",
|
||||
"href": self._get_next_link(request,
|
||||
last_item_id,
|
||||
collection_name),
|
||||
})
|
||||
return links
|
||||
|
||||
def _update_link_prefix(self, orig_url, prefix):
|
||||
if not prefix:
|
||||
return orig_url
|
||||
url_parts = list(urlparse.urlsplit(orig_url))
|
||||
prefix_parts = list(urlparse.urlsplit(prefix))
|
||||
url_parts[0:2] = prefix_parts[0:2]
|
||||
url_parts[2] = prefix_parts[2] + url_parts[2]
|
||||
return urlparse.urlunsplit(url_parts).rstrip('/')
|
||||
|
||||
def _update_masakari_link_prefix(self, orig_url):
|
||||
return self._update_link_prefix(orig_url,
|
||||
CONF.osapi_masakari_link_prefix)
|
458
masakari/api/openstack/extensions.py
Normal file
458
masakari/api/openstack/extensions.py
Normal file
@ -0,0 +1,458 @@
|
||||
# Copyright (c) 2016 NTT DATA
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import abc
|
||||
import functools
|
||||
import os
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import importutils
|
||||
import six
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
import masakari.api.openstack
|
||||
from masakari.api.openstack import wsgi
|
||||
from masakari import exception
|
||||
from masakari.i18n import _
|
||||
from masakari.i18n import _LE
|
||||
from masakari.i18n import _LW
|
||||
import masakari.policy
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExtensionDescriptor(object):
|
||||
"""Base class that defines the contract for extensions.
|
||||
|
||||
Note that you don't have to derive from this class to have a valid
|
||||
extension; it is purely a convenience.
|
||||
|
||||
"""
|
||||
|
||||
# The name of the extension, e.g., 'Fox In Socks'
|
||||
name = None
|
||||
|
||||
# The alias for the extension, e.g., 'FOXNSOX'
|
||||
alias = None
|
||||
|
||||
# Description comes from the docstring for the class
|
||||
|
||||
# The timestamp when the extension was last updated, e.g.,
|
||||
# '2011-01-22T19:25:27Z'
|
||||
updated = None
|
||||
|
||||
def __init__(self, ext_mgr):
|
||||
"""Register extension with the extension manager."""
|
||||
|
||||
ext_mgr.register(self)
|
||||
self.ext_mgr = ext_mgr
|
||||
|
||||
def get_resources(self):
|
||||
"""List of extensions.ResourceExtension extension objects.
|
||||
|
||||
Resources define new nouns, and are accessible through URLs.
|
||||
|
||||
"""
|
||||
resources = []
|
||||
return resources
|
||||
|
||||
def get_controller_extensions(self):
|
||||
"""List of extensions.ControllerExtension extension objects.
|
||||
|
||||
Controller extensions are used to extend existing controllers.
|
||||
"""
|
||||
controller_exts = []
|
||||
return controller_exts
|
||||
|
||||
def __repr__(self):
|
||||
return "<Extension: name=%s, alias=%s, updated=%s>" % (
|
||||
self.name, self.alias, self.updated)
|
||||
|
||||
def is_valid(self):
|
||||
"""Validate required fields for extensions.
|
||||
|
||||
Raises an attribute error if the attr is not defined
|
||||
"""
|
||||
for attr in ('name', 'alias', 'updated', 'namespace'):
|
||||
if getattr(self, attr) is None:
|
||||
raise AttributeError("%s is None, needs to be defined" % attr)
|
||||
return True
|
||||
|
||||
|
||||
class ExtensionsController(wsgi.Resource):
|
||||
|
||||
def __init__(self, extension_manager):
|
||||
self.extension_manager = extension_manager
|
||||
super(ExtensionsController, self).__init__(None)
|
||||
|
||||
def _translate(self, ext):
|
||||
ext_data = {}
|
||||
ext_data['name'] = ext.name
|
||||
ext_data['alias'] = ext.alias
|
||||
ext_data['description'] = ext.__doc__
|
||||
ext_data['namespace'] = ext.namespace
|
||||
ext_data['updated'] = ext.updated
|
||||
ext_data['links'] = []
|
||||
return ext_data
|
||||
|
||||
def index(self, req):
|
||||
extensions = []
|
||||
for ext in self.extension_manager.sorted_extensions():
|
||||
extensions.append(self._translate(ext))
|
||||
return dict(extensions=extensions)
|
||||
|
||||
def show(self, req, id):
|
||||
try:
|
||||
ext = self.extension_manager.extensions[id]
|
||||
except KeyError:
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
return dict(extension=self._translate(ext))
|
||||
|
||||
def delete(self, req, id):
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
def create(self, req, body):
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
|
||||
class ExtensionManager(object):
|
||||
"""Load extensions from the configured extension path."""
|
||||
|
||||
def sorted_extensions(self):
|
||||
if self.sorted_ext_list is None:
|
||||
self.sorted_ext_list = sorted(six.iteritems(self.extensions))
|
||||
|
||||
for _alias, ext in self.sorted_ext_list:
|
||||
yield ext
|
||||
|
||||
def is_loaded(self, alias):
|
||||
return alias in self.extensions
|
||||
|
||||
def register(self, ext):
|
||||
# Do nothing if the extension doesn't check out
|
||||
if not self._check_extension(ext):
|
||||
return
|
||||
|
||||
alias = ext.alias
|
||||
if alias in self.extensions:
|
||||
raise exception.MasakariException(
|
||||
"Found duplicate extension: %s" % alias)
|
||||
self.extensions[alias] = ext
|
||||
self.sorted_ext_list = None
|
||||
|
||||
def get_resources(self):
|
||||
"""Returns a list of ResourceExtension objects."""
|
||||
|
||||
resources = []
|
||||
resources.append(ResourceExtension('extensions',
|
||||
ExtensionsController(self)))
|
||||
for ext in self.sorted_extensions():
|
||||
try:
|
||||
resources.extend(ext.get_resources())
|
||||
except AttributeError:
|
||||
pass
|
||||
return resources
|
||||
|
||||
def get_controller_extensions(self):
|
||||
"""Returns a list of ControllerExtension objects."""
|
||||
controller_exts = []
|
||||
for ext in self.sorted_extensions():
|
||||
try:
|
||||
get_ext_method = ext.get_controller_extensions
|
||||
except AttributeError:
|
||||
continue
|
||||
controller_exts.extend(get_ext_method())
|
||||
return controller_exts
|
||||
|
||||
def _check_extension(self, extension):
|
||||
"""Checks for required methods in extension objects."""
|
||||
try:
|
||||
extension.is_valid()
|
||||
except AttributeError:
|
||||
LOG.exception(_LE("Exception loading extension"))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def load_extension(self, ext_factory):
|
||||
"""Execute an extension factory.
|
||||
|
||||
Loads an extension. The 'ext_factory' is the name of a
|
||||
callable that will be imported and called with one
|
||||
argument--the extension manager. The factory callable is
|
||||
expected to call the register() method at least once.
|
||||
"""
|
||||
|
||||
LOG.debug("Loading extension %s", ext_factory)
|
||||
|
||||
if isinstance(ext_factory, six.string_types):
|
||||
# Load the factory
|
||||
factory = importutils.import_class(ext_factory)
|
||||
else:
|
||||
factory = ext_factory
|
||||
|
||||
# Call it
|
||||
LOG.debug("Calling extension factory %s", ext_factory)
|
||||
factory(self)
|
||||
|
||||
def _load_extensions(self):
|
||||
"""Load extensions specified on the command line."""
|
||||
|
||||
extensions = list(self.cls_list)
|
||||
|
||||
for ext_factory in extensions:
|
||||
try:
|
||||
self.load_extension(ext_factory)
|
||||
except Exception as exc:
|
||||
LOG.warning(_LW('Failed to load extension %(ext_factory)s: '
|
||||
'%(exc)s'),
|
||||
{'ext_factory': ext_factory, 'exc': exc})
|
||||
|
||||
|
||||
class ControllerExtension(object):
|
||||
"""Extend core controllers of masakari OpenStack API.
|
||||
|
||||
Provide a way to extend existing masakari OpenStack API core
|
||||
controllers.
|
||||
"""
|
||||
|
||||
def __init__(self, extension, collection, controller):
|
||||
self.extension = extension
|
||||
self.collection = collection
|
||||
self.controller = controller
|
||||
|
||||
|
||||
class ResourceExtension(object):
|
||||
"""Add top level resources to the OpenStack API in masakari."""
|
||||
|
||||
def __init__(self, collection, controller=None, parent=None,
|
||||
collection_actions=None, member_actions=None,
|
||||
custom_routes_fn=None, inherits=None, member_name=None):
|
||||
if not collection_actions:
|
||||
collection_actions = {}
|
||||
if not member_actions:
|
||||
member_actions = {}
|
||||
self.collection = collection
|
||||
self.controller = controller
|
||||
self.parent = parent
|
||||
self.collection_actions = collection_actions
|
||||
self.member_actions = member_actions
|
||||
self.custom_routes_fn = custom_routes_fn
|
||||
self.inherits = inherits
|
||||
self.member_name = member_name
|
||||
|
||||
|
||||
def load_standard_extensions(ext_mgr, logger, path, package, ext_list=None):
|
||||
"""Registers all standard API extensions."""
|
||||
|
||||
# Walk through all the modules in our directory...
|
||||
our_dir = path[0]
|
||||
for dirpath, dirnames, filenames in os.walk(our_dir):
|
||||
# Compute the relative package name from the dirpath
|
||||
relpath = os.path.relpath(dirpath, our_dir)
|
||||
if relpath == '.':
|
||||
relpkg = ''
|
||||
else:
|
||||
relpkg = '.%s' % '.'.join(relpath.split(os.sep))
|
||||
|
||||
# Now, consider each file in turn, only considering .py files
|
||||
for fname in filenames:
|
||||
root, ext = os.path.splitext(fname)
|
||||
|
||||
# Skip __init__ and anything that's not .py
|
||||
if ext != '.py' or root == '__init__':
|
||||
continue
|
||||
|
||||
# Try loading it
|
||||
classname = "%s%s" % (root[0].upper(), root[1:])
|
||||
classpath = ("%s%s.%s.%s" %
|
||||
(package, relpkg, root, classname))
|
||||
|
||||
if ext_list is not None and classname not in ext_list:
|
||||
logger.debug("Skipping extension: %s" % classpath)
|
||||
continue
|
||||
|
||||
try:
|
||||
ext_mgr.load_extension(classpath)
|
||||
except Exception as exc:
|
||||
logger.warn(_LW('Failed to load extension %(classpath)s: '
|
||||
'%(exc)s'),
|
||||
{'classpath': classpath, 'exc': exc})
|
||||
|
||||
# Now, let's consider any subdirectories we may have...
|
||||
subdirs = []
|
||||
for dname in dirnames:
|
||||
# Skip it if it does not have __init__.py
|
||||
if not os.path.exists(os.path.join(dirpath, dname, '__init__.py')):
|
||||
continue
|
||||
|
||||
# If it has extension(), delegate...
|
||||
ext_name = "%s%s.%s.extension" % (package, relpkg, dname)
|
||||
try:
|
||||
ext = importutils.import_class(ext_name)
|
||||
except ImportError:
|
||||
# extension() doesn't exist on it, so we'll explore
|
||||
# the directory for ourselves
|
||||
subdirs.append(dname)
|
||||
else:
|
||||
try:
|
||||
ext(ext_mgr)
|
||||
except Exception as exc:
|
||||
logger.warn(_LW('Failed to load extension %(ext_name)s:'
|
||||
'%(exc)s'),
|
||||
{'ext_name': ext_name, 'exc': exc})
|
||||
|
||||
# Update the list of directories we'll explore...
|
||||
# using os.walk 'the caller can modify the dirnames list in-place,
|
||||
# and walk() will only recurse into the subdirectories whose names
|
||||
# remain in dirnames'
|
||||
# https://docs.python.org/2/library/os.html#os.walk
|
||||
dirnames[:] = subdirs
|
||||
|
||||
|
||||
# This will be deprecated after policy cleanup finished
|
||||
def core_authorizer(api_name, extension_name):
|
||||
def authorize(context, target=None, action=None):
|
||||
if target is None:
|
||||
target = {'project_id': context.project_id,
|
||||
'user_id': context.user_id}
|
||||
if action is None:
|
||||
act = '%s:%s' % (api_name, extension_name)
|
||||
else:
|
||||
act = '%s:%s:%s' % (api_name, extension_name, action)
|
||||
masakari.policy.enforce(context, act, target)
|
||||
return authorize
|
||||
|
||||
|
||||
def _soft_authorizer(hard_authorizer, api_name, extension_name):
|
||||
hard_authorize = hard_authorizer(api_name, extension_name)
|
||||
|
||||
def authorize(context, target=None, action=None):
|
||||
try:
|
||||
hard_authorize(context, target=target, action=action)
|
||||
return True
|
||||
except exception.Forbidden:
|
||||
return False
|
||||
return authorize
|
||||
|
||||
|
||||
# This will be deprecated after policy cleanup finished
|
||||
def soft_core_authorizer(api_name, extension_name):
|
||||
return _soft_authorizer(core_authorizer, api_name, extension_name)
|
||||
|
||||
|
||||
def os_masakari_authorizer(extension_name):
|
||||
return core_authorizer('os_masakari_api', extension_name)
|
||||
|
||||
|
||||
def os_masakari_soft_authorizer(extension_name):
|
||||
return soft_core_authorizer('os_masakari_api', extension_name)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class V1APIExtensionBase(object):
|
||||
"""Abstract base class for all v1 API extensions.
|
||||
|
||||
All v1 API extensions must derive from this class and implement
|
||||
the abstract methods get_resources and get_controller_extensions
|
||||
even if they just return an empty list. The extensions must also
|
||||
define the abstract properties.
|
||||
"""
|
||||
|
||||
def __init__(self, extension_info):
|
||||
self.extension_info = extension_info
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_resources(self):
|
||||
"""Return a list of resources extensions.
|
||||
|
||||
The extensions should return a list of ResourceExtension
|
||||
objects. This list may be empty.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_controller_extensions(self):
|
||||
"""Return a list of controller extensions.
|
||||
|
||||
The extensions should return a list of ControllerExtension
|
||||
objects. This list may be empty.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abc.abstractproperty
|
||||
def name(self):
|
||||
"""Name of the extension."""
|
||||
pass
|
||||
|
||||
@abc.abstractproperty
|
||||
def alias(self):
|
||||
"""Alias for the extension."""
|
||||
pass
|
||||
|
||||
@abc.abstractproperty
|
||||
def version(self):
|
||||
"""Version of the extension."""
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return "<Extension: name=%s, alias=%s, version=%s>" % (
|
||||
self.name, self.alias, self.version)
|
||||
|
||||
def is_valid(self):
|
||||
"""Validate required fields for extensions.
|
||||
|
||||
Raises an attribute error if the attr is not defined
|
||||
"""
|
||||
for attr in ('name', 'alias', 'version'):
|
||||
if getattr(self, attr) is None:
|
||||
raise AttributeError("%s is None, needs to be defined" % attr)
|
||||
return True
|
||||
|
||||
|
||||
def expected_errors(errors):
|
||||
"""Decorator for v1 API methods which specifies expected exceptions.
|
||||
|
||||
Specify which exceptions may occur when an API method is called. If an
|
||||
unexpected exception occurs then return a 500 instead and ask the user
|
||||
of the API to file a bug report.
|
||||
"""
|
||||
def decorator(f):
|
||||
@functools.wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except Exception as exc:
|
||||
if isinstance(exc, webob.exc.WSGIHTTPException):
|
||||
if isinstance(errors, int):
|
||||
t_errors = (errors,)
|
||||
else:
|
||||
t_errors = errors
|
||||
if exc.code in t_errors:
|
||||
raise
|
||||
elif isinstance(exc, exception.Forbidden):
|
||||
raise
|
||||
elif isinstance(exc, exception.ValidationError):
|
||||
raise
|
||||
LOG.exception(_LE("Unexpected exception in API method"))
|
||||
msg = _('Unexpected API Error. Please report this at '
|
||||
'http://bugs.launchpad.net/masakari/ and attach the '
|
||||
'Nova API log if possible.\n%s') % type(exc)
|
||||
raise webob.exc.HTTPInternalServerError(explanation=msg)
|
||||
|
||||
return wrapped
|
||||
|
||||
return decorator
|
41
masakari/api/openstack/ha/__init__.py
Normal file
41
masakari/api/openstack/ha/__init__.py
Normal file
@ -0,0 +1,41 @@
|
||||
# Copyright (c) 2016 NTT Data
|
||||
# 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 Compute API.
|
||||
"""
|
||||
|
||||
import masakari.api.openstack
|
||||
from masakari.api.openstack.ha import extension_info
|
||||
import masakari.conf
|
||||
|
||||
|
||||
CONF = masakari.conf.CONF
|
||||
|
||||
|
||||
class APIRouterV1(masakari.api.openstack.APIRouterV1):
|
||||
"""Routes requests on the OpenStack API to the appropriate controller
|
||||
and method.
|
||||
"""
|
||||
def __init__(self, init_only=None):
|
||||
self._loaded_extension_info = extension_info.LoadedExtensionInfo()
|
||||
super(APIRouterV1, self).__init__(init_only)
|
||||
|
||||
def _register_extension(self, ext):
|
||||
return self.loaded_extension_info.register_extension(ext.obj)
|
||||
|
||||
@property
|
||||
def loaded_extension_info(self):
|
||||
return self._loaded_extension_info
|
144
masakari/api/openstack/ha/extension_info.py
Normal file
144
masakari/api/openstack/ha/extension_info.py
Normal file
@ -0,0 +1,144 @@
|
||||
# Copyright (c) 2016 NTT DATA
|
||||
#
|
||||
# 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_log import log as logging
|
||||
import six
|
||||
from six.moves import http_client
|
||||
import webob.exc
|
||||
|
||||
from masakari.api.openstack import extensions
|
||||
from masakari.api.openstack import wsgi
|
||||
from masakari import exception
|
||||
from masakari.i18n import _LE
|
||||
|
||||
ALIAS = 'extensions'
|
||||
LOG = logging.getLogger(__name__)
|
||||
authorize = extensions.os_masakari_authorizer(ALIAS)
|
||||
|
||||
|
||||
class FakeExtension(object):
|
||||
def __init__(self, name, alias, description=""):
|
||||
self.name = name
|
||||
self.alias = alias
|
||||
self.__doc__ = description
|
||||
self.version = -1
|
||||
|
||||
|
||||
class ExtensionInfoController(wsgi.Controller):
|
||||
|
||||
def __init__(self, extension_info):
|
||||
self.extension_info = extension_info
|
||||
|
||||
def _translate(self, ext):
|
||||
ext_data = {}
|
||||
ext_data["name"] = ext.name
|
||||
ext_data["alias"] = ext.alias
|
||||
ext_data["description"] = ext.__doc__
|
||||
ext_data["namespace"] = ""
|
||||
ext_data["updated"] = ""
|
||||
ext_data["links"] = []
|
||||
return ext_data
|
||||
|
||||
def _create_fake_ext(self, name, alias, description=""):
|
||||
return FakeExtension(name, alias, description)
|
||||
|
||||
def _get_extensions(self, context):
|
||||
"""Filter extensions list based on policy."""
|
||||
|
||||
discoverable_extensions = dict()
|
||||
|
||||
for alias, ext in six.iteritems(self.extension_info.get_extensions()):
|
||||
authorize = extensions.os_masakari_soft_authorizer(alias)
|
||||
if authorize(context, action='discoverable'):
|
||||
discoverable_extensions[alias] = ext
|
||||
else:
|
||||
LOG.debug("Filter out extension %s from discover list",
|
||||
alias)
|
||||
|
||||
return discoverable_extensions
|
||||
|
||||
@extensions.expected_errors(())
|
||||
def index(self, req):
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
discoverable_extensions = self._get_extensions(context)
|
||||
sorted_ext_list = sorted(
|
||||
six.iteritems(discoverable_extensions))
|
||||
|
||||
extensions = []
|
||||
for _alias, ext in sorted_ext_list:
|
||||
extensions.append(self._translate(ext))
|
||||
|
||||
return dict(extensions=extensions)
|
||||
|
||||
@extensions.expected_errors(http_client.NOT_FOUND)
|
||||
def show(self, req, id):
|
||||
context = req.environ['masakari.context']
|
||||
authorize(context)
|
||||
try:
|
||||
ext = self._get_extensions(context)[id]
|
||||
except KeyError:
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
return dict(extension=self._translate(ext))
|
||||
|
||||
|
||||
class ExtensionInfo(extensions.V1APIExtensionBase):
|
||||
"""Extension information."""
|
||||
|
||||
name = "Extensions"
|
||||
alias = ALIAS
|
||||
version = 1
|
||||
|
||||
def get_resources(self):
|
||||
resources = [
|
||||
extensions.ResourceExtension(
|
||||
ALIAS, ExtensionInfoController(self.extension_info),
|
||||
member_name='extension')]
|
||||
return resources
|
||||
|
||||
def get_controller_extensions(self):
|
||||
return []
|
||||
|
||||
|
||||
class LoadedExtensionInfo(object):
|
||||
"""Keep track of all loaded API extensions."""
|
||||
|
||||
def __init__(self):
|
||||
self.extensions = {}
|
||||
|
||||
def register_extension(self, ext):
|
||||
if not self._check_extension(ext):
|
||||
return False
|
||||
|
||||
alias = ext.alias
|
||||
|
||||
if alias in self.extensions:
|
||||
raise exception.MasakariException(
|
||||
"Found duplicate extension: %s" % alias)
|
||||
self.extensions[alias] = ext
|
||||
return True
|
||||
|
||||
def _check_extension(self, extension):
|
||||
"""Checks for required methods in extension objects."""
|
||||
try:
|
||||
extension.is_valid()
|
||||
except AttributeError:
|
||||
LOG.exception(_LE("Exception loading extension"))
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def get_extensions(self):
|
||||
return self.extensions
|
78
masakari/api/openstack/ha/versions.py
Normal file
78
masakari/api/openstack/ha/versions.py
Normal file
@ -0,0 +1,78 @@
|
||||
# Copyright (c) 2016 NTT DATA
|
||||
#
|
||||
# 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
|
||||
from six.moves import http_client
|
||||
|
||||
from masakari.api.openstack.ha.views import versions as views_versions
|
||||
from masakari.api.openstack import wsgi
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
LINKS = {
|
||||
'v1.0': {
|
||||
'html': 'http://docs.openstack.org/'
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
VERSIONS = {
|
||||
"v1.0": {
|
||||
"id": "v1.0",
|
||||
"status": "CURRENT",
|
||||
"version": "1.0",
|
||||
"min_version": "1.0",
|
||||
"updated": "2016-07-01T11:33:21Z",
|
||||
"links": [
|
||||
{
|
||||
"rel": "describedby",
|
||||
"type": "text/html",
|
||||
"href": LINKS['v1.0']['html'],
|
||||
},
|
||||
],
|
||||
"media-types": [
|
||||
{
|
||||
"base": "application/json",
|
||||
"type": "application/vnd.openstack.masakari+json;version=1",
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Versions(wsgi.Resource):
|
||||
def __init__(self):
|
||||
super(Versions, self).__init__(None)
|
||||
|
||||
def index(self, req, body=None):
|
||||
"""Return all versions."""
|
||||
builder = views_versions.get_view_builder(req)
|
||||
return builder.build_versions(VERSIONS)
|
||||
|
||||
@wsgi.response(http_client.MULTIPLE_CHOICES)
|
||||
def multi(self, req, body=None):
|
||||
"""Return multiple choices."""
|
||||
builder = views_versions.get_view_builder(req)
|
||||
return builder.build_choices(VERSIONS, req)
|
||||
|
||||
def get_action_args(self, request_environment):
|
||||
"""Parse dictionary created by routes library."""
|
||||
args = {}
|
||||
if request_environment['PATH_INFO'] == '/':
|
||||
args['action'] = 'index'
|
||||
else:
|
||||
args['action'] = 'multi'
|
||||
|
||||
return args
|
58
masakari/api/openstack/ha/versionsV1.py
Normal file
58
masakari/api/openstack/ha/versionsV1.py
Normal file
@ -0,0 +1,58 @@
|
||||
# Copyright (c) 2016 NTT Data
|
||||
# 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.
|
||||
|
||||
from six.moves import http_client
|
||||
import webob.exc
|
||||
|
||||
from masakari.api.openstack import extensions
|
||||
from masakari.api.openstack.ha import versions
|
||||
from masakari.api.openstack.ha.views import versions as views_versions
|
||||
from masakari.api.openstack import wsgi
|
||||
|
||||
|
||||
ALIAS = "versions"
|
||||
|
||||
|
||||
class VersionsController(wsgi.Controller):
|
||||
@extensions.expected_errors(http_client.NOT_FOUND)
|
||||
def show(self, req, id='v1'):
|
||||
builder = views_versions.get_view_builder(req)
|
||||
if id not in versions.VERSIONS:
|
||||
raise webob.exc.HTTPNotFound()
|
||||
|
||||
return builder.build_version(versions.VERSIONS[id])
|
||||
|
||||
|
||||
class Versions(extensions.V1APIExtensionBase):
|
||||
"""API Version information."""
|
||||
|
||||
name = "Versions"
|
||||
alias = ALIAS
|
||||
version = 1
|
||||
|
||||
def get_resources(self):
|
||||
resources = [
|
||||
extensions.ResourceExtension(ALIAS, VersionsController(),
|
||||
custom_routes_fn=self.version_map)]
|
||||
return resources
|
||||
|
||||
def get_controller_extensions(self):
|
||||
return []
|
||||
|
||||
def version_map(self, mapper, wsgi_resource):
|
||||
mapper.connect("versions", "/",
|
||||
controller=wsgi_resource,
|
||||
action='show', conditions={"method": ['GET']})
|
||||
mapper.redirect("", "/")
|
0
masakari/api/openstack/ha/views/__init__.py
Normal file
0
masakari/api/openstack/ha/views/__init__.py
Normal file
93
masakari/api/openstack/ha/views/versions.py
Normal file
93
masakari/api/openstack/ha/views/versions.py
Normal file
@ -0,0 +1,93 @@
|
||||
# Copyright (c) 2016 NTT Data
|
||||
# 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 copy
|
||||
|
||||
from masakari.api.openstack import common
|
||||
|
||||
|
||||
def get_view_builder(req):
|
||||
base_url = req.application_url
|
||||
return ViewBuilder(base_url)
|
||||
|
||||
|
||||
class ViewBuilder(common.ViewBuilder):
|
||||
|
||||
def __init__(self, base_url):
|
||||
""":param base_url: url of the root wsgi application."""
|
||||
self.prefix = self._update_masakari_link_prefix(base_url)
|
||||
self.base_url = base_url
|
||||
|
||||
def build_choices(self, VERSIONS, req):
|
||||
version_objs = []
|
||||
for version in sorted(VERSIONS):
|
||||
version = VERSIONS[version]
|
||||
version_objs.append({
|
||||
"id": version['id'],
|
||||
"status": version['status'],
|
||||
"links": [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": self.generate_href(version['id'], req.path),
|
||||
},
|
||||
],
|
||||
"media-types": version['media-types'],
|
||||
})
|
||||
|
||||
return dict(choices=version_objs)
|
||||
|
||||
def build_versions(self, versions):
|
||||
version_objs = []
|
||||
for version in sorted(versions.keys()):
|
||||
version = versions[version]
|
||||
version_objs.append({
|
||||
"id": version['id'],
|
||||
"status": version['status'],
|
||||
"version": version['version'],
|
||||
"min_version": version['min_version'],
|
||||
"updated": version['updated'],
|
||||
"links": self._build_links(version),
|
||||
})
|
||||
|
||||
return dict(versions=version_objs)
|
||||
|
||||
def build_version(self, version):
|
||||
reval = copy.deepcopy(version)
|
||||
reval['links'].insert(0, {
|
||||
"rel": "self",
|
||||
"href": self.prefix.rstrip('/') + '/',
|
||||
})
|
||||
return dict(version=reval)
|
||||
|
||||
def _build_links(self, version_data):
|
||||
"""Generate a container of links that refer to the provided version."""
|
||||
href = self.generate_href(version_data['id'])
|
||||
|
||||
links = [
|
||||
{
|
||||
"rel": "self",
|
||||
"href": href,
|
||||
},
|
||||
]
|
||||
|
||||
return links
|
||||
|
||||
def generate_href(self, version, path=None):
|
||||
"""Create an url that refers to a specific version_number."""
|
||||
if version.find('v1') == 0:
|
||||
version_number = 'v1'
|
||||
|
||||
path = path or ''
|
||||
return common.url_join(self.prefix, version_number, path)
|
1061
masakari/api/openstack/wsgi.py
Normal file
1061
masakari/api/openstack/wsgi.py
Normal file
File diff suppressed because it is too large
Load Diff
292
masakari/api/urlmap.py
Normal file
292
masakari/api/urlmap.py
Normal file
@ -0,0 +1,292 @@
|
||||
# Copyright (c) 2016 NTT DATA
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import re
|
||||
|
||||
from oslo_log import log as logging
|
||||
import paste.urlmap
|
||||
import six
|
||||
|
||||
if six.PY2:
|
||||
import urllib2
|
||||
else:
|
||||
from urllib import request as urllib2
|
||||
|
||||
from masakari.api.openstack import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_quoted_string_re = r'"[^"\\]*(?:\\.[^"\\]*)*"'
|
||||
_option_header_piece_re = re.compile(
|
||||
r';\s*([^\s;=]+|%s)\s*'
|
||||
r'(?:=\s*([^;]+|%s))?\s*' % (_quoted_string_re, _quoted_string_re))
|
||||
|
||||
|
||||
def unquote_header_value(value):
|
||||
"""Unquotes a header value.
|
||||
This does not use the real unquoting but what browsers are actually
|
||||
using for quoting.
|
||||
|
||||
:param value: the header value to unquote.
|
||||
"""
|
||||
if value and value[0] == value[-1] == '"':
|
||||
# this is not the real unquoting, but fixing this so that the
|
||||
# RFC is met will result in bugs with internet explorer and
|
||||
# probably some other browsers as well. IE for example is
|
||||
# uploading files with "C:\foo\bar.txt" as filename
|
||||
value = value[1:-1]
|
||||
return value
|
||||
|
||||
|
||||
def parse_list_header(value):
|
||||
"""Parse lists as described by RFC 2068 Section 2.
|
||||
|
||||
In particular, parse comma-separated lists where the elements of
|
||||
the list may include quoted-strings. A quoted-string could
|
||||
contain a comma. A non-quoted string could have quotes in the
|
||||
middle. Quotes are removed automatically after parsing.
|
||||
|
||||
The return value is a standard :class:`list`:
|
||||
|
||||
>>> parse_list_header('token, "quoted value"')
|
||||
['token', 'quoted value']
|
||||
|
||||
:param value: a string with a list header.
|
||||
:return: :class:`list`
|
||||
"""
|
||||
result = []
|
||||
for item in urllib2.parse_http_list(value):
|
||||
if item[:1] == item[-1:] == '"':
|
||||
item = unquote_header_value(item[1:-1])
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
def parse_options_header(value):
|
||||
"""Parse a ``Content-Type`` like header into a tuple with the content
|
||||
type and the options:
|
||||
|
||||
>>> parse_options_header('Content-Type: text/html; mimetype=text/html')
|
||||
('Content-Type:', {'mimetype': 'text/html'})
|
||||
|
||||
:param value: the header to parse.
|
||||
:return: (str, options)
|
||||
"""
|
||||
def _tokenize(string):
|
||||
for match in _option_header_piece_re.finditer(string):
|
||||
key, value = match.groups()
|
||||
key = unquote_header_value(key)
|
||||
if value is not None:
|
||||
value = unquote_header_value(value)
|
||||
yield key, value
|
||||
|
||||
if not value:
|
||||
return '', {}
|
||||
|
||||
parts = _tokenize(';' + value)
|
||||
name = next(parts)[0]
|
||||
extra = dict(parts)
|
||||
return name, extra
|
||||
|
||||
|
||||
class Accept(object):
|
||||
def __init__(self, value):
|
||||
self._content_types = [parse_options_header(v) for v in
|
||||
parse_list_header(value)]
|
||||
|
||||
def best_match(self, supported_content_types):
|
||||
best_quality = -1
|
||||
best_content_type = None
|
||||
best_params = {}
|
||||
best_match = '*/*'
|
||||
|
||||
for content_type in supported_content_types:
|
||||
for content_mask, params in self._content_types:
|
||||
try:
|
||||
quality = float(params.get('q', 1))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
if quality < best_quality:
|
||||
continue
|
||||
elif best_quality == quality:
|
||||
if best_match.count('*') <= content_mask.count('*'):
|
||||
continue
|
||||
|
||||
if self._match_mask(content_mask, content_type):
|
||||
best_quality = quality
|
||||
best_content_type = content_type
|
||||
best_params = params
|
||||
best_match = content_mask
|
||||
|
||||
return best_content_type, best_params
|
||||
|
||||
def _match_mask(self, mask, content_type):
|
||||
if '*' not in mask:
|
||||
return content_type == mask
|
||||
if mask == '*/*':
|
||||
return True
|
||||
mask_major = mask[:-2]
|
||||
content_type_major = content_type.split('/', 1)[0]
|
||||
return content_type_major == mask_major
|
||||
|
||||
|
||||
def urlmap_factory(loader, global_conf, **local_conf):
|
||||
if 'not_found_app' in local_conf:
|
||||
not_found_app = local_conf.pop('not_found_app')
|
||||
else:
|
||||
not_found_app = global_conf.get('not_found_app')
|
||||
if not_found_app:
|
||||
not_found_app = loader.get_app(not_found_app, global_conf=global_conf)
|
||||
urlmap = URLMap(not_found_app=not_found_app)
|
||||
for path, app_name in local_conf.items():
|
||||
path = paste.urlmap.parse_path_expression(path)
|
||||
app = loader.get_app(app_name, global_conf=global_conf)
|
||||
urlmap[path] = app
|
||||
return urlmap
|
||||
|
||||
|
||||
class URLMap(paste.urlmap.URLMap):
|
||||
def _match(self, host, port, path_info):
|
||||
"""Find longest match for a given URL path."""
|
||||
for (domain, app_url), app in self.applications:
|
||||
if domain and domain != host and domain != host + ':' + port:
|
||||
continue
|
||||
if (path_info == app_url
|
||||
or path_info.startswith(app_url + '/')):
|
||||
return app, app_url
|
||||
|
||||
return None, None
|
||||
|
||||
def _set_script_name(self, app, app_url):
|
||||
def wrap(environ, start_response):
|
||||
environ['SCRIPT_NAME'] += app_url
|
||||
return app(environ, start_response)
|
||||
|
||||
return wrap
|
||||
|
||||
def _munge_path(self, app, path_info, app_url):
|
||||
def wrap(environ, start_response):
|
||||
environ['SCRIPT_NAME'] += app_url
|
||||
environ['PATH_INFO'] = path_info[len(app_url):]
|
||||
return app(environ, start_response)
|
||||
|
||||
return wrap
|
||||
|
||||
def _path_strategy(self, host, port, path_info):
|
||||
"""Check path suffix for MIME type and path prefix for API version."""
|
||||
mime_type = app = app_url = None
|
||||
|
||||
parts = path_info.rsplit('.', 1)
|
||||
if len(parts) > 1:
|
||||
possible_type = 'application/' + parts[1]
|
||||
if possible_type in wsgi.get_supported_content_types():
|
||||
mime_type = possible_type
|
||||
|
||||
parts = path_info.split('/')
|
||||
if len(parts) > 1:
|
||||
possible_app, possible_app_url = self._match(host, port, path_info)
|
||||
# Don't use prefix if it ends up matching default
|
||||
if possible_app and possible_app_url:
|
||||
app_url = possible_app_url
|
||||
app = self._munge_path(possible_app, path_info, app_url)
|
||||
|
||||
return mime_type, app, app_url
|
||||
|
||||
def _content_type_strategy(self, host, port, environ):
|
||||
"""Check Content-Type header for API version."""
|
||||
app = None
|
||||
params = parse_options_header(environ.get('CONTENT_TYPE', ''))[1]
|
||||
if 'version' in params:
|
||||
app, app_url = self._match(host, port, '/v' + params['version'])
|
||||
if app:
|
||||
app = self._set_script_name(app, app_url)
|
||||
|
||||
return app
|
||||
|
||||
def _accept_strategy(self, host, port, environ, supported_content_types):
|
||||
"""Check Accept header for best matching MIME type and API version."""
|
||||
accept = Accept(environ.get('HTTP_ACCEPT', ''))
|
||||
|
||||
app = None
|
||||
|
||||
# Find the best match in the Accept header
|
||||
mime_type, params = accept.best_match(supported_content_types)
|
||||
if 'version' in params:
|
||||
app, app_url = self._match(host, port, '/v' + params['version'])
|
||||
if app:
|
||||
app = self._set_script_name(app, app_url)
|
||||
|
||||
return mime_type, app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
host = environ.get('HTTP_HOST', environ.get('SERVER_NAME')).lower()
|
||||
if ':' in host:
|
||||
host, port = host.split(':', 1)
|
||||
else:
|
||||
if environ['wsgi.url_scheme'] == 'http':
|
||||
port = '80'
|
||||
else:
|
||||
port = '443'
|
||||
|
||||
path_info = environ['PATH_INFO']
|
||||
path_info = self.normalize_url(path_info, False)[1]
|
||||
|
||||
# The MIME type for the response is determined in one of two ways:
|
||||
# 1) URL path suffix (eg /servers/detail.json)
|
||||
# 2) Accept header (eg application/json;q=0.8, application/xml;q=0.2)
|
||||
|
||||
# The API version is determined in one of three ways:
|
||||
# 1) URL path prefix (eg /v1.1/tenant/servers/detail)
|
||||
# 2) Content-Type header (eg application/json;version=1.1)
|
||||
# 3) Accept header (eg application/json;q=0.8;version=1.1)
|
||||
|
||||
supported_content_types = list(wsgi.get_supported_content_types())
|
||||
|
||||
mime_type, app, app_url = self._path_strategy(host, port, path_info)
|
||||
|
||||
# Accept application/atom+xml for the index query of each API
|
||||
# version mount point as well as the root index
|
||||
if (app_url and app_url + '/' == path_info) or path_info == '/':
|
||||
supported_content_types.append('application/atom+xml')
|
||||
|
||||
if not app:
|
||||
app = self._content_type_strategy(host, port, environ)
|
||||
|
||||
if not mime_type or not app:
|
||||
possible_mime_type, possible_app = self._accept_strategy(
|
||||
host, port, environ, supported_content_types)
|
||||
if possible_mime_type and not mime_type:
|
||||
mime_type = possible_mime_type
|
||||
if possible_app and not app:
|
||||
app = possible_app
|
||||
|
||||
if not mime_type:
|
||||
mime_type = 'application/json'
|
||||
|
||||
if not app:
|
||||
# Didn't match a particular version, probably matches default
|
||||
app, app_url = self._match(host, port, path_info)
|
||||
if app:
|
||||
app = self._munge_path(app, path_info, app_url)
|
||||
|
||||
if app:
|
||||
environ['masakari.best_content_type'] = mime_type
|
||||
return app(environ, start_response)
|
||||
|
||||
LOG.debug('Could not find application for %s', environ['PATH_INFO'])
|
||||
environ['paste.urlmap_object'] = self
|
||||
return self.not_found_application(environ, start_response)
|
35
masakari/api/versioned_method.py
Normal file
35
masakari/api/versioned_method.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
#
|
||||
# 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.
|
||||
|
||||
|
||||
class VersionedMethod(object):
|
||||
|
||||
def __init__(self, name, start_version, end_version, func):
|
||||
"""Versioning information for a single method
|
||||
|
||||
@name: Name of the method
|
||||
@start_version: Minimum acceptable version
|
||||
@end_version: Maximum acceptable_version
|
||||
@func: Method to call
|
||||
|
||||
Minimum and maximums are inclusive
|
||||
"""
|
||||
self.name = name
|
||||
self.start_version = start_version
|
||||
self.end_version = end_version
|
||||
self.func = func
|
||||
|
||||
def __str__(self):
|
||||
return ("Version Method %s: min: %s, max: %s"
|
||||
% (self.name, self.start_version, self.end_version))
|
0
masakari/cmd/__init__.py
Normal file
0
masakari/cmd/__init__.py
Normal file
57
masakari/cmd/api.py
Normal file
57
masakari/cmd/api.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
"""Starter script for Masakari API.
|
||||
|
||||
"""
|
||||
|
||||
import sys
|
||||
|
||||
from oslo_log import log as logging
|
||||
import six
|
||||
|
||||
import masakari.conf
|
||||
from masakari import config
|
||||
from masakari import exception
|
||||
from masakari.i18n import _LE
|
||||
from masakari.i18n import _LW
|
||||
from masakari import service
|
||||
|
||||
|
||||
CONF = masakari.conf.CONF
|
||||
|
||||
|
||||
def main():
|
||||
config.parse_args(sys.argv)
|
||||
logging.setup(CONF, "masakari")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
launcher = service.process_launcher()
|
||||
started = 0
|
||||
try:
|
||||
server = service.WSGIService("masakari_api", use_ssl=CONF.use_ssl)
|
||||
launcher.launch_service(server, workers=server.workers or 1)
|
||||
started += 1
|
||||
except exception.PasteAppNotFound as ex:
|
||||
log.warning(
|
||||
_LW("%s. ``enabled_apis`` includes bad values. "
|
||||
"Fix to remove this warning."), six.text_type(ex))
|
||||
|
||||
if started == 0:
|
||||
log.error(_LE('No APIs were started. '
|
||||
'Check the enabled_apis config option.'))
|
||||
sys.exit(1)
|
||||
|
||||
launcher.wait()
|
0
masakari/common/__init__.py
Normal file
0
masakari/common/__init__.py
Normal file
39
masakari/common/config.py
Normal file
39
masakari/common/config.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
#
|
||||
# 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
|
||||
from oslo_middleware import cors
|
||||
|
||||
|
||||
def set_middleware_defaults():
|
||||
"""Update default configuration options for oslo.middleware."""
|
||||
# CORS Defaults
|
||||
cfg.set_defaults(cors.CORS_OPTS,
|
||||
allow_headers=['X-Auth-Token',
|
||||
'X-Openstack-Request-Id',
|
||||
'X-Identity-Status',
|
||||
'X-Roles',
|
||||
'X-Service-Catalog',
|
||||
'X-User-Id',
|
||||
'X-Tenant-Id'],
|
||||
expose_headers=['X-Auth-Token',
|
||||
'X-Openstack-Request-Id',
|
||||
'X-Subject-Token',
|
||||
'X-Service-Token'],
|
||||
allow_methods=['GET',
|
||||
'PUT',
|
||||
'POST',
|
||||
'DELETE',
|
||||
'PATCH']
|
||||
)
|
34
masakari/conf/__init__.py
Normal file
34
masakari/conf/__init__.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from masakari.conf import api
|
||||
from masakari.conf import database
|
||||
from masakari.conf import exceptions
|
||||
from masakari.conf import osapi_v1
|
||||
from masakari.conf import service
|
||||
from masakari.conf import ssl
|
||||
from masakari.conf import wsgi
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
api.register_opts(CONF)
|
||||
database.register_opts(CONF)
|
||||
exceptions.register_opts(CONF)
|
||||
osapi_v1.register_opts(CONF)
|
||||
ssl.register_opts(CONF)
|
||||
service.register_opts(CONF)
|
||||
wsgi.register_opts(CONF)
|
112
masakari/conf/api.py
Normal file
112
masakari/conf/api.py
Normal file
@ -0,0 +1,112 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
auth_opts = [
|
||||
cfg.StrOpt("auth_strategy",
|
||||
default="keystone",
|
||||
choices=("keystone", "noauth2"),
|
||||
help="""
|
||||
This determines the strategy to use for authentication: keystone or noauth2.
|
||||
'noauth2' is designed for testing only, as it does no actual credential
|
||||
checking. 'noauth2' provides administrative credentials only if 'admin' is
|
||||
specified as the username.
|
||||
|
||||
* Possible values:
|
||||
|
||||
Either 'keystone' (default) or 'noauth2'.
|
||||
|
||||
* Services that use this:
|
||||
|
||||
``masakari-api``
|
||||
|
||||
* Related options:
|
||||
|
||||
None
|
||||
"""),
|
||||
cfg.BoolOpt("use_forwarded_for",
|
||||
default=False,
|
||||
help="""
|
||||
When True, the 'X-Forwarded-For' header is treated as the canonical remote
|
||||
address. When False (the default), the 'remote_address' header is used.
|
||||
|
||||
You should only enable this if you have an HTML sanitizing proxy.
|
||||
|
||||
* Possible values:
|
||||
|
||||
True, False (default)
|
||||
|
||||
* Services that use this:
|
||||
|
||||
``masakari-api``
|
||||
|
||||
* Related options:
|
||||
|
||||
None
|
||||
"""),
|
||||
]
|
||||
|
||||
|
||||
osapi_opts = [
|
||||
cfg.IntOpt("osapi_max_limit",
|
||||
default=1000,
|
||||
help="""
|
||||
As a query can potentially return many thousands of items, you can limit the
|
||||
maximum number of items in a single response by setting this option.
|
||||
|
||||
* Possible values:
|
||||
|
||||
Any positive integer. Default is 1000.
|
||||
|
||||
* Services that use this:
|
||||
|
||||
``masakari-api``
|
||||
|
||||
* Related options:
|
||||
|
||||
None
|
||||
"""),
|
||||
cfg.StrOpt("osapi_masakari_link_prefix",
|
||||
help="""
|
||||
This string is prepended to the normal URL that is returned in links to the
|
||||
OpenStack Masakari API. If it is empty (the default), the URLs are returned
|
||||
unchanged.
|
||||
|
||||
* Possible values:
|
||||
|
||||
Any string, including an empty string (the default).
|
||||
|
||||
* Services that use this:
|
||||
|
||||
``masakari-api``
|
||||
|
||||
* Related options:
|
||||
|
||||
None
|
||||
"""),
|
||||
]
|
||||
|
||||
|
||||
ALL_OPTS = (auth_opts + osapi_opts)
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_opts(ALL_OPTS)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {"DEFAULT": ALL_OPTS}
|
30
masakari/conf/database.py
Normal file
30
masakari/conf/database.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
from masakari.conf import paths
|
||||
|
||||
from oslo_db import options as oslo_db_options
|
||||
|
||||
_DEFAULT_SQL_CONNECTION = 'sqlite:///' + paths.state_path_def(
|
||||
'masakari.sqlite')
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
oslo_db_options.set_defaults(conf, connection=_DEFAULT_SQL_CONNECTION,
|
||||
sqlite_db='masakari.sqlite')
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {'DEFAULT': []}
|
30
masakari/conf/exceptions.py
Normal file
30
masakari/conf/exceptions.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
exc_log_opts = [
|
||||
cfg.BoolOpt('fatal_exception_format_errors',
|
||||
default=False,
|
||||
help='Make exception message format errors fatal'),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_opts(exc_log_opts)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {'DEFAULT': exc_log_opts}
|
88
masakari/conf/opts.py
Normal file
88
masakari/conf/opts.py
Normal file
@ -0,0 +1,88 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This is the single point of entry to generate the sample configuration
|
||||
file for Masakari. It collects all the necessary info from the other modules
|
||||
in this package. It is assumed that:
|
||||
|
||||
* every other module in this package has a 'list_opts' function which
|
||||
return a dict where
|
||||
* the keys are strings which are the group names
|
||||
* the value of each key is a list of config options for that group
|
||||
* the masakari.conf package doesn't have further packages with config options
|
||||
* this module is only used in the context of sample file generation
|
||||
"""
|
||||
|
||||
import collections
|
||||
import importlib
|
||||
import os
|
||||
import pkgutil
|
||||
|
||||
LIST_OPTS_FUNC_NAME = "list_opts"
|
||||
|
||||
|
||||
def _tupleize(dct):
|
||||
"""Take the dict of options and convert to the 2-tuple format."""
|
||||
return [(key, val) for key, val in dct.items()]
|
||||
|
||||
|
||||
def list_opts():
|
||||
opts = collections.defaultdict(list)
|
||||
module_names = _list_module_names()
|
||||
imported_modules = _import_modules(module_names)
|
||||
_append_config_options(imported_modules, opts)
|
||||
return _tupleize(opts)
|
||||
|
||||
|
||||
def _list_module_names():
|
||||
module_names = []
|
||||
package_path = os.path.dirname(os.path.abspath(__file__))
|
||||
for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]):
|
||||
if modname == "opts" or ispkg:
|
||||
continue
|
||||
else:
|
||||
module_names.append(modname)
|
||||
return module_names
|
||||
|
||||
|
||||
def _import_modules(module_names):
|
||||
imported_modules = []
|
||||
for modname in module_names:
|
||||
mod = importlib.import_module("masakari.conf." + modname)
|
||||
if not hasattr(mod, LIST_OPTS_FUNC_NAME):
|
||||
msg = "The module 'masakari.conf.%s' should have a '%s' "\
|
||||
"function which returns the config options." % \
|
||||
(modname, LIST_OPTS_FUNC_NAME)
|
||||
raise Exception(msg)
|
||||
else:
|
||||
imported_modules.append(mod)
|
||||
return imported_modules
|
||||
|
||||
|
||||
def _process_old_opts(configs):
|
||||
"""Convert old-style 2-tuple configs to dicts."""
|
||||
if isinstance(configs, tuple):
|
||||
configs = [configs]
|
||||
return {label: options for label, options in configs}
|
||||
|
||||
|
||||
def _append_config_options(imported_modules, config_options):
|
||||
for mod in imported_modules:
|
||||
configs = mod.list_opts()
|
||||
if not isinstance(configs, dict):
|
||||
configs = _process_old_opts(configs)
|
||||
for key, val in configs.items():
|
||||
config_options[key].extend(val)
|
106
masakari/conf/osapi_v1.py
Normal file
106
masakari/conf/osapi_v1.py
Normal file
@ -0,0 +1,106 @@
|
||||
# Copyright (c) 2016 NTT Data
|
||||
# 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.
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
|
||||
api_opts = [
|
||||
cfg.ListOpt("extensions_blacklist",
|
||||
default=[],
|
||||
deprecated_for_removal=True,
|
||||
deprecated_group="osapi_v1",
|
||||
help="""
|
||||
*DEPRECATED*
|
||||
|
||||
This option is a list of all of the v2.1 API extensions to never load. However,
|
||||
it will be removed in the near future, after which the all the functionality
|
||||
that was previously in extensions will be part of the standard API, and thus
|
||||
always accessible.
|
||||
|
||||
* Possible values:
|
||||
|
||||
A list of strings, each being the alias of an extension that you do not
|
||||
wish to load.
|
||||
|
||||
* Services that use this:
|
||||
|
||||
``masakari-api``
|
||||
|
||||
* Related options:
|
||||
|
||||
enabled, extensions_whitelist
|
||||
"""),
|
||||
cfg.ListOpt("extensions_whitelist",
|
||||
default=[],
|
||||
deprecated_for_removal=True,
|
||||
deprecated_group="osapi_v1",
|
||||
help="""
|
||||
*DEPRECATED*
|
||||
|
||||
This is a list of extensions. If it is empty, then *all* extensions except
|
||||
those specified in the extensions_blacklist option will be loaded. If it is not
|
||||
empty, then only those extensions in this list will be loaded, provided that
|
||||
they are also not in the extensions_blacklist option. Once this deprecated
|
||||
option is removed, after which the all the functionality that was previously in
|
||||
extensions will be part of the standard API, and thus always accessible.
|
||||
|
||||
* Possible values:
|
||||
|
||||
A list of strings, each being the alias of an extension that you wish to
|
||||
load, or an empty list, which indicates that all extensions are to be run.
|
||||
|
||||
* Services that use this:
|
||||
|
||||
``masakari-api``
|
||||
|
||||
* Related options:
|
||||
|
||||
enabled, extensions_blacklist
|
||||
"""),
|
||||
cfg.StrOpt("project_id_regex",
|
||||
default=None,
|
||||
deprecated_for_removal=True,
|
||||
deprecated_group="osapi_v1",
|
||||
help="""
|
||||
*DEPRECATED*
|
||||
|
||||
This option is a string representing a regular expression (regex) that matches
|
||||
the project_id as contained in URLs. If not set, it will match normal UUIDs
|
||||
created by keystone.
|
||||
|
||||
* Possible values:
|
||||
|
||||
A string representing any legal regular expression
|
||||
|
||||
* Services that use this:
|
||||
|
||||
``masakari-api``
|
||||
|
||||
* Related options:
|
||||
|
||||
None
|
||||
"""),
|
||||
]
|
||||
|
||||
api_opts_group = cfg.OptGroup(name="osapi_v1", title="API v1 Options")
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_group(api_opts_group)
|
||||
conf.register_opts(api_opts, api_opts_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {api_opts_group: api_opts}
|
54
masakari/conf/paths.py
Normal file
54
masakari/conf/paths.py
Normal file
@ -0,0 +1,54 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
path_opts = [
|
||||
cfg.StrOpt('pybasedir',
|
||||
default=os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||
'../../')),
|
||||
help='Directory where the masakari python module is installed'),
|
||||
cfg.StrOpt('bindir',
|
||||
default=os.path.join(sys.prefix, 'local', 'bin'),
|
||||
help='Directory where masakari binaries are installed'),
|
||||
cfg.StrOpt('state_path',
|
||||
default='$pybasedir',
|
||||
help="Top-level directory for maintaining masakari's state"),
|
||||
]
|
||||
|
||||
|
||||
def basedir_def(*args):
|
||||
"""Return an uninterpolated path relative to $pybasedir."""
|
||||
return os.path.join('$pybasedir', *args)
|
||||
|
||||
|
||||
def bindir_def(*args):
|
||||
"""Return an uninterpolated path relative to $bindir."""
|
||||
return os.path.join('$bindir', *args)
|
||||
|
||||
|
||||
def state_path_def(*args):
|
||||
"""Return an uninterpolated path relative to $state_path."""
|
||||
return os.path.join('$state_path', *args)
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_opts(path_opts)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {"DEFAULT": path_opts}
|
55
masakari/conf/service.py
Normal file
55
masakari/conf/service.py
Normal file
@ -0,0 +1,55 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
service_opts = [
|
||||
cfg.IntOpt('report_interval',
|
||||
default=10,
|
||||
help='Seconds between nodes reporting state to datastore'),
|
||||
cfg.BoolOpt('periodic_enable',
|
||||
default=True,
|
||||
help='Enable periodic tasks'),
|
||||
cfg.IntOpt('periodic_fuzzy_delay',
|
||||
default=60,
|
||||
help='Range of seconds to randomly delay when starting the'
|
||||
' periodic task scheduler to reduce stampeding.'
|
||||
' (Disable by setting to 0)'),
|
||||
cfg.BoolOpt('use_ssl',
|
||||
default=False,
|
||||
help='Use APIs with SSL enabled'),
|
||||
cfg.StrOpt('masakari_api_listen',
|
||||
default="0.0.0.0",
|
||||
help='The IP address on which the Masakari API will listen.'),
|
||||
cfg.IntOpt('masakari_api_listen_port',
|
||||
default=15868,
|
||||
min=1,
|
||||
max=65535,
|
||||
help='The port on which the Masakari API will listen.'),
|
||||
cfg.IntOpt('masakari_api_workers',
|
||||
help='Number of workers for Masakari API service. The default '
|
||||
'will be the number of CPUs available.'),
|
||||
cfg.IntOpt('service_down_time',
|
||||
default=60,
|
||||
help='Maximum time since last check-in for up service'),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_opts(service_opts)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {'DEFAULT': service_opts}
|
24
masakari/conf/ssl.py
Normal file
24
masakari/conf/ssl.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
from oslo_service import sslutils
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
sslutils.register_opts(conf)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return sslutils.list_opts()
|
124
masakari/conf/wsgi.py
Normal file
124
masakari/conf/wsgi.py
Normal file
@ -0,0 +1,124 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
wsgi_group = cfg.OptGroup(
|
||||
'wsgi',
|
||||
title='WSGI Options')
|
||||
|
||||
api_paste_config = cfg.StrOpt(
|
||||
'api_paste_config',
|
||||
default="api-paste.ini",
|
||||
help='File name for the paste.deploy config for masakari-api',
|
||||
deprecated_group='DEFAULT')
|
||||
|
||||
# TODO(abhishekk): It is not possible to rename this to 'log_format'
|
||||
# yet, as doing so would cause a conflict if '[DEFAULT] log_format'
|
||||
# were used. When 'deprecated_group' is removed after Ocata, this
|
||||
# should be changed.
|
||||
wsgi_log_format = cfg.StrOpt(
|
||||
'wsgi_log_format',
|
||||
default='%(client_ip)s "%(request_line)s" status: %(status_code)s'
|
||||
' len: %(body_length)s time: %(wall_seconds).7f',
|
||||
help='A python format string that is used as the template to '
|
||||
'generate log lines. The following values can be formatted '
|
||||
'into it: client_ip, date_time, request_line, status_code, '
|
||||
'body_length, wall_seconds.',
|
||||
deprecated_group='DEFAULT')
|
||||
|
||||
secure_proxy_ssl_header = cfg.StrOpt(
|
||||
'secure_proxy_ssl_header',
|
||||
help='The HTTP header used to determine the scheme for the '
|
||||
'original request, even if it was removed by an SSL '
|
||||
'terminating proxy. Typical value is '
|
||||
'"HTTP_X_FORWARDED_PROTO".',
|
||||
deprecated_group='DEFAULT')
|
||||
|
||||
ssl_ca_file = cfg.StrOpt(
|
||||
'ssl_ca_file',
|
||||
help='CA certificate file to use to verify connecting clients',
|
||||
deprecated_group='DEFAULT')
|
||||
|
||||
ssl_cert_file = cfg.StrOpt(
|
||||
'ssl_cert_file',
|
||||
help='SSL certificate of API server',
|
||||
deprecated_group='DEFAULT')
|
||||
|
||||
ssl_key_file = cfg.StrOpt(
|
||||
'ssl_key_file',
|
||||
help='SSL private key of API server',
|
||||
deprecated_group='DEFAULT')
|
||||
|
||||
tcp_keepidle = cfg.IntOpt(
|
||||
'tcp_keepidle',
|
||||
default=600,
|
||||
help='Sets the value of TCP_KEEPIDLE in seconds for each '
|
||||
'server socket. Not supported on OS X.',
|
||||
deprecated_group='DEFAULT')
|
||||
|
||||
default_pool_size = cfg.IntOpt(
|
||||
'default_pool_size',
|
||||
default=1000,
|
||||
help='Size of the pool of greenthreads used by wsgi',
|
||||
deprecated_group='DEFAULT',
|
||||
deprecated_name='wsgi_default_pool_size')
|
||||
|
||||
max_header_line = 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).',
|
||||
deprecated_group='DEFAULT')
|
||||
|
||||
keep_alive = cfg.BoolOpt(
|
||||
'keep_alive',
|
||||
default=True,
|
||||
help='If False, closes the client socket connection explicitly.',
|
||||
deprecated_group='DEFAULT',
|
||||
deprecated_name='wsgi_keep_alive')
|
||||
|
||||
client_socket_timeout = 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.",
|
||||
deprecated_group='DEFAULT')
|
||||
|
||||
ALL_OPTS = [api_paste_config,
|
||||
wsgi_log_format,
|
||||
secure_proxy_ssl_header,
|
||||
ssl_ca_file,
|
||||
ssl_cert_file,
|
||||
ssl_key_file,
|
||||
tcp_keepidle,
|
||||
default_pool_size,
|
||||
max_header_line,
|
||||
keep_alive,
|
||||
client_socket_timeout
|
||||
]
|
||||
|
||||
|
||||
def register_opts(conf):
|
||||
conf.register_group(wsgi_group)
|
||||
conf.register_opts(ALL_OPTS, group=wsgi_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {wsgi_group: ALL_OPTS}
|
35
masakari/config.py
Normal file
35
masakari/config.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
#
|
||||
# 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_log import log
|
||||
|
||||
from masakari.common import config
|
||||
import masakari.conf
|
||||
from masakari import version
|
||||
|
||||
|
||||
CONF = masakari.conf.CONF
|
||||
|
||||
|
||||
def parse_args(argv, default_config_files=None, configure_db=True,
|
||||
init_rpc=True):
|
||||
log.register_options(CONF)
|
||||
# We use the oslo.log default log levels which includes suds=INFO
|
||||
# and add only the extra levels that Masakari needs
|
||||
log.set_defaults(default_log_levels=log.get_default_log_levels())
|
||||
config.set_middleware_defaults()
|
||||
|
||||
CONF(argv[1:],
|
||||
project='masakari',
|
||||
version=version.version_string(),
|
||||
default_config_files=default_config_files)
|
257
masakari/context.py
Normal file
257
masakari/context.py
Normal file
@ -0,0 +1,257 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
"""
|
||||
RequestContext: context for requests that persist through all of masakari.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from keystoneauth1.access import service_catalog as ksa_service_catalog
|
||||
from keystoneauth1 import plugin
|
||||
from oslo_context import context
|
||||
from oslo_db.sqlalchemy import enginefacade
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import timeutils
|
||||
import six
|
||||
|
||||
from masakari import exception
|
||||
from masakari.i18n import _
|
||||
from masakari.i18n import _LW
|
||||
from masakari import policy
|
||||
from masakari import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _ContextAuthPlugin(plugin.BaseAuthPlugin):
|
||||
"""A keystoneauth auth plugin that uses the values from the Context.
|
||||
|
||||
Ideally we would use the plugin provided by auth_token middleware however
|
||||
this plugin isn't serialized yet so we construct one from the serialized
|
||||
auth data.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_token, sc):
|
||||
super(_ContextAuthPlugin, self).__init__()
|
||||
|
||||
self.auth_token = auth_token
|
||||
self.service_catalog = ksa_service_catalog.ServiceCatalogV2(sc)
|
||||
|
||||
def get_token(self, *args, **kwargs):
|
||||
return self.auth_token
|
||||
|
||||
def get_endpoint(self, session, service_type=None, interface=None,
|
||||
region_name=None, service_name=None, **kwargs):
|
||||
return self.service_catalog.url_for(service_type=service_type,
|
||||
service_name=service_name,
|
||||
interface=interface,
|
||||
region_name=region_name)
|
||||
|
||||
|
||||
@enginefacade.transaction_context_provider
|
||||
class RequestContext(context.RequestContext):
|
||||
"""Security context and request information.
|
||||
|
||||
Represents the user taking a given action within the system.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, user_id=None, project_id=None,
|
||||
is_admin=None, read_deleted="no",
|
||||
roles=None, remote_address=None, timestamp=None,
|
||||
request_id=None, auth_token=None, overwrite=True,
|
||||
user_name=None, project_name=None, service_catalog=None,
|
||||
user_auth_plugin=None, **kwargs):
|
||||
""":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 user_auth_plugin: The auth plugin for the current request's
|
||||
authentication data.
|
||||
|
||||
:param kwargs: Extra arguments that might be present, but we ignore
|
||||
because they possibly came in from older rpc messages.
|
||||
"""
|
||||
user = kwargs.pop('user', None)
|
||||
tenant = kwargs.pop('tenant', None)
|
||||
super(RequestContext, self).__init__(
|
||||
auth_token=auth_token,
|
||||
user=user_id or user,
|
||||
tenant=project_id or tenant,
|
||||
domain=kwargs.pop('domain', None),
|
||||
user_domain=kwargs.pop('user_domain', None),
|
||||
project_domain=kwargs.pop('project_domain', None),
|
||||
is_admin=is_admin,
|
||||
read_only=kwargs.pop('read_only', False),
|
||||
show_deleted=kwargs.pop('show_deleted', False),
|
||||
request_id=request_id,
|
||||
resource_uuid=kwargs.pop('resource_uuid', None),
|
||||
overwrite=overwrite,
|
||||
roles=roles)
|
||||
# oslo_context's RequestContext.to_dict() generates this field, we can
|
||||
# safely ignore this as we don't use it.
|
||||
kwargs.pop('user_identity', None)
|
||||
if kwargs:
|
||||
LOG.warning(_LW('Arguments dropped when creating context: %s'),
|
||||
str(kwargs))
|
||||
|
||||
# FIXME: user_id and project_id duplicate information that is
|
||||
# already present in the oslo_context's RequestContext. We need to
|
||||
# get rid of them.
|
||||
self.user_id = user_id
|
||||
self.project_id = project_id
|
||||
self.read_deleted = read_deleted
|
||||
self.remote_address = remote_address
|
||||
if not timestamp:
|
||||
timestamp = timeutils.utcnow()
|
||||
if isinstance(timestamp, six.string_types):
|
||||
timestamp = timeutils.parse_strtime(timestamp)
|
||||
self.timestamp = timestamp
|
||||
|
||||
if service_catalog:
|
||||
# Only include required parts of service_catalog
|
||||
self.service_catalog = [
|
||||
s for s in service_catalog if s.get('type') in (
|
||||
'key-manager')]
|
||||
else:
|
||||
# if list is empty or none
|
||||
self.service_catalog = []
|
||||
|
||||
self.user_name = user_name
|
||||
self.project_name = project_name
|
||||
self.is_admin = is_admin
|
||||
|
||||
self.user_auth_plugin = user_auth_plugin
|
||||
if self.is_admin is None:
|
||||
self.is_admin = policy.check_is_admin(self)
|
||||
|
||||
def get_auth_plugin(self):
|
||||
if self.user_auth_plugin:
|
||||
return self.user_auth_plugin
|
||||
else:
|
||||
return _ContextAuthPlugin(self.auth_token, self.service_catalog)
|
||||
|
||||
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):
|
||||
values = super(RequestContext, self).to_dict()
|
||||
# FIXME: defensive hasattr() checks need to be
|
||||
# removed once we figure out why we are seeing stack
|
||||
# traces
|
||||
values.update({
|
||||
'user_id': getattr(self, 'user_id', None),
|
||||
'project_id': getattr(self, 'project_id', None),
|
||||
'is_admin': getattr(self, 'is_admin', None),
|
||||
'read_deleted': getattr(self, 'read_deleted', 'no'),
|
||||
'remote_address': getattr(self, 'remote_address', None),
|
||||
'timestamp': utils.strtime(self.timestamp) if hasattr(
|
||||
self, 'timestamp') else None,
|
||||
'request_id': getattr(self, 'request_id', None),
|
||||
'user_name': getattr(self, 'user_name', None),
|
||||
'service_catalog': getattr(self, 'service_catalog', None),
|
||||
'project_name': getattr(self, 'project_name', None)
|
||||
})
|
||||
return values
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, values):
|
||||
return cls(**values)
|
||||
|
||||
def elevated(self, read_deleted=None):
|
||||
"""Return a version of this context with admin flag set."""
|
||||
context = copy.copy(self)
|
||||
# context.roles must be deepcopied to leave original roles
|
||||
# without changes
|
||||
context.roles = copy.deepcopy(self.roles)
|
||||
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 __str__(self):
|
||||
return "<Context %s>" % self.to_dict()
|
||||
|
||||
|
||||
def get_admin_context(read_deleted="no"):
|
||||
return RequestContext(user_id=None,
|
||||
project_id=None,
|
||||
is_admin=True,
|
||||
read_deleted=read_deleted,
|
||||
overwrite=False)
|
||||
|
||||
|
||||
def is_user_context(context):
|
||||
"""Indicates if the request context is a normal user."""
|
||||
if not context:
|
||||
return False
|
||||
if context.is_admin:
|
||||
return False
|
||||
if not context.user_id or not context.project_id:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def require_admin_context(ctxt):
|
||||
"""Raise exception.AdminRequired() if context is not an admin context."""
|
||||
if not ctxt.is_admin:
|
||||
raise exception.AdminRequired()
|
||||
|
||||
|
||||
def require_context(ctxt):
|
||||
"""Raise exception.Forbidden() if context is not a user or an
|
||||
admin context.
|
||||
"""
|
||||
if not ctxt.is_admin and not is_user_context(ctxt):
|
||||
raise exception.Forbidden()
|
||||
|
||||
|
||||
def authorize_project_context(context, project_id):
|
||||
"""Ensures a request has permission to access the given project."""
|
||||
if is_user_context(context):
|
||||
if not context.project_id:
|
||||
raise exception.Forbidden()
|
||||
elif context.project_id != project_id:
|
||||
raise exception.Forbidden()
|
||||
|
||||
|
||||
def authorize_user_context(context, user_id):
|
||||
"""Ensures a request has permission to access the given user."""
|
||||
if is_user_context(context):
|
||||
if not context.user_id:
|
||||
raise exception.Forbidden()
|
||||
elif context.user_id != user_id:
|
||||
raise exception.Forbidden()
|
215
masakari/exception.py
Normal file
215
masakari/exception.py
Normal file
@ -0,0 +1,215 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
"""Masakari base exception handling.
|
||||
|
||||
Includes decorator for re-raising Masakari-type exceptions.
|
||||
|
||||
SHOULD include dedicated exception logging.
|
||||
|
||||
"""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
import six
|
||||
import webob.exc
|
||||
from webob import util as woutil
|
||||
|
||||
import masakari.conf
|
||||
from masakari.i18n import _
|
||||
from masakari.i18n import _LE
|
||||
from masakari import safe_utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CONF = masakari.conf.CONF
|
||||
|
||||
|
||||
class ConvertedException(webob.exc.WSGIHTTPException):
|
||||
def __init__(self, code, 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 = woutil.status_reasons[self.code]
|
||||
except KeyError:
|
||||
msg = _LE("Improper or unknown HTTP status code used: %d")
|
||||
LOG.error(msg, code)
|
||||
self.title = woutil.status_generic_reasons[self.code // 100]
|
||||
self.explanation = explanation
|
||||
super(ConvertedException, self).__init__()
|
||||
|
||||
|
||||
def _cleanse_dict(original):
|
||||
"""Strip all admin_password, new_pass, rescue_pass keys from a dict."""
|
||||
return {k: v for k, v in six.iteritems(original) if "_pass" not in k}
|
||||
|
||||
|
||||
def wrap_exception(notifier=None, get_notifier=None):
|
||||
"""This decorator wraps a method to catch any exceptions that may
|
||||
get thrown. It also optionally sends the exception to the notification
|
||||
system.
|
||||
"""
|
||||
def inner(f):
|
||||
def wrapped(self, context, *args, **kw):
|
||||
# Don't store self or context in the payload, it now seems to
|
||||
# contain confidential information.
|
||||
try:
|
||||
return f(self, context, *args, **kw)
|
||||
except Exception as e:
|
||||
with excutils.save_and_reraise_exception():
|
||||
if notifier or get_notifier:
|
||||
payload = dict(exception=e)
|
||||
wrapped_func = safe_utils.get_wrapped_function(f)
|
||||
call_dict = inspect.getcallargs(wrapped_func, self,
|
||||
context, *args, **kw)
|
||||
# self can't be serialized and shouldn't be in the
|
||||
# payload
|
||||
call_dict.pop('self', None)
|
||||
cleansed = _cleanse_dict(call_dict)
|
||||
payload.update({'args': cleansed})
|
||||
|
||||
# If f has multiple decorators, they must use
|
||||
# functools.wraps to ensure the name is
|
||||
# propagated.
|
||||
event_type = f.__name__
|
||||
|
||||
(notifier or get_notifier()).error(context,
|
||||
event_type,
|
||||
payload)
|
||||
|
||||
return functools.wraps(f)(wrapped)
|
||||
return inner
|
||||
|
||||
|
||||
class MasakariException(Exception):
|
||||
"""Base Masakari Exception
|
||||
|
||||
To correctly use this class, inherit from it and define
|
||||
a 'msg_fmt' property. That msg_fmt will get printf'd
|
||||
with the keyword arguments provided to the constructor.
|
||||
|
||||
"""
|
||||
msg_fmt = _("An unknown exception occurred.")
|
||||
code = 500
|
||||
headers = {}
|
||||
safe = False
|
||||
|
||||
def __init__(self, message=None, **kwargs):
|
||||
self.kwargs = kwargs
|
||||
|
||||
if 'code' not in self.kwargs:
|
||||
try:
|
||||
self.kwargs['code'] = self.code
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if not message:
|
||||
try:
|
||||
message = self.msg_fmt % 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 six.iteritems(kwargs):
|
||||
LOG.error("%s: %s" % (name, value)) # noqa
|
||||
|
||||
if CONF.fatal_exception_format_errors:
|
||||
six.reraise(*exc_info)
|
||||
else:
|
||||
# at least get the core message out if something happened
|
||||
message = self.msg_fmt
|
||||
|
||||
self.message = message
|
||||
super(MasakariException, self).__init__(message)
|
||||
|
||||
def format_message(self):
|
||||
# NOTE: use the first argument to the python Exception object
|
||||
# which should be our full MasakariException message, (see __init__)
|
||||
return self.args[0]
|
||||
|
||||
|
||||
class Invalid(MasakariException):
|
||||
msg_fmt = _("Bad Request - Invalid Parameters")
|
||||
code = 400
|
||||
|
||||
|
||||
class InvalidAPIVersionString(Invalid):
|
||||
msg_fmt = _("API Version String %(version)s is of invalid format. Must "
|
||||
"be of format MajorNum.MinorNum.")
|
||||
|
||||
|
||||
class MalformedRequestBody(MasakariException):
|
||||
msg_fmt = _("Malformed message body: %(reason)s")
|
||||
|
||||
|
||||
# NOTE: NotFound should only be used when a 404 error is
|
||||
# appropriate to be returned
|
||||
class ConfigNotFound(MasakariException):
|
||||
msg_fmt = _("Could not find config at %(path)s")
|
||||
|
||||
|
||||
class Forbidden(MasakariException):
|
||||
msg_fmt = _("Forbidden")
|
||||
code = 403
|
||||
|
||||
|
||||
class AdminRequired(Forbidden):
|
||||
msg_fmt = _("User does not have admin privileges")
|
||||
|
||||
|
||||
class PolicyNotAuthorized(Forbidden):
|
||||
msg_fmt = _("Policy doesn't allow %(action)s to be performed.")
|
||||
|
||||
|
||||
class PasteAppNotFound(MasakariException):
|
||||
msg_fmt = _("Could not load paste app '%(name)s' from %(path)s")
|
||||
|
||||
|
||||
class InvalidContentType(Invalid):
|
||||
msg_fmt = _("Invalid content type %(content_type)s.")
|
||||
|
||||
|
||||
class VersionNotFoundForAPIMethod(Invalid):
|
||||
msg_fmt = _("API version %(version)s is not supported on this method.")
|
||||
|
||||
|
||||
class InvalidGlobalAPIVersion(Invalid):
|
||||
msg_fmt = _("Version %(req_ver)s is not supported by the API. Minimum "
|
||||
"is %(min_ver)s and maximum is %(max_ver)s.")
|
||||
|
||||
|
||||
class ApiVersionsIntersect(Invalid):
|
||||
msg_fmt = _("Version of %(name) %(min_ver) %(max_ver) intersects "
|
||||
"with another versions.")
|
||||
|
||||
|
||||
class ValidationError(Invalid):
|
||||
msg_fmt = "%(detail)s"
|
46
masakari/i18n.py
Normal file
46
masakari/i18n.py
Normal file
@ -0,0 +1,46 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
#
|
||||
# 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
|
||||
|
||||
DOMAIN = 'masakari'
|
||||
|
||||
_translators = oslo_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 translate(value, user_locale):
|
||||
return oslo_i18n.translate(value, user_locale)
|
||||
|
||||
|
||||
def get_available_languages():
|
||||
return oslo_i18n.get_available_languages(DOMAIN)
|
138
masakari/policy.py
Normal file
138
masakari/policy.py
Normal file
@ -0,0 +1,138 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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 Masakari."""
|
||||
|
||||
import logging
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_policy import policy
|
||||
from oslo_utils import excutils
|
||||
|
||||
from masakari import exception
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
_ENFORCER = None
|
||||
|
||||
|
||||
def reset():
|
||||
global _ENFORCER
|
||||
if _ENFORCER:
|
||||
_ENFORCER.clear()
|
||||
_ENFORCER = None
|
||||
|
||||
|
||||
def init(policy_file=None, rules=None, default_rule=None, use_conf=True):
|
||||
"""Init an Enforcer class.
|
||||
|
||||
:param policy_file: Custom policy file to use, if none is specified,
|
||||
`CONF.policy_file` will be used.
|
||||
:param rules: Default dictionary / Rules to use. It will be
|
||||
considered just in the first instantiation.
|
||||
:param default_rule: Default rule to use, CONF.default_rule will
|
||||
be used if none is specified.
|
||||
:param use_conf: Whether to load rules from config file.
|
||||
"""
|
||||
global _ENFORCER
|
||||
if not _ENFORCER:
|
||||
_ENFORCER = policy.Enforcer(CONF,
|
||||
policy_file=policy_file,
|
||||
rules=rules,
|
||||
default_rule=default_rule,
|
||||
use_conf=use_conf)
|
||||
|
||||
|
||||
def set_rules(rules, overwrite=True, use_conf=False):
|
||||
"""Set rules based on the provided dict of rules.
|
||||
|
||||
:param rules: New rules to use. It should be an instance of dict.
|
||||
:param overwrite: Whether to overwrite current rules or update them
|
||||
with the new rules.
|
||||
:param use_conf: Whether to reload rules from config file.
|
||||
"""
|
||||
|
||||
init(use_conf=False)
|
||||
_ENFORCER.set_rules(rules, overwrite, use_conf)
|
||||
|
||||
|
||||
def enforce(context, action, target, do_raise=True, exc=None):
|
||||
"""Verifies that the action is valid on the target in this context.
|
||||
|
||||
:param context: masakari context
|
||||
:param action: string representing the action to be checked
|
||||
this should be colon separated for clarity.
|
||||
:param target: 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}``
|
||||
:param do_raise: if True (the default), raises PolicyNotAuthorized;
|
||||
if False, returns False
|
||||
|
||||
:raises masakari.exception.PolicyNotAuthorized: if verification fails
|
||||
and do_raise is True.
|
||||
|
||||
:return: returns a non-False value (not necessarily "True") if
|
||||
authorized, and the exact value False if not authorized and
|
||||
do_raise is False.
|
||||
"""
|
||||
init()
|
||||
credentials = context.to_dict()
|
||||
if not exc:
|
||||
exc = exception.PolicyNotAuthorized
|
||||
try:
|
||||
result = _ENFORCER.enforce(action, target, credentials,
|
||||
do_raise=do_raise, exc=exc, action=action)
|
||||
except Exception:
|
||||
credentials.pop('auth_token', None)
|
||||
with excutils.save_and_reraise_exception():
|
||||
LOG.debug('Policy check for %(action)s failed with credentials '
|
||||
'%(credentials)s',
|
||||
{'action': action, 'credentials': credentials})
|
||||
return result
|
||||
|
||||
|
||||
def check_is_admin(context):
|
||||
"""Whether or not roles contains 'admin' role according to policy setting.
|
||||
|
||||
"""
|
||||
|
||||
init()
|
||||
# the target is user-self
|
||||
credentials = context.to_dict()
|
||||
target = credentials
|
||||
return _ENFORCER.enforce('context_is_admin', target, credentials)
|
||||
|
||||
|
||||
@policy.register('is_admin')
|
||||
class IsAdminCheck(policy.Check):
|
||||
"""An explicit check for is_admin."""
|
||||
|
||||
def __init__(self, kind, match):
|
||||
"""Initialize the check."""
|
||||
|
||||
self.expected = (match.lower() == 'true')
|
||||
|
||||
super(IsAdminCheck, self).__init__(kind, str(self.expected))
|
||||
|
||||
def __call__(self, target, creds, enforcer):
|
||||
"""Determine whether is_admin matches the requested value."""
|
||||
|
||||
return creds['is_admin'] == self.expected
|
||||
|
||||
|
||||
def get_rules():
|
||||
if _ENFORCER:
|
||||
return _ENFORCER.rules
|
37
masakari/safe_utils.py
Normal file
37
masakari/safe_utils.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
"""Utilities and helper functions that won't produce circular imports."""
|
||||
|
||||
|
||||
def get_wrapped_function(function):
|
||||
"""Get the method at the bottom of a stack of decorators."""
|
||||
if not hasattr(function, '__closure__') or not function.__closure__:
|
||||
return function
|
||||
|
||||
def _get_wrapped_function(function):
|
||||
if not hasattr(function, '__closure__') or not function.__closure__:
|
||||
return None
|
||||
|
||||
for closure in function.__closure__:
|
||||
func = closure.cell_contents
|
||||
|
||||
deeper_func = _get_wrapped_function(func)
|
||||
if deeper_func:
|
||||
return deeper_func
|
||||
elif hasattr(closure.cell_contents, '__call__'):
|
||||
return closure.cell_contents
|
||||
|
||||
return _get_wrapped_function(function)
|
127
masakari/service.py
Normal file
127
masakari/service.py
Normal file
@ -0,0 +1,127 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
"""Generic Node base class for all workers that run on hosts."""
|
||||
|
||||
from oslo_concurrency import processutils
|
||||
from oslo_log import log as logging
|
||||
from oslo_service import service
|
||||
|
||||
|
||||
import masakari.conf
|
||||
from masakari import exception
|
||||
from masakari.i18n import _
|
||||
from masakari import wsgi
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
CONF = masakari.conf.CONF
|
||||
|
||||
|
||||
class WSGIService(service.Service):
|
||||
"""Provides ability to launch API from a 'paste' configuration."""
|
||||
|
||||
def __init__(self, name, loader=None, use_ssl=False, max_url_len=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.binary = 'masakari-%s' % name
|
||||
self.topic = None
|
||||
self.loader = loader or wsgi.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)s is invalid, "
|
||||
"must be greater than 0") %
|
||||
{'worker_name': worker_name,
|
||||
'workers': str(self.workers)})
|
||||
raise exception.InvalidInput(msg)
|
||||
|
||||
self.use_ssl = use_ssl
|
||||
self.server = wsgi.Server(name,
|
||||
self.app,
|
||||
host=self.host,
|
||||
port=self.port,
|
||||
use_ssl=self.use_ssl,
|
||||
max_url_len=max_url_len)
|
||||
|
||||
def reset(self):
|
||||
"""Reset server greenpool size to default.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
self.server.reset()
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
self.server.start()
|
||||
|
||||
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 process_launcher():
|
||||
return service.ProcessLauncher(CONF)
|
||||
|
||||
|
||||
# NOTE: 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():
|
||||
_launcher.wait()
|
@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2010-2011 OpenStack Foundation
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2016 NTT Data.
|
||||
#
|
||||
# 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
|
||||
|
213
masakari/utils.py
Normal file
213
masakari/utils.py
Normal file
@ -0,0 +1,213 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
"""Utilities and helper functions."""
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import pyclbr
|
||||
import sys
|
||||
|
||||
import eventlet
|
||||
from oslo_context import context as common_context
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import timeutils
|
||||
import six
|
||||
|
||||
import masakari.conf
|
||||
from masakari import safe_utils
|
||||
|
||||
|
||||
CONF = masakari.conf.CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def utf8(value):
|
||||
"""Try to turn a string into utf-8 if possible.
|
||||
|
||||
The original code was copied from the utf8 function in
|
||||
http://github.com/facebook/tornado/blob/master/tornado/escape.py
|
||||
|
||||
"""
|
||||
if value is None or isinstance(value, six.binary_type):
|
||||
return value
|
||||
|
||||
if not isinstance(value, six.text_type):
|
||||
value = six.text_type(value)
|
||||
|
||||
return value.encode('utf-8')
|
||||
|
||||
|
||||
def monkey_patch():
|
||||
"""If the CONF.monkey_patch set as True,
|
||||
this function patches a decorator
|
||||
for all functions in specified modules.
|
||||
You can set decorators for each modules
|
||||
using CONF.monkey_patch_modules.
|
||||
The format is "Module path:Decorator function".
|
||||
|
||||
name - name of the function
|
||||
function - object of the function
|
||||
"""
|
||||
# If CONF.monkey_patch is not True, this function do nothing.
|
||||
if not CONF.monkey_patch:
|
||||
return
|
||||
if six.PY2:
|
||||
is_method = inspect.ismethod
|
||||
else:
|
||||
def is_method(obj):
|
||||
# Unbound methods became regular functions on Python 3
|
||||
return inspect.ismethod(obj) or inspect.isfunction(obj)
|
||||
# Get list of modules and decorators
|
||||
for module_and_decorator in CONF.monkey_patch_modules:
|
||||
module, decorator_name = module_and_decorator.split(':')
|
||||
# import decorator function
|
||||
decorator = importutils.import_class(decorator_name)
|
||||
__import__(module)
|
||||
# Retrieve module information using pyclbr
|
||||
module_data = pyclbr.readmodule_ex(module)
|
||||
for key, value in module_data.items():
|
||||
# set the decorator for the class methods
|
||||
if isinstance(value, pyclbr.Class):
|
||||
clz = importutils.import_class("%s.%s" % (module, key))
|
||||
for method, func in inspect.getmembers(clz, is_method):
|
||||
setattr(clz, method,
|
||||
decorator("%s.%s.%s" % (module, key,
|
||||
method), func))
|
||||
# set the decorator for the function
|
||||
if isinstance(value, pyclbr.Function):
|
||||
func = importutils.import_class("%s.%s" % (module, key))
|
||||
setattr(sys.modules[module], key,
|
||||
decorator("%s.%s" % (module, key), func))
|
||||
|
||||
|
||||
def walk_class_hierarchy(clazz, encountered=None):
|
||||
"""Walk class hierarchy, yielding most derived classes first."""
|
||||
if not encountered:
|
||||
encountered = []
|
||||
for subclass in clazz.__subclasses__():
|
||||
if subclass not in encountered:
|
||||
encountered.append(subclass)
|
||||
# drill down to leaves first
|
||||
for subsubclass in walk_class_hierarchy(subclass, encountered):
|
||||
yield subsubclass
|
||||
yield subclass
|
||||
|
||||
|
||||
def expects_func_args(*args):
|
||||
def _decorator_checker(dec):
|
||||
@functools.wraps(dec)
|
||||
def _decorator(f):
|
||||
base_f = safe_utils.get_wrapped_function(f)
|
||||
arg_names, a, kw, _default = inspect.getargspec(base_f)
|
||||
if a or kw or set(args) <= set(arg_names):
|
||||
# NOTE : We can't really tell if correct stuff will
|
||||
# be passed if it's a function with *args or **kwargs so
|
||||
# we still carry on and hope for the best
|
||||
return dec(f)
|
||||
else:
|
||||
raise TypeError("Decorated function %(f_name)s does not "
|
||||
"have the arguments expected by the "
|
||||
"decorator %(d_name)s" %
|
||||
{'f_name': base_f.__name__,
|
||||
'd_name': dec.__name__})
|
||||
return _decorator
|
||||
return _decorator_checker
|
||||
|
||||
|
||||
def isotime(at=None):
|
||||
"""Current time as ISO string,
|
||||
as timeutils.isotime() is deprecated
|
||||
|
||||
:returns: Current time in ISO format
|
||||
"""
|
||||
if not at:
|
||||
at = timeutils.utcnow()
|
||||
date_string = at.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC'
|
||||
date_string += ('Z' if tz == 'UTC' else tz)
|
||||
return date_string
|
||||
|
||||
|
||||
def strtime(at):
|
||||
return at.strftime("%Y-%m-%dT%H:%M:%S.%f")
|
||||
|
||||
|
||||
class ExceptionHelper(object):
|
||||
"""Class to wrap another and translate the ClientExceptions raised by its
|
||||
function calls to the actual ones.
|
||||
"""
|
||||
|
||||
def __init__(self, target):
|
||||
self._target = target
|
||||
|
||||
def __getattr__(self, name):
|
||||
func = getattr(self._target, name)
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
six.reraise(*e.exc_info)
|
||||
return wrapper
|
||||
|
||||
|
||||
def spawn(func, *args, **kwargs):
|
||||
"""Passthrough method for eventlet.spawn.
|
||||
|
||||
This utility exists so that it can be stubbed for testing without
|
||||
interfering with the service spawns.
|
||||
|
||||
It will also grab the context from the threadlocal store and add it to
|
||||
the store on the new thread. This allows for continuity in logging the
|
||||
context when using this method to spawn a new thread.
|
||||
"""
|
||||
_context = common_context.get_current()
|
||||
|
||||
@functools.wraps(func)
|
||||
def context_wrapper(*args, **kwargs):
|
||||
# NOTE: If update_store is not called after spawn it won't be
|
||||
# available for the logger to pull from threadlocal storage.
|
||||
if _context is not None:
|
||||
_context.update_store()
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return eventlet.spawn(context_wrapper, *args, **kwargs)
|
||||
|
||||
|
||||
def spawn_n(func, *args, **kwargs):
|
||||
"""Passthrough method for eventlet.spawn_n.
|
||||
|
||||
This utility exists so that it can be stubbed for testing without
|
||||
interfering with the service spawns.
|
||||
|
||||
It will also grab the context from the threadlocal store and add it to
|
||||
the store on the new thread. This allows for continuity in logging the
|
||||
context when using this method to spawn a new thread.
|
||||
"""
|
||||
_context = common_context.get_current()
|
||||
|
||||
@functools.wraps(func)
|
||||
def context_wrapper(*args, **kwargs):
|
||||
# NOTE: If update_store is not called after spawn_n it won't be
|
||||
# available for the logger to pull from threadlocal storage.
|
||||
if _context is not None:
|
||||
_context.update_store()
|
||||
func(*args, **kwargs)
|
||||
|
||||
eventlet.spawn_n(context_wrapper, *args, **kwargs)
|
24
masakari/version.py
Normal file
24
masakari/version.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Copyright 2016 NTT DATA
|
||||
# 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.
|
||||
|
||||
from pbr import version as pbr_version
|
||||
|
||||
MASAKARI_VENDOR = "OpenStack Foundation"
|
||||
MASAKARI_PRODUCT = "OpenStack Masakari"
|
||||
MASAKARI_PACKAGE = None # OS distro package version suffix
|
||||
|
||||
loaded = False
|
||||
version_info = pbr_version.VersionInfo('masakari')
|
||||
version_string = version_info.version_string
|
497
masakari/wsgi.py
Normal file
497
masakari/wsgi.py
Normal file
@ -0,0 +1,497 @@
|
||||
# Copyright (c) 2016 NTT DATA
|
||||
#
|
||||
# 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."""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import os.path
|
||||
import socket
|
||||
import ssl
|
||||
import sys
|
||||
|
||||
import eventlet
|
||||
import eventlet.wsgi
|
||||
import greenlet
|
||||
from oslo_log import log as logging
|
||||
from oslo_service import service
|
||||
from oslo_utils import excutils
|
||||
from paste import deploy
|
||||
import routes.middleware
|
||||
import six
|
||||
import webob.dec
|
||||
import webob.exc
|
||||
|
||||
import masakari.conf
|
||||
from masakari import exception
|
||||
from masakari.i18n import _
|
||||
from masakari.i18n import _LE
|
||||
from masakari.i18n import _LI
|
||||
from masakari import utils
|
||||
|
||||
CONF = masakari.conf.CONF
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Server(service.ServiceBase):
|
||||
"""Server class to manage a WSGI server, serving a WSGI application."""
|
||||
|
||||
default_pool_size = CONF.wsgi.default_pool_size
|
||||
|
||||
def __init__(self, name, app, host='0.0.0.0', port=0, pool_size=None,
|
||||
protocol=eventlet.wsgi.HttpProtocol, backlog=128,
|
||||
use_ssl=False, max_url_len=None):
|
||||
"""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.
|
||||
:param backlog: Maximum number of queued connections.
|
||||
:param max_url_len: Maximum length of permitted URLs.
|
||||
:returns: None
|
||||
:raises: masakari.exception.InvalidInput
|
||||
"""
|
||||
# Allow operators to customize http requests max header line size.
|
||||
eventlet.wsgi.MAX_HEADER_LINE = CONF.wsgi.max_header_line
|
||||
self.name = name
|
||||
self.app = app
|
||||
self._server = 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("masakari.%s.wsgi.server" % self.name)
|
||||
self._use_ssl = use_ssl
|
||||
self._max_url_len = max_url_len
|
||||
|
||||
self.client_socket_timeout = CONF.wsgi.client_socket_timeout or None
|
||||
|
||||
if backlog < 1:
|
||||
raise exception.InvalidInput(
|
||||
reason=_('The backlog must be more than 0'))
|
||||
|
||||
bind_addr = (host, port)
|
||||
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
|
||||
|
||||
try:
|
||||
self._socket = eventlet.listen(bind_addr, family, backlog=backlog)
|
||||
except EnvironmentError:
|
||||
LOG.error(_LE("Could not bind to %(host)s:%(port)s"),
|
||||
{'host': host, 'port': port})
|
||||
raise
|
||||
|
||||
(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
|
||||
"""
|
||||
# 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)
|
||||
# sockets can hang around forever without keepalive
|
||||
dup_socket.setsockopt(socket.SOL_SOCKET,
|
||||
socket.SO_KEEPALIVE, 1)
|
||||
|
||||
# This option isn't available in the OS X version of eventlet
|
||||
if hasattr(socket, 'TCP_KEEPIDLE'):
|
||||
dup_socket.setsockopt(socket.IPPROTO_TCP,
|
||||
socket.TCP_KEEPIDLE,
|
||||
CONF.wsgi.tcp_keepidle)
|
||||
|
||||
if self._use_ssl:
|
||||
try:
|
||||
ca_file = CONF.wsgi.ssl_ca_file
|
||||
cert_file = CONF.wsgi.ssl_cert_file
|
||||
key_file = CONF.wsgi.ssl_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"))
|
||||
ssl_kwargs = {
|
||||
'server_side': True,
|
||||
'certfile': cert_file,
|
||||
'keyfile': key_file,
|
||||
'cert_reqs': ssl.CERT_NONE,
|
||||
}
|
||||
|
||||
if CONF.wsgi.ssl_ca_file:
|
||||
ssl_kwargs['ca_certs'] = ca_file
|
||||
ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED
|
||||
|
||||
dup_socket = eventlet.wrap_ssl(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"),
|
||||
{'name': self.name, 'host': self.host,
|
||||
'port': self.port})
|
||||
|
||||
wsgi_kwargs = {
|
||||
'func': eventlet.wsgi.server,
|
||||
'sock': dup_socket,
|
||||
'site': self.app,
|
||||
'protocol': self._protocol,
|
||||
'custom_pool': self._pool,
|
||||
'log': self._logger,
|
||||
'log_format': CONF.wsgi.wsgi_log_format,
|
||||
'debug': False,
|
||||
'keepalive': CONF.wsgi.keep_alive,
|
||||
'socket_timeout': self.client_socket_timeout
|
||||
}
|
||||
|
||||
if self._max_url_len:
|
||||
wsgi_kwargs['url_length_limit'] = self._max_url_len
|
||||
|
||||
self._server = utils.spawn(**wsgi_kwargs)
|
||||
|
||||
def reset(self):
|
||||
"""Reset server greenpool size to default.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
self._pool.resize(self.pool_size)
|
||||
|
||||
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."))
|
||||
|
||||
|
||||
class Request(webob.Request):
|
||||
def __init__(self, environ, *args, **kwargs):
|
||||
if CONF.wsgi.secure_proxy_ssl_header:
|
||||
scheme = environ.get(CONF.wsgi.secure_proxy_ssl_header)
|
||||
if scheme:
|
||||
environ['wsgi.url_scheme'] = scheme
|
||||
super(Request, self).__init__(environ, *args, **kwargs)
|
||||
|
||||
|
||||
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 = masakari.api.fancy_api:Wadl.factory
|
||||
|
||||
which would result in a call to the `Wadl` class as
|
||||
|
||||
import masakari.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):
|
||||
r"""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, or ...)
|
||||
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 = masakari.api.analytics:Analytics.factory
|
||||
|
||||
which would result in a call to the `Analytics` class as
|
||||
|
||||
import masakari.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')
|
||||
for key, value in req.environ.items():
|
||||
print(key, '=', value)
|
||||
print()
|
||||
resp = req.get_response(self.application)
|
||||
|
||||
print(('*' * 40) + ' RESPONSE HEADERS')
|
||||
for (key, value) in six.iteritems(resp.headers):
|
||||
print(key, '=', value)
|
||||
print()
|
||||
|
||||
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')
|
||||
for part in app_iter:
|
||||
sys.stdout.write(part)
|
||||
sys.stdout.flush()
|
||||
yield part
|
||||
print()
|
||||
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
self.config_path = None
|
||||
|
||||
config_path = config_path or CONF.wsgi.api_paste_config
|
||||
if not os.path.isabs(config_path):
|
||||
self.config_path = CONF.find_file(config_path)
|
||||
elif os.path.exists(config_path):
|
||||
self.config_path = config_path
|
||||
|
||||
if not self.config_path:
|
||||
raise exception.ConfigNotFound(path=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: `masakari.exception.PasteAppNotFound`
|
||||
|
||||
"""
|
||||
try:
|
||||
LOG.debug("Loading app %(name)s from %(path)s",
|
||||
{'name': name, 'path': self.config_path})
|
||||
return deploy.loadapp("config:%s" % self.config_path, name=name)
|
||||
except LookupError:
|
||||
LOG.exception(_LE("Couldn't lookup app: %s"), name)
|
||||
raise exception.PasteAppNotFound(name=name, path=self.config_path)
|
@ -2,4 +2,18 @@
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
pbr>=1.6
|
||||
Babel>=2.3.4 # BSD
|
||||
iso8601>=0.1.11 # MIT
|
||||
microversion-parse>=0.1.2 # Apache-2.0
|
||||
oslo.config>=3.10.0 # Apache-2.0
|
||||
oslo.db>=4.1.0 # Apache-2.0
|
||||
oslo.i18n>=2.1.0 # Apache-2.0
|
||||
oslo.log>=1.14.0 # Apache-2.0
|
||||
oslo.middleware>=3.0.0 # Apache-2.0
|
||||
oslo.policy>=0.5.0 # Apache-2.0
|
||||
oslo.service>=1.10.0 # Apache-2.0
|
||||
oslo.utils>=3.11.0 # Apache-2.0
|
||||
pbr>=1.6 # Apache-2.0
|
||||
setuptools>=16.0 # PSF/ZPL
|
||||
six>=1.9.0 # MIT
|
||||
stevedore>=1.10.0 # Apache-2.0
|
||||
|
10
setup.cfg
10
setup.cfg
@ -23,6 +23,14 @@ classifier =
|
||||
packages =
|
||||
masakari
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
masakari-api = masakari.cmd.api:main
|
||||
|
||||
masakari.api.v1.extensions =
|
||||
versions = masakari.api.openstack.ha.versionsV1:Versions
|
||||
extension_info = masakari.api.openstack.ha.extension_info:ExtensionInfo
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
@ -48,4 +56,4 @@ output_file = masakari/locale/masakari.pot
|
||||
[build_releasenotes]
|
||||
all_files = 1
|
||||
build-dir = releasenotes/build
|
||||
source-dir = releasenotes/source
|
||||
source-dir = releasenotes/source
|
||||
|
2
setup.py
2
setup.py
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright (c) 2016 NTT Data.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
|
Loading…
x
Reference in New Issue
Block a user