Add bare bones ekko-api

Foundation for ekko-api, ported over from the Ironic project.

The goal here was to copy enough to give a solid foundation that
conforms to the "OpenStack way", without a lot of extra frills that we
either may not need right away, or makes it harder see what's going on
starting out.

After installation the service can be started via 'ekko-api', which
listens on port 6800 by default. This was chosen from the list of
unassigned ports at
http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.txt

There are still plenty of TODO's, primarily keystone integration and
config file generation. Foundations for these are here but completely
untested as of this commit.

Change-Id: If136a3bb66949ef710d741eaf3691f36f7b60692
This commit is contained in:
Paul Bourke 2016-01-25 15:25:35 +00:00 committed by Sam Yaple
parent 4bb6a26ad8
commit a5f99317ac
28 changed files with 1588 additions and 25 deletions

View File

@ -2,6 +2,6 @@
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-7200} \
${PYTHON:-python} -m subunit.run discover ${OS_TEST_PATH:-./tests} $LISTOPT $IDOPTION
${PYTHON:-python} -m subunit.run discover ${OS_TEST_PATH:-./ekko/tests} $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

58
ekko/api/__init__.py Normal file
View File

@ -0,0 +1,58 @@
#
# 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
# TODO(pbourke): add i18n support
_ = lambda x: x
API_SERVICE_OPTS = [
cfg.StrOpt('host_ip',
default='0.0.0.0',
help=_('The IP address on which ekko-api listens.')),
cfg.PortOpt('port',
default=6800,
help=_('The TCP port on which ekko-api listens.')),
cfg.IntOpt('max_limit',
default=1000,
help=_('The maximum number of items returned in a single '
'response from a collection resource.')),
cfg.StrOpt('public_endpoint',
default=None,
# TODO(pbourke): decide on port for ekko
help=_("Public URL to use when building the links to the API "
"resources (for example, \"https://ekko.rocks:6800\")."
" If None the links will be built using the request's "
"host URL. If the API is operating behind a proxy, you "
"will want to change this to represent the proxy's URL. "
"Defaults to None.")),
cfg.IntOpt('api_workers',
help=_('Number of workers for OpenStack ekko API service. '
'The default is equal to the number of CPUs available '
'if that can be determined, else a default worker '
'count of 1 is returned.')),
cfg.BoolOpt('enable_ssl_api',
default=False,
help=_("Enable the integrated stand-alone API to service "
"requests via HTTPS instead of HTTP. If there is a "
"front-end service performing HTTPS offloading from "
"the service, this option should be False; note, you "
"will want to change public API endpoint to represent "
"SSL termination URL with 'public_endpoint' option.")),
]
CONF = cfg.CONF
opt_group = cfg.OptGroup(name='api',
title='Options for the ekko-api service')
CONF.register_group(opt_group)
CONF.register_opts(API_SERVICE_OPTS, opt_group)

95
ekko/api/app.py Normal file
View File

@ -0,0 +1,95 @@
#
# 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 oslo_config import cfg
from ekko.api import config
# TODO(pbourke): port hooks module
# from ekko.api import hooks
from ekko.api import middleware
# TODO(pbourke): add i18n support
_ = lambda x: x
api_opts = [
cfg.StrOpt(
'auth_strategy',
default='keystone',
help=_('Authentication strategy used by ekko-api: one of "keystone" '
'or "noauth". "noauth" should not be used in a production '
'environment because all authentication will be disabled.')),
cfg.BoolOpt('debug_tracebacks_in_api',
default=False,
help=_('Return server tracebacks in the API response for any '
'error responses. WARNING: this is insecure '
'and should not be used in a production environment.')),
cfg.BoolOpt('pecan_debug',
default=False,
help=_('Enable pecan debug mode. WARNING: this is insecure '
'and should not be used in a production environment.')),
]
CONF = cfg.CONF
CONF.register_opts(api_opts)
def get_pecan_config():
# Set up the pecan configuration
filename = config.__file__.replace('.pyc', '.py')
return pecan.configuration.conf_from_file(filename)
def setup_app(pecan_config=None, extra_hooks=None):
# TODO(pbourke): port hooks module
# app_hooks = [hooks.ConfigHook(),
# hooks.DBHook(),
# hooks.ContextHook(pecan_config.app.acl_public_routes),
# hooks.RPCHook(),
# hooks.NoExceptionTracebackHook(),
# hooks.PublicUrlHook()]
# if extra_hooks:
# app_hooks.extend(extra_hooks)
if not pecan_config:
pecan_config = get_pecan_config()
# if pecan_config.app.enable_acl:
# app_hooks.append(hooks.TrustedCallHook())
pecan.configuration.set_config(dict(pecan_config), overwrite=True)
app = pecan.make_app(
pecan_config.app.root,
static_root=pecan_config.app.static_root,
debug=CONF.pecan_debug,
force_canonical=getattr(pecan_config.app, 'force_canonical', True),
# hooks=app_hooks,
wrap_app=middleware.ParsableErrorMiddleware,
)
# if pecan_config.app.enable_acl:
# app = acl.install(app, cfg.CONF, pecan_config.app.acl_public_routes)
return app
class VersionSelectorApplication(object):
def __init__(self):
pc = get_pecan_config()
pc.app.enable_acl = (CONF.auth_strategy == 'keystone')
self.v1 = setup_app(pecan_config=pc)
def __call__(self, environ, start_response):
return self.v1(environ, start_response)

