diff --git a/devstack/lib/zun b/devstack/lib/zun index ea3e0cad7..773e5d066 100644 --- a/devstack/lib/zun +++ b/devstack/lib/zun @@ -101,6 +101,17 @@ function install_docker { sudo systemctl enable docker.service sudo systemctl start docker || true fi + + # We put the stack user as owner of the socket so we do not need to + # run the Docker commands with sudo when developing. + local docker_socket_file=/var/run/docker.sock + echo -n "Waiting for Docker to create its socket file" + while [ ! -e "$docker_socket_file" ]; do + echo -n "." + sleep 1 + done + echo "" + sudo chown "$STACK_USER":docker "$docker_socket_file" } # Test if any zun services are enabled diff --git a/zun/api/controllers/v1/containers.py b/zun/api/controllers/v1/containers.py index fdf66ce40..704035967 100644 --- a/zun/api/controllers/v1/containers.py +++ b/zun/api/controllers/v1/containers.py @@ -407,7 +407,7 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_start with %s', container.uuid) context = pecan.request.context - pecan.request.rpcapi.container_start(context, container) + container = pecan.request.rpcapi.container_start(context, container) return Container.convert_with_links(container) @pecan.expose('json') @@ -418,7 +418,8 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_stop with %s' % container.uuid) context = pecan.request.context - pecan.request.rpcapi.container_stop(context, container, timeout) + container = pecan.request.rpcapi.container_stop(context, container, + timeout) return Container.convert_with_links(container) @pecan.expose('json') @@ -429,7 +430,8 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_reboot with %s' % container.uuid) context = pecan.request.context - pecan.request.rpcapi.container_reboot(context, container, timeout) + container = pecan.request.rpcapi.container_reboot(context, container, + timeout) return Container.convert_with_links(container) @pecan.expose('json') @@ -440,7 +442,7 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_pause with %s' % container.uuid) context = pecan.request.context - pecan.request.rpcapi.container_pause(context, container) + container = pecan.request.rpcapi.container_pause(context, container) return Container.convert_with_links(container) @pecan.expose('json') @@ -451,7 +453,7 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_unpause with %s' % container.uuid) context = pecan.request.context - pecan.request.rpcapi.container_unpause(context, container) + container = pecan.request.rpcapi.container_unpause(context, container) return Container.convert_with_links(container) @pecan.expose('json') @@ -483,6 +485,6 @@ class ContainersController(rest.RestController): LOG.debug('Calling compute.container_kill with %s signal %s' % (container.uuid, kw.get('signal', kw.get('signal', None)))) context = pecan.request.context - pecan.request.rpcapi.container_kill(context, - container, kw.get('signal', None)) + container = pecan.request.rpcapi.container_kill(context, container, + kw.get('signal', None)) return Container.convert_with_links(container) diff --git a/zun/tests/contrib/post_test_hook.sh b/zun/tests/contrib/post_test_hook.sh index f1f7c2abb..790984033 100755 --- a/zun/tests/contrib/post_test_hook.sh +++ b/zun/tests/contrib/post_test_hook.sh @@ -57,7 +57,7 @@ sudo chown -R jenkins:stack $BASE/new/tempest # show tempest config cat etc/tempest.conf -sudo -H -u jenkins tox -eall-plugin -- zun.tests.tempest.api --concurrency=1 +sudo -E tox -eall-plugin -- zun.tests.tempest.api --concurrency=1 EXIT_CODE=$? diff --git a/zun/tests/tempest/api/clients.py b/zun/tests/tempest/api/clients.py index eb26de679..2b7c79e1f 100644 --- a/zun/tests/tempest/api/clients.py +++ b/zun/tests/tempest/api/clients.py @@ -16,6 +16,7 @@ from tempest.lib.common import rest_client from tempest.lib.services.compute import keypairs_client from tempest import manager +from zun.container.docker import utils as docker_utils from zun.tests.tempest.api.models import container_model from zun.tests.tempest.api.models import service_model from zun.tests.tempest import utils @@ -51,34 +52,46 @@ class ZunClient(rest_client.RestClient): return resp, model_type.from_json(body) @classmethod - def add_filters(cls, url, filters): - """add_filters adds dict values (filters) to url as query parameters - - :param url: base URL for the request - :param filters: dict with var:val pairs to add as parameters to URL - :returns: url string - """ - return url + "?" + parse(filters) - - @classmethod - def containers_uri(cls, filters=None): + def containers_uri(cls, action=None): url = "/containers/" - if filters: - url = cls.add_filters(url, filters) + if action: + url = "{0}/{1}".format(url, action) return url @classmethod - def container_uri(cls, container_id): + def container_uri(cls, container_id, action=None, params=None): """Construct container uri """ - return "{0}/{1}".format(cls.containers_uri(), container_id) + url = None + if action is None: + url = "{0}/{1}".format(cls.containers_uri(), container_id) + else: + url = "{0}/{1}/{2}".format(cls.containers_uri(), container_id, + action) + + if params: + url = cls.add_params(url, params) + + return url @classmethod - def services_uri(cls, filters=None): + def add_params(cls, url, params): + """add_params adds dict values (params) to url as query parameters + + :param url: base URL for the request + :param params: dict with var:val pairs to add as parameters to URL + :returns: url string + """ + url_parts = list(parse.urlparse(url)) + query = dict(parse.parse_qsl(url_parts[4])) + query.update(params) + url_parts[4] = parse.urlencode(query) + return parse.urlunparse(url_parts) + + @classmethod + def services_uri(cls): url = "/services/" - if filters: - url = cls.add_filters(url, filters) return url def post_container(self, model, **kwargs): @@ -90,20 +103,66 @@ class ZunClient(rest_client.RestClient): body=model.to_json(), **kwargs) return self.deserialize(resp, body, container_model.ContainerEntity) + def run_container(self, model, **kwargs): + resp, body = self.post( + self.containers_uri(action='run'), + body=model.to_json(), **kwargs) + return self.deserialize(resp, body, container_model.ContainerEntity) + def get_container(self, container_id): resp, body = self.get(self.container_uri(container_id)) return self.deserialize(resp, body, container_model.ContainerEntity) - def list_containers(self, filters=None, **kwargs): - resp, body = self.get(self.containers_uri(filters), **kwargs) + def list_containers(self, **kwargs): + resp, body = self.get(self.containers_uri(), **kwargs) return self.deserialize(resp, body, container_model.ContainerCollection) - def delete_container(self, container_id, **kwargs): - self.delete(self.container_uri(container_id), **kwargs) + def delete_container(self, container_id, params=None, **kwargs): + return self.delete( + self.container_uri(container_id, params=params), **kwargs) - def list_services(self, filters=None, **kwargs): - resp, body = self.get(self.services_uri(filters), **kwargs) + def start_container(self, container_id, **kwargs): + resp, body = self.post( + self.container_uri(container_id, action='start'), None, **kwargs) + return self.deserialize(resp, body, container_model.ContainerEntity) + + def stop_container(self, container_id, **kwargs): + resp, body = self.post( + self.container_uri(container_id, action='stop'), None, *kwargs) + return self.deserialize(resp, body, container_model.ContainerEntity) + + def pause_container(self, container_id, **kwargs): + resp, body = self.post( + self.container_uri(container_id, action='pause'), None, **kwargs) + return self.deserialize(resp, body, container_model.ContainerEntity) + + def unpause_container(self, container_id, **kwargs): + resp, body = self.post( + self.container_uri(container_id, action='unpause'), None, **kwargs) + return self.deserialize(resp, body, container_model.ContainerEntity) + + def kill_container(self, container_id, **kwargs): + resp, body = self.post( + self.container_uri(container_id, action='kill'), None, **kwargs) + return self.deserialize(resp, body, container_model.ContainerEntity) + + def reboot_container(self, container_id, **kwargs): + resp, body = self.post( + self.container_uri(container_id, action='reboot'), None, **kwargs) + return self.deserialize(resp, body, container_model.ContainerEntity) + + def exec_container(self, container_id, command, **kwargs): + return self.post( + self.container_uri(container_id, action='execute'), + '{"command": "%s"}' % command, **kwargs) + + def logs_container(self, container_id, **kwargs): + return self.get( + self.container_uri(container_id, action='logs'), None, **kwargs) + + def list_services(self, **kwargs): + resp, body = self.get(self.services_uri(), **kwargs) return self.deserialize(resp, body, service_model.ServiceCollection) @@ -116,3 +175,13 @@ class ZunClient(rest_client.RestClient): return True utils.wait_for_condition(container_created) + + +class DockerClient(object): + + def get_container(self, container_id): + with docker_utils.docker_client() as docker: + for info in docker.list_instances(inspect=True): + if container_id in info['Name']: + return info + return None diff --git a/zun/tests/tempest/api/test_containers.py b/zun/tests/tempest/api/test_containers.py index 4929c8114..6243bcc72 100644 --- a/zun/tests/tempest/api/test_containers.py +++ b/zun/tests/tempest/api/test_containers.py @@ -22,7 +22,6 @@ class TestContainer(base.BaseZunTest): @classmethod def get_client_manager(cls, credential_type=None, roles=None, force_new=None): - manager = super(TestContainer, cls).get_client_manager( credential_type=credential_type, roles=roles, @@ -32,45 +31,155 @@ class TestContainer(base.BaseZunTest): @classmethod def setup_clients(cls): - super(TestContainer, cls).setup_clients() cls.container_client = cls.os.container_client + cls.docker_client = clients.DockerClient() @classmethod def resource_setup(cls): - super(TestContainer, cls).resource_setup() def tearDown(self): _, model = self.container_client.list_containers() for c in model.containers: - self.container_client.ensure_container_created(c['uuid']) - self.container_client.delete_container(c['uuid']) + self.container_client.delete_container(c['uuid'], + params={'force': True}) super(TestContainer, self).tearDown() - def _create_container(self, **kwargs): - - model = datagen.container_data(**kwargs) - return self.container_client.post_container(model) - - def _delete_container(self, container_id, **kwargs): - - self.container_client.delete_container(container_id, **kwargs) - - @decorators.idempotent_id('a04f61f2-15ae-4200-83b7-1f311b101f35') - def test_container_create_list_delete(self): - - resp, container = self._create_container() - self.assertEqual(202, resp.status) + @decorators.idempotent_id('b8946b8c-57d5-4fdc-a09a-001d6b552725') + def test_create_container(self): + self._create_container() + @decorators.idempotent_id('b3e307d4-844b-4a57-8c60-8fb3f57aea7c') + def test_list_containers(self): + _, container = self._create_container() resp, model = self.container_client.list_containers() self.assertEqual(200, resp.status) self.assertGreater(len(model.containers), 0) - # NOTE(mkrai): Check and wait for container creation to get over - self.container_client.ensure_container_created(container.uuid) - self._delete_container(container.uuid, headers={'force': True}) + self.assertIn( + container.uuid, + list([c['uuid'] for c in model.containers])) - resp, model = self.container_client.list_containers() + @decorators.idempotent_id('0dd13c28-c5ff-4b9e-b73b-61185b410de4') + def test_get_container(self): + _, container = self._create_container() + resp, model = self.container_client.get_container(container.uuid) self.assertEqual(200, resp.status) - self.assertEqual(len(model.containers), 0) + self.assertEqual(container.uuid, model.uuid) + + @decorators.idempotent_id('cef53a56-22b7-4808-b01c-06b2b7126115') + def test_delete_container(self): + _, container = self._create_container() + self._delete_container(container.uuid) + + @decorators.idempotent_id('ef69c9e7-0ce0-4e14-b7ec-c1dc581a3927') + def test_run_container(self): + self._run_container() + + @decorators.idempotent_id('3fa024ef-aba1-48fe-9682-0d6b7854faa3') + def test_start_stop_container(self): + _, model = self._run_container() + + resp, model = self.container_client.stop_container(model.uuid) + self.assertEqual(200, resp.status) + self.assertEqual('Stopped', model.status) + self.assertEqual('Stopped', self._get_container_state(model.uuid)) + + resp, model = self.container_client.start_container(model.uuid) + self.assertEqual(200, resp.status) + self.assertEqual('Running', model.status) + self.assertEqual('Running', self._get_container_state(model.uuid)) + + @decorators.idempotent_id('b5f39756-8898-4e0e-a48b-dda0a06b66b6') + def test_pause_unpause_container(self): + _, model = self._run_container() + + resp, model = self.container_client.pause_container(model.uuid) + self.assertEqual(200, resp.status) + self.assertEqual('Paused', model.status) + self.assertEqual('Paused', self._get_container_state(model.uuid)) + + resp, model = self.container_client.unpause_container(model.uuid) + self.assertEqual(200, resp.status) + self.assertEqual('Running', model.status) + self.assertEqual('Running', self._get_container_state(model.uuid)) + + @decorators.idempotent_id('6179a588-3d48-4372-9599-f228411d1449') + def test_kill_container(self): + _, model = self._run_container() + + resp, model = self.container_client.kill_container(model.uuid) + self.assertEqual(200, resp.status) + self.assertEqual('Stopped', model.status) + self.assertEqual('Stopped', self._get_container_state(model.uuid)) + + @decorators.idempotent_id('c2e54321-0a70-4331-ba62-9dcaa75ac250') + def test_reboot_container(self): + _, model = self._run_container() + container = self.docker_client.get_container(model.uuid) + pid = container.get('State').get('Pid') + + resp, model = self.container_client.reboot_container(model.uuid) + self.assertEqual(200, resp.status) + self.assertEqual('Running', model.status) + self.assertEqual('Running', self._get_container_state(model.uuid)) + # assert pid is changed + container = self.docker_client.get_container(model.uuid) + self.assertNotEqual(pid, container.get('State').get('Pid')) + + @decorators.idempotent_id('8a591ff8-6793-427f-82a6-e3921d8b4f81') + def test_exec_container(self): + _, model = self._run_container() + resp, body = self.container_client.exec_container(model.uuid, + command='echo hello') + self.assertEqual(200, resp.status) + self.assertTrue('hello' in body) + + @decorators.idempotent_id('a912ca23-14e7-442f-ab15-e05aaa315204') + def test_logs_container(self): + _, model = self._run_container(command='echo hello') + resp, body = self.container_client.logs_container(model.uuid) + self.assertEqual(200, resp.status) + self.assertTrue('hello' in body) + + def _create_container(self, **kwargs): + gen_model = datagen.container_data(**kwargs) + resp, model = self.container_client.post_container(gen_model) + self.assertEqual(202, resp.status) + # Wait for container to finish creation + self.container_client.ensure_container_created(model.uuid) + + # Assert the container is created + resp, model = self.container_client.get_container(model.uuid) + self.assertEqual(200, resp.status) + self.assertEqual('Stopped', model.status) + self.assertEqual('Stopped', self._get_container_state(model.uuid)) + return resp, model + + def _run_container(self, **kwargs): + gen_model = datagen.container_data(**kwargs) + resp, model = self.container_client.run_container(gen_model) + self.assertEqual(200, resp.status) + self.assertTrue(model.status in ['Running', 'Stopped']) + self.assertTrue(self._get_container_state(model.uuid) in + ['Running', 'Stopped']) + return resp, model + + def _delete_container(self, container_id): + resp, _ = self.container_client.delete_container(container_id) + self.assertEqual(204, resp.status) + container = self.docker_client.get_container(container_id) + self.assertIsNone(container) + + def _get_container_state(self, container_id): + container = self.docker_client.get_container(container_id) + status = container.get('State') + if status.get('Error') is True: + return 'Error' + elif status.get('Paused'): + return 'Paused' + elif status.get('Running'): + return 'Running' + else: + return 'Stopped' diff --git a/zun/tests/unit/api/controllers/v1/test_containers.py b/zun/tests/unit/api/controllers/v1/test_containers.py index 8ca2f9cc8..6bde187cf 100644 --- a/zun/tests/unit/api/controllers/v1/test_containers.py +++ b/zun/tests/unit/api/controllers/v1/test_containers.py @@ -401,21 +401,27 @@ class TestContainerController(api_base.FunctionalTest): @patch('zun.compute.api.API.container_start') def test_start_by_uuid(self, mock_container_start): - mock_container_start.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_start.return_value = test_container_obj test_container = utils.get_test_container() self._action_test(test_container, 'start', 'uuid', mock_container_start) @patch('zun.compute.api.API.container_start') def test_start_by_name(self, mock_container_start): - mock_container_start.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_start.return_value = test_container_obj test_container = utils.get_test_container() self._action_test(test_container, 'start', 'name', mock_container_start) @patch('zun.compute.api.API.container_stop') def test_stop_by_uuid(self, mock_container_stop): - mock_container_stop.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_stop.return_value = test_container_obj test_container = utils.get_test_container() self._action_test(test_container, 'stop', 'uuid', mock_container_stop, @@ -423,7 +429,9 @@ class TestContainerController(api_base.FunctionalTest): @patch('zun.compute.api.API.container_stop') def test_stop_by_name(self, mock_container_stop): - mock_container_stop.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_stop.return_value = test_container_obj test_container = utils.get_test_container() self._action_test(test_container, 'stop', 'name', mock_container_stop, @@ -431,35 +439,45 @@ class TestContainerController(api_base.FunctionalTest): @patch('zun.compute.api.API.container_pause') def test_pause_by_uuid(self, mock_container_pause): - mock_container_pause.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_pause.return_value = test_container_obj test_container = utils.get_test_container() self._action_test(test_container, 'pause', 'uuid', mock_container_pause) @patch('zun.compute.api.API.container_pause') def test_pause_by_name(self, mock_container_pause): - mock_container_pause.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_pause.return_value = test_container_obj test_container = utils.get_test_container() self._action_test(test_container, 'pause', 'name', mock_container_pause) @patch('zun.compute.api.API.container_unpause') def test_unpause_by_uuid(self, mock_container_unpause): - mock_container_unpause.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_unpause.return_value = test_container_obj test_container = utils.get_test_container() self._action_test(test_container, 'unpause', 'uuid', mock_container_unpause) @patch('zun.compute.api.API.container_unpause') def test_unpause_by_name(self, mock_container_unpause): - mock_container_unpause.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_unpause.return_value = test_container_obj test_container = utils.get_test_container() self._action_test(test_container, 'unpause', 'name', mock_container_unpause) @patch('zun.compute.api.API.container_reboot') def test_reboot_by_uuid(self, mock_container_reboot): - mock_container_reboot.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_reboot.return_value = test_container_obj test_container = utils.get_test_container() self._action_test(test_container, 'reboot', 'uuid', mock_container_reboot, @@ -467,7 +485,9 @@ class TestContainerController(api_base.FunctionalTest): @patch('zun.compute.api.API.container_reboot') def test_reboot_by_name(self, mock_container_reboot): - mock_container_reboot.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_reboot.return_value = test_container_obj test_container = utils.get_test_container() self._action_test(test_container, 'reboot', 'name', mock_container_reboot, @@ -587,7 +607,9 @@ class TestContainerController(api_base.FunctionalTest): @patch('zun.objects.Container.get_by_uuid') def test_kill_container_by_uuid(self, mock_get_by_uuid, mock_container_kill): - mock_container_kill.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_kill.return_value = test_container_obj test_container = utils.get_test_container() test_container_obj = objects.Container(self.context, **test_container) mock_get_by_uuid.return_value = test_container_obj @@ -604,7 +626,9 @@ class TestContainerController(api_base.FunctionalTest): @patch('zun.objects.Container.get_by_name') def test_kill_container_by_name(self, mock_get_by_name, mock_container_kill): - mock_container_kill.return_value = "" + test_container_obj = objects.Container(self.context, + **utils.get_test_container()) + mock_container_kill.return_value = test_container_obj test_container = utils.get_test_container() test_container_obj = objects.Container(self.context, **test_container) mock_get_by_name.return_value = test_container_obj