Add functional tests for execution

Partially implements: blueprint qinling-functional-tests
Change-Id: I448fd148c6d046412a459ec933ca1e88ddfa9cad
This commit is contained in:
Lingxian Kong 2017-09-19 14:59:17 +12:00
parent 0b36d909d6
commit 4272c94a08
7 changed files with 203 additions and 23 deletions

View File

@ -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_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 DEFAULT logging_context_format_string "%(asctime)s %(process)d %(color)s %(levelname)s [%(request_id)s] %(message)s %(resource)s (%(name)s)"
iniset $QINLING_CONF_FILE storage file_system_dir $QINLING_FUNCTION_STORAGE_DIR
iniset $QINLING_CONF_FILE kubernetes qinling_service_address $DEFAULT_HOST_IP

View File

@ -27,6 +27,7 @@ CONF = cfg.CONF
class DefaultEngine(object):
def __init__(self, orchestrator):
self.orchestrator = orchestrator
self.session = requests.Session()
def create_runtime(self, ctx, runtime_id):
LOG.info('Start to create.',
@ -102,7 +103,7 @@ class DefaultEngine(object):
)
data = {'input': input, 'execution_id': execution_id}
r = requests.post(func_url, json=data)
r = self.session.post(func_url, json=data)
res = r.json()
LOG.debug('Finished execution %s', execution_id)

View File

@ -20,6 +20,7 @@ import jinja2
from kubernetes import client
from oslo_log import log as logging
import requests
import six
import tenacity
import yaml
@ -56,6 +57,10 @@ class KubernetesManager(base.OrchestratorBase):
self.service_template = jinja_env.get_template('service.j2')
self.pod_template = jinja_env.get_template('pod.j2')
# Refer to
# http://docs.python-requests.org/en/master/user/advanced/#session-objects
self.session = requests.Session()
def _ensure_namespace(self):
ret = self.v1.list_namespace()
cur_names = [i.metadata.name for i in ret.items]
@ -301,18 +306,24 @@ class KubernetesManager(base.OrchestratorBase):
name, request_url, data
)
# TODO(kong): Here we sleep some time to avoid 'Failed to establish a
# new connection' error for some reason. Needs to find a better
# solution.
time.sleep(1)
r = requests.post(request_url, json=data)
exception = None
for a in six.moves.xrange(10):
try:
r = self.session.post(request_url, json=data)
if r.status_code != requests.codes.ok:
raise exc.OrchestratorException(
'Failed to download function code package.'
)
if r.status_code != requests.codes.ok:
raise exc.OrchestratorException(
'Failed to download function code package.'
)
return name, pod_service_url
except (requests.ConnectionError, requests.Timeout) as e:
exception = e
LOG.warning("Could not connect to service. Retrying.")
time.sleep(1)
return name, pod_service_url
raise exc.OrchestratorException(
'Could not connect to service. Reason: %s', exception
)
def _create_pod(self, image, pod_name, labels, input):
pod_body = self.pod_template.render(
@ -383,7 +394,7 @@ class KubernetesManager(base.OrchestratorBase):
LOG.info('Invoke function %s, url: %s', function_id, func_url)
r = requests.post(func_url, json=data)
r = self.session.post(func_url, json=data)
return r.json()
else:
status = None

View File

@ -0,0 +1,155 @@
# 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
# TODO(kong): Be careful that for k8s cluster, the default pool size is 3,
# maybe we need customize that in future if there are many test cases but with
# insufficient pods.
class ExecutionsTest(base.BaseQinlingTest):
name_prefix = 'ExecutionsTest'
@classmethod
def resource_setup(cls):
super(ExecutionsTest, cls).resource_setup()
cls.runtime_id = None
# Create runtime for execution 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,
ignore_notfound=True)
super(ExecutionsTest, cls).resource_cleanup()
def setUp(self):
super(ExecutionsTest, 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()
# 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
)
self.function_id = body['id']
self.addCleanup(self.client.delete_resource, 'functions',
self.function_id, ignore_notfound=True)
@decorators.idempotent_id('2a93fab0-2dae-4748-b0d4-f06b735ff451')
def test_create_list_get_delete_execution(self):
resp, body = self.client.create_execution(self.function_id,
input={'name': 'Qinling'})
self.assertEqual(201, resp.status)
self.assertEqual('success', body['status'])
execution_id = body['id']
self.addCleanup(self.client.delete_resource, 'executions',
execution_id, ignore_notfound=True)
# Get executions
resp, body = self.client.get_resources('executions')
self.assertEqual(200, resp.status)
self.assertIn(
execution_id,
[execution['id'] for execution in body['executions']]
)
# Delete execution
resp = self.client.delete_resource('executions', execution_id)
self.assertEqual(204, resp.status)
@decorators.idempotent_id('8096cc52-64d2-4660-a657-9ac0bdd743ae')
def test_execution_async(self):
resp, body = self.client.create_execution(self.function_id, sync=False)
self.assertEqual(201, resp.status)
self.assertEqual('running', body['status'])
execution_id = body['id']
self.addCleanup(self.client.delete_resource, 'executions',
execution_id, ignore_notfound=True)
self.await_execution_success(execution_id)
@decorators.idempotent_id('6cb47b1d-a8c6-48f2-a92f-c4f613c33d1c')
def test_execution_log(self):
resp, body = self.client.create_execution(self.function_id,
input={'name': 'OpenStack'})
self.assertEqual(201, resp.status)
self.assertEqual('success', body['status'])
execution_id = body['id']
self.addCleanup(self.client.delete_resource, 'executions',
execution_id, ignore_notfound=True)
# Get execution log
resp, body = self.client.get_execution_log(execution_id)
self.assertEqual(200, resp.status)
self.assertIn('Hello, OpenStack', body)