39
ekko/api/config.py Normal file
View File

@ -0,0 +1,39 @@
#
# 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.
# Server Specific Configurations
# See https://pecan.readthedocs.org/en/latest/configuration.html#server-configuration # noqa
server = {
'port': '6800',
'host': '0.0.0.0'
}
# Pecan Application Configurations
# See https://pecan.readthedocs.org/en/latest/configuration.html#application-configuration # noqa
app = {
'root': 'ekko.api.controllers.root.RootController',
'modules': ['ekko.api'],
'static_root': '%(confdir)s/public',
'debug': False,
'enable_acl': True,
'acl_public_routes': [
'/',
'/v1',
],
}
# WSME Configurations
# See https://wsme.readthedocs.org/en/latest/integrate.html#configuration
wsme = {
'debug': False,
}

View File

@ -0,0 +1,112 @@
#
# 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 datetime
import functools
from webob import exc
import wsme
from wsme import types as wtypes
# TODO(pbourke): add i18n support
_ = lambda x: x
class APIBase(wtypes.Base):
created_at = wsme.wsattr(datetime.datetime, readonly=True)
"""The time in UTC at which the object is created"""
updated_at = wsme.wsattr(datetime.datetime, readonly=True)
"""The time in UTC at which the object is updated"""
def as_dict(self):
"""Render this object as a dict of its fields."""
return dict((k, getattr(self, k))
for k in self.fields
if hasattr(self, k) and
getattr(self, k) != wsme.Unset)
def unset_fields_except(self, except_list=None):
"""Unset fields so they don't appear in the message body.
:param except_list: A list of fields that won't be touched.
"""
if except_list is None:
except_list = []
for k in self.as_dict():
if k not in except_list:
setattr(self, k, wsme.Unset)
@functools.total_ordering
class Version(object):
"""API Version object."""
string = 'X-OpenStack-Ekko-API-Version'
"""HTTP Header string carrying the requested version"""
min_string = 'X-OpenStack-Ekko-API-Minimum-Version'
"""HTTP response header"""
max_string = 'X-OpenStack-Ekko-API-Maximum-Version'
"""HTTP response header"""
def __init__(self, headers, default_version, latest_version):
"""Create an API Version object from the supplied headers.
:param headers: webob headers
:param default_version: version to use if not specified in headers
:param latest_version: version to use if latest is requested
:raises: webob.HTTPNotAcceptable
"""
(self.major, self.minor) = Version.parse_headers(
headers, default_version, latest_version)
def __repr__(self):
return '%s.%s' % (self.major, self.minor)
@staticmethod
def parse_headers(headers, default_version, latest_version):
"""Determine the API version requested based on the headers supplied.
:param headers: webob headers
:param default_version: version to use if not specified in headers
:param latest_version: version to use if latest is requested
:returns: a tupe of (major, minor) version numbers
:raises: webob.HTTPNotAcceptable
"""
version_str = headers.get(Version.string, default_version)
if version_str.lower() == 'latest':
parse_str = latest_version
else:
parse_str = version_str
try:
version = tuple(int(i) for i in parse_str.split('.'))
except ValueError:
version = ()
if len(version) != 2:
raise exc.HTTPNotAcceptable(_(
"Invalid value for %s header") % Version.string)
return version
def __gt__(a, b):
return (a.major, a.minor) > (b.major, b.minor)
def __eq__(a, b):
return (a.major, a.minor) == (b.major, b.minor)

View File

@ -0,0 +1,111 @@
#
# 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 pecan import rest
from wsme import types as wtypes
from ekko.api.controllers import base
from ekko.api.controllers import v1
from ekko.api.controllers.v1 import versions
from ekko.api import expose
ID_VERSION1 = 'v1'
class Version(base.APIBase):
"""An API version representation.
This class represents an API version, including the minimum and
maximum minor versions that are supported within the major version.
"""
id = wtypes.text
"""The ID of the (major) version, also acts as the release number"""
status = wtypes.text
"""Status of the version.
One of:
* CURRENT - the latest version of API,
* SUPPORTED - supported, but not latest, version of API,
* DEPRECATED - supported, but deprecated, version of API.
"""
version = wtypes.text
"""The current, maximum supported (major.minor) version of API."""
min_version = wtypes.text
"""Minimum supported (major.minor) version of API."""
def __init__(self, id, min_version, version, status='CURRENT'):
self.id = id
self.status = status
self.version = version
self.min_version = min_version
class Root(base.APIBase):
name = wtypes.text
"""The name of the API"""
description = wtypes.text
"""Some information about this API"""
versions = [Version]
"""Links to all the versions available in this API"""
default_version = Version
"""A link to the default version of the API"""
@staticmethod
def convert():
root = Root()
root.name = "OpenStack Ekko API"
root.description = ("Block-based backups stored in object-storage.")
root.default_version = Version(ID_VERSION1,
versions.MIN_VERSION_STRING,
versions.MAX_VERSION_STRING)
root.versions = [root.default_version]
return root
class RootController(rest.RestController):
_versions = [ID_VERSION1]
"""All supported API versions"""
_default_version = ID_VERSION1
"""The default API version"""
v1 = v1.Controller()
@expose.expose(Root)
def get(self):
# NOTE: The reason why convert() it's being called for every
# request is because we need to get the host url from
# the request object to make the links.
return Root.convert()
@pecan.expose()
def _route(self, args):
"""Overrides the default routing behavior.
It redirects the request to the default version of the ekko API
if the version number is not specified in the url.
"""
if args[0] and args[0] not in self._versions:
args = [self._default_version] + args
return super(RootController, self)._route(args)

