Add cpu and memory_size options support for function

Allow users to specify cpu/memory_size when creating function, and
allow to update their values saved in function database.

This patch is api part of customized cpu/mem in qinling server and
is based on patch [0].

[0]: https://review.openstack.org/#/c/562507/
Story: 2001586
Task: 14366

Change-Id: I7a245f93a445a00c2722238d3f94d3a960f16af4
This commit is contained in:
Jiangyuan 2018-04-24 15:48:28 +08:00 committed by Hunt Xu
parent c3a081f273
commit e0e8f3d13d
6 changed files with 152 additions and 1 deletions

View File

@ -34,6 +34,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 common
from qinling.utils import constants from qinling.utils import constants
from qinling.utils import etcd_util from qinling.utils import etcd_util
from qinling.utils.openstack import keystone as keystone_util from qinling.utils.openstack import keystone as keystone_util
@ -45,7 +46,8 @@ 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', 'code', 'package', 'entry']) UPDATE_ALLOWED = set(['name', 'description', 'code', 'package', 'entry',
'cpu', 'memory_size'])
class FunctionWorkerController(rest.RestController): class FunctionWorkerController(rest.RestController):
@ -147,8 +149,22 @@ class FunctionsController(rest.RestController):
'runtime_id': kwargs.get('runtime_id'), 'runtime_id': kwargs.get('runtime_id'),
'code': json.loads(kwargs['code']), 'code': json.loads(kwargs['code']),
'entry': kwargs.get('entry', 'main.main'), '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
),
} }
# Check cpu and memory_size values.
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') source = values['code'].get('source')
if not source or source not in CODE_SOURCE: if not source or source not in CODE_SOURCE:
raise exc.InputException( raise exc.InputException(
@ -302,6 +318,22 @@ class FunctionsController(rest.RestController):
else: else:
source = values.get('code', {}).get('source') source = values.get('code', {}).get('source')
md5sum = values.get('code', {}).get('md5sum') 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(): with db_api.transaction():
pre_func = db_api.get_function(id) pre_func = db_api.get_function(id)

View File

@ -164,6 +164,7 @@ class Function(Resource):
id = wtypes.text id = wtypes.text
name = wtypes.text name = wtypes.text
description = wtypes.text description = wtypes.text
cpu = int
memory_size = int memory_size = int
timeout = int timeout = int
runtime_id = wsme.wsattr(types.uuid, readonly=True) runtime_id = wsme.wsattr(types.uuid, readonly=True)
@ -181,6 +182,7 @@ class Function(Resource):
id='123e4567-e89b-12d3-a456-426655440000', id='123e4567-e89b-12d3-a456-426655440000',
name='hello_world', name='hello_world',
description='this is the first function.', description='this is the first function.',
cpu=1,
memory_size=1, memory_size=1,
timeout=1, timeout=1,
runtime_id='123e4567-e89b-12d3-a456-426655440001', runtime_id='123e4567-e89b-12d3-a456-426655440001',

View File

@ -184,6 +184,40 @@ etcd_opts = [
cfg.PortOpt('port', default=2379, help='Etcd service port.'), cfg.PortOpt('port', default=2379, help='Etcd service port.'),
] ]
RLIMITS_GROUP = 'resource_limits'
rlimits_opts = [
cfg.IntOpt(
'default_cpu',
default=100,
help='Default cpu resource(unit: millicpu).'
),
cfg.IntOpt(
'min_cpu',
default=100,
help='Minimum cpu resource(unit: millicpu).'
),
cfg.IntOpt(
'max_cpu',
default=300,
help='Maximum cpu resource(unit: millicpu).'
),
cfg.IntOpt(
'default_memory',
default=33554432,
help='Default memory resource(unit: bytes).'
),
cfg.IntOpt(
'min_memory',
default=33554432,
help='Minimum memory resource(unit: bytes).'
),
cfg.IntOpt(
'max_memory',
default=134217728,
help='Maximum memory resource(unit: bytes).'
),
]
def list_opts(): def list_opts():
keystone_middleware_opts = auth_token.list_opts() keystone_middleware_opts = auth_token.list_opts()
@ -198,6 +232,7 @@ def list_opts():
(STORAGE_GROUP, storage_opts), (STORAGE_GROUP, storage_opts),
(KUBERNETES_GROUP, kubernetes_opts), (KUBERNETES_GROUP, kubernetes_opts),
(ETCD_GROUP, etcd_opts), (ETCD_GROUP, etcd_opts),
(RLIMITS_GROUP, rlimits_opts),
(None, [launch_opt]), (None, [launch_opt]),
(None, default_opts), (None, default_opts),
] ]

View File

