diff --git a/api-ref/source/databases.inc b/api-ref/source/databases.inc index c7c1df77f7..e0d91ef7a7 100644 --- a/api-ref/source/databases.inc +++ b/api-ref/source/databases.inc @@ -4,7 +4,10 @@ Databases ========= - +Currently, the Database and User API is only supported by mysql datastore. For +others, the recommended way is to get root password +(``POST /v1.0/{project_id}/instances/{instance_id}/root``) and communicate with +the database service directly for database and user management. Create database diff --git a/api-ref/source/users.inc b/api-ref/source/users.inc index 686a84fddd..91be9969af 100644 --- a/api-ref/source/users.inc +++ b/api-ref/source/users.inc @@ -4,6 +4,10 @@ Users ===== +Currently, the Database and User API is only supported by mysql datastore. For +others, the recommended way is to get root password +(``POST /v1.0/{project_id}/instances/{instance_id}/root``) and communicate with +the database service directly for database and user management. Create user diff --git a/doc/source/user/create-db.rst b/doc/source/user/create-db.rst index b8446b84e4..6118ba3936 100644 --- a/doc/source/user/create-db.rst +++ b/doc/source/user/create-db.rst @@ -1,16 +1,16 @@ .. _create_db: -============================ -Create and access a database -============================ +===================================== +Create and access a database instance +===================================== Assume that you have installed the Database service and populated your data store with images for the type and versions of databases that you -want, and that you can create and access a database. +want, and that you can create and access a database instance. This example shows you how to create and access a MySQL 5.7 database. -Create and access a database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Create and access a database instance +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #. **Determine which flavor to use for your database** diff --git a/doc/source/user/manage-db-and-users.rst b/doc/source/user/manage-db-and-users.rst index 38a80442dc..ad25b1d8b2 100644 --- a/doc/source/user/manage-db-and-users.rst +++ b/doc/source/user/manage-db-and-users.rst @@ -2,60 +2,70 @@ Manage databases and users on Trove instances ============================================= -Assume that you installed Trove service and uploaded images with datastore -of your choice. -This section shows how to manage users and databases in a MySQL 5.7 instance. +Assume that you installed Trove service and uploaded images with datastore of +your choice. This section shows how to manage users and databases in a MySQL +5.7 instance. -Add new database and user to an existing Trove instance -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. warning:: -Trove provides API to manage users and databases on -datastores including relational (e.g. MySQL, PostgreSQL) and non-relational -(e.g. Redis, Cassandra). Once a Trove instance with a datastore of choice is -active you can use Trove API to create new databases and/or users. + Currently, the Database and User API is only supported by mysql datastore. + For others, the recommended way is to get root password (``POST + /v1.0/{project_id}/instances/{instance_id}/root``) and communicate with the + database service directly for database and user management. + +Manage root user +~~~~~~~~~~~~~~~~ + +For all the datastores, the user could enable root and get root password for +further database operations. .. code-block:: console - $ openstack database user list db-instance + $ openstack database root enable f22ce0d9-8c9c-403a-8599-2269761a66de + +----------+--------------------------------------+ + | Field | Value | + +----------+--------------------------------------+ + | name | root | + | password | I5nPpBj1qf1eGR1idQorj1szppXGpYyYNj4h | + +----------+--------------------------------------+ +If needed, ``openstack database root disable `` command could +disable the root user. + +Database and User management via Trove API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Trove provides API to manage users and databases for mysql datastore. + +.. code-block:: console + + $ openstack database user list db-instance +------+------+-----------+ | Name | Host | Databases | +------+------+-----------+ | test | % | testdb | +------+------+-----------+ - - $ openstack database user create db-instance newuser userpass --databases testdb - - $ openstack database user list db-instance - + $ openstack database user create db-instance newuser userpass --databases testdb + $ openstack database user list db-instance +---------+------+-----------+ | Name | Host | Databases | +---------+------+-----------+ | newuser | % | testdb | | test | % | testdb | +---------+------+-----------+ - - - $ mysql -h 172.24.4.199 -u newuser -p testdb + $ mysql -h 172.24.4.199 -u newuser -p testdb Enter password: - mysql> show databases; - +--------------------+ | Database | +--------------------+ | information_schema | | testdb | +--------------------+ - 2 rows in set (0.00 sec) - - $ openstack database db create db-instance newdb - - - $ openstack database db list db-instance - + $ openstack database db create db-instance newdb + $ openstack database db list db-instance +--------+ | Name | +--------+ @@ -64,8 +74,7 @@ active you can use Trove API to create new databases and/or users. | testdb | +--------+ - $ mysql -h 172.24.4.199 -u newuser -p newdb - + $ mysql -h 172.24.4.199 -u newuser -p newdb Enter password: ERROR 1044 (42000): Access denied for user 'newuser'@'%' to database 'newdb' @@ -73,15 +82,14 @@ active you can use Trove API to create new databases and/or users. Manage access to databases ~~~~~~~~~~~~~~~~~~~~~~~~~~ -With Trove API you can grant and revoke database access rights for existing users. +With Trove API you can grant and revoke database access rights for existing +users. .. code-block:: console - $ openstack database user grant access db-instance newuser newdb - - - $ openstack database user show access db-instance newuser + $ openstack database user grant access db-instance newuser newdb + $ openstack database user show access db-instance newuser +--------+ | Name | +--------+ @@ -89,30 +97,24 @@ With Trove API you can grant and revoke database access rights for existing user | testdb | +--------+ - $ mysql -h IP_ADDRESS -u newuser -p newdb + $ mysql -h IP_ADDRESS -u newuser -p newdb Enter password: - - $ openstack database user show access db-instance test - + $ openstack database user show access db-instance test +--------+ | Name | +--------+ | testdb | +--------+ - $ mysql -h IP_ADDRESS -u test -p newdb + $ mysql -h IP_ADDRESS -u test -p newdb Enter password: - ERROR 1044 (42000): Access denied for user 'test'@'%' to database 'newdb' + $ openstack database user revoke access db-instance newuser newdb - $ openstack database user revoke access db-instance newuser newdb - - - $ mysql -h IP_ADDRESS -u newuser -p newdb + $ mysql -h IP_ADDRESS -u newuser -p newdb Enter password: - ERROR 1044 (42000): Access denied for user 'newuser'@'%' to database 'newdb' @@ -123,8 +125,7 @@ Lastly, Trove provides API for deleting databases. .. code-block:: console - $ openstack database db list db-instance - + $ openstack database db list db-instance +--------+ | Name | +--------+ @@ -133,10 +134,9 @@ Lastly, Trove provides API for deleting databases. | testdb | +--------+ - $ openstack database db delete db-instance testdb - - $ openstack database db list db-instance + $ openstack database db delete db-instance testdb + $ openstack database db list db-instance +--------+ | Name | +--------+ @@ -144,7 +144,6 @@ Lastly, Trove provides API for deleting databases. | sys | +--------+ - $ mysql -h IP_ADDRESS -u test -p testdb + $ mysql -h IP_ADDRESS -u test -p testdb Enter password: - ERROR 1049 (42000): Unknown database 'testdb' \ No newline at end of file diff --git a/trove/extensions/common/models.py b/trove/extensions/common/models.py index 81f202c24e..34079cd128 100644 --- a/trove/extensions/common/models.py +++ b/trove/extensions/common/models.py @@ -26,23 +26,34 @@ from trove.instance import models as base_models LOG = logging.getLogger(__name__) -def load_and_verify(context, instance_id): - # Load InstanceServiceStatus to verify if its running +def load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb']): + """Check instance datastore. + + Some API operations are only supported for some specific datastores. + """ instance = base_models.Instance.load(context, instance_id) + + if instance.datastore.name not in enabled_datastore: + raise exception.UnprocessableEntity( + f"Operation not supported for datastore {instance.datastore.name}." + ) + if not instance.is_datastore_running: raise exception.UnprocessableEntity( "Instance %s is not ready, status: %s." % (instance.id, instance.datastore_status.status) ) - else: - return instance + + return instance class Root(object): @classmethod def load(cls, context, instance_id): - load_and_verify(context, instance_id) + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) # TODO(pdmars): remove the is_root_enabled call from the guest agent, # just check the database for this information. # If the root history returns null or raises an exception, the root @@ -58,7 +69,8 @@ class Root(object): @classmethod def create(cls, context, instance_id, root_password, cluster_instances_list=None): - load_and_verify(context, instance_id) + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) if root_password: root = create_guest_client(context, instance_id).enable_root_with_password( @@ -79,7 +91,8 @@ class Root(object): @classmethod def delete(cls, context, instance_id): - load_and_verify(context, instance_id) + load_and_verify(context, instance_id, + enabled_datastore=['mysql', 'mariadb', 'postgresql']) create_guest_client(context, instance_id).disable_root() diff --git a/trove/extensions/common/service.py b/trove/extensions/common/service.py index 5b8bf16191..d645de4d71 100644 --- a/trove/extensions/common/service.py +++ b/trove/extensions/common/service.py @@ -260,7 +260,15 @@ class RootController(ExtensionController): try: clazz = CONF.get(manager).get('root_controller') LOG.debug("Loading Root Controller class %s.", clazz) + + if not clazz: + raise exception.DatastoreOperationNotSupported( + datastore=manager, operation='root') + root_controller = import_class(clazz) return root_controller() except NoSuchOptError: - return None + LOG.warning( + f"root_controller not configured for datastore {manager}") + raise exception.DatastoreOperationNotSupported( + datastore=manager, operation='root') diff --git a/trove/tests/unittests/extensions/redis/__init__.py b/trove/tests/unittests/extensions/redis/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/trove/tests/unittests/extensions/redis/test_service.py b/trove/tests/unittests/extensions/redis/test_service.py deleted file mode 100644 index 0b8b60aa12..0000000000 --- a/trove/tests/unittests/extensions/redis/test_service.py +++ /dev/null @@ -1,236 +0,0 @@ -# Copyright 2017 Eayun, Inc. -# 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 uuid - -from unittest.mock import Mock, patch - -from trove.common import exception -from trove.datastore import models as datastore_models -from trove.extensions.common import models -from trove.extensions.redis.models import RedisRoot -from trove.extensions.redis.service import RedisRootController -from trove.instance import models as instance_models -from trove.instance.models import DBInstance -from trove.instance.tasks import InstanceTasks -from trove.taskmanager import api as task_api -from trove.tests.unittests import trove_testtools -from trove.tests.unittests.util import util - - -class TestRedisRootController(trove_testtools.TestCase): - - @patch.object(task_api.API, 'get_client', Mock(return_value=Mock())) - def setUp(self): - util.init_db() - self.context = trove_testtools.TroveTestContext(self, is_admin=True) - self.datastore = datastore_models.DBDatastore.create( - id=str(uuid.uuid4()), - name='redis' + str(uuid.uuid4()), - ) - self.datastore_version = ( - datastore_models.DBDatastoreVersion.create( - id=str(uuid.uuid4()), - datastore_id=self.datastore.id, - name="3.2" + str(uuid.uuid4()), - manager="redis", - image_id="image_id", - packages="", - active=True)) - self.tenant_id = "UUID" - self.single_db_info = DBInstance.create( - id="redis-single", - name="redis-single", - flavor_id=1, - datastore_version_id=self.datastore_version.id, - tenant_id=self.tenant_id, - volume_size=None, - task_status=InstanceTasks.NONE) - self.master_db_info = DBInstance.create( - id="redis-master", - name="redis-master", - flavor_id=1, - datastore_version_id=self.datastore_version.id, - tenant_id=self.tenant_id, - volume_size=None, - task_status=InstanceTasks.NONE) - self.slave_db_info = DBInstance.create( - id="redis-slave", - name="redis-slave", - flavor_id=1, - datastore_version_id=self.datastore_version.id, - tenant_id=self.tenant_id, - volume_size=None, - task_status=InstanceTasks.NONE, - slave_of_id=self.master_db_info.id) - - super(TestRedisRootController, self).setUp() - self.controller = RedisRootController() - - def tearDown(self): - self.datastore.delete() - self.datastore_version.delete() - self.master_db_info.delete() - self.slave_db_info.delete() - super(TestRedisRootController, self).tearDown() - - @patch.object(instance_models.Instance, "load") - @patch.object(models.Root, "create") - def test_root_create_on_single_instance(self, root_create, *args): - user = Mock() - context = Mock() - context.user = Mock() - context.user.__getitem__ = Mock(return_value=user) - req = Mock() - req.environ = Mock() - req.environ.__getitem__ = Mock(return_value=context) - tenant_id = self.tenant_id - instance_id = self.single_db_info.id - is_cluster = False - password = Mock() - body = {"password": password} - self.controller.root_create(req, body, tenant_id, - instance_id, is_cluster) - root_create.assert_called_with(context, instance_id, - password) - - @patch.object(instance_models.Instance, "load") - @patch.object(models.Root, "create") - def test_root_create_on_master_instance(self, root_create, *args): - user = Mock() - context = Mock() - context.user = Mock() - context.user.__getitem__ = Mock(return_value=user) - req = Mock() - req.environ = Mock() - req.environ.__getitem__ = Mock(return_value=context) - tenant_id = self.tenant_id - instance_id = self.master_db_info.id - slave_instance_id = self.slave_db_info.id - is_cluster = False - password = Mock() - body = {"password": password} - self.controller.root_create(req, body, tenant_id, - instance_id, is_cluster) - root_create.assert_called_with(context, slave_instance_id, - password) - - def test_root_create_on_slave(self): - user = Mock() - context = Mock() - context.user = Mock() - context.user.__getitem__ = Mock(return_value=user) - req = Mock() - req.environ = Mock() - req.environ.__getitem__ = Mock(return_value=context) - tenant_id = self.tenant_id - instance_id = self.slave_db_info.id - is_cluster = False - body = {} - self.assertRaises( - exception.SlaveOperationNotSupported, - self.controller.root_create, - req, body, tenant_id, instance_id, is_cluster) - - def test_root_create_with_cluster(self): - req = Mock() - tenant_id = self.tenant_id - instance_id = self.master_db_info.id - is_cluster = True - body = {} - self.assertRaises( - exception.ClusterOperationNotSupported, - self.controller.root_create, - req, body, tenant_id, instance_id, is_cluster) - - @patch.object(instance_models.Instance, "load") - @patch.object(RedisRoot, "get_auth_password") - @patch.object(models.Root, "delete") - @patch.object(models.Root, "load") - def test_root_delete_on_single_instance(self, root_load, - root_delete, *args): - context = Mock() - req = Mock() - req.environ = Mock() - req.environ.__getitem__ = Mock(return_value=context) - tenant_id = self.tenant_id - instance_id = self.single_db_info.id - is_cluster = False - root_load.return_value = True - self.controller.root_delete(req, tenant_id, instance_id, is_cluster) - root_load.assert_called_with(context, instance_id) - root_delete.assert_called_with(context, instance_id) - - @patch.object(instance_models.Instance, "load") - @patch.object(RedisRoot, "get_auth_password") - @patch.object(models.Root, "delete") - @patch.object(models.Root, "load") - def test_root_delete_on_master_instance(self, root_load, - root_delete, *args): - context = Mock() - req = Mock() - req.environ = Mock() - req.environ.__getitem__ = Mock(return_value=context) - tenant_id = self.tenant_id - instance_id = self.master_db_info.id - slave_instance_id = self.slave_db_info.id - is_cluster = False - root_load.return_value = True - self.controller.root_delete(req, tenant_id, instance_id, is_cluster) - root_load.assert_called_with(context, instance_id) - root_delete.assert_called_with(context, slave_instance_id) - - def test_root_delete_on_slave(self): - context = Mock() - req = Mock() - req.environ = Mock() - req.environ.__getitem__ = Mock(return_value=context) - tenant_id = self.tenant_id - instance_id = self.slave_db_info.id - is_cluster = False - self.assertRaises( - exception.SlaveOperationNotSupported, - self.controller.root_delete, - req, tenant_id, instance_id, is_cluster) - - def test_root_delete_with_cluster(self): - req = Mock() - tenant_id = self.tenant_id - instance_id = self.master_db_info.id - is_cluster = True - self.assertRaises( - exception.ClusterOperationNotSupported, - self.controller.root_delete, - req, tenant_id, instance_id, is_cluster) - - @patch.object(instance_models.Instance, "load") - @patch.object(models.Root, "delete") - @patch.object(models.Root, "load") - def test_root_delete_without_root_enabled(self, root_load, - root_delete, *args): - context = Mock() - req = Mock() - req.environ = Mock() - req.environ.__getitem__ = Mock(return_value=context) - tenant_id = self.tenant_id - instance_id = self.single_db_info.id - is_cluster = False - root_load.return_value = False - self.assertRaises( - exception.RootHistoryNotFound, - self.controller.root_delete, - req, tenant_id, instance_id, is_cluster) - root_load.assert_called_with(context, instance_id) - root_delete.assert_not_called()