Initial API code structure
This commit is contained in:
commit
9492a02bab
1
qinling
1
qinling
@ -1 +0,0 @@
|
|||||||
Subproject commit e089c31e73c1e313293ea794a25a30b15b4dad8e
|
|
0
qinling/__init__.py
Normal file
0
qinling/__init__.py
Normal file
0
qinling/api/__init__.py
Normal file
0
qinling/api/__init__.py
Normal file
81
qinling/api/access_control.py
Normal file
81
qinling/api/access_control.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Access Control API server."""
|
||||||
|
|
||||||
|
from keystonemiddleware import auth_token
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_policy import policy
|
||||||
|
|
||||||
|
from qinling import exceptions as exc
|
||||||
|
|
||||||
|
_ENFORCER = None
|
||||||
|
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
if cfg.CONF.pecan.auth_enable:
|
||||||
|
conf = dict(cfg.CONF.keystone_authtoken)
|
||||||
|
|
||||||
|
# Change auth decisions of requests to the app itself.
|
||||||
|
conf.update({'delay_auth_decision': True})
|
||||||
|
|
||||||
|
_ensure_enforcer_initialization()
|
||||||
|
|
||||||
|
return auth_token.AuthProtocol(app, conf)
|
||||||
|
else:
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def enforce(action, context, target=None, do_raise=True,
|
||||||
|
exc=exc.NotAllowedException):
|
||||||
|
"""Verifies that the action is valid on the target in this context.
|
||||||
|
|
||||||
|
:param action: String, representing the action to be checked.
|
||||||
|
This should be colon separated for clarity.
|
||||||
|
i.e. ``workflows:create``
|
||||||
|
:param context: Mistral context.
|
||||||
|
: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 specified exception.
|
||||||
|
:param exc: Exception to be raised if not authorized. Default is
|
||||||
|
mistral.exceptions.NotAllowedException.
|
||||||
|
|
||||||
|
:return: returns True if authorized and False if not authorized and
|
||||||
|
do_raise is False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
target_obj = {
|
||||||
|
'project_id': context.project_id,
|
||||||
|
'user_id': context.user_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
target_obj.update(target or {})
|
||||||
|
_ensure_enforcer_initialization()
|
||||||
|
|
||||||
|
return _ENFORCER.enforce(
|
||||||
|
action,
|
||||||
|
target_obj,
|
||||||
|
context.to_dict(),
|
||||||
|
do_raise=do_raise,
|
||||||
|
exc=exc
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_enforcer_initialization():
|
||||||
|
global _ENFORCER
|
||||||
|
if not _ENFORCER:
|
||||||
|
_ENFORCER = policy.Enforcer(cfg.CONF)
|
||||||
|
_ENFORCER.load_rules()
|
53
qinling/api/app.py
Normal file
53
qinling/api/app.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
import pecan
|
||||||
|
|
||||||
|
from qinling.api import access_control
|
||||||
|
from qinling import context as ctx
|
||||||
|
|
||||||
|
|
||||||
|
def get_pecan_config():
|
||||||
|
# Set up the pecan configuration.
|
||||||
|
opts = cfg.CONF.pecan
|
||||||
|
|
||||||
|
cfg_dict = {
|
||||||
|
"app": {
|
||||||
|
"root": opts.root,
|
||||||
|
"modules": opts.modules,
|
||||||
|
"debug": opts.debug,
|
||||||
|
"auth_enable": opts.auth_enable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pecan.configuration.conf_from_dict(cfg_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def setup_app(config=None):
|
||||||
|
if not config:
|
||||||
|
config = get_pecan_config()
|
||||||
|
|
||||||
|
app_conf = dict(config.app)
|
||||||
|
app = pecan.make_app(
|
||||||
|
app_conf.pop('root'),
|
||||||
|
hooks=lambda: [ctx.ContextHook(), ctx.AuthHook()],
|
||||||
|
logging=getattr(config, 'logging', {}),
|
||||||
|
**app_conf
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set up access control.
|
||||||
|
app = access_control.setup(app)
|
||||||
|
|
||||||
|
return app
|
0
qinling/api/controllers/__init__.py
Normal file
0
qinling/api/controllers/__init__.py
Normal file
78
qinling/api/controllers/root.py
Normal file
78
qinling/api/controllers/root.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
# Copyright 2013 - Mirantis, Inc.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
import pecan
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from qinling.api.controllers.v1 import resources
|
||||||
|
from qinling.api.controllers.v1 import root as v1_root
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
API_STATUS = wtypes.Enum(str, 'SUPPORTED', 'CURRENT', 'DEPRECATED')
|
||||||
|
|
||||||
|
|
||||||
|
class APIVersion(resources.Resource):
|
||||||
|
"""An API Version."""
|
||||||
|
|
||||||
|
id = wtypes.text
|
||||||
|
"The version identifier."
|
||||||
|
|
||||||
|
status = API_STATUS
|
||||||
|
"The status of the API (SUPPORTED, CURRENT or DEPRECATED)."
|
||||||
|
|
||||||
|
links = wtypes.ArrayType(resources.Link)
|
||||||
|
"The link to the versioned API."
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
return cls(
|
||||||
|
id='v1.0',
|
||||||
|
status='CURRENT',
|
||||||
|
links=[
|
||||||
|
resources.Link(target_name='v1', rel="self",
|
||||||
|
href='http://example.com:7070/v1')
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class APIVersions(resources.Resource):
|
||||||
|
"""API Versions."""
|
||||||
|
versions = wtypes.ArrayType(APIVersion)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
v1 = APIVersion(id='v1.0', status='CURRENT', rel="self",
|
||||||
|
href='http://example.com:7070/v1')
|
||||||
|
return cls(versions=[v1])
|
||||||
|
|
||||||
|
|
||||||
|
class RootController(object):
|
||||||
|
v1 = v1_root.Controller()
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(APIVersions)
|
||||||
|
def index(self):
|
||||||
|
LOG.info("Fetching API versions.")
|
||||||
|
|
||||||
|
host_url_v1 = '%s/%s' % (pecan.request.host_url, 'v1')
|
||||||
|
api_v1 = APIVersion(
|
||||||
|
id='v1.0',
|
||||||
|
status='CURRENT',
|
||||||
|
links=[resources.Link(href=host_url_v1, target='v1',
|
||||||
|
rel="self", )]
|
||||||
|
)
|
||||||
|
|
||||||
|
return APIVersions(versions=[api_v1])
|
0
qinling/api/controllers/v1/__init__.py
Normal file
0
qinling/api/controllers/v1/__init__.py
Normal file
67
qinling/api/controllers/v1/function.py
Normal file
67
qinling/api/controllers/v1/function.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# 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 json
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
import pecan
|
||||||
|
from pecan import rest
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from qinling.api.controllers.v1 import resources
|
||||||
|
from qinling import exceptions as exc
|
||||||
|
from qinling.utils import rest_utils
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
POST_REQUIRED = set(['name', 'runtime', 'code'])
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionsController(rest.RestController):
|
||||||
|
@rest_utils.wrap_pecan_controller_exception
|
||||||
|
@pecan.expose()
|
||||||
|
def post(self, **kwargs):
|
||||||
|
"""Create a new function.
|
||||||
|
|
||||||
|
:param func: Function object.
|
||||||
|
"""
|
||||||
|
LOG.info("Create function, params=%s", kwargs)
|
||||||
|
|
||||||
|
if not POST_REQUIRED.issubset(set(kwargs.keys())):
|
||||||
|
raise exc.InputException(
|
||||||
|
'Required param is missing. Required: %s' % POST_REQUIRED
|
||||||
|
)
|
||||||
|
|
||||||
|
func = resources.Function()
|
||||||
|
|
||||||
|
func.name = kwargs['name']
|
||||||
|
func.runtime = kwargs['runtime']
|
||||||
|
func.code = json.loads(kwargs['code'])
|
||||||
|
|
||||||
|
if func.code.get('package', False):
|
||||||
|
data = kwargs['package'].file.read()
|
||||||
|
print data
|
||||||
|
|
||||||
|
pecan.response.status = 201
|
||||||
|
return func.to_json()
|
||||||
|
|
||||||
|
@rest_utils.wrap_wsme_controller_exception
|
||||||
|
@wsme_pecan.wsexpose(resources.Functions)
|
||||||
|
def get_all(self):
|
||||||
|
LOG.info("Get all functions.")
|
||||||
|
|
||||||
|
funcs = resources.Functions()
|
||||||
|
funcs.functions = []
|
||||||
|
|
||||||
|
return funcs
|
213
qinling/api/controllers/v1/resources.py
Normal file
213
qinling/api/controllers/v1/resources.py
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# 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 json
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from qinling.api.controllers.v1 import types
|
||||||
|
|
||||||
|
PROVIDER_TYPES = wtypes.Enum(str, 'docker', 'fission')
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(wtypes.Base):
|
||||||
|
"""REST API Resource."""
|
||||||
|
|
||||||
|
_wsme_attributes = []
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
d = {}
|
||||||
|
|
||||||
|
for attr in self._wsme_attributes:
|
||||||
|
attr_val = getattr(self, attr.name)
|
||||||
|
if not isinstance(attr_val, wtypes.UnsetType):
|
||||||
|
d[attr.name] = attr_val
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, d):
|
||||||
|
obj = cls()
|
||||||
|
|
||||||
|
for key, val in d.items():
|
||||||
|
if hasattr(obj, key):
|
||||||
|
setattr(obj, key, val)
|
||||||
|
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""WSME based implementation of __str__."""
|
||||||
|
|
||||||
|
res = "%s [" % type(self).__name__
|
||||||
|
|
||||||
|
first = True
|
||||||
|
for attr in self._wsme_attributes:
|
||||||
|
if not first:
|
||||||
|
res += ', '
|
||||||
|
else:
|
||||||
|
first = False
|
||||||
|
|
||||||
|
res += "%s='%s'" % (attr.name, getattr(self, attr.name))
|
||||||
|
|
||||||
|
return res + "]"
|
||||||
|
|
||||||
|
def to_json(self):
|
||||||
|
return json.dumps(self.to_dict())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_fields(cls):
|
||||||
|
obj = cls()
|
||||||
|
|
||||||
|
return [attr.name for attr in obj._wsme_attributes]
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceList(Resource):
|
||||||
|
"""Resource containing the list of other resources."""
|
||||||
|
|
||||||
|
next = wtypes.text
|
||||||
|
"""A link to retrieve the next subset of the resource list"""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def collection(self):
|
||||||
|
return getattr(self, self._type)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def convert_with_links(cls, resources, limit, url=None, fields=None,
|
||||||
|
**kwargs):
|
||||||
|
resource_collection = cls()
|
||||||
|
|
||||||
|
setattr(resource_collection, resource_collection._type, resources)
|
||||||
|
|
||||||
|
resource_collection.next = resource_collection.get_next(
|
||||||
|
limit,
|
||||||
|
url=url,
|
||||||
|
fields=fields,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
return resource_collection
|
||||||
|
|
||||||
|
def has_next(self, limit):
|
||||||
|
"""Return whether resources has more items."""
|
||||||
|
return len(self.collection) and len(self.collection) == limit
|
||||||
|
|
||||||
|
def get_next(self, limit, url=None, fields=None, **kwargs):
|
||||||
|
"""Return a link to the next subset of the resources."""
|
||||||
|
if not self.has_next(limit):
|
||||||
|
return wtypes.Unset
|
||||||
|
|
||||||
|
q_args = ''.join(
|
||||||
|
['%s=%s&' % (key, value) for key, value in kwargs.items()]
|
||||||
|
)
|
||||||
|
|
||||||
|
resource_args = (
|
||||||
|
'?%(args)slimit=%(limit)d&marker=%(marker)s' %
|
||||||
|
{
|
||||||
|
'args': q_args,
|
||||||
|
'limit': limit,
|
||||||
|
'marker': self.collection[-1].id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fields is handled specially here, we can move it above when it's
|
||||||
|
# supported by all resources query.
|
||||||
|
if fields:
|
||||||
|
resource_args += '&fields=%s' % fields
|
||||||
|
|
||||||
|
next_link = "%(host_url)s/v2/%(resource)s%(args)s" % {
|
||||||
|
'host_url': url,
|
||||||
|
'resource': self._type,
|
||||||
|
'args': resource_args
|
||||||
|
}
|
||||||
|
|
||||||
|
return next_link
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
d = {}
|
||||||
|
|
||||||
|
for attr in self._wsme_attributes:
|
||||||
|
attr_val = getattr(self, attr.name)
|
||||||
|
|
||||||
|
if isinstance(attr_val, list):
|
||||||
|
if isinstance(attr_val[0], Resource):
|
||||||
|
d[attr.name] = [v.to_dict() for v in attr_val]
|
||||||
|
elif not isinstance(attr_val, wtypes.UnsetType):
|
||||||
|
d[attr.name] = attr_val
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
class Link(Resource):
|
||||||
|
"""Web link."""
|
||||||
|
|
||||||
|
href = wtypes.text
|
||||||
|
target = wtypes.text
|
||||||
|
rel = wtypes.text
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
return cls(href='http://example.com/here',
|
||||||
|
target='here', rel='self')
|
||||||
|
|
||||||
|
|
||||||
|
class Function(Resource):
|
||||||
|
"""Function resource."""
|
||||||
|
|
||||||
|
id = wtypes.text
|
||||||
|
name = wtypes.text
|
||||||
|
description = wtypes.text
|
||||||
|
memorysize = int
|
||||||
|
timeout = int
|
||||||
|
runtime = wtypes.text
|
||||||
|
code = types.jsontype
|
||||||
|
provider = PROVIDER_TYPES
|
||||||
|
created_at = wtypes.text
|
||||||
|
updated_at = wtypes.text
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
return cls(
|
||||||
|
id='123e4567-e89b-12d3-a456-426655440000',
|
||||||
|
name='hello_world',
|
||||||
|
description='this is the first function.',
|
||||||
|
memorysize=1,
|
||||||
|
timeout=1,
|
||||||
|
runtime='python2.7',
|
||||||
|
code={'zip': True},
|
||||||
|
provider='docker',
|
||||||
|
created_at='1970-01-01T00:00:00.000000',
|
||||||
|
updated_at='1970-01-01T00:00:00.000000'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Functions(ResourceList):
|
||||||
|
"""A collection of Function resources."""
|
||||||
|
|
||||||
|
functions = [Function]
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self._type = 'functions'
|
||||||
|
|
||||||
|
super(Functions, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sample(cls):
|
||||||
|
sample = cls()
|
||||||
|
sample.functions = [Function.sample()]
|
||||||
|
sample.next = (
|
||||||
|
"http://localhost:7070/v1/functions?"
|
||||||
|
"sort_keys=id,name&sort_dirs=asc,desc&limit=10&"
|
||||||
|
"marker=123e4567-e89b-12d3-a456-426655440000"
|
||||||
|
)
|
||||||
|
|
||||||
|
return sample
|
39
qinling/api/controllers/v1/root.py
Normal file
39
qinling/api/controllers/v1/root.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import pecan
|
||||||
|
from wsme import types as wtypes
|
||||||
|
import wsmeext.pecan as wsme_pecan
|
||||||
|
|
||||||
|
from qinling.api.controllers.v1 import function
|
||||||
|
from qinling.api.controllers.v1 import resources
|
||||||
|
|
||||||
|
|
||||||
|
class RootResource(resources.Resource):
|
||||||
|
"""Root resource for API version 1.
|
||||||
|
|
||||||
|
It references all other resources belonging to the API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
uri = wtypes.text
|
||||||
|
|
||||||
|
|
||||||
|
class Controller(object):
|
||||||
|
"""API root controller for version 1."""
|
||||||
|
|
||||||
|
functions = function.FunctionsController()
|
||||||
|
|
||||||
|
@wsme_pecan.wsexpose(RootResource)
|
||||||
|
def index(self):
|
||||||
|
return RootResource(uri='%s/%s' % (pecan.request.host_url, 'v1'))
|
125
qinling/api/controllers/v1/types.py
Normal file
125
qinling/api/controllers/v1/types.py
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# 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 json
|
||||||
|
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
import six
|
||||||
|
from wsme import types as wtypes
|
||||||
|
|
||||||
|
from qinling import exceptions as exc
|
||||||
|
|
||||||
|
|
||||||
|
class ListType(wtypes.UserType):
|
||||||
|
"""A simple list type."""
|
||||||
|
|
||||||
|
basetype = wtypes.text
|
||||||
|
name = 'list'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value):
|
||||||
|
"""Validate and convert the input to a ListType.
|
||||||
|
|
||||||
|
:param value: A comma separated string of values
|
||||||
|
:returns: A list of values.
|
||||||
|
"""
|
||||||
|
items = [v.strip().lower() for v in six.text_type(value).split(',')]
|
||||||
|
|
||||||
|
# remove empty items.
|
||||||
|
return [x for x in items if x]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def frombasetype(value):
|
||||||
|
return ListType.validate(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueListType(ListType):
|
||||||
|
"""A simple list type with no duplicate items."""
|
||||||
|
|
||||||
|
name = 'uniquelist'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value):
|
||||||
|
"""Validate and convert the input to a UniqueListType.
|
||||||
|
|
||||||
|
:param value: A comma separated string of values.
|
||||||
|
:returns: A list with no duplicate items.
|
||||||
|
"""
|
||||||
|
items = ListType.validate(value)
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
return [x for x in items if not (x in seen or seen.add(x))]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def frombasetype(value):
|
||||||
|
return UniqueListType.validate(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
class UuidType(wtypes.UserType):
|
||||||
|
"""A simple UUID type.
|
||||||
|
|
||||||
|
The builtin UuidType class in wsme.types doesn't work properly with pecan.
|
||||||
|
"""
|
||||||
|
|
||||||
|
basetype = wtypes.text
|
||||||
|
name = 'uuid'
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value):
|
||||||
|
if not uuidutils.is_uuid_like(value):
|
||||||
|
raise exc.InputException(
|
||||||
|
"Expected a uuid but received %s." % value
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def frombasetype(value):
|
||||||
|
return UuidType.validate(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
class JsonType(wtypes.UserType):
|
||||||
|
"""A simple JSON type."""
|
||||||
|
|
||||||
|
basetype = wtypes.text
|
||||||
|
name = 'json'
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
if not value:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
raise exc.InputException(
|
||||||
|
'JsonType field value must be a dictionary [actual=%s]' % value
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def frombasetype(self, value):
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return value
|
||||||
|
try:
|
||||||
|
return json.loads(value) if value is not None else None
|
||||||
|
except TypeError as e:
|
||||||
|
raise ValueError(e)
|
||||||
|
|
||||||
|
def tobasetype(self, value):
|
||||||
|
# Value must be a dict.
|
||||||
|
return json.dumps(value) if value is not None else None
|
||||||
|
|
||||||
|
|
||||||
|
uuid = UuidType()
|
||||||
|
list = ListType()
|
||||||
|
uniquelist = UniqueListType()
|
||||||
|
jsontype = JsonType()
|
52
qinling/api/service.py
Normal file
52
qinling/api/service.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# 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_concurrency import processutils
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_service import service
|
||||||
|
from oslo_service import wsgi
|
||||||
|
|
||||||
|
from qinling.api import app
|
||||||
|
|
||||||
|
|
||||||
|
class WSGIService(service.ServiceBase):
|
||||||
|
"""Provides ability to launch Mistral API from wsgi app."""
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
self.app = app.setup_app()
|
||||||
|
self.workers = (
|
||||||
|
cfg.CONF.api.api_workers or processutils.get_worker_count()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.server = wsgi.Server(
|
||||||
|
cfg.CONF,
|
||||||
|
name,
|
||||||
|
self.app,
|
||||||
|
host=cfg.CONF.api.host,
|
||||||
|
port=cfg.CONF.api.port,
|
||||||
|
use_ssl=cfg.CONF.api.enable_ssl_api
|
||||||
|
)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.server.start()
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.server.stop()
|
||||||
|
|
||||||
|
def wait(self):
|
||||||
|
self.server.wait()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.server.reset()
|
0
qinling/cmd/__init__.py
Normal file
0
qinling/cmd/__init__.py
Normal file
150
qinling/cmd/launch.py
Normal file
150
qinling/cmd/launch.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Copyright 2017 - Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import eventlet
|
||||||
|
|
||||||
|
eventlet.monkey_patch(
|
||||||
|
os=True,
|
||||||
|
select=True,
|
||||||
|
socket=True,
|
||||||
|
thread=False if '--use-debugger' in sys.argv else True,
|
||||||
|
time=True)
|
||||||
|
|
||||||
|
import os # noqa
|
||||||
|
|
||||||
|
# If ../qingling/__init__.py exists, add ../ to Python search path, so that
|
||||||
|
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||||
|
POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||||
|
os.pardir,
|
||||||
|
os.pardir))
|
||||||
|
if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'qinling', '__init__.py')):
|
||||||
|
sys.path.insert(0, POSSIBLE_TOPDIR)
|
||||||
|
|
||||||
|
from oslo_config import cfg # noqa
|
||||||
|
from oslo_log import log as logging # noqa
|
||||||
|
from oslo_service import service # noqa
|
||||||
|
|
||||||
|
from qinling.api import service as api_service # noqa
|
||||||
|
from qinling import config # noqa
|
||||||
|
from qinling import version # noqa
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def launch_api():
|
||||||
|
launcher = service.ProcessLauncher(cfg.CONF)
|
||||||
|
|
||||||
|
server = api_service.WSGIService('qinling_api')
|
||||||
|
|
||||||
|
launcher.launch_service(server, workers=server.workers)
|
||||||
|
|
||||||
|
launcher.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def launch_any(options):
|
||||||
|
# Launch the servers on different threads.
|
||||||
|
threads = [eventlet.spawn(LAUNCH_OPTIONS[option])
|
||||||
|
for option in options]
|
||||||
|
|
||||||
|
[thread.wait() for thread in threads]
|
||||||
|
|
||||||
|
|
||||||
|
LAUNCH_OPTIONS = {
|
||||||
|
'api': launch_api,
|
||||||
|
}
|
||||||
|
|
||||||
|
QINLING_TITLE = r"""
|
||||||
|
/^L_ ,."\
|
||||||
|
/~\ __ /~ \ ./ \
|
||||||
|
/ _\ _/ \ /T~\|~\_\ / \_ /~| _^
|
||||||
|
/ \ /W \ / V^\/X /~ T . \/ \ ,v-./
|
||||||
|
,'`-. /~ ^ H , . \/ ; . \ `. \-' /
|
||||||
|
M ~ | . ; / , _ : . ~\_,-'
|
||||||
|
/ ~ . \ / : ' \ ,/`
|
||||||
|
I o. ^ oP '98b - _ 9.` `\9b.
|
||||||
|
8oO888. oO888P d888b9bo. .8o 888o. 8bo. o 988o.
|
||||||
|
88888888888888888888888888bo.98888888bo. 98888bo. .d888P
|
||||||
|
88888888888888888888888888888888888888888888888888888888888
|
||||||
|
_ __ _
|
||||||
|
___ _ (_) ___ / / (_) ___ ___ _
|
||||||
|
/ _ `/ / / / _ \ / / / / / _ \ / _ `/
|
||||||
|
\_, / /_/ /_//_//_/ /_/ /_//_/ \_, /
|
||||||
|
/_/ /___/
|
||||||
|
|
||||||
|
Function as a Service in OpenStack, version: %s
|
||||||
|
""" % version.version_string()
|
||||||
|
|
||||||
|
|
||||||
|
def print_server_info():
|
||||||
|
print(QINLING_TITLE)
|
||||||
|
|
||||||
|
comp_str = ("[%s]" % ','.join(LAUNCH_OPTIONS)
|
||||||
|
if cfg.CONF.server == ['all'] else cfg.CONF.server)
|
||||||
|
|
||||||
|
print('Launching server components %s...' % comp_str)
|
||||||
|
|
||||||
|
|
||||||
|
def get_properly_ordered_parameters():
|
||||||
|
"""Orders launch parameters in the right order.
|
||||||
|
|
||||||
|
In oslo it's important the order of the launch parameters.
|
||||||
|
if --config-file came after the command line parameters the command
|
||||||
|
line parameters are ignored.
|
||||||
|
So to make user command line parameters are never ignored this method
|
||||||
|
moves --config-file to be always first.
|
||||||
|
"""
|
||||||
|
args = sys.argv[1:]
|
||||||
|
|
||||||
|
for arg in sys.argv[1:]:
|
||||||
|
if arg == '--config-file' or arg.startswith('--config-file='):
|
||||||
|
if "=" in arg:
|
||||||
|
conf_file_value = arg.split("=", 1)[1]
|
||||||
|
else:
|
||||||
|
conf_file_value = args[args.index(arg) + 1]
|
||||||
|
args.remove(conf_file_value)
|
||||||
|
args.remove(arg)
|
||||||
|
args.insert(0, "--config-file")
|
||||||
|
args.insert(1, conf_file_value)
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
config.parse_args(get_properly_ordered_parameters())
|
||||||
|
print_server_info()
|
||||||
|
|
||||||
|
logging.setup(CONF, 'Qingling')
|
||||||
|
|
||||||
|
if cfg.CONF.server == ['all']:
|
||||||
|
# Launch all servers.
|
||||||
|
launch_any(LAUNCH_OPTIONS.keys())
|
||||||
|
else:
|
||||||
|
# Validate launch option.
|
||||||
|
if set(cfg.CONF.server) - set(LAUNCH_OPTIONS.keys()):
|
||||||
|
raise Exception('Valid options are all or any combination of '
|
||||||
|
', '.join(LAUNCH_OPTIONS.keys()))
|
||||||
|
|
||||||
|
# Launch distinct set of server(s).
|
||||||
|
launch_any(set(cfg.CONF.server))
|
||||||
|
|
||||||
|
except RuntimeError as excp:
|
||||||
|
sys.stderr.write("ERROR: %s\n" % excp)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
117
qinling/config.py
Normal file
117
qinling/config.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Configuration options registration and useful routines.
|
||||||
|
"""
|
||||||
|
import itertools
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log
|
||||||
|
|
||||||
|
from qinling import version
|
||||||
|
|
||||||
|
launch_opt = cfg.ListOpt(
|
||||||
|
'server',
|
||||||
|
default=['all'],
|
||||||
|
help='Specifies which qinling server to start by the launch script.'
|
||||||
|
)
|
||||||
|
|
||||||
|
api_opts = [
|
||||||
|
cfg.StrOpt('host', default='0.0.0.0', help='Qinling API server host.'),
|
||||||
|
cfg.PortOpt('port', default=7070, help='Qinling API server port.'),
|
||||||
|
cfg.BoolOpt(
|
||||||
|
'enable_ssl_api',
|
||||||
|
default=False,
|
||||||
|
help='Enable the integrated stand-alone API to service requests'
|
||||||
|
'via HTTPS instead of HTTP.'
|
||||||
|
),
|
||||||
|
cfg.IntOpt(
|
||||||
|
'api_workers',
|
||||||
|
help='Number of workers for Qinling API service '
|
||||||
|
'default is equal to the number of CPUs available if that can '
|
||||||
|
'be determined, else a default worker count of 1 is returned.'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
pecan_opts = [
|
||||||
|
cfg.StrOpt(
|
||||||
|
'root',
|
||||||
|
default='qinling.api.controllers.root.RootController',
|
||||||
|
help='Pecan root controller'
|
||||||
|
),
|
||||||
|
cfg.ListOpt(
|
||||||
|
'modules',
|
||||||
|
default=["qinling.api"],
|
||||||
|
help='A list of modules where pecan will search for applications.'
|
||||||
|
),
|
||||||
|
cfg.BoolOpt(
|
||||||
|
'debug',
|
||||||
|
default=False,
|
||||||
|
help='Enables the ability to display tracebacks in the browser and'
|
||||||
|
' interactively debug during development.'
|
||||||
|
),
|
||||||
|
cfg.BoolOpt(
|
||||||
|
'auth_enable',
|
||||||
|
default=True,
|
||||||
|
help='Enables user authentication in pecan.'
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
API_GROUP = 'api'
|
||||||
|
PECAN_GROUP = 'pecan'
|
||||||
|
CLI_OPTS = [launch_opt]
|
||||||
|
|
||||||
|
CONF.register_opts(api_opts, group=API_GROUP)
|
||||||
|
CONF.register_opts(pecan_opts, group=PECAN_GROUP)
|
||||||
|
CONF.register_cli_opts(CLI_OPTS)
|
||||||
|
|
||||||
|
default_group_opts = itertools.chain(
|
||||||
|
CLI_OPTS,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_opts():
|
||||||
|
return [
|
||||||
|
(API_GROUP, api_opts),
|
||||||
|
(PECAN_GROUP, pecan_opts),
|
||||||
|
(None, default_group_opts)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
_DEFAULT_LOG_LEVELS = [
|
||||||
|
'eventlet.wsgi.server=WARN',
|
||||||
|
'oslo_service.periodic_task=INFO',
|
||||||
|
'oslo_service.loopingcall=INFO',
|
||||||
|
'oslo_db=WARN',
|
||||||
|
'oslo_concurrency.lockutils=WARN'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args(args=None, usage=None, default_config_files=None):
|
||||||
|
default_log_levels = log.get_default_log_levels()
|
||||||
|
default_log_levels.extend(_DEFAULT_LOG_LEVELS)
|
||||||
|
log.set_defaults(default_log_levels=default_log_levels)
|
||||||
|
|
||||||
|
log.register_options(CONF)
|
||||||
|
|
||||||
|
CONF(
|
||||||
|
args=args,
|
||||||
|
project='qinling',
|
||||||
|
version=version,
|
||||||
|
usage=usage,
|
||||||
|
default_config_files=default_config_files
|
||||||
|
)
|
69
qinling/context.py
Normal file
69
qinling/context.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# 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_context import context as oslo_context
|
||||||
|
import pecan
|
||||||
|
from pecan import hooks
|
||||||
|
|
||||||
|
from qinling import exceptions as exc
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
ALLOWED_WITHOUT_AUTH = ['/', '/v1/']
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate(req):
|
||||||
|
# Refer to:
|
||||||
|
# https://docs.openstack.org/developer/keystonemiddleware/middlewarearchitecture.html#exchanging-user-information
|
||||||
|
identity_status = req.headers.get('X-Identity-Status')
|
||||||
|
service_identity_status = req.headers.get('X-Service-Identity-Status')
|
||||||
|
|
||||||
|
if (identity_status == 'Confirmed' or
|
||||||
|
service_identity_status == 'Confirmed'):
|
||||||
|
return
|
||||||
|
|
||||||
|
if req.headers.get('X-Auth-Token'):
|
||||||
|
msg = 'Auth token is invalid: %s' % req.headers['X-Auth-Token']
|
||||||
|
else:
|
||||||
|
msg = 'Authentication required'
|
||||||
|
|
||||||
|
raise exc.UnauthorizedException(msg)
|
||||||
|
|
||||||
|
|
||||||
|
class AuthHook(hooks.PecanHook):
|
||||||
|
def before(self, state):
|
||||||
|
if state.request.path in ALLOWED_WITHOUT_AUTH:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not CONF.pecan.auth_enable:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
authenticate(state.request)
|
||||||
|
except Exception as e:
|
||||||
|
msg = "Failed to validate access token: %s" % str(e)
|
||||||
|
|
||||||
|
pecan.abort(
|
||||||
|
status_code=401,
|
||||||
|
detail=msg,
|
||||||
|
headers={'Server-Error-Message': msg}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ContextHook(hooks.PecanHook):
|
||||||
|
def on_route(self, state):
|
||||||
|
context_obj = oslo_context.RequestContext.from_environ(
|
||||||
|
state.request.environ
|
||||||
|
)
|
||||||
|
state.request.context['qinling_context'] = context_obj
|
64
qinling/exceptions.py
Normal file
64
qinling/exceptions.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# 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 QinlingException(Exception):
|
||||||
|
"""Qinling specific exception.
|
||||||
|
|
||||||
|
Reserved for situations that are not critical for program continuation.
|
||||||
|
It is possible to recover from this type of problems automatically and
|
||||||
|
continue program execution. Such problems may be related with invalid user
|
||||||
|
input (such as invalid syntax) or temporary environmental problems.
|
||||||
|
|
||||||
|
In case if an instance of a certain exception type bubbles up to API layer
|
||||||
|
then this type of exception it must be associated with an http code so it's
|
||||||
|
clear how to represent it for a client.
|
||||||
|
|
||||||
|
To correctly use this class, inherit from it and define a 'message' and
|
||||||
|
'http_code' properties.
|
||||||
|
"""
|
||||||
|
message = "An unknown exception occurred"
|
||||||
|
http_code = 500
|
||||||
|
|
||||||
|
def __init__(self, message=None):
|
||||||
|
if message is not None:
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
super(QinlingException, self).__init__(
|
||||||
|
'%d: %s' % (self.http_code, self.message))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code(self):
|
||||||
|
"""This is here for webob to read.
|
||||||
|
|
||||||
|
https://github.com/Pylons/webob/blob/master/webob/exc.py
|
||||||
|
"""
|
||||||
|
return self.http_code
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message
|
||||||
|
|
||||||
|
|
||||||
|
class InputException(QinlingException):
|
||||||
|
http_code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class UnauthorizedException(QinlingException):
|
||||||
|
http_code = 401
|
||||||
|
message = "Unauthorized"
|
||||||
|
|
||||||
|
|
||||||
|
class NotAllowedException(QinlingException):
|
||||||
|
http_code = 403
|
||||||
|
message = "Operation not allowed"
|
0
qinling/tests/__init__.py
Normal file
0
qinling/tests/__init__.py
Normal file
23
qinling/tests/base.py
Normal file
23
qinling/tests/base.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright 2010-2011 OpenStack Foundation
|
||||||
|
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||||
|
#
|
||||||
|
# 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 oslotest import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestCase(base.BaseTestCase):
|
||||||
|
|
||||||
|
"""Test case base class for all unit tests."""
|
28
qinling/tests/test_qinling.py
Normal file
28
qinling/tests/test_qinling.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# -*- 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
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
test_qinling
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Tests for `qinling` module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from qinling.tests import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestQinling(base.TestCase):
|
||||||
|
|
||||||
|
def test_something(self):
|
||||||
|
pass
|
0
qinling/utils/__init__.py
Normal file
0
qinling/utils/__init__.py
Normal file
72
qinling/utils/rest_utils.py
Normal file
72
qinling/utils/rest_utils.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# 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 functools
|
||||||
|
import json
|
||||||
|
|
||||||
|
from oslo_log import log as logging
|
||||||
|
import pecan
|
||||||
|
import six
|
||||||
|
import webob
|
||||||
|
from wsme import exc as wsme_exc
|
||||||
|
|
||||||
|
from qinling import exceptions as exc
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_wsme_controller_exception(func):
|
||||||
|
"""Decorator for controllers method.
|
||||||
|
|
||||||
|
This decorator wraps controllers method to manage wsme exceptions:
|
||||||
|
In case of expected error it aborts the request with specific status code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except exc.QinlingException as e:
|
||||||
|
pecan.response.translatable_error = e
|
||||||
|
|
||||||
|
LOG.error('Error during API call: %s' % str(e))
|
||||||
|
raise wsme_exc.ClientSideError(
|
||||||
|
msg=six.text_type(e),
|
||||||
|
status_code=e.http_code
|
||||||
|
)
|
||||||
|
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
|
def wrap_pecan_controller_exception(func):
|
||||||
|
"""Decorator for controllers method.
|
||||||
|
|
||||||
|
This decorator wraps controllers method to manage pecan exceptions:
|
||||||
|
In case of expected error it aborts the request with specific status code.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(func)
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except exc.QinlingException as e:
|
||||||
|
LOG.error('Error during API call: %s' % str(e))
|
||||||
|
return webob.Response(
|
||||||
|
status=e.http_code,
|
||||||
|
content_type='application/json',
|
||||||
|
body=json.dumps(dict(faultstring=six.text_type(e))),
|
||||||
|
charset='UTF-8'
|
||||||
|
)
|
||||||
|
|
||||||
|
return wrapped
|
18
qinling/version.py
Normal file
18
qinling/version.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Copyright 2017 Catalyst IT Limited
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
|
||||||
|
version_info = version.VersionInfo('qinling')
|
||||||
|
version_string = version_info.version_string
|
Loading…
Reference in New Issue
Block a user