diff --git a/etc/reddwarf/reddwarf.conf.test b/etc/reddwarf/reddwarf.conf.test index 2ab4bd3d9e..5bf787b884 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 df23a48747..e1e43775ef 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 16b7b8ab51..775cdc8ab1 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 f05dbfa81f..c452ab1da8 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 e5b41db372..6066f50692 100644 --- a/reddwarf/extensions/mysql/models.py +++ b/reddwarf/extensions/mysql/models.py @@ -56,6 +56,19 @@ class User(object): self.password = password 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 @@ -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 62ca24c5cb..f50a86ee71 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 9cdd27b0fe..2877da0b15 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 081af5bd42..4543661f0b 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 95e4560626..4338e7c32d 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 e1fae5425d..a40d8b8da1 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 c67a1d79c1..7ec751f451 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 badea25608..c093763f2f 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 0000000000..2b5af35b0d --- /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 3378657738..43dee3bec2 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) @@ -130,6 +131,16 @@ class TestUsers(object): assert_raises(exceptions.BadRequest, self.dbaas.users.create, 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 @@ -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 b66b0723b9..d1e53348f4 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 f99debdeab..91e0a94454 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 + 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", + ] - 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) + 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) + 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("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.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) + 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) - 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 d55c8e4208..e026015b39 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 c5b507e830..c7afb609c6 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 f81a3e5391..a095ae4f1f 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