@ -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 |