@ -17,6 +17,7 @@ import json
import tempfile import tempfile
import mock import mock
from oslo_config import cfg
from qinling import status from qinling import status
from qinling.tests.unit.api import base from qinling.tests.unit.api import base
@ -97,6 +98,8 @@ class TestFunctionController(base.APITest):
"name": db_func.name, "name": db_func.name,
'entry': 'main.main', 'entry': 'main.main',
"project_id": unit_base.DEFAULT_PROJECT_ID, "project_id": unit_base.DEFAULT_PROJECT_ID,
"cpu": cfg.CONF.resource_limits.default_cpu,
"memory_size": cfg.CONF.resource_limits.default_memory,
} }
resp = self.app.get('/v1/functions/%s' % db_func.id) resp = self.app.get('/v1/functions/%s' % db_func.id)
@ -111,6 +114,8 @@ class TestFunctionController(base.APITest):
"name": db_func.name, "name": db_func.name,
'entry': 'main.main', 'entry': 'main.main',
"project_id": unit_base.DEFAULT_PROJECT_ID, "project_id": unit_base.DEFAULT_PROJECT_ID,
"cpu": cfg.CONF.resource_limits.default_cpu,
"memory_size": cfg.CONF.resource_limits.default_memory,
} }
resp = self.app.get('/v1/functions') resp = self.app.get('/v1/functions')
@ -173,6 +178,63 @@ class TestFunctionController(base.APITest):
self.assertEqual(400, resp.status_int) self.assertEqual(400, resp.status_int)
def test_put_cpu_with_type_error(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, {'cpu': 'non-int'},
expect_errors=True
)
self.assertEqual(400, resp.status_int)
self.assertIn(
'Invalid cpu resource specified. An integer is required.',
resp.json['faultstring']
)
def test_put_cpu_with_overrun_error(self):
db_func = self.create_function(runtime_id=self.runtime_id)
# Check for cpu error with input out of range.
resp = self.app.put_json(
'/v1/functions/%s' % db_func.id, {'cpu': 0},
expect_errors=True
)
self.assertEqual(400, resp.status_int)
self.assertIn(
'cpu resource limitation not within the allowable range',
resp.json['faultstring']
)
@mock.patch('qinling.utils.etcd_util.delete_function')
@mock.patch('qinling.rpc.EngineClient.delete_function')
def test_put_cpu_and_memorysize(
self, mock_delete_func, mock_etcd_del
):
# Test for updating cpu/mem with good input values.
db_func = self.create_function(runtime_id=self.runtime_id)
req_body = {
'cpu': str(cfg.CONF.resource_limits.default_cpu + 1),
'memory_size': str(cfg.CONF.resource_limits.default_memory + 1)
}
resp = self.app.put_json('/v1/functions/%s' % db_func.id, req_body)
self.assertEqual(200, resp.status_int)
self.assertEqual(
cfg.CONF.resource_limits.default_cpu + 1,
resp.json['cpu']
)
self.assertEqual(
cfg.CONF.resource_limits.default_memory + 1,
resp.json['memory_size']
)
mock_delete_func.assert_called_once_with(db_func.id)
mock_etcd_del.assert_called_once_with(db_func.id)
@mock.patch('qinling.utils.etcd_util.delete_function') @mock.patch('qinling.utils.etcd_util.delete_function')
@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')

View File

@ -139,6 +139,7 @@ class DbTestCase(BaseTest):
(config.STORAGE_GROUP, config.storage_opts), (config.STORAGE_GROUP, config.storage_opts),
(config.KUBERNETES_GROUP, config.kubernetes_opts), (config.KUBERNETES_GROUP, config.kubernetes_opts),
(config.ETCD_GROUP, config.etcd_opts), (config.ETCD_GROUP, config.etcd_opts),
(config.RLIMITS_GROUP, config.rlimits_opts),
(None, [config.launch_opt]), (None, [config.launch_opt]),
(None, config.default_opts) (None, config.default_opts)
] ]
@ -193,6 +194,8 @@ class DbTestCase(BaseTest):
# 'auth_enable' is disabled by default, we create runtime for # 'auth_enable' is disabled by default, we create runtime for
# default tenant. # default tenant.
'project_id': DEFAULT_PROJECT_ID, 'project_id': DEFAULT_PROJECT_ID,
'cpu': cfg.CONF.resource_limits.default_cpu,
'memory_size': cfg.CONF.resource_limits.default_memory,
} }
) )

View File

@ -20,6 +20,7 @@ import warnings
from oslo_utils import uuidutils from oslo_utils import uuidutils
import six import six
from qinling import exceptions as exc
from qinling import version from qinling import version
@ -91,6 +92,22 @@ def generate_unicode_uuid(dashed=True):
return uuidutils.generate_uuid(dashed=dashed) return uuidutils.generate_uuid(dashed=dashed)
def validate_int_in_range(name, value, min_allowed, max_allowed):
try:
value_int = int(value)
except ValueError:
raise exc.InputException(
'Invalid %s resource specified. An integer is required.' % name
)
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')
)
def disable_ssl_warnings(func): def disable_ssl_warnings(func):
@functools.wraps(func) @functools.wraps(func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):