Add support for exec resize

This endpoint will be used by cleint to resize the tty session
of exec.

Partial-Implements: blueprint support-interactive-mode
Change-Id: Ied3b90295c75a32f8543a4e92481a01c22b870df
This commit is contained in:
Hongbin Lu 2017-03-23 18:22:41 -05:00
parent ab81332458
commit cd49efcc90
13 changed files with 112 additions and 1 deletions

View File

@ -17,6 +17,7 @@
"container:unpause": "rule:admin_or_user",
"container:logs": "rule:admin_or_user",
"container:execute": "rule:admin_or_user",
"container:execute_resize": "rule:admin_or_user",
"container:kill": "rule:admin_or_user",
"container:update": "rule:admin_or_user",
"container:rename": "rule:admin_or_user",

View File

@ -86,6 +86,7 @@ class ContainersController(rest.RestController):
'unpause': ['POST'],
'logs': ['GET'],
'execute': ['POST'],
'execute_resize': ['POST'],
'kill': ['POST'],
'rename': ['POST'],
'attach': ['GET'],
@ -427,6 +428,21 @@ class ContainersController(rest.RestController):
return compute_api.container_exec(context, container, kw['command'],
run, interactive)
@pecan.expose('json')
@exception.wrap_pecan_controller_exception
@validation.validate_query_param(pecan.request,
schema.query_param_execute_resize)
def execute_resize(self, container_id, exec_id, **kw):
container = _get_container(container_id)
check_policy_on_container(container.as_dict(),
"container:execute_resize")
utils.validate_container_state(container, 'execute_resize')
LOG.debug('Calling tty resize used by exec %s', exec_id)
context = pecan.request.context
compute_api = pecan.request.compute_api
return compute_api.container_exec_resize(
context, container, exec_id, kw.get('h', None), kw.get('w', None))
@pecan.expose('json')
@exception.wrap_pecan_controller_exception
def kill(self, container_id, **kw):

View File

@ -109,3 +109,13 @@ query_param_resize = {
},
'additionalProperties': False
}
query_param_execute_resize = {
'type': 'object',
'properties': {
'exec_id': parameter_types.exec_id,
'h': parameter_types.positive_integer,
'w': parameter_types.positive_integer
},
'additionalProperties': False
}

View File

@ -43,6 +43,7 @@ VALID_STATES = {
'unpause': ['Paused'],
'kill': ['Running'],
'execute': ['Running'],
'execute_resize': ['Running'],
'update': ['Running', 'Stopped', 'Paused', 'Created'],
'attach': ['Running'],
'resize': ['Running'],

View File

@ -151,3 +151,10 @@ logs_since = {
'pattern': '(^[0-9]*$)|\
([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{1,3})'
}
exec_id = {
'type': 'string',
'maxLength': 64,
'minLength': 64,
'pattern': '^[a-f0-9]*$'
}

View File

@ -85,6 +85,9 @@ class API(object):
def container_exec(self, context, container, *args):
return self.rpcapi.container_exec(context, container, *args)
def container_exec_resize(self, context, container, *args):
return self.rpcapi.container_exec_resize(context, container, *args)
def container_kill(self, context, container, *args):
return self.rpcapi.container_kill(context, container, *args)

View File

@ -347,6 +347,19 @@ class Manager(object):
LOG.exception("Unexpected exception: %s", six.text_type(e))
raise
@translate_exception
def container_exec_resize(self, context, exec_id, height, width):
LOG.debug('Resizing the tty session used by the exec: %s', exec_id)
try:
return self.driver.execute_resize(exec_id, height, width)
except exception.DockerError as e:
LOG.error("Error occurred while calling Docker exec API: %s",
six.text_type(e))
raise
except Exception as e:
LOG.exception("Unexpected exception: %s", six.text_type(e))
raise
def _do_container_kill(self, context, container, signal, reraise=False):
LOG.debug('kill signal to container: %s', container.uuid)
try:

View File

@ -80,6 +80,11 @@ class API(rpc_service.API):
container=container, command=command, run=run,
interactive=interactive)
def container_exec_resize(self, context, container, exec_id, height,
width):
return self._call(container.host, 'container_exec_resize',
exec_id=exec_id, height=height, width=width)
def container_kill(self, context, container, signal):
self._cast(container.host, 'container_kill', container=container,
signal=signal)

