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