From 67dfeb8ebdbe17671332ecbef66fb767c6e5f178 Mon Sep 17 00:00:00 2001 From: Sam Morrison Date: Thu, 5 Nov 2020 10:32:52 +1100 Subject: [PATCH] Reinstate removed api tests Story: 2008365 Task: 41273 Depends-On: https://review.opendev.org/c/openstack/trove/+/765066 Change-Id: Ib08bf8951352c978b88a4746aa7b44e0b8cec177 --- .zuul.yaml | 31 +-- trove_tempest_plugin/common/__init__.py | 0 trove_tempest_plugin/common/utils.py | 24 ++ trove_tempest_plugin/common/waiters.py | 115 +++++++++ trove_tempest_plugin/config.py | 28 ++- .../services/database/__init__.py | 0 .../services/database/json/__init__.py | 0 .../services/database/json/backups_client.py | 59 +++++ .../database/json/datastores_client.py | 31 +++ .../services/database/json/flavors_client.py | 37 +++ .../database/json/instances_client.py | 234 ++++++++++++++++++ .../services/database/json/limits_client.py | 31 +++ .../services/database/json/versions_client.py | 37 +++ trove_tempest_plugin/tests/api/__init__.py | 0 .../tests/api/database/__init__.py | 0 .../tests/api/database/base.py | 158 ++++++++++++ .../tests/api/database/datastores/__init__.py | 0 .../database/datastores/test_datastores.py | 33 +++ .../tests/api/database/flavors/__init__.py | 0 .../api/database/flavors/test_flavors.py | 58 +++++ .../database/flavors/test_flavors_negative.py | 36 +++ .../tests/api/database/instances/__init__.py | 0 .../tests/api/database/instances/base.py | 46 ++++ .../instances/test_instance_actions.py | 119 +++++++++ .../instances/test_instance_backups.py | 74 ++++++ .../tests/api/database/limits/__init__.py | 0 .../tests/api/database/limits/test_limits.py | 47 ++++ .../tests/api/database/versions/__init__.py | 0 .../api/database/versions/test_versions.py | 41 +++ trove_tempest_plugin/tests/base.py | 3 + .../tests/scenario/base_basic.py | 3 + 31 files changed, 1224 insertions(+), 21 deletions(-) create mode 100644 trove_tempest_plugin/common/__init__.py create mode 100644 trove_tempest_plugin/common/utils.py create mode 100644 trove_tempest_plugin/common/waiters.py create mode 100644 trove_tempest_plugin/services/database/__init__.py create mode 100644 trove_tempest_plugin/services/database/json/__init__.py create mode 100644 trove_tempest_plugin/services/database/json/backups_client.py create mode 100644 trove_tempest_plugin/services/database/json/datastores_client.py create mode 100644 trove_tempest_plugin/services/database/json/flavors_client.py create mode 100644 trove_tempest_plugin/services/database/json/instances_client.py create mode 100644 trove_tempest_plugin/services/database/json/limits_client.py create mode 100644 trove_tempest_plugin/services/database/json/versions_client.py create mode 100644 trove_tempest_plugin/tests/api/__init__.py create mode 100644 trove_tempest_plugin/tests/api/database/__init__.py create mode 100644 trove_tempest_plugin/tests/api/database/base.py create mode 100644 trove_tempest_plugin/tests/api/database/datastores/__init__.py create mode 100644 trove_tempest_plugin/tests/api/database/datastores/test_datastores.py create mode 100644 trove_tempest_plugin/tests/api/database/flavors/__init__.py create mode 100644 trove_tempest_plugin/tests/api/database/flavors/test_flavors.py create mode 100644 trove_tempest_plugin/tests/api/database/flavors/test_flavors_negative.py create mode 100644 trove_tempest_plugin/tests/api/database/instances/__init__.py create mode 100644 trove_tempest_plugin/tests/api/database/instances/base.py create mode 100644 trove_tempest_plugin/tests/api/database/instances/test_instance_actions.py create mode 100644 trove_tempest_plugin/tests/api/database/instances/test_instance_backups.py create mode 100644 trove_tempest_plugin/tests/api/database/limits/__init__.py create mode 100644 trove_tempest_plugin/tests/api/database/limits/test_limits.py create mode 100644 trove_tempest_plugin/tests/api/database/versions/__init__.py create mode 100644 trove_tempest_plugin/tests/api/database/versions/test_versions.py diff --git a/.zuul.yaml b/.zuul.yaml index c0ba067..c5c0039 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -25,10 +25,10 @@ This job is for stable branch prior to Ussuri for testing on py2. required-projects: - - openstack/neutron - - openstack/trove - - openstack/trove-tempest-plugin - - openstack/tempest + - opendev.org/openstack/neutron + - opendev.org/openstack/trove + - opendev.org/openstack/trove-tempest-plugin + - opendev.org/openstack/tempest vars: tox_envlist: all devstack_localrc: @@ -51,14 +51,15 @@ - job: name: trove-tempest-plugin parent: devstack-tempest + nodeset: trove-ubuntu-bionic timeout: 7800 description: | This job is for testing on py3 which is Ussuri onwards. required-projects: &base_required_projects - - openstack/python-troveclient - - openstack/trove - - openstack/trove-tempest-plugin - - openstack/tempest + - opendev.org/openstack/python-troveclient + - opendev.org/openstack/trove + - opendev.org/openstack/trove-tempest-plugin + - opendev.org/openstack/tempest irrelevant-files: - ^.*\.rst$ - ^doc/.*$ @@ -69,24 +70,17 @@ tempest_concurrency: 1 devstack_localrc: TEMPEST_PLUGINS: /opt/stack/trove-tempest-plugin - USE_PYTHON3: true devstack_local_conf: post-config: $TROVE_CONF: DEFAULT: - usage_timeout: 1800 + usage_timeout: 600 + agent_heartbeat_expiry: 300 devstack_plugins: trove: https://opendev.org/openstack/trove.git devstack_services: 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 @@ -98,11 +92,12 @@ s-object: true s-proxy: true tempest: true - tempest_test_regex: ^trove_tempest_plugin\.tests.scenario\.test_instance_basic\.TestInstanceBasicMySQL\.test_database_access + tempest_test_regex: ^trove_tempest_plugin - job: name: trove-tempest-ipv6-only parent: devstack-tempest-ipv6 + nodeset: trove-ubuntu-bionic description: | Trove devstack tempest tests job for IPv6-only deployment required-projects: *base_required_projects diff --git a/trove_tempest_plugin/common/__init__.py b/trove_tempest_plugin/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/common/utils.py b/trove_tempest_plugin/common/utils.py new file mode 100644 index 0000000..62772ef --- /dev/null +++ b/trove_tempest_plugin/common/utils.py @@ -0,0 +1,24 @@ +# 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.common.utils import data_utils + + +def rand_name(): + """Return a safe name to use for a user or a DB name + + Some datastores have limits on size and characters + """ + return data_utils.rand_name().replace('-', '')[:16] diff --git a/trove_tempest_plugin/common/waiters.py b/trove_tempest_plugin/common/waiters.py new file mode 100644 index 0000000..37bd513 --- /dev/null +++ b/trove_tempest_plugin/common/waiters.py @@ -0,0 +1,115 @@ +# Copyright 2019 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. + +import re +import time + +from tempest.lib import exceptions as lib_exc + + +def wait_for_db_instance_status(client, instance_id, status, + failure_pattern='ERROR'): + """Wait for a db instance to reach a given status""" + start = int(time.time()) + fail_regexp = re.compile(failure_pattern) + if status == 'ACTIVE': + status = ['ACTIVE', 'HEALTHY'] + else: + status = [status] + error_count = 0 + while True: + try: + body = client.show_db_instance(instance_id)['instance'] + except lib_exc.NotFound: + if status == ['DELETE_COMPLETE']: + return + instance_name = body['name'] + instance_status = body['status'] + if instance_status in status: + return body + if fail_regexp.search(instance_status): + error_count += 1 + if error_count > 10: + raise KeyError("Instance in ERROR state") + if int(time.time()) - start >= client.build_timeout: + message = ('DB Instance %s failed to reach %s status' + '(current: %s) within the required time (%s s).' % + (instance_name, status, instance_status, + client.build_timeout)) + raise lib_exc.TimeoutException(message) + + time.sleep(client.build_interval) + + +def wait_for_db_instance_decommission(client, instance_id): + """Wait for a db instance decomission""" + start = int(time.time()) + while True: + try: + client.show_db_instance(instance_id)['instance'] + except lib_exc.NotFound: + return + if int(time.time()) - start >= client.build_timeout: + message = ('DB Instance %s deletion failed ' + 'within the required time (%s s).' % + (instance_id, client.build_timeout)) + raise lib_exc.TimeoutException(message) + + time.sleep(client.build_interval) + + +def wait_for_backup_status(client, backup_id, status, + failure_pattern='FAILED'): + """Wait for a backup to reach a given status""" + start = int(time.time()) + fail_regexp = re.compile(failure_pattern) + + while True: + try: + body = client.show_backup(backup_id)['backup'] + except lib_exc.NotFound: + if status == 'DELETE_COMPLETE': + return + backup_name = body['name'] + backup_status = body['status'] + if backup_status == status: + return body + if fail_regexp.search(backup_status): + raise KeyError("Backup in FAILED state") + if int(time.time()) - start >= client.build_timeout: + message = ('Backup %s failed to reach %s status' + '(current: %s) within the required time (%s s).' % + (backup_name, status, backup_status, + client.build_timeout)) + raise lib_exc.TimeoutException(message) + + time.sleep(client.build_interval) + + +def wait_for_backup_delete(client, backup_id): + """Wait for a backup to be deleted""" + start = int(time.time()) + while True: + try: + client.show_backup(backup_id)['backup'] + except lib_exc.NotFound: + return + if int(time.time()) - start >= client.build_timeout: + message = ('Backup %s deletion failed ' + 'within the required time (%s s).' % + (backup_id, client.build_timeout)) + raise lib_exc.TimeoutException(message) + + time.sleep(client.build_interval) diff --git a/trove_tempest_plugin/config.py b/trove_tempest_plugin/config.py index e83fa7f..3b4d4bc 100644 --- a/trove_tempest_plugin/config.py +++ b/trove_tempest_plugin/config.py @@ -38,10 +38,28 @@ DatabaseGroup = [ 'internalURL'], help="The endpoint type to use for the Database service." ), + cfg.StrOpt('db_current_version', + default="v1.0", + help="Current database version to use in database tests."), cfg.ListOpt( 'enabled_datastores', default=['mysql'] ), + cfg.StrOpt('datastore_type', + default="MySQL", + help="Type of the Database"), + cfg.StrOpt('datastore_version', + default=None, + help="Specific datastore version to use (optional)"), + cfg.StrOpt('availability_zone', + default='nova', + help="Availability zone of the db instance to use"), + cfg.IntOpt('volume_size', + default=1, + help="Volume size for the db instances"), + cfg.StrOpt('dns_name_server', + default=None, + help="The DNS server used to query trove instance domain name"), cfg.DictOpt( 'default_datastore_versions', default={'mysql': '5.7.29'}, @@ -59,7 +77,7 @@ DatabaseGroup = [ 'build.'), cfg.IntOpt( 'database_restore_timeout', - default=3600, + default=1800, help='Timeout in seconds to wait for a database instance to ' 'be restored.' ), @@ -71,12 +89,16 @@ DatabaseGroup = [ cfg.StrOpt( 'flavor_id', default="d2", - help="The Nova flavor ID used for creating database instance." + help="The Nova flavor ID used for creating database instance.", + deprecated_group='database', + deprecated_name='db_flavor_ref', ), cfg.StrOpt( 'resize_flavor_id', default="d3", - help="The Nova flavor ID used for resizing database instance." + help="The Nova flavor ID used for resizing database instance.", + deprecated_group='database', + deprecated_name='db_flavor_ref_alt', ), cfg.StrOpt( 'shared_network', diff --git a/trove_tempest_plugin/services/database/__init__.py b/trove_tempest_plugin/services/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/services/database/json/__init__.py b/trove_tempest_plugin/services/database/json/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/services/database/json/backups_client.py b/trove_tempest_plugin/services/database/json/backups_client.py new file mode 100644 index 0000000..d73dfb2 --- /dev/null +++ b/trove_tempest_plugin/services/database/json/backups_client.py @@ -0,0 +1,59 @@ +# 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_serialization import jsonutils as json +from six.moves.urllib import parse as urllib +from tempest.lib.common import rest_client + + +class DatabaseBackupsClient(rest_client.RestClient): + + def list_backups(self, params=None): + """List all available backups.""" + url = 'backups' + if params: + url += '?%s' % urllib.urlencode(params) + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def create_backup(self, instance_id, name, description=None, parent=None, + incremental=False): + """Create a backup.""" + url = 'backups' + data = {'instance': instance_id, + 'name': name, + 'incremental': int(incremental)} + post_body = json.dumps({"backup": data}) + resp, body = self.post(url, body=post_body) + self.expected_success(202, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def delete_backup(self, backup_id): + """Delete the backup""" + url = 'backups/%s' % backup_id + resp, body = self.delete(url) + self.expected_success(202, resp.status) + return rest_client.ResponseBody(resp, body) + + def show_backup(self, backup_id): + """Show backups.""" + url = 'backups/%s' % backup_id + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) diff --git a/trove_tempest_plugin/services/database/json/datastores_client.py b/trove_tempest_plugin/services/database/json/datastores_client.py new file mode 100644 index 0000000..3eb85ed --- /dev/null +++ b/trove_tempest_plugin/services/database/json/datastores_client.py @@ -0,0 +1,31 @@ +# 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_serialization import jsonutils as json +from six.moves.urllib import parse as urllib +from tempest.lib.common import rest_client + + +class DatabaseDatastoresClient(rest_client.RestClient): + + def list_db_datastores(self, params=None): + """List all available datastores.""" + url = 'datastores' + if params: + url += '?%s' % urllib.urlencode(params) + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) diff --git a/trove_tempest_plugin/services/database/json/flavors_client.py b/trove_tempest_plugin/services/database/json/flavors_client.py new file mode 100644 index 0000000..95ecfdc --- /dev/null +++ b/trove_tempest_plugin/services/database/json/flavors_client.py @@ -0,0 +1,37 @@ +# 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_serialization import jsonutils as json +from six.moves import urllib +from tempest.lib.common import rest_client + + +class DatabaseFlavorsClient(rest_client.RestClient): + + def list_db_flavors(self, params=None): + url = 'flavors' + if params: + url += '?%s' % urllib.parse.urlencode(params) + + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def show_db_flavor(self, db_flavor_id): + resp, body = self.get("flavors/%s" % db_flavor_id) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) diff --git a/trove_tempest_plugin/services/database/json/instances_client.py b/trove_tempest_plugin/services/database/json/instances_client.py new file mode 100644 index 0000000..ece431e --- /dev/null +++ b/trove_tempest_plugin/services/database/json/instances_client.py @@ -0,0 +1,234 @@ +# 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. + +import time + +from oslo_serialization import jsonutils as json +from six.moves.urllib import parse as urllib +from tempest.lib.common import rest_client + + +class DatabaseInstancesClient(rest_client.RestClient): + + def list_db_instances(self, params=None): + """List all available instances.""" + url = 'instances' + if params: + url += '?%s' % urllib.urlencode(params) + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def create_db_instance(self, params): + """Create an instance.""" + url = 'instances' + headers = self.get_headers() + resp, body = self.post(url, headers=headers, body=params) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def show_db_instance(self, instance_id): + """Show the db instance""" + url = 'instances/%s' % instance_id + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def update_db_instance(self, instance_id, **kwargs): + """Updates the db instance""" + url = 'instances/%s' % instance_id + post_body = json.dumps({'instance': kwargs}) + resp, body = self.patch(url, post_body) + self.expected_success(202, resp.status) + if body: + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def _action(self, instance_id, action_name, **kwargs): + post_body = json.dumps({action_name: kwargs}) + resp, body = self.post('instances/%s/action' % instance_id, + post_body) + self.expected_success(202, resp.status) + if body: + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def restart_db_instance(self, instance_id): + """Restart the db instance""" + return self._action(instance_id, "restart") + + def upgrade_db_instance(self, instance_id, datastore_version): + body = json.dumps({"instance": { + "datastore_version": datastore_version}}) + resp, body = self.patch('instances/%s' % instance_id, + body) + self.expected_success(202, resp.status) + if body: + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def resize_db_instance(self, instance_id, flavor_id): + return self._action(instance_id, "resize", flavorRef=flavor_id) + + def resize_db_instance_volume(self, instance_id, new_size): + return self._action(instance_id, "resize", volume={'size': new_size}) + + def delete_db_instance(self, instance_id): + """Delete the db instance""" + url = 'instances/%s' % instance_id + resp, body = self.delete(url) + self.expected_success(202, resp.status) + return rest_client.ResponseBody(resp, body) + + def list_databases(self, instance_id): + """List all databases on an instance.""" + resp, body = self.get('instances/%s/databases' % instance_id) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def create_database(self, instance_id, name): + """Creates a database on an instance""" + post_body = json.dumps({"databases": [{"name": name}]}) + resp, body = self.post('instances/%s/databases' % instance_id, + post_body) + self.expected_success(202, resp.status) + if body: + body = json.loads(body) + time.sleep(2) + return rest_client.ResponseBody(resp, body) + + def delete_database(self, instance_id, name): + """Deletes a database on an instance""" + resp, body = self.delete('instances/%s/databases/%s' % (instance_id, + name)) + self.expected_success(202, resp.status) + if body: + body = json.loads(body) + time.sleep(2) + return rest_client.ResponseBody(resp, body) + + def root_show(self, instance_id): + """Shows if root has ever been enabled on an instance""" + url = 'instances/%s/root' % instance_id + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def root_enable(self, instance_id): + """Enables root on an instance""" + url = 'instances/%s/root' % instance_id + resp, body = self.post(url, body=json.dumps({})) + self.expected_success(200, resp.status) + if body: + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def root_disable(self, instance_id): + """Disables root on an instance""" + url = 'instances/%s/root' % instance_id + resp, body = self.delete(url) + self.expected_success(204, resp.status) + if body: + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def list_users(self, instance_id): + """List all users on an instance.""" + resp, body = self.get('instances/%s/users' % instance_id) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def show_user(self, instance_id, name): + """Get a user on an instance.""" + resp, body = self.get('instances/%s/users/%s' % (instance_id, name)) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def create_user(self, instance_id, name, password, databases=[]): + """Creates a user on an instance""" + post_body = json.dumps({"users": [{"name": name, + "password": password, + "databases": databases}]}) + resp, body = self.post('instances/%s/users' % instance_id, + post_body) + self.expected_success(202, resp.status) + if body: + body = json.loads(body) + time.sleep(2) + return rest_client.ResponseBody(resp, body) + + def update_user(self, instance_id, name, **kwargs): + """Updates the user""" + url = 'instances/%s/users/%s' % (instance_id, name) + post_body = json.dumps({'user': kwargs}) + resp, body = self.put(url, post_body) + self.expected_success(202, resp.status) + if body: + body = json.loads(body) + time.sleep(2) + return rest_client.ResponseBody(resp, body) + + def delete_user(self, instance_id, name): + """Updates the user""" + url = 'instances/%s/users/%s' % (instance_id, name) + resp, body = self.delete(url) + self.expected_success(202, resp.status) + if body: + body = json.loads(body) + time.sleep(2) + return rest_client.ResponseBody(resp, body) + + def grant_user_access(self, instance_id, name, databases): + """Grants a user access to a database""" + url = 'instances/%s/users/%s/databases' % (instance_id, name) + databases = [{'name': x} for x in databases] + post_body = json.dumps({'databases': databases}) + resp, body = self.put(url, post_body) + self.expected_success(202, resp.status) + if body: + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def revoke_user_access(self, instance_id, name, database): + """Revokes a user access to a database""" + url = 'instances/%s/users/%s/databases/%s' % (instance_id, name, + database) + resp, body = self.delete(url) + self.expected_success(202, resp.status) + if body: + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def show_user_access(self, instance_id, name): + """Shows access details of a user of an instanceqq""" + url = 'instances/%s/users/%s/databases' % (instance_id, name) + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) + + def list_backups(self, instance_id): + """List all backups on an instance.""" + resp, body = self.get('instances/%s/backups' % instance_id) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) diff --git a/trove_tempest_plugin/services/database/json/limits_client.py b/trove_tempest_plugin/services/database/json/limits_client.py new file mode 100644 index 0000000..23164a8 --- /dev/null +++ b/trove_tempest_plugin/services/database/json/limits_client.py @@ -0,0 +1,31 @@ +# 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_serialization import jsonutils as json +from six.moves.urllib import parse as urllib +from tempest.lib.common import rest_client + + +class DatabaseLimitsClient(rest_client.RestClient): + + def list_db_limits(self, params=None): + """List all limits.""" + url = 'limits' + if params: + url += '?%s' % urllib.urlencode(params) + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) diff --git a/trove_tempest_plugin/services/database/json/versions_client.py b/trove_tempest_plugin/services/database/json/versions_client.py new file mode 100644 index 0000000..d7154f2 --- /dev/null +++ b/trove_tempest_plugin/services/database/json/versions_client.py @@ -0,0 +1,37 @@ +# 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_serialization import jsonutils as json +from six.moves.urllib import parse as urllib +from tempest.lib.common import rest_client + + +class DatabaseVersionsClient(rest_client.RestClient): + + def __init__(self, auth_provider, service, region, **kwargs): + super(DatabaseVersionsClient, self).__init__( + auth_provider, service, region, **kwargs) + self.skip_path() + + def list_db_versions(self, params=None): + """List all versions.""" + url = '' + if params: + url += '?%s' % urllib.urlencode(params) + + resp, body = self.get(url) + self.expected_success(200, resp.status) + body = json.loads(body) + return rest_client.ResponseBody(resp, body) diff --git a/trove_tempest_plugin/tests/api/__init__.py b/trove_tempest_plugin/tests/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/tests/api/database/__init__.py b/trove_tempest_plugin/tests/api/database/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/tests/api/database/base.py b/trove_tempest_plugin/tests/api/database/base.py new file mode 100644 index 0000000..fcd3c24 --- /dev/null +++ b/trove_tempest_plugin/tests/api/database/base.py @@ -0,0 +1,158 @@ +# 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. + +import json + +from tempest import config +from tempest.lib.common.utils import data_utils +from tempest.lib.common.utils import test_utils +import tempest.test + +from trove_tempest_plugin.common import waiters +from trove_tempest_plugin.services.database.json import backups_client +from trove_tempest_plugin.services.database.json import datastores_client +from trove_tempest_plugin.services.database.json import flavors_client +from trove_tempest_plugin.services.database.json import instances_client +from trove_tempest_plugin.services.database.json import limits_client +from trove_tempest_plugin.services.database.json import versions_client + +CONF = config.CONF + + +class BaseDatabaseTest(tempest.test.BaseTestCase): + """Base test case class for all Database API tests.""" + + credentials = ['primary'] + + @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): + super(BaseDatabaseTest, cls).setup_clients() + default_params = config.service_client_config() + + # NOTE: Tempest uses timeout values of compute API if project specific + # timeout values don't exist. + default_params_with_timeout_values = { + 'build_interval': CONF.compute.build_interval, + 'build_timeout': CONF.database.database_build_timeout + } + default_params_with_timeout_values.update(default_params) + cls.database_flavors_client = flavors_client.DatabaseFlavorsClient( + cls.os_primary.auth_provider, + CONF.database.catalog_type, + CONF.identity.region, + **default_params_with_timeout_values) + cls.os_flavors_client = cls.os_primary.flavors_client + cls.database_limits_client = limits_client.DatabaseLimitsClient( + cls.os_primary.auth_provider, + CONF.database.catalog_type, + CONF.identity.region, + **default_params_with_timeout_values) + cls.database_versions_client = versions_client.DatabaseVersionsClient( + cls.os_primary.auth_provider, + CONF.database.catalog_type, + CONF.identity.region, + **default_params_with_timeout_values) + cls.database_datastores_client =\ + datastores_client.DatabaseDatastoresClient( + cls.os_primary.auth_provider, + CONF.database.catalog_type, + CONF.identity.region, + **default_params_with_timeout_values) + cls.database_instances_client =\ + instances_client.DatabaseInstancesClient( + cls.os_primary.auth_provider, + CONF.database.catalog_type, + CONF.identity.region, + **default_params_with_timeout_values) + cls.database_backups_client =\ + backups_client.DatabaseBackupsClient( + cls.os_primary.auth_provider, + CONF.database.catalog_type, + CONF.identity.region, + **default_params_with_timeout_values) + + @classmethod + def resource_setup(cls): + super(BaseDatabaseTest, cls).resource_setup() + + cls.catalog_type = CONF.database.catalog_type + cls.flavor_id = CONF.database.flavor_id + cls.resize_flavor_id = CONF.database.resize_flavor_id + cls.db_current_version = CONF.database.db_current_version + cls.datastore_type = CONF.database.datastore_type + cls.datastore_version = CONF.database.datastore_version + cls.availability_zone = CONF.database.availability_zone + cls.volume_size = CONF.database.volume_size + cls.dns_name_server = CONF.database.dns_name_server + + @classmethod + def create_test_instance(cls, backup_id=None, datastore_version=None): + """Wrapper utility that returns a test serinstancever. + + This wrapper utility calls the common create test instance and + returns a test instance. The purpose of this wrapper is to minimize + the impact on the code of the tests already using this + function. + + :param validatable: Whether the server will connectable via db protocol + :param validation_resources: Dictionary of validation resources as + returned by `get_class_validation_resources`. + :param kwargs: Extra arguments are passed down to the + `create_test_instance` call. + """ + name = data_utils.rand_name(cls.__name__ + "-instance") + tenant_network = cls.get_tenant_network() + + instance_dict = { + "users": [], + "availability_zone": CONF.database.availability_zone, + "flavorRef": CONF.database.flavor_id, + "volume": {"size": CONF.database.volume_size}, + "databases": [], + "datastore": {"type": CONF.database.datastore_type}, + "name": name, + "nics": [{"network_id": tenant_network['id']}], + } + if CONF.database.datastore_version and not datastore_version: + datastore_version = CONF.database.datastore_version + if datastore_version: + instance_dict["datastore"]["version"] = datastore_version + if backup_id: + instance_dict["restorePoint"] = {"backupRef": backup_id} + + post_body = json.dumps({'instance': instance_dict}) + instance = cls.client.create_db_instance(post_body)['instance'] + + # For each instance schedule wait and delete, so we first delete all + # and then wait for all + cls.addClassResourceCleanup( + waiters.wait_for_db_instance_decommission, + cls.database_instances_client, instance['id']) + + cls.addClassResourceCleanup( + test_utils.call_and_ignore_notfound_exc, + cls.database_instances_client.delete_db_instance, instance['id']) + + waiters.wait_for_db_instance_status(cls.client, instance['id'], + 'ACTIVE') + + return instance diff --git a/trove_tempest_plugin/tests/api/database/datastores/__init__.py b/trove_tempest_plugin/tests/api/database/datastores/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/tests/api/database/datastores/test_datastores.py b/trove_tempest_plugin/tests/api/database/datastores/test_datastores.py new file mode 100644 index 0000000..5a6bd36 --- /dev/null +++ b/trove_tempest_plugin/tests/api/database/datastores/test_datastores.py @@ -0,0 +1,33 @@ +# 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.api.database import base + + +class DatabaseDatastoresTest(base.BaseDatabaseTest): + + @classmethod + def resource_setup(cls): + super(DatabaseDatastoresTest, cls).resource_setup() + cls.client = cls.database_datastores_client + + @testtools.attr('smoke') + @decorators.idempotent_id('e4cdcadf-51bc-41ec-8cc6-530a3da08d10') + def test_datastores(self): + datastores = self.client.list_db_datastores()['datastores'] + self.assertTrue(len(datastores) > 0, "No available datastores found") diff --git a/trove_tempest_plugin/tests/api/database/flavors/__init__.py b/trove_tempest_plugin/tests/api/database/flavors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/tests/api/database/flavors/test_flavors.py b/trove_tempest_plugin/tests/api/database/flavors/test_flavors.py new file mode 100644 index 0000000..babf2d0 --- /dev/null +++ b/trove_tempest_plugin/tests/api/database/flavors/test_flavors.py @@ -0,0 +1,58 @@ +# 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.api.database import base + + +class DatabaseFlavorsTest(base.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_db_flavor(self.flavor_id) + ['flavor']) + self.assertEqual(self.flavor_id, str(flavor['str_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_db_flavor(self.flavor_id) + ['flavor']) + # List of all flavors should contain the expected flavor + flavors = self.client.list_db_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) diff --git a/trove_tempest_plugin/tests/api/database/flavors/test_flavors_negative.py b/trove_tempest_plugin/tests/api/database/flavors/test_flavors_negative.py new file mode 100644 index 0000000..2bbd0a4 --- /dev/null +++ b/trove_tempest_plugin/tests/api/database/flavors/test_flavors_negative.py @@ -0,0 +1,36 @@ +# 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.api.database import base + + +class DatabaseFlavorsNegativeTest(base.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_db_flavor, -1) diff --git a/trove_tempest_plugin/tests/api/database/instances/__init__.py b/trove_tempest_plugin/tests/api/database/instances/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/tests/api/database/instances/base.py b/trove_tempest_plugin/tests/api/database/instances/base.py new file mode 100644 index 0000000..1b1db2c --- /dev/null +++ b/trove_tempest_plugin/tests/api/database/instances/base.py @@ -0,0 +1,46 @@ +# Copyright 2019 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 exceptions as lib_exc + +from trove_tempest_plugin.common import waiters +from trove_tempest_plugin.tests.api.database import base + + +class WithInstanceBaseTest(base.BaseDatabaseTest): + + def setUp(self): + # Normally we use the same server with all test cases, + # but if it has an issue, we build a new one + super(WithInstanceBaseTest, self).setUp() + # Check if the server is in a clean state after test + try: + waiters.wait_for_db_instance_status(self.client, self.instance_id, + 'ACTIVE') + except lib_exc.NotFound: + instance = self.create_test_instance() + self.__class__.instance_id = instance['id'] + + @classmethod + def setup_clients(cls): + super(WithInstanceBaseTest, cls).setup_clients() + cls.client = cls.database_instances_client + + @classmethod + def resource_setup(cls, datastore_version=None): + super(WithInstanceBaseTest, cls).resource_setup() + instance = cls.create_test_instance( + datastore_version=datastore_version) + cls.instance_id = instance['id'] diff --git a/trove_tempest_plugin/tests/api/database/instances/test_instance_actions.py b/trove_tempest_plugin/tests/api/database/instances/test_instance_actions.py new file mode 100644 index 0000000..14bcfaa --- /dev/null +++ b/trove_tempest_plugin/tests/api/database/instances/test_instance_actions.py @@ -0,0 +1,119 @@ +# Copyright 2019 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 trove_tempest_plugin.common import utils +from trove_tempest_plugin.common import waiters +from trove_tempest_plugin.tests.api.database.instances import base + + +class InstanceActionsTest(base.WithInstanceBaseTest): + + @decorators.idempotent_id('ace549b3-eee0-4502-bf20-7594d4bf4856') + def test_restart_server(self): + self.client.restart_db_instance(self.instance_id) + waiters.wait_for_db_instance_status(self.client, self.instance_id, + 'ACTIVE') + + @decorators.idempotent_id('39eff887-008b-4a93-bead-6ddc969117c3') + def test_resize_server(self): + self.client.resize_db_instance(self.instance_id, + self.resize_flavor_id) + waiters.wait_for_db_instance_status(self.client, self.instance_id, + 'ACTIVE') + instance = self.client.show_db_instance(self.instance_id)['instance'] + self.assertEqual(self.resize_flavor_id, instance['flavor']['id']) + + @decorators.idempotent_id('bba1be08-409b-4916-b9a8-6ec03425cd6e') + def test_resize_volume(self): + new_size = 2 + self.client.resize_db_instance_volume(self.instance_id, new_size) + waiters.wait_for_db_instance_status(self.client, self.instance_id, + 'ACTIVE') + instance = self.client.show_db_instance(self.instance_id)['instance'] + self.assertEqual(new_size, instance['volume']['size']) + + @decorators.idempotent_id('215fbcbe-40d3-4a8f-9fe1-55ea9e8fb814') + def test_update_name(self): + new_name = 'new-name' + self.client.update_db_instance(self.instance_id, name=new_name) + waiters.wait_for_db_instance_status(self.client, self.instance_id, + 'ACTIVE') + + # Verify the name of the instance has changed + instance = self.client.show_db_instance(self.instance_id)['instance'] + self.assertEqual(new_name, instance['name']) + + @decorators.idempotent_id('38b5462a-308e-4cb8-9530-cc6741c95501') + def test_list_create_delete_database(self): + name = utils.rand_name() + self.client.create_database(self.instance_id, name=name) + databases = self.client.list_databases(self.instance_id)['databases'] + databases = [x['name'] for x in databases] + self.assertIn(name, databases) + self.client.delete_database(self.instance_id, name=name) + databases = self.client.list_databases(self.instance_id)['databases'] + databases = [x['name'] for x in databases] + self.assertNotIn(name, databases) + + @decorators.idempotent_id('bf4840fe-8cf8-46a1-8371-13021e87c690') + def test_enable_disable_root(self): + root_show = self.client.root_show(self.instance_id) + self.assertFalse(root_show['rootEnabled']) + root_enable = self.client.root_enable(self.instance_id) + self.assertIn('password', list(root_enable['user'].keys())) + # TODO(sorrison) Test connection with root user/password + root_show = self.client.root_show(self.instance_id) + self.assertTrue(root_show['rootEnabled']) + self.client.root_disable(self.instance_id) + root_show = self.client.root_show(self.instance_id) + # Show root show's if root as ever been enabled so disabling should + # have no impact + self.assertTrue(root_show['rootEnabled']) + + @decorators.idempotent_id('9f11d15b-9640-4c33-a7db-c78224763014') + def test_list_create_delete_user(self): + name = utils.rand_name() + self.client.create_user(self.instance_id, name=name, password='secret') + users = self.client.list_users(self.instance_id)['users'] + users = [x['name'] for x in users] + self.assertIn(name, users) + self.client.delete_user(self.instance_id, name=name) + users = self.client.list_users(self.instance_id)['users'] + users = [x['name'] for x in users] + self.assertNotIn(name, users) + + @decorators.idempotent_id('6f8b8350-f2a9-47b2-a108-8e3653cb9b57') + def test_grant_revoke_list_access(self): + user = utils.rand_name() + db = utils.rand_name() + self.client.create_user(self.instance_id, name=user, password='secret') + self.client.create_database(self.instance_id, name=db) + access = self.client.show_user_access(self.instance_id, user) + self.assertEqual([], access['databases']) + self.client.grant_user_access(self.instance_id, user, [db]) + access = self.client.show_user_access(self.instance_id, user) + access = [x['name'] for x in access['databases']] + self.assertIn(db, access) + self.client.revoke_user_access(self.instance_id, user, db) + access = self.client.show_user_access(self.instance_id, user) + access = [x['name'] for x in access['databases']] + self.assertNotIn(db, access) + + @decorators.idempotent_id('b13ff6fb-6214-416b-8aea-23dc3c24d00e') + def test_list_backups(self): + backups = self.client.list_backups(self.instance_id) + self.assertEqual([], backups['backups']) diff --git a/trove_tempest_plugin/tests/api/database/instances/test_instance_backups.py b/trove_tempest_plugin/tests/api/database/instances/test_instance_backups.py new file mode 100644 index 0000000..66bcfc7 --- /dev/null +++ b/trove_tempest_plugin/tests/api/database/instances/test_instance_backups.py @@ -0,0 +1,74 @@ +# 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.common.utils import test_utils +from tempest.lib import decorators + +from trove_tempest_plugin.common import utils +from trove_tempest_plugin.common import waiters +from trove_tempest_plugin.tests.api.database.instances import base + + +class InstanceBackupsTest(base.WithInstanceBaseTest): + + @classmethod + def setup_clients(cls): + super(InstanceBackupsTest, cls).setup_clients() + cls.backup_client = cls.database_backups_client + + def _add_cleanup(self, backup_id): + self.addClassResourceCleanup( + waiters.wait_for_backup_delete, + self.backup_client, backup_id) + + self.addClassResourceCleanup( + test_utils.call_and_ignore_notfound_exc, + self.backup_client.delete_backup, backup_id) + + @decorators.idempotent_id('e4c2bf6d-e619-4d9b-a79b-67f5463a8705') + def test_list_create_delete_backup(self): + name = utils.rand_name() + backup = self.backup_client.create_backup(self.instance_id, name) + backup_id = backup['backup']['id'] + waiters.wait_for_backup_status(self.backup_client, backup_id, + 'COMPLETED') + backups = self.backup_client.list_backups() + backup_ids = [x['id'] for x in backups['backups']] + self.assertIn(backup_id, backup_ids) + + self.backup_client.delete_backup(backup_id) + waiters.wait_for_backup_delete(self.backup_client, backup_id) + + backups = self.backup_client.list_backups() + backup_ids = [x['id'] for x in backups['backups']] + self.assertNotIn(backup_id, backup_ids) + + @decorators.idempotent_id('2ddab860-b487-481f-b89e-9ad06d5f6286') + def test_backup_incremental(self): + name = utils.rand_name() + backup = self.backup_client.create_backup(self.instance_id, name) + parent_id = backup['backup']['id'] + self._add_cleanup(parent_id) + waiters.wait_for_backup_status(self.backup_client, parent_id, + 'COMPLETED') + backup = self.backup_client.create_backup(self.instance_id, name, + incremental=True, + parent=parent_id) + backup_id = backup['backup']['id'] + self._add_cleanup(backup_id) + waiters.wait_for_backup_status(self.backup_client, backup_id, + 'COMPLETED') + backup = self.backup_client.show_backup(backup_id) + self.assertEqual(parent_id, backup['backup']['parent_id']) diff --git a/trove_tempest_plugin/tests/api/database/limits/__init__.py b/trove_tempest_plugin/tests/api/database/limits/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/tests/api/database/limits/test_limits.py b/trove_tempest_plugin/tests/api/database/limits/test_limits.py new file mode 100644 index 0000000..02c28f9 --- /dev/null +++ b/trove_tempest_plugin/tests/api/database/limits/test_limits.py @@ -0,0 +1,47 @@ +# 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.api.database import base + + +class DatabaseLimitsTest(base.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_db_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/database/versions/__init__.py b/trove_tempest_plugin/tests/api/database/versions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/tests/api/database/versions/test_versions.py b/trove_tempest_plugin/tests/api/database/versions/test_versions.py new file mode 100644 index 0000000..f12da1a --- /dev/null +++ b/trove_tempest_plugin/tests/api/database/versions/test_versions.py @@ -0,0 +1,41 @@ +# 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.api.database import base + + +class DatabaseVersionsTest(base.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_db_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 index a60bcb1..03988b2 100644 --- a/trove_tempest_plugin/tests/base.py +++ b/trove_tempest_plugin/tests/base.py @@ -355,10 +355,13 @@ class BaseTroveTest(test.BaseTestCase): raise loopingcall.LoopingCallDone() return + error_count = 0 if cur_status in expected_status: LOG.info('Instance %s becomes %s', id, cur_status) raise loopingcall.LoopingCallDone() elif "ERROR" not in expected_status and cur_status == "ERROR": + error_count += 1 + elif error_count > 10: # If instance status goes to ERROR but is not expected, stop # waiting message = "Instance status is ERROR." diff --git a/trove_tempest_plugin/tests/scenario/base_basic.py b/trove_tempest_plugin/tests/scenario/base_basic.py index 6bdac48..be3d29f 100644 --- a/trove_tempest_plugin/tests/scenario/base_basic.py +++ b/trove_tempest_plugin/tests/scenario/base_basic.py @@ -11,6 +11,8 @@ # 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 unittest + from oslo_log import log as logging from tempest import config from tempest.lib import decorators @@ -196,6 +198,7 @@ class TestInstanceBasicMySQLBase(TestInstanceBasicBase): LOG.info(f"Accessing database on {self.instance_ip}") self._access_db(self.instance_ip) + @unittest.skip("https://storyboard.openstack.org/#!/story/2008410") @decorators.idempotent_id("c5a9dcda-af5b-11ea-b87c-00224d6b7bc1") def test_user_database(self): db1 = 'foo'