From 605a0d3a51be5aa6ed21c4095e664c33e95a49ce Mon Sep 17 00:00:00 2001 From: Lingxian Kong Date: Thu, 5 Sep 2019 00:34:39 +1200 Subject: [PATCH] Refactor trove-tempest-plugin Depends-On: https://review.opendev.org/#/c/697870/ Story: 2006554 Task: 36639 Change-Id: I6251f070f330ee886e6436d92c20d78e0401d59e --- .zuul.yaml | 52 +++- doc/requirements.txt | 6 +- requirements.txt | 10 +- test-requirements.txt | 8 +- trove_tempest_plugin/clients.py | 34 --- trove_tempest_plugin/config.py | 63 +++-- trove_tempest_plugin/plugin.py | 8 +- trove_tempest_plugin/services/client.py | 72 +++++ .../services/database/__init__.py | 26 -- .../services/database/base_client.py | 47 ---- .../services/database/flavors_client.py | 28 -- .../services/database/limits_client.py | 25 -- .../services/database/versions_client.py | 30 --- .../tests/api/test_flavors.py | 88 ------ .../tests/api/test_flavors_negative.py | 36 --- trove_tempest_plugin/tests/api/test_limits.py | 47 ---- .../tests/api/test_versions.py | 41 --- trove_tempest_plugin/tests/base.py | 252 ++++++++++++++++++ trove_tempest_plugin/tests/base_test.py | 86 ------ .../tests/scenario/__init__.py | 0 .../tests/scenario/test_instance_basic.py | 29 ++ trove_tempest_plugin/tests/utils.py | 51 ++++ 22 files changed, 511 insertions(+), 528 deletions(-) delete mode 100644 trove_tempest_plugin/clients.py create mode 100644 trove_tempest_plugin/services/client.py delete mode 100644 trove_tempest_plugin/services/database/__init__.py delete mode 100644 trove_tempest_plugin/services/database/base_client.py delete mode 100644 trove_tempest_plugin/services/database/flavors_client.py delete mode 100644 trove_tempest_plugin/services/database/limits_client.py delete mode 100644 trove_tempest_plugin/services/database/versions_client.py delete mode 100644 trove_tempest_plugin/tests/api/test_flavors.py delete mode 100644 trove_tempest_plugin/tests/api/test_flavors_negative.py delete mode 100644 trove_tempest_plugin/tests/api/test_limits.py delete mode 100644 trove_tempest_plugin/tests/api/test_versions.py create mode 100644 trove_tempest_plugin/tests/base.py delete mode 100644 trove_tempest_plugin/tests/base_test.py create mode 100644 trove_tempest_plugin/tests/scenario/__init__.py create mode 100644 trove_tempest_plugin/tests/scenario/test_instance_basic.py create mode 100644 trove_tempest_plugin/tests/utils.py diff --git a/.zuul.yaml b/.zuul.yaml index 98a8473..688648a 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -5,32 +5,66 @@ - tempest-plugin-jobs check: jobs: - - python-troveclient-tempest-neutron-src - - trove-tempest-plugin - - trove-tempest-ipv6-only + - trove-tempest-plugin: + voting: false + - trove-tempest-ipv6-only: + voting: false gate: queue: trove jobs: - - python-troveclient-tempest-neutron-src - - trove-tempest-plugin - - trove-tempest-ipv6-only + - trove-tempest-plugin: + voting: false + - trove-tempest-ipv6-only: + voting: false + - job: name: trove-tempest-plugin parent: devstack-tempest + timeout: 7800 required-projects: &base_required_projects - - openstack/neutron + - openstack/python-troveclient - openstack/trove - openstack/trove-tempest-plugin - openstack/tempest + irrelevant-files: + - ^.*\.rst$ + - ^doc/.*$ + - ^etc/.*$ + - ^releasenotes/.*$ vars: &base_vars tox_envlist: all + tempest_concurrency: 2 devstack_localrc: TEMPEST_PLUGINS: /opt/stack/trove-tempest-plugin + USE_PYTHON3: true + devstack_local_conf: + post-config: + $TROVE_CONF: + DEFAULT: + usage_timeout: 1800 devstack_plugins: - trove: https://opendev.org/openstack/trove + trove: https://opendev.org/openstack/trove.git devstack_services: - tempest: true + etcd3: false tls-proxy: false + ceilometer-acentral: false + ceilometer-acompute: false + ceilometer-alarm-evaluator: false + ceilometer-alarm-notifier: false + ceilometer-anotification: false + ceilometer-api: false + ceilometer-collector: false + cinder: true + c-sch: true + c-api: true + c-vol: true + c-bak: false + swift: true + s-account: true + s-container: true + s-object: true + s-proxy: true + tempest: true tempest_test_regex: ^trove_tempest_plugin\.tests - job: diff --git a/doc/requirements.txt b/doc/requirements.txt index 244b6b6..ddf8411 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -2,6 +2,10 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD +sphinxcontrib-apidoc>=0.2.0 # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.2,<2.0.0;python_version=='2.7' # BSD +sphinx!=1.6.6,!=1.6.7,>=1.6.2,!=2.1.0;python_version>='3.4' # BSD openstackdocstheme>=1.18.1 # Apache-2.0 + +# releasenotes reno>=2.5.0 # Apache-2.0 diff --git a/requirements.txt b/requirements.txt index 5346b78..ea1c2fc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,12 @@ # process, which may cause wedges in the gate later. pbr!=2.1.0,>=2.0.0 # Apache-2.0 -six>=1.10.0 # MIT oslo.config>=5.2.0 # Apache-2.0 -oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 -testtools>=2.2.0 # MIT +oslo.log>=3.44.1 # Apache-2.0 +oslo.serialization>=2.29.1 # Apache-2.0 +oslo.service>=1.40.1 # Apache-2.0 +oslotest>=3.2.0 # Apache-2.0 +requests>=2.14.2 # Apache-2.0 +six>=1.10.0 # MIT tempest>=17.1.0 # Apache-2.0 +tenacity>=5.1.1 # Apache-2.0 diff --git a/test-requirements.txt b/test-requirements.txt index e4c2478..47c128f 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -2,5 +2,11 @@ # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. -hacking<0.13,>=0.12.0 # Apache-2.0 +hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 +coverage!=4.4,>=4.0 # Apache-2.0 +python-subunit>=1.0.0 # Apache-2.0/BSD +oslotest>=3.2.0 # Apache-2.0 +stestr>=2.0.0 # Apache-2.0 +testscenarios>=0.4 # Apache-2.0/BSD +testtools>=2.2.0 # MIT diff --git a/trove_tempest_plugin/clients.py b/trove_tempest_plugin/clients.py deleted file mode 100644 index ac99c31..0000000 --- a/trove_tempest_plugin/clients.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2018 Samsung Electronics -# All Rights Reserved. -# -# 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. - -from tempest import config -from tempest.lib.services import clients - -CONF = config.CONF - - -class Manager(clients.ServiceClients): - """Service clients proxy. - - Enhances tests with a convenient way to access available service clients - configured for a specified set of credentials. - """ - - def __init__(self, credentials, service=None): - if CONF.identity.auth_version == 'v2': - identity_uri = CONF.identity.uri - else: - identity_uri = CONF.identity.uri_v3 - super(Manager, self).__init__(credentials, identity_uri) diff --git a/trove_tempest_plugin/config.py b/trove_tempest_plugin/config.py index 691d5c4..69a5212 100644 --- a/trove_tempest_plugin/config.py +++ b/trove_tempest_plugin/config.py @@ -14,27 +14,50 @@ from oslo_config import cfg -service_option = cfg.BoolOpt('trove', - default=True, - help="Whether or not Trove is expected to be " - "available") +service_option = cfg.BoolOpt( + 'trove', + default=True, + help="Whether or not Trove is expected to be available" +) -database_group = cfg.OptGroup(name='database', - title='Database Service Options') +database_group = cfg.OptGroup( + name='database', + title='Database Service Options' +) DatabaseGroup = [ - cfg.StrOpt('catalog_type', - default='database', - help="Catalog type of the Database service."), - cfg.StrOpt('endpoint_type', - default='publicURL', - choices=['public', 'admin', 'internal', - 'publicURL', 'adminURL', 'internalURL'], - help="The endpoint type to use for the Database service."), - cfg.StrOpt('db_flavor_ref', - default="1", - help="Valid primary flavor to use in Database tests."), - cfg.StrOpt('db_current_version', - default="v1.0", - help="Current database version to use in Database tests."), + cfg.StrOpt( + 'catalog_type', + default='database', + help="Catalog type of the Database service." + ), + cfg.StrOpt( + 'endpoint_type', + default='publicURL', + choices=['public', 'admin', 'internal', 'publicURL', 'adminURL', + 'internalURL'], + help="The endpoint type to use for the Database service." + ), + cfg.IntOpt('database_build_timeout', + default=1800, + help='Timeout in seconds to wait for a database instance to ' + 'build.'), + cfg.StrOpt( + 'flavor_id', + default="d2", + help="The Nova flavor ID used for creating database instance." + ), + cfg.StrOpt( + 'subnet_cidr', + default='10.1.1.0/24', + help=('The Neutron CIDR format subnet to use for database network ' + 'creation.') + ), + cfg.StrOpt( + 'volume_type', + default="lvmdriver-1", + help="The Cinder volume type used for creating database instance." + ), + cfg.StrOpt('datastore_type', default="mysql"), + cfg.StrOpt('datastore_version', default="5.7"), ] diff --git a/trove_tempest_plugin/plugin.py b/trove_tempest_plugin/plugin.py index 8d8edcd..231be86 100644 --- a/trove_tempest_plugin/plugin.py +++ b/trove_tempest_plugin/plugin.py @@ -45,12 +45,8 @@ class TroveTempestPlugin(plugins.TempestPlugin): service_params = { 'name': 'database', 'service_version': 'database', - 'module_path': 'trove_tempest_plugin.services.database', - 'client_names': [ - 'FlavorsClient', - 'LimitsClient', - 'VersionsClient' - ] + 'module_path': 'trove_tempest_plugin.services.client', + 'client_names': ['TroveClient'] } service_params.update(service_config) return [service_params] diff --git a/trove_tempest_plugin/services/client.py b/trove_tempest_plugin/services/client.py new file mode 100644 index 0000000..a748c33 --- /dev/null +++ b/trove_tempest_plugin/services/client.py @@ -0,0 +1,72 @@ +# Copyright 2018 Samsung Electronics +# All Rights Reserved. +# +# 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. + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urlparse + +from tempest.lib.common import rest_client +from tempest.lib import exceptions + + +class TroveClient(rest_client.RestClient): + def __init__(self, auth_provider, **kwargs): + super(TroveClient, self).__init__(auth_provider, **kwargs) + + def get_resource(self, obj, id, expected_status_code=200): + url = '/%s/%s' % (obj, id) + resp, body = self.get(url) + self.expected_success(expected_status_code, resp.status) + + return rest_client.ResponseBody(resp, json.loads(body)) + + def list_resources(self, obj, expected_status_code=200, **filters): + url = '/%s' % obj + if filters: + # Encode provided dict of fields into a series of key=value pairs + # separated by '&' characters. + # + # The field value can be a sequence. Setting option doseq to True + # enforces producing individual key-value pair for each element of + # the sequence under the same key. + # + # e.g. {'foo': 'bar', 'baz': ['test1', 'test2']} + # => foo=bar&baz=test1&baz=test2 + url += '?' + urlparse.urlencode(filters, doseq=True) + + resp, body = self.get(url) + self.expected_success(expected_status_code, resp.status) + + return rest_client.ResponseBody(resp, json.loads(body)) + + def delete_resource(self, obj, id, ignore_notfound=False): + try: + resp, _ = self.delete('/{obj}/{id}'.format(obj=obj, id=id)) + return resp + except exceptions.NotFound: + if ignore_notfound: + pass + else: + raise + + def create_resource(self, obj, req_body, extra_headers={}, + expected_status_code=200): + headers = {"Content-Type": "application/json"} + headers = dict(headers, **extra_headers) + url = '/%s' % obj + + resp, body = self.post(url, json.dumps(req_body), headers=headers) + self.expected_success(expected_status_code, resp.status) + + return rest_client.ResponseBody(resp, json.loads(body)) diff --git a/trove_tempest_plugin/services/database/__init__.py b/trove_tempest_plugin/services/database/__init__.py deleted file mode 100644 index 758cd40..0000000 --- a/trove_tempest_plugin/services/database/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2018 Samsung Electronics -# All Rights Reserved. -# -# 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. - -from trove_tempest_plugin.services.database.flavors_client import FlavorsClient -from trove_tempest_plugin.services.database.limits_client import LimitsClient -from trove_tempest_plugin.services.database.versions_client import ( - VersionsClient) - - -__all__ = [ - 'FlavorsClient', - 'LimitsClient', - 'VersionsClient' -] diff --git a/trove_tempest_plugin/services/database/base_client.py b/trove_tempest_plugin/services/database/base_client.py deleted file mode 100644 index 30b43be..0000000 --- a/trove_tempest_plugin/services/database/base_client.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2018 Samsung Electronics -# All Rights Reserved. -# -# 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. - -from oslo_serialization import jsonutils as json -from six.moves.urllib import parse as urllib - -from tempest.lib.common import rest_client - - -class BaseClient(rest_client.RestClient): - - def show_resource(self, uri, expected_status_code=200, **fields): - if fields: - # Encode provided dict of fields into a series of key=value pairs - # separated by '&' characters. - # - # The field value can be a sequence. Setting option doseq to True - # enforces producing individual key-value pair for each element of - # the sequence under the same key. - # - # e.g. {'foo': 'bar', 'baz': ['test1', 'test2']} - # => foo=bar&baz=test1&baz=test2 - uri += '?' + urllib.urlencode(fields, doseq=True) - resp, body = self.get(uri) - self.expected_success(expected_status_code, resp.status) - body = json.loads(body) - return rest_client.ResponseBody(resp, body) - - def list_resources(self, uri, expected_status_code=200, **filters): - if filters: - uri += '?' + urllib.urlencode(filters, doseq=True) - resp, body = self.get(uri) - self.expected_success(expected_status_code, resp.status) - body = json.loads(body) - return rest_client.ResponseBody(resp, body) diff --git a/trove_tempest_plugin/services/database/flavors_client.py b/trove_tempest_plugin/services/database/flavors_client.py deleted file mode 100644 index 78fab04..0000000 --- a/trove_tempest_plugin/services/database/flavors_client.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -from trove_tempest_plugin.services.database import base_client - - -class FlavorsClient(base_client.BaseClient): - - uri = '/flavors' - - def list_flavors(self): - return self.list_resources(self.uri) - - def show_flavor(self, flavor_id): - uri = '%s/%s' % (self.uri, flavor_id) - return self.show_resource(uri) diff --git a/trove_tempest_plugin/services/database/limits_client.py b/trove_tempest_plugin/services/database/limits_client.py deleted file mode 100644 index e37b356..0000000 --- a/trove_tempest_plugin/services/database/limits_client.py +++ /dev/null @@ -1,25 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -from trove_tempest_plugin.services.database import base_client - - -class LimitsClient(base_client.BaseClient): - - uri = '/limits' - - def list_limits(self): - """List all limits.""" - return self.list_resources(self.uri) diff --git a/trove_tempest_plugin/services/database/versions_client.py b/trove_tempest_plugin/services/database/versions_client.py deleted file mode 100644 index 2367b4a..0000000 --- a/trove_tempest_plugin/services/database/versions_client.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -from trove_tempest_plugin.services.database import base_client - - -class VersionsClient(base_client.BaseClient): - - uri = '' - - def __init__(self, auth_provider, service, region, **kwargs): - super(VersionsClient, self).__init__( - auth_provider, service, region, **kwargs) - self.skip_path() - - def list_versions(self): - """List all versions.""" - return self.list_resources(self.uri) diff --git a/trove_tempest_plugin/tests/api/test_flavors.py b/trove_tempest_plugin/tests/api/test_flavors.py deleted file mode 100644 index 17a3106..0000000 --- a/trove_tempest_plugin/tests/api/test_flavors.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -from tempest.common import utils -from tempest.lib import decorators -from testtools import testcase as testtools - -from trove_tempest_plugin.tests import base_test - - -class DatabaseFlavorsTest(base_test.BaseDatabaseTest): - - @classmethod - def setup_clients(cls): - super(DatabaseFlavorsTest, cls).setup_clients() - cls.client = cls.database_flavors_client - - @testtools.attr('smoke') - @decorators.idempotent_id('c94b825e-0132-4686-8049-8a4a2bc09525') - def test_get_db_flavor(self): - # The expected flavor details should be returned - flavor = (self.client.show_flavor(self.db_flavor_ref) - ['flavor']) - self.assertEqual(self.db_flavor_ref, str(flavor['id'])) - self.assertIn('ram', flavor) - self.assertIn('links', flavor) - self.assertIn('name', flavor) - - @testtools.attr('smoke') - @decorators.idempotent_id('685025d6-0cec-4673-8a8d-995cb8e0d3bb') - def test_list_db_flavors(self): - flavor = (self.client.show_flavor(self.db_flavor_ref) - ['flavor']) - # List of all flavors should contain the expected flavor - flavors = self.client.list_flavors()['flavors'] - self.assertIn(flavor, flavors) - - def _check_values(self, names, db_flavor, os_flavor, in_db=True): - for name in names: - self.assertIn(name, os_flavor) - if in_db: - self.assertIn(name, db_flavor) - self.assertEqual(str(db_flavor[name]), str(os_flavor[name]), - "DB flavor differs from OS on '%s' value" - % name) - else: - self.assertNotIn(name, db_flavor) - - @testtools.attr('smoke') - @decorators.idempotent_id('afb2667f-4ec2-4925-bcb7-313fdcffb80d') - @utils.services('compute') - def test_compare_db_flavors_with_os(self): - db_flavors = self.client.list_flavors()['flavors'] - os_flavors = (self.os_flavors_client.list_flavors(detail=True) - ['flavors']) - self.assertEqual(len(os_flavors), len(db_flavors), - "OS flavors %s do not match DB flavors %s" % - (os_flavors, db_flavors)) - for os_flavor in os_flavors: - db_flavor =\ - self.client.show_flavor(os_flavor['id'])['flavor'] - if db_flavor['id']: - self.assertIn('id', db_flavor) - self.assertEqual(str(db_flavor['id']), str(os_flavor['id']), - "DB flavor id differs from OS flavor id value" - ) - else: - self.assertIn('str_id', db_flavor) - self.assertEqual(db_flavor['str_id'], str(os_flavor['id']), - "DB flavor id differs from OS flavor id value" - ) - - self._check_values(['name', 'ram', 'vcpus', - 'disk'], db_flavor, os_flavor) - self._check_values(['swap'], db_flavor, os_flavor, - in_db=False) diff --git a/trove_tempest_plugin/tests/api/test_flavors_negative.py b/trove_tempest_plugin/tests/api/test_flavors_negative.py deleted file mode 100644 index 6a798d8..0000000 --- a/trove_tempest_plugin/tests/api/test_flavors_negative.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# All Rights Reserved. -# -# 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. - - -from tempest.lib import decorators -from tempest.lib import exceptions as lib_exc -from testtools import testcase as testtools - -from trove_tempest_plugin.tests import base_test - - -class DatabaseFlavorsNegativeTest(base_test.BaseDatabaseTest): - - @classmethod - def setup_clients(cls): - super(DatabaseFlavorsNegativeTest, cls).setup_clients() - cls.client = cls.database_flavors_client - - @testtools.attr('negative') - @decorators.idempotent_id('f8e7b721-373f-4a64-8e9c-5327e975af3e') - def test_get_non_existent_db_flavor(self): - # flavor details are not returned for non-existent flavors - self.assertRaises(lib_exc.NotFound, - self.client.show_flavor, -1) diff --git a/trove_tempest_plugin/tests/api/test_limits.py b/trove_tempest_plugin/tests/api/test_limits.py deleted file mode 100644 index e85c284..0000000 --- a/trove_tempest_plugin/tests/api/test_limits.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -from tempest.lib import decorators -from testtools import testcase as testtools - -from trove_tempest_plugin.tests import base_test - - -class DatabaseLimitsTest(base_test.BaseDatabaseTest): - - @classmethod - def resource_setup(cls): - super(DatabaseLimitsTest, cls).resource_setup() - cls.client = cls.database_limits_client - - @testtools.attr('smoke') - @decorators.idempotent_id('73024538-f316-4829-b3e9-b459290e137a') - def test_absolute_limits(self): - # Test to verify if all absolute limit parameters are - # present when verb is ABSOLUTE - limits = self.client.list_limits()['limits'] - expected_abs_limits = ['max_backups', 'max_volumes', - 'max_instances', 'verb'] - absolute_limit = [l for l in limits - if l['verb'] == 'ABSOLUTE'] - self.assertEqual(1, len(absolute_limit), "One ABSOLUTE limit " - "verb is allowed. Fetched %s" - % len(absolute_limit)) - actual_abs_limits = absolute_limit[0].keys() - missing_abs_limit = set(expected_abs_limits) - set(actual_abs_limits) - self.assertEmpty(missing_abs_limit, - "Failed to find the following absolute limit(s)" - " in a fetched list: %s" % - ', '.join(str(a) for a in missing_abs_limit)) diff --git a/trove_tempest_plugin/tests/api/test_versions.py b/trove_tempest_plugin/tests/api/test_versions.py deleted file mode 100644 index 52856fa..0000000 --- a/trove_tempest_plugin/tests/api/test_versions.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -from tempest.lib import decorators -from testtools import testcase as testtools - -from trove_tempest_plugin.tests import base_test - - -class DatabaseVersionsTest(base_test.BaseDatabaseTest): - - @classmethod - def setup_clients(cls): - super(DatabaseVersionsTest, cls).setup_clients() - cls.client = cls.database_versions_client - - @testtools.attr('smoke') - @decorators.idempotent_id('6952cd77-90cd-4dca-bb60-8e2c797940cf') - def test_list_db_versions(self): - versions = self.client.list_versions()['versions'] - self.assertTrue(len(versions) > 0, "No database versions found") - # List of all versions should contain the current version, and there - # should only be one 'current' version - current_versions = list() - for version in versions: - if 'CURRENT' == version['status']: - current_versions.append(version['id']) - self.assertEqual(1, len(current_versions)) - self.assertIn(self.db_current_version, current_versions) diff --git a/trove_tempest_plugin/tests/base.py b/trove_tempest_plugin/tests/base.py new file mode 100644 index 0000000..b22ea83 --- /dev/null +++ b/trove_tempest_plugin/tests/base.py @@ -0,0 +1,252 @@ +# Copyright 2014 OpenStack Foundation +# All Rights Reserved. +# +# 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. +from oslo_log import log as logging +from oslo_service import loopingcall +import tenacity + +from tempest import config +from tempest.lib.common.utils import data_utils +from tempest.lib.common.utils import test_utils +from tempest.lib import exceptions +from tempest import test + +from trove_tempest_plugin.tests import utils + +CONF = config.CONF +LOG = logging.getLogger(__name__) + + +class BaseTroveTest(test.BaseTestCase): + credentials = ('admin', 'primary') + + @classmethod + def get_resource_name(cls, resource_type): + prefix = "trove-tempest-%s" % cls.__name__ + return data_utils.rand_name(resource_type, prefix=prefix) + + @classmethod + def skip_checks(cls): + super(BaseTroveTest, cls).skip_checks() + + if not CONF.service_available.trove: + raise cls.skipException("Database service is not available.") + + @classmethod + def setup_clients(cls): + super(BaseTroveTest, cls).setup_clients() + + cls.client = cls.os_primary.database.TroveClient() + cls.admin_client = cls.os_admin.database.TroveClient() + + @classmethod + def setup_credentials(cls): + # Do not create network resources automatically. + cls.set_network_resources() + super(BaseTroveTest, cls).setup_credentials() + + @classmethod + @tenacity.retry( + retry=tenacity.retry_if_exception_type(exceptions.Conflict), + wait=tenacity.wait_incrementing(1, 1, 5), + stop=tenacity.stop_after_attempt(15) + ) + def _delete_network(cls, net_id): + """Make sure the network is deleted. + + Neutron can be slow to clean up ports from the subnets/networks. + Retry this delete a few times if we get a "Conflict" error to give + neutron time to fully cleanup the ports. + """ + networks_client = cls.os_primary.networks_client + try: + networks_client.delete_network(net_id) + except Exception: + LOG.error('Unable to delete network %s', net_id) + raise + + @classmethod + @tenacity.retry( + retry=tenacity.retry_if_exception_type(exceptions.Conflict), + wait=tenacity.wait_incrementing(1, 1, 5), + stop=tenacity.stop_after_attempt(15) + ) + def _delete_subnet(cls, subnet_id): + """Make sure the subnet is deleted. + + Neutron can be slow to clean up ports from the subnets/networks. + Retry this delete a few times if we get a "Conflict" error to give + neutron time to fully cleanup the ports. + """ + subnets_client = cls.os_primary.subnets_client + try: + subnets_client.delete_subnet(subnet_id) + except Exception: + LOG.error('Unable to delete subnet %s', subnet_id) + raise + + @classmethod + def _create_network(cls): + """Create database instance network.""" + networks_client = cls.os_primary.networks_client + subnets_client = cls.os_primary.subnets_client + routers_client = cls.os_primary.routers_client + + network_kwargs = {"name": cls.get_resource_name("network")} + result = networks_client.create_network(**network_kwargs) + LOG.info('Private network created: %s', result['network']) + cls.private_network = result['network']["id"] + cls.addClassResourceCleanup( + utils.wait_for_removal, + cls._delete_network, + networks_client.show_network, + cls.private_network + ) + + subnet_kwargs = { + 'name': cls.get_resource_name("subnet"), + 'network_id': cls.private_network, + 'cidr': CONF.database.subnet_cidr, + 'ip_version': 4 + } + result = subnets_client.create_subnet(**subnet_kwargs) + subnet_id = result['subnet']['id'] + LOG.info('Private subnet created: %s', result['subnet']) + cls.addClassResourceCleanup( + utils.wait_for_removal, + cls._delete_subnet, + subnets_client.show_subnet, + subnet_id + ) + + # In dev node, Trove instance needs to connect with control host + router_params = { + 'name': cls.get_resource_name("router"), + 'external_gateway_info': { + "network_id": CONF.network.public_network_id + } + } + result = routers_client.create_router(**router_params) + router_id = result['router']['id'] + LOG.info('Private router created: %s', result['router']) + cls.addClassResourceCleanup( + utils.wait_for_removal, + routers_client.delete_router, + routers_client.show_router, + router_id + ) + + routers_client.add_router_interface(router_id, subnet_id=subnet_id) + LOG.info('Subnet %s added to the router %s', subnet_id, router_id) + cls.addClassResourceCleanup( + routers_client.remove_router_interface, + router_id, + subnet_id=subnet_id + ) + + @classmethod + def resource_setup(cls): + super(BaseTroveTest, cls).resource_setup() + + # Create network for database instance, use cls.private_network as the + # network ID. + cls._create_network() + + @classmethod + def create_instance(cls, database="test_db", username="test_user", + password="password"): + """Create database instance. + + Creating database instance is time-consuming, so we define this method + as a class method, which means the instance is shared in a single + TestCase. According to + https://docs.openstack.org/tempest/latest/write_tests.html#adding-a-new-testcase, + all test methods within a TestCase are assumed to be executed serially. + """ + name = cls.get_resource_name("instance") + body = { + "instance": { + "name": name, + "flavorRef": CONF.database.flavor_id, + "volume": { + "size": 1, + "type": CONF.database.volume_type + }, + "databases": [ + { + "name": database + } + ], + "users": [ + { + "name": username, + "password": password, + } + ], + "datastore": { + "type": CONF.database.datastore_type, + "version": CONF.database.datastore_version + }, + "nics": [ + { + "net-id": cls.private_network + } + ] + } + } + + res = cls.client.create_resource("instances", body) + instance_id = res["instance"]["id"] + cls.addClassResourceCleanup(cls.wait_for_instance_status, instance_id, + need_delete=True, status="DELETED") + + return instance_id + + @classmethod + def wait_for_instance_status(cls, id, status="ACTIVE", need_delete=False): + def _wait(): + try: + res = cls.client.get_resource("instances", id) + except exceptions.NotFound: + if need_delete or status == "DELETED": + raise loopingcall.LoopingCallDone() + return + + if res["instance"]["status"] == status: + raise loopingcall.LoopingCallDone() + elif status != "ERROR" and res["instance"]["status"] == "ERROR": + # If instance status goes to ERROR but is not expected, stop + # waiting + message = "Instance status is ERROR." + caller = test_utils.find_test_caller() + if caller: + message = '({caller}) {message}'.format(caller=caller, + message=message) + raise exceptions.UnexpectedResponseCode(message) + + if need_delete: + cls.client.delete_resource("instances", id, ignore_notfound=True) + + timer = loopingcall.FixedIntervalWithTimeoutLoopingCall(_wait) + try: + timer.start(interval=10, + timeout=CONF.database.database_build_timeout).wait() + except loopingcall.LoopingCallTimeOut: + message = ("Instance %s is not in the expected status: %s" % + (id, status)) + caller = test_utils.find_test_caller() + if caller: + message = '({caller}) {message}'.format(caller=caller, + message=message) + raise exceptions.TimeoutException(message) diff --git a/trove_tempest_plugin/tests/base_test.py b/trove_tempest_plugin/tests/base_test.py deleted file mode 100644 index 2cef0c6..0000000 --- a/trove_tempest_plugin/tests/base_test.py +++ /dev/null @@ -1,86 +0,0 @@ -# Copyright 2014 OpenStack Foundation -# All Rights Reserved. -# -# 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. - -from tempest import config -from tempest import test - -from trove_tempest_plugin import clients - -CONF = config.CONF - - -class BaseDatabaseTest(test.BaseTestCase): - """Base test case class. - - Includes parts common to API and scenario tests: - * test case callbacks, - * service clients initialization. - """ - - credentials = ['primary'] - client_manager = clients.Manager - - @classmethod - def skip_checks(cls): - super(BaseDatabaseTest, cls).skip_checks() - if not CONF.service_available.trove: - skip_msg = ("%s skipped as trove is not available" % cls.__name__) - raise cls.skipException(skip_msg) - - @classmethod - def setup_clients(cls): - """Setups service clients. - - Tempest provides a convenient fabrication interface, which can be used - to produce instances of clients configured with the required parameters - and a selected set of credentials. Thanks to this interface, the - complexity of client initialization is hidden from the developer. All - parameters such as "catalog_type", "auth_provider", "build_timeout" - etc. are read from Tempest configuration and then automatically - installed in the clients. - - The fabrication interface is enabled through the client manager, which - is hooked to the class by the "client_manager" property. - - To initialize a new client, one need to specify the set of credentials - (primary, admin) to be used and the category of client (eg compute, - image, database, etc.). Together, they constitute a proxy for the - fabricators of specific client classes from a given category. - - For example, initializing a new flavors client from the database - category with primary privileges boils down to the following call: - - flavors_client = cls.os_primary.database.FlavorsClient() - - In order to initialize a new networks client from the compute category - with administrator privilages: - - networks_client = cls.os_admin.compute.NetworksClient() - - Note, that selected set of credentials must be declared in the - "credentials" property of this class. - """ - super(BaseDatabaseTest, cls).setup_clients() - cls.database_flavors_client = cls.os_primary.database.FlavorsClient() - cls.os_flavors_client = cls.os_primary.compute.FlavorsClient() - cls.database_limits_client = cls.os_primary.database.LimitsClient() - cls.database_versions_client = cls.os_primary.database.VersionsClient() - - @classmethod - def resource_setup(cls): - super(BaseDatabaseTest, cls).resource_setup() - cls.catalog_type = CONF.database.catalog_type - cls.db_flavor_ref = CONF.database.db_flavor_ref - cls.db_current_version = CONF.database.db_current_version diff --git a/trove_tempest_plugin/tests/scenario/__init__.py b/trove_tempest_plugin/tests/scenario/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/tests/scenario/test_instance_basic.py b/trove_tempest_plugin/tests/scenario/test_instance_basic.py new file mode 100644 index 0000000..5876eaf --- /dev/null +++ b/trove_tempest_plugin/tests/scenario/test_instance_basic.py @@ -0,0 +1,29 @@ +# Copyright 2019 Catalyst Cloud Ltd. +# +# 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. +from tempest.lib import decorators + +from trove_tempest_plugin.tests import base + + +class TestInstanceBasic(base.BaseTroveTest): + @classmethod + def resource_setup(cls): + super(TestInstanceBasic, cls).resource_setup() + + cls.instance_id = cls.create_instance() + cls.wait_for_instance_status(cls.instance_id) + + @decorators.idempotent_id("40cf38ce-cfbf-11e9-8760-1458d058cfb2") + def test_database_access(self): + pass diff --git a/trove_tempest_plugin/tests/utils.py b/trove_tempest_plugin/tests/utils.py new file mode 100644 index 0000000..512edab --- /dev/null +++ b/trove_tempest_plugin/tests/utils.py @@ -0,0 +1,51 @@ +# Copyright 2019 Catalyst Cloud Ltd. +# +# 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 time + +from oslo_log import log as logging + +from tempest.lib import exceptions + +LOG = logging.getLogger(__name__) + + +def wait_for_removal(delete_func, show_func, *args, **kwargs): + """Call the delete function, then wait for it to be 'NotFound' + + :param delete_func: The delete function to call. + :param show_func: The show function to call looking for 'NotFound'. + :param ID: The ID of the object to delete/show. + :raises TimeoutException: The object did not achieve the status or ERROR in + the check_timeout period. + :returns: None + """ + check_timeout = 15 + try: + delete_func(*args, **kwargs) + except exceptions.NotFound: + return + + start = int(time.time()) + LOG.info('Waiting for object to be NotFound') + while True: + try: + show_func(*args, **kwargs) + except exceptions.NotFound: + return + + if int(time.time()) - start >= check_timeout: + message = ('%s did not raise NotFound in %s seconds.' % + (show_func.__name__, check_timeout)) + raise exceptions.TimeoutException(message) + time.sleep(3)