View File

@ -302,6 +302,18 @@ class DockerDriver(driver.ContainerDriver):
inspect_res = docker.exec_inspect(exec_id)
return {"output": output, "exit_code": inspect_res['ExitCode']}
def execute_resize(self, exec_id, height, width):
height = int(height)
width = int(width)
with docker_utils.docker_client() as docker:
try:
docker.exec_resize(exec_id, height=height, width=width)
except errors.APIError as api_error:
if '404' in str(api_error):
raise exception.Invalid(_(
"no such exec instance: %s") % str(api_error))
raise
@check_container_id
def kill(self, container, signal=None):
with docker_utils.docker_client() as docker:
@ -350,7 +362,7 @@ class DockerDriver(driver.ContainerDriver):
return url
@check_container_id
def resize(self, container, height=None, width=None):
def resize(self, container, height, width):
with docker_utils.docker_client() as docker:
height = int(height)
width = int(width)

View File

@ -108,6 +108,10 @@ class ContainerDriver(object):
"""Run the command specified by an execute instance."""
raise NotImplementedError()
def execute_resize(self, exec_id, height, width):
"""Resizes the tty session used by the exec."""
raise NotImplementedError()
def kill(self, container, signal):
"""kill signal to a container."""
raise NotImplementedError()

View File

@ -1230,3 +1230,22 @@ class TestContainerEnforcement(api_base.FunctionalTest):
self._owner_check('container:%s' % action, self.post_json,
'/containers/%s/%s/' % (container.uuid, action),
{}, expect_errors=True)
@patch('zun.common.utils.validate_container_state')
@patch('zun.compute.api.API.container_exec_resize')
@patch('zun.api.utils.get_resource')
def test_execute_resize_container_exec(
self, mock_get_resource, mock_exec_resize, mock_validate):
test_container = utils.get_test_container()
test_container_obj = objects.Container(self.context, **test_container)
mock_get_resource.return_value = test_container_obj
container_name = test_container.get('name')
url = '/v1/containers/%s/%s/' % (container_name, 'execute_resize')
fake_exec_id = ('7df36611fa1fc855618c2c643835d41d'
'ac3fe568e7688f0bae66f7bcb3cccc6c')
kwargs = {'exec_id': fake_exec_id, 'h': '100', 'w': '100'}
response = self.app.post(url, kwargs)
self.assertEqual(200, response.status_int)
mock_exec_resize.assert_called_once_with(
mock.ANY, test_container_obj, fake_exec_id, kwargs['h'],
kwargs['w'])

View File

@ -500,3 +500,16 @@ class TestManager(base.TestCase):
mock_save.assert_called_once()
mock_inspect.assert_called_once_with(repo_tag)
mock_load.assert_called_once_with(repo_tag, ret['path'])
@mock.patch.object(fake_driver, 'execute_resize')
def test_container_exec_resize(self, mock_resize):
self.compute_manager.container_exec_resize(
self.context, 'fake_exec_id', "100", "100")
mock_resize.assert_called_once_with('fake_exec_id', "100", "100")
@mock.patch.object(fake_driver, 'execute_resize')
def test_container_exec_resize_failed(self, mock_resize):
mock_resize.side_effect = exception.DockerError
self.assertRaises(exception.DockerError,
self.compute_manager.container_exec_resize,
self.context, 'fake_exec_id', "100", "100")

View File

@ -405,6 +405,13 @@ class TestDockerDriver(base.DriverTestCase):
self.assertEqual(result_addresses,
{'default': [{'addr': '127.0.0.1', }, ], })
def test_execute_resize(self):
self.mock_docker.exec_resize = mock.Mock()
fake_exec_id = 'fake_id'
self.driver.execute_resize(fake_exec_id, "100", "100")
self.mock_docker.exec_resize.assert_called_once_with(
fake_exec_id, height=100, width=100)
class TestNovaDockerDriver(base.DriverTestCase):
def setUp(self):