From 9dcdbecbdd0b9f247e53d887e3c9712250b8174e Mon Sep 17 00:00:00 2001 From: Hongbin Lu Date: Sun, 12 May 2019 21:45:33 +0000 Subject: [PATCH] Introduce fullstack tests Change-Id: I4bd1851f608ee657a4f391961e4cff0fac9a1fe4 --- .zuul.yaml | 27 ++++ lower-constraints.txt | 1 + playbooks/fullstack/run.yaml | 5 + test-requirements.txt | 1 + tox.ini | 8 ++ zun/tests/fullstack/__init__.py | 0 zun/tests/fullstack/base.py | 77 ++++++++++++ zun/tests/fullstack/test_containers.py | 168 +++++++++++++++++++++++++ zun/tests/fullstack/utils.py | 78 ++++++++++++ 9 files changed, 365 insertions(+) create mode 100644 playbooks/fullstack/run.yaml create mode 100644 zun/tests/fullstack/__init__.py create mode 100644 zun/tests/fullstack/base.py create mode 100644 zun/tests/fullstack/test_containers.py create mode 100644 zun/tests/fullstack/utils.py diff --git a/.zuul.yaml b/.zuul.yaml index 047b2e63c..e710104dc 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -75,6 +75,31 @@ name: zun-tempest-multinode-docker-sql parent: zun-tempest-base-multinode +- job: + name: zun-fullstack + parent: devstack + required-projects: + - openstack/devstack + - openstack/devstack-plugin-container + - openstack/kuryr-libnetwork + - openstack/zun + - openstack/zun-tempest-plugin + - openstack/python-zunclient + run: playbooks/fullstack/run.yaml + irrelevant-files: + - ^.*\.rst$ + - ^doc/.*$ + - ^api-ref/.*$ + vars: + tox_envlist: fullstack + tox_install_siblings: false + devstack_localrc: + USE_PYTHON3: true + devstack_plugins: + zun: https://opendev.org/openstack/zun + kuryr-libnetwork: https://opendev.org/openstack/kuryr-libnetwork + devstack-plugin-container: https://opendev.org/openstack/devstack-plugin-container + - project: templates: - check-requirements @@ -90,6 +115,8 @@ - zun-tempest-docker-sql - zun-tempest-py3-docker-sql - zun-tempest-multinode-docker-sql + - zun-fullstack: + voting: false - kolla-ansible-ubuntu-source-zun: voting: false gate: diff --git a/lower-constraints.txt b/lower-constraints.txt index bf0c49bf0..e664f179c 100644 --- a/lower-constraints.txt +++ b/lower-constraints.txt @@ -121,6 +121,7 @@ python-keystoneclient==3.15.0 python-mimeparse==1.6.0 python-neutronclient==6.7.0 python-subunit==1.2.0 +python-zunclient==3.3.0 pytz==2018.3 PyYAML==3.12 reno==2.5.0 diff --git a/playbooks/fullstack/run.yaml b/playbooks/fullstack/run.yaml new file mode 100644 index 000000000..bebbce232 --- /dev/null +++ b/playbooks/fullstack/run.yaml @@ -0,0 +1,5 @@ +- hosts: all + roles: + - orchestrate-devstack + - ensure-tox + - tox diff --git a/test-requirements.txt b/test-requirements.txt index 152b46dc4..466b34556 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -19,3 +19,4 @@ testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT stestr>=1.0.0 # Apache-2.0 Pygments>=2.2.0 # BSD license +python-zunclient>=3.3.0 # Apache-2.0 diff --git a/tox.ini b/tox.ini index 632b90ea2..54009b388 100644 --- a/tox.ini +++ b/tox.ini @@ -144,3 +144,11 @@ deps = # separately, outside of the requirements files. deps = bindep commands = bindep test + +[testenv:fullstack] +basepython = python3 +setenv = {[testenv]setenv} +deps = {[testenv]deps} +commands = + stestr --test-path=./zun/tests/fullstack run {posargs} + stestr slowest diff --git a/zun/tests/fullstack/__init__.py b/zun/tests/fullstack/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/zun/tests/fullstack/base.py b/zun/tests/fullstack/base.py new file mode 100644 index 000000000..62494acec --- /dev/null +++ b/zun/tests/fullstack/base.py @@ -0,0 +1,77 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import docker +from oslo_log import log as logging + +import zun.conf +from zun.tests import base +from zun.tests.fullstack import utils + + +CONF = zun.conf.CONF +LOG = logging.getLogger(__name__) + + +class BaseFullStackTestCase(base.TestCase): + + def setUp(self): + super(BaseFullStackTestCase, self).setUp() + + self.docker = docker.APIClient(base_url='tcp://0.0.0.0:2375') + try: + self.zun = utils.get_zun_client_from_env() + except Exception as e: + # We may missing or didn't source configured openrc file. + message = ("Missing environment variable %s in your local." + "Please add it and also check other missing " + "environment variables. After that please source " + "the openrc file. " + "Trying credentials from DevStack cloud.yaml ...") + LOG.warning(message, e.args[0]) + self.zun = utils.get_zun_client_from_creds() + + def ensure_container_deleted(self, container_id): + def is_container_deleted(): + containers = self.zun.containers.list() + container_ids = [c.uuid for c in containers] + if container_id in container_ids: + return False + else: + return True + utils.wait_for_condition(is_container_deleted) + + def ensure_container_in_desired_state(self, container_id, status): + def is_container_in_desired_state(): + c = self.zun.containers.get(container_id) + if c.status == status: + return True + else: + return False + utils.wait_for_condition(is_container_in_desired_state, timeout=300) + + def _get_container_in_docker(self, container): + return self.docker.inspect_container('zun-' + container.uuid) + + def _get_state_in_docker(self, container): + container = self.docker.inspect_container('zun-' + container.uuid) + status = container.get('State') + if status.get('Error') is True: + return 'Error' + elif status.get('Paused'): + return 'Paused' + elif status.get('Running'): + return 'Running' + elif status.get('Status') == 'created': + return 'Created' + else: + return 'Stopped' diff --git a/zun/tests/fullstack/test_containers.py b/zun/tests/fullstack/test_containers.py new file mode 100644 index 000000000..239de45d9 --- /dev/null +++ b/zun/tests/fullstack/test_containers.py @@ -0,0 +1,168 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import docker +from tempest.lib import decorators + +from zun.tests.fullstack import base +from zun.tests.fullstack import utils + + +class TestContainer(base.BaseFullStackTestCase): + + def setUp(self): + super(TestContainer, self).setUp() + self.containers = [] + + def tearDown(self): + containers = self.zun.containers.list() + for c in containers: + if c.uuid in self.containers: + self.zun.containers.delete(c.uuid, stop=True) + self.ensure_container_deleted(c.uuid) + + super(TestContainer, self).tearDown() + + @decorators.idempotent_id('039fc590-7711-4b87-86bf-fe9048c3feb9') + def test_run_container(self): + self._run_container() + + def _run_container(self, **kwargs): + if not kwargs.get('image'): + kwargs['image'] = 'cirros:latest' + kwargs['command'] = ['sleep', '100000'] + + kwargs.setdefault('cpu', 0.1) + kwargs.setdefault('memory', 128) + + container = self.zun.containers.run(**kwargs) + self.containers.append(container.uuid) + # Wait for container to started + self.ensure_container_in_desired_state(container.uuid, 'Running') + + # Assert the container is started + container = self.zun.containers.get(container.uuid) + self.assertEqual('Running', container.status) + self.assertEqual('Running', self._get_state_in_docker(container)) + return container + + def _create_container(self, **kwargs): + if not kwargs.get('image'): + kwargs['image'] = 'cirros:latest' + kwargs['command'] = ['sleep', '100000'] + + kwargs.setdefault('cpu', 0.1) + kwargs.setdefault('memory', 128) + + container = self.zun.containers.create(**kwargs) + self.containers.append(container.uuid) + # Wait for container to finish creation + self.ensure_container_in_desired_state(container.uuid, 'Created') + + # Assert the container is created + container = self.zun.containers.get(container.uuid) + self.assertEqual('Created', container.status) + self.assertEqual('Created', self._get_state_in_docker(container)) + return container + + @decorators.idempotent_id('8c6f0844-1a5c-4bf4-81d5-38dccb2c2b25') + def test_delete_container(self): + container = self._create_container() + self.zun.containers.delete(container.uuid) + self.ensure_container_deleted(container.uuid) + self.assertRaises(docker.errors.NotFound, + self._get_container_in_docker, container) + + @decorators.idempotent_id('6f7a4d0f-273a-4321-ba14-246c6ea387a1') + def test_run_container_with_environment(self): + container = self._run_container( + environment={'key1': 'env1', 'key2': 'env2'}) + + docker_container = self._get_container_in_docker(container) + env = docker_container['Config']['Env'] + self.assertTrue('key1=env1' in env) + self.assertTrue('key2=env2' in env) + + @decorators.idempotent_id('25e19899-d450-4d6b-9dbd-160f9c557877') + def test_run_container_with_labels(self): + container = self._run_container( + labels={'key1': 'label1', 'key2': 'label2'}) + + docker_container = self._get_container_in_docker(container) + labels = docker_container['Config']['Labels'] + self.assertEqual({'key1': 'label1', 'key2': 'label2'}, labels) + + @decorators.idempotent_id('8a920b08-32df-448e-ab53-b640611ac769') + def test_run_container_with_restart_policy(self): + container = self._run_container(restart_policy={ + 'Name': 'on-failure', 'MaximumRetryCount': 2}) + + docker_container = self._get_container_in_docker(container) + policy = docker_container['HostConfig']['RestartPolicy'] + self.assertEqual('on-failure', policy['Name']) + self.assertEqual(2, policy['MaximumRetryCount']) + + @decorators.idempotent_id('6b3229af-c8c8-4a11-8b22-981e2ff63b51') + def test_run_container_with_interactive(self): + container = self._run_container(interactive=True) + + docker_container = self._get_container_in_docker(container) + tty = docker_container['Config']['Tty'] + stdin_open = docker_container['Config']['OpenStdin'] + self.assertIs(True, tty) + self.assertIs(True, stdin_open) + + @decorators.idempotent_id('f189624c-c9b8-4181-9485-2b5cacb633bc') + def test_reboot_container(self): + container = self._run_container() + docker_container = self._get_container_in_docker(container) + pid = docker_container['State']['Pid'] + + self.zun.containers.restart(container.uuid, timeout=10) + self._ensure_container_pid_changed(container, pid) + self.assertEqual('Running', self._get_state_in_docker(container)) + # assert pid is changed + docker_container = self._get_container_in_docker(container) + self.assertNotEqual(pid, docker_container['State']['Pid']) + + def _ensure_container_pid_changed(self, container, pid): + def is_pid_changed(): + docker_container = self._get_container_in_docker(container) + new_pid = docker_container['State']['Pid'] + if pid != new_pid: + return True + else: + return False + utils.wait_for_condition(is_pid_changed) + + @decorators.idempotent_id('44e28cdc-6b33-4394-b7cb-3b2f36a2839a') + def test_update_container(self): + container = self._run_container(cpu=0.1, memory=100) + self.assertEqual('100', container.memory) + self.assertEqual(0.1, container.cpu) + docker_container = self._get_container_in_docker(container) + self._assert_resource_constraints(docker_container, cpu=0.1, + memory=100) + + container = self.zun.containers.update(container.uuid, cpu=0.2, + memory=200) + self.assertEqual('200', container.memory) + self.assertEqual(0.2, container.cpu) + docker_container = self._get_container_in_docker(container) + self._assert_resource_constraints(docker_container, cpu=0.2, + memory=200) + + def _assert_resource_constraints(self, docker_container, cpu, memory): + cpu_shares = docker_container['HostConfig']['CpuShares'] + self.assertEqual(int(cpu * 1024), cpu_shares) + docker_memory = docker_container['HostConfig']['Memory'] + self.assertEqual(memory * 1024 * 1024, docker_memory) diff --git a/zun/tests/fullstack/utils.py b/zun/tests/fullstack/utils.py new file mode 100644 index 000000000..4ce049e94 --- /dev/null +++ b/zun/tests/fullstack/utils.py @@ -0,0 +1,78 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import os +import time + +from keystoneauth1 import identity +from keystoneauth1 import session as ks +import os_client_config +from zunclient import client + + +def get_zun_client_from_env(): + # We should catch KeyError exception with the purpose of + # source or configure openrc file. + auth_url = os.environ['OS_AUTH_URL'] + username = os.environ['OS_USERNAME'] + password = os.environ['OS_PASSWORD'] + project_name = os.environ['OS_PROJECT_NAME'] + + # Either project(user)_domain_name or project(user)_domain_id + # would be acceptable. + project_domain_name = os.environ.get("OS_PROJECT_DOMAIN_NAME") + project_domain_id = os.environ.get("OS_PROJECT_DOMAIN_ID") + user_domain_name = os.environ.get("OS_USER_DOMAIN_NAME") + user_domain_id = os.environ.get("OS_USER_DOMAIN_ID") + + auth = identity.Password(auth_url=auth_url, + username=username, + password=password, + project_name=project_name, + project_domain_id=project_domain_id, + project_domain_name=project_domain_name, + user_domain_id=user_domain_id, + user_domain_name=user_domain_name) + session = ks.Session(auth=auth) + return client.Client('1.latest', session=session) + + +def _get_cloud_config_auth_data(cloud='devstack-admin'): + """Retrieves Keystone auth data to run tests + + Credentials are either read via os-client-config from the environment + or from a config file ('clouds.yaml'). Environment variables override + those from the config file. + devstack produces a clouds.yaml with two named clouds - one named + 'devstack' which has user privs and one named 'devstack-admin' which + has admin privs. This function will default to getting the devstack-admin + cloud as that is the current expected behavior. + """ + cloud_config = os_client_config.OpenStackConfig().get_one_cloud(cloud) + return cloud_config.get_auth(), cloud_config.get_session() + + +def get_zun_client_from_creds(): + auth_plugin, session = _get_cloud_config_auth_data() + return client.Client('1.latest', session=session, auth=auth_plugin) + + +def wait_for_condition(condition, interval=2, timeout=60): + start_time = time.time() + end_time = time.time() + timeout + while time.time() < end_time: + result = condition() + if result: + return result + time.sleep(interval) + raise Exception(("Timed out after %s seconds. Started on %s and ended " + "on %s") % (timeout, start_time, end_time))