From c3425ce83faff1a2770bcebbe80886d058d9cfd5 Mon Sep 17 00:00:00 2001 From: Pradeep Kumar Singh Date: Fri, 11 Nov 2016 13:28:35 +0000 Subject: [PATCH] Add support for 'zun run' command at server side This patch adds support for 'zun run' command at server side, for implementing this 'run' method is implemented in container controller. REST api endpoint to access this method would be '/v1/containers/run'. Closes-Bug: #1627589 Change-Id: I2fe69961e3b7ea227dc0ce4ed257de7c680161da --- etc/zun/policy.json | 1 + zun/api/controllers/v1/containers.py | 32 +++++- zun/compute/api.py | 3 + zun/compute/manager.py | 89 ++++++++++----- .../api/controllers/v1/test_containers.py | 14 +++ .../unit/compute/test_compute_manager.py | 104 +++++++++++++++++- 6 files changed, 210 insertions(+), 33 deletions(-) diff --git a/etc/zun/policy.json b/etc/zun/policy.json index 8297a3e5e..dd00dffa9 100644 --- a/etc/zun/policy.json +++ b/etc/zun/policy.json @@ -18,6 +18,7 @@ "container:logs": "rule:admin_or_user", "container:execute": "rule:admin_or_user", "container:kill": "rule:admin_or_user", + "container:run": "rule:default", "image:create": "rule:default", "image:get_all": "rule:default", diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index cfe9fa297..feb869a48 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -217,7 +217,8 @@ class ContainersController(rest.RestController): 'unpause': ['POST'], 'logs': ['GET'], 'execute': ['POST'], - 'kill': ['POST'] + 'kill': ['POST'], + 'run': ['POST'] } @pecan.expose('json') @@ -314,6 +315,35 @@ class ContainersController(rest.RestController): pecan.response.status = 202 return Container.convert_with_links(new_container) + @pecan.expose('json') + @api_utils.enforce_content_types(['application/json']) + @exception.wrap_pecan_controller_exception + def run(self, **container_dict): + """Create and starts a new container. + + :param container: a container within the request body. + """ + context = pecan.request.context + policy.enforce(context, "container:run", + action="container:run") + container_dict = Container(**container_dict).as_dict() + container_dict['project_id'] = context.project_id + container_dict['user_id'] = context.user_id + name = container_dict.get('name') or \ + self._generate_name_for_container() + container_dict['name'] = name + container_dict['status'] = fields.ContainerStatus.CREATING + new_container = objects.Container(context, **container_dict) + new_container.create(context) + container = pecan.request.rpcapi.container_run(context, + new_container) + + # Set the HTTP Location Header + pecan.response.location = link.build_url('containers', + container.uuid) + pecan.response.status = 200 + return Container.convert_with_links(container) + @pecan.expose('json') @exception.wrap_pecan_controller_exception def patch(self, container_id, **kwargs): diff --git a/zun/compute/api.py b/zun/compute/api.py index 58d269ca6..cedfa3d81 100644 --- a/zun/compute/api.py +++ b/zun/compute/api.py @@ -37,6 +37,9 @@ class API(rpc_service.API): def container_create(self, context, container): return self._cast('container_create', container=container) + def container_run(self, context, container): + return self._call('container_run', container=container) + def container_delete(self, context, container, force): return self._call('container_delete', container=container, force=force) diff --git a/zun/compute/manager.py b/zun/compute/manager.py index 2b6cf9961..52f286659 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -15,6 +15,7 @@ import six from oslo_log import log as logging +from oslo_utils import excutils from oslo_utils import strutils from zun.common import exception @@ -62,7 +63,17 @@ class Manager(object): def container_create(self, context, container): utils.spawn_n(self._do_container_create, context, container) - def _do_container_create(self, context, container): + @translate_exception + def container_run(self, context, container): + return self._do_container_run(context, container) + + def _do_container_run(self, context, container): + created_container = self._do_container_create(context, + container, + reraise=True) + return self._do_container_start(context, created_container) + + def _do_container_create(self, context, container, reraise=False): LOG.debug('Creating container...', context=context, container=container) @@ -75,33 +86,64 @@ class Manager(object): image = image_driver.pull_image(context, repo, tag, image_pull_policy) except exception.ImageNotFound as e: - LOG.error(six.text_type(e)) - self._fail_container(container, six.text_type(e)) + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.error(six.text_type(e)) + self._fail_container(container, six.text_type(e)) return except exception.DockerError as e: - LOG.error(_LE("Error occured while calling docker image API: %s"), - six.text_type(e)) - self._fail_container(container, six.text_type(e)) + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.error(_LE( + "Error occured while calling docker image API: %s"), + six.text_type(e)) + self._fail_container(container, six.text_type(e)) return except Exception as e: - LOG.exception(_LE("Unexpected exception: %s"), six.text_type(e)) - self._fail_container(container, six.text_type(e)) + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.exception(_LE("Unexpected exception: %s"), + six.text_type(e)) + self._fail_container(container, six.text_type(e)) return container.task_state = fields.TaskState.CONTAINER_CREATING container.save() try: container = self.driver.create(container, image) - except exception.DockerError as e: - LOG.error(_LE("Error occured while calling docker create API: %s"), - six.text_type(e)) - self._fail_container(container, six.text_type(e)) - except Exception as e: - LOG.exception(_LE("Unexpected exception: %s"), six.text_type(e)) - self._fail_container(container, six.text_type(e)) - finally: container.task_state = None container.save() + return container + except exception.DockerError as e: + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.error(_LE( + "Error occured while calling docker create API: %s"), + six.text_type(e)) + self._fail_container(container, six.text_type(e)) + return + except Exception as e: + with excutils.save_and_reraise_exception(reraise=reraise): + LOG.exception(_LE("Unexpected exception: %s"), + six.text_type(e)) + self._fail_container(container, six.text_type(e)) + return + + def _do_container_start(self, context, container): + LOG.debug('Starting container...', context=context, + container=container.uuid) + try: + # Although we dont need this validation, but i still + # keep it for extra surity + self._validate_container_state(container, 'start') + container = self.driver.start(container) + container.save() + return container + except exception.DockerError as e: + LOG.error(_LE("Error occured while calling docker start API: %s"), + six.text_type(e)) + self._fail_container(container, six.text_type(e)) + raise + except Exception as e: + LOG.exception(_LE("Unexpected exception: %s"), str(e)) + self._fail_container(container, six.text_type(e)) + raise @translate_exception def container_delete(self, context, container, force): @@ -186,20 +228,7 @@ class Manager(object): @translate_exception def container_start(self, context, container): - LOG.debug('Starting container...', context=context, - container=container.uuid) - try: - self._validate_container_state(container, 'start') - container = self.driver.start(container) - container.save() - return container - except exception.DockerError as e: - LOG.error(_LE("Error occured while calling docker start API: %s"), - six.text_type(e)) - raise - except Exception as e: - LOG.exception(_LE("Unexpected exception: %s"), str(e)) - raise e + return self._do_container_start(context, container) @translate_exception def container_pause(self, context, container): diff --git a/zun/tests/unit/api/controllers/v1/test_containers.py b/zun/tests/unit/api/controllers/v1/test_containers.py index 8c8e3bc47..5976c0607 100644 --- a/zun/tests/unit/api/controllers/v1/test_containers.py +++ b/zun/tests/unit/api/controllers/v1/test_containers.py @@ -23,6 +23,20 @@ from zun.tests.unit.objects import utils as obj_utils class TestContainerController(api_base.FunctionalTest): + @patch('zun.compute.api.API.container_run') + def test_run_container(self, mock_container_run): + mock_container_run.side_effect = lambda x, y: y + + params = ('{"name": "MyDocker", "image": "ubuntu",' + '"command": "env", "memory": "512m",' + '"environment": {"key1": "val1", "key2": "val2"}}') + response = self.app.post('/v1/containers/run', + params=params, + content_type='application/json') + + self.assertEqual(200, response.status_int) + self.assertTrue(mock_container_run.called) + @patch('zun.compute.api.API.container_create') def test_create_container(self, mock_container_create): mock_container_create.side_effect = lambda x, y: y diff --git a/zun/tests/unit/compute/test_compute_manager.py b/zun/tests/unit/compute/test_compute_manager.py index ac744ce1b..8bc9b8500 100644 --- a/zun/tests/unit/compute/test_compute_manager.py +++ b/zun/tests/unit/compute/test_compute_manager.py @@ -115,6 +115,101 @@ class TestManager(base.TestCase): self.compute_manager._do_container_create(self.context, container) mock_fail.assert_called_once_with(container, "Creation Failed") + @mock.patch.object(Container, 'save') + @mock.patch('zun.image.driver.pull_image') + @mock.patch.object(fake_driver, 'create') + @mock.patch.object(fake_driver, 'start') + def test_container_run(self, mock_start, + mock_create, mock_pull, mock_save): + container = Container(self.context, **utils.get_test_container()) + mock_pull.return_value = 'fake_path' + mock_create.return_value = container + container.status = 'Stopped' + self.compute_manager.container_run(self.context, container) + mock_save.assert_called_with() + mock_pull.assert_called_once_with(self.context, + container.image, + 'latest', 'always') + mock_create.assert_called_once_with(container, 'fake_path') + mock_start.assert_called_once_with(container) + + @mock.patch.object(Container, 'save') + @mock.patch('zun.image.driver.pull_image') + @mock.patch.object(manager.Manager, '_fail_container') + def test_container_run_image_not_found(self, mock_fail, + mock_pull, mock_save): + container = Container(self.context, **utils.get_test_container()) + mock_pull.side_effect = exception.ImageNotFound( + message="Image Not Found") + with self.assertRaisesRegexp(exception.ImageNotFound, + 'Image Not Found'): + self.compute_manager._do_container_run(self.context, + container) + mock_save.assert_called_with() + mock_fail.assert_called_with(container, 'Image Not Found') + mock_pull.assert_called_once_with(self.context, + container.image, + 'latest', 'always') + + @mock.patch.object(Container, 'save') + @mock.patch('zun.image.driver.pull_image') + @mock.patch.object(manager.Manager, '_fail_container') + def test_container_run_image_pull_exception_raised(self, mock_fail, + mock_pull, mock_save): + container = Container(self.context, **utils.get_test_container()) + mock_pull.side_effect = exception.ZunException( + message="Image Not Found") + with self.assertRaisesRegexp(exception.ZunException, + 'Image Not Found'): + self.compute_manager._do_container_run(self.context, + container) + mock_save.assert_called_with() + mock_fail.assert_called_with(container, 'Image Not Found') + mock_pull.assert_called_once_with(self.context, + container.image, + 'latest', 'always') + + @mock.patch.object(Container, 'save') + @mock.patch('zun.image.driver.pull_image') + @mock.patch.object(manager.Manager, '_fail_container') + def test_container_run_image_pull_docker_error(self, mock_fail, + mock_pull, mock_save): + container = Container(self.context, **utils.get_test_container()) + mock_pull.side_effect = exception.DockerError( + message="Docker Error occurred") + with self.assertRaisesRegexp(exception.DockerError, + 'Docker Error occurred'): + self.compute_manager._do_container_run(self.context, + container) + mock_save.assert_called_with() + mock_fail.assert_called_with(container, 'Docker Error occurred') + mock_pull.assert_called_once_with(self.context, + container.image, + 'latest', 'always') + + @mock.patch.object(Container, 'save') + @mock.patch('zun.image.driver.pull_image') + @mock.patch.object(manager.Manager, '_fail_container') + @mock.patch.object(fake_driver, 'create') + def test_container_run_create_raises_docker_error(self, mock_create, + mock_fail, + mock_pull, mock_save): + container = Container(self.context, **utils.get_test_container()) + mock_pull.return_value = {'name': 'nginx', 'path': None} + mock_create.side_effect = exception.DockerError( + message="Docker Error occurred") + with self.assertRaisesRegexp(exception.DockerError, + 'Docker Error occurred'): + self.compute_manager._do_container_run(self.context, + container) + mock_save.assert_called_with() + mock_fail.assert_called_with(container, 'Docker Error occurred') + mock_pull.assert_called_once_with(self.context, + container.image, + 'latest', 'always') + mock_create.assert_called_once_with(container, + {'name': 'nginx', 'path': None}) + @mock.patch.object(manager.Manager, '_validate_container_state') @mock.patch.object(fake_driver, 'delete') def test_container_delete(self, mock_delete, mock_validate): @@ -221,21 +316,26 @@ class TestManager(base.TestCase): mock_start.assert_called_once_with(container) @mock.patch.object(manager.Manager, '_validate_container_state') - def test_container_start_invalid_state(self, mock_validate): + @mock.patch.object(manager.Manager, '_fail_container') + def test_container_start_invalid_state(self, mock_fail, mock_validate): container = Container(self.context, **utils.get_test_container()) mock_validate.side_effect = exception.InvalidStateException self.assertRaises(exception.InvalidStateException, self.compute_manager.container_start, self.context, container) + mock_fail.assert_called_once() @mock.patch.object(manager.Manager, '_validate_container_state') + @mock.patch.object(manager.Manager, '_fail_container') @mock.patch.object(fake_driver, 'start') - def test_container_start_failed(self, mock_start, mock_validate): + def test_container_start_failed(self, mock_start, + mock_fail, mock_validate): container = Container(self.context, **utils.get_test_container()) mock_start.side_effect = exception.DockerError self.assertRaises(exception.DockerError, self.compute_manager.container_start, self.context, container) + mock_fail.assert_called_once() @mock.patch.object(manager.Manager, '_validate_container_state') @mock.patch.object(fake_driver, 'pause')