View File

@ -53,3 +53,14 @@ class BaseQinlingTest(test.BaseTestCase):
self.assertEqual(200, resp.status)
self.assertEqual('available', body['status'])
@tenacity.retry(
wait=tenacity.wait_fixed(3),
stop=tenacity.stop_after_attempt(10),
retry=tenacity.retry_if_exception_type(AssertionError)
)
def await_execution_success(self, id):
resp, body = self.client.get_resource('executions', id)
self.assertEqual(200, resp.status)
self.assertEqual('success', body['status'])

View File

@ -65,7 +65,6 @@ class BasicOpsTest(base.BaseQinlingTest):
4. Check result and execution log.
"""
name = data_utils.rand_name('runtime', prefix=self.name_prefix)
resp, body = self.admin_client.create_runtime(
'openstackqinling/python-runtime', name
)
@ -77,7 +76,7 @@ class BasicOpsTest(base.BaseQinlingTest):
runtime_id = body['id']
self.await_runtime_available(runtime_id)
self.addCleanup(self.admin_client.delete_resource, 'runtimes',
runtime_id)
runtime_id, ignore_notfound=True)
# Create function
function_name = data_utils.rand_name('function',
@ -94,7 +93,7 @@ class BasicOpsTest(base.BaseQinlingTest):
self.assertEqual(201, resp.status_code)
self.addCleanup(self.client.delete_resource, 'functions',
function_id)
function_id, ignore_notfound=True)
# Invoke function
resp, body = self.client.create_execution(function_id,
@ -105,7 +104,7 @@ class BasicOpsTest(base.BaseQinlingTest):
execution_id = body['id']
self.addCleanup(self.client.delete_resource, 'executions',
execution_id)
execution_id, ignore_notfound=True)
# Get execution log
resp, body = self.client.get_execution_log(execution_id)

View File

@ -23,12 +23,15 @@ function delete_resources(){
openstack function delete $id
done
# Delete runtimes
ids=$(openstack runtime list -f yaml -c Id | awk '{print $3}')
for id in $ids
do
openstack runtime delete $id
done
if [ "$1" = "admin" ]
then
# Delete runtimes
ids=$(openstack runtime list -f yaml -c Id | awk '{print $3}')
for id in $ids
do
openstack runtime delete $id
done
fi
}
delete_resources