View File

@ -0,0 +1,119 @@
# All Rights Reserved.
#
# 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 pecan import rest
from webob import exc
from wsme import types as wtypes
from ekko.api.controllers import base
from ekko.api.controllers.v1 import versions
from ekko.api import expose
# TODO(pbourke): add i18n support
_ = lambda x: x
BASE_VERSION = versions.BASE_VERSION
MIN_VER = base.Version(
{base.Version.string: versions.MIN_VERSION_STRING},
versions.MIN_VERSION_STRING, versions.MAX_VERSION_STRING)
MAX_VER = base.Version(
{base.Version.string: versions.MAX_VERSION_STRING},
versions.MIN_VERSION_STRING, versions.MAX_VERSION_STRING)
class MediaType(base.APIBase):
"""A media type representation."""
base = wtypes.text
type = wtypes.text
def __init__(self, base, type):
self.base = base
self.type = type
class V1(base.APIBase):
"""The representation of the version 1 of the API."""
id = wtypes.text
"""The ID of the version, also acts as the release number"""
media_types = [MediaType]
"""An array of supported media types for this version"""
@staticmethod
def convert():
v1 = V1()
v1.id = "v1"
v1.media_types = [MediaType('application/json',
'application/vnd.openstack.ekko.v1+json')]
return v1
class Controller(rest.RestController):
"""Version 1 API controller root."""
@expose.expose(V1)
def get(self):
# NOTE: The reason why convert() it's being called for every
# request is because we need to get the host url from
# the request object to make the links.
return V1.convert()
def _check_version(self, version, headers=None):
if headers is None:
headers = {}
# ensure that major version in the URL matches the header
if version.major != BASE_VERSION:
raise exc.HTTPNotAcceptable(_(
"Mutually exclusive versions requested. Version %(ver)s "
"requested but not supported by this service. The supported "
"version range is: [%(min)s, %(max)s].") %
{'ver': version, 'min': versions.MIN_VERSION_STRING,
'max': versions.MAX_VERSION_STRING},
headers=headers)
# ensure the minor version is within the supported range
if version < MIN_VER or version > MAX_VER:
raise exc.HTTPNotAcceptable(_(
"Version %(ver)s was requested but the minor version is not "
"supported by this service. The supported version range is: "
"[%(min)s, %(max)s].") %
{'ver': version, 'min': versions.MIN_VERSION_STRING,
'max': versions.MAX_VERSION_STRING},
headers=headers)
@pecan.expose()
def _route(self, args):
v = base.Version(pecan.request.headers, versions.MIN_VERSION_STRING,
versions.MAX_VERSION_STRING)
# Always set the min and max headers
pecan.response.headers[base.Version.min_string] = (
versions.MIN_VERSION_STRING)
pecan.response.headers[base.Version.max_string] = (
versions.MAX_VERSION_STRING)
# assert that requested version is supported
self._check_version(v, pecan.response.headers)
pecan.response.headers[base.Version.string] = str(v)
pecan.request.version = v
return super(Controller, self)._route(args)
__all__ = (Controller)

View File

@ -0,0 +1,32 @@
#
# 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.
# This is the version 1 API
BASE_VERSION = 1
# Here goes a short log of changes in every version.
# Refer to doc/source/webapi/v1.rst for a detailed explanation of what
# each version contains.
#
# v1.0: Initial version of the Ekko API
MINOR_0_INITIAL_VERSION = 0
# When adding another version, update MINOR_MAX_VERSION and also update
# doc/source/webapi/v1.rst with a detailed explanation of what the version has
# changed.
MINOR_MAX_VERSION = MINOR_0_INITIAL_VERSION
# String representations of the minor and maximum versions
MIN_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_0_INITIAL_VERSION)
MAX_VERSION_STRING = '{}.{}'.format(BASE_VERSION, MINOR_MAX_VERSION)

View File

@ -1,3 +1,4 @@
#
# 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
@ -10,14 +11,11 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslotest import base
import wsmeext.pecan as wsme_pecan
class NoopTest(base.BaseTestCase):
def setUp(self):
super(NoopTest, self).setUp()
def test_noop(self):
self.assertTrue(True)
def expose(*args, **kwargs):
"""Ensure that only JSON, and not XML, is supported."""
if 'rest_content_types' not in kwargs:
kwargs['rest_content_types'] = ('json',)
return wsme_pecan.wsexpose(*args, **kwargs)

View File

@ -0,0 +1,22 @@
#
# 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 ekko.api.middleware import auth_token
from ekko.api.middleware import parsable_error
ParsableErrorMiddleware = parsable_error.ParsableErrorMiddleware
AuthTokenMiddleware = auth_token.AuthTokenMiddleware
__all__ = (ParsableErrorMiddleware,
AuthTokenMiddleware)

View File

