Support policy check for api access

- Support oslo.policy in api layer
- Add tempest test for basic qinling operation

Change-Id: Icf12ee4df54652e81013e951b91163c085d961b7
This commit is contained in:
Lingxian Kong 2017-09-15 23:21:25 +12:00
parent 851227deea
commit cff88e1b64
17 changed files with 272 additions and 63 deletions

View File

@ -64,11 +64,15 @@ function configure_qinling {
mkdir_chown_stack "$QINLING_FUNCTION_STORAGE_DIR" mkdir_chown_stack "$QINLING_FUNCTION_STORAGE_DIR"
rm -f "$QINLING_FUNCTION_STORAGE_DIR"/* rm -f "$QINLING_FUNCTION_STORAGE_DIR"/*
cp $QINLING_DIR/etc/policy.json.sample $QINLING_POLICY_FILE
# Generate Qinling configuration file and configure common parameters. # Generate Qinling configuration file and configure common parameters.
oslo-config-generator --config-file $QINLING_DIR/tools/config/config-generator.qinling.conf --output-file $QINLING_CONF_FILE oslo-config-generator --config-file $QINLING_DIR/tools/config/config-generator.qinling.conf --output-file $QINLING_CONF_FILE
iniset $QINLING_CONF_FILE oslo_policy policy_file $QINLING_POLICY_FILE
iniset $QINLING_CONF_FILE DEFAULT debug $QINLING_DEBUG iniset $QINLING_CONF_FILE DEFAULT debug $QINLING_DEBUG
iniset $QINLING_CONF_FILE DEFAULT server all iniset $QINLING_CONF_FILE DEFAULT server all
iniset $QINLING_CONF_FILE DEFAULT logging_default_format_string $QINLING_LOG_FORMAT
iniset $QINLING_CONF_FILE storage file_system_dir $QINLING_FUNCTION_STORAGE_DIR iniset $QINLING_CONF_FILE storage file_system_dir $QINLING_FUNCTION_STORAGE_DIR
iniset $QINLING_CONF_FILE kubernetes qinling_service_address $DEFAULT_HOST_IP iniset $QINLING_CONF_FILE kubernetes qinling_service_address $DEFAULT_HOST_IP

View File

@ -19,6 +19,8 @@ QINLING_SERVICE_PROTOCOL=${QINLING_SERVICE_PROTOCOL:-$SERVICE_PROTOCOL}
QINLING_DEBUG=${QINLING_DEBUG:-True} QINLING_DEBUG=${QINLING_DEBUG:-True}
QINLING_CONF_DIR=${QINLING_CONF_DIR:-/etc/qinling} QINLING_CONF_DIR=${QINLING_CONF_DIR:-/etc/qinling}
QINLING_CONF_FILE=${QINLING_CONF_DIR}/qinling.conf QINLING_CONF_FILE=${QINLING_CONF_DIR}/qinling.conf
QINLING_POLICY_FILE=${QINLING_CONF_DIR}/policy.json
QINLING_AUTH_CACHE_DIR=${QINLING_AUTH_CACHE_DIR:-/var/cache/qinling} QINLING_AUTH_CACHE_DIR=${QINLING_AUTH_CACHE_DIR:-/var/cache/qinling}
QINLING_FUNCTION_STORAGE_DIR=${QINLING_FUNCTION_STORAGE_DIR:-/opt/qinling/funtion/packages} QINLING_FUNCTION_STORAGE_DIR=${QINLING_FUNCTION_STORAGE_DIR:-/opt/qinling/funtion/packages}
QINLING_PYTHON_RUNTIME_IMAGE=${QINLING_PYTHON_RUNTIME_IMAGE:-openstackqinling/python-runtime} 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"}

10
etc/policy.json.sample Normal file
View File

@ -0,0 +1,10 @@
{
"context_is_admin": "role:admin or is_admin:1",
"owner" : "project_id:%(project_id)s",
"admin_or_owner": "rule:context_is_admin or rule:owner",
"default": "rule:admin_or_owner",
"runtime:create": "rule:context_is_admin",
"runtime:update": "rule:context_is_admin",
"runtime:delete": "rule:context_is_admin",
}

View File

@ -56,10 +56,14 @@ def enforce(action, context, target=None, do_raise=True,
:return: returns True if authorized and False if not authorized and :return: returns True if authorized and False if not authorized and
do_raise is False. do_raise is False.
""" """
if not cfg.CONF.pecan.auth_enable:
return
ctx_dict = context.to_policy_values()
target_obj = { target_obj = {
'project_id': context.project_id, 'project_id': ctx_dict['project_id'],
'user_id': context.user_id, 'user_id': ctx_dict['user_id'],
} }
target_obj.update(target or {}) target_obj.update(target or {})
@ -68,7 +72,7 @@ def enforce(action, context, target=None, do_raise=True,
return _ENFORCER.enforce( return _ENFORCER.enforce(
action, action,
target_obj, target_obj,
context.to_dict(), ctx_dict,
do_raise=do_raise, do_raise=do_raise,
exc=exc exc=exc
) )

View File

@ -37,7 +37,7 @@ from qinling.utils import rest_utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
POST_REQUIRED = set(['name', 'code']) POST_REQUIRED = set(['code'])
CODE_SOURCE = set(['package', 'swift', 'image']) CODE_SOURCE = set(['package', 'swift', 'image'])
UPDATE_ALLOWED = set(['name', 'description', 'entry']) UPDATE_ALLOWED = set(['name', 'description', 'entry'])
@ -106,7 +106,7 @@ class FunctionsController(rest.RestController):
) )
values = { values = {
'name': kwargs['name'], 'name': kwargs.get('name'),
'description': kwargs.get('description'), 'description': kwargs.get('description'),
'runtime_id': kwargs.get('runtime_id'), 'runtime_id': kwargs.get('runtime_id'),
'code': json.loads(kwargs['code']), 'code': json.loads(kwargs['code']),

View File

@ -218,6 +218,7 @@ class Runtime(Resource):
name = wtypes.text name = wtypes.text
image = wtypes.text image = wtypes.text
description = wtypes.text description = wtypes.text
is_public = wsme.wsattr(bool, default=True)
status = wsme.wsattr(wtypes.text, readonly=True) status = wsme.wsattr(wtypes.text, readonly=True)
project_id = wsme.wsattr(wtypes.text, readonly=True) project_id = wsme.wsattr(wtypes.text, readonly=True)
created_at = wsme.wsattr(wtypes.text, readonly=True) created_at = wsme.wsattr(wtypes.text, readonly=True)
@ -230,6 +231,7 @@ class Runtime(Resource):
name='python2.7', name='python2.7',
image='lingxiankong/python', image='lingxiankong/python',
status='available', status='available',
is_public=True,
project_id='default', project_id='default',
description='Python 2.7 environment.', description='Python 2.7 environment.',
created_at='1970-01-01T00:00:00.000000', created_at='1970-01-01T00:00:00.000000',

View File

@ -16,8 +16,10 @@ from oslo_log import log as logging
from pecan import rest from pecan import rest
import wsmeext.pecan as wsme_pecan import wsmeext.pecan as wsme_pecan
from qinling.api import access_control as acl
from qinling.api.controllers.v1 import resources from qinling.api.controllers.v1 import resources
from qinling.api.controllers.v1 import types from qinling.api.controllers.v1 import types
from qinling import context
from qinling.db import api as db_api from qinling.db import api as db_api
from qinling import exceptions as exc from qinling import exceptions as exc
from qinling import rpc from qinling import rpc
@ -63,6 +65,8 @@ class RuntimesController(rest.RestController):
status_code=201 status_code=201
) )
def post(self, runtime): def post(self, runtime):
acl.enforce('runtime:create', context.get_ctx())
params = runtime.to_dict() params = runtime.to_dict()
if not POST_REQUIRED.issubset(set(params.keys())): if not POST_REQUIRED.issubset(set(params.keys())):
@ -82,13 +86,15 @@ class RuntimesController(rest.RestController):
@rest_utils.wrap_wsme_controller_exception @rest_utils.wrap_wsme_controller_exception
@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):
acl.enforce('runtime:delete', context.get_ctx())
LOG.info("Delete resource.", resource={'type': self.type, 'id': id}) LOG.info("Delete resource.", resource={'type': self.type, 'id': id})
with db_api.transaction(): with db_api.transaction():
runtime_db = db_api.get_runtime(id) runtime_db = db_api.get_runtime(id)
# Runtime can not be deleted if still associate with functions. # Runtime can not be deleted if still associate with functions.
funcs = db_api.get_functions(runtime_id={'eq': id}) funcs = db_api.get_functions(insecure=True, runtime_id={'eq': id})
if len(funcs): if len(funcs):
raise exc.NotAllowedException( raise exc.NotAllowedException(
'Runtime %s is still in use.' % id 'Runtime %s is still in use.' % id
@ -111,6 +117,8 @@ class RuntimesController(rest.RestController):
Currently, we only support update name, description, image. When Currently, we only support update name, description, image. When
updating image, send message to engine for asynchronous handling. updating image, send message to engine for asynchronous handling.
""" """
acl.enforce('runtime:update', context.get_ctx())
values = {} values = {}
for key in UPDATE_ALLOWED: for key in UPDATE_ALLOWED:
if runtime.to_dict().get(key) is not None: if runtime.to_dict().get(key) is not None:

View File

@ -109,7 +109,15 @@ def _secure_query(model, *columns):
if not issubclass(model, model_base.QinlingSecureModelBase): if not issubclass(model, model_base.QinlingSecureModelBase):
return query return query
query = query.filter(model.project_id == context.get_ctx().projectid) if model == models.Runtime:
query_criterion = sa.or_(
model.project_id == context.get_ctx().projectid,
model.is_public
)
else:
query_criterion = model.project_id == context.get_ctx().projectid
query = query.filter(query_criterion)
return query return query
@ -274,7 +282,13 @@ def create_runtime(values, session=None):
@db_base.session_aware() @db_base.session_aware()
def get_runtime(id, session=None): def get_runtime(id, session=None):
runtime = _get_db_object_by_id(models.Runtime, id) model = models.Runtime
filters = sa.and_(
model.id == id,
sa.or_(model.project_id == context.get_ctx().projectid,
model.is_public),
)
runtime = db_base.model_query(model).filter(filters).first()
if not runtime: if not runtime:
raise exc.DBEntityNotFoundError("Runtime not found [id=%s]" % id) raise exc.DBEntityNotFoundError("Runtime not found [id=%s]" % id)

View File

@ -54,8 +54,8 @@ def upgrade():
sa.Column('description', sa.String(length=255), nullable=True), sa.Column('description', sa.String(length=255), nullable=True),
sa.Column('image', sa.String(length=255), nullable=False), sa.Column('image', sa.String(length=255), nullable=False),
sa.Column('status', sa.String(length=32), nullable=False), sa.Column('status', sa.String(length=32), nullable=False),
sa.Column('is_public', sa.BOOLEAN, nullable=False),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('image', 'project_id'),
info={"check_ifexists": True} info={"check_ifexists": True}
) )
@ -74,7 +74,6 @@ def upgrade():
sa.Column('entry', sa.String(length=80), nullable=False), sa.Column('entry', sa.String(length=80), nullable=False),
sa.Column('count', sa.Integer, nullable=False), sa.Column('count', sa.Integer, nullable=False),
sa.PrimaryKeyConstraint('id'), sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name', 'project_id'),
sa.ForeignKeyConstraint(['runtime_id'], [u'runtimes.id']), sa.ForeignKeyConstraint(['runtime_id'], [u'runtimes.id']),
info={"check_ifexists": True} info={"check_ifexists": True}
) )

View File

@ -23,24 +23,17 @@ from qinling.utils import common
class Runtime(model_base.QinlingSecureModelBase): class Runtime(model_base.QinlingSecureModelBase):
__tablename__ = 'runtimes' __tablename__ = 'runtimes'
__table_args__ = (
sa.UniqueConstraint('image', 'project_id'),
)
name = sa.Column(sa.String(255)) name = sa.Column(sa.String(255))
description = sa.Column(sa.String(255)) description = sa.Column(sa.String(255))
image = sa.Column(sa.String(255), nullable=False) image = sa.Column(sa.String(255), nullable=False)
status = sa.Column(sa.String(32), nullable=False) status = sa.Column(sa.String(32), nullable=False)
is_public = sa.Column(sa.BOOLEAN, default=True)
class Function(model_base.QinlingSecureModelBase): class Function(model_base.QinlingSecureModelBase):
__tablename__ = 'functions' __tablename__ = 'functions'
__table_args__ = ( name = sa.Column(sa.String(255), nullable=True)
sa.UniqueConstraint('name', 'project_id'),
)
name = sa.Column(sa.String(255), nullable=False)
description = sa.Column(sa.String(255)) description = sa.Column(sa.String(255))
runtime_id = sa.Column( runtime_id = sa.Column(
sa.String(36), sa.ForeignKey(Runtime.id), nullable=True sa.String(36), sa.ForeignKey(Runtime.id), nullable=True

View File

@ -37,7 +37,8 @@ class TestRuntimeController(base.APITest):
"image": self.db_runtime.image, "image": self.db_runtime.image,
"name": self.db_runtime.name, "name": self.db_runtime.name,
"project_id": test_base.DEFAULT_PROJECT_ID, "project_id": test_base.DEFAULT_PROJECT_ID,
"status": status.AVAILABLE "status": status.AVAILABLE,
"is_public": True,
} }
self.assertEqual(200, resp.status_int) self.assertEqual(200, resp.status_int)
@ -51,7 +52,8 @@ class TestRuntimeController(base.APITest):
"image": self.db_runtime.image, "image": self.db_runtime.image,
"name": self.db_runtime.name, "name": self.db_runtime.name,
"project_id": test_base.DEFAULT_PROJECT_ID, "project_id": test_base.DEFAULT_PROJECT_ID,
"status": status.AVAILABLE "status": status.AVAILABLE,
"is_public": True,
} }
self.assertEqual(200, resp.status_int) self.assertEqual(200, resp.status_int)

View File

@ -0,0 +1,17 @@
# 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.
def main(name='World'):
print('Hello, %s' % name)

View File

@ -21,9 +21,6 @@ class QinlingClientBase(rest_client.RestClient):
def __init__(self, auth_provider, **kwargs): def __init__(self, auth_provider, **kwargs):
super(QinlingClientBase, self).__init__(auth_provider, **kwargs) super(QinlingClientBase, self).__init__(auth_provider, **kwargs)
self.runtimes = []
self.functions = []
def get_list_objs(self, obj): def get_list_objs(self, obj):
resp, body = self.get('/v1/%s' % obj) resp, body = self.get('/v1/%s' % obj)

View File

@ -14,19 +14,73 @@
import json import json
import requests
from qinling_tempest_plugin.services import base as client_base from qinling_tempest_plugin.services import base as client_base
class QinlingClient(client_base.QinlingClientBase): class QinlingClient(client_base.QinlingClientBase):
"""Tempest REST client for Qinling.""" """Tempest REST client for Qinling."""
def delete_resource(self, res, id):
resp, _ = self.delete_obj(res, id)
return resp
def get_resource(self, res, id):
resp, body = self.get_obj(res, id)
return resp, body
def get_resources(self, res):
resp, body = self.get_list_objs(res)
return resp, body
def create_runtime(self, image, name=None): def create_runtime(self, image, name=None):
body = {"image": image} req_body = {"image": image}
if name: if name:
body.update({'name': name}) req_body.update({'name': name})
resp, body = self.post('runtimes', json.dumps(body)) resp, body = self.post_json('runtimes', req_body)
self.runtimes.append(json.loads(body)['id'])
return resp, json.loads(body) return resp, body
def create_function(self, code, runtime_id, name='', package_data=None,
entry=None):
"""Create function.
Tempest rest client doesn't support multipart upload, so use requests
lib instead.
"""
headers = {'X-Auth-Token': self.auth_provider.get_token()}
req_body = {
'name': name,
'runtime_id': runtime_id,
'code': json.dumps(code)
}
if entry:
req_body['entry'] = entry
req_kwargs = {
'headers': headers,
'data': req_body
}
if package_data:
req_kwargs.update({'files': {'package': package_data}})
url_path = '%s/v1/functions' % (self.base_url)
resp = requests.post(url_path, **req_kwargs)
return resp, json.loads(resp.text)
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)
return resp, body
def get_execution_log(self, execution_id):
return self.get('/v1/executions/%s/log' % execution_id,
headers={'Accept': 'text/plain'})

View File

@ -13,7 +13,6 @@
# limitations under the License. # limitations under the License.
from tempest.lib.common.utils import data_utils from tempest.lib.common.utils import data_utils
from tempest.lib import decorators from tempest.lib import decorators
import tenacity
from qinling_tempest_plugin.tests import base from qinling_tempest_plugin.tests import base
@ -21,33 +20,21 @@ from qinling_tempest_plugin.tests import base
class RuntimesTest(base.BaseQinlingTest): class RuntimesTest(base.BaseQinlingTest):
name_prefix = 'RuntimesTest' name_prefix = 'RuntimesTest'
@tenacity.retry(
wait=tenacity.wait_fixed(2),
stop=tenacity.stop_after_delay(10),
retry=tenacity.retry_if_exception_type(AssertionError)
)
def _await_runtime_available(self, id):
resp, body = self.qinling_client.get_obj('runtimes', id)
self.assertEqual(200, resp.status)
self.assertEqual('available', body['status'])
@decorators.idempotent_id('fdc2f07f-dd1d-4981-86d3-5bc7908d9a9b') @decorators.idempotent_id('fdc2f07f-dd1d-4981-86d3-5bc7908d9a9b')
def test_create_list_get_delete_runtime(self): def test_create_list_get_delete_runtime(self):
name = data_utils.rand_name('runtime', prefix=self.name_prefix) name = data_utils.rand_name('runtime', prefix=self.name_prefix)
req_body = { resp, body = self.admin_client.create_runtime(
'name': name, 'openstackqinling/python-runtime', 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(201, resp.status)
self.assertEqual(name, body['name']) self.assertEqual(name, body['name'])
runtime_id = body['id']
# Get runtimes # Get runtimes
resp, body = self.qinling_client.get_list_objs('runtimes') resp, body = self.client.get_resources('runtimes')
self.assertEqual(200, resp.status) self.assertEqual(200, resp.status)
self.assertIn( self.assertIn(
@ -56,7 +43,7 @@ class RuntimesTest(base.BaseQinlingTest):
) )
# Wait for runtime to be available # Wait for runtime to be available
self._await_runtime_available(runtime_id) self.await_runtime_available(runtime_id)
# Check k8s resource # Check k8s resource
deploy = self.k8s_v1extention.read_namespaced_deployment( deploy = self.k8s_v1extention.read_namespaced_deployment(
@ -70,6 +57,6 @@ class RuntimesTest(base.BaseQinlingTest):
) )
# Delete runtime # Delete runtime
resp, _ = self.qinling_client.delete_obj('runtimes', runtime_id) resp = self.admin_client.delete_resource('runtimes', runtime_id)
self.assertEqual(204, resp.status) self.assertEqual(204, resp.status)

View File

@ -11,17 +11,16 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# 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.
from kubernetes import client as k8s_client from kubernetes import client as k8s_client
from tempest import config from tempest import config
from tempest import test from tempest import test
import tenacity
CONF = config.CONF CONF = config.CONF
class BaseQinlingTest(test.BaseTestCase): class BaseQinlingTest(test.BaseTestCase):
credentials = ('primary',) credentials = ('admin', 'primary', 'alt')
force_tenant_isolation = False
@classmethod @classmethod
def skip_checks(cls): def skip_checks(cls):
@ -34,19 +33,23 @@ class BaseQinlingTest(test.BaseTestCase):
def setup_clients(cls): def setup_clients(cls):
super(BaseQinlingTest, cls).setup_clients() super(BaseQinlingTest, cls).setup_clients()
# os here is tempest.lib.services.clients.ServiceClients object cls.client = cls.os_primary.qinling.QinlingClient()
os = getattr(cls, 'os_%s' % cls.credentials[0]) cls.alt_client = cls.os_alt.qinling.QinlingClient()
cls.qinling_client = os.qinling.QinlingClient() cls.admin_client = cls.os_admin.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 # Initilize k8s client
k8s_client.Configuration().host = CONF.qinling.kube_host k8s_client.Configuration().host = CONF.qinling.kube_host
cls.k8s_v1 = k8s_client.CoreV1Api() cls.k8s_v1 = k8s_client.CoreV1Api()
cls.k8s_v1extention = k8s_client.ExtensionsV1beta1Api() cls.k8s_v1extention = k8s_client.ExtensionsV1beta1Api()
cls.namespace = 'qinling' cls.namespace = 'qinling'
@tenacity.retry(
wait=tenacity.wait_fixed(2),
stop=tenacity.stop_after_delay(10),
retry=tenacity.retry_if_exception_type(AssertionError)
)
def await_runtime_available(self, id):
resp, body = self.client.get_resource('runtimes', id)
self.assertEqual(200, resp.status)
self.assertEqual('available', body['status'])

View File

@ -0,0 +1,113 @@
# 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 BasicOpsTest(base.BaseQinlingTest):
name_prefix = 'BasicOpsTest'
def setUp(self):
super(BasicOpsTest, self).setUp()
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
)
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):
"""Basic qinling operations test case, including following steps:
1. Admin user creates a runtime.
2. Normal user creates function.
3. Normal user creates execution(invoke function).
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
)
self.assertEqual(201, resp.status)
self.assertEqual(name, body['name'])
# Wait for runtime to be available
runtime_id = body['id']
self.await_runtime_available(runtime_id)
self.addCleanup(self.admin_client.delete_resource, 'runtimes',
runtime_id)
# 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"},
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)
# Invoke function
resp, body = self.client.create_execution(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)
# Get execution log
resp, body = self.client.get_execution_log(execution_id)
self.assertEqual(200, resp.status)
self.assertIn('Hello, Qinling', body)