diff --git a/devstack/plugin.sh b/devstack/plugin.sh index cad563c0..37e78291 100755 --- a/devstack/plugin.sh +++ b/devstack/plugin.sh @@ -72,7 +72,7 @@ function configure_qinling { iniset $QINLING_CONF_FILE oslo_policy policy_file $QINLING_POLICY_FILE iniset $QINLING_CONF_FILE DEFAULT debug $QINLING_DEBUG iniset $QINLING_CONF_FILE DEFAULT server all - iniset $QINLING_CONF_FILE DEFAULT logging_default_format_string $QINLING_LOG_FORMAT + iniset $QINLING_CONF_FILE DEFAULT logging_context_format_string "%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s %(resource)s" iniset $QINLING_CONF_FILE storage file_system_dir $QINLING_FUNCTION_STORAGE_DIR iniset $QINLING_CONF_FILE kubernetes qinling_service_address $DEFAULT_HOST_IP diff --git a/devstack/settings b/devstack/settings index 58b0384f..ceef5121 100644 --- a/devstack/settings +++ b/devstack/settings @@ -23,4 +23,3 @@ QINLING_POLICY_FILE=${QINLING_CONF_DIR}/policy.json QINLING_AUTH_CACHE_DIR=${QINLING_AUTH_CACHE_DIR:-/var/cache/qinling} QINLING_FUNCTION_STORAGE_DIR=${QINLING_FUNCTION_STORAGE_DIR:-/opt/qinling/funtion/packages} QINLING_PYTHON_RUNTIME_IMAGE=${QINLING_PYTHON_RUNTIME_IMAGE:-openstackqinling/python-runtime} -QINLING_LOG_FORMAT=${QINLING_LOG_FORMAT:-"%(asctime)s.%(msecs)03d %(process)d %(levelname)s %(name)s [%(request_id)s %(user_identity)s] %(instance)s%(message)s %(resource)s"} diff --git a/qinling_tempest_plugin/services/qinling_client.py b/qinling_tempest_plugin/services/qinling_client.py index ecde652b..ade765e6 100644 --- a/qinling_tempest_plugin/services/qinling_client.py +++ b/qinling_tempest_plugin/services/qinling_client.py @@ -15,6 +15,7 @@ import json import requests +from tempest.lib import exceptions from qinling_tempest_plugin.services import base as client_base @@ -22,10 +23,15 @@ from qinling_tempest_plugin.services import base as client_base class QinlingClient(client_base.QinlingClientBase): """Tempest REST client for Qinling.""" - def delete_resource(self, res, id): - resp, _ = self.delete_obj(res, id) - - return resp + def delete_resource(self, res, id, ignore_notfound=False): + try: + resp, _ = self.delete_obj(res, id) + return resp + except exceptions.NotFound: + if ignore_notfound: + pass + else: + raise def get_resource(self, res, id): resp, body = self.get_obj(res, id) @@ -37,8 +43,8 @@ class QinlingClient(client_base.QinlingClientBase): return resp, body - def create_runtime(self, image, name=None): - req_body = {"image": image} + def create_runtime(self, image, name=None, is_public=True): + req_body = {"image": image, "is_public": is_public} if name: req_body.update({'name': name}) @@ -75,6 +81,10 @@ class QinlingClient(client_base.QinlingClientBase): return resp, json.loads(resp.text) + def download_function(self, function_id): + return self.get('/v1/functions/%s?download=true' % function_id, + headers={}) + def create_execution(self, function_id, input=None, sync=True): req_body = {'function_id': function_id, 'sync': sync, 'input': input} resp, body = self.post_json('executions', req_body) diff --git a/qinling_tempest_plugin/tests/api/test_functions.py b/qinling_tempest_plugin/tests/api/test_functions.py new file mode 100644 index 00000000..3d6f0c53 --- /dev/null +++ b/qinling_tempest_plugin/tests/api/test_functions.py @@ -0,0 +1,117 @@ +# 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 os +import tempfile +import zipfile + +from tempest.lib.common.utils import data_utils +from tempest.lib import decorators + +from qinling_tempest_plugin.tests import base + + +class FunctionsTest(base.BaseQinlingTest): + name_prefix = 'FunctionsTest' + + @classmethod + def resource_setup(cls): + super(FunctionsTest, cls).resource_setup() + + cls.runtime_id = None + + # Create runtime for function tests + name = data_utils.rand_name('runtime', prefix=cls.name_prefix) + _, body = cls.admin_client.create_runtime( + 'openstackqinling/python-runtime', name + ) + cls.runtime_id = body['id'] + + @classmethod + def resource_cleanup(cls): + if cls.runtime_id: + cls.admin_client.delete_resource('runtimes', cls.runtime_id) + + super(FunctionsTest, cls).resource_cleanup() + + def setUp(self): + super(FunctionsTest, self).setUp() + + # Wait until runtime is available + self.await_runtime_available(self.runtime_id) + + python_file_path = os.path.abspath( + os.path.join( + os.path.dirname(__file__), + os.pardir, + os.pardir, + 'functions/python_test.py' + ) + ) + + base_name, extention = os.path.splitext(python_file_path) + self.base_name = os.path.basename(base_name) + self.python_zip_file = os.path.join( + tempfile.gettempdir(), + '%s.zip' % self.base_name + ) + + if not os.path.isfile(self.python_zip_file): + zf = zipfile.ZipFile(self.python_zip_file, mode='w') + try: + # Use default compression mode, may change in future. + zf.write( + python_file_path, + '%s%s' % (self.base_name, extention), + compress_type=zipfile.ZIP_STORED + ) + finally: + zf.close() + + @decorators.idempotent_id('9c36ac64-9a44-4c44-9e44-241dcc6b0933') + def test_create_list_get_delete_function(self): + # Create function + function_name = data_utils.rand_name('function', + prefix=self.name_prefix) + with open(self.python_zip_file, 'rb') as package_data: + resp, body = self.client.create_function( + {"source": "package"}, + self.runtime_id, + name=function_name, + package_data=package_data, + entry='%s.main' % self.base_name + ) + function_id = body['id'] + + self.assertEqual(201, resp.status_code) + self.addCleanup(self.client.delete_resource, 'functions', + function_id, ignore_notfound=True) + + # Get functions + resp, body = self.client.get_resources('functions') + + self.assertEqual(200, resp.status) + self.assertIn( + function_id, + [function['id'] for function in body['functions']] + ) + + # Download function package + resp, data = self.client.download_function(function_id) + self.assertEqual(200, resp.status) + self.assertEqual(os.path.getsize(self.python_zip_file), len(data)) + + # Delete function + resp = self.client.delete_resource('functions', function_id) + + self.assertEqual(204, resp.status) diff --git a/qinling_tempest_plugin/tests/api/test_runtimes.py b/qinling_tempest_plugin/tests/api/test_runtimes.py index 2b539987..1432d306 100644 --- a/qinling_tempest_plugin/tests/api/test_runtimes.py +++ b/qinling_tempest_plugin/tests/api/test_runtimes.py @@ -32,6 +32,8 @@ class RuntimesTest(base.BaseQinlingTest): self.assertEqual(name, body['name']) runtime_id = body['id'] + self.addCleanup(self.admin_client.delete_resource, 'runtimes', + runtime_id, ignore_notfound=True) # Get runtimes resp, body = self.client.get_resources('runtimes') @@ -60,3 +62,32 @@ class RuntimesTest(base.BaseQinlingTest): resp = self.admin_client.delete_resource('runtimes', runtime_id) self.assertEqual(204, resp.status) + + @decorators.idempotent_id('c1db56bd-c3a8-4ca6-9482-c362fd492db0') + def test_create_private_runtime(self): + """Private runtime test. + + Admin user creates a private runtime which can not be used by other + projects. + """ + name = data_utils.rand_name('runtime', prefix=self.name_prefix) + resp, body = self.admin_client.create_runtime( + 'openstackqinling/python-runtime', name, is_public=False + ) + + self.assertEqual(201, resp.status) + self.assertEqual(name, body['name']) + self.assertFalse(body['is_public']) + + runtime_id = body['id'] + self.addCleanup(self.admin_client.delete_resource, 'runtimes', + runtime_id, ignore_notfound=True) + + # Get runtimes + resp, body = self.client.get_resources('runtimes') + + self.assertEqual(200, resp.status) + self.assertNotIn( + runtime_id, + [runtime['id'] for runtime in body['runtimes']] + ) diff --git a/qinling_tempest_plugin/tests/base.py b/qinling_tempest_plugin/tests/base.py index d49a4893..d7da7a30 100644 --- a/qinling_tempest_plugin/tests/base.py +++ b/qinling_tempest_plugin/tests/base.py @@ -44,8 +44,8 @@ class BaseQinlingTest(test.BaseTestCase): cls.namespace = 'qinling' @tenacity.retry( - wait=tenacity.wait_fixed(2), - stop=tenacity.stop_after_delay(10), + wait=tenacity.wait_fixed(3), + stop=tenacity.stop_after_attempt(10), retry=tenacity.retry_if_exception_type(AssertionError) ) def await_runtime_available(self, id): diff --git a/qinling_tempest_plugin/tests/scenario/test_basic_ops.py b/qinling_tempest_plugin/tests/scenario/test_basic_ops.py index 7f5adede..ef16d576 100644 --- a/qinling_tempest_plugin/tests/scenario/test_basic_ops.py +++ b/qinling_tempest_plugin/tests/scenario/test_basic_ops.py @@ -43,16 +43,17 @@ class BasicOpsTest(base.BaseQinlingTest): '%s.zip' % self.base_name ) - zf = zipfile.ZipFile(self.python_zip_file, mode='w') - try: - # Use default compression mode, may change in future. - zf.write( - python_file_path, - '%s%s' % (self.base_name, extention), - compress_type=zipfile.ZIP_STORED - ) - finally: - zf.close() + if not os.path.isfile(self.python_zip_file): + zf = zipfile.ZipFile(self.python_zip_file, mode='w') + try: + # Use default compression mode, may change in future. + zf.write( + python_file_path, + '%s%s' % (self.base_name, extention), + compress_type=zipfile.ZIP_STORED + ) + finally: + zf.close() @decorators.idempotent_id('205fd749-2468-4d9f-9c05-45558d6d8f9e') def test_basic_ops(self):