24 changed files with 1249 additions and 1 deletions
@ -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() |
@ -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,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,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 |
@ -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 |
@ -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')) |
@ -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() |
@ -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,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()) |
@ -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 |
||||
) |
@ -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 |
@ -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,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.""" |
@ -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,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 |
@ -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