From 0e95d556077f20a1e447ea7d8e301d1eb4c54bf5 Mon Sep 17 00:00:00 2001 From: Feng Shengqin Date: Sat, 11 Feb 2017 01:17:27 +0800 Subject: [PATCH] Support the command "zun top" The BP adds "zun top" which can display the running progresses inside the container. Change-Id: I55524598e220e6f227ab789901df9d49fa2357f7 Implements: blueprint support-zun-top --- etc/zun/policy.json | 1 + zun/api/controllers/v1/containers.py | 14 ++++++ zun/api/controllers/v1/schemas/containers.py | 7 +++ zun/common/utils.py | 1 + zun/common/validation/parameter_types.py | 5 ++ zun/compute/api.py | 3 ++ zun/compute/manager.py | 14 ++++++ zun/compute/rpcapi.py | 4 ++ zun/container/docker/driver.py | 11 +++++ zun/container/driver.py | 4 ++ .../api/controllers/v1/test_containers.py | 49 +++++++++++++++++++ 11 files changed, 113 insertions(+) diff --git a/etc/zun/policy.json b/etc/zun/policy.json index bef772ee1..460997f0f 100644 --- a/etc/zun/policy.json +++ b/etc/zun/policy.json @@ -22,6 +22,7 @@ "container:rename": "rule:admin_or_user", "container:attach": "rule:admin_or_user", "container:resize": "rule:admin_or_user", + "container:top": "rule:admin_or_user", "image:pull": "rule:default", "image:get_all": "rule:default", diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index ec1124cc2..eacf5abc4 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -90,6 +90,7 @@ class ContainersController(rest.RestController): 'rename': ['POST'], 'attach': ['GET'], 'resize': ['POST'], + 'top': ['GET'] } @pecan.expose('json') @@ -447,3 +448,16 @@ class ContainersController(rest.RestController): compute_api = pecan.request.compute_api compute_api.container_resize(context, container, kw.get('h', None), kw.get('w', None)) + + @pecan.expose('json') + @exception.wrap_pecan_controller_exception + @validation.validate_query_param(pecan.request, schema.query_param_top) + def top(self, container_id, ps_args=None): + container = _get_container(container_id) + check_policy_on_container(container.as_dict(), "container:top") + utils.validate_container_state(container, 'top') + LOG.debug('Calling compute.container_top with %s' % + container.uuid) + context = pecan.request.context + compute_api = pecan.request.compute_api + return compute_api.container_top(context, container, ps_args) diff --git a/zun/api/controllers/v1/schemas/containers.py b/zun/api/controllers/v1/schemas/containers.py index 09991d328..e181e188f 100644 --- a/zun/api/controllers/v1/schemas/containers.py +++ b/zun/api/controllers/v1/schemas/containers.py @@ -89,6 +89,13 @@ query_param_logs = { 'additionalProperties': False } +query_param_top = { + 'type': 'object', + 'properties': { + 'ps_args': parameter_types.string_ps_args + }, + 'additionalProperties': False +} query_param_stop = copy.deepcopy(query_param_reboot) query_param_resize = { diff --git a/zun/common/utils.py b/zun/common/utils.py index 9add7f488..5966e0b7a 100644 --- a/zun/common/utils.py +++ b/zun/common/utils.py @@ -47,6 +47,7 @@ VALID_STATES = { 'update': ['Running', 'Stopped', 'Paused'], 'attach': ['Running'], 'resize': ['Running'], + 'top': ['Running'], } diff --git a/zun/common/validation/parameter_types.py b/zun/common/validation/parameter_types.py index b1d339d49..f39c02604 100644 --- a/zun/common/validation/parameter_types.py +++ b/zun/common/validation/parameter_types.py @@ -136,3 +136,8 @@ restart_policy = { "additionalProperties": False, "required": ['Name'] } + +string_ps_args = { + 'type': ['string'], + 'pattern': '[a-zA-Z- ,+]*' +} diff --git a/zun/compute/api.py b/zun/compute/api.py index b2a110b00..26cd115f7 100644 --- a/zun/compute/api.py +++ b/zun/compute/api.py @@ -93,6 +93,9 @@ class API(object): def container_resize(self, context, container, *args): return self.rpcapi.container_resize(context, container, *args) + def container_top(self, context, container, *args): + return self.rpcapi.container_top(context, container, *args) + def image_pull(self, context, image, *args): return self.rpcapi.image_pull(context, image, *args) diff --git a/zun/compute/manager.py b/zun/compute/manager.py index 0b4b939bd..a89be76f6 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -378,6 +378,20 @@ class Manager(object): raise return container + @translate_exception + def container_top(self, context, container, ps_args): + LOG.debug('Displaying the running processes inside the container: %s', + container.uuid) + try: + return self.driver.top(container, ps_args) + except exception.DockerError as e: + LOG.error(_LE("Error occurred while calling Docker top API: %s"), + six.text_type(e)) + raise + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s"), six.text_type(e)) + raise + def image_pull(self, context, image): utils.spawn_n(self._do_image_pull, context, image) diff --git a/zun/compute/rpcapi.py b/zun/compute/rpcapi.py index 6be16ba17..f22e81fb7 100644 --- a/zun/compute/rpcapi.py +++ b/zun/compute/rpcapi.py @@ -91,6 +91,10 @@ class API(rpc_service.API): return self._call(container.host, 'container_resize', container=container, height=height, width=width) + def container_top(self, context, container, ps_args): + return self._call(container.host, 'container_top', + container=container, ps_args=ps_args) + def image_pull(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 diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py index a17c0ac5d..37a1ad162 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -318,6 +318,17 @@ class DockerDriver(driver.ContainerDriver): docker.resize(container.container_id, height, width) return container + @check_container_id + def top(self, container, ps_args=None): + with docker_utils.docker_client() as docker: + try: + if ps_args is None or ps_args == 'None': + return docker.top(container.container_id) + else: + return docker.top(container.container_id, ps_args) + except errors.APIError: + raise + def _encode_utf8(self, value): if six.PY2 and not isinstance(value, unicode): value = unicode(value) diff --git a/zun/container/driver.py b/zun/container/driver.py index 46f77a134..b0e3c0d74 100644 --- a/zun/container/driver.py +++ b/zun/container/driver.py @@ -115,6 +115,10 @@ class ContainerDriver(object): """resize tty of a container.""" raise NotImplementedError() + def top(self, container, ps_args): + """display the running progresses inside the container.""" + raise NotImplementedError() + def create_sandbox(self, context, container, **kwargs): """Create a sandbox.""" raise NotImplementedError() diff --git a/zun/tests/unit/api/controllers/v1/test_containers.py b/zun/tests/unit/api/controllers/v1/test_containers.py index 55f100a1c..05ca85122 100644 --- a/zun/tests/unit/api/controllers/v1/test_containers.py +++ b/zun/tests/unit/api/controllers/v1/test_containers.py @@ -1270,6 +1270,55 @@ class TestContainerController(api_base.FunctionalTest): (container_uuid, 'resize'), body) self.assertTrue(mock_container_resize.called) + @patch('zun.common.utils.validate_container_state') + @patch('zun.compute.api.API.container_top') + @patch('zun.objects.Container.get_by_name') + def test_top_command_by_name(self, mock_get_by_name, + mock_container_top, mock_validate): + mock_container_top.return_value = "" + test_container = utils.get_test_container() + test_container_obj = objects.Container(self.context, **test_container) + mock_get_by_name.return_value = test_container_obj + + container_name = test_container.get('name') + response = self.app.get('/v1/containers/%s/top?ps_args=None' % + container_name) + self.assertEqual(200, response.status_int) + self.assertTrue(mock_container_top.called) + + @patch('zun.common.utils.validate_container_state') + @patch('zun.compute.api.API.container_top') + @patch('zun.objects.Container.get_by_uuid') + def test_top_command_by_uuid(self, mock_get_by_uuid, + mock_container_top, mock_validate): + mock_container_top.return_value = "" + test_container = utils.get_test_container() + test_container_obj = objects.Container(self.context, **test_container) + mock_get_by_uuid.return_value = test_container_obj + + container_uuid = test_container.get('uuid') + response = self.app.get('/v1/containers/%s/top?ps_args=aux' % + container_uuid) + self.assertEqual(200, response.status_int) + self.assertTrue(mock_container_top.called) + + @patch('zun.common.utils.validate_container_state') + @patch('zun.compute.api.API.container_top') + @patch('zun.objects.Container.get_by_uuid') + def test_top_command_invalid_ps(self, mock_get_by_uuid, + mock_container_top, mock_validate): + mock_container_top.return_value = "" + test_container = utils.get_test_container() + test_container_obj = objects.Container(self.context, **test_container) + mock_get_by_uuid.return_value = test_container_obj + mock_container_top.side_effect = Exception + + container_uuid = test_container.get('uuid') + self.assertRaises(AppError, self.app.get, + '/v1/containers/%s/top?ps_args=kkkk' % + container_uuid) + self.assertTrue(mock_container_top.called) + class TestContainerEnforcement(api_base.FunctionalTest):