diff --git a/requirements.txt b/requirements.txt index ea1c2fc..0e6ac0f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,5 @@ 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 +SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT +PyMySQL>=0.7.6 # MIT License \ No newline at end of file diff --git a/trove_tempest_plugin/config.py b/trove_tempest_plugin/config.py index 69a5212..895ac00 100644 --- a/trove_tempest_plugin/config.py +++ b/trove_tempest_plugin/config.py @@ -38,6 +38,10 @@ DatabaseGroup = [ 'internalURL'], help="The endpoint type to use for the Database service." ), + cfg.ListOpt( + 'enabled_datastores', + default=['mysql'] + ), cfg.IntOpt('database_build_timeout', default=1800, help='Timeout in seconds to wait for a database instance to ' @@ -47,6 +51,12 @@ DatabaseGroup = [ default="d2", help="The Nova flavor ID used for creating database instance." ), + cfg.StrOpt( + 'shared_network', + default="private", + help=('Pre-defined network name or ID used for creating database ' + 'instance.') + ), cfg.StrOpt( 'subnet_cidr', default='10.1.1.0/24', @@ -58,6 +68,4 @@ DatabaseGroup = [ default="lvmdriver-1", help="The Cinder volume type used for creating database instance." ), - cfg.StrOpt('datastore_type', default="mysql"), - cfg.StrOpt('datastore_version', default="5.7"), ] diff --git a/trove_tempest_plugin/tests/base.py b/trove_tempest_plugin/tests/base.py index b22ea83..bf1cbbb 100644 --- a/trove_tempest_plugin/tests/base.py +++ b/trove_tempest_plugin/tests/base.py @@ -14,6 +14,7 @@ # under the License. from oslo_log import log as logging from oslo_service import loopingcall +from oslo_utils import uuidutils import tenacity from tempest import config @@ -30,6 +31,7 @@ LOG = logging.getLogger(__name__) class BaseTroveTest(test.BaseTestCase): credentials = ('admin', 'primary') + datastore = None @classmethod def get_resource_name(cls, resource_type): @@ -43,6 +45,11 @@ class BaseTroveTest(test.BaseTestCase): if not CONF.service_available.trove: raise cls.skipException("Database service is not available.") + if cls.datastore not in CONF.database.enabled_datastores: + raise cls.skipException( + "Datastore %s is not enabled." % cls.datastore + ) + @classmethod def setup_clients(cls): super(BaseTroveTest, cls).setup_clients() @@ -103,6 +110,22 @@ class BaseTroveTest(test.BaseTestCase): subnets_client = cls.os_primary.subnets_client routers_client = cls.os_primary.routers_client + if CONF.database.shared_network: + private_network = CONF.database.shared_network + if not uuidutils.is_uuid_like(private_network): + networks = networks_client.list_networks()['networks'] + for net in networks: + if net['name'] == private_network: + private_network = net['id'] + break + else: + raise exceptions.NotFound( + 'Shared network %s not found' % private_network + ) + + cls.private_network = private_network + return + network_kwargs = {"name": cls.get_resource_name("network")} result = networks_client.create_network(**network_kwargs) LOG.info('Private network created: %s', result['network']) @@ -163,6 +186,9 @@ class BaseTroveTest(test.BaseTestCase): # network ID. cls._create_network() + cls.instance_id = cls.create_instance() + cls.wait_for_instance_status(cls.instance_id) + @classmethod def create_instance(cls, database="test_db", username="test_user", password="password"): @@ -175,6 +201,17 @@ class BaseTroveTest(test.BaseTestCase): all test methods within a TestCase are assumed to be executed serially. """ name = cls.get_resource_name("instance") + + # Get datastore version + res = cls.client.list_resources("datastores") + for d in res['datastores']: + if d['name'] == cls.datastore: + if d.get('default_version'): + datastore_version = d['default_version'] + else: + datastore_version = d['versions'][0]['name'] + break + body = { "instance": { "name": name, @@ -195,8 +232,8 @@ class BaseTroveTest(test.BaseTestCase): } ], "datastore": { - "type": CONF.database.datastore_type, - "version": CONF.database.datastore_version + "type": cls.datastore, + "version": datastore_version }, "nics": [ { @@ -220,10 +257,12 @@ class BaseTroveTest(test.BaseTestCase): res = cls.client.get_resource("instances", id) except exceptions.NotFound: if need_delete or status == "DELETED": + LOG.info('Instance %s is deleted', id) raise loopingcall.LoopingCallDone() return if res["instance"]["status"] == status: + LOG.info('Instance %s becomes %s', id, status) raise loopingcall.LoopingCallDone() elif status != "ERROR" and res["instance"]["status"] == "ERROR": # If instance status goes to ERROR but is not expected, stop diff --git a/trove_tempest_plugin/tests/scenario/mysql/__init__.py b/trove_tempest_plugin/tests/scenario/mysql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/trove_tempest_plugin/tests/scenario/mysql/test_instance_basic.py b/trove_tempest_plugin/tests/scenario/mysql/test_instance_basic.py new file mode 100644 index 0000000..a4b2562 --- /dev/null +++ b/trove_tempest_plugin/tests/scenario/mysql/test_instance_basic.py @@ -0,0 +1,53 @@ +# 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 oslo_utils import netutils +from tempest.lib import decorators + +from trove_tempest_plugin.tests import base +from trove_tempest_plugin.tests import utils + +LOG = logging.getLogger(__name__) + + +class TestMySQLInstanceBasic(base.BaseTroveTest): + datastore = 'mysql' + + def _access_db(self, ip, username='test_user', password='password'): + db_engine = utils.LocalSqlClient.init_engine(ip, username, password) + db_client = utils.LocalSqlClient(db_engine) + + LOG.info('Trying to access the database %s', ip) + + with db_client: + cmd = "SELECT 1;" + db_client.execute(cmd) + + @decorators.idempotent_id("40cf38ce-cfbf-11e9-8760-1458d058cfb2") + def test_database_access(self): + res = self.client.get_resource("instances", self.instance_id) + ips = res["instance"].get('ip', []) + + # TODO(lxkong): IPv6 needs to be supported. + v4_ip = None + for ip in ips: + if netutils.is_valid_ipv4(ip): + v4_ip = ip + break + + self.assertIsNotNone(v4_ip) + time.sleep(5) + self._access_db(v4_ip) diff --git a/trove_tempest_plugin/tests/scenario/test_instance_basic.py b/trove_tempest_plugin/tests/scenario/test_instance_basic.py deleted file mode 100644 index 5876eaf..0000000 --- a/trove_tempest_plugin/tests/scenario/test_instance_basic.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2019 Catalyst Cloud Ltd. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from tempest.lib import decorators - -from trove_tempest_plugin.tests import base - - -class TestInstanceBasic(base.BaseTroveTest): - @classmethod - def resource_setup(cls): - super(TestInstanceBasic, cls).resource_setup() - - cls.instance_id = cls.create_instance() - cls.wait_for_instance_status(cls.instance_id) - - @decorators.idempotent_id("40cf38ce-cfbf-11e9-8760-1458d058cfb2") - def test_database_access(self): - pass diff --git a/trove_tempest_plugin/tests/utils.py b/trove_tempest_plugin/tests/utils.py index 512edab..ff87c55 100644 --- a/trove_tempest_plugin/tests/utils.py +++ b/trove_tempest_plugin/tests/utils.py @@ -14,7 +14,7 @@ import time from oslo_log import log as logging - +import sqlalchemy from tempest.lib import exceptions LOG = logging.getLogger(__name__) @@ -49,3 +49,41 @@ def wait_for_removal(delete_func, show_func, *args, **kwargs): (show_func.__name__, check_timeout)) raise exceptions.TimeoutException(message) time.sleep(3) + + +class LocalSqlClient(object): + """A sqlalchemy wrapper to manage transactions.""" + + def __init__(self, engine): + self.engine = engine + + def __enter__(self): + self.conn = self.engine.connect() + self.trans = self.conn.begin() + return self.conn + + def __exit__(self, type, value, traceback): + if self.trans: + if type is not None: + self.trans.rollback() + else: + self.trans.commit() + self.conn.close() + + def execute(self, t, **kwargs): + try: + return self.conn.execute(t, kwargs) + except Exception as e: + self.trans.rollback() + self.trans = None + raise exceptions.TempestException( + 'Failed to execute database command %s, error: %s' % + (t, str(e)) + ) + + @staticmethod + def init_engine(host, user, password): + return sqlalchemy.create_engine( + "mysql+pymysql://%s:%s@%s:3306" % (user, password, host), + pool_recycle=1800 + )