@ -0,0 +1,64 @@
#
# 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 re
from keystonemiddleware import auth_token
from oslo_log import log
from ekko.common import exception
LOG = log.getLogger(__name__)
# TODO(pbourke): add i18n support
_ = lambda x: x
class AuthTokenMiddleware(auth_token.AuthProtocol):
"""A wrapper on Keystone auth_token middleware.
Does not perform verification of authentication tokens
for public routes in the API.
"""
def __init__(self, app, conf, public_api_routes=[]):
self._ekko_app = app
# TODO(mrda): Remove .xml and ensure that doesn't result in a
# 401 Authentication Required instead of 404 Not Found
route_pattern_tpl = '%s(\.json|\.xml)?$'
try:
self.public_api_routes = [re.compile(route_pattern_tpl % route_tpl)
for route_tpl in public_api_routes]
except re.error as e:
msg = _('Cannot compile public API routes: %s') % e
LOG.error(msg)
raise exception.ConfigInvalid(error_msg=msg)
super(AuthTokenMiddleware, self).__init__(app, conf)
def __call__(self, env, start_response):
# TODO(pbourke): ironic uses a utils.safe_rstrip function here.
path = env.get('PATH_INFO').rstrip('/')
# The information whether the API call is being performed against the
# public API is required for some other components. Saving it to the
# WSGI environment is reasonable thereby.
env['is_public_api'] = any(map(lambda pattern: re.match(pattern, path),
self.public_api_routes))
if env['is_public_api']:
return self._ekko_app(env, start_response)
return super(AuthTokenMiddleware, self).__call__(env, start_response)

View File

