Refactor trove-tempest-plugin

Depends-On: https://review.opendev.org/#/c/697870/
Story: 2006554
Task: 36639
Change-Id: I6251f070f330ee886e6436d92c20d78e0401d59e
This commit is contained in:
Lingxian Kong 2019-09-05 00:34:39 +12:00
parent eda92dae2d
commit 605a0d3a51
22 changed files with 511 additions and 528 deletions

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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"),
]

View File

@ -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]

View File

@ -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))

View File

@ -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'
]

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)