def put(self, id, **kwargs): """Update function. - 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 = {} for key in UPDATE_ALLOWED: if kwargs.get(key) is not None: values.update({key: kwargs[key]}) LOG.info('Update resource, params: %s', values, resource={ 'type': self.type, 'id': id }) ctx = context.get_ctx() if set(values.keys()).issubset(set(['name', 'description'])): func_db = db_api.update_function(id, values) else: 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) self.engine_client.delete_function(id) func_db = db_api.update_function(id, values) pecan.response.status = 200 return resources.Function.from_dict(func_db.to_dict()).to_dict()
def create_function_version(self, old_version, function_id=None, **kwargs): if not function_id: function_id = self.create_function().id db_api.increase_function_version(function_id, old_version, **kwargs) db_api.update_function(function_id, {"latest_version": old_version + 1})
def test_create_execution_prepare_execution_exception( self, etcd_util_get_service_url_mock): """test_create_execution_prepare_execution_exception Create execution for image type function, prepare_execution method raises exception. """ function = self.create_function() function_id = function.id runtime_id = function.runtime_id db_api.update_function( function_id, { 'code': { 'source': constants.IMAGE_FUNCTION, 'image': self.rand_name('image', prefix=self.prefix) } }) function = db_api.get_function(function_id) execution = self.create_execution(function_id=function_id) execution_id = execution.id prepare_execution = self.orchestrator.prepare_execution prepare_execution.side_effect = exc.OrchestratorException( 'Exception in prepare_execution') etcd_util_get_service_url_mock.return_value = None self.default_engine.create_execution(mock.Mock(), execution_id, function_id, 0, runtime_id) execution = db_api.get_execution(execution_id) self.assertEqual(execution.status, status.ERROR) self.assertEqual(execution.logs, '') self.assertEqual(execution.result, {'error': 'Function execution failed.'})
def put(self, id, func): """Update function. Currently, we only support update name, description, entry. """ values = {} for key in UPDATE_ALLOWED: if func.to_dict().get(key) is not None: values.update({key: func.to_dict()[key]}) LOG.info('Update resource, params: %s', values, resource={ 'type': self.type, 'id': id }) with db_api.transaction(): func_db = db_api.update_function(id, values) if 'entry' in values: # Update entry will delete allocated resources in orchestrator. db_api.delete_function_service_mapping(id) self.engine_client.delete_function(id) return resources.Function.from_dict(func_db.to_dict())
def test_handle_function_service_no_function_version( self, mock_etcd_url, mock_etcd_delete): db_func = self.create_function() function_id = db_func.id # Update function to simulate function execution db_api.update_function(function_id, {'count': 1}) time.sleep(1.5) mock_etcd_url.return_value = 'http://localhost:37718' self.override_config('function_service_expiration', 1, 'engine') mock_engine = mock.Mock() periodics.handle_function_service_expiration(self.ctx, mock_engine) mock_engine.delete_function.assert_called_once_with( self.ctx, function_id, 0) mock_etcd_delete.assert_called_once_with(function_id, 0)
def test_function_service_expiration_handler(self, mock_etcd_url, mock_etcd_delete): db_func = self.create_function(runtime_id=None, prefix=self.TEST_CASE_NAME) function_id = db_func.id # Update function to simulate function execution db_api.update_function(function_id, {'count': 1}) time.sleep(1.5) mock_k8s = mock.Mock() mock_etcd_url.return_value = 'http://localhost:37718' self.override_config('function_service_expiration', 1, 'engine') engine = default_engine.DefaultEngine(mock_k8s, CONF.qinling_endpoint) periodics.handle_function_service_expiration(self.ctx, engine) self.assertEqual(1, mock_k8s.delete_function.call_count) args, kwargs = mock_k8s.delete_function.call_args self.assertIn(function_id, args) mock_etcd_delete.assert_called_once_with(function_id)
def test_handle_function_service_with_versioned_function_version_0( self, mock_srv_url, mock_etcd_delete): # This case tests that if a function has multiple versions, service # which serves executions of function version 0 is correctly handled # when expired. db_func = self.create_function() function_id = db_func.id self.create_function_version(0, function_id, description="new_version") # Simulate an execution using version 0 db_api.update_function(function_id, {'count': 1}) time.sleep(1.5) self.override_config('function_service_expiration', 1, 'engine') mock_srv_url.return_value = 'http://localhost:37718' mock_engine = mock.Mock() periodics.handle_function_service_expiration(self.ctx, mock_engine) mock_engine.delete_function.assert_called_once_with( self.ctx, function_id, 0) mock_etcd_delete.assert_called_once_with(function_id, 0)
def put(self, id, **kwargs): """Update function. - 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 = {} try: for key in UPDATE_ALLOWED: if kwargs.get(key) is not None: if key == "code": kwargs[key] = json.loads(kwargs[key]) values.update({key: kwargs[key]}) except Exception as e: raise exc.InputException("Invalid input, %s" % str(e)) LOG.info('Update function %s, params: %s', id, values) ctx = context.get_ctx() if values.get('timeout'): common.validate_int_in_range('timeout', values['timeout'], CONF.resource_limits.min_timeout, CONF.resource_limits.max_timeout) db_update_only = set(['name', 'description', 'timeout']) if set(values.keys()).issubset(db_update_only): func_db = db_api.update_function(id, values) else: source = values.get('code', {}).get('source') md5sum = values.get('code', {}).get('md5sum') cpu = values.get('cpu') memory_size = values.get('memory_size') # Check cpu and memory_size values when updating. if cpu is not None: common.validate_int_in_range('cpu', values['cpu'], CONF.resource_limits.min_cpu, CONF.resource_limits.max_cpu) if memory_size is not None: common.validate_int_in_range('memory', values['memory_size'], CONF.resource_limits.min_memory, CONF.resource_limits.max_memory) 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'] pre_md5sum = pre_func.code.get('md5sum') if source and source != pre_source: raise exc.InputException( "The function code type can not be changed.") if pre_source == constants.IMAGE_FUNCTION: raise exc.InputException( "The image type function code can not be changed.") # Package type function. 'code' and 'entry' make sense only if # 'package' is provided package_updated = False if (pre_source == constants.PACKAGE_FUNCTION and values.get('package') is not None): if md5sum and md5sum == pre_md5sum: raise exc.InputException( "The function code checksum is not changed.") # Update the package data. data = values['package'].file.read() package_updated, md5sum = self.storage_provider.store( ctx.projectid, id, data, md5sum=md5sum) values.setdefault('code', {}).update({ "md5sum": md5sum, "source": pre_source }) values.pop('package') # Swift type function if (pre_source == constants.SWIFT_FUNCTION and "swift" in values.get('code', {})): swift_info = values['code']["swift"] if not (swift_info.get('container') or swift_info.get('object')): raise exc.InputException( "Either container or object must be provided for " "swift type function update.") new_swift_info = pre_func.code['swift'] new_swift_info.update(swift_info) self._check_swift(new_swift_info.get('container'), new_swift_info.get('object')) values['code'] = { "source": pre_source, "swift": new_swift_info } # Delete allocated resources in orchestrator and etcd. self.engine_client.delete_function(id) etcd_util.delete_function(id) func_db = db_api.update_function(id, values) # Delete the old function package if needed if package_updated: self.storage_provider.delete(ctx.projectid, id, pre_md5sum) pecan.response.status = 200 return resources.Function.from_db_obj(func_db).to_dict()
def post(self, **kwargs): # When using image to create function, runtime_id is not a required # param. if not POST_REQUIRED.issubset(set(kwargs.keys())): raise exc.InputException( 'Required param is missing. Required: %s' % POST_REQUIRED) LOG.info("Creating function, params: %s", kwargs) values = { 'name': kwargs.get('name'), 'description': kwargs.get('description'), 'runtime_id': kwargs.get('runtime_id'), 'code': json.loads(kwargs['code']), 'entry': kwargs.get('entry', 'main.main'), 'cpu': kwargs.get('cpu', CONF.resource_limits.default_cpu), 'memory_size': kwargs.get('memory_size', CONF.resource_limits.default_memory), 'timeout': kwargs.get('timeout', CONF.resource_limits.default_timeout), } common.validate_int_in_range('timeout', values['timeout'], CONF.resource_limits.min_timeout, CONF.resource_limits.max_timeout) common.validate_int_in_range('cpu', values['cpu'], CONF.resource_limits.min_cpu, CONF.resource_limits.max_cpu) common.validate_int_in_range('memory', values['memory_size'], CONF.resource_limits.min_memory, CONF.resource_limits.max_memory) source = values['code'].get('source') if not source or source not in CODE_SOURCE: raise exc.InputException( 'Invalid code source specified, available sources: %s' % ', '.join(CODE_SOURCE)) if source != constants.IMAGE_FUNCTION: if not kwargs.get('runtime_id'): raise exc.InputException('"runtime_id" must be specified.') runtime = db_api.get_runtime(kwargs['runtime_id']) if runtime.status != 'available': raise exc.InputException('Runtime %s is not available.' % kwargs['runtime_id']) store = False create_trust = True if source == constants.PACKAGE_FUNCTION: store = True md5sum = values['code'].get('md5sum') data = kwargs['package'].file.read() elif source == constants.SWIFT_FUNCTION: swift_info = values['code'].get('swift', {}) if not (swift_info.get('container') and swift_info.get('object')): raise exc.InputException("Both container and object must be " "provided for swift type function.") self._check_swift(swift_info.get('container'), swift_info.get('object')) else: create_trust = False values['entry'] = None if cfg.CONF.pecan.auth_enable and create_trust: try: values['trust_id'] = keystone_util.create_trust().id LOG.debug('Trust %s created', values['trust_id']) except Exception: raise exc.TrustFailedException( 'Trust creation failed for function.') # Create function and store the package data inside a db transaction so # that the function won't be created if any error happened during # package store. with db_api.transaction(): func_db = db_api.create_function(values) if store: try: ctx = context.get_ctx() _, actual_md5 = self.storage_provider.store(ctx.projectid, func_db.id, data, md5sum=md5sum) values['code'].update({"md5sum": actual_md5}) func_db = db_api.update_function(func_db.id, values) except Exception as e: LOG.exception("Failed to store function package.") keystone_util.delete_trust(values['trust_id']) raise e pecan.response.status = 201 return resources.Function.from_db_obj(func_db).to_dict()
def test_create_execution_image_type_function(self, mock_svc_url): """Create 2 executions for an image type function.""" function = self.create_function() function_id = function.id runtime_id = function.runtime_id db_api.update_function( function_id, { 'code': { 'source': constants.IMAGE_FUNCTION, 'image': self.rand_name('image', prefix=self.prefix) } }) function = db_api.get_function(function_id) execution_1 = self.create_execution(function_id=function_id) execution_1_id = execution_1.id execution_2 = self.create_execution(function_id=function_id) execution_2_id = execution_2.id mock_svc_url.return_value = None self.orchestrator.prepare_execution.return_value = (mock.Mock(), None) self.orchestrator.run_execution.side_effect = [(True, { 'duration': 5, 'logs': 'fake log' }), (False, { 'duration': 0, 'error': 'Function execution failed.' })] # Create two executions, with different results self.default_engine.create_execution(mock.Mock(), execution_1_id, function_id, 0, runtime_id) self.default_engine.create_execution(mock.Mock(), execution_2_id, function_id, 0, runtime_id, input='input') get_service_url_calls = [ mock.call(function_id, 0), mock.call(function_id, 0) ] mock_svc_url.assert_has_calls(get_service_url_calls) prepare_calls = [ mock.call(function_id, 0, rlimit=self.rlimit, image=function.code['image'], identifier=mock.ANY, labels=None, input=None), mock.call(function_id, 0, rlimit=self.rlimit, image=function.code['image'], identifier=mock.ANY, labels=None, input='input') ] self.orchestrator.prepare_execution.assert_has_calls(prepare_calls) run_calls = [ mock.call(execution_1_id, function_id, 0, rlimit=None, input=None, identifier=mock.ANY, service_url=None, entry=function.entry, trust_id=function.trust_id), mock.call(execution_2_id, function_id, 0, rlimit=None, input='input', identifier=mock.ANY, service_url=None, entry=function.entry, trust_id=function.trust_id) ] self.orchestrator.run_execution.assert_has_calls(run_calls) execution_1 = db_api.get_execution(execution_1_id) execution_2 = db_api.get_execution(execution_2_id) self.assertEqual(status.SUCCESS, execution_1.status) self.assertEqual('fake log', execution_1.logs) self.assertEqual({"duration": 5}, execution_1.result) self.assertEqual(status.FAILED, execution_2.status) self.assertEqual('', execution_2.logs) self.assertEqual({ 'duration': 0, 'error': 'Function execution failed.' }, execution_2.result)