Support to update function code
Change-Id: If7698350925119140b46cf319ad74f3e063ef0a6 Closes-Bug: #1733477
This commit is contained in:
parent
b2d82ee744
commit
9f5b474b6f
@ -1,13 +1,27 @@
|
|||||||
|
# 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 os
|
import os
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from zaqarclient.queues import client
|
from zaqarclient.queues import client
|
||||||
|
|
||||||
|
|
||||||
def _send_message(z_client, queue_name, status, message=''):
|
def _send_message(z_client, queue_name, status, server=''):
|
||||||
queue_name = queue_name or 'test_queue'
|
queue_name = queue_name or 'test_queue'
|
||||||
queue = z_client.queue(queue_name)
|
queue = z_client.queue(queue_name)
|
||||||
queue.post({"body": {'status': status, 'message': message}})
|
queue.post({"body": {'status': status, 'server': server}})
|
||||||
print 'message posted.'
|
print 'message posted.'
|
||||||
|
|
||||||
|
|
||||||
@ -24,14 +38,14 @@ def check_and_trigger(context, **kwargs):
|
|||||||
with open(file_name, 'r+') as f:
|
with open(file_name, 'r+') as f:
|
||||||
count = int(f.readline())
|
count = int(f.readline())
|
||||||
count += 1
|
count += 1
|
||||||
if count >= 3:
|
if count == 3:
|
||||||
|
# Send message and stop trigger after 3 checks
|
||||||
z_client = client.Client(
|
z_client = client.Client(
|
||||||
session=context['os_session'],
|
session=context['os_session'],
|
||||||
version=2,
|
version=2,
|
||||||
)
|
)
|
||||||
_send_message(z_client, kwargs.get('queue'), r.status_code,
|
_send_message(z_client, kwargs.get('queue'), r.status_code,
|
||||||
'Service Not Available!')
|
'api1.production.catalyst.co.nz')
|
||||||
count = 0
|
|
||||||
|
|
||||||
f.seek(0)
|
f.seek(0)
|
||||||
f.write(str(count))
|
f.write(str(count))
|
||||||
|
@ -31,6 +31,7 @@ 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
|
||||||
from qinling.storage import base as storage_base
|
from qinling.storage import base as storage_base
|
||||||
|
from qinling.utils import constants
|
||||||
from qinling.utils.openstack import keystone as keystone_util
|
from qinling.utils.openstack import keystone as keystone_util
|
||||||
from qinling.utils.openstack import swift as swift_util
|
from qinling.utils.openstack import swift as swift_util
|
||||||
from qinling.utils import rest_utils
|
from qinling.utils import rest_utils
|
||||||
@ -40,7 +41,7 @@ CONF = cfg.CONF
|
|||||||
|
|
||||||
POST_REQUIRED = set(['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', 'code', 'package', 'entry'])
|
||||||
|
|
||||||
|
|
||||||
class FunctionsController(rest.RestController):
|
class FunctionsController(rest.RestController):
|
||||||
@ -56,6 +57,15 @@ class FunctionsController(rest.RestController):
|
|||||||
|
|
||||||
super(FunctionsController, self).__init__(*args, **kwargs)
|
super(FunctionsController, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def _check_swift(self, container, object):
|
||||||
|
# Auth needs to be enabled because qinling needs to check swift
|
||||||
|
# object using user's credential.
|
||||||
|
if not CONF.pecan.auth_enable:
|
||||||
|
raise exc.InputException('Swift object not supported.')
|
||||||
|
|
||||||
|
if not swift_util.check_object(container, object):
|
||||||
|
raise exc.InputException('Object does not exist in Swift.')
|
||||||
|
|
||||||
@rest_utils.wrap_pecan_controller_exception
|
@rest_utils.wrap_pecan_controller_exception
|
||||||
@pecan.expose()
|
@pecan.expose()
|
||||||
def get(self, id):
|
def get(self, id):
|
||||||
@ -121,7 +131,7 @@ class FunctionsController(rest.RestController):
|
|||||||
', '.join(CODE_SOURCE)
|
', '.join(CODE_SOURCE)
|
||||||
)
|
)
|
||||||
|
|
||||||
if source != 'image':
|
if source != constants.IMAGE_FUNCTION:
|
||||||
if not kwargs.get('runtime_id'):
|
if not kwargs.get('runtime_id'):
|
||||||
raise exc.InputException('"runtime_id" must be specified.')
|
raise exc.InputException('"runtime_id" must be specified.')
|
||||||
|
|
||||||
@ -132,20 +142,13 @@ class FunctionsController(rest.RestController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
store = False
|
store = False
|
||||||
if values['code']['source'] == 'package':
|
if values['code']['source'] == constants.PACKAGE_FUNCTION:
|
||||||
store = True
|
store = True
|
||||||
data = kwargs['package'].file.read()
|
data = kwargs['package'].file.read()
|
||||||
elif values['code']['source'] == 'swift':
|
elif values['code']['source'] == constants.SWIFT_FUNCTION:
|
||||||
# Auth needs to be enabled because qinling needs to check swift
|
swift_info = values['code'].get('swift', {})
|
||||||
# object using user's credential.
|
self._check_swift(swift_info.get('container'),
|
||||||
if not CONF.pecan.auth_enable:
|
swift_info.get('object'))
|
||||||
raise exc.InputException('Swift object not supported.')
|
|
||||||
|
|
||||||
container = values['code']['swift'].get('container')
|
|
||||||
object = values['code']['swift'].get('object')
|
|
||||||
|
|
||||||
if not swift_util.check_object(container, object):
|
|
||||||
raise exc.InputException('Object does not exist in Swift.')
|
|
||||||
|
|
||||||
if cfg.CONF.pecan.auth_enable:
|
if cfg.CONF.pecan.auth_enable:
|
||||||
try:
|
try:
|
||||||
@ -208,33 +211,70 @@ class FunctionsController(rest.RestController):
|
|||||||
# This will also delete function service mapping as well.
|
# This will also delete function service mapping as well.
|
||||||
db_api.delete_function(id)
|
db_api.delete_function(id)
|
||||||
|
|
||||||
@rest_utils.wrap_wsme_controller_exception
|
@rest_utils.wrap_pecan_controller_exception
|
||||||
@wsme_pecan.wsexpose(
|
@pecan.expose('json')
|
||||||
resources.Function,
|
def put(self, id, **kwargs):
|
||||||
types.uuid,
|
|
||||||
body=resources.Function
|
|
||||||
)
|
|
||||||
def put(self, id, func):
|
|
||||||
"""Update function.
|
"""Update function.
|
||||||
|
|
||||||
Currently, we only support update name, description, entry.
|
- Function can not being used by job.
|
||||||
|
- Function can not being executed.
|
||||||
|
- (TODO)Function status should be changed so no execution will create
|
||||||
|
when function is updating.
|
||||||
"""
|
"""
|
||||||
values = {}
|
values = {}
|
||||||
for key in UPDATE_ALLOWED:
|
for key in UPDATE_ALLOWED:
|
||||||
if func.to_dict().get(key) is not None:
|
if kwargs.get(key) is not None:
|
||||||
values.update({key: func.to_dict()[key]})
|
values.update({key: kwargs[key]})
|
||||||
|
|
||||||
LOG.info('Update resource, params: %s', values,
|
LOG.info('Update resource, params: %s', values,
|
||||||
resource={'type': self.type, 'id': id})
|
resource={'type': self.type, 'id': id})
|
||||||
|
|
||||||
with db_api.transaction():
|
ctx = context.get_ctx()
|
||||||
|
|
||||||
|
if set(values.keys()).issubset(set(['name', 'description'])):
|
||||||
func_db = db_api.update_function(id, values)
|
func_db = db_api.update_function(id, values)
|
||||||
if 'entry' in values:
|
else:
|
||||||
# Update entry will delete allocated resources in orchestrator.
|
source = values.get('code', {}).get('source')
|
||||||
|
with db_api.transaction():
|
||||||
|
pre_func = db_api.get_function(id)
|
||||||
|
|
||||||
|
if len(pre_func.jobs) > 0:
|
||||||
|
raise exc.NotAllowedException(
|
||||||
|
'The function is still associated with running job(s).'
|
||||||
|
)
|
||||||
|
|
||||||
|
pre_source = pre_func.code['source']
|
||||||
|
if source and source != pre_source:
|
||||||
|
raise exc.InputException(
|
||||||
|
"The function code type can not be changed."
|
||||||
|
)
|
||||||
|
if source == constants.IMAGE_FUNCTION:
|
||||||
|
raise exc.InputException(
|
||||||
|
"The image type function code can not be changed."
|
||||||
|
)
|
||||||
|
if (pre_source == constants.PACKAGE_FUNCTION and
|
||||||
|
values.get('package') is not None):
|
||||||
|
# Update the package data.
|
||||||
|
data = values['package'].file.read()
|
||||||
|
self.storage_provider.store(
|
||||||
|
ctx.projectid,
|
||||||
|
id,
|
||||||
|
data
|
||||||
|
)
|
||||||
|
values.pop('package')
|
||||||
|
if pre_source == constants.SWIFT_FUNCTION:
|
||||||
|
swift_info = values['code'].get('swift', {})
|
||||||
|
self._check_swift(swift_info.get('container'),
|
||||||
|
swift_info.get('object'))
|
||||||
|
|
||||||
|
# Delete allocated resources in orchestrator.
|
||||||
db_api.delete_function_service_mapping(id)
|
db_api.delete_function_service_mapping(id)
|
||||||
self.engine_client.delete_function(id)
|
self.engine_client.delete_function(id)
|
||||||
|
|
||||||
return resources.Function.from_dict(func_db.to_dict())
|
func_db = db_api.update_function(id, values)
|
||||||
|
|
||||||
|
pecan.response.status = 200
|
||||||
|
return resources.Function.from_dict(func_db.to_dict()).to_dict()
|
||||||
|
|
||||||
@rest_utils.wrap_wsme_controller_exception
|
@rest_utils.wrap_wsme_controller_exception
|
||||||
@wsme_pecan.wsexpose(
|
@wsme_pecan.wsexpose(
|
||||||
|
@ -106,6 +106,7 @@ class Context(oslo_context.RequestContext):
|
|||||||
{
|
{
|
||||||
'is_trust_scoped': self.is_trust_scoped,
|
'is_trust_scoped': self.is_trust_scoped,
|
||||||
'trust_id': self.trust_id,
|
'trust_id': self.trust_id,
|
||||||
|
'auth_token': self.auth_token,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import requests
|
|||||||
from qinling.db import api as db_api
|
from qinling.db import api as db_api
|
||||||
from qinling import status
|
from qinling import status
|
||||||
from qinling.utils import common
|
from qinling.utils import common
|
||||||
|
from qinling.utils import constants
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -119,7 +120,7 @@ class DefaultEngine(object):
|
|||||||
identifier = None
|
identifier = None
|
||||||
labels = None
|
labels = None
|
||||||
|
|
||||||
if source == 'image':
|
if source == constants.IMAGE_FUNCTION:
|
||||||
image = function.code['image']
|
image = function.code['image']
|
||||||
identifier = ('%s-%s' %
|
identifier = ('%s-%s' %
|
||||||
(common.generate_unicode_uuid(dashed=False),
|
(common.generate_unicode_uuid(dashed=False),
|
||||||
|
@ -33,6 +33,12 @@ class FileSystemStorage(base.PackageStorage):
|
|||||||
fileutils.ensure_tree(CONF.storage.file_system_dir)
|
fileutils.ensure_tree(CONF.storage.file_system_dir)
|
||||||
|
|
||||||
def store(self, project_id, function, data):
|
def store(self, project_id, function, data):
|
||||||
|
"""Store the function package data to local file system.
|
||||||
|
|
||||||
|
:param project_id: Project ID.
|
||||||
|
:param function: Function ID.
|
||||||
|
:param data: Package data.
|
||||||
|
"""
|
||||||
LOG.info(
|
LOG.info(
|
||||||
'Store package, function: %s, project: %s', function, project_id
|
'Store package, function: %s, project: %s', function, project_id
|
||||||
)
|
)
|
||||||
@ -46,10 +52,15 @@ class FileSystemStorage(base.PackageStorage):
|
|||||||
|
|
||||||
if not zipfile.is_zipfile(func_zip):
|
if not zipfile.is_zipfile(func_zip):
|
||||||
fileutils.delete_if_exists(func_zip)
|
fileutils.delete_if_exists(func_zip)
|
||||||
|
|
||||||
raise exc.InputException("Package is not a valid ZIP package.")
|
raise exc.InputException("Package is not a valid ZIP package.")
|
||||||
|
|
||||||
def retrieve(self, project_id, function):
|
def retrieve(self, project_id, function):
|
||||||
|
"""Get function package data.
|
||||||
|
|
||||||
|
:param project_id: Project ID.
|
||||||
|
:param function: Function ID.
|
||||||
|
:return: File descriptor that needs to close outside.
|
||||||
|
"""
|
||||||
LOG.info(
|
LOG.info(
|
||||||
'Get package data, function: %s, project: %s', function, project_id
|
'Get package data, function: %s, project: %s', function, project_id
|
||||||
)
|
)
|
||||||
|
@ -36,10 +36,6 @@ class TestFunctionController(base.APITest):
|
|||||||
|
|
||||||
@mock.patch('qinling.storage.file_system.FileSystemStorage.store')
|
@mock.patch('qinling.storage.file_system.FileSystemStorage.store')
|
||||||
def test_post(self, mock_store):
|
def test_post(self, mock_store):
|
||||||
class File(object):
|
|
||||||
def __init__(self, f):
|
|
||||||
self.file = f
|
|
||||||
|
|
||||||
with tempfile.NamedTemporaryFile() as f:
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
body = {
|
body = {
|
||||||
'name': self.rand_name('function', prefix=TEST_CASE_NAME),
|
'name': self.rand_name('function', prefix=TEST_CASE_NAME),
|
||||||
@ -107,6 +103,24 @@ class TestFunctionController(base.APITest):
|
|||||||
self.assertEqual(200, resp.status_int)
|
self.assertEqual(200, resp.status_int)
|
||||||
self.assertEqual('new_name', resp.json['name'])
|
self.assertEqual('new_name', resp.json['name'])
|
||||||
|
|
||||||
|
@mock.patch('qinling.storage.file_system.FileSystemStorage.store')
|
||||||
|
@mock.patch('qinling.rpc.EngineClient.delete_function')
|
||||||
|
def test_put_package(self, mock_delete_func, mock_store):
|
||||||
|
db_func = self.create_function(
|
||||||
|
runtime_id=self.runtime_id, prefix=TEST_CASE_NAME
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile() as f:
|
||||||
|
resp = self.app.put(
|
||||||
|
'/v1/functions/%s' % db_func.id,
|
||||||
|
params={},
|
||||||
|
upload_files=[('package', f.name, f.read())]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(200, resp.status_int)
|
||||||
|
self.assertEqual(1, mock_store.call_count)
|
||||||
|
mock_delete_func.assert_called_once_with(db_func.id)
|
||||||
|
|
||||||
@mock.patch('qinling.rpc.EngineClient.delete_function')
|
@mock.patch('qinling.rpc.EngineClient.delete_function')
|
||||||
@mock.patch('qinling.storage.file_system.FileSystemStorage.delete')
|
@mock.patch('qinling.storage.file_system.FileSystemStorage.delete')
|
||||||
def test_delete(self, mock_delete, mock_delete_func):
|
def test_delete(self, mock_delete, mock_delete_func):
|
||||||
|
@ -16,3 +16,7 @@ EXECUTION_BY_JOB = 'Created by Job %s'
|
|||||||
|
|
||||||
PERIODIC_JOB_HANDLER = 'job_handler'
|
PERIODIC_JOB_HANDLER = 'job_handler'
|
||||||
PERIODIC_FUNC_MAPPING_HANDLER = 'function_mapping_handler'
|
PERIODIC_FUNC_MAPPING_HANDLER = 'function_mapping_handler'
|
||||||
|
|
||||||
|
PACKAGE_FUNCTION = 'package'
|
||||||
|
SWIFT_FUNCTION = 'swift'
|
||||||
|
IMAGE_FUNCTION = 'image'
|
||||||
|
Loading…
Reference in New Issue
Block a user