diff --git a/qinling/api/controllers/v1/function.py b/qinling/api/controllers/v1/function.py index 33220425..d88a882d 100644 --- a/qinling/api/controllers/v1/function.py +++ b/qinling/api/controllers/v1/function.py @@ -46,7 +46,7 @@ CONF = cfg.CONF POST_REQUIRED = set(['code']) CODE_SOURCE = set(['package', 'swift', 'image']) UPDATE_ALLOWED = set(['name', 'description', 'code', 'package', 'entry', - 'cpu', 'memory_size']) + 'cpu', 'memory_size', 'timeout']) class FunctionWorkerController(rest.RestController): @@ -169,7 +169,10 @@ class FunctionsController(rest.RestController): ), } - # Check cpu and memory_size values. + 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 @@ -364,7 +367,14 @@ class FunctionsController(rest.RestController): LOG.info('Update function %s, params: %s', id, values) ctx = context.get_ctx() - if set(values.keys()).issubset(set(['name', 'description'])): + 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') diff --git a/qinling/api/controllers/v1/resources.py b/qinling/api/controllers/v1/resources.py index ed945f56..40d7639d 100644 --- a/qinling/api/controllers/v1/resources.py +++ b/qinling/api/controllers/v1/resources.py @@ -158,11 +158,6 @@ class Link(Resource): target = wtypes.text rel = wtypes.text - @classmethod - def sample(cls): - return cls(href='http://example.com/here', - target='here', rel='self') - class Function(Resource): id = wtypes.text @@ -180,25 +175,6 @@ class Function(Resource): created_at = wtypes.text updated_at = wtypes.text - @classmethod - def sample(cls): - return cls( - id='123e4567-e89b-12d3-a456-426655440000', - name='hello_world', - description='this is the first function.', - cpu=1, - memory_size=1, - timeout=1, - runtime_id='123e4567-e89b-12d3-a456-426655440001', - code={'zip': True}, - entry='main', - count=10, - latest_version=0, - project_id='default', - created_at='1970-01-01T00:00:00.000000', - updated_at='1970-01-01T00:00:00.000000' - ) - class Functions(ResourceList): functions = [Function] @@ -208,18 +184,6 @@ class Functions(ResourceList): super(Functions, self).__init__(**kwargs) - @classmethod - def sample(cls): - sample = cls() - sample.functions = [Function.sample()] - sample.next = ( - "http://localhost:7070/v1/functions?" - "sort_keys=id,name&sort_dirs=asc,desc&limit=10&" - "marker=123e4567-e89b-12d3-a456-426655440000" - ) - - return sample - class FunctionWorker(Resource): function_id = wsme.wsattr(types.uuid, readonly=True) diff --git a/qinling/config.py b/qinling/config.py index 0fdd13ae..1ceb3bfe 100644 --- a/qinling/config.py +++ b/qinling/config.py @@ -259,6 +259,16 @@ rlimits_opts = [ default=5, help='Default function execution timeout(unit: seconds)' ), + cfg.IntOpt( + 'min_timeout', + default=1, + help='Minimum function execution timeout(unit: seconds).' + ), + cfg.IntOpt( + 'max_timeout', + default=300, + help='Maximum function execution timeout(unit: seconds).' + ), ] diff --git a/qinling/tests/unit/api/controllers/v1/test_function.py b/qinling/tests/unit/api/controllers/v1/test_function.py index 5675641d..6846261e 100644 --- a/qinling/tests/unit/api/controllers/v1/test_function.py +++ b/qinling/tests/unit/api/controllers/v1/test_function.py @@ -58,7 +58,8 @@ class TestFunctionController(base.APITest): body.update( { 'entry': 'main.main', - 'code': {"source": "package", "md5sum": "fake_md5"} + 'code': {"source": "package", "md5sum": "fake_md5"}, + 'timeout': cfg.CONF.resource_limits.default_timeout } ) self._assertDictContainsSubset(resp.json, body) @@ -82,6 +83,26 @@ class TestFunctionController(base.APITest): self.assertEqual(201, resp.status_int) self.assertEqual(3, resp.json['timeout']) + def test_post_timeout_invalid(self): + with tempfile.NamedTemporaryFile() as f: + body = { + 'runtime_id': self.runtime_id, + 'code': json.dumps({"source": "package"}), + 'timeout': cfg.CONF.resource_limits.max_timeout + 1 + } + resp = self.app.post( + '/v1/functions', + params=body, + upload_files=[('package', f.name, f.read())], + expect_errors=True + ) + + self.assertEqual(400, resp.status_int) + self.assertIn( + 'timeout resource limitation not within the allowable range', + resp.json['faultstring'] + ) + @mock.patch("qinling.utils.openstack.keystone.create_trust") @mock.patch('qinling.utils.openstack.keystone.get_swiftclient') @mock.patch('qinling.context.AuthHook.before') @@ -217,6 +238,32 @@ class TestFunctionController(base.APITest): self.assertEqual(200, resp.status_int) self.assertEqual('new_name', resp.json['name']) + def test_put_timeout(self): + db_func = self.create_function(runtime_id=self.runtime_id) + + resp = self.app.put_json( + '/v1/functions/%s' % db_func.id, {'timeout': 10} + ) + + self.assertEqual(200, resp.status_int) + self.assertEqual(10, resp.json['timeout']) + + def test_put_timeout_invalid(self): + db_func = self.create_function(runtime_id=self.runtime_id) + + # Check for type of cpu values. + resp = self.app.put_json( + '/v1/functions/%s' % db_func.id, + {'timeout': cfg.CONF.resource_limits.max_timeout + 1}, + expect_errors=True + ) + + self.assertEqual(400, resp.status_int) + self.assertIn( + 'timeout resource limitation not within the allowable range', + resp.json['faultstring'] + ) + @mock.patch('qinling.utils.etcd_util.delete_function') @mock.patch('qinling.storage.file_system.FileSystemStorage.store') @mock.patch('qinling.storage.file_system.FileSystemStorage.delete') diff --git a/qinling/tests/unit/base.py b/qinling/tests/unit/base.py index b8a6ba13..014966b0 100644 --- a/qinling/tests/unit/base.py +++ b/qinling/tests/unit/base.py @@ -182,7 +182,7 @@ class DbTestCase(BaseTest): return runtime - def create_function(self, runtime_id=None, code=None): + def create_function(self, runtime_id=None, code=None, timeout=None): if not runtime_id: runtime_id = self.create_runtime().id @@ -197,7 +197,7 @@ class DbTestCase(BaseTest): 'project_id': DEFAULT_PROJECT_ID, 'cpu': cfg.CONF.resource_limits.default_cpu, 'memory_size': cfg.CONF.resource_limits.default_memory, - 'timeout': cfg.CONF.resource_limits.default_timeout, + 'timeout': timeout or cfg.CONF.resource_limits.default_timeout } ) diff --git a/qinling/utils/common.py b/qinling/utils/common.py index e422984d..1c09ca18 100644 --- a/qinling/utils/common.py +++ b/qinling/utils/common.py @@ -93,6 +93,12 @@ def generate_unicode_uuid(dashed=True): def validate_int_in_range(name, value, min_allowed, max_allowed): + unit_mapping = { + "cpu": "millicpu", + "memory": "bytes", + "timeout": "seconds" + } + try: value_int = int(value) except ValueError: @@ -103,8 +109,8 @@ def validate_int_in_range(name, value, min_allowed, max_allowed): if (value_int < min_allowed or value_int > max_allowed): raise exc.InputException( '%s resource limitation not within the allowable range: ' - '%s ~ %s(%s).' % (name, min_allowed, max_allowed, - 'millicpu' if name == 'cpu' else 'bytes') + '%s ~ %s(%s).' % + (name, min_allowed, max_allowed, unit_mapping[name]) ) diff --git a/qinling_tempest_plugin/tests/api/test_executions.py b/qinling_tempest_plugin/tests/api/test_executions.py index b1bcb0ba..37e245b9 100644 --- a/qinling_tempest_plugin/tests/api/test_executions.py +++ b/qinling_tempest_plugin/tests/api/test_executions.py @@ -506,7 +506,27 @@ class ExecutionsTest(base.BaseQinlingTest): result = jsonutils.loads(body['result']) - self.assertGreater(result['duration'], 5) + self.assertGreaterEqual(result['duration'], 5) self.assertIn( 'Function execution timeout', result['output'] ) + + # Update function timeout + resp, _ = self.client.update_function( + function_id, + timeout=10 + ) + self.assertEqual(200, resp.status_code) + + resp, body = self.client.create_execution( + function_id, + input='{"seconds": 7}' + ) + + self.assertEqual(201, resp.status) + self.addCleanup(self.client.delete_resource, 'executions', + body['id'], ignore_notfound=True) + self.assertEqual('success', body['status']) + + result = jsonutils.loads(body['result']) + self.assertGreaterEqual(result['duration'], 7)