Add container-update API
Allow updating the cpu/memory of a running container Co-Authored-By: Hongbin Lu <hongbin.lu@huawei.com> Change-Id: Id9356c88f995fad6aed33bc21681ee58b2da8ac1 Implements: blueprint container-update
This commit is contained in:
parent
44da21a8f4
commit
4ff152785b
@ -18,6 +18,7 @@
|
||||
"container:logs": "rule:admin_or_user",
|
||||
"container:execute": "rule:admin_or_user",
|
||||
"container:kill": "rule:admin_or_user",
|
||||
"container:update": "rule:admin_or_user",
|
||||
"container:rename": "rule:admin_or_user",
|
||||
|
||||
"image:pull": "rule:default",
|
||||
|
@ -226,7 +226,8 @@ class ContainersController(rest.RestController):
|
||||
|
||||
@pecan.expose('json')
|
||||
@exception.wrap_pecan_controller_exception
|
||||
def patch(self, container_id, **kwargs):
|
||||
@validation.validated(schema.container_update)
|
||||
def patch(self, container_id, **patch):
|
||||
"""Update an existing container.
|
||||
|
||||
:param patch: a json PATCH document to apply to this container.
|
||||
@ -234,25 +235,13 @@ class ContainersController(rest.RestController):
|
||||
context = pecan.request.context
|
||||
container = _get_container(container_id)
|
||||
check_policy_on_container(container.as_dict(), "container:update")
|
||||
try:
|
||||
patch = kwargs.get('patch')
|
||||
container_dict = container.as_dict()
|
||||
new_container_fields = api_utils.apply_jsonpatch(
|
||||
container_dict, patch)
|
||||
except api_utils.JSONPATCH_EXCEPTIONS as e:
|
||||
raise exception.PatchError(patch=patch, reason=e)
|
||||
|
||||
# Update only the fields that have changed
|
||||
for field in objects.Container.fields:
|
||||
try:
|
||||
patch_val = new_container_fields[field]
|
||||
except AttributeError:
|
||||
# Ignore fields that aren't exposed in the API
|
||||
continue
|
||||
if getattr(container, field) != patch_val:
|
||||
setattr(container, field, patch_val)
|
||||
|
||||
container.save(context)
|
||||
utils.validate_container_state(container, 'update')
|
||||
if 'memory' in patch:
|
||||
patch['memory'] = str(patch['memory']) + 'M'
|
||||
if 'cpu' in patch:
|
||||
patch['cpu'] = float(patch['cpu'])
|
||||
compute_api = pecan.request.compute_api
|
||||
container = compute_api.container_update(context, container, patch)
|
||||
return view.format_container(pecan.request.host_url, container)
|
||||
|
||||
@pecan.expose('json')
|
||||
|
@ -50,6 +50,17 @@ query_param_create = {
|
||||
'additionalProperties': False
|
||||
}
|
||||
|
||||
_container_update_properties = {
|
||||
'cpu': parameter_types.cpu,
|
||||
'memory': parameter_types.memory,
|
||||
}
|
||||
|
||||
container_update = {
|
||||
'type': 'object',
|
||||
'properties': _container_update_properties,
|
||||
'additionalProperties': False
|
||||
}
|
||||
|
||||
query_param_delete = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
|
@ -44,6 +44,7 @@ VALID_STATES = {
|
||||
'unpause': ['Paused'],
|
||||
'kill': ['Running'],
|
||||
'execute': ['Running'],
|
||||
'update': ['Running', 'Stopped', 'Paused'],
|
||||
}
|
||||
|
||||
|
||||
@ -140,6 +141,7 @@ def translate_exception(function):
|
||||
return function(self, context, *args, **kwargs)
|
||||
except Exception as e:
|
||||
if not isinstance(e, exception.ZunException):
|
||||
LOG.exception(_LE("Unexpected error: %s"), six.text_type(e))
|
||||
e = exception.ZunException("Unexpected error: %s"
|
||||
% six.text_type(e))
|
||||
raise e
|
||||
|
@ -84,6 +84,9 @@ class API(object):
|
||||
def container_kill(self, context, container, *args):
|
||||
return self.rpcapi.container_kill(context, container, *args)
|
||||
|
||||
def container_update(self, context, container, *args):
|
||||
return self.rpcapi.container_update(context, container, *args)
|
||||
|
||||
def image_show(self, context, image, *args):
|
||||
return self.rpcapi.image_show(context, image, *args)
|
||||
|
||||
|
@ -327,6 +327,24 @@ class Manager(object):
|
||||
def container_kill(self, context, container, signal):
|
||||
utils.spawn_n(self._do_container_kill, context, container, signal)
|
||||
|
||||
@translate_exception
|
||||
def container_update(self, context, container, patch):
|
||||
LOG.debug('Updating a container...', container=container)
|
||||
# Update only the fields that have changed
|
||||
for field, patch_val in patch.items():
|
||||
if getattr(container, field) != patch_val:
|
||||
setattr(container, field, patch_val)
|
||||
|
||||
try:
|
||||
self.driver.update(container)
|
||||
except exception.DockerError as e:
|
||||
LOG.error(_LE("Error occured while calling docker API: %s"),
|
||||
six.text_type(e))
|
||||
raise
|
||||
|
||||
container.save(context)
|
||||
return container
|
||||
|
||||
def image_pull(self, context, image):
|
||||
utils.spawn_n(self._do_image_pull, context, image)
|
||||
|
||||
|
@ -78,6 +78,10 @@ class API(rpc_service.API):
|
||||
self._cast(container.host, 'container_kill', container=container,
|
||||
signal=signal)
|
||||
|
||||
def container_update(self, context, container, patch):
|
||||
return self._call(container.host, 'container_update',
|
||||
container=container, patch=patch)
|
||||
|
||||
def image_show(self, context, image):
|
||||
# NOTE(hongbin): Image API doesn't support multiple compute nodes
|
||||
# scenario yet, so we temporarily set host to None and rpc will
|
||||
|
@ -18,7 +18,7 @@ docker_group = cfg.OptGroup(name='docker',
|
||||
|
||||
docker_opts = [
|
||||
cfg.StrOpt('docker_remote_api_version',
|
||||
default='1.20',
|
||||
default='1.22',
|
||||
help='Docker remote api version. Override it according to '
|
||||
'specific docker api version in your environment.'),
|
||||
cfg.IntOpt('default_timeout',
|
||||
|
@ -230,6 +230,26 @@ class DockerDriver(driver.ContainerDriver):
|
||||
self._populate_container(container, response)
|
||||
return container
|
||||
|
||||
@check_container_id
|
||||
def update(self, container):
|
||||
patch = container.obj_get_changes()
|
||||
|
||||
args = {}
|
||||
memory = patch.get('memory')
|
||||
if memory is not None:
|
||||
args['mem_limit'] = memory
|
||||
cpu = patch.get('cpu')
|
||||
if cpu is not None:
|
||||
args['cpu_quota'] = int(100000 * cpu)
|
||||
args['cpu_period'] = 100000
|
||||
|
||||
with docker_utils.docker_client() as docker:
|
||||
try:
|
||||
resp = docker.update_container(container.container_id, **args)
|
||||
return resp
|
||||
except errors.APIError:
|
||||
raise
|
||||
|
||||
def _encode_utf8(self, value):
|
||||
if six.PY2 and not isinstance(value, unicode):
|
||||
value = unicode(value)
|
||||
|
@ -139,4 +139,7 @@ class ContainerDriver(object):
|
||||
|
||||
def get_addresses(self, context, container):
|
||||
"""Retrieve IP addresses of the container."""
|
||||
|
||||
def update(self, container):
|
||||
"""Update a container."""
|
||||
raise NotImplementedError()
|
||||
|
@ -155,6 +155,11 @@ class ZunClient(rest_client.RestClient):
|
||||
return self.get(
|
||||
self.container_uri(container_id, action='logs'), None, **kwargs)
|
||||
|
||||
def update_container(self, container_id, model, **kwargs):
|
||||
resp, body = self.patch(
|
||||
self.container_uri(container_id), body=model.to_json(), **kwargs)
|
||||
return self.deserialize(resp, body, container_model.ContainerEntity)
|
||||
|
||||
def list_services(self, **kwargs):
|
||||
resp, body = self.get(self.services_uri(), **kwargs)
|
||||
return self.deserialize(resp, body,
|
||||
|
@ -60,3 +60,15 @@ def container_data(**kwargs):
|
||||
model = container_model.ContainerEntity.from_dict(data)
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def container_patch_data(**kwargs):
|
||||
data = {
|
||||
'cpu': 0.2,
|
||||
'memory': '512',
|
||||
}
|
||||
|
||||
data.update(kwargs)
|
||||
model = container_model.ContainerPatchEntity.from_dict(data)
|
||||
|
||||
return model
|
||||
|
@ -28,3 +28,14 @@ class ContainerCollection(base_model.CollectionModel):
|
||||
"""Collection Model that represents a list of ContainerData objects"""
|
||||
COLLECTION_NAME = 'containerlists'
|
||||
MODEL_TYPE = ContainerData
|
||||
|
||||
|
||||
class ContainerPatchData(base_model.BaseModel):
|
||||
"""Data that encapsulates container update attributes"""
|
||||
pass
|
||||
|
||||
|
||||
class ContainerPatchEntity(base_model.EntityModel):
|
||||
"""Entity Model that represents a single instance of ContainerPatchData"""
|
||||
ENTITY_NAME = 'containerpatch'
|
||||
MODEL_TYPE = ContainerPatchData
|
||||
|
@ -149,6 +149,33 @@ class TestContainer(base.BaseZunTest):
|
||||
self.assertEqual(200, resp.status)
|
||||
self.assertTrue('hello' in body)
|
||||
|
||||
@decorators.idempotent_id('d383f359-3ebd-40ef-9dc5-d36922790230')
|
||||
def test_update_container(self):
|
||||
_, model = self._run_container(cpu=0.1, memory=100)
|
||||
self.assertEqual('100M', model.memory)
|
||||
self.assertEqual(0.1, model.cpu)
|
||||
container = self.docker_client.get_container(model.uuid)
|
||||
self._assert_resource_constraints(container, cpu=0.1, memory=100)
|
||||
|
||||
gen_model = datagen.container_patch_data(cpu=0.2, memory=200)
|
||||
resp, model = self.container_client.update_container(model.uuid,
|
||||
gen_model)
|
||||
self.assertEqual(200, resp.status)
|
||||
self.assertEqual('200M', model.memory)
|
||||
self.assertEqual(0.2, model.cpu)
|
||||
container = self.docker_client.get_container(model.uuid)
|
||||
self._assert_resource_constraints(container, cpu=0.2, memory=200)
|
||||
|
||||
def _assert_resource_constraints(self, container, cpu=None, memory=None):
|
||||
if cpu is not None:
|
||||
cpu_quota = container.get('HostConfig').get('CpuQuota')
|
||||
self.assertEqual(int(cpu * 100000), cpu_quota)
|
||||
cpu_period = container.get('HostConfig').get('CpuPeriod')
|
||||
self.assertEqual(100000, cpu_period)
|
||||
if memory is not None:
|
||||
docker_memory = container.get('HostConfig').get('Memory')
|
||||
self.assertEqual(memory * 1024 * 1024, docker_memory)
|
||||
|
||||
def _create_container(self, **kwargs):
|
||||
gen_model = datagen.container_data(**kwargs)
|
||||
resp, model = self.container_client.post_container(gen_model)
|
||||
|
@ -539,43 +539,39 @@ class TestContainerController(api_base.FunctionalTest):
|
||||
self.assertEqual(test_container['uuid'],
|
||||
response.json['uuid'])
|
||||
|
||||
@patch('zun.compute.rpcapi.API.container_update')
|
||||
@patch('zun.objects.Container.get_by_uuid')
|
||||
def test_patch_by_uuid(self, mock_container_get_by_uuid):
|
||||
def test_patch_by_uuid(self, mock_container_get_by_uuid, mock_update):
|
||||
test_container = utils.get_test_container()
|
||||
test_container_obj = objects.Container(self.context, **test_container)
|
||||
mock_container_get_by_uuid.return_value = test_container_obj
|
||||
mock_update.return_value = test_container_obj
|
||||
|
||||
with patch.object(test_container_obj, 'save') as mock_save:
|
||||
params = {'patch': [{'path': '/name',
|
||||
'value': 'new_name',
|
||||
'op': 'replace'}]}
|
||||
container_uuid = test_container.get('uuid')
|
||||
response = self.app.patch_json(
|
||||
'/v1/containers/%s/' % container_uuid,
|
||||
params=params)
|
||||
params = {'cpu': 1}
|
||||
container_uuid = test_container.get('uuid')
|
||||
response = self.app.patch_json(
|
||||
'/v1/containers/%s/' % container_uuid,
|
||||
params=params)
|
||||
|
||||
mock_save.assert_called_once()
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual('new_name', test_container_obj.name)
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertTrue(mock_update.called)
|
||||
|
||||
@patch('zun.compute.rpcapi.API.container_update')
|
||||
@patch('zun.objects.Container.get_by_name')
|
||||
def test_patch_by_name(self, mock_container_get_by_name):
|
||||
def test_patch_by_name(self, mock_container_get_by_name, mock_update):
|
||||
test_container = utils.get_test_container()
|
||||
test_container_obj = objects.Container(self.context, **test_container)
|
||||
mock_container_get_by_name.return_value = test_container_obj
|
||||
mock_update.return_value = test_container_obj
|
||||
|
||||
with patch.object(test_container_obj, 'save') as mock_save:
|
||||
params = {'patch': [{'path': '/name',
|
||||
'value': 'new_name',
|
||||
'op': 'replace'}]}
|
||||
container_name = test_container.get('name')
|
||||
response = self.app.patch_json(
|
||||
'/v1/containers/%s/' % container_name,
|
||||
params=params)
|
||||
params = {'cpu': 1}
|
||||
container_name = test_container.get('name')
|
||||
response = self.app.patch_json(
|
||||
'/v1/containers/%s/' % container_name,
|
||||
params=params)
|
||||
|
||||
mock_save.assert_called_once()
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertEqual('new_name', test_container_obj.name)
|
||||
self.assertEqual(200, response.status_int)
|
||||
self.assertTrue(mock_update.called)
|
||||
|
||||
def _action_test(self, container, action, ident_field,
|
||||
mock_container_action, status_code, query_param=''):
|
||||
@ -1131,9 +1127,7 @@ class TestContainerEnforcement(api_base.FunctionalTest):
|
||||
|
||||
def test_policy_disallow_update(self):
|
||||
container = obj_utils.create_test_container(self.context)
|
||||
params = {'patch': [{'path': '/name',
|
||||
'value': 'new_name',
|
||||
'op': 'replace'}]}
|
||||
params = {'cpu': 1}
|
||||
self._common_policy_check(
|
||||
'container:update', self.app.patch_json,
|
||||
'/v1/containers/%s/' % container.uuid, params,
|
||||
@ -1178,8 +1172,7 @@ class TestContainerEnforcement(api_base.FunctionalTest):
|
||||
self._owner_check(
|
||||
"container:update", self.patch_json,
|
||||
'/containers/%s/' % container.uuid,
|
||||
{'patch': [{
|
||||
'path': '/name', 'value': "new_name", 'op': 'replace'}]},
|
||||
{'cpu': 1},
|
||||
expect_errors=True)
|
||||
|
||||
def test_policy_only_owner_delete(self):
|
||||
|
@ -357,3 +357,20 @@ class TestManager(base.TestCase):
|
||||
self.assertRaises(exception.DockerError,
|
||||
self.compute_manager._do_container_kill,
|
||||
self.context, container, None, reraise=True)
|
||||
|
||||
@mock.patch.object(Container, 'save')
|
||||
@mock.patch.object(fake_driver, 'update')
|
||||
def test_container_update(self, mock_update, mock_save):
|
||||
container = Container(self.context, **utils.get_test_container())
|
||||
self.compute_manager.container_update(self.context, container,
|
||||
{'memory': 512})
|
||||
mock_save.assert_called_with(self.context)
|
||||
mock_update.assert_called_once_with(container)
|
||||
|
||||
@mock.patch.object(fake_driver, 'update')
|
||||
def test_container_update_failed(self, mock_update):
|
||||
container = Container(self.context, **utils.get_test_container())
|
||||
mock_update.side_effect = exception.DockerError
|
||||
self.assertRaises(exception.DockerError,
|
||||
self.compute_manager.container_update,
|
||||
self.context, container, {})
|
||||
|
@ -83,3 +83,7 @@ class FakeDriver(driver.ContainerDriver):
|
||||
|
||||
def get_addresses(self, context, container):
|
||||
pass
|
||||
|
||||
@check_container_id
|
||||
def update(self, container):
|
||||
pass
|
||||
|
Loading…
Reference in New Issue
Block a user