@ -0,0 +1,93 @@
#
# 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.
"""
Middleware to replace the plain text message body of an error
response with one formatted so the client can parse it.
Based on pecan.middleware.errordocument
"""
import json
from xml import etree as et
from oslo_log import log
import six
import webob
LOG = log.getLogger(__name__)
# TODO(pbourke): add i18n support
_ = _LE = lambda x: x
class ParsableErrorMiddleware(object):
"""Replace error body with something the client can parse."""
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
# Request for this state, modified by replace_start_response()
# and used when an error is being reported.
state = {}
def replacement_start_response(status, headers, exc_info=None):
"""Overrides the default response to make errors parsable."""
try:
status_code = int(status.split(' ')[0])
state['status_code'] = status_code
except (ValueError, TypeError): # pragma: nocover
raise Exception(_(
'ErrorDocumentMiddleware received an invalid '
'status %s') % status)
else:
if (state['status_code'] // 100) not in (2, 3):
# Remove some headers so we can replace them later
# when we have the full error message and can
# compute the length.
headers = [(h, v)
for (h, v) in headers
if h not in ('Content-Length', 'Content-Type')
]
# Save the headers in case we need to modify them.
state['headers'] = headers
return start_response(status, headers, exc_info)
app_iter = self.app(environ, replacement_start_response)
if (state['status_code'] // 100) not in (2, 3):
req = webob.Request(environ)
if (req.accept.best_match(['application/json', 'application/xml'])
== 'application/xml'):
try:
# simple check xml is valid
body = [et.ElementTree.tostring(
et.ElementTree.fromstring('<error_message>'
+ '\n'.join(app_iter)
+ '</error_message>'))]
except et.ElementTree.ParseError as err:
LOG.error(_LE('Error parsing HTTP response: %s'), err)
body = ['<error_message>%s' % state['status_code']
+ '</error_message>']
state['headers'].append(('Content-Type', 'application/xml'))
else:
if six.PY3:
app_iter = [i.decode('utf-8') for i in app_iter]
body = [json.dumps({'error_message': '\n'.join(app_iter)})]
if six.PY3:
body = [item.encode('utf-8') for item in body]
state['headers'].append(('Content-Type', 'application/json'))
state['headers'].append(('Content-Length', str(len(body[0]))))
else:
body = app_iter
return body

0
ekko/cmd/__init__.py Normal file
View File

34
ekko/cmd/api.py Normal file
View File

@ -0,0 +1,34 @@
#
# 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
from oslo_config import cfg
from ekko.common import service as ekko_service
CONF = cfg.CONF
def main():
# Parse config file and command line options, then start logging
ekko_service.prepare_service(sys.argv)
# Build and start the WSGI app
launcher = ekko_service.process_launcher()
server = ekko_service.WSGIService('ekko_api', CONF.api.enable_ssl_api)
launcher.launch_service(server, workers=server.workers)
launcher.wait()
if __name__ == '__main__':
sys.exit(main())

0
ekko/common/__init__.py Normal file
View File

26
ekko/common/config.py Normal file
View File

@ -0,0 +1,26 @@
#
# 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 ekko.common import rpc
# from ekko import version
def parse_args(argv, default_config_files=None):
# rpc.set_defaults(control_exchange='ekko')
cfg.CONF(argv[1:],
project='ekko',
# version=version.version_info.release_string(),
default_config_files=default_config_files)
# rpc.init(cfg.CONF)

131
ekko/common/exception.py Normal file
View File

@ -0,0 +1,131 @@
#
# 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_log import log as logging
import six
from six.moves import http_client
# TODO(pbourke): add i18n support
_ = _LE = _LW = lambda x: x
LOG = logging.getLogger(__name__)
exc_log_opts = [
cfg.BoolOpt('fatal_exception_format_errors',
default=False,
help=_('Used if there is a formatting error when generating '
'an exception message (a programming error). If True, '
'raise an exception; if False, use the unformatted '
'message.')),
]
CONF = cfg.CONF
CONF.register_opts(exc_log_opts)
class EkkoException(Exception):
"""Base Ekko Exception
To correctly use this class, inherit from it and define
a '_msg_fmt' property. That message will get printf'd
with the keyword arguments provided to the constructor.
If you need to access the message from an exception you should use
six.text_type(exc)
"""
_msg_fmt = _("An unknown exception occurred.")
code = http_client.INTERNAL_SERVER_ERROR
headers = {}
safe = False
def __init__(self, message=None, **kwargs):
self.kwargs = kwargs
if 'code' not in self.kwargs:
try:
self.kwargs['code'] = self.code
except AttributeError:
pass
if not message:
# Check if class is using deprecated 'message' attribute.
if (hasattr(self, 'message') and self.message):
LOG.warning(_LW("Exception class: %s Using the 'message' "
"attribute in an exception has been "
"deprecated. The exception class should be "
"modified to use the '_msg_fmt' "
"attribute."), self.__class__.__name__)
self._msg_fmt = self.message
try:
message = self._msg_fmt % kwargs
except Exception as e:
# kwargs doesn't match a variable in self._msg_fmt
# log the issue and the kwargs
LOG.exception(_LE('Exception in string format operation'))
for name, value in kwargs.items():
LOG.error("%s: %s" % (name, value))
if CONF.fatal_exception_format_errors:
raise e
else:
# at least get the core self._msg_fmt out if something
# happened
message = self._msg_fmt
super(EkkoException, self).__init__(message)
def __str__(self):
"""Encode to utf-8 then wsme api can consume it as well."""
if not six.PY3:
return unicode(self.args[0]).encode('utf-8')
return self.args[0]
def __unicode__(self):
"""Return a unicode representation of the exception message."""
return unicode(self.args[0])
class NotAuthorized(EkkoException):
_msg_fmt = _("Not authorized.")
code = http_client.FORBIDDEN
class OperationNotPermitted(NotAuthorized):
_msg_fmt = _("Operation not permitted.")
class Invalid(EkkoException):
_msg_fmt = _("Unacceptable parameters.")
code = http_client.BAD_REQUEST
class Conflict(EkkoException):
_msg_fmt = _('Conflict.')
code = http_client.CONFLICT
class TemporaryFailure(EkkoException):
_msg_fmt = _("Resource temporarily unavailable, please retry.")
code = http_client.SERVICE_UNAVAILABLE
class NotAcceptable(EkkoException):
# TODO(deva): We need to set response headers in the API for this exception
_msg_fmt = _("Request not acceptable.")
code = http_client.NOT_ACCEPTABLE

109
ekko/common/service.py Normal file
View File

@ -0,0 +1,109 @@
#
# 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 socket
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log
from oslo_service import service
from oslo_service import wsgi
from ekko.api import app
from ekko.common import config
# TODO(pbourke): add i18n support
_ = lambda x: x
service_opts = [
cfg.IntOpt('periodic_interval',
default=60,
help=_('Seconds between running periodic tasks.')),
cfg.StrOpt('host',
default=socket.getfqdn(),
help=_('Name of this node. This can be an opaque identifier. '
'It is not necessarily a hostname, FQDN, or IP address. '
'However, the node name must be valid within '
'an AMQP key, and if using ZeroMQ, a valid '
'hostname, FQDN, or IP address.')),
]
CONF = cfg.CONF
LOG = log.getLogger(__name__)
CONF.register_opts(service_opts)
def prepare_service(argv=[]):
log.register_options(CONF)
config.parse_args(argv)
log.setup(CONF, 'ekko')
def process_launcher():
return service.ProcessLauncher(CONF)
class WSGIService(service.ServiceBase):
"""Provides ability to launch ekko API from wsgi app."""
def __init__(self, name, use_ssl=False):
"""Initialize, but do not start the WSGI server.
:param name: The name of the WSGI server given to the loader.
:param use_ssl: Wraps the socket in an SSL context if True.
:returns: None
"""
self.name = name
self.app = app.VersionSelectorApplication()
self.workers = (CONF.api.api_workers or
processutils.get_worker_count())
if self.workers and self.workers < 1:
raise Exception(
_("api_workers value of %d is invalid, "
"must be greater than 0.") % self.workers)
self.server = wsgi.Server(CONF, name, self.app,
host=CONF.api.host_ip,
port=CONF.api.port,
use_ssl=use_ssl)
def start(self):
"""Start serving this service using loaded configuration.
:returns: None
"""
self.server.start()
def stop(self):
"""Stop serving this API.
:returns: None
"""
self.server.stop()
def wait(self):
"""Wait for the service to stop serving this API.
:returns: None
"""
self.server.wait()
def reset(self):
"""Reset server greenpool size to default.
:returns: None
"""
self.server.reset()

View File

@ -1,23 +1,142 @@
# -*- 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
# 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
# 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.
# 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
"""Base classes for our unit tests.
Allows overriding of config for use of fakes, and some black magic for
inline callbacks.
"""
# import copy
import os
import sys
# import tempfile
import eventlet
eventlet.monkey_patch(os=False)
import fixtures
from oslo_config import cfg
from oslo_context import context as ekko_context
from oslo_log import log as logging
import testtools
# from ekko.objects import base as objects_base
from ekko.tests.unit import conf_fixture
# from ekko.tests.unit import policy_fixture
class TestCase(base.BaseTestCase):
CONF = cfg.CONF
logging.register_options(CONF)
CONF.set_override('use_stderr', False)
logging.setup(CONF, 'ekko')
class ReplaceModule(fixtures.Fixture):
"""Replace a module with a fake module."""
def __init__(self, name, new_value):
self.name = name
self.new_value = new_value
def _restore(self, old_value):
sys.modules[self.name] = old_value
def setUp(self):
super(ReplaceModule, self).setUp()
old_value = sys.modules.get(self.name)
sys.modules[self.name] = self.new_value
self.addCleanup(self._restore, old_value)
class TestingException(Exception):
pass
class TestCase(testtools.TestCase):
"""Test case base class for all unit tests."""
def setUp(self):
"""Run before each test method to initialize test environment."""
super(TestCase, self).setUp()
self.context = ekko_context.get_admin_context()
test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
try:
test_timeout = int(test_timeout)
except ValueError:
# If timeout value is invalid do not set a timeout.
test_timeout = 0
if test_timeout > 0:
self.useFixture(fixtures.Timeout(test_timeout, gentle=True))
self.useFixture(fixtures.NestedTempfile())
self.useFixture(fixtures.TempHomeDir())
# self.config(tempdir=tempfile.tempdir)
if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
os.environ.get('OS_STDOUT_CAPTURE') == '1'):
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
os.environ.get('OS_STDERR_CAPTURE') == '1'):
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
self.log_fixture = self.useFixture(fixtures.FakeLogger())
self.useFixture(conf_fixture.ConfFixture(CONF))
# NOTE(danms): Make sure to reset us back to non-remote objects
# for each test to avoid interactions. Also, backup the object
# registry
# objects_base.EkkoObject.indirection_api = None
# self._base_test_obj_backup = copy.copy(
# objects_base.EkkoObjectRegistry.obj_classes())
# self.addCleanup(self._restore_obj_registry)
self.addCleanup(self._clear_attrs)
self.useFixture(fixtures.EnvironmentVariable('http_proxy'))
# self.policy = self.useFixture(policy_fixture.PolicyFixture())
# CONF.set_override('fatal_exception_format_errors', True)
# def _restore_obj_registry(self):
# objects_base.EkkoObjectRegistry._registry._obj_classes = (
# self._base_test_obj_backup)
def _clear_attrs(self):
# Delete attributes that don't start with _ so they don't pin
# memory around unnecessarily for the duration of the test
# suite
for key in [k for k in self.__dict__.keys() if k[0] != '_']:
del self.__dict__[key]
def config(self, **kw):
"""Override config options for a test."""
group = kw.pop('group', None)
for k, v in kw.items():
CONF.set_override(k, v, group)
def path_get(self, project_file=None):
"""Get the absolute path to a file. Used for testing the API.
:param project_file: File whose path to return. Default: None.
:returns: path to the specified file, or path to project root.
"""
root = os.path.abspath(os.path.join(os.path.dirname(__file__),
'..',
'..',
)
)
if project_file:
return os.path.join(root, project_file)
else:
return root

