Initial API code structure

This commit is contained in:
Lingxian Kong 2017-04-14 15:24:02 +12:00
commit 9492a02bab
24 changed files with 1249 additions and 1 deletions

@ -1 +0,0 @@
Subproject commit e089c31e73c1e313293ea794a25a30b15b4dad8e

0
qinling/__init__.py Normal file
View File

0
qinling/api/__init__.py Normal file
View File

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

View File

View 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])

View File

View 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

View 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

View 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'))

View 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
View 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
View File

150
qinling/cmd/launch.py Normal file
View 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
View 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
View 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
View 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"

View File

23
qinling/tests/base.py Normal file
View 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."""

View 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

View File

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