Only enable user and database API for MySQL

Change-Id: Ic59f2fd94c5f216414effe7d13d0dd486dce9243
This commit is contained in:
Lingxian Kong 2020-09-11 13:18:02 +12:00
parent df4f74769e
commit 72e20e4e97
8 changed files with 94 additions and 303 deletions

View File

@ -4,7 +4,10 @@
Databases 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 Create database

View File

@ -4,6 +4,10 @@
Users 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 Create user

View File

@ -1,16 +1,16 @@
.. _create_db: .. _create_db:
============================ =====================================
Create and access a database Create and access a database instance
============================ =====================================
Assume that you have installed the Database service and populated your Assume that you have installed the Database service and populated your
data store with images for the type and versions of databases that you 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. 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** #. **Determine which flavor to use for your database**

View File

@ -2,60 +2,70 @@
Manage databases and users on Trove instances Manage databases and users on Trove instances
============================================= =============================================
Assume that you installed Trove service and uploaded images with datastore Assume that you installed Trove service and uploaded images with datastore of
of your choice. your choice. This section shows how to manage users and databases in a MySQL
This section shows how to manage users and databases in a MySQL 5.7 instance. 5.7 instance.
Add new database and user to an existing Trove instance .. warning::
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Trove provides API to manage users and databases on Currently, the Database and User API is only supported by mysql datastore.
datastores including relational (e.g. MySQL, PostgreSQL) and non-relational For others, the recommended way is to get root password (``POST
(e.g. Redis, Cassandra). Once a Trove instance with a datastore of choice is /v1.0/{project_id}/instances/{instance_id}/root``) and communicate with the
active you can use Trove API to create new databases and/or users. 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 root enable f22ce0d9-8c9c-403a-8599-2269761a66de
+----------+--------------------------------------+
| Field | Value |
+----------+--------------------------------------+
| name | root |
| password | I5nPpBj1qf1eGR1idQorj1szppXGpYyYNj4h |
+----------+--------------------------------------+
If needed, ``openstack database root disable <instance_id>`` 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 .. code-block:: console
$ openstack database user list db-instance $ openstack database user list db-instance
+------+------+-----------+ +------+------+-----------+
| Name | Host | Databases | | Name | Host | Databases |
+------+------+-----------+ +------+------+-----------+
| test | % | testdb | | test | % | testdb |
+------+------+-----------+ +------+------+-----------+
$ openstack database user create db-instance newuser userpass --databases testdb $ openstack database user create db-instance newuser userpass --databases testdb
$ openstack database user list db-instance $ openstack database user list db-instance
+---------+------+-----------+ +---------+------+-----------+
| Name | Host | Databases | | Name | Host | Databases |
+---------+------+-----------+ +---------+------+-----------+
| newuser | % | testdb | | newuser | % | testdb |
| test | % | 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: Enter password:
mysql> show databases; mysql> show databases;
+--------------------+ +--------------------+
| Database | | Database |
+--------------------+ +--------------------+
| information_schema | | information_schema |
| testdb | | testdb |
+--------------------+ +--------------------+
2 rows in set (0.00 sec) 2 rows in set (0.00 sec)
$ openstack database db create db-instance newdb $ openstack database db create db-instance newdb
$ openstack database db list db-instance $ openstack database db list db-instance
+--------+ +--------+
| Name | | Name |
+--------+ +--------+
@ -65,7 +75,6 @@ active you can use Trove API to create new databases and/or users.
+--------+ +--------+
$ mysql -h 172.24.4.199 -u newuser -p newdb $ mysql -h 172.24.4.199 -u newuser -p newdb
Enter password: Enter password:
ERROR 1044 (42000): Access denied for user 'newuser'@'%' to database 'newdb' 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 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 .. code-block:: console
$ openstack database user grant access db-instance newuser newdb $ openstack database user grant access db-instance newuser newdb
$ openstack database user show access db-instance newuser $ openstack database user show access db-instance newuser
+--------+ +--------+
| Name | | Name |
+--------+ +--------+
@ -92,9 +100,7 @@ With Trove API you can grant and revoke database access rights for existing user
$ mysql -h IP_ADDRESS -u newuser -p newdb $ mysql -h IP_ADDRESS -u newuser -p newdb
Enter password: Enter password:
$ openstack database user show access db-instance test $ openstack database user show access db-instance test
+--------+ +--------+
| Name | | Name |
+--------+ +--------+
@ -103,16 +109,12 @@ With Trove API you can grant and revoke database access rights for existing user
$ mysql -h IP_ADDRESS -u test -p newdb $ mysql -h IP_ADDRESS -u test -p newdb
Enter password: Enter password:
ERROR 1044 (42000): Access denied for user 'test'@'%' to database 'newdb' 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: Enter password:
ERROR 1044 (42000): Access denied for user 'newuser'@'%' to database 'newdb' ERROR 1044 (42000): Access denied for user 'newuser'@'%' to database 'newdb'
@ -124,7 +126,6 @@ Lastly, Trove provides API for deleting databases.
.. code-block:: console .. code-block:: console
$ openstack database db list db-instance $ openstack database db list db-instance
+--------+ +--------+
| Name | | Name |
+--------+ +--------+
@ -136,7 +137,6 @@ Lastly, Trove provides API for deleting databases.
$ openstack database db delete db-instance testdb $ openstack database db delete db-instance testdb
$ openstack database db list db-instance $ openstack database db list db-instance
+--------+ +--------+
| Name | | Name |
+--------+ +--------+
@ -146,5 +146,4 @@ Lastly, Trove provides API for deleting databases.
$ mysql -h IP_ADDRESS -u test -p testdb $ mysql -h IP_ADDRESS -u test -p testdb
Enter password: Enter password:
ERROR 1049 (42000): Unknown database 'testdb' ERROR 1049 (42000): Unknown database 'testdb'