View File

View File

237
ekko/tests/unit/api/base.py Normal file
View File

@ -0,0 +1,237 @@
#
# 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.
# NOTE: Ported from ceilometer/tests/api.py (subsequently moved to
# ceilometer/tests/api/__init__.py). This should be oslo'ified:
# https://bugs.launchpad.net/ekko/+bug/1255115.
import mock
from oslo_config import cfg
import pecan
import pecan.testing
from six.moves.urllib import parse as urlparse
# from ekko.tests.unit.db import base
from ekko.tests import base
PATH_PREFIX = '/v1'
cfg.CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token')
# class BaseApiTest(base.DbTestCase):
class BaseApiTest(base.TestCase):
"""Pecan controller functional testing class.
Used for functional tests of Pecan controllers where you need to
test your literal application and its integration with the
framework.
"""
SOURCE_DATA = {'test_source': {'somekey': '666'}}
def setUp(self):
super(BaseApiTest, self).setUp()
cfg.CONF.set_override("auth_version", "v2.0",
group='keystone_authtoken')
cfg.CONF.set_override("admin_user", "admin",
group='keystone_authtoken')
self.app = self._make_app()
def reset_pecan():
pecan.set_config({}, overwrite=True)
self.addCleanup(reset_pecan)
p = mock.patch('ekko.api.controllers.v1.Controller._check_version')
self._check_version = p.start()
self.addCleanup(p.stop)
def _make_app(self, enable_acl=False):
# Determine where we are so we can set up paths in the config
root_dir = self.path_get()
self.config = {
'app': {
'root': 'ekko.api.controllers.root.RootController',
'modules': ['ekko.api'],
'static_root': '%s/public' % root_dir,
'template_path': '%s/api/templates' % root_dir,
'enable_acl': enable_acl,
'acl_public_routes': ['/', '/v1'],
},
}
return pecan.testing.load_test_app(self.config)
def _request_json(self, path, params, expect_errors=False, headers=None,
method="post", extra_environ=None, status=None,
path_prefix=PATH_PREFIX):
"""Sends simulated HTTP request to Pecan test app.
:param path: url path of target service
:param params: content for wsgi.input of request
:param expect_errors: Boolean value; whether an error is expected based
on request
:param headers: a dictionary of headers to send along with the request
:param method: Request method type. Appropriate method function call
should be used rather than passing attribute in.
:param extra_environ: a dictionary of environ variables to send along
with the request
:param status: expected status code of response
:param path_prefix: prefix of the url path
"""
full_path = path_prefix + path
print('%s: %s %s' % (method.upper(), full_path, params))
response = getattr(self.app, "%s_json" % method)(
str(full_path),
params=params,
headers=headers,
status=status,
extra_environ=extra_environ,
expect_errors=expect_errors
)
print('GOT:%s' % response)
return response
def put_json(self, path, params, expect_errors=False, headers=None,
extra_environ=None, status=None):
"""Sends simulated HTTP PUT request to Pecan test app.
:param path: url path of target service
:param params: content for wsgi.input of request
:param expect_errors: Boolean value; whether an error is expected based
on request
:param headers: a dictionary of headers to send along with the request
:param extra_environ: a dictionary of environ variables to send along
with the request
:param status: expected status code of response
"""
return self._request_json(path=path, params=params,
expect_errors=expect_errors,
headers=headers, extra_environ=extra_environ,
status=status, method="put")
def post_json(self, path, params, expect_errors=False, headers=None,
extra_environ=None, status=None):
"""Sends simulated HTTP POST request to Pecan test app.
:param path: url path of target service
:param params: content for wsgi.input of request
:param expect_errors: Boolean value; whether an error is expected based
on request
:param headers: a dictionary of headers to send along with the request
:param extra_environ: a dictionary of environ variables to send along
with the request
:param status: expected status code of response
"""
return self._request_json(path=path, params=params,
expect_errors=expect_errors,
headers=headers, extra_environ=extra_environ,
status=status, method="post")
def patch_json(self, path, params, expect_errors=False, headers=None,
extra_environ=None, status=None):
"""Sends simulated HTTP PATCH request to Pecan test app.
:param path: url path of target service
:param params: content for wsgi.input of request
:param expect_errors: Boolean value; whether an error is expected based
on request
:param headers: a dictionary of headers to send along with the request
:param extra_environ: a dictionary of environ variables to send along
with the request
:param status: expected status code of response
"""
return self._request_json(path=path, params=params,
expect_errors=expect_errors,
headers=headers, extra_environ=extra_environ,
status=status, method="patch")
def delete(self, path, expect_errors=False, headers=None,
extra_environ=None, status=None, path_prefix=PATH_PREFIX):
"""Sends simulated HTTP DELETE request to Pecan test app.
:param path: url path of target service
:param expect_errors: Boolean value; whether an error is expected based
on request
:param headers: a dictionary of headers to send along with the request
:param extra_environ: a dictionary of environ variables to send along
with the request
:param status: expected status code of response
:param path_prefix: prefix of the url path
"""
full_path = path_prefix + path
print('DELETE: %s' % (full_path))
response = self.app.delete(str(full_path),
headers=headers,
status=status,
extra_environ=extra_environ,
expect_errors=expect_errors)
print('GOT:%s' % response)
return response
def get_json(self, path, expect_errors=False, headers=None,
extra_environ=None, q=[], path_prefix=PATH_PREFIX, **params):
"""Sends simulated HTTP GET request to Pecan test app.
:param path: url path of target service
:param expect_errors: Boolean value;whether an error is expected based
on request
:param headers: a dictionary of headers to send along with the request
:param extra_environ: a dictionary of environ variables to send along
with the request
:param q: list of queries consisting of: field, value, op, and type
keys
:param path_prefix: prefix of the url path
:param params: content for wsgi.input of request
"""
full_path = path_prefix + path
query_params = {'q.field': [],
'q.value': [],
'q.op': [],
}
for query in q:
for name in ['field', 'op', 'value']:
query_params['q.%s' % name].append(query.get(name, ''))
all_params = {}
all_params.update(params)
if q:
all_params.update(query_params)
print('GET: %s %r' % (full_path, all_params))
response = self.app.get(full_path,
params=all_params,
headers=headers,
extra_environ=extra_environ,
expect_errors=expect_errors)
if not expect_errors:
response = response.json
print('GOT:%s' % response)
return response
def validate_link(self, link, bookmark=False):
"""Checks if the given link can get correct data."""
# removes the scheme and net location parts of the link
url_parts = list(urlparse.urlparse(link))
url_parts[0] = url_parts[1] = ''
# bookmark link should not have the version in the URL
if bookmark and url_parts[2].startswith(PATH_PREFIX):
return False
full_path = urlparse.urlunparse(url_parts)
try:
self.get_json(full_path, path_prefix='')
return True
except Exception:
return False

