diff --git a/trove/common/pagination.py b/trove/common/pagination.py index 2946d30644..82f3bb2939 100644 --- a/trove/common/pagination.py +++ b/trove/common/pagination.py @@ -24,26 +24,48 @@ def url_quote(s): return urllib_parse.quote(str(s)) -def paginate_list(li, limit=None, marker=None, include_marker=False): +def paginate_list(li, limit=None, marker=None, include_marker=False, + key=lambda x: x): """Sort the given list and return a sublist containing a page of items. :param list li: The list to be paginated. :param int limit: Maximum number of iterms to be returned. :param marker: Key of the first item to appear on the sublist. :param bool include_marker: Include the marker value itself in the sublist. + :param lambda key: Sorting expression. :return: """ - li.sort() + sli = sorted(li, key=key) + index = [key(item) for item in sli] if include_marker: - pos = bisect.bisect_left(li, marker) + pos = bisect.bisect_left(index, marker) else: - pos = bisect.bisect(li, marker) + pos = bisect.bisect(index, marker) - if limit and pos + limit < len(li): - page = li[pos:pos + limit] - return page, page[-1] + if limit and pos + limit < len(sli): + page = sli[pos:pos + limit] + return page, key(page[-1]) else: - return li[pos:], None + return sli[pos:], None + + +def paginate_object_list(li, attr_name, limit=None, marker=None, + include_marker=False): + """Wrapper for paginate_list to handle lists of generic objects paginated + based on an attribute. + """ + return paginate_list(li, limit=limit, marker=marker, + include_marker=include_marker, + key=lambda x: getattr(x, attr_name)) + + +def paginate_dict_list(li, key, limit=None, marker=None, include_marker=False): + """Wrapper for paginate_list to handle lists of dicts paginated + based on a key. + """ + return paginate_list(li, limit=limit, marker=marker, + include_marker=include_marker, + key=lambda x: x[key]) class PaginatedDataView(object): diff --git a/trove/guestagent/datastore/experimental/couchdb/service.py b/trove/guestagent/datastore/experimental/couchdb/service.py index 9099485f86..bf45ecc1d9 100644 --- a/trove/guestagent/datastore/experimental/couchdb/service.py +++ b/trove/guestagent/datastore/experimental/couchdb/service.py @@ -199,9 +199,7 @@ class CouchDBAdmin(object): if not type(self).admin_user: creds = CouchDBCredentials() creds.read(system.COUCHDB_ADMIN_CREDS_FILE) - user = models.CouchDBUser() - user.name = creds.username - user.password = creds.password + user = models.CouchDBUser(creds.username, creds.password) type(self).admin_user = user return type(self).admin_user @@ -212,13 +210,15 @@ class CouchDBAdmin(object): return False return True + def _is_modifiable_database(self, name): + return name not in cfg.get_ignored_dbs() + def create_user(self, users): LOG.debug("Creating user(s) for accessing CouchDB database(s).") self._admin_user() try: for item in users: - user = models.CouchDBUser() - user.deserialize(item) + user = models.CouchDBUser.deserialize_user(item) try: LOG.debug("Creating user: %s." % user.name) utils.execute_with_timeout( @@ -234,8 +234,7 @@ class CouchDBAdmin(object): pass for database in user.databases: - mydb = models.CouchDBSchema() - mydb.deserialize(database) + mydb = models.CouchDBSchema.deserialize_schema(database) try: LOG.debug("Granting user: %s access to database: %s." % (user.name, mydb.name)) @@ -258,8 +257,7 @@ class CouchDBAdmin(object): def delete_user(self, user): LOG.debug("Delete a given CouchDB user.") - couchdb_user = models.CouchDBUser() - couchdb_user.deserialize(user) + couchdb_user = models.CouchDBUser.deserialize_user(user) db_names = self.list_database_names() for db in db_names: @@ -346,8 +344,7 @@ class CouchDBAdmin(object): elif uname[17:]: userlist.append(uname[17:]) for i in range(len(userlist)): - user = models.CouchDBUser() - user.name = userlist[i] + user = models.CouchDBUser(userlist[i]) for db in db_names: try: out2, err = utils.execute_with_timeout( @@ -381,8 +378,7 @@ class CouchDBAdmin(object): return user.serialize() def _get_user(self, username, hostname): - user = models.CouchDBUser() - user.name = username + user = models.CouchDBUser(username) db_names = self.list_database_names() for db in db_names: try: @@ -413,8 +409,7 @@ class CouchDBAdmin(object): 'Cannot grant access for non-existant user: ' '%(user)s') % {'user': username}) else: - user = models.CouchDBUser() - user.name = username + user = models.CouchDBUser(username) if not self._is_modifiable_user(user.name): LOG.warning(_('Cannot grant access for reserved user ' '%(user)s') % {'user': username}) @@ -462,12 +457,7 @@ class CouchDBAdmin(object): def enable_root(self, root_pwd=None): '''Create admin user root''' - if not root_pwd: - LOG.debug('Generating root user password.') - root_pwd = utils.generate_random_password() - root_user = models.CouchDBUser() - root_user.name = 'root' - root_user.password = root_pwd + root_user = models.CouchDBRootUser(password=root_pwd) out, err = utils.execute_with_timeout( system.ENABLE_ROOT % {'admin_name': self._admin_user().name, @@ -497,19 +487,24 @@ class CouchDBAdmin(object): for database in databases: dbName = models.CouchDBSchema.deserialize_schema(database).name - LOG.debug('Creating CouchDB database %s' % dbName) - try: - utils.execute_with_timeout( - system.CREATE_DB_COMMAND % - {'admin_name': self._admin_user().name, - 'admin_password': self._admin_user().password, - 'dbname': dbName}, - shell=True) - except exception.ProcessExecutionError: - LOG.exception(_( - "There was an error creating database: %s.") % dbName) + if self._is_modifiable_database(dbName): + LOG.debug('Creating CouchDB database %s' % dbName) + try: + utils.execute_with_timeout( + system.CREATE_DB_COMMAND % + {'admin_name': self._admin_user().name, + 'admin_password': self._admin_user().password, + 'dbname': dbName}, + shell=True) + except exception.ProcessExecutionError: + LOG.exception(_( + "There was an error creating database: %s.") % dbName) + db_create_failed.append(dbName) + pass + else: + LOG.warning(_('Cannot create database with a reserved name ' + '%(db)s') % {'db': dbName}) db_create_failed.append(dbName) - pass if len(db_create_failed) > 0: LOG.exception(_("Creating the following databases failed: %s.") % db_create_failed) @@ -540,21 +535,24 @@ class CouchDBAdmin(object): def delete_database(self, database): '''Delete the specified database.''' - dbName = None - try: - dbName = models.CouchDBSchema.deserialize_schema(database).name - LOG.debug("Deleting CouchDB database: %s." % dbName) - utils.execute_with_timeout( - system.DELETE_DB_COMMAND % - {'admin_name': self._admin_user().name, - 'admin_password': self._admin_user().password, - 'dbname': dbName}, - shell=True) - except exception.ProcessExecutionError: - LOG.exception(_( - "There was an error while deleting database:%s.") % dbName) - raise exception.GuestError(_("Unable to delete database: %s.") % - dbName) + dbName = models.CouchDBSchema.deserialize_schema(database).name + if self._is_modifiable_database(dbName): + try: + LOG.debug("Deleting CouchDB database: %s." % dbName) + utils.execute_with_timeout( + system.DELETE_DB_COMMAND % + {'admin_name': self._admin_user().name, + 'admin_password': self._admin_user().password, + 'dbname': dbName}, + shell=True) + except exception.ProcessExecutionError: + LOG.exception(_( + "There was an error while deleting database:%s.") % dbName) + raise exception.GuestError(_("Unable to delete database: %s.") + % dbName) + else: + LOG.warning(_('Cannot delete a reserved database ' + '%(db)s') % {'db': dbName}) class CouchDBCredentials(object): diff --git a/trove/guestagent/datastore/experimental/postgresql/manager.py b/trove/guestagent/datastore/experimental/postgresql/manager.py index b491506862..a48d03167d 100644 --- a/trove/guestagent/datastore/experimental/postgresql/manager.py +++ b/trove/guestagent/datastore/experimental/postgresql/manager.py @@ -26,10 +26,10 @@ from .service.status import PgSqlAppStatus from trove.common import cfg from trove.common.notification import EndNotification -from trove.common import utils from trove.guestagent import backup from trove.guestagent.datastore.experimental.postgresql import pgutil from trove.guestagent.datastore import manager +from trove.guestagent.db import models from trove.guestagent import guest_log from trove.guestagent import volume @@ -61,25 +61,24 @@ class Manager( @property def datastore_log_defs(self): - owner = 'postgres' datastore_dir = '/var/log/postgresql/' long_query_time = CONF.get(self.manager).get( 'guest_log_long_query_time') general_log_file = self.build_log_file_name( - self.GUEST_LOG_DEFS_GENERAL_LABEL, owner, + self.GUEST_LOG_DEFS_GENERAL_LABEL, self.PGSQL_OWNER, datastore_dir=datastore_dir) general_log_dir, general_log_filename = os.path.split(general_log_file) return { self.GUEST_LOG_DEFS_GENERAL_LABEL: { self.GUEST_LOG_TYPE_LABEL: guest_log.LogType.USER, - self.GUEST_LOG_USER_LABEL: owner, + self.GUEST_LOG_USER_LABEL: self.PGSQL_OWNER, self.GUEST_LOG_FILE_LABEL: general_log_file, self.GUEST_LOG_ENABLE_LABEL: { 'logging_collector': 'on', - 'log_destination': self._quote_str('stderr'), - 'log_directory': self._quote_str(general_log_dir), - 'log_filename': self._quote_str(general_log_filename), - 'log_statement': self._quote_str('all'), + 'log_destination': self._quote('stderr'), + 'log_directory': self._quote(general_log_dir), + 'log_filename': self._quote(general_log_filename), + 'log_statement': self._quote('all'), 'debug_print_plan': 'on', 'log_min_duration_statement': long_query_time, }, @@ -90,9 +89,6 @@ class Manager( }, } - def _quote_str(self, value): - return "'%s'" % value - def do_prepare(self, context, packages, databases, memory_mb, users, device_path, mount_point, backup_info, config_contents, root_password, overrides, cluster_config, snapshot): @@ -118,11 +114,11 @@ class Manager( def _secure(self, context): # Create a new administrative user for Trove and also # disable the built-in superuser. - self.create_database(context, [{'_name': self.ADMIN_USER}]) - self._create_admin_user(context) + os_admin_db = models.PostgreSQLSchema(self.ADMIN_USER) + self._create_database(context, os_admin_db) + self._create_admin_user(context, databases=[os_admin_db]) pgutil.PG_ADMIN = self.ADMIN_USER - postgres = {'_name': self.PG_BUILTIN_ADMIN, - '_password': utils.generate_random_password()} + postgres = models.PostgreSQLRootUser() self.alter_user(context, postgres, 'NOSUPERUSER', 'NOLOGIN') def create_backup(self, context, backup_info): diff --git a/trove/guestagent/datastore/experimental/postgresql/pgutil.py b/trove/guestagent/datastore/experimental/postgresql/pgutil.py index b53cacdb15..0efda57c80 100644 --- a/trove/guestagent/datastore/experimental/postgresql/pgutil.py +++ b/trove/guestagent/datastore/experimental/postgresql/pgutil.py @@ -207,7 +207,10 @@ class UserQuery(object): @classmethod def update_name(cls, old, new): - """Query to update the name of a user.""" + """Query to update the name of a user. + This statement also results in an automatic permission transfer to the + new username. + """ return "ALTER USER \"{old}\" RENAME TO \"{new}\"".format( old=old, @@ -231,7 +234,8 @@ class AccessQuery(object): "SELECT datname, pg_encoding_to_char(encoding), datcollate " "FROM pg_database " "WHERE datistemplate = false " - "AND 'user {user}=CTc' = ANY (datacl)".format(user=user) + "AND 'user \"{user}\"=CTc/{admin}' = ANY (datacl)".format( + user=user, admin=PG_ADMIN) ) @classmethod diff --git a/trove/guestagent/datastore/experimental/postgresql/service/access.py b/trove/guestagent/datastore/experimental/postgresql/service/access.py index 51243649f8..f3b34944fd 100644 --- a/trove/guestagent/datastore/experimental/postgresql/service/access.py +++ b/trove/guestagent/datastore/experimental/postgresql/service/access.py @@ -18,6 +18,7 @@ from oslo_log import log as logging from trove.common import cfg from trove.common.i18n import _ from trove.guestagent.datastore.experimental.postgresql import pgutil +from trove.guestagent.db import models LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -73,21 +74,20 @@ class PgSqlAccess(object): def list_access(self, context, username, hostname): """List database for which the given user as access. - - The username and hostname parameters are strings. - - Return value is a list of dictionaries in the following form: - - [{"_name": "", "_collate": None, "_character_set": None}, ...] + Return a list of serialized Postgres databases. """ + + if self.user_exists(username): + return [db.serialize() for db in self._get_databases_for(username)] + + raise exception.UserNotFound(username) + + def _get_databases_for(self, username): + """Return all Postgres databases accessible by a given user.""" results = pgutil.query( pgutil.AccessQuery.list(user=username), timeout=30, ) - - # Convert to dictionaries. - results = ( - {'_name': r[0].strip(), '_collate': None, '_character_set': None} - for r in results - ) - return tuple(results) + return [models.PostgreSQLSchema( + row[0].strip(), character_set=row[1], collate=row[2]) + for row in results] diff --git a/trove/guestagent/datastore/experimental/postgresql/service/database.py b/trove/guestagent/datastore/experimental/postgresql/service/database.py index 52f1b1d887..1b174dcbe5 100644 --- a/trove/guestagent/datastore/experimental/postgresql/service/database.py +++ b/trove/guestagent/datastore/experimental/postgresql/service/database.py @@ -13,14 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. -import itertools - from oslo_log import log as logging from trove.common import cfg from trove.common.i18n import _ from trove.common.notification import EndNotification +from trove.common import pagination from trove.guestagent.datastore.experimental.postgresql import pgutil +from trove.guestagent.db import models LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -34,51 +34,58 @@ class PgSqlDatabase(object): def create_database(self, context, databases): """Create the list of specified databases. - The databases parameter is a list of dictionaries in the following - form: - - {"_name": "", "_character_set": "", "_collate": ""} - - Encoding and collation values are validated in - trove.guestagent.db.models. + The databases parameter is a list of serialized Postgres databases. """ with EndNotification(context): for database in databases: - encoding = database.get('_character_set') - collate = database.get('_collate') - LOG.info( - _("{guest_id}: Creating database {name}.").format( - guest_id=CONF.guest_id, - name=database['_name'], - ) - ) - pgutil.psql( - pgutil.DatabaseQuery.create( - name=database['_name'], - encoding=encoding, - collation=collate, - ), - timeout=30, - ) + self._create_database( + context, + models.PostgreSQLSchema.deserialize_schema(database)) + + def _create_database(self, context, database): + """Create a database. + + :param database: Database to be created. + :type database: PostgreSQLSchema + """ + LOG.info( + _("{guest_id}: Creating database {name}.").format( + guest_id=CONF.guest_id, + name=database.name, + ) + ) + pgutil.psql( + pgutil.DatabaseQuery.create( + name=database.name, + encoding=database.character_set, + collation=database.collate, + ), + timeout=30, + ) def delete_database(self, context, database): """Delete the specified database. - - The database parameter is a dictionary in the following form: - - {"_name": ""} """ with EndNotification(context): - LOG.info( - _("{guest_id}: Dropping database {name}.").format( - guest_id=CONF.guest_id, - name=database['_name'], - ) - ) - pgutil.psql( - pgutil.DatabaseQuery.drop(name=database['_name']), - timeout=30, + self._drop_database( + models.PostgreSQLSchema.deserialize_schema(database)) + + def _drop_database(self, database): + """Drop a given Postgres database. + + :param database: Database to be dropped. + :type database: PostgreSQLSchema + """ + LOG.info( + _("{guest_id}: Dropping database {name}.").format( + guest_id=CONF.guest_id, + name=database.name, ) + ) + pgutil.psql( + pgutil.DatabaseQuery.drop(name=database.name), + timeout=30, + ) def list_databases( self, @@ -87,42 +94,19 @@ class PgSqlDatabase(object): marker=None, include_marker=False, ): - """List databases created on this instance. - - Return value is a list of dictionaries in the following form: - - [{"_name": "", "_character_set": "", "_collate": ""}, ...] + """List all databases on the instance. + Return a paginated list of serialized Postgres databases. """ + page, next_name = pagination.paginate_object_list( + self._get_databases(), 'name', limit, marker, include_marker) + return [db.serialize() for db in page], next_name + + def _get_databases(self): + """Return all non-system Postgres databases on the instance.""" results = pgutil.query( pgutil.DatabaseQuery.list(ignore=cfg.get_ignored_dbs()), timeout=30, ) - # Convert results to dictionaries. - results = ( - {'_name': r[0].strip(), '_character_set': r[1], '_collate': r[2]} - for r in results - ) - # Force __iter__ of generator until marker found. - if marker is not None: - try: - item = next(results) - while item['_name'] != marker: - item = next(results) - except StopIteration: - pass - - remainder = None - if limit is not None: - remainder = results - results = itertools.islice(results, limit) - - results = tuple(results) - - next_marker = None - if remainder is not None: - try: - next_marker = next(remainder) - except StopIteration: - pass - - return results, next_marker + return [models.PostgreSQLSchema( + row[0].strip(), character_set=row[1], collate=row[2]) + for row in results] diff --git a/trove/guestagent/datastore/experimental/postgresql/service/root.py b/trove/guestagent/datastore/experimental/postgresql/service/root.py index 34ba9fa756..27f10046c0 100644 --- a/trove/guestagent/datastore/experimental/postgresql/service/root.py +++ b/trove/guestagent/datastore/experimental/postgresql/service/root.py @@ -14,10 +14,10 @@ # under the License. from trove.common import cfg -from trove.common import utils from trove.guestagent.datastore.experimental.postgresql import pgutil from trove.guestagent.datastore.experimental.postgresql.service.users import ( PgSqlUsers) +from trove.guestagent.db import models CONF = cfg.CONF @@ -72,18 +72,15 @@ class PgSqlRoot(PgSqlUsers): {"_name": "postgres", "_password": ""} """ - user = { - "_name": "postgres", - "_password": root_password or utils.generate_random_password(), - } + user = models.PostgreSQLRootUser(password=root_password) query = pgutil.UserQuery.alter_user( - user['_name'], - user['_password'], + user.name, + user.password, None, *self.ADMIN_OPTIONS ) pgutil.psql(query, timeout=30) - return user + return user.serialize() def disable_root(self, context): """Generate a new random password for the public superuser account. @@ -93,4 +90,4 @@ class PgSqlRoot(PgSqlUsers): self.enable_root(context) def enable_root_with_password(self, context, root_password=None): - self.enable_root(context, root_password) + return self.enable_root(context, root_password) diff --git a/trove/guestagent/datastore/experimental/postgresql/service/users.py b/trove/guestagent/datastore/experimental/postgresql/service/users.py index 829e9ddf3b..8cfb864aae 100644 --- a/trove/guestagent/datastore/experimental/postgresql/service/users.py +++ b/trove/guestagent/datastore/experimental/postgresql/service/users.py @@ -13,17 +13,19 @@ # License for the specific language governing permissions and limitations # under the License. -import itertools - from oslo_log import log as logging from trove.common import cfg +from trove.common import exception from trove.common.i18n import _ from trove.common.notification import EndNotification +from trove.common import pagination from trove.common import utils from trove.guestagent.datastore.experimental.postgresql import pgutil from trove.guestagent.datastore.experimental.postgresql.service.access import ( PgSqlAccess) +from trove.guestagent.db import models +from trove.guestagent.db.models import PostgreSQLSchema LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -51,32 +53,46 @@ class PgSqlUsers(PgSqlAccess): 'REPLICATION', 'LOGIN'] - def _create_admin_user(self, context): + def _create_admin_user(self, context, databases=None): """Create an administrative user for Trove. Force password encryption. """ password = utils.generate_random_password() - os_admin = {'_name': self.ADMIN_USER, '_password': password, - '_databases': [{'_name': self.ADMIN_USER}]} + os_admin = models.PostgreSQLUser(self.ADMIN_USER, password) + if databases: + os_admin.databases.extend([db.serialize() for db in databases]) self._create_user(context, os_admin, True, *self.ADMIN_OPTIONS) def create_user(self, context, users): """Create users and grant privileges for the specified databases. - The users parameter is a list of dictionaries in the following form: - - {"_name": "", "_password": "", "_databases": [{"_name": ""}, ...]} + The users parameter is a list of serialized Postgres users. """ with EndNotification(context): for user in users: - self._create_user(context, user, None) + self._create_user( + context, + models.PostgreSQLUser.deserialize_user(user), None) def _create_user(self, context, user, encrypt_password=None, *options): + """Create a user and grant privileges for the specified databases. + + :param user: User to be created. + :type user: PostgreSQLUser + + :param encrypt_password: Store passwords encrypted if True. + Fallback to configured default + behavior if None. + :type encrypt_password: boolean + + :param options: Other user options. + :type options: list + """ LOG.info( _("{guest_id}: Creating user {user} {with_clause}.") .format( guest_id=CONF.guest_id, - user=user['_name'], + user=user.name, with_clause=pgutil.UserQuery._build_with_clause( '', encrypt_password, @@ -86,18 +102,23 @@ class PgSqlUsers(PgSqlAccess): ) pgutil.psql( pgutil.UserQuery.create( - user['_name'], - user['_password'], + user.name, + user.password, encrypt_password, *options ), timeout=30, ) + self._grant_access( + context, user.name, + [PostgreSQLSchema.deserialize_schema(db) for db in user.databases]) + + def _grant_access(self, context, username, databases): self.grant_access( context, - user['_name'], + username, None, - [d['_name'] for d in user['_databases']], + [db.name for db in databases], ) def list_users( @@ -108,121 +129,111 @@ class PgSqlUsers(PgSqlAccess): include_marker=False, ): """List all users on the instance along with their access permissions. - - Return value is a list of dictionaries in the following form: - - [{"_name": "", "_password": None, "_host": None, - "_databases": [{"_name": ""}, ...]}, ...] + Return a paginated list of serialized Postgres users. """ + page, next_name = pagination.paginate_object_list( + self._get_users(context), 'name', limit, marker, include_marker) + return [db.serialize() for db in page], next_name + + def _get_users(self, context): + """Return all non-system Postgres users on the instance.""" results = pgutil.query( pgutil.UserQuery.list(ignore=cfg.get_ignored_users()), timeout=30, ) - # Convert results into dictionaries. - results = ( - { - '_name': r[0].strip(), - '_password': None, - '_host': None, - '_databases': self.list_access(context, r[0], None), - } - for r in results - ) + return [self._build_user(context, row[0].strip()) for row in results] - # Force __iter__ of generator until marker found. - if marker is not None: - try: - item = next(results) - while item['_name'] != marker: - item = next(results) - except StopIteration: - pass - - remainder = None - if limit is not None: - remainder = results - results = itertools.islice(results, limit) - - results = tuple(results) - - next_marker = None - if remainder is not None: - try: - next_marker = next(remainder) - except StopIteration: - pass - - return results, next_marker + def _build_user(self, context, username): + """Build a model representation of a Postgres user. + Include all databases it has access to. + """ + user = models.PostgreSQLUser(username) + dbs = self.list_access(context, username, None) + for d in dbs: + user.databases.append(d) + return user def delete_user(self, context, user): """Delete the specified user. - - The user parameter is a dictionary in the following form: - - {"_name": ""} """ with EndNotification(context): - LOG.info( - _("{guest_id}: Dropping user {name}.").format( - guest_id=CONF.guest_id, - name=user['_name'], - ) - ) - pgutil.psql( - pgutil.UserQuery.drop(name=user['_name']), - timeout=30, + self._drop_user(models.PostgreSQLUser.deserialize_user(user)) + + def _drop_user(self, user): + """Drop a given Postgres user. + + :param user: User to be dropped. + :type user: PostgreSQLUser + """ + LOG.info( + _("{guest_id}: Dropping user {name}.").format( + guest_id=CONF.guest_id, + name=user.name, ) + ) + pgutil.psql( + pgutil.UserQuery.drop(name=user.name), + timeout=30, + ) def get_user(self, context, username, hostname): - """Return a single user matching the criteria. + """Return a serialized representation of a user with a given name. + """ + user = self._find_user(context, username) + return user.serialize() if user is not None else None - The username and hostname parameter are strings. - - The return value is a dictionary in the following form: - - {"_name": "", "_host": None, "_password": None, - "_databases": [{"_name": ""}, ...]} - - Where "_databases" is a list of databases the user has access to. + def _find_user(self, context, username): + """Lookup a user with a given username. + Return a new Postgres user instance or raise if no match is found. """ results = pgutil.query( pgutil.UserQuery.get(name=username), timeout=30, ) - results = tuple(results) - if len(results) < 1: - return None - return { - "_name": results[0][0], - "_host": None, - "_password": None, - "_databases": self.list_access(context, username, None), - } + if results: + return self._build_user(context, username) + + return None + + def user_exists(self, username): + """Return whether a given user exists on the instance.""" + results = pgutil.query( + pgutil.UserQuery.get(name=username), + timeout=30, + ) + + return bool(results) def change_passwords(self, context, users): """Change the passwords of one or more existing users. - - The users parameter is a list of dictionaries in the following form: - - {"name": "", "password": ""} + The users parameter is a list of serialized Postgres users. """ with EndNotification(context): for user in users: - self.alter_user(context, user, None) + self.alter_user( + context, + models.PostgreSQLUser.deserialize_user(user), None) def alter_user(self, context, user, encrypt_password=None, *options): """Change the password and options of an existing users. - The user parameter is a dictionary of the following form: + :param user: User to be altered. + :type user: PostgreSQLUser - {"name": "", "password": ""} + :param encrypt_password: Store passwords encrypted if True. + Fallback to configured default + behavior if None. + :type encrypt_password: boolean + + :param options: Other user options. + :type options: list """ LOG.info( _("{guest_id}: Altering user {user} {with_clause}.") .format( guest_id=CONF.guest_id, - user=user['_name'], + user=user.name, with_clause=pgutil.UserQuery._build_with_clause( '', encrypt_password, @@ -232,8 +243,8 @@ class PgSqlUsers(PgSqlAccess): ) pgutil.psql( pgutil.UserQuery.alter_user( - user['_name'], - user['_password'], + user.name, + user.password, encrypt_password, *options), timeout=30, @@ -250,47 +261,42 @@ class PgSqlUsers(PgSqlAccess): Each key/value pair in user_attrs is optional. """ with EndNotification(context): - if user_attrs.get('password') is not None: - self.change_passwords( - context, - ( - { - "name": username, - "password": user_attrs['password'], - }, - ), - ) + user = self._build_user(context, username) + new_username = user_attrs.get('name') + new_password = user_attrs.get('password') - if user_attrs.get('name') is not None: - access = self.list_access(context, username, None) - LOG.info( - _("{guest_id}: Changing username for {old} to {new}." - ).format( - guest_id=CONF.guest_id, - old=username, - new=user_attrs['name'], - ) - ) - pgutil.psql( - pgutil.psql.UserQuery.update_name( - old=username, - new=user_attrs['name'], - ), - timeout=30, - ) - # Regrant all previous access after the name change. - LOG.info( - _("{guest_id}: Regranting permissions from {old} " - "to {new}.") - .format( - guest_id=CONF.guest_id, - old=username, - new=user_attrs['name'], - ) - ) - self.grant_access( - context, - username=user_attrs['name'], - hostname=None, - databases=(db['_name'] for db in access) - ) + if new_username is not None: + self._rename_user(context, user, new_username) + # Make sure we can retrieve the renamed user. + user = self._find_user(context, new_username) + if user is None: + raise exception.TroveError(_( + "Renamed user %s could not be found on the instance.") + % new_username) + + if new_password is not None: + user.password = new_password + self.alter_user(context, user) + + def _rename_user(self, context, user, new_username): + """Rename a given Postgres user and transfer all access to the + new name. + + :param user: User to be renamed. + :type user: PostgreSQLUser + """ + LOG.info( + _("{guest_id}: Changing username for {old} to {new}.").format( + guest_id=CONF.guest_id, + old=user.name, + new=new_username, + ) + ) + # PostgreSQL handles the permission transfer itself. + pgutil.psql( + pgutil.UserQuery.update_name( + old=user.name, + new=new_username, + ), + timeout=30, + ) diff --git a/trove/guestagent/db/models.py b/trove/guestagent/db/models.py index 6a2cb73c2c..7457804fe0 100644 --- a/trove/guestagent/db/models.py +++ b/trove/guestagent/db/models.py @@ -53,11 +53,23 @@ class Base(object): class DatastoreSchema(Base): """Represents a database schema.""" - def __init__(self): + def __init__(self, name, deserializing=False, *args, **kwargs): self._name = None self._collate = None self._character_set = None + # If both or neither are passed in this is a bug. + if not (bool(deserializing) != bool(name)): + raise RuntimeError("Bug in DatastoreSchema()") + if not deserializing: + self.name = name + + def __str__(self): + return str(self.name) + + def __repr__(self): + return str(self.serialize()) + @classmethod def deserialize_schema(cls, value): if not cls._validate_dict(value): @@ -65,7 +77,7 @@ class DatastoreSchema(Base): "Required: %(reqs)s") % ({'keys': value.keys(), 'reqs': cls._dict_requirements()})) - schema = cls(deserializing=True) + schema = cls(name=None, deserializing=True) schema.deserialize(value) return schema @@ -133,16 +145,8 @@ class MongoDBSchema(DatastoreSchema): name_regex = re.compile(r'^[a-zA-Z0-9_\-]+$') - def __init__(self, name=None, deserializing=False): - super(MongoDBSchema, self).__init__() - # need one or the other, not both, not none (!= ~ XOR) - if not (bool(deserializing) != bool(name)): - raise ValueError(_("Bad args. name: %(name)s, " - "deserializing %(deser)s.") - % ({'name': bool(name), - 'deser': bool(deserializing)})) - if not deserializing: - self.name = name + def __init__(self, name, *args, **kwargs): + super(MongoDBSchema, self).__init__(name, *args, **kwargs) @property def _max_schema_name_length(self): @@ -165,16 +169,8 @@ class CassandraSchema(DatastoreSchema): the first of which is an alpha character. """ - def __init__(self, name=None, deserializing=False): - super(CassandraSchema, self).__init__() - - if not (bool(deserializing) != bool(name)): - raise ValueError(_("Bad args. name: %(name)s, " - "deserializing %(deser)s.") - % ({'name': bool(name), - 'deser': bool(deserializing)})) - if not deserializing: - self.name = name + def __init__(self, name, *args, **kwargs): + super(CassandraSchema, self).__init__(name, *args, **kwargs) @property def _max_schema_name_length(self): @@ -201,17 +197,8 @@ class CouchDBSchema(DatastoreSchema): name_regex = re.compile(r'^[a-z][a-z0-9_$()+/-]*$') - def __init__(self, name=None, deserializing=False): - super(CouchDBSchema, self).__init__() - self._ignore_dbs = cfg.get_ignored_dbs() - # need one or the other, not both, not none (!= ~ XOR) - if not (bool(deserializing) != bool(name)): - raise ValueError(_("Bad args. name: %(name)s, " - "deserializing %(deser)s.") - % ({'name': bool(name), - 'deser': bool(deserializing)})) - if not deserializing: - self.name = name + def __init__(self, name, *args, **kwargs): + super(CouchDBSchema, self).__init__(name, *args, **kwargs) @property def _max_schema_name_length(self): @@ -219,8 +206,6 @@ class CouchDBSchema(DatastoreSchema): def _is_valid_schema_name(self, value): # https://wiki.apache.org/couchdb/HTTP_database_API - if value.lower() in self._ignore_dbs: - return False if re.match(r'^[a-z]*$', value[0]): return True else: @@ -240,15 +225,20 @@ class PostgreSQLSchema(DatastoreSchema): """ name_regex = re.compile(u(r'^[\u0001-\u007F\u0080-\uFFFF]+[^\s]$')) - def __init__(self, name=None, deserializing=False): - super(PostgreSQLSchema, self).__init__() - if not (bool(deserializing) != bool(name)): - raise ValueError(_("Bad args. name: %(name)s, " - "deserializing %(deser)s.") - % ({'name': bool(name), - 'deser': bool(deserializing)})) - if not deserializing: - self.name = name + def __init__(self, name, character_set=None, collate=None, + *args, **kwargs): + super(PostgreSQLSchema, self).__init__(name, *args, **kwargs) + + self.character_set = character_set + self.collate = collate + + @DatastoreSchema.collate.setter + def collate(self, value): + self._collate = value + + @DatastoreSchema.character_set.setter + def character_set(self, value): + self._character_set = value @property def _max_schema_name_length(self): @@ -586,12 +576,28 @@ class DatastoreUser(Base): _HOSTNAME_WILDCARD = '%' - def __init__(self): + def __init__(self, name, password, deserializing=False, *args, **kwargs): self._name = None self._password = None self._host = None self._databases = [] + # need only one of: deserializing, name, or (name and password) + if ((not (bool(deserializing) != bool(name))) or + (bool(deserializing) and bool(password))): + raise RuntimeError("Bug in DatastoreUser()") + if not deserializing: + if name: + self.name = name + if password is not None: + self.password = password + + def __str__(self): + return str(self.name) + + def __repr__(self): + return str(self.serialize()) + @classmethod def deserialize_user(cls, value): if not cls._validate_dict(value): @@ -599,7 +605,7 @@ class DatastoreUser(Base): "Required: %(reqs)s") % ({'keys': value.keys(), 'reqs': cls._dict_requirements()})) - user = cls(deserializing=True) + user = cls(name=None, password=None, deserializing=True) user.deserialize(value) return user @@ -713,24 +719,11 @@ class MongoDBUser(DatastoreUser): Trove stores this as . """ - def __init__(self, name=None, password=None, deserializing=False): - super(MongoDBUser, self).__init__() - self._name = None + def __init__(self, name=None, password=None, *args, **kwargs): self._username = None self._database = None self._roles = [] - # need only one of: deserializing, name, or (name and password) - if ((not (bool(deserializing) != bool(name))) or - (bool(deserializing) and bool(password))): - raise ValueError(_("Bad args. name: %(name)s, " - "password %(pass)s, " - "deserializing %(deser)s.") - % ({'name': bool(name), - 'pass': bool(password), - 'deser': bool(deserializing)})) - if not deserializing: - self.name = name - self.password = password + super(MongoDBUser, self).__init__(name, password, *args, **kwargs) @property def username(self): @@ -855,20 +848,8 @@ class MongoDBUser(DatastoreUser): class CassandraUser(DatastoreUser): """Represents a Cassandra user and its associated properties.""" - def __init__(self, name=None, password=None, deserializing=False): - super(CassandraUser, self).__init__() - - if ((not (bool(deserializing) != bool(name))) or - (bool(deserializing) and bool(password))): - raise ValueError(_("Bad args. name: %(name)s, " - "password %(pass)s, " - "deserializing %(deser)s.") - % ({'name': bool(name), - 'pass': bool(password), - 'deser': bool(deserializing)})) - if not deserializing: - self.name = name - self.password = password + def __init__(self, name, password=None, *args, **kwargs): + super(CassandraUser, self).__init__(name, password, *args, **kwargs) def _build_database_schema(self, name): return CassandraSchema(name) @@ -894,60 +875,28 @@ class CassandraUser(DatastoreUser): class CouchDBUser(DatastoreUser): """Represents a CouchDB user and its associated properties.""" - def __init__(self): - self._name = None - self._host = None - self._password = None - self._databases = [] - self._ignore_users = cfg.get_ignored_users() + def __init__(self, name, password=None, *args, **kwargs): + super(CouchDBUser, self).__init__(name, password, *args, **kwargs) - def _is_valid(self, value): + def _build_database_schema(self, name): + return CouchDBSchema(name) + + @property + def _max_username_length(self): + return None + + def _is_valid_name(self, value): return True - @property - def name(self): - return self._name + def _is_valid_host_name(self, value): + return True - @name.setter - def name(self, value): - if not self._is_valid(value): - raise ValueError(_("'%s' is not a valid user name.") % value) - else: - self._name = value + def _is_valid_password(self, value): + return True - @property - def password(self): - return self._password - - @password.setter - def password(self, value): - if not self._is_valid(value): - raise ValueError(_("'%s' is not a valid password.") % value) - else: - self._password = value - - @property - def databases(self): - return self._databases - - @databases.setter - def databases(self, value): - mydb = ValidatedMySQLDatabase() - mydb.name = value - self._databases.append(mydb.serialize()) - - @property - def host(self): - if self._host is None: - return '%' - return self._host - - @host.setter - def host(self, value): - if not self._is_valid_host_name(value): - raise ValueError(_("'%s' is not a valid hostname.") % value) - else: - self._host = value + @classmethod + def _dict_requirements(cls): + return ['_name'] class MySQLUser(Base): @@ -1046,21 +995,8 @@ class MySQLUser(Base): class PostgreSQLUser(DatastoreUser): """Represents a PostgreSQL user and its associated properties.""" - def __init__(self, name=None, password=None, deserializing=False): - super(PostgreSQLUser, self).__init__() - - if ((not (bool(deserializing) != bool(name))) or - (bool(deserializing) and bool(password))): - raise ValueError(_("Bad args. name: %(name)s, " - "password %(pass)s, " - "deserializing %(deser)s.") - % ({'name': bool(name), - 'pass': bool(password), - 'deser': bool(deserializing)})) - - if not deserializing: - self.name = name - self.password = password + def __init__(self, name, password=None, *args, **kwargs): + super(PostgreSQLUser, self).__init__(name, password, *args, **kwargs) def _build_database_schema(self, name): return PostgreSQLSchema(name) @@ -1080,7 +1016,7 @@ class PostgreSQLUser(DatastoreUser): @classmethod def _dict_requirements(cls): - return ['name'] + return ['_name'] class RootUser(MySQLUser): @@ -1116,6 +1052,17 @@ class CassandraRootUser(CassandraUser): class PostgreSQLRootUser(PostgreSQLUser): """Represents the PostgreSQL default superuser.""" - def __init__(self, password=None): + def __init__(self, password=None, *args, **kwargs): password = password if not None else utils.generate_random_password() - super(PostgreSQLRootUser, self).__init__("postgres", password=password) + super(PostgreSQLRootUser, self).__init__("postgres", password=password, + *args, **kwargs) + + +class CouchDBRootUser(CouchDBUser): + """Represents the CouchDB default superuser.""" + + def __init__(self, password=None, *args, **kwargs): + if password is None: + password = utils.generate_random_password() + super(CouchDBRootUser, self).__init__("root", password=password, + *args, **kwargs) diff --git a/trove/tests/unittests/common/test_pagination.py b/trove/tests/unittests/common/test_pagination.py index 38d54d8cc7..9bd8e8d67c 100644 --- a/trove/tests/unittests/common/test_pagination.py +++ b/trove/tests/unittests/common/test_pagination.py @@ -15,6 +15,9 @@ # License for the specific language governing permissions and limitations # under the License. # + +from mock import Mock + from trove.common import pagination from trove.tests.unittests import trove_testtools @@ -41,7 +44,8 @@ class TestPaginatedDataView(trove_testtools.TestCase): def _do_paginate_list(self, limit=None, marker=None, include_marker=False): li = ['a', 'b', 'c', 'd', 'e'] - return pagination.paginate_list(li, limit, marker, include_marker) + return pagination.paginate_list(li, limit=limit, marker=marker, + include_marker=include_marker) def test_paginate_list(self): # start list @@ -75,3 +79,48 @@ class TestPaginatedDataView(trove_testtools.TestCase): li_4, marker_4 = self._do_paginate_list(marker='f') self.assertEqual([], li_4) self.assertIsNone(marker_4) + + li_5, marker_5 = self._do_paginate_list(limit=1, marker='f') + self.assertEqual([], li_5) + self.assertIsNone(marker_5) + + def test_dict_paginate(self): + li = [{'_collate': 'en_US.UTF-8', + '_character_set': 'UTF8', + '_name': 'db1'}, + {'_collate': 'en_US.UTF-8', + '_character_set': 'UTF8', + '_name': 'db3'}, + {'_collate': 'en_US.UTF-8', + '_character_set': 'UTF8', + '_name': 'db2'}, + {'_collate': 'en_US.UTF-8', + '_character_set': 'UTF8', + '_name': 'db5'}, + {'_collate': 'en_US.UTF-8', + '_character_set': 'UTF8', + '_name': 'db4'} + ] + + l, m = pagination.paginate_dict_list(li, '_name', limit=1, + marker='db1', + include_marker=True) + self.assertEqual(l[0], li[0]) + self.assertEqual(m, 'db1') + + def test_object_paginate(self): + + def build_mock_object(name): + o = Mock() + o.name = name + o.attr = 'attr' + return o + + li = [build_mock_object('db1'), build_mock_object('db2'), + build_mock_object('db3')] + + l, m = pagination.paginate_object_list(li, 'name', limit=1, + marker='db1', + include_marker=True) + self.assertEqual(l[0], li[0]) + self.assertEqual(m, 'db1')