View File

@ -26,15 +26,25 @@ from trove.instance import models as base_models
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
def load_and_verify(context, instance_id): def load_and_verify(context, instance_id,
# Load InstanceServiceStatus to verify if its running 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) 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: if not instance.is_datastore_running:
raise exception.UnprocessableEntity( raise exception.UnprocessableEntity(
"Instance %s is not ready, status: %s." % "Instance %s is not ready, status: %s." %
(instance.id, instance.datastore_status.status) (instance.id, instance.datastore_status.status)
) )
else:
return instance return instance
@ -42,7 +52,8 @@ class Root(object):
@classmethod @classmethod
def load(cls, context, instance_id): 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, # TODO(pdmars): remove the is_root_enabled call from the guest agent,
# just check the database for this information. # just check the database for this information.
# If the root history returns null or raises an exception, the root # If the root history returns null or raises an exception, the root
@ -58,7 +69,8 @@ class Root(object):
@classmethod @classmethod
def create(cls, context, instance_id, root_password, def create(cls, context, instance_id, root_password,
cluster_instances_list=None): cluster_instances_list=None):
load_and_verify(context, instance_id) load_and_verify(context, instance_id,
enabled_datastore=['mysql', 'mariadb', 'postgresql'])
if root_password: if root_password:
root = create_guest_client(context, root = create_guest_client(context,
instance_id).enable_root_with_password( instance_id).enable_root_with_password(
@ -79,7 +91,8 @@ class Root(object):
@classmethod @classmethod
def delete(cls, context, instance_id): 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() create_guest_client(context, instance_id).disable_root()

View File

@ -260,7 +260,15 @@ class RootController(ExtensionController):
try: try:
clazz = CONF.get(manager).get('root_controller') clazz = CONF.get(manager).get('root_controller')
LOG.debug("Loading Root Controller class %s.", clazz) LOG.debug("Loading Root Controller class %s.", clazz)
if not clazz:
raise exception.DatastoreOperationNotSupported(
datastore=manager, operation='root')
root_controller = import_class(clazz) root_controller = import_class(clazz)
return root_controller() return root_controller()
except NoSuchOptError: except NoSuchOptError:
return None LOG.warning(
f"root_controller not configured for datastore {manager}")
raise exception.DatastoreOperationNotSupported(
datastore=manager, operation='root')

View File

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