diff --git a/etc/zun/policy.json b/etc/zun/policy.json index 460997f0f..d9c614e1a 100644 --- a/etc/zun/policy.json +++ b/etc/zun/policy.json @@ -23,6 +23,8 @@ "container:attach": "rule:admin_or_user", "container:resize": "rule:admin_or_user", "container:top": "rule:admin_or_user", + "container:get_archive": "rule:admin_or_user", + "container:put_archive": "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 70dcbb809..d503b3885 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -90,7 +90,9 @@ class ContainersController(rest.RestController): 'rename': ['POST'], 'attach': ['GET'], 'resize': ['POST'], - 'top': ['GET'] + 'top': ['GET'], + 'get_archive': ['GET'], + 'put_archive': ['POST'], } @pecan.expose('json') @@ -464,3 +466,29 @@ class ContainersController(rest.RestController): context = pecan.request.context compute_api = pecan.request.compute_api return compute_api.container_top(context, container, ps_args) + + @pecan.expose('json') + @exception.wrap_pecan_controller_exception + def get_archive(self, container_id, **kw): + container = _get_container(container_id) + check_policy_on_container(container.as_dict(), "container:get_archive") + utils.validate_container_state(container, 'get_archive') + LOG.debug('Calling compute.container_get_archive with %s path %s' + % (container.uuid, kw['path'])) + context = pecan.request.context + compute_api = pecan.request.compute_api + return compute_api.container_get_archive(context, + container, kw['path']) + + @pecan.expose('json') + @exception.wrap_pecan_controller_exception + def put_archive(self, container_id, **kw): + container = _get_container(container_id) + check_policy_on_container(container.as_dict(), "container:put_archive") + utils.validate_container_state(container, 'put_archive') + LOG.debug('Calling compute.container_put_archive with %s path %s' + % (container.uuid, kw['path'])) + context = pecan.request.context + compute_api = pecan.request.compute_api + compute_api.container_put_archive(context, container, + kw['path'], kw['data']) diff --git a/zun/common/utils.py b/zun/common/utils.py index b8697e934..9799d690a 100644 --- a/zun/common/utils.py +++ b/zun/common/utils.py @@ -49,6 +49,8 @@ VALID_STATES = { 'attach': ['Running'], 'resize': ['Running'], 'top': ['Running'], + 'get_archive': ['Running'], + 'put_archive': ['Running'], } diff --git a/zun/compute/api.py b/zun/compute/api.py index ecd6a0fb6..71dcc69ae 100644 --- a/zun/compute/api.py +++ b/zun/compute/api.py @@ -98,6 +98,12 @@ class API(object): def container_top(self, context, container, *args): return self.rpcapi.container_top(context, container, *args) + def container_get_archive(self, context, container, *args): + return self.rpcapi.container_get_archive(context, container, *args) + + def container_put_archive(self, context, container, *args): + return self.rpcapi.container_put_archive(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 aa393d1f9..7220b20a2 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -395,6 +395,34 @@ class Manager(object): LOG.exception(_LE("Unexpected exception: %s"), six.text_type(e)) raise + @translate_exception + def container_get_archive(self, context, container, path): + LOG.debug('Copy resource from the container: %s', container.uuid) + try: + return self.driver.get_archive(container, path) + except exception.DockerError as e: + LOG.error(_LE( + "Error occurred while calling Docker get_archive API: %s"), + six.text_type(e)) + raise + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s"), six.text_type(e)) + raise + + @translate_exception + def container_put_archive(self, context, container, path, data): + LOG.debug('Copy resource to the container: %s', container.uuid) + try: + return self.driver.put_archive(container, path, data) + except exception.DockerError as e: + LOG.error(_LE( + "Error occurred while calling Docker put_archive 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 7f1352342..0fc8b86e7 100644 --- a/zun/compute/rpcapi.py +++ b/zun/compute/rpcapi.py @@ -97,6 +97,14 @@ class API(rpc_service.API): return self._call(container.host, 'container_top', container=container, ps_args=ps_args) + def container_get_archive(self, context, container, path): + return self._call(container.host, 'container_get_archive', + container=container, path=path) + + def container_put_archive(self, context, container, path, data): + return self._call(container.host, 'container_put_archive', + container=container, path=path, data=data) + 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 6b908a50e..074731afc 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -350,6 +350,24 @@ class DockerDriver(driver.ContainerDriver): except errors.APIError: raise + @check_container_id + def get_archive(self, container, path): + with docker_utils.docker_client() as docker: + try: + return docker.get_archive(container.container_id, path) + except errors.APIError: + raise + + @check_container_id + def put_archive(self, container, path, data): + with docker_utils.docker_client() as docker: + try: + f = open(data, 'rb') + filedata = f.read() + docker.put_archive(container.container_id, path, filedata) + 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 5464d0547..d66ad7318 100644 --- a/zun/container/driver.py +++ b/zun/container/driver.py @@ -122,6 +122,14 @@ class ContainerDriver(object): """display the running processes inside the container.""" raise NotImplementedError() + def get_archive(self, container, path): + """copy resource froma container.""" + raise NotImplementedError() + + def put_archive(self, container, path, data): + """copy resource to a 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 89acb0df4..f5ab5391d 100644 --- a/zun/tests/unit/api/controllers/v1/test_containers.py +++ b/zun/tests/unit/api/controllers/v1/test_containers.py @@ -1349,6 +1349,88 @@ class TestContainerController(api_base.FunctionalTest): container_uuid) self.assertTrue(mock_container_top.called) + @patch('zun.common.utils.validate_container_state') + @patch('zun.compute.api.API.container_get_archive') + @patch('zun.objects.Container.get_by_uuid') + def test_get_archive_by_uuid(self, + mock_get_by_uuid, + container_get_archive, + mock_validate): + container_get_archive.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') + url = '/v1/containers/%s/%s/' % (container_uuid, 'get_archive') + cmd = {'path': '/home/1.txt'} + response = self.app.get(url, cmd) + self.assertEqual(200, response.status_int) + container_get_archive.assert_called_once_with( + mock.ANY, test_container_obj, cmd['path']) + + @patch('zun.common.utils.validate_container_state') + @patch('zun.compute.api.API.container_get_archive') + @patch('zun.objects.Container.get_by_name') + def test_get_archive_by_name(self, + mock_get_by_name, + container_get_archive, + mock_validate): + container_get_archive.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') + url = '/v1/containers/%s/%s/' % (container_name, 'get_archive') + cmd = {'path': '/home/1.txt'} + response = self.app.get(url, cmd) + self.assertEqual(200, response.status_int) + container_get_archive.assert_called_once_with( + mock.ANY, test_container_obj, cmd['path']) + + @patch('zun.common.utils.validate_container_state') + @patch('zun.compute.api.API.container_put_archive') + @patch('zun.objects.Container.get_by_uuid') + def test_put_archive_by_uuid(self, + mock_get_by_uuid, + container_put_archive, + mock_validate): + container_put_archive.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') + url = '/v1/containers/%s/%s/' % (container_uuid, 'put_archive') + cmd = {'path': '/home/', + 'data': '/home/1.tar'} + response = self.app.post(url, cmd) + self.assertEqual(200, response.status_int) + container_put_archive.assert_called_once_with( + mock.ANY, test_container_obj, cmd['path'], cmd['data']) + + @patch('zun.common.utils.validate_container_state') + @patch('zun.compute.api.API.container_put_archive') + @patch('zun.objects.Container.get_by_name') + def test_put_archive_by_name(self, + mock_get_by_name, + container_put_archive, + mock_validate): + container_put_archive.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') + url = '/v1/containers/%s/%s/' % (container_name, 'put_archive') + cmd = {'path': '/home/', + 'data': '/home/1.tar'} + response = self.app.post(url, cmd) + self.assertEqual(200, response.status_int) + container_put_archive.assert_called_once_with( + mock.ANY, test_container_obj, cmd['path'], cmd['data']) + class TestContainerEnforcement(api_base.FunctionalTest):