diff --git a/.testr.conf b/.testr.conf index 6d83b3c4..c4b60c06 100644 --- a/.testr.conf +++ b/.testr.conf @@ -2,6 +2,7 @@ test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ - ${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION + OS_LOG_CAPTURE=${OS_LOG_CAPTURE:-1} \ + ${PYTHON:-python} -m subunit.run discover -t ./ ./qinling/tests $LISTOPT $IDOPTION test_id_option=--load-list $IDFILE test_list_option=--list diff --git a/devstack/plugin.sh b/devstack/plugin.sh index a39db6d2..0da3d247 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -19,6 +19,13 @@ function install_qinlingclient { } +function install_k8s { + pushd $QINLING_DIR + source tools/gate/setup_gate.sh + popd +} + + function create_qinling_accounts { create_service_user "qinling" "admin" @@ -60,6 +67,7 @@ function configure_qinling { iniset $QINLING_CONF_FILE DEFAULT debug $QINLING_DEBUG iniset $QINLING_CONF_FILE DEFAULT server all iniset $QINLING_CONF_FILE storage file_system_dir $QINLING_FUNCTION_STORAGE_DIR + iniset $QINLING_CONF_FILE kubernetes qinling_service_address $DEFAULT_HOST_IP # Setup keystone_authtoken section configure_auth_token_middleware $QINLING_CONF_FILE qinling $QINLING_AUTH_CACHE_DIR @@ -116,6 +124,10 @@ if is_service_enabled qinling; then if is_service_enabled key; then create_qinling_accounts fi + + echo_summary "Installing kubernetes cluster" + install_k8s + configure_qinling elif [[ "$1" == "stack" && "$2" == "extra" ]]; then diff --git a/qinling_tempest_plugin/config.py b/qinling_tempest_plugin/config.py index 672237c4..7438081a 100644 --- a/qinling_tempest_plugin/config.py +++ b/qinling_tempest_plugin/config.py @@ -13,5 +13,34 @@ # License for the specific language governing permissions and limitations # under the License. -if __name__ == '__main__': - pass +from oslo_config import cfg + +service_option = cfg.BoolOpt( + 'qinling', + default=True, + help="Whether or not Qinling is expected to be" + "available" +) + + +qingling_group = cfg.OptGroup(name="qingling", title="Qinling Service Options") + +QinlingGroup = [ + cfg.StrOpt("region", + default="", + help="The region name to use. If empty, the value " + "of identity.region is used instead. If no such region " + "is found in the service catalog, the first found one is " + "used."), + cfg.StrOpt("catalog_type", + default="function", + help="Catalog type of the Qinling service."), + cfg.StrOpt('endpoint_type', + default='publicURL', + choices=['public', 'admin', 'internal', + 'publicURL', 'adminURL', 'internalURL'], + help="The endpoint type to use for the qinling service."), + cfg.StrOpt('kube_host', + default='127.0.0.1:8001', + help="The Kubernetes service address."), +] diff --git a/qinling_tempest_plugin/plugin.py b/qinling_tempest_plugin/plugin.py index b2394ebc..a614c35e 100644 --- a/qinling_tempest_plugin/plugin.py +++ b/qinling_tempest_plugin/plugin.py @@ -16,8 +16,11 @@ import os +from tempest import config from tempest.test_discover import plugins +from qinling_tempest_plugin import config as qinling_config + class QinlingTempestPlugin(plugins.TempestPlugin): def load_tests(self): @@ -29,7 +32,26 @@ class QinlingTempestPlugin(plugins.TempestPlugin): return full_test_dir, base_path def register_opts(self, conf): - pass + conf.register_opt( + qinling_config.service_option, group='service_available' + ) + + conf.register_group(qinling_config.qingling_group) + conf.register_opts(qinling_config.QinlingGroup, group='qinling') def get_opt_lists(self): - pass + return [ + ('service_available', [qinling_config.service_option]), + (qinling_config.qingling_group.name, qinling_config.QinlingGroup) + ] + + def get_service_clients(self): + qinling_config = config.service_client_config('qinling') + params = { + 'name': 'qinling', + 'service_version': 'qinling', + 'module_path': 'qinling_tempest_plugin.services.qinling_client', + 'client_names': ['QinlingClient'], + } + params.update(qinling_config) + return [params] diff --git a/qinling_tempest_plugin/pre_test_hook.sh b/qinling_tempest_plugin/pre_test_hook.sh index b3819753..382f0cbd 100755 --- a/qinling_tempest_plugin/pre_test_hook.sh +++ b/qinling_tempest_plugin/pre_test_hook.sh @@ -14,15 +14,4 @@ # This script is executed inside pre_test_hook function in devstack gate. -set -ex - -export localconf=$BASE/new/devstack/local.conf -export QINLING_CONF=/etc/qinling/qinling.conf - -# Install k8s cluster -pushd $BASE/new/qinling/ -bash tools/gate/setup_gate.sh -popd - -echo -e "[[post-config|$QINLING_CONF]]\n[kubernetes]\n" >> $localconf -echo -e "qinling_service_address=${DEFAULT_HOST_IP}\n" >> $localconf +echo "Pass" diff --git a/qinling_tempest_plugin/services/base.py b/qinling_tempest_plugin/services/base.py new file mode 100644 index 00000000..68687c46 --- /dev/null +++ b/qinling_tempest_plugin/services/base.py @@ -0,0 +1,44 @@ +# Copyright 2017 Catalyst IT Ltd +# +# 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 tempest.lib.common import rest_client + + +class QinlingClientBase(rest_client.RestClient): + def __init__(self, auth_provider, **kwargs): + super(QinlingClientBase, self).__init__(auth_provider, **kwargs) + + self.runtimes = [] + + def get_list_objs(self, url_path): + resp, body = self.get(url_path) + + return resp, json.loads(body) + + def delete_obj(self, obj, id): + return self.delete('{obj}/{id}'.format(obj=obj, id=id)) + + def get_obj(self, obj, id): + resp, body = self.get('{obj}/{id}'.format(obj=obj, id=id)) + + return resp, json.loads(body) + + def post_json(self, url_path, obj, extra_headers={}): + headers = {"Content-Type": "application/json"} + headers = dict(headers, **extra_headers) + resp, body = self.post(url_path, json.dumps(obj), headers=headers) + + return resp, json.loads(body) diff --git a/qinling_tempest_plugin/services/qinling_client.py b/qinling_tempest_plugin/services/qinling_client.py new file mode 100644 index 00000000..93514e18 --- /dev/null +++ b/qinling_tempest_plugin/services/qinling_client.py @@ -0,0 +1,32 @@ +# Copyright 2017 Catalyst IT Ltd +# +# 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 qinling_tempest_plugin.services import base as client_base + + +class QinlingClient(client_base.QinlingClientBase): + """Tempest REST client for Qinling.""" + + def create_runtime(self, image, name=None): + body = {"image": image} + + if name: + body.update({'name': name}) + + resp, body = self.post('runtimes', json.dumps(body)) + self.runtimes.append(json.loads(body)['id']) + + return resp, json.loads(body) diff --git a/qinling_tempest_plugin/tests/api/test_runtimes.py b/qinling_tempest_plugin/tests/api/test_runtimes.py new file mode 100644 index 00000000..0dc70c48 --- /dev/null +++ b/qinling_tempest_plugin/tests/api/test_runtimes.py @@ -0,0 +1,55 @@ +# Copyright 2017 Catalyst IT Ltd +# +# 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 tempest.lib.common.utils import data_utils +from tempest.lib import decorators + +from qinling_tempest_plugin.tests import base + + +class RuntimesTest(base.BaseQinlingTest): + name_prefix = 'RuntimesTest' + + @decorators.idempotent_id('fdc2f07f-dd1d-4981-86d3-5bc7908d9a9b') + def test_create_delete_runtime(self): + name = data_utils.rand_name('runtime', prefix=self.name_prefix) + + req_body = { + 'name': name, + 'image': 'openstackqinling/python-runtime' + } + resp, body = self.qinling_client.post_json('runtimes', req_body) + runtime_id = body['id'] + + self.assertEqual(201, resp.status) + self.assertEqual(name, body['name']) + + resp, body = self.qinling_client.get_list_objs('runtimes') + + self.assertEqual(200, resp.status) + self.assertIn( + runtime_id, + [runtime['id'] for runtime in body['runtimes']] + ) + + deploy = self.k8s_v1extention.read_namespaced_deployment( + runtime_id, + namespace=self.namespace + ) + + self.assertEqual(runtime_id, deploy.metadata.name) + + resp, _ = self.qinling_client.delete_obj('runtimes', runtime_id) + + self.assertEqual(204, resp.status) diff --git a/qinling_tempest_plugin/tests/base.py b/qinling_tempest_plugin/tests/base.py new file mode 100644 index 00000000..0596de99 --- /dev/null +++ b/qinling_tempest_plugin/tests/base.py @@ -0,0 +1,52 @@ +# Copyright 2017 Catalyst IT Ltd +# +# 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 import client as k8s_client +from tempest import config +from tempest import test + +CONF = config.CONF + + +class BaseQinlingTest(test.BaseTestCase): + credentials = ('primary',) + force_tenant_isolation = False + + @classmethod + def skip_checks(cls): + super(BaseQinlingTest, cls).skip_checks() + + if not CONF.service_available.qinling: + raise cls.skipException("Qinling service is not available.") + + @classmethod + def setup_clients(cls): + super(BaseQinlingTest, cls).setup_clients() + + # os here is tempest.lib.services.clients.ServiceClients object + os = getattr(cls, 'os_%s' % cls.credentials[0]) + cls.qinling_client = os.qinling.QinlingClient() + + if CONF.identity.auth_version == 'v3': + project_id = os.auth_provider.auth_data[1]['project']['id'] + else: + project_id = os.auth_provider.auth_data[1]['token']['tenant']['id'] + cls.tenant_id = project_id + cls.user_id = os.auth_provider.auth_data[1]['user']['id'] + + # Initilize k8s client + k8s_client.Configuration().host = CONF.qinling.kube_host + cls.k8s_v1 = k8s_client.CoreV1Api() + cls.k8s_v1extention = k8s_client.ExtensionsV1beta1Api() + cls.namespace = 'qinling' diff --git a/test-requirements.txt b/test-requirements.txt index 179c0fb0..71163e16 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,9 +8,11 @@ python-subunit>=0.0.18 # Apache-2.0/BSD sphinx>=1.6.2 # BSD oslosphinx>=4.7.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 +os-testr>=0.8.0 # Apache-2.0 testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT +tempest>=16.1.0 # Apache-2.0 # releasenotes reno!=2.3.1,>=1.8.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index a0508574..c621e486 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ setenv = deps = -r{toxinidir}/test-requirements.txt commands = find . -type f -name "*.pyc" -delete - python setup.py testr --slowest --testr-args='{posargs}' + ostestr {posargs} whitelist_externals = rm find