Support kubernetes python client 4.0.0
From 4.0.0, kubernetes-incubator/client-python uses multiprocessing libaray to send request to k8s cluster, which is not supported by eventlet. This patch introduced the following changes to fix the issue: - Use cotyledon for engine service rather than oslo.service - Update global requirments - Provide separate scripts for api and engine service References: [1] https://github.com/eventlet/eventlet/issues/147 [2] https://bugs.launchpad.net/taskflow/+bug/1225275 Change-Id: Ib99565e00eedc72c388e8ebec6b7f1453f77f30f
This commit is contained in:
parent
76f87d59ec
commit
aa765e2ae9
|
@ -80,7 +80,7 @@ function configure_qinling {
|
||||||
|
|
||||||
# Setup keystone_authtoken section
|
# Setup keystone_authtoken section
|
||||||
configure_auth_token_middleware $QINLING_CONF_FILE qinling $QINLING_AUTH_CACHE_DIR
|
configure_auth_token_middleware $QINLING_CONF_FILE qinling $QINLING_AUTH_CACHE_DIR
|
||||||
iniset $QINLING_CONF_FILE keystone_authtoken auth_uri $KEYSTONE_AUTH_URI_V3
|
iniset $QINLING_CONF_FILE keystone_authtoken www_authenticate_uri $KEYSTONE_AUTH_URI_V3
|
||||||
|
|
||||||
# Setup RabbitMQ credentials
|
# Setup RabbitMQ credentials
|
||||||
iniset_rpc_backend qinling $QINLING_CONF_FILE
|
iniset_rpc_backend qinling $QINLING_CONF_FILE
|
||||||
|
@ -99,8 +99,8 @@ function init_qinling {
|
||||||
|
|
||||||
|
|
||||||
function start_qinling {
|
function start_qinling {
|
||||||
run_process qinling-engine "$QINLING_BIN_DIR/qinling-server --server engine --config-file $QINLING_CONF_FILE"
|
run_process qinling-engine "$QINLING_BIN_DIR/qinling-engine --config-file $QINLING_CONF_FILE"
|
||||||
run_process qinling-api "$QINLING_BIN_DIR/qinling-server --server api --config-file $QINLING_CONF_FILE"
|
run_process qinling-api "$QINLING_BIN_DIR/qinling-api --config-file $QINLING_CONF_FILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -76,7 +76,6 @@ class FunctionsController(rest.RestController):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.storage_provider = storage_base.load_storage_provider(CONF)
|
self.storage_provider = storage_base.load_storage_provider(CONF)
|
||||||
self.engine_client = rpc.get_engine_client()
|
self.engine_client = rpc.get_engine_client()
|
||||||
self.type = 'function'
|
|
||||||
|
|
||||||
super(FunctionsController, self).__init__(*args, **kwargs)
|
super(FunctionsController, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -92,7 +91,7 @@ class FunctionsController(rest.RestController):
|
||||||
@rest_utils.wrap_pecan_controller_exception
|
@rest_utils.wrap_pecan_controller_exception
|
||||||
@pecan.expose()
|
@pecan.expose()
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
LOG.info("Get resource.", resource={'type': self.type, 'id': id})
|
LOG.info("Get function %s.", id)
|
||||||
|
|
||||||
download = strutils.bool_from_string(
|
download = strutils.bool_from_string(
|
||||||
pecan.request.GET.get('download', False)
|
pecan.request.GET.get('download', False)
|
||||||
|
@ -104,6 +103,7 @@ class FunctionsController(rest.RestController):
|
||||||
pecan.override_template('json')
|
pecan.override_template('json')
|
||||||
return resources.Function.from_dict(func_db.to_dict()).to_dict()
|
return resources.Function.from_dict(func_db.to_dict()).to_dict()
|
||||||
else:
|
else:
|
||||||
|
LOG.info("Downloading function %s", id)
|
||||||
source = func_db.code['source']
|
source = func_db.code['source']
|
||||||
|
|
||||||
if source == 'package':
|
if source == 'package':
|
||||||
|
@ -126,11 +126,12 @@ class FunctionsController(rest.RestController):
|
||||||
pecan.response.headers['Content-Disposition'] = (
|
pecan.response.headers['Content-Disposition'] = (
|
||||||
'attachment; filename="%s"' % os.path.basename(func_db.name)
|
'attachment; filename="%s"' % os.path.basename(func_db.name)
|
||||||
)
|
)
|
||||||
|
LOG.info("Downloaded function %s", id)
|
||||||
|
|
||||||
@rest_utils.wrap_pecan_controller_exception
|
@rest_utils.wrap_pecan_controller_exception
|
||||||
@pecan.expose('json')
|
@pecan.expose('json')
|
||||||
def post(self, **kwargs):
|
def post(self, **kwargs):
|
||||||
LOG.info("Creating %s, params: %s", self.type, kwargs)
|
LOG.info("Creating function, params: %s", kwargs)
|
||||||
|
|
||||||
# When using image to create function, runtime_id is not a required
|
# When using image to create function, runtime_id is not a required
|
||||||
# param.
|
# param.
|
||||||
|
@ -221,7 +222,7 @@ class FunctionsController(rest.RestController):
|
||||||
filters = rest_utils.get_filters(
|
filters = rest_utils.get_filters(
|
||||||
project_id=project_id,
|
project_id=project_id,
|
||||||
)
|
)
|
||||||
LOG.info("Get all %ss. filters=%s", self.type, filters)
|
LOG.info("Get all functions. filters=%s", filters)
|
||||||
db_functions = db_api.get_functions(insecure=all_projects, **filters)
|
db_functions = db_api.get_functions(insecure=all_projects, **filters)
|
||||||
functions = [resources.Function.from_dict(db_model.to_dict())
|
functions = [resources.Function.from_dict(db_model.to_dict())
|
||||||
for db_model in db_functions]
|
for db_model in db_functions]
|
||||||
|
@ -232,7 +233,7 @@ class FunctionsController(rest.RestController):
|
||||||
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
@wsme_pecan.wsexpose(None, types.uuid, status_code=204)
|
||||||
def delete(self, id):
|
def delete(self, id):
|
||||||
"""Delete the specified function."""
|
"""Delete the specified function."""
|
||||||
LOG.info("Delete resource.", resource={'type': self.type, 'id': id})
|
LOG.info("Delete function %s.", id)
|
||||||
|
|
||||||
with db_api.transaction():
|
with db_api.transaction():
|
||||||
func_db = db_api.get_function(id)
|
func_db = db_api.get_function(id)
|
||||||
|
@ -280,9 +281,7 @@ class FunctionsController(rest.RestController):
|
||||||
if kwargs.get(key) is not None:
|
if kwargs.get(key) is not None:
|
||||||
values.update({key: kwargs[key]})
|
values.update({key: kwargs[key]})
|
||||||
|
|
||||||
LOG.info('Update resource, params: %s', values,
|
LOG.info('Update function %s, params: %s', id, values)
|
||||||
resource={'type': self.type, 'id': id})
|
|
||||||
|
|
||||||
ctx = context.get_ctx()
|
ctx = context.get_ctx()
|
||||||
|
|
||||||
if set(values.keys()).issubset(set(['name', 'description'])):
|
if set(values.keys()).issubset(set(['name', 'description'])):
|
||||||
|
|
|
@ -14,25 +14,34 @@
|
||||||
|
|
||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
from oslo_service import service
|
from oslo_service import service
|
||||||
from oslo_service import wsgi
|
from oslo_service import wsgi
|
||||||
|
|
||||||
from qinling.api import app
|
from qinling.api import app
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class WSGIService(service.ServiceBase):
|
class WSGIService(service.ServiceBase):
|
||||||
"""Provides ability to launch Mistral API from wsgi app."""
|
"""Provides ability to launch Mistral API from wsgi app."""
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self):
|
||||||
self.name = name
|
|
||||||
self.app = app.setup_app()
|
self.app = app.setup_app()
|
||||||
self.workers = (
|
|
||||||
cfg.CONF.api.api_workers or processutils.get_worker_count()
|
self.workers = CONF.api.api_workers
|
||||||
)
|
if self.workers is not None and self.workers < 1:
|
||||||
|
LOG.warning(
|
||||||
|
"Value of config option api_workers must be integer "
|
||||||
|
"greater than 1. Input value ignored."
|
||||||
|
)
|
||||||
|
self.workers = None
|
||||||
|
self.workers = self.workers or processutils.get_worker_count()
|
||||||
|
|
||||||
self.server = wsgi.Server(
|
self.server = wsgi.Server(
|
||||||
cfg.CONF,
|
cfg.CONF,
|
||||||
name,
|
"qinling_api",
|
||||||
self.app,
|
self.app,
|
||||||
host=cfg.CONF.api.host,
|
host=cfg.CONF.api.host,
|
||||||
port=cfg.CONF.api.port,
|
port=cfg.CONF.api.port,
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
# 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 eventlet
|
||||||
|
eventlet.monkey_patch()
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_service import service
|
||||||
|
|
||||||
|
from qinling.api import service as api_service
|
||||||
|
from qinling import config
|
||||||
|
from qinling import rpc
|
||||||
|
from qinling.utils import common
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
config.parse_args(args=common.get_properly_ordered_parameters())
|
||||||
|
common.print_server_info("api")
|
||||||
|
logging.setup(CONF, 'qinling')
|
||||||
|
# Initialize RPC configuration.
|
||||||
|
rpc.get_transport()
|
||||||
|
|
||||||
|
api_server = api_service.WSGIService()
|
||||||
|
launcher = service.launch(CONF, api_server, workers=api_server.workers)
|
||||||
|
launcher.wait()
|
||||||
|
except RuntimeError as excp:
|
||||||
|
sys.stderr.write("ERROR: %s\n" % excp)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,49 @@
|
||||||
|
# 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 cotyledon
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from qinling import config
|
||||||
|
from qinling.engine import service as eng_service
|
||||||
|
from qinling import rpc
|
||||||
|
from qinling.utils import common
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
config.parse_args(args=common.get_properly_ordered_parameters())
|
||||||
|
common.print_server_info("engine")
|
||||||
|
logging.setup(CONF, 'qinling')
|
||||||
|
# Initialize RPC configuration.
|
||||||
|
rpc.get_transport()
|
||||||
|
|
||||||
|
sm = cotyledon.ServiceManager()
|
||||||
|
sm.add(
|
||||||
|
eng_service.EngineService,
|
||||||
|
workers=1,
|
||||||
|
)
|
||||||
|
sm.run()
|
||||||
|
except RuntimeError as excp:
|
||||||
|
sys.stderr.write("ERROR: %s\n" % excp)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -1,161 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
||||||
# If ../qinling/__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
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_service import service
|
|
||||||
|
|
||||||
from qinling.api import service as api_service
|
|
||||||
from qinling import config
|
|
||||||
from qinling.engine import service as eng_service
|
|
||||||
from qinling import rpc
|
|
||||||
from qinling import version
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
|
|
||||||
|
|
||||||
def launch_api():
|
|
||||||
try:
|
|
||||||
server = api_service.WSGIService('qinling_api')
|
|
||||||
launcher = service.launch(CONF, server, workers=server.workers)
|
|
||||||
return launcher
|
|
||||||
except Exception as e:
|
|
||||||
sys.stderr.write("ERROR: %s\n" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def launch_engine():
|
|
||||||
try:
|
|
||||||
server = eng_service.EngineService()
|
|
||||||
launcher = service.launch(CONF, server)
|
|
||||||
return launcher
|
|
||||||
except Exception as e:
|
|
||||||
sys.stderr.write("ERROR: %s\n" % e)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def launch_any(options):
|
|
||||||
launchers = [LAUNCH_OPTIONS[option]() for option in options]
|
|
||||||
for l in launchers:
|
|
||||||
l.wait()
|
|
||||||
|
|
||||||
|
|
||||||
LAUNCH_OPTIONS = {
|
|
||||||
'api': launch_api,
|
|
||||||
'engine': launch_engine
|
|
||||||
}
|
|
||||||
|
|
||||||
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, 'Qinling')
|
|
||||||
|
|
||||||
# Initialize RPC configuration.
|
|
||||||
rpc.get_transport()
|
|
||||||
|
|
||||||
if cfg.CONF.server == ['all']:
|
|
||||||
launch_any(LAUNCH_OPTIONS.keys())
|
|
||||||
else:
|
|
||||||
if set(cfg.CONF.server) - set(LAUNCH_OPTIONS.keys()):
|
|
||||||
raise Exception('Valid options are all or any combination of '
|
|
||||||
', '.join(LAUNCH_OPTIONS.keys()))
|
|
||||||
|
|
||||||
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())
|
|
|
@ -38,6 +38,7 @@ api_opts = [
|
||||||
),
|
),
|
||||||
cfg.IntOpt(
|
cfg.IntOpt(
|
||||||
'api_workers',
|
'api_workers',
|
||||||
|
default=1,
|
||||||
help='Number of workers for Qinling API service '
|
help='Number of workers for Qinling API service '
|
||||||
'default is equal to the number of CPUs available if that can '
|
'default is equal to the number of CPUs available if that can '
|
||||||
'be determined, else a default worker count of 1 is returned.'
|
'be determined, else a default worker count of 1 is returned.'
|
||||||
|
@ -134,7 +135,7 @@ kubernetes_opts = [
|
||||||
),
|
),
|
||||||
cfg.StrOpt(
|
cfg.StrOpt(
|
||||||
'kube_host',
|
'kube_host',
|
||||||
default='127.0.0.1:8001',
|
default='http://127.0.0.1:8001',
|
||||||
help='Kubernetes server address, e.g. you can start a proxy to the '
|
help='Kubernetes server address, e.g. you can start a proxy to the '
|
||||||
'Kubernetes API server by using "kubectl proxy" command.'
|
'Kubernetes API server by using "kubectl proxy" command.'
|
||||||
),
|
),
|
||||||
|
@ -194,6 +195,7 @@ def parse_args(args=None, usage=None, default_config_files=None):
|
||||||
'keystoneclient=INFO',
|
'keystoneclient=INFO',
|
||||||
'requests.packages.urllib3.connectionpool=CRITICAL',
|
'requests.packages.urllib3.connectionpool=CRITICAL',
|
||||||
'urllib3.connectionpool=CRITICAL',
|
'urllib3.connectionpool=CRITICAL',
|
||||||
|
'cotyledon=INFO'
|
||||||
]
|
]
|
||||||
default_log_levels = log.get_default_log_levels()
|
default_log_levels = log.get_default_log_levels()
|
||||||
default_log_levels.extend(_DEFAULT_LOG_LEVELS)
|
default_log_levels.extend(_DEFAULT_LOG_LEVELS)
|
||||||
|
|
|
@ -12,11 +12,11 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import cotyledon
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import oslo_messaging as messaging
|
import oslo_messaging as messaging
|
||||||
from oslo_messaging.rpc import dispatcher
|
from oslo_messaging.rpc import dispatcher
|
||||||
from oslo_service import service
|
|
||||||
|
|
||||||
from qinling.db import api as db_api
|
from qinling.db import api as db_api
|
||||||
from qinling.engine import default_engine as engine
|
from qinling.engine import default_engine as engine
|
||||||
|
@ -28,12 +28,12 @@ LOG = logging.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
class EngineService(service.Service):
|
class EngineService(cotyledon.Service):
|
||||||
def __init__(self):
|
def __init__(self, worker_id):
|
||||||
super(EngineService, self).__init__()
|
super(EngineService, self).__init__(worker_id)
|
||||||
self.server = None
|
self.server = None
|
||||||
|
|
||||||
def start(self):
|
def run(self):
|
||||||
orchestrator = orchestra_base.load_orchestrator(CONF)
|
orchestrator = orchestra_base.load_orchestrator(CONF)
|
||||||
db_api.setup_db()
|
db_api.setup_db()
|
||||||
|
|
||||||
|
@ -47,11 +47,10 @@ class EngineService(service.Service):
|
||||||
transport,
|
transport,
|
||||||
target,
|
target,
|
||||||
[endpoint],
|
[endpoint],
|
||||||
executor='eventlet',
|
executor='threading',
|
||||||
access_policy=access_policy,
|
access_policy=access_policy,
|
||||||
serializer=rpc.ContextSerializer(
|
serializer=rpc.ContextSerializer(
|
||||||
messaging.serializer.JsonPayloadSerializer()
|
messaging.serializer.JsonPayloadSerializer())
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
LOG.info('Starting function mapping periodic task...')
|
LOG.info('Starting function mapping periodic task...')
|
||||||
|
@ -60,25 +59,10 @@ class EngineService(service.Service):
|
||||||
LOG.info('Starting engine...')
|
LOG.info('Starting engine...')
|
||||||
self.server.start()
|
self.server.start()
|
||||||
|
|
||||||
super(EngineService, self).start()
|
def terminate(self):
|
||||||
|
|
||||||
def stop(self, graceful=False):
|
|
||||||
periodics.stop()
|
periodics.stop()
|
||||||
|
|
||||||
if self.server:
|
if self.server:
|
||||||
LOG.info('Stopping engine...')
|
LOG.info('Stopping engine...')
|
||||||
self.server.stop()
|
self.server.stop()
|
||||||
if graceful:
|
self.server.wait()
|
||||||
LOG.info(
|
|
||||||
'Consumer successfully stopped. Waiting for final '
|
|
||||||
'messages to be processed...'
|
|
||||||
)
|
|
||||||
self.server.wait()
|
|
||||||
|
|
||||||
super(EngineService, self).stop(graceful=graceful)
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
if self.server:
|
|
||||||
self.server.reset()
|
|
||||||
|
|
||||||
super(EngineService, self).reset()
|
|
||||||
|
|
|
@ -82,7 +82,7 @@ def get_request_data(conf, function_id, execution_id, input, entry, trust_id):
|
||||||
data.update(
|
data.update(
|
||||||
{
|
{
|
||||||
'token': ctx.auth_token,
|
'token': ctx.auth_token,
|
||||||
'auth_url': conf.keystone_authtoken.auth_uri,
|
'auth_url': conf.keystone_authtoken.www_authenticate_uri,
|
||||||
'username': conf.keystone_authtoken.username,
|
'username': conf.keystone_authtoken.username,
|
||||||
'password': conf.keystone_authtoken.password,
|
'password': conf.keystone_authtoken.password,
|
||||||
'trust_id': trust_id
|
'trust_id': trust_id
|
||||||
|
|
|
@ -18,7 +18,6 @@ import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
from kubernetes import client
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
import requests
|
import requests
|
||||||
import tenacity
|
import tenacity
|
||||||
|
@ -27,6 +26,7 @@ import yaml
|
||||||
from qinling.engine import utils
|
from qinling.engine import utils
|
||||||
from qinling import exceptions as exc
|
from qinling import exceptions as exc
|
||||||
from qinling.orchestrator import base
|
from qinling.orchestrator import base
|
||||||
|
from qinling.orchestrator.kubernetes import utils as k8s_util
|
||||||
from qinling.utils import common
|
from qinling.utils import common
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
@ -38,9 +38,9 @@ class KubernetesManager(base.OrchestratorBase):
|
||||||
def __init__(self, conf):
|
def __init__(self, conf):
|
||||||
self.conf = conf
|
self.conf = conf
|
||||||
|
|
||||||
client.Configuration().host = self.conf.kubernetes.kube_host
|
clients = k8s_util.get_k8s_clients(self.conf)
|
||||||
self.v1 = client.CoreV1Api()
|
self.v1 = clients['v1']
|
||||||
self.v1extention = client.ExtensionsV1beta1Api()
|
self.v1extention = clients['v1extention']
|
||||||
|
|
||||||
# Create namespace if not exists
|
# Create namespace if not exists
|
||||||
self._ensure_namespace()
|
self._ensure_namespace()
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Copyright 2018 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 kubernetes.client import api_client
|
||||||
|
from kubernetes.client.apis import core_v1_api
|
||||||
|
from kubernetes.client.apis import extensions_v1beta1_api
|
||||||
|
from kubernetes.client import configuration as k8s_config
|
||||||
|
|
||||||
|
|
||||||
|
def get_k8s_clients(conf):
|
||||||
|
config = k8s_config.Configuration()
|
||||||
|
config.host = conf.kubernetes.kube_host
|
||||||
|
config.verify_ssl = False
|
||||||
|
client = api_client.ApiClient(configuration=config)
|
||||||
|
v1 = core_v1_api.CoreV1Api(client)
|
||||||
|
v1extention = extensions_v1beta1_api.ExtensionsV1beta1Api(client)
|
||||||
|
|
||||||
|
clients = {
|
||||||
|
'v1': v1,
|
||||||
|
'v1extention': v1extention
|
||||||
|
}
|
||||||
|
|
||||||
|
return clients
|
|
@ -39,7 +39,7 @@ class FileSystemStorage(base.PackageStorage):
|
||||||
:param function: Function ID.
|
:param function: Function ID.
|
||||||
:param data: Package data.
|
:param data: Package data.
|
||||||
"""
|
"""
|
||||||
LOG.info(
|
LOG.debug(
|
||||||
'Store package, function: %s, project: %s', function, project_id
|
'Store package, function: %s, project: %s', function, project_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ class FileSystemStorage(base.PackageStorage):
|
||||||
:param function: Function ID.
|
:param function: Function ID.
|
||||||
:return: File descriptor that needs to close outside.
|
:return: File descriptor that needs to close outside.
|
||||||
"""
|
"""
|
||||||
LOG.info(
|
LOG.debug(
|
||||||
'Get package data, function: %s, project: %s', function, project_id
|
'Get package data, function: %s, project: %s', function, project_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -80,11 +80,12 @@ class FileSystemStorage(base.PackageStorage):
|
||||||
)
|
)
|
||||||
|
|
||||||
f = open(func_zip, 'rb')
|
f = open(func_zip, 'rb')
|
||||||
|
LOG.debug('Found package data')
|
||||||
|
|
||||||
return f
|
return f
|
||||||
|
|
||||||
def delete(self, project_id, function):
|
def delete(self, project_id, function):
|
||||||
LOG.info(
|
LOG.debug(
|
||||||
'Delete package data, function: %s, project: %s', function,
|
'Delete package data, function: %s, project: %s', function,
|
||||||
project_id
|
project_id
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,11 +12,65 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import functools
|
import functools
|
||||||
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
import six
|
import six
|
||||||
|
|
||||||
|
from qinling import version
|
||||||
|
|
||||||
|
|
||||||
|
def print_server_info(service):
|
||||||
|
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()
|
||||||
|
|
||||||
|
print(QINLING_TITLE)
|
||||||
|
print('Launching server components %s...' % service)
|
||||||
|
|
||||||
|
|
||||||
|
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 convert_dict_to_string(d):
|
def convert_dict_to_string(d):
|
||||||
temp_list = ['%s=%s' % (k, v) for k, v in d.items()]
|
temp_list = ['%s=%s' % (k, v) for k, v in d.items()]
|
||||||
|
|
|
@ -30,7 +30,7 @@ def _get_user_keystone_session():
|
||||||
ctx = context.get_ctx()
|
ctx = context.get_ctx()
|
||||||
|
|
||||||
auth = v3.Token(
|
auth = v3.Token(
|
||||||
auth_url=CONF.keystone_authtoken.auth_uri,
|
auth_url=CONF.keystone_authtoken.www_authenticate_uri,
|
||||||
token=ctx.auth_token,
|
token=ctx.auth_token,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ def get_swiftclient():
|
||||||
@common.disable_ssl_warnings
|
@common.disable_ssl_warnings
|
||||||
def get_user_client():
|
def get_user_client():
|
||||||
ctx = context.get_ctx()
|
ctx = context.get_ctx()
|
||||||
auth_url = CONF.keystone_authtoken.auth_uri
|
auth_url = CONF.keystone_authtoken.www_authenticate_uri
|
||||||
client = ks_client.Client(
|
client = ks_client.Client(
|
||||||
user_id=ctx.user,
|
user_id=ctx.user,
|
||||||
token=ctx.auth_token,
|
token=ctx.auth_token,
|
||||||
|
@ -67,7 +67,7 @@ def get_service_client():
|
||||||
username=CONF.keystone_authtoken.username,
|
username=CONF.keystone_authtoken.username,
|
||||||
password=CONF.keystone_authtoken.password,
|
password=CONF.keystone_authtoken.password,
|
||||||
project_name=CONF.keystone_authtoken.project_name,
|
project_name=CONF.keystone_authtoken.project_name,
|
||||||
auth_url=CONF.keystone_authtoken.auth_uri,
|
auth_url=CONF.keystone_authtoken.www_authenticate_uri,
|
||||||
user_domain_name=CONF.keystone_authtoken.user_domain_name,
|
user_domain_name=CONF.keystone_authtoken.user_domain_name,
|
||||||
project_domain_name=CONF.keystone_authtoken.project_domain_name
|
project_domain_name=CONF.keystone_authtoken.project_domain_name
|
||||||
)
|
)
|
||||||
|
@ -80,7 +80,7 @@ def get_trust_client(trust_id):
|
||||||
client = ks_client.Client(
|
client = ks_client.Client(
|
||||||
username=CONF.keystone_authtoken.username,
|
username=CONF.keystone_authtoken.username,
|
||||||
password=CONF.keystone_authtoken.password,
|
password=CONF.keystone_authtoken.password,
|
||||||
auth_url=CONF.keystone_authtoken.auth_uri,
|
auth_url=CONF.keystone_authtoken.www_authenticate_uri,
|
||||||
trust_id=trust_id
|
trust_id=trust_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -41,6 +41,6 @@ QinlingGroup = [
|
||||||
'publicURL', 'adminURL', 'internalURL'],
|
'publicURL', 'adminURL', 'internalURL'],
|
||||||
help="The endpoint type to use for the qinling service."),
|
help="The endpoint type to use for the qinling service."),
|
||||||
cfg.StrOpt('kube_host',
|
cfg.StrOpt('kube_host',
|
||||||
default='127.0.0.1:8001',
|
default='http://127.0.0.1:8001',
|
||||||
help="The Kubernetes service address."),
|
help="The Kubernetes service address."),
|
||||||
]
|
]
|
||||||
|
|
|
@ -13,12 +13,13 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from kubernetes import client as k8s_client
|
|
||||||
from tempest import config
|
from tempest import config
|
||||||
from tempest.lib.common.utils import data_utils
|
from tempest.lib.common.utils import data_utils
|
||||||
from tempest import test
|
from tempest import test
|
||||||
import tenacity
|
import tenacity
|
||||||
|
|
||||||
|
from qinling_tempest_plugin.tests import utils
|
||||||
|
|
||||||
CONF = config.CONF
|
CONF = config.CONF
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,9 +42,9 @@ class BaseQinlingTest(test.BaseTestCase):
|
||||||
cls.admin_client = cls.os_admin.qinling.QinlingClient()
|
cls.admin_client = cls.os_admin.qinling.QinlingClient()
|
||||||
|
|
||||||
# Initilize k8s client
|
# Initilize k8s client
|
||||||
k8s_client.Configuration().host = CONF.qinling.kube_host
|
clients = utils.get_k8s_clients(CONF)
|
||||||
cls.k8s_v1 = k8s_client.CoreV1Api()
|
cls.k8s_v1 = clients['v1']
|
||||||
cls.k8s_v1extention = k8s_client.ExtensionsV1beta1Api()
|
cls.k8s_v1extention = clients['v1extention']
|
||||||
cls.namespace = 'qinling'
|
cls.namespace = 'qinling'
|
||||||
|
|
||||||
@tenacity.retry(
|
@tenacity.retry(
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Copyright 2018 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 kubernetes.client import api_client
|
||||||
|
from kubernetes.client.apis import core_v1_api
|
||||||
|
from kubernetes.client.apis import extensions_v1beta1_api
|
||||||
|
from kubernetes.client import configuration as k8s_config
|
||||||
|
|
||||||
|
|
||||||
|
def get_k8s_clients(conf):
|
||||||
|
config = k8s_config.Configuration()
|
||||||
|
config.host = conf.qinling.kube_host
|
||||||
|
config.verify_ssl = False
|
||||||
|
client = api_client.ApiClient(configuration=config)
|
||||||
|
v1 = core_v1_api.CoreV1Api(client)
|
||||||
|
v1extention = extensions_v1beta1_api.ExtensionsV1beta1Api(client)
|
||||||
|
|
||||||
|
clients = {
|
||||||
|
'v1': v1,
|
||||||
|
'v1extention': v1extention
|
||||||
|
}
|
||||||
|
|
||||||
|
return clients
|
|
@ -4,30 +4,30 @@
|
||||||
|
|
||||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||||
Babel!=2.4.0,>=2.3.4 # BSD
|
Babel!=2.4.0,>=2.3.4 # BSD
|
||||||
eventlet!=0.18.3,!=0.20.1,<0.21.0,>=0.18.2 # MIT
|
keystoneauth1>=3.3.0 # Apache-2.0
|
||||||
keystoneauth1>=2.21.0 # Apache-2.0
|
keystonemiddleware>=4.17.0 # Apache-2.0
|
||||||
keystonemiddleware>=4.12.0 # Apache-2.0
|
oslo.concurrency>=3.20.0 # Apache-2.0
|
||||||
oslo.concurrency>=3.8.0 # Apache-2.0
|
oslo.config>=5.1.0 # Apache-2.0
|
||||||
oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0
|
oslo.db>=4.27.0 # Apache-2.0
|
||||||
oslo.db>=4.23.0 # Apache-2.0
|
oslo.messaging>=5.29.0 # Apache-2.0
|
||||||
oslo.messaging!=5.25.0,>=5.24.2 # Apache-2.0
|
oslo.policy>=1.30.0 # Apache-2.0
|
||||||
oslo.policy>=1.23.0 # Apache-2.0
|
oslo.utils>=3.33.0 # Apache-2.0
|
||||||
oslo.utils>=3.20.0 # Apache-2.0
|
oslo.log>=3.30.0 # Apache-2.0
|
||||||
oslo.log>=3.22.0 # Apache-2.0
|
oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0
|
||||||
oslo.serialization>=1.10.0 # Apache-2.0
|
oslo.service!=1.28.1,>=1.24.0 # Apache-2.0
|
||||||
oslo.service>=1.10.0 # Apache-2.0
|
|
||||||
pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD
|
pecan!=1.0.2,!=1.0.3,!=1.0.4,!=1.2,>=1.0.0 # BSD
|
||||||
setuptools!=24.0.0,!=34.0.0,!=34.0.1,!=34.0.2,!=34.0.3,!=34.1.0,!=34.1.1,!=34.2.0,!=34.3.0,!=34.3.1,!=34.3.2,>=16.0 # PSF/ZPL
|
setuptools!=24.0.0,!=34.0.0,!=34.0.1,!=34.0.2,!=34.0.3,!=34.1.0,!=34.1.1,!=34.2.0,!=34.3.0,!=34.3.1,!=34.3.2,!=36.2.0,>=16.0 # PSF/ZPL
|
||||||
six>=1.9.0 # MIT
|
six>=1.10.0 # MIT
|
||||||
SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT
|
SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT
|
||||||
sqlalchemy-migrate>=0.11.0 # Apache-2.0
|
sqlalchemy-migrate>=0.11.0 # Apache-2.0
|
||||||
stevedore>=1.20.0 # Apache-2.0
|
stevedore>=1.20.0 # Apache-2.0
|
||||||
WSME>=0.8 # MIT
|
WSME>=0.8.0 # MIT
|
||||||
kubernetes>=1.0.0b1 # Apache-2.0
|
kubernetes>=4.0.0 # Apache-2.0
|
||||||
PyYAML>=3.10.0 # MIT
|
PyYAML>=3.10 # MIT
|
||||||
python-swiftclient>=3.2.0 # Apache-2.0
|
python-swiftclient>=3.2.0 # Apache-2.0
|
||||||
croniter>=0.3.4 # MIT License
|
croniter>=0.3.4 # MIT License
|
||||||
python-dateutil>=2.4.2 # BSD
|
python-dateutil>=2.4.2 # BSD
|
||||||
tenacity>=3.2.1 # Apache-2.0
|
tenacity>=3.2.1 # Apache-2.0
|
||||||
PyMySQL>=0.7.6 # MIT License
|
PyMySQL>=0.7.6 # MIT License
|
||||||
etcd3gw>=0.2.0 # Apache-2.0
|
etcd3gw>=0.2.0 # Apache-2.0
|
||||||
|
cotyledon>=1.3.0 # Apache-2.0
|
||||||
|
|
|
@ -25,7 +25,8 @@ packages =
|
||||||
|
|
||||||
[entry_points]
|
[entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
qinling-server = qinling.cmd.launch:main
|
qinling-api = qinling.cmd.api:main
|
||||||
|
qinling-engine = qinling.cmd.engine:main
|
||||||
qinling-db-manage = qinling.db.sqlalchemy.migration.cli:main
|
qinling-db-manage = qinling.db.sqlalchemy.migration.cli:main
|
||||||
|
|
||||||
qinling.storage.provider:
|
qinling.storage.provider:
|
||||||
|
|
2
setup.py
2
setup.py
|
@ -25,5 +25,5 @@ except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
setup_requires=['pbr>=2.0.0'],
|
setup_requires=['pbr'],
|
||||||
pbr=True)
|
pbr=True)
|
||||||
|
|
|
@ -8,11 +8,10 @@ sphinx>=1.6.2 # BSD
|
||||||
oslotest>=1.10.0 # Apache-2.0
|
oslotest>=1.10.0 # Apache-2.0
|
||||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||||
testscenarios>=0.4 # Apache-2.0/BSD
|
testscenarios>=0.4 # Apache-2.0/BSD
|
||||||
testtools>=1.4.0 # MIT
|
testtools>=2.2.0 # MIT
|
||||||
tempest>=16.1.0 # Apache-2.0
|
tempest>=17.1.0 # Apache-2.0
|
||||||
futurist>=1.2.0 # Apache-2.0
|
futurist>=1.2.0 # Apache-2.0
|
||||||
|
openstackdocstheme>=1.17.0 # Apache-2.0
|
||||||
|
reno>=2.5.0 # Apache-2.0
|
||||||
|
|
||||||
openstackdocstheme>=1.16.0 # Apache-2.0
|
kubernetes>=4.0.0 # Apache-2.0
|
||||||
|
|
||||||
# releasenotes
|
|
||||||
reno!=2.3.1,>=1.8.0 # Apache-2.0
|
|
||||||
|
|
Loading…
Reference in New Issue