summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--etc/reddwarf/reddwarf.conf.test1
-rw-r--r--reddwarf/common/exception.py10
-rw-r--r--reddwarf/common/wsgi.py2
-rw-r--r--reddwarf/extensions/mysql.py18
-rw-r--r--reddwarf/extensions/mysql/models.py56
-rw-r--r--reddwarf/extensions/mysql/service.py85
-rw-r--r--reddwarf/extensions/mysql/views.py9
-rw-r--r--reddwarf/guestagent/api.py28
-rw-r--r--reddwarf/guestagent/dbaas.py236
-rw-r--r--reddwarf/guestagent/manager.py15
-rw-r--r--reddwarf/guestagent/query.py337
-rw-r--r--reddwarf/taskmanager/api.py2
-rw-r--r--reddwarf/tests/api/user_access.py199
-rw-r--r--reddwarf/tests/api/users.py23
-rw-r--r--reddwarf/tests/fakes/guestagent.py40
-rw-r--r--reddwarf/tests/unittests/guestagent/test_dbaas.py207
-rw-r--r--reddwarf/tests/util/check.py2
-rw-r--r--run_tests.py1
-rw-r--r--tools/test-requires2
19 files changed, 1072 insertions, 201 deletions
diff --git a/etc/reddwarf/reddwarf.conf.test b/etc/reddwarf/reddwarf.conf.test
index 2ab4bd3d..5bf787b8 100644
--- a/etc/reddwarf/reddwarf.conf.test
+++ b/etc/reddwarf/reddwarf.conf.test
@@ -1,6 +1,7 @@
[DEFAULT]
remote_implementation = fake
+fake_mode_events = eventlet
log_file = rdtest.log
diff --git a/reddwarf/common/exception.py b/reddwarf/common/exception.py
index df23a487..e1e43775 100644
--- a/reddwarf/common/exception.py
+++ b/reddwarf/common/exception.py
@@ -65,6 +65,16 @@ class FlavorNotFound(ReddwarfError):
message = _("Resource %(uuid)s cannot be found")
+class UserNotFound(NotFound):
+
+ message = _("User %(uuid)s cannot be found on the instance.")
+
+
+class DatabaseNotFound(NotFound):
+
+ message = _("Database %(uuid)s cannot be found on the instance.")
+
+
class ComputeInstanceNotFound(NotFound):
internal_message = _("Cannot find compute instance %(server_id)s for "
diff --git a/reddwarf/common/wsgi.py b/reddwarf/common/wsgi.py
index 16b7b8ab..775cdc8a 100644
--- a/reddwarf/common/wsgi.py
+++ b/reddwarf/common/wsgi.py
@@ -317,6 +317,8 @@ class Controller(object):
exception.NotFound,
exception.ComputeInstanceNotFound,
exception.ModelNotFoundError,
+ exception.UserNotFound,
+ exception.DatabaseNotFound,
],
webob.exc.HTTPConflict: [],
webob.exc.HTTPRequestEntityTooLarge: [
diff --git a/reddwarf/extensions/mysql.py b/reddwarf/extensions/mysql.py
index f05dbfa8..c452ab1d 100644
--- a/reddwarf/extensions/mysql.py
+++ b/reddwarf/extensions/mysql.py
@@ -47,6 +47,7 @@ class Mysql(extensions.ExtensionsDescriptor):
serializer = wsgi.ReddwarfResponseSerializer(
body_serializers={'application/xml':
wsgi.ReddwarfXMLDictSerializer()})
+
resource = extensions.ResourceExtension(
'databases',
service.SchemaController(),
@@ -55,6 +56,7 @@ class Mysql(extensions.ExtensionsDescriptor):
deserializer=wsgi.ReddwarfRequestDeserializer(),
serializer=serializer)
resources.append(resource)
+
resource = extensions.ResourceExtension(
'users',
service.UserController(),
@@ -62,8 +64,21 @@ class Mysql(extensions.ExtensionsDescriptor):
'collection_name': '{tenant_id}/instances'},
# deserializer=extensions.ExtensionsXMLSerializer()
deserializer=wsgi.ReddwarfRequestDeserializer(),
- serializer=serializer)
+ serializer=serializer,
+ collection_actions={'update': 'PUT'})
+ resources.append(resource)
+
+ collection_url = '{tenant_id}/instances/:instance_id/users'
+ resource = extensions.ResourceExtension(
+ 'databases',
+ service.UserAccessController(),
+ parent={'member_name': 'user',
+ 'collection_name': collection_url},
+ deserializer=wsgi.ReddwarfRequestDeserializer(),
+ serializer=serializer,
+ collection_actions={'update': 'PUT'})
resources.append(resource)
+
resource = extensions.ResourceExtension(
'root',
service.RootController(),
@@ -71,7 +86,6 @@ class Mysql(extensions.ExtensionsDescriptor):
'collection_name': '{tenant_id}/instances'},
deserializer=wsgi.ReddwarfRequestDeserializer(),
serializer=serializer)
-
resources.append(resource)
return resources
diff --git a/reddwarf/extensions/mysql/models.py b/reddwarf/extensions/mysql/models.py
index e5b41db3..6066f506 100644
--- a/reddwarf/extensions/mysql/models.py
+++ b/reddwarf/extensions/mysql/models.py
@@ -57,6 +57,19 @@ class User(object):
self.databases = databases
@classmethod
+ def load(cls, context, instance_id, user):
+ load_and_verify(context, instance_id)
+ client = create_guest_client(context, instance_id)
+ found_user = client.get_user(username=user)
+ if not found_user:
+ return None
+ database_names = [{'name': db['_name']}
+ for db in found_user['_databases']]
+ return cls(found_user['_name'],
+ found_user['_password'],
+ database_names)
+
+ @classmethod
def create(cls, context, instance_id, users):
# Load InstanceServiceStatus to verify if it's running
load_and_verify(context, instance_id)
@@ -78,6 +91,49 @@ class User(object):
load_and_verify(context, instance_id)
create_guest_client(context, instance_id).delete_user(username)
+ @classmethod
+ def access(cls, context, instance_id, username):
+ load_and_verify(context, instance_id)
+ client = create_guest_client(context, instance_id)
+ databases = client.list_access(username)
+ dbs = []
+ for db in databases:
+ dbs.append(Schema(name=db['_name'],
+ collate=db['_collate'],
+ character_set=db['_character_set']))
+ return UserAccess(dbs)
+
+ @classmethod
+ def grant(cls, context, instance_id, username, databases):
+ load_and_verify(context, instance_id)
+ client = create_guest_client(context, instance_id)
+ client.grant_access(username, databases)
+
+ @classmethod
+ def revoke(cls, context, instance_id, username, database):
+ load_and_verify(context, instance_id)
+ client = create_guest_client(context, instance_id)
+ client.revoke_access(username, database)
+
+ @classmethod
+ def change_password(cls, context, instance_id, users):
+ load_and_verify(context, instance_id)
+ client = create_guest_client(context, instance_id)
+ change_users = []
+ for user in users:
+ change_user = {'name': user.name,
+ 'password': user.password,
+ }
+ change_users.append(change_user)
+ client.change_passwords(change_users)
+
+
+class UserAccess(object):
+ _data_fields = ['databases']
+
+ def __init__(self, databases):
+ self.databases = databases
+
class Root(object):
diff --git a/reddwarf/extensions/mysql/service.py b/reddwarf/extensions/mysql/service.py
index 62ca24c5..f50a86ee 100644
--- a/reddwarf/extensions/mysql/service.py
+++ b/reddwarf/extensions/mysql/service.py
@@ -28,6 +28,8 @@ from reddwarf.guestagent.db import models as guest_models
from reddwarf.openstack.common import log as logging
from reddwarf.openstack.common.gettextutils import _
+from urllib import unquote
+
LOG = logging.getLogger(__name__)
@@ -61,7 +63,6 @@ class UserController(wsgi.Controller):
"""Validate that the request has all the required parameters"""
if not body:
raise exception.BadRequest("The request contains an empty body")
-
if not body.get('users', ''):
raise exception.MissingKey(key='users')
for user in body.get('users'):
@@ -106,7 +107,87 @@ class UserController(wsgi.Controller):
return wsgi.Result(None, 202)
def show(self, req, tenant_id, instance_id, id):
- raise webob.exc.HTTPNotImplemented()
+ """Return a single user."""
+ LOG.info(_("Showing a user for instance '%s'") % instance_id)
+ LOG.info(_("req : '%s'\n\n") % req)
+ context = req.environ[wsgi.CONTEXT_KEY]
+ username = unquote(id)
+ user = models.User.load(context, instance_id, username)
+ if not user:
+ raise exception.UserNotFound(uuid=username)
+ view = views.UserView(user)
+ return wsgi.Result(view.data(), 200)
+
+ def update(self, req, body, tenant_id, instance_id):
+ """Change the password of one or more users."""
+ LOG.info(_("Updating user passwords for instance '%s'") % instance_id)
+ LOG.info(_("req : '%s'\n\n") % req)
+ context = req.environ[wsgi.CONTEXT_KEY]
+ self.validate(body)
+ users = body['users']
+ model_users = []
+ for user in users:
+ mu = guest_models.MySQLUser()
+ mu.name = user['name']
+ mu.password = user['password']
+ model_users.append(mu)
+ models.User.change_password(context, instance_id, model_users)
+ return wsgi.Result(None, 202)
+
+
+class UserAccessController(wsgi.Controller):
+ """Controller for adding and removing database access for a user."""
+
+ @classmethod
+ def validate(cls, body):
+ """Validate that the request has all the required parameters"""
+ if not body:
+ raise exception.BadRequest("The request contains an empty body")
+ if not body.get('databases', ''):
+ raise exception.MissingKey(key='databases')
+ for database in body.get('databases'):
+ if not database.get('name', ''):
+ raise exception.MissingKey(key='name')
+
+ def index(self, req, tenant_id, instance_id, user_id):
+ """Show permissions for the given user."""
+ LOG.info(_("Showing user access for instance '%s'") % instance_id)
+ LOG.info(_("req : '%s'\n\n") % req)
+ context = req.environ[wsgi.CONTEXT_KEY]
+ # Make sure this user exists.
+ username = unquote(user_id)
+ user = models.User.load(context, instance_id, username)
+ if not user:
+ raise exception.UserNotFound(uuid=user_id)
+ access = models.User.access(context, instance_id, username)
+ view = views.UserAccessView(access.databases)
+ return wsgi.Result(view.data(), 200)
+
+ def update(self, req, body, tenant_id, instance_id, user_id):
+ """Grant access for a user to one or more databases."""
+ context = req.environ[wsgi.CONTEXT_KEY]
+ self.validate(body)
+ user = models.User.load(context, instance_id, user_id)
+ if not user:
+ raise exception.UserNotFound(uuid=user_id)
+ databases = [db['name'] for db in body['databases']]
+ models.User.grant(context, instance_id, user_id, databases)
+ return wsgi.Result(None, 202)
+
+ def delete(self, req, tenant_id, instance_id, user_id, id):
+ """Revoke access for a user."""
+ context = req.environ[wsgi.CONTEXT_KEY]
+ user = models.User.load(context, instance_id, user_id)
+ if not user:
+ raise exception.UserNotFound(uuid=user_id)
+ # Make sure the database exists for the user.
+ username = unquote(user_id)
+ access = models.User.access(context, instance_id, username)
+ databases = [db.name for db in access.databases]
+ if not id in databases:
+ raise exception.DatabaseNotFound(uuid=id)
+ models.User.revoke(context, instance_id, user_id, id)
+ return wsgi.Result(None, 202)
class SchemaController(wsgi.Controller):
diff --git a/reddwarf/extensions/mysql/views.py b/reddwarf/extensions/mysql/views.py
index 9cdd27b0..2877da0b 100644
--- a/reddwarf/extensions/mysql/views.py
+++ b/reddwarf/extensions/mysql/views.py
@@ -43,6 +43,15 @@ class UsersView(object):
return {"users": data}
+class UserAccessView(object):
+ def __init__(self, databases):
+ self.databases = databases
+
+ def data(self):
+ dbs = [{"name": db.name} for db in self.databases]
+ return {"databases": dbs}
+
+
class RootCreatedView(UserView):
def data(self):
diff --git a/reddwarf/guestagent/api.py b/reddwarf/guestagent/api.py
index 081af5bd..4543661f 100644
--- a/reddwarf/guestagent/api.py
+++ b/reddwarf/guestagent/api.py
@@ -106,11 +106,39 @@ class API(proxy.RpcProxy):
LOG.warn(mnfe)
raise exception.GuestTimeout()
+ def change_passwords(self, users):
+ """Make an asynchronous call to change the passwords of one or more
+ users."""
+ LOG.debug(_("Changing passwords for users on Instance %s"), self.id)
+ self._cast("change_passwords", users=users)
+
def create_user(self, users):
"""Make an asynchronous call to create a new database user"""
LOG.debug(_("Creating Users for Instance %s"), self.id)
self._cast("create_user", users=users)
+ def get_user(self, username):
+ """Make an asynchronous call to get a single database user."""
+ LOG.debug(_("Getting a user on Instance %s"), self.id)
+ LOG.debug("User name is %s" % username)
+ return self._call("get_user", AGENT_LOW_TIMEOUT, username=username)
+
+ def list_access(self, username):
+ """Show all the databases to which a user has more than USAGE."""
+ LOG.debug(_("Showing user grants on Instance %s"), self.id)
+ LOG.debug("User name is %s" % username)
+ return self._call("list_access", AGENT_LOW_TIMEOUT, username=username)
+
+ def grant_access(self, username, databases):
+ """Give a user permission to use a given database."""
+ return self._call("grant_access", AGENT_LOW_TIMEOUT,
+ username=username, databases=databases)
+
+ def revoke_access(self, username, database):
+ """Remove a user's permission to use a given database."""
+ return self._call("revoke_access", AGENT_LOW_TIMEOUT,
+ username=username, database=database)
+
def list_users(self, limit=None, marker=None, include_marker=False):
"""Make an asynchronous call to list database users"""
LOG.debug(_("Listing Users for Instance %s"), self.id)
diff --git a/reddwarf/guestagent/dbaas.py b/reddwarf/guestagent/dbaas.py
index 95e45606..4338e7c3 100644
--- a/reddwarf/guestagent/dbaas.py
+++ b/reddwarf/guestagent/dbaas.py
@@ -40,8 +40,8 @@ from reddwarf import db
from reddwarf.common.exception import ProcessExecutionError
from reddwarf.common import cfg
from reddwarf.common import utils
+from reddwarf.guestagent import query
from reddwarf.guestagent.db import models
-from reddwarf.guestagent.query import Query
from reddwarf.guestagent import pkg
from reddwarf.instance import models as rd_models
from reddwarf.openstack.common import log as logging
@@ -50,7 +50,7 @@ from reddwarf.openstack.common.gettextutils import _
ADMIN_USER_NAME = "os_admin"
LOG = logging.getLogger(__name__)
-FLUSH = text("""FLUSH PRIVILEGES;""")
+FLUSH = text(query.FLUSH)
ENGINE = None
MYSQLD_ARGS = None
@@ -308,57 +308,92 @@ class LocalSqlClient(object):
class MySqlAdmin(object):
"""Handles administrative tasks on the MySQL database."""
+ def _associate_dbs(self, user):
+ """Internal. Given a MySQLUser, populate its databases attribute."""
+ LOG.debug("Associating dbs to user %s" % user.name)
+ with LocalSqlClient(get_engine()) as client:
+ q = query.Query()
+ q.columns = ["grantee", "table_schema"]
+ q.tables = ["information_schema.SCHEMA_PRIVILEGES"]
+ q.group = ["grantee", "table_schema"]
+ q.where = ["privilege_type != 'USAGE'"]
+ t = text(str(q))
+ db_result = client.execute(t)
+ for db in db_result:
+ LOG.debug("\t db: %s" % db)
+ if db['grantee'] == "'%s'@'%%'" % (user.name):
+ mysql_db = models.MySQLDatabase()
+ mysql_db.name = db['table_schema']
+ user.databases.append(mysql_db.serialize())
+
+ def change_passwords(self, users):
+ """Change the passwords of one or more existing users."""
+ LOG.debug("Changing the password of some users.""")
+ LOG.debug("Users is %s" % users)
+ with LocalSqlClient(get_engine()) as client:
+ for item in users:
+ LOG.debug("\tUser: %s" % item)
+ user_dict = {'_name': item['name'],
+ '_password': item['password'],
+ }
+ user = models.MySQLUser()
+ user.deserialize(user_dict)
+ LOG.debug("\tDeserialized: %s" % user.__dict__)
+ uu = query.UpdateUser(user.name, clear=user.password)
+ t = text(str(uu))
+ client.execute(t)
+
def create_database(self, databases):
"""Create the list of specified databases"""
- client = LocalSqlClient(get_engine())
- with client:
+ with LocalSqlClient(get_engine()) as client:
for item in databases:
mydb = models.MySQLDatabase()
mydb.deserialize(item)
- t = text("""CREATE DATABASE IF NOT EXISTS
- `%s` CHARACTER SET = %s COLLATE = %s;"""
- % (mydb.name, mydb.character_set, mydb.collate))
+ cd = query.CreateDatabase(mydb.name,
+ mydb.character_set,
+ mydb.collate)
+ t = text(str(cd))
client.execute(t)
def create_user(self, users):
"""Create users and grant them privileges for the
specified databases"""
host = "%"
- client = LocalSqlClient(get_engine())
- with client:
+ with LocalSqlClient(get_engine()) as client:
for item in users:
user = models.MySQLUser()
user.deserialize(item)
# TODO(cp16net):Should users be allowed to create users
# 'os_admin' or 'debian-sys-maint'
- t = text("""GRANT USAGE ON *.* TO '%s'@\"%s\"
- IDENTIFIED BY '%s';"""
- % (user.name, host, user.password))
+ g = query.Grant(user=user.name, host=host,
+ clear=user.password)
+ t = text(str(g))
client.execute(t)
for database in user.databases:
mydb = models.MySQLDatabase()
mydb.deserialize(database)
- t = text("""
- GRANT ALL PRIVILEGES ON `%s`.* TO `%s`@:host;
- """ % (mydb.name, user.name))
- client.execute(t, host=host)
+ g = query.Grant(permissions='ALL', database=mydb.name,
+ user=user.name, host=host,
+ clear=user.password)
+ t = text(str(g))
+ client.execute(t)
def delete_database(self, database):
"""Delete the specified database"""
- client = LocalSqlClient(get_engine())
- with client:
+ with LocalSqlClient(get_engine()) as client:
mydb = models.MySQLDatabase()
mydb.deserialize(database)
- t = text("""DROP DATABASE `%s`;""" % mydb.name)
+ dd = query.DropDatabase(mydb.name)
+ t = text(str(dd))
client.execute(t)
def delete_user(self, user):
"""Delete the specified users"""
- client = LocalSqlClient(get_engine())
- with client:
+ with LocalSqlClient(get_engine()) as client:
mysql_user = models.MySQLUser()
mysql_user.deserialize(user)
- t = text("""DROP USER `%s`""" % mysql_user.name)
+ du = query.DropUser(mysql_user.name)
+ t = text(str(du))
client.execute(t)
def enable_root(self):
@@ -367,31 +402,68 @@ class MySqlAdmin(object):
user = models.MySQLUser()
user.name = "root"
user.password = generate_random_password()
- client = LocalSqlClient(get_engine())
- with client:
+ with LocalSqlClient(get_engine()) as client:
try:
- t = text("""CREATE USER :user@:host;""")
- client.execute(t, user=user.name, host=host, pwd=user.password)
+ cu = query.CreateUser(user.name, host=host)
+ t = text(str(cu))
+ client.execute(t, **cu.keyArgs)
except exc.OperationalError as err:
# Ignore, user is already created, just reset the password
# TODO(rnirmal): More fine grained error checking later on
LOG.debug(err)
- with client:
- t = text("""UPDATE mysql.user SET Password=PASSWORD(:pwd)
- WHERE User=:user;""")
- client.execute(t, user=user.name, pwd=user.password)
- t = text("""GRANT ALL PRIVILEGES ON *.* TO :user@:host
- WITH GRANT OPTION;""")
- client.execute(t, user=user.name, host=host)
+ with LocalSqlClient(get_engine()) as client:
+ uu = query.UpdateUser(user.name, host=host,
+ clear=user.password)
+ t = text(str(uu))
+ client.execute(t)
+ g = query.Grant(permissions="ALL", user=user.name, host=host,
+ grant_option=True, clear=user.password)
+ t = text(str(g))
+ client.execute(t)
return user.serialize()
+ def get_user(self, username):
+ user = self._get_user(username)
+ if not user:
+ return None
+ return user.serialize()
+
+ def _get_user(self, username):
+ """Return a single user matching the criteria"""
+ user = models.MySQLUser()
+ user.name = username
+ with LocalSqlClient(get_engine()) as client:
+ q = query.Query()
+ q.columns = ['User', 'Password']
+ q.tables = ['mysql.user']
+ q.where = ["Host != 'localhost'",
+ "User = '%s'" % username,
+ ]
+ q.order = ['User']
+ t = text(str(q))
+ result = client.execute(t).fetchall()
+ LOG.debug("Result: %s" % result)
+ if len(result) != 1:
+ return None
+ found_user = result[0]
+ user.password = found_user['Password']
+ self._associate_dbs(user)
+ return user
+
+ def grant_access(self, username, databases):
+ """Give a user permission to use a given database."""
+ user = self._get_user(username)
+ with LocalSqlClient(get_engine()) as client:
+ for database in databases:
+ g = query.Grant(permissions='ALL', database=database,
+ user=user.name, host='%', hashed=user.password)
+ t = text(str(g))
+ client.execute(t)
+
def is_root_enabled(self):
"""Return True if root access is enabled; False otherwise."""
- client = LocalSqlClient(get_engine())
- with client:
- mysql_user = models.MySQLUser()
- t = text("""SELECT User FROM mysql.user where User = 'root'
- and host != 'localhost';""")
+ with LocalSqlClient(get_engine()) as client:
+ t = text(query.ROOT_ENABLED)
result = client.execute(t)
LOG.debug("result = " + str(result))
return result.rowcount != 0
@@ -400,23 +472,22 @@ class MySqlAdmin(object):
"""List databases the user created on this mysql instance"""
LOG.debug(_("---Listing Databases---"))
databases = []
- client = LocalSqlClient(get_engine())
- with client:
+ with LocalSqlClient(get_engine()) as client:
# If you have an external volume mounted at /var/lib/mysql
# the lost+found directory will show up in mysql as a database
# which will create errors if you try to do any database ops
# on it. So we remove it here if it exists.
- q = Query()
+ q = query.Query()
q.columns = [
'schema_name as name',
'default_character_set_name as charset',
'default_collation_name as collation',
]
q.tables = ['information_schema.schemata']
- q.where = ['''schema_name not in (
- 'mysql', 'information_schema',
- 'lost+found', '#mysql50#lost+found'
- )''']
+ q.where = ["schema_name NOT IN ("
+ "'mysql', 'information_schema', "
+ "'lost+found', '#mysql50#lost+found'"
+ ")"]
q.order = ['schema_name ASC']
if limit:
q.limit = limit + 1
@@ -447,10 +518,9 @@ class MySqlAdmin(object):
"""List users that have access to the database"""
LOG.debug(_("---Listing Users---"))
users = []
- client = LocalSqlClient(get_engine())
- with client:
+ with LocalSqlClient(get_engine()) as client:
mysql_user = models.MySQLUser()
- q = Query()
+ q = query.Query()
q.columns = ['User']
q.tables = ['mysql.user']
q.where = ["host != 'localhost'"]
@@ -471,21 +541,8 @@ class MySqlAdmin(object):
LOG.debug("user = " + str(row))
mysql_user = models.MySQLUser()
mysql_user.name = row['User']
+ self._associate_dbs(mysql_user)
next_marker = row['User']
- # Now get the databases
- q = Query()
- q.columns = ['grantee', 'table_schema']
- q.tables = ['information_schema.SCHEMA_PRIVILEGES']
- q.group = ['grantee', 'table_schema']
- t = text(str(q))
- db_result = client.execute(t)
- for db in db_result:
- matches = re.match("^'(.+)'@", db['grantee'])
- if (matches is not None and
- matches.group(1) == mysql_user.name):
- mysql_db = models.MySQLDatabase()
- mysql_db.name = db['table_schema']
- mysql_user.databases.append(mysql_db.serialize())
users.append(mysql_user.serialize())
if result.rowcount <= limit:
next_marker = None
@@ -493,6 +550,21 @@ class MySqlAdmin(object):
return users, next_marker
+ def revoke_access(self, username, database):
+ """Give a user permission to use a given database."""
+ user = self._get_user(username)
+ with LocalSqlClient(get_engine()) as client:
+ r = query.Revoke(database=database, user=user.name, host='%',
+ hashed=user.password)
+ t = text(str(r))
+ client.execute(t)
+
+ def list_access(self, username):
+ """Show all the databases to which the user has more than
+ USAGE granted."""
+ user = self._get_user(username)
+ return user.databases
+
class KeepAliveConnection(interfaces.PoolListener):
"""
@@ -531,25 +603,26 @@ class MySqlApp(object):
Create a os_admin user with a random password
with all privileges similar to the root user
"""
- t = text("CREATE USER :user@'localhost';")
- client.execute(t, user=ADMIN_USER_NAME)
- t = text("""
- UPDATE mysql.user SET Password=PASSWORD(:pwd)
- WHERE User=:user;
- """)
- client.execute(t, pwd=password, user=ADMIN_USER_NAME)
- t = text("""
- GRANT ALL PRIVILEGES ON *.* TO :user@'localhost'
- WITH GRANT OPTION;
- """)
- client.execute(t, user=ADMIN_USER_NAME)
+ localhost = "localhost"
+ cu = query.CreateUser(ADMIN_USER_NAME, host=localhost)
+ t = text(str(cu))
+ client.execute(t, **cu.keyArgs)
+ uu = query.UpdateUser(ADMIN_USER_NAME, host=localhost, clear=password)
+ t = text(str(uu))
+ client.execute(t)
+ g = query.Grant(permissions='ALL', user=ADMIN_USER_NAME,
+ host=localhost, grant_option=True, clear=password)
+ t = text(str(g))
+ client.execute(t)
@staticmethod
def _generate_root_password(client):
""" Generate and set a random root password and forget about it. """
- t = text("""UPDATE mysql.user SET Password=PASSWORD(:pwd)
- WHERE User='root';""")
- client.execute(t, pwd=generate_random_password())
+ localhost = "localhost"
+ uu = query.UpdateUser("root", host=localhost,
+ clear=generate_random_password())
+ t = text(str(uu))
+ client.execute(t)
def install_and_secure(self, memory_mb):
"""Prepare the guest machine with a secure mysql server installation"""
@@ -562,8 +635,7 @@ class MySqlApp(object):
admin_password = generate_random_password()
engine = create_engine("mysql://root:@localhost:3306", echo=True)
- client = LocalSqlClient(engine)
- with client:
+ with LocalSqlClient(engine) as client:
self._generate_root_password(client)
self._remove_anonymous_user(client)
self._remove_remote_root_access(client)
@@ -618,13 +690,11 @@ class MySqlApp(object):
raise RuntimeError("Could not stop MySQL!")
def _remove_anonymous_user(self, client):
- t = text("""DELETE FROM mysql.user WHERE User='';""")
+ t = text(query.REMOVE_ANON)
client.execute(t)
def _remove_remote_root_access(self, client):
- t = text("""DELETE FROM mysql.user
- WHERE User='root'
- AND Host!='localhost';""")
+ t = text(query.REMOVE_ROOT)
client.execute(t)
def restart(self):
diff --git a/reddwarf/guestagent/manager.py b/reddwarf/guestagent/manager.py
index e1fae542..a40d8b8d 100644
--- a/reddwarf/guestagent/manager.py
+++ b/reddwarf/guestagent/manager.py
@@ -15,6 +15,9 @@ class Manager(periodic_task.PeriodicTasks):
"""Update the status of the MySQL service"""
dbaas.MySqlAppStatus.get().update()
+ def change_passwords(self, context, users):
+ return dbaas.MySqlAdmin().change_passwords(users)
+
def create_database(self, context, databases):
return dbaas.MySqlAdmin().create_database(databases)
@@ -27,6 +30,18 @@ class Manager(periodic_task.PeriodicTasks):
def delete_user(self, context, user):
dbaas.MySqlAdmin().delete_user(user)
+ def get_user(self, context, username):
+ return dbaas.MySqlAdmin().get_user(username)
+
+ def grant_access(self, context, username, databases):
+ return dbaas.MySqlAdmin().grant_access(username, databases)
+
+ def revoke_access(self, context, username, database):
+ return dbaas.MySqlAdmin().revoke_access(username, database)
+
+ def list_access(self, context, username):
+ return dbaas.MySqlAdmin().list_access(username)
+
def list_databases(self, context, limit=None, marker=None,
include_marker=False):
return dbaas.MySqlAdmin().list_databases(limit, marker,
diff --git a/reddwarf/guestagent/query.py b/reddwarf/guestagent/query.py
index c67a1d79..7ec751f4 100644
--- a/reddwarf/guestagent/query.py
+++ b/reddwarf/guestagent/query.py
@@ -18,6 +18,8 @@
"""
Intermediary class for building SQL queries for use by the guest agent.
+Do not hard-code strings into the guest agent; use this module to build
+them for you.
"""
@@ -33,15 +35,18 @@ class Query(object):
self.group = group or []
self.limit = limit
+ def __repr__(self):
+ return str(self)
+
@property
def _columns(self):
if not self.columns:
return "SELECT *"
- return "SELECT %s" % (', '.join(self.columns))
+ return "SELECT %s" % (", ".join(self.columns))
@property
def _tables(self):
- return "FROM %s" % (', '.join(self.tables))
+ return "FROM %s" % (", ".join(self.tables))
@property
def _where(self):
@@ -52,19 +57,19 @@ class Query(object):
@property
def _order(self):
if not self.order:
- return ''
- return "ORDER BY %s" % (', '.join(self.order))
+ return ""
+ return "ORDER BY %s" % (", ".join(self.order))
@property
def _group_by(self):
if not self.group:
- return ''
- return "GROUP BY %s" % (', '.join(self.group))
+ return ""
+ return "GROUP BY %s" % (", ".join(self.group))
@property
def _limit(self):
if not self.limit:
- return ''
+ return ""
return "LIMIT %s" % str(self.limit)
def __str__(self):
@@ -76,7 +81,323 @@ class Query(object):
self._group_by,
self._limit,
]
- return '\n'.join(query)
+ return " ".join(query) + ";"
+
+
+class Grant(object):
+
+ PERMISSIONS = ["ALL",
+ "ALL PRIVILEGES",
+ "ALTER ROUTINE",
+ "ALTER",
+ "CREATE ROUTINE",
+ "CREATE TEMPORARY TABLES",
+ "CREATE USER",
+ "CREATE VIEW",
+ "CREATE",
+ "DELETE",
+ "DROP",
+ "EVENT",
+ "EXECUTE",
+ "FILE",
+ "INDEX",
+ "INSERT",
+ "LOCK TABLES",
+ "PROCESS",
+ "REFERENCES",
+ "RELOAD",
+ "REPLICATION CLIENT",
+ "REPLICATION SLAVE",
+ "SELECT",
+ "SHOW DATABASES",
+ "SHOW VIEW",
+ "SHUTDOWN",
+ "SUPER",
+ "TRIGGER",
+ "UPDATE",
+ "USAGE",
+ ]
+
+ def __init__(self, permissions=None, database=None, table=None, user=None,
+ host=None, clear=None, hashed=None, grant_option=True):
+ self.permissions = permissions or []
+ self.database = database
+ self.table = table
+ self.user = user
+ self.host = host
+ self.clear = clear
+ self.hashed = hashed
+ self.grant_option = grant_option
def __repr__(self):
return str(self)
+
+ @property
+ def _permissions(self):
+ if not self.permissions:
+ return "USAGE"
+ if "ALL" in self.permissions:
+ return "ALL PRIVILEGES"
+ if "ALL PRIVILEGES" in self.permissions:
+ return "ALL PRIVILEGES"
+ filtered = [perm for perm in set(self.permissions)
+ if perm in self.PERMISSIONS]
+ return ", ".join(sorted(filtered))
+
+ @property
+ def _database(self):
+ if not self.database:
+ return "*"
+ return "`%s`" % self.database
+
+ @property
+ def _table(self):
+ if self.table:
+ return "'%s'" % self.table
+ return "*"
+
+ @property
+ def _user(self):
+ return self.user or ""
+
+ @property
+ def _identity(self):
+ if self.clear:
+ return "IDENTIFIED BY '%s'" % self.clear
+ if self.hashed:
+ return "IDENTIFIED BY PASSWORD '%s'" % self.hashed
+ return ""
+
+ @property
+ def _host(self):
+ return self.host or "%"
+
+ @property
+ def _user_host(self):
+ return "`%s`@`%s`" % (self._user, self._host)
+
+ @property
+ def _what(self):
+ # Permissions to be granted to the user.
+ return "GRANT %s" % self._permissions
+
+ @property
+ def _where(self):
+ # Database and table to which the user is granted permissions.
+ return "ON %s.%s" % (self._database, self._table)
+
+ @property
+ def _whom(self):
+ # User and host to be granted permission. Optionally, password, too.
+ whom = [("TO %s" % self._user_host),
+ self._identity,
+ ]
+ return " ".join(whom)
+
+ @property
+ def _with(self):
+ clauses = []
+
+ if self.grant_option:
+ clauses.append("GRANT OPTION")
+
+ if not clauses:
+ return ""
+
+ return "WITH %s" % ", ".join(clauses)
+
+ def __str__(self):
+ query = [self._what,
+ self._where,
+ self._whom,
+ self._with,
+ ]
+ return " ".join(query) + ";"
+
+
+class Revoke(Grant):
+
+ def __init__(self, permissions=None, database=None, table=None, user=None,
+ host=None, clear=None, hashed=None):
+ self.permissions = permissions or []
+ self.database = database
+ self.table = table
+ self.user = user
+ self.host = host
+ self.clear = clear
+ self.hashed = hashed
+
+ def __str__(self):
+ query = [self._what,
+ self._where,
+ self._whom,
+ ]
+ return " ".join(query) + ";"
+
+ @property
+ def _permissions(self):
+ if not self.permissions:
+ return "ALL"
+ if "ALL" in self.permissions:
+ return "ALL"
+ if "ALL PRIVILEGES" in self.permissions:
+ return "ALL"
+ filtered = [perm for perm in self.permissions
+ if perm in self.PERMISSIONS]
+ return ", ".join(sorted(filtered))
+
+ @property
+ def _what(self):
+ # Permissions to be revoked from the user.
+ return "REVOKE %s" % self._permissions
+
+ @property
+ def _whom(self):
+ # User and host from whom to revoke permission.
+ # Optionally, password, too.
+ whom = [("FROM %s" % self._user_host),
+ self._identity,
+ ]
+ return " ".join(whom)
+
+
+class CreateDatabase(object):
+
+ def __init__(self, database, charset=None, collate=None):
+ self.database = database
+ self.charset = charset
+ self.collate = collate
+
+ def __repr__(self):
+ return str(self)
+
+ @property
+ def _charset(self):
+ if not self.charset:
+ return ""
+ return "CHARACTER SET = '%s'" % self.charset
+
+ @property
+ def _collate(self):
+ if not self.collate:
+ return ""
+ return "COLLATE = '%s'" % self.collate
+
+ def __str__(self):
+ query = [("CREATE DATABASE IF NOT EXISTS `%s`" % self.database),
+ self._charset,
+ self._collate,
+ ]
+ return " ".join(query) + ";"
+
+
+class DropDatabase(object):
+
+ def __init__(self, database):
+ self.database = database
+
+ def __repr__(self):
+ return str(self)
+
+ def __str__(self):
+ return "DROP DATABASE `%s`;" % self.database
+
+
+class CreateUser(object):
+
+ def __init__(self, user, host=None, clear=None, hashed=None):
+ self.user = user
+ self.host = host
+ self.clear = clear # A clear password
+ self.hashed = hashed # A hashed password
+
+ def __repr__(self):
+ return str(self)
+
+ @property
+ def keyArgs(self):
+ return {'user': self.user,
+ 'host': self._host,
+ }
+
+ @property
+ def _host(self):
+ if not self.host:
+ return "%"
+ return self.host
+
+ @property
+ def _identity(self):
+ if self.clear:
+ return "IDENTIFIED BY '%s'" % self.clear
+ if self.hashed:
+ return "IDENTIFIED BY PASSWORD '%s'" % self.hashed
+ return ""
+
+ def __str__(self):
+ #query = [("CREATE USER '%s'@'%s'" % (self.user, self._host)),
+ query = ["CREATE USER :user@:host"]
+ if self._identity:
+ query.append(self._identity)
+ return " ".join(query) + ";"
+
+
+class UpdateUser(object):
+
+ def __init__(self, user, host=None, clear=None):
+ self.user = user
+ self.host = host
+ self.clear = clear
+
+ def __repr__(self):
+ return str(self)
+
+ @property
+ def _set_password(self):
+ return "SET Password=PASSWORD('%s')" % self.clear
+
+ @property
+ def _host(self):
+ if not self.host:
+ return "%"
+ return self.host
+
+ @property
+ def _where(self):
+ clauses = []
+ if self.user:
+ clauses.append("User = '%s'" % self.user)
+ if self.host:
+ clauses.append("Host = '%s'" % self._host)
+ if not clauses:
+ return ""
+ return "WHERE %s" % " AND ".join(clauses)
+
+ def __str__(self):
+ query = ["UPDATE mysql.user",
+ self._set_password,
+ self._where,
+ ]
+ return " ".join(query) + ";"
+
+
+class DropUser(object):
+
+ def __init__(self, user):
+ self.user = user
+
+ def __repr__(self):
+ return str(self)
+
+ def __str__(self):
+ return "DROP USER `%s`;" % self.user
+
+
+### Miscellaneous queries that need no parameters.
+
+FLUSH = "FLUSH PRIVILEGES;"
+ROOT_ENABLED = ("SELECT User FROM mysql.user "
+ "WHERE User = 'root' AND host != 'localhost';")
+REMOVE_ANON = "DELETE FROM mysql.user WHERE User = '';"
+REMOVE_ROOT = ("DELETE FROM mysql.user "
+ "WHERE User = 'root' AND Host != 'localhost';")
diff --git a/reddwarf/taskmanager/api.py b/reddwarf/taskmanager/api.py
index badea256..c093763f 100644
--- a/reddwarf/taskmanager/api.py
+++ b/reddwarf/taskmanager/api.py
@@ -50,7 +50,7 @@ class API(ManagerAPI):
type_, value, tb = sys.exc_info()
LOG.error("Error running async task:")
LOG.error((traceback.format_exception(type_, value, tb)))
- raise type_, value, tb
+ raise type_(*value.args), None, tb
get_event_spawer()(0, func)
diff --git a/reddwarf/tests/api/user_access.py b/reddwarf/tests/api/user_access.py
new file mode 100644
index 00000000..2b5af35b
--- /dev/null
+++ b/reddwarf/tests/api/user_access.py
@@ -0,0 +1,199 @@
+# Copyright 2013 OpenStack LLC
+#
+# 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
+import re
+
+from reddwarfclient import exceptions
+
+from proboscis import after_class
+from proboscis import before_class
+from proboscis import test
+from proboscis.asserts import *
+
+from reddwarf import tests
+from reddwarf.tests.api.instances import instance_info
+from reddwarf.tests import util
+from reddwarf.tests.util import test_config
+from reddwarf.tests.api.users import TestUsers
+
+GROUP = "dbaas.api.useraccess"
+
+
+@test(depends_on_classes=[TestUsers],
+ groups=[tests.DBAAS_API, GROUP, tests.INSTANCES],
+ runs_after=[TestUsers])
+class TestUserAccess(object):
+ """
+ Test the creation and deletion of user grants.
+ """
+
+ @before_class
+ def setUp(self):
+ self.dbaas = util.create_dbaas_client(instance_info.user)
+ self.users = ["test_access_user"]
+ self.databases = [("test_access_db%02i" % i) for i in range(4)]
+ # None of the ghosts are real databases or users.
+ self.ghostdbs = ["test_user_access_ghost_db"]
+ self.ghostusers = ["test_ghostuser"]
+ self.revokedbs = self.databases[:1]
+ self.remainingdbs = self.databases[1:]
+
+ def _test_access(self, expecteddbs):
+ for user in self.users:
+ access = self.dbaas.users.list_access(instance_info.id, user)
+ assert_equal(200, self.dbaas.last_http_code)
+ access = [db.name for db in access]
+ assert_equal(set(access), set(expecteddbs))
+
+ def _grant_access(self, databases):
+ for user in self.users:
+ self.dbaas.users.grant(instance_info.id, user, databases)
+ assert_equal(202, self.dbaas.last_http_code)
+
+ def _revoke_access(self, databases):
+ for user in self.users:
+ for database in databases:
+ self.dbaas.users.revoke(instance_info.id, user, database)
+ assert_true(self.dbaas.last_http_code in [202, 404])
+
+ def _reset_access(self):
+ for user in self.users:
+ for database in self.databases + self.ghostdbs:
+ try:
+ self.dbaas.users.revoke(instance_info.id, user, database)
+ assert_true(self.dbaas.last_http_code in [202, 404])
+ except exceptions.NotFound as nf:
+ # This is all right here, since we're resetting.
+ pass
+ self._test_access([])
+
+ def _ensure_nothing_else_created(self):
+ # Make sure grants and revokes do not create users or databases.
+ databases = self.dbaas.databases.list(instance_info.id)
+ database_names = [db.name for db in databases]
+ for ghost in self.ghostdbs:
+ assert_true(ghost not in database_names)
+ users = self.dbaas.users.list(instance_info.id)
+ user_names = [user.name for user in users]
+ for ghost in self.ghostusers:
+ assert_true(ghost not in user_names)
+
+ @test()
+ def test_create_user_and_dbs(self):
+ users = [{"name": user, "password": "password", "databases": []}
+ for user in self.users]
+ self.dbaas.users.create(instance_info.id, users)
+ assert_equal(202, self.dbaas.last_http_code)
+
+ databases = [{"name": db} for db in self.databases]
+ self.dbaas.databases.create(instance_info.id, databases)
+ assert_equal(202, self.dbaas.last_http_code)
+
+ @test(depends_on=[test_create_user_and_dbs])
+ def test_no_access(self):
+ # No users have any access to any database.
+ self._reset_access()
+ self._test_access([])
+
+ @test(depends_on=[test_no_access])
+ def test_grant_full_access(self):
+ # The users are granted access to all test databases.
+ self._reset_access()
+ self._grant_access(self.databases)
+ self._test_access(self.databases)
+
+ @test(depends_on=[test_grant_full_access])
+ def test_grant_idempotence(self):
+ # Grant operations can be repeated with no ill effects.
+ self._reset_access()
+ self._grant_access(self.databases)
+ self._grant_access(self.databases)
+ self._test_access(self.databases)
+
+ @test(depends_on=[test_grant_full_access])
+ def test_revoke_one_database(self):
+ # Revoking permission removes that database from a user's list.
+ self._reset_access()
+ self._grant_access(self.databases)
+ self._test_access(self.databases)
+ self._revoke_access(self.revokedbs)
+ self._test_access(self.remainingdbs)
+
+ @test(depends_on=[test_grant_full_access])
+ def test_revoke_non_idempotence(self):
+ # Revoking access cannot be repeated.
+ self._reset_access()
+ self._grant_access(self.databases)
+ self._revoke_access(self.revokedbs)
+ assert_raises(exceptions.NotFound,
+ self._revoke_access,
+ self.revokedbs)
+ self._test_access(self.remainingdbs)
+
+ @test(depends_on=[test_grant_full_access])
+ def test_revoke_all_access(self):
+ # Revoking access to all databases will leave their access empty.
+ self._reset_access()
+ self._grant_access(self.databases)
+ self._revoke_access(self.databases)
+ self._test_access([])
+
+ @test(depends_on=[test_grant_full_access])
+ def test_grant_ghostdbs(self):
+ # Grants to imaginary databases are acceptable, and are honored.
+ self._reset_access()
+ self._ensure_nothing_else_created()
+ self._grant_access(self.ghostdbs)
+ self._ensure_nothing_else_created()
+
+ @test(depends_on=[test_grant_full_access])
+ def test_revoke_ghostdbs(self):
+ # Revokes to imaginary databases are acceptable, and are honored.
+ self._reset_access()
+ self._ensure_nothing_else_created()
+ self._grant_access(self.ghostdbs)
+ self._revoke_access(self.ghostdbs)
+ self._ensure_nothing_else_created()
+
+ @test(depends_on=[test_grant_full_access])
+ def test_grant_ghostusers(self):
+ # You cannot grant permissions to imaginary users, as imaginary users
+ # don't have passwords we can pull from mysql.users
+ self._reset_access()
+ for user in self.ghostusers:
+ assert_raises(exceptions.NotFound,
+ self.dbaas.users.grant,
+ instance_info.id, user, self.databases)
+ assert_equal(404, self.dbaas.last_http_code)
+
+ @test(depends_on=[test_grant_full_access])
+ def test_revoke_ghostusers(self):
+ # You cannot revoke permissions from imaginary users, as imaginary
+ # users don't have passwords we can pull from mysql.users
+ self._reset_access()
+ for user in self.ghostusers:
+ for database in self.databases:
+ assert_raises(exceptions.NotFound,
+ self.dbaas.users.revoke,
+ instance_info.id, user, database)
+ assert_equal(404, self.dbaas.last_http_code)
+
+ @after_class(always_run=True)
+ def tearDown(self):
+ self._reset_access()
+
+ for database in self.databases:
+ self.dbaas.databases.delete(instance_info.id, database)
+ assert_equal(202, self.dbaas.last_http_code)
diff --git a/reddwarf/tests/api/users.py b/reddwarf/tests/api/users.py
index 33786577..43dee3be 100644
--- a/reddwarf/tests/api/users.py
+++ b/reddwarf/tests/api/users.py
@@ -55,8 +55,8 @@ class TestUsers(object):
username1 = "anous*&^er"
username1_urlendcoded = "anous%2A%26%5Eer"
password1 = "anopas*?.sword"
- db1 = "firstdb"
- db2 = "seconddb"
+ db1 = "usersfirstdb"
+ db2 = "usersseconddb"
created_users = [username, username1]
system_users = ['root', 'debian_sys_maint']
@@ -89,6 +89,7 @@ class TestUsers(object):
"databases": [{"name": self.db1}, {"name": self.db2}]})
self.dbaas.users.create(instance_info.id, users)
assert_equal(202, self.dbaas.last_http_code)
+
# Do we need this?
if not FAKE:
time.sleep(5)
@@ -131,6 +132,16 @@ class TestUsers(object):
instance_info.id, users)
@test(depends_on=[test_create_users_list])
+ def test_get_one_user(self):
+ user = self.dbaas.users.get(instance_info.id, user=self.username)
+ assert_equal(200, self.dbaas.last_http_code)
+ assert_equal(user.name, self.username)
+ assert_equal(1, len(user.databases))
+ for db in user.databases:
+ assert_equal(db["name"], self.db1)
+ self.check_database_for_user(self.username, self.password, [self.db1])
+
+ @test(depends_on=[test_create_users_list])
def test_create_users_list_system(self):
#tests for users that should not be listed
users = self.dbaas.users.list(instance_info.id)
@@ -172,7 +183,7 @@ class TestUsers(object):
assert_true(
db in actual_list,
"No match for db %s in dblist. %s :(" % (db, actual_list))
- # Confirm via API.
+ # Confirm via API list.
result = self.dbaas.users.list(instance_info.id)
assert_equal(200, self.dbaas.last_http_code)
for item in result:
@@ -181,6 +192,12 @@ class TestUsers(object):
else:
fail("User %s not added to collection." % user)
+ # Confirm via API get.
+ result = self.dbaas.users.get(instance_info.id, user)
+ assert_equal(200, self.dbaas.last_http_code)
+ if result.name != user:
+ fail("User %s not found via get." % user)
+
@test
def test_username_too_long(self):
users = []
diff --git a/reddwarf/tests/fakes/guestagent.py b/reddwarf/tests/fakes/guestagent.py
index b66b0723..d1e53348 100644
--- a/reddwarf/tests/fakes/guestagent.py
+++ b/reddwarf/tests/fakes/guestagent.py
@@ -19,6 +19,7 @@ from reddwarf.openstack.common import log as logging
import time
from reddwarf.tests.fakes.common import get_event_spawer
+from reddwarf.common import exception as rd_exception
DB = {}
LOG = logging.getLogger(__name__)
@@ -33,6 +34,7 @@ class FakeGuest(object):
self.root_was_enabled = False
self.version = 1
self.event_spawn = get_event_spawer()
+ self.grants = {}
def get_hwinfo(self):
return {'mem_total': 524288, 'num_cpus': 1}
@@ -108,6 +110,9 @@ class FakeGuest(object):
def list_users(self, limit=None, marker=None, include_marker=False):
return self._list_resource(self.users, limit, marker, include_marker)
+ def get_user(self, username):
+ return self.users.get(username, None)
+
def prepare(self, memory_mb, databases, users, device_path=None,
mount_point=None):
from reddwarf.instance.models import DBInstance
@@ -160,6 +165,41 @@ class FakeGuest(object):
"""Return used volume information in bytes."""
return {'used': 175756487}
+ def grant_access(self, username, databases):
+ """Add a database to a users's grant list."""
+ if username not in self.users:
+ raise rd_exception.UserNotFound(
+ "User %s cannot be found on the instance." % username)
+ current_grants = self.grants.get((username, '%'), set())
+ for db in databases:
+ current_grants.add(db)
+ self.grants[(username, '%')] = current_grants
+
+ def revoke_access(self, username, database):
+ """Remove a database from a users's grant list."""
+ if username not in self.users:
+ raise rd_exception.UserNotFound(
+ "User %s cannot be found on the instance." % username)
+ g = self.grants.get((username, '%'), set())
+ if database not in self.grants.get((username, '%'), set()):
+ raise rd_exception.DatabaseNotFound(
+ "Database %s cannot be found on the instance." % database)
+ current_grants = self.grants.get((username, '%'), set())
+ if database in current_grants:
+ current_grants.remove(database)
+ self.grants[(username, '%')] = current_grants
+
+ def list_access(self, username):
+ if username not in self.users:
+ raise rd_exception.UserNotFound(
+ "User %s cannot be found on the instance." % username)
+ current_grants = self.grants.get((username, '%'), set())
+ dbs = [{'_name': db,
+ '_collate': '',
+ '_character_set': '',
+ } for db in current_grants]
+ return dbs
+
def get_or_create(id):
if id not in DB:
diff --git a/reddwarf/tests/unittests/guestagent/test_dbaas.py b/reddwarf/tests/unittests/guestagent/test_dbaas.py
index f99debde..91e0a944 100644
--- a/reddwarf/tests/unittests/guestagent/test_dbaas.py
+++ b/reddwarf/tests/unittests/guestagent/test_dbaas.py
@@ -146,9 +146,9 @@ class MySqlAdminTest(testtools.TestCase):
self.mySqlAdmin.create_database(databases)
args, _ = dbaas.LocalSqlClient.execute.call_args_list[0]
- expected = "CREATE DATABASE IF NOT EXISTS\n " \
- " `testDB` CHARACTER SET = latin2 COLLATE = " \
- "latin2_general_ci;"
+ expected = ("CREATE DATABASE IF NOT EXISTS "
+ "`testDB` CHARACTER SET = 'latin2' "
+ "COLLATE = 'latin2_general_ci';")
self.assertEquals(args[0].text, expected,
"Create database queries are not the same")
@@ -164,16 +164,16 @@ class MySqlAdminTest(testtools.TestCase):
self.mySqlAdmin.create_database(databases)
args, _ = dbaas.LocalSqlClient.execute.call_args_list[0]
- expected = "CREATE DATABASE IF NOT EXISTS\n " \
- " `testDB` CHARACTER SET = latin2 COLLATE = " \
- "latin2_general_ci;"
+ expected = ("CREATE DATABASE IF NOT EXISTS "
+ "`testDB` CHARACTER SET = 'latin2' "
+ "COLLATE = 'latin2_general_ci';")
self.assertEquals(args[0].text, expected,
"Create database queries are not the same")
args, _ = dbaas.LocalSqlClient.execute.call_args_list[1]
- expected = "CREATE DATABASE IF NOT EXISTS\n " \
- " `testDB2` CHARACTER SET = latin2 COLLATE = " \
- "latin2_general_ci;"
+ expected = ("CREATE DATABASE IF NOT EXISTS "
+ "`testDB2` CHARACTER SET = 'latin2' "
+ "COLLATE = 'latin2_general_ci';")
self.assertEquals(args[0].text, expected,
"Create database queries are not the same")
@@ -211,7 +211,7 @@ class MySqlAdminTest(testtools.TestCase):
self.mySqlAdmin.delete_user(user)
args, _ = dbaas.LocalSqlClient.execute.call_args
- expected = "DROP USER `testUser`"
+ expected = "DROP USER `testUser`;"
self.assertEquals(args[0].text, expected,
"Delete user queries are not the same")
@@ -220,7 +220,9 @@ class MySqlAdminTest(testtools.TestCase):
def test_create_user(self):
self.mySqlAdmin.create_user(FAKE_USER)
- expected = 'GRANT ALL PRIVILEGES ON `testDB`.* TO `random`@:host;'
+ expected = ("GRANT ALL PRIVILEGES ON `testDB`.* TO `random`@`%` "
+ "IDENTIFIED BY 'guesswhat' "
+ "WITH GRANT OPTION;")
args, _ = dbaas.LocalSqlClient.execute.call_args
self.assertEquals(args[0].text.strip(), expected,
"Create user queries are not the same")
@@ -243,10 +245,12 @@ class EnableRootTest(MySqlAdminTest):
self.mySqlAdmin.enable_root()
args_list = dbaas.LocalSqlClient.execute.call_args_list
args, keyArgs = args_list[0]
+
self.assertEquals(args[0].text.strip(), "CREATE USER :user@:host;",
"Create user queries are not the same")
self.assertEquals(keyArgs['user'], 'root')
self.assertEquals(keyArgs['host'], '%')
+
args, keyArgs = args_list[1]
self.assertTrue("UPDATE mysql.user" in args[0].text)
args, keyArgs = args_list[2]
@@ -262,44 +266,44 @@ class EnableRootTest(MySqlAdminTest):
def test_is_root_enable(self):
self.mySqlAdmin.is_root_enabled()
args, _ = dbaas.LocalSqlClient.execute.call_args
- self.assertTrue("""SELECT User FROM mysql.user where User = 'root'
- and host != 'localhost';""" in args[0].text)
+ expected = ("""SELECT User FROM mysql.user WHERE User = 'root' """
+ """AND host != 'localhost';""")
+ self.assertTrue(expected in args[0].text,
+ "%s not in query." % expected)
def test_list_databases(self):
self.mySqlAdmin.list_databases()
args, _ = dbaas.LocalSqlClient.execute.call_args
-
- self.assertTrue("SELECT schema_name as name," in args[0].text)
- self.assertTrue("default_character_set_name as charset,"
- in args[0].text)
- self.assertTrue("default_collation_name as collation" in args[0].text)
-
- self.assertTrue("FROM information_schema.schemata" in args[0].text)
-
- self.assertTrue('''schema_name not in (
- 'mysql', 'information_schema',
- 'lost+found', '#mysql50#lost+found'
- )''' in args[0].text)
- self.assertTrue("ORDER BY schema_name ASC" in args[0].text)
+ expected = ["SELECT schema_name as name,",
+ "default_character_set_name as charset,",
+ "default_collation_name as collation",
+ "FROM information_schema.schemata",
+ ("schema_name NOT IN ("
+ "'mysql', 'information_schema', "
+ "'lost+found', '#mysql50#lost+found'"
+ ")"),
+ "ORDER BY schema_name ASC",
+ ]
+ for text in expected:
+ self.assertTrue(text in args[0].text, "%s not in query." % text)
self.assertFalse("LIMIT " in args[0].text)
def test_list_databases_with_limit(self):
limit = 2
self.mySqlAdmin.list_databases(limit)
args, _ = dbaas.LocalSqlClient.execute.call_args
-
- self.assertTrue("SELECT schema_name as name," in args[0].text)
- self.assertTrue("default_character_set_name as charset,"
- in args[0].text)
- self.assertTrue("default_collation_name as collation" in args[0].text)
-
- self.assertTrue("FROM information_schema.schemata" in args[0].text)
-
- self.assertTrue('''schema_name not in (
- 'mysql', 'information_schema',
- 'lost+found', '#mysql50#lost+found'
- )''' in args[0].text)
- self.assertTrue("ORDER BY schema_name ASC" in args[0].text)
+ expected = ["SELECT schema_name as name,",
+ "default_character_set_name as charset,",
+ "default_collation_name as collation",
+ "FROM information_schema.schemata",
+ ("schema_name NOT IN ("
+ "'mysql', 'information_schema', "
+ "'lost+found', '#mysql50#lost+found'"
+ ")"),
+ "ORDER BY schema_name ASC",
+ ]
+ for text in expected:
+ self.assertTrue(text in args[0].text, "%s not in query." % text)
self.assertTrue("LIMIT " + str(limit + 1) in args[0].text)
@@ -307,19 +311,19 @@ class EnableRootTest(MySqlAdminTest):
marker = "aMarker"
self.mySqlAdmin.list_databases(marker=marker)
args, _ = dbaas.LocalSqlClient.execute.call_args
-
- self.assertTrue("SELECT schema_name as name," in args[0].text)
- self.assertTrue("default_character_set_name as charset,"
- in args[0].text)
- self.assertTrue("default_collation_name as collation" in args[0].text)
-
- self.assertTrue("FROM information_schema.schemata" in args[0].text)
-
- self.assertTrue('''schema_name not in (
- 'mysql', 'information_schema',
- 'lost+found', '#mysql50#lost+found'
- )''' in args[0].text)
- self.assertTrue("ORDER BY schema_name ASC" in args[0].text)
+ expected = ["SELECT schema_name as name,",
+ "default_character_set_name as charset,",
+ "default_collation_name as collation",
+ "FROM information_schema.schemata",
+ ("schema_name NOT IN ("
+ "'mysql', 'information_schema', "
+ "'lost+found', '#mysql50#lost+found'"
+ ")"),
+ "ORDER BY schema_name ASC",
+ ]
+
+ for text in expected:
+ self.assertTrue(text in args[0].text, "%s not in query." % text)
self.assertFalse("LIMIT " in args[0].text)
@@ -330,19 +334,18 @@ class EnableRootTest(MySqlAdminTest):
self.mySqlAdmin.list_databases(marker=marker, include_marker=True)
args, _ = dbaas.LocalSqlClient.execute.call_args
- self.assertTrue("SELECT schema_name as name," in args[0].text)
- self.assertTrue("default_character_set_name as charset,"
- in args[0].text)
- self.assertTrue("default_collation_name as collation" in args[0].text)
-
- self.assertTrue("FROM information_schema.schemata"
- in args[0].text)
-
- self.assertTrue('''schema_name not in (
- 'mysql', 'information_schema',
- 'lost+found', '#mysql50#lost+found'
- )''' in args[0].text)
- self.assertTrue("ORDER BY schema_name ASC" in args[0].text)
+ expected = ["SELECT schema_name as name,",
+ "default_character_set_name as charset,",
+ "default_collation_name as collation",
+ "FROM information_schema.schemata",
+ ("schema_name NOT IN ("
+ "'mysql', 'information_schema', "
+ "'lost+found', '#mysql50#lost+found'"
+ ")"),
+ "ORDER BY schema_name ASC",
+ ]
+ for text in expected:
+ self.assertTrue(text in args[0].text, "%s not in query." % text)
self.assertFalse("LIMIT " in args[0].text)
@@ -352,12 +355,14 @@ class EnableRootTest(MySqlAdminTest):
self.mySqlAdmin.list_users()
args, _ = dbaas.LocalSqlClient.execute.call_args
- self.assertTrue("SELECT User" in args[0].text)
-
- self.assertTrue("FROM mysql.user" in args[0].text)
+ expected = ["SELECT User",
+ "FROM mysql.user",
+ "WHERE host != 'localhost'",
+ "ORDER BY User",
+ ]
+ for text in expected:
+ self.assertTrue(text in args[0].text, "%s not in query." % text)
- self.assertTrue("WHERE host != 'localhost'" in args[0].text)
- self.assertTrue("ORDER BY User" in args[0].text)
self.assertFalse("LIMIT " in args[0].text)
self.assertFalse("AND User > '" in args[0].text)
@@ -366,29 +371,30 @@ class EnableRootTest(MySqlAdminTest):
self.mySqlAdmin.list_users(limit)
args, _ = dbaas.LocalSqlClient.execute.call_args
- self.assertTrue("SELECT User" in args[0].text)
-
- self.assertTrue("FROM mysql.user" in args[0].text)
-
- self.assertTrue("WHERE host != 'localhost'" in args[0].text)
- self.assertTrue("ORDER BY User" in args[0].text)
-
- self.assertTrue("LIMIT " + str(limit + 1) in args[0].text)
+ expected = ["SELECT User",
+ "FROM mysql.user",
+ "WHERE host != 'localhost'",
+ "ORDER BY User",
+ ("LIMIT " + str(limit + 1)),
+ ]
+ for text in expected:
+ self.assertTrue(text in args[0].text, "%s not in query." % text)
def test_list_users_with_marker(self):
marker = "aMarker"
self.mySqlAdmin.list_users(marker=marker)
args, _ = dbaas.LocalSqlClient.execute.call_args
- self.assertTrue("SELECT User" in args[0].text)
-
- self.assertTrue("FROM mysql.user" in args[0].text)
+ expected = ["SELECT User",
+ "FROM mysql.user",
+ "WHERE host != 'localhost'",
+ "ORDER BY User",
+ ]
- self.assertTrue("WHERE host != 'localhost'" in args[0].text)
- self.assertTrue("ORDER BY User" in args[0].text)
+ for text in expected:
+ self.assertTrue(text in args[0].text, "%s not in query." % text)
self.assertFalse("LIMIT " in args[0].text)
-
self.assertTrue("AND User > '" + marker + "'" in args[0].text)
def test_list_users_with_include_marker(self):
@@ -396,12 +402,14 @@ class EnableRootTest(MySqlAdminTest):
self.mySqlAdmin.list_users(marker=marker, include_marker=True)
args, _ = dbaas.LocalSqlClient.execute.call_args
- self.assertTrue("SELECT User" in args[0].text)
+ expected = ["SELECT User",
+ "FROM mysql.user",
+ "WHERE host != 'localhost'",
+ "ORDER BY User",
+ ]
- self.assertTrue("FROM mysql.user" in args[0].text)
-
- self.assertTrue("WHERE host != 'localhost'" in args[0].text)
- self.assertTrue("ORDER BY User" in args[0].text)
+ for text in expected:
+ self.assertTrue(text in args[0].text, "%s not in query." % text)
self.assertFalse("LIMIT " in args[0].text)
@@ -429,8 +437,8 @@ class MySqlAppTest(testtools.TestCase):
InstanceServiceStatus.find_by(instance_id=self.FAKE_ID).delete()
def assert_reported_status(self, expected_status):
- service_status = InstanceServiceStatus.find_by(instance_id=
- self.FAKE_ID)
+ service_status = InstanceServiceStatus.find_by(
+ instance_id=self.FAKE_ID)
self.assertEqual(expected_status, service_status.status)
def mysql_starts_successfully(self):
@@ -509,17 +517,16 @@ class MySqlAppTest(testtools.TestCase):
def test_wipe_ib_logfiles_no_file(self):
from reddwarf.common.exception import ProcessExecutionError
- dbaas.utils.execute_with_timeout = \
- Mock(side_effect=
- ProcessExecutionError('No such file or directory'))
+ processexecerror = ProcessExecutionError('No such file or directory')
+ dbaas.utils.execute_with_timeout = Mock(side_effect=processexecerror)
self.mySqlApp.wipe_ib_logfiles()
def test_wipe_ib_logfiles_error(self):
from reddwarf.common.exception import ProcessExecutionError
- dbaas.utils.execute_with_timeout = Mock(side_effect=
- ProcessExecutionError('Error'))
+ mocked = Mock(side_effect=ProcessExecutionError('Error'))
+ dbaas.utils.execute_with_timeout = mocked
self.assertRaises(ProcessExecutionError,
self.mySqlApp.wipe_ib_logfiles)
@@ -553,8 +560,8 @@ class MySqlAppTest(testtools.TestCase):
self.mySqlApp._enable_mysql_on_boot = Mock()
from reddwarf.common.exception import ProcessExecutionError
- dbaas.utils.execute_with_timeout = Mock(side_effect=
- ProcessExecutionError('Error'))
+ mocked = Mock(side_effect=ProcessExecutionError('Error'))
+ dbaas.utils.execute_with_timeout = mocked
self.assertRaises(RuntimeError, self.mySqlApp.start_mysql)
@@ -834,8 +841,8 @@ class MySqlAppStatusTest(testtools.TestCase):
def test_get_actual_db_status_error_shutdown(self):
from reddwarf.common.exception import ProcessExecutionError
- dbaas.utils.execute_with_timeout = Mock(side_effect=
- ProcessExecutionError())
+ mocked = Mock(side_effect=ProcessExecutionError())
+ dbaas.utils.execute_with_timeout = mocked
dbaas.load_mysqld_options = Mock()
dbaas.os.path.exists = Mock(return_value=False)
diff --git a/reddwarf/tests/util/check.py b/reddwarf/tests/util/check.py
index d55c8e42..e026015b 100644
--- a/reddwarf/tests/util/check.py
+++ b/reddwarf/tests/util/check.py
@@ -96,7 +96,7 @@ class Checker(object):
final_message = '\n'.join(self.messages)
if _type is not None: # An error occurred
if len(self.messages) == 0:
- raise _type, value, tb
+ raise _type(*value.args), None, tb
self._add_exception(_type, value, tb)
if len(self.messages) != 0:
final_message = '\n'.join(self.messages)
diff --git a/run_tests.py b/run_tests.py
index c5b507e8..c7afb609 100644
--- a/run_tests.py
+++ b/run_tests.py
@@ -120,6 +120,7 @@ if __name__=="__main__":
from reddwarf.tests.api import databases
from reddwarf.tests.api import root
from reddwarf.tests.api import users
+ from reddwarf.tests.api import user_access
from reddwarf.tests.api.mgmt import accounts
from reddwarf.tests.api.mgmt import admin_required
from reddwarf.tests.api.mgmt import instances
diff --git a/tools/test-requires b/tools/test-requires
index f81a3e53..a095ae4f 100644
--- a/tools/test-requires
+++ b/tools/test-requires
@@ -10,7 +10,7 @@ pylint
webtest
wsgi_intercept
proboscis
-python-reddwarfclient
+python-reddwarfclient==0.1.2
mock
mox
testtools>=0.9.22