View File

@ -0,0 +1,117 @@
#
# 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 mock
from six.moves import http_client
from webob import exc
from ekko.api.controllers import base as cbase
from ekko.tests.unit.api import base
class TestBase(base.BaseApiTest):
def test_api_setup(self):
pass
def test_bad_uri(self):
response = self.get_json('/bad/path',
expect_errors=True,
headers={"Accept": "application/json"})
self.assertEqual(http_client.NOT_FOUND, response.status_int)
self.assertEqual("application/json", response.content_type)
self.assertTrue(response.json['error_message'])
class TestVersion(base.BaseApiTest):
@mock.patch('ekko.api.controllers.base.Version.parse_headers')
def test_init(self, mock_parse):
a = mock.Mock()
b = mock.Mock()
mock_parse.return_value = (a, b)
v = cbase.Version('test', 'foo', 'bar')
mock_parse.assert_called_with('test', 'foo', 'bar')
self.assertEqual(a, v.major)
self.assertEqual(b, v.minor)
@mock.patch('ekko.api.controllers.base.Version.parse_headers')
def test_repr(self, mock_parse):
mock_parse.return_value = (123, 456)
v = cbase.Version('test', mock.ANY, mock.ANY)
result = "%s" % v
self.assertEqual('123.456', result)
@mock.patch('ekko.api.controllers.base.Version.parse_headers')
def test_repr_with_strings(self, mock_parse):
mock_parse.return_value = ('abc', 'def')
v = cbase.Version('test', mock.ANY, mock.ANY)
result = "%s" % v
self.assertEqual('abc.def', result)
def test_parse_headers_ok(self):
version = cbase.Version.parse_headers(
{cbase.Version.string: '123.456'}, mock.ANY, mock.ANY)
self.assertEqual((123, 456), version)
def test_parse_headers_latest(self):
for s in ['latest', 'LATEST']:
version = cbase.Version.parse_headers(
{cbase.Version.string: s}, mock.ANY, '1.9')
self.assertEqual((1, 9), version)
def test_parse_headers_bad_length(self):
self.assertRaises(
exc.HTTPNotAcceptable,
cbase.Version.parse_headers,
{cbase.Version.string: '1'},
mock.ANY,
mock.ANY)
self.assertRaises(
exc.HTTPNotAcceptable,
cbase.Version.parse_headers,
{cbase.Version.string: '1.2.3'},
mock.ANY,
mock.ANY)
def test_parse_no_header(self):
# this asserts that the minimum version string of "1.1" is applied
version = cbase.Version.parse_headers({}, '1.1', '1.5')
self.assertEqual((1, 1), version)
def test_equals(self):
ver_1 = cbase.Version(
{cbase.Version.string: '123.456'}, mock.ANY, mock.ANY)
ver_2 = cbase.Version(
{cbase.Version.string: '123.456'}, mock.ANY, mock.ANY)
self.assertTrue(hasattr(ver_1, '__eq__'))
self.assertTrue(ver_1 == ver_2)
def test_greaterthan(self):
ver_1 = cbase.Version(
{cbase.Version.string: '123.457'}, mock.ANY, mock.ANY)
ver_2 = cbase.Version(
{cbase.Version.string: '123.456'}, mock.ANY, mock.ANY)
self.assertTrue(hasattr(ver_1, '__gt__'))
self.assertTrue(ver_1 > ver_2)
def test_lessthan(self):
# __lt__ is created by @functools.total_ordering, make sure it exists
# and works
ver_1 = cbase.Version(
{cbase.Version.string: '123.456'}, mock.ANY, mock.ANY)
ver_2 = cbase.Version(
{cbase.Version.string: '123.457'}, mock.ANY, mock.ANY)
self.assertTrue(hasattr(ver_1, '__lt__'))
self.assertTrue(ver_1 < ver_2)

View File

@ -0,0 +1,37 @@
#
# 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 fixtures
from oslo_config import cfg
from ekko.common import config
CONF = cfg.CONF
CONF.import_opt('host', 'ekko.common.service')
class ConfFixture(fixtures.Fixture):
"""Fixture to manage global conf settings."""
def __init__(self, conf):
self.conf = conf
def setUp(self):
super(ConfFixture, self).setUp()
self.conf.set_default('host', 'fake-mini')
# self.conf.set_default('connection', "sqlite://", group='database')
# self.conf.set_default('sqlite_synchronous', False, group='database')
self.conf.set_default('verbose', True)
config.parse_args([], default_config_files=[])
self.addCleanup(self.conf.reset)

View File

@ -6,3 +6,10 @@ pbr>=1.6 # Apache-2.0
six>=1.9.0 # MIT
oslo.utils>=3.4.0 # Apache-2.0
stevedore>=1.5.0 # Apache-2.0
oslo.concurrency>=2.3.0 # Apache-2.0
oslo.config>=3.4.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0
oslo.service>=1.0.0 # Apache-2.0
pecan>=1.0.0 # BSD
WSME>=0.8 # MIT
keystonemiddleware>=4.0.0,!=4.1.0 # Apache-2.0

View File

@ -33,6 +33,8 @@ ekko.storage.compression_drivers =
zlib = ekko.storage._compression_drivers.zlib_:ZlibCompression
ekko.storage.encryption_drivers =
noop = ekko.storage._encryption_drivers.noop:NoopEncryption
console_scripts =
ekko-api = ekko.cmd.api:main
[files]
packages =

View File

@ -10,6 +10,7 @@ install_command =
pip install -U {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
TESTS_DIR=./ekko/tests/unit/
deps = -r{toxinidir}/test-requirements.txt
commands = python setup.py testr --slowest --testr-args='{posargs}'