
1 new changeset in galaxy-central: http://bitbucket.org/galaxy/galaxy-central/changeset/f018b1450140/ changeset: f018b1450140 user: natefoo date: 2011-08-04 16:39:16 summary: User/group disk quotas. affected #: 26 files (42.9 KB) --- a/lib/galaxy/app.py Thu Aug 04 16:55:13 2011 +1000 +++ b/lib/galaxy/app.py Thu Aug 04 10:39:16 2011 -0400 @@ -7,6 +7,7 @@ import galaxy.model import galaxy.datatypes.registry import galaxy.security +import galaxy.quota from galaxy.tags.tag_handler import GalaxyTagHandler from galaxy.tools.imp_exp import load_history_imp_exp_tools from galaxy.sample_tracking import external_service_types @@ -57,6 +58,11 @@ #Load security policy self.security_agent = self.model.security_agent self.host_security_agent = galaxy.security.HostAgent( model=self.security_agent.model, permitted_actions=self.security_agent.permitted_actions ) + # Load quota management + if self.config.enable_quotas: + self.quota_agent = galaxy.quota.QuotaAgent( self.model ) + else: + self.quota_agent = galaxy.quota.NoQuotaAgent( self.model ) # Heartbeat and memdump for thread / heap profiling self.heartbeat = None self.memdump = None --- a/lib/galaxy/config.py Thu Aug 04 16:55:13 2011 +1000 +++ b/lib/galaxy/config.py Thu Aug 04 10:39:16 2011 -0400 @@ -45,6 +45,7 @@ # web API self.enable_api = string_as_bool( kwargs.get( 'enable_api', False ) ) self.enable_openid = string_as_bool( kwargs.get( 'enable_openid', False ) ) + self.enable_quotas = string_as_bool( kwargs.get( 'enable_quotas', False ) ) self.tool_path = resolve_path( kwargs.get( "tool_path", "tools" ), self.root ) self.tool_data_path = resolve_path( kwargs.get( "tool_data_path", "tool-data" ), os.getcwd() ) self.len_file_path = kwargs.get( "len_file_path", resolve_path(os.path.join(self.tool_data_path, 'shared','ucsc','chrom'), self.root) ) --- a/lib/galaxy/jobs/__init__.py Thu Aug 04 16:55:13 2011 +1000 +++ b/lib/galaxy/jobs/__init__.py Thu Aug 04 10:39:16 2011 -0400 @@ -203,7 +203,7 @@ elif job_state == JOB_DELETED: log.info( "job %d deleted by user while still queued" % job.id ) elif job_state == JOB_ADMIN_DELETED: - job.info( "job %d deleted by admin while still queued" % job.id ) + log.info( "job %d deleted by admin while still queued" % job.id ) else: log.error( "unknown job state '%s' for job %d" % ( job_state, job.id ) ) if not self.track_jobs_in_database: @@ -229,6 +229,15 @@ return JOB_DELETED elif job.state == model.Job.states.ERROR: return JOB_ADMIN_DELETED + elif self.app.config.enable_quotas: + quota = self.app.quota_agent.get_quota( job.user ) + if quota is not None: + try: + usage = self.app.quota_agent.get_usage( user=job.user, history=job.history ) + if usage > quota: + return JOB_WAIT + except AssertionError, e: + pass # No history, should not happen with an anon user for dataset_assoc in job.input_datasets + job.input_library_datasets: idata = dataset_assoc.dataset if not idata: --- a/lib/galaxy/model/__init__.py Thu Aug 04 16:55:13 2011 +1000 +++ b/lib/galaxy/model/__init__.py Thu Aug 04 10:39:16 2011 -0400 @@ -483,6 +483,53 @@ self.type = type self.deleted = deleted +class UserQuotaAssociation( object ): + def __init__( self, user, quota ): + self.user = user + self.quota = quota + +class GroupQuotaAssociation( object ): + def __init__( self, group, quota ): + self.group = group + self.quota = quota + +class Quota( object ): + valid_operations = ( '+', '-', '=' ) + def __init__( self, name="", description="", amount=0, operation="=" ): + self.name = name + self.description = description + if amount is None: + self.bytes = -1 + else: + self.bytes = amount + self.operation = operation + def get_amount( self ): + if self.bytes == -1: + return None + return self.bytes + def set_amount( self, amount ): + if amount is None: + self.bytes = -1 + else: + self.bytes = amount + amount = property( get_amount, set_amount ) + @property + def display_amount( self ): + if self.bytes == -1: + return "unlimited" + else: + return util.nice_size( self.bytes ) + +class DefaultQuotaAssociation( Quota ): + types = Bunch( + UNREGISTERED = 'unregistered', + REGISTERED = 'registered' + ) + def __init__( self, type, quota ): + assert type in self.types.__dict__.values(), 'Invalid type' + self.type = type + self.quota = quota + class DatasetPermissions( object ): def __init__( self, action, dataset, role ): self.action = action --- a/lib/galaxy/model/mapping.py Thu Aug 04 16:55:13 2011 +1000 +++ b/lib/galaxy/model/mapping.py Thu Aug 04 10:39:16 2011 -0400 @@ -197,6 +197,37 @@ Column( "type", String( 40 ), index=True ), Column( "deleted", Boolean, index=True, default=False ) ) +UserQuotaAssociation.table = Table( "user_quota_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "user_id", Integer, ForeignKey( "galaxy_user.id" ), index=True ), + Column( "quota_id", Integer, ForeignKey( "quota.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +GroupQuotaAssociation.table = Table( "group_quota_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "group_id", Integer, ForeignKey( "galaxy_group.id" ), index=True ), + Column( "quota_id", Integer, ForeignKey( "quota.id" ), index=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ) ) + +Quota.table = Table( "quota", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "name", String( 255 ), index=True, unique=True ), + Column( "description", TEXT ), + Column( "bytes", Integer ), + Column( "operation", String( 8 ) ), + Column( "deleted", Boolean, index=True, default=False ) ) + +DefaultQuotaAssociation.table = Table( "default_quota_association", metadata, + Column( "id", Integer, primary_key=True ), + Column( "create_time", DateTime, default=now ), + Column( "update_time", DateTime, default=now, onupdate=now ), + Column( "type", String( 32 ), index=True, unique=True ), + Column( "quota_id", Integer, ForeignKey( "quota.id" ), index=True ) ) + DatasetPermissions.table = Table( "dataset_permissions", metadata, Column( "id", Integer, primary_key=True ), Column( "create_time", DateTime, default=now ), @@ -1251,6 +1282,21 @@ ) ) +assign_mapper( context, Quota, Quota.table, + properties=dict( users=relation( UserQuotaAssociation ), + groups=relation( GroupQuotaAssociation ) ) ) + +assign_mapper( context, UserQuotaAssociation, UserQuotaAssociation.table, + properties=dict( user=relation( User, backref="quotas" ), + quota=relation( Quota ) ) ) + +assign_mapper( context, GroupQuotaAssociation, GroupQuotaAssociation.table, + properties=dict( group=relation( Group, backref="quotas" ), + quota=relation( Quota ) ) ) + +assign_mapper( context, DefaultQuotaAssociation, DefaultQuotaAssociation.table, + properties=dict( quota=relation( Quota, backref="default" ) ) ) + assign_mapper( context, DatasetPermissions, DatasetPermissions.table, properties=dict( dataset=relation( Dataset, backref="actions" ), --- a/lib/galaxy/util/__init__.py Thu Aug 04 16:55:13 2011 +1000 +++ b/lib/galaxy/util/__init__.py Thu Aug 04 10:39:16 2011 -0400 @@ -542,6 +542,32 @@ return "%.1f %s" % (size, word) return '??? bytes' +def size_to_bytes( size ): + """ + Returns a number of bytes if given a reasably formatted string with the size + """ + # Assume input in bytes if we can convert directly to an int + try: + return int( size ) + except: + pass + # Otherwise it must have non-numeric characters + size_re = re.compile( '([\d\.]+)\s*([tgmk]b?|b|bytes?)$' ) + size_match = re.match( size_re, size.lower() ) + assert size_match is not None + size = float( size_match.group(1) ) + multiple = size_match.group(2) + if multiple.startswith( 't' ): + return int( size * 1024**4 ) + elif multiple.startswith( 'g' ): + return int( size * 1024**3 ) + elif multiple.startswith( 'm' ): + return int( size * 1024**2 ) + elif multiple.startswith( 'k' ): + return int( size * 1024 ) + elif multiple.startswith( 'b' ): + return int( size ) + def send_mail( frm, to, subject, body, config ): """ Sends an email. --- a/lib/galaxy/web/base/controller.py Thu Aug 04 16:55:13 2011 +1000 +++ b/lib/galaxy/web/base/controller.py Thu Aug 04 10:39:16 2011 -0400 @@ -1111,6 +1111,7 @@ user_list_grid = None role_list_grid = None group_list_grid = None + quota_list_grid = None @web.expose @web.require_admin @@ -1518,6 +1519,481 @@ message=util.sanitize_text( message ), status='done' ) ) + # Galaxy Quota Stuff + @web.expose + @web.require_admin + def quotas( self, trans, **kwargs ): + if 'operation' in kwargs: + operation = kwargs['operation'].lower() + if operation == "quotas": + return self.quota( trans, **kwargs ) + if operation == "create": + return self.create_quota( trans, **kwargs ) + if operation == "delete": + return self.mark_quota_deleted( trans, **kwargs ) + if operation == "undelete": + return self.undelete_quota( trans, **kwargs ) + if operation == "purge": + return self.purge_quota( trans, **kwargs ) + if operation == "change amount": + return self.edit_quota( trans, **kwargs ) + if operation == "manage users and groups": + return self.manage_users_and_groups_for_quota( trans, **kwargs ) + if operation == "rename": + return self.rename_quota( trans, **kwargs ) + if operation == "edit": + return self.edit_quota( trans, **kwargs ) + # Render the list view + return self.quota_list_grid( trans, **kwargs ) + + @web.expose + @web.require_admin + def create_quota( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + name = util.restore_text( params.get( 'name', '' ) ) + description = util.restore_text( params.get( 'description', '' ) ) + amount = util.restore_text( params.get( 'amount', '' ).strip() ) + if amount.lower() in ( 'unlimited', 'none', 'no limit' ): + create_amount = None + else: + try: + create_amount = util.size_to_bytes( amount ) + except AssertionError: + create_amount = False + operation = params.get( 'operation', '' ) + default = params.get( 'default', 'no' ) + in_users = util.listify( params.get( 'in_users', [] ) ) + out_users = util.listify( params.get( 'out_users', [] ) ) + in_groups = util.listify( params.get( 'in_groups', [] ) ) + out_groups = util.listify( params.get( 'out_groups', [] ) ) + if params.get( 'create_quota_button', False ): + if not name or not description: + message = "Enter a valid name and a description." + status = 'error' + elif trans.sa_session.query( trans.app.model.Quota ).filter( trans.app.model.Quota.table.c.name==name ).first(): + message = "Quota names must be unique and a quota with that name already exists, so choose another name." + status = 'error' + elif not params.get( 'amount', None ): + message = "Enter a valid quota amount." + status = 'error' + elif create_amount is False: + message = "Unable to parse the provided amount." + status = 'error' + elif operation not in trans.app.model.Quota.valid_operations: + message = "Enter a valid operation." + status = 'error' + elif default != 'no' and default not in trans.app.model.DefaultQuotaAssociation.types.__dict__.values(): + message = "Enter a valid default type." + status = 'error' + elif default != 'no' and operation != '=': + message = "Operation for a default quota must be '='." + status = 'error' + operation = '=' + else: + # Create the quota + quota = trans.app.model.Quota( name=name, description=description, amount=create_amount, operation=operation ) + trans.sa_session.add( quota ) + # If this is a default quota, create the DefaultQuotaAssociation + if default != 'no': + trans.app.quota_agent.set_default_quota( default, quota ) + else: + # Create the UserQuotaAssociations + for user in [ trans.sa_session.query( trans.app.model.User ).get( x ) for x in in_users ]: + uqa = trans.app.model.UserQuotaAssociation( user, quota ) + trans.sa_session.add( uqa ) + # Create the GroupQuotaAssociations + for group in [ trans.sa_session.query( trans.app.model.Group ).get( x ) for x in in_groups ]: + gqa = trans.app.model.GroupQuotaAssociation( group, quota ) + trans.sa_session.add( gqa ) + trans.sa_session.flush() + message = "Quota '%s' has been created with %d associated users and %d associated groups." % ( quota.name, len( in_users ), len( in_groups ) ) + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + in_users = map( int, in_users ) + in_groups = map( int, in_groups ) + new_in_users = [] + new_in_groups = [] + for user in trans.sa_session.query( trans.app.model.User ) \ + .filter( trans.app.model.User.table.c.deleted==False ) \ + .order_by( trans.app.model.User.table.c.email ): + if user.id in in_users: + new_in_users.append( ( user.id, user.email ) ) + else: + out_users.append( ( user.id, user.email ) ) + for group in trans.sa_session.query( trans.app.model.Group ) \ + .filter( trans.app.model.Group.table.c.deleted==False ) \ + .order_by( trans.app.model.Group.table.c.name ): + if group.id in in_groups: + new_in_groups.append( ( group.id, group.name ) ) + else: + out_groups.append( ( group.id, group.name ) ) + return trans.fill_template( '/admin/quota/quota_create.mako', + webapp=webapp, + name=name, + description=description, + amount=amount, + operation=operation, + default=default, + in_users=new_in_users, + out_users=out_users, + in_groups=new_in_groups, + out_groups=out_groups, + message=message, + status=status ) + @web.expose + @web.require_admin + def rename_quota( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + id = params.get( 'id', None ) + error = True + try: + assert id, 'No quota ids received for renaming' + quota = get_quota( trans, id ) + assert quota, 'Quota id (%s) is invalid' % id + assert quota.id != 1, 'The default quota cannot be renamed' + error = False + except AssertionError, e: + message = str( e ) + if error: + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + if params.get( 'rename_quota_button', False ): + old_name = quota.name + new_name = util.restore_text( params.name ) + new_description = util.restore_text( params.description ) + if not new_name: + message = 'Enter a valid name' + status='error' + elif trans.sa_session.query( trans.app.model.Quota ).filter( trans.app.model.Quota.table.c.name==new_name ).first(): + message = 'A quota with that name already exists' + status = 'error' + else: + quota.name = new_name + quota.description = new_description + trans.sa_session.add( quota ) + trans.sa_session.flush() + message = "Quota '%s' has been renamed to '%s'" % ( old_name, new_name ) + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + return trans.fill_template( '/admin/quota/quota_rename.mako', + quota=quota, + webapp=webapp, + message=message, + status=status ) + @web.expose + @web.require_admin + def manage_users_and_groups_for_quota( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + id = params.get( 'id', None ) + error = True + try: + assert id, 'No quota ids received for managing users and groups' + quota = get_quota( trans, id ) + assert quota, 'Quota id (%s) is invalid' % id + assert not quota.default, 'Default quotas cannot be associated with specific users and groups' + error = False + except AssertionError, e: + message = str( e ) + if error: + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + if params.get( 'quota_members_edit_button', False ): + in_users = [ trans.sa_session.query( trans.app.model.User ).get( x ) for x in util.listify( params.in_users ) ] + in_groups = [ trans.sa_session.query( trans.app.model.Group ).get( x ) for x in util.listify( params.in_groups ) ] + trans.app.quota_agent.set_entity_quota_associations( quotas=[ quota ], users=in_users, groups=in_groups ) + trans.sa_session.refresh( quota ) + message = "Quota '%s' has been updated with %d associated users and %d associated groups" % ( quota.name, len( in_users ), len( in_groups ) ) + trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status=status ) ) + in_users = [] + out_users = [] + in_groups = [] + out_groups = [] + for user in trans.sa_session.query( trans.app.model.User ) \ + .filter( trans.app.model.User.table.c.deleted==False ) \ + .order_by( trans.app.model.User.table.c.email ): + if user in [ x.user for x in quota.users ]: + in_users.append( ( user.id, user.email ) ) + else: + out_users.append( ( user.id, user.email ) ) + for group in trans.sa_session.query( trans.app.model.Group ) \ + .filter( trans.app.model.Group.table.c.deleted==False ) \ + .order_by( trans.app.model.Group.table.c.name ): + if group in [ x.group for x in quota.groups ]: + in_groups.append( ( group.id, group.name ) ) + else: + out_groups.append( ( group.id, group.name ) ) + return trans.fill_template( '/admin/quota/quota.mako', + quota=quota, + in_users=in_users, + out_users=out_users, + in_groups=in_groups, + out_groups=out_groups, + webapp=webapp, + message=message, + status=status ) + @web.expose + @web.require_admin + def edit_quota( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + id = params.get( 'id', None ) + if not id: + message = "No quota ids received for editing" + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + quota = get_quota( trans, id ) + if params.get( 'edit_quota_button', False ): + amount = util.restore_text( params.get( 'amount', '' ).strip() ) + if amount.lower() in ( 'unlimited', 'none', 'no limit' ): + new_amount = None + else: + try: + new_amount = util.size_to_bytes( amount ) + except AssertionError: + new_amount = False + operation = params.get( 'operation', None ) + if not params.get( 'amount', None ): + message = 'Enter a valid amount' + status='error' + elif new_amount is False: + message = 'Unable to parse the provided amount' + status = 'error' + elif operation not in trans.app.model.Quota.valid_operations: + message = 'Enter a valid operation' + status = 'error' + else: + quota.amount = new_amount + quota.operation = operation + trans.sa_session.add( quota ) + trans.sa_session.flush() + message = "Quota '%s' is now '%s'" % ( quota.name, quota.operation + quota.display_amount ) + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + + return trans.fill_template( '/admin/quota/quota_edit.mako', + quota=quota, + webapp=webapp, + message=message, + status=status ) + @web.expose + @web.require_admin + def set_quota_default( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + default = params.get( 'default', '' ) + id = params.get( 'id', None ) + if not id: + message = "No quota ids received for managing defaults" + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + quota = get_quota( trans, id ) + if params.get( 'set_default_quota_button', False ): + if default != 'no' and default not in trans.app.model.DefaultQuotaAssociation.types.__dict__.values(): + message = "Enter a valid default type." + status = 'error' + else: + if default != 'no': + trans.app.quota_agent.set_default_quota( default, quota ) + message = "Quota '%s' is now the default for %s users" % ( quota.name, default ) + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + if not default: + default = 'no' + return trans.fill_template( '/admin/quota/quota_set_default.mako', + quota=quota, + webapp=webapp, + default=default, + message=message, + status=status ) + @web.expose + @web.require_admin + def unset_quota_default( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + message = util.restore_text( params.get( 'message', '' ) ) + status = params.get( 'status', 'done' ) + default = params.get( 'default', '' ) + id = params.get( 'id', None ) + if not id: + message = "No quota ids received for managing defaults" + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + quota = get_quota( trans, id ) + if not quota.default: + message = "Quota '%s' is not a default." % quota.name + status = 'error' + else: + message = "Quota '%s' is no longer the default for %s users." % ( quota.name, quota.default[0].type ) + status = 'done' + for dqa in quota.default: + trans.sa_session.delete( dqa ) + trans.sa_session.flush() + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status=status ) ) + + @web.expose + @web.require_admin + def mark_quota_deleted( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + id = kwd.get( 'id', None ) + ids = util.listify( id ) + error = True + quotas = [] + try: + assert id, 'No quota ids received for deleting' + for quota_id in ids: + quota = get_quota( trans, quota_id ) + assert quota, 'Quota id (%s) is invalid' % id + assert not quota.default, "Quota '%s' is a default, please unset it as a default before deleting it" % ( quota.name ) + quotas.append( quota ) + error = False + except AssertionError, e: + message = str( e ) + if error: + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + message = "Deleted %d quotas: " % len( ids ) + for quota in quotas: + quota.deleted = True + trans.sa_session.add( quota ) + message += " %s " % quota.name + trans.sa_session.flush() + trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + @web.expose + @web.require_admin + def undelete_quota( self, trans, **kwd ): + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + id = kwd.get( 'id', None ) + ids = util.listify( id ) + error = True + quotas = [] + try: + assert id, 'No quota ids received for undeleting' + for quota_id in ids: + quota = get_quota( trans, quota_id ) + assert quota, 'Quota id (%s) is invalid' % id + assert quota.deleted, "Quota '%s' has not been deleted, so it cannot be undeleted." % quota.name + quotas.append( quota ) + error = False + except AssertionError, e: + message = str( e ) + if error: + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + message = "Undeleted %d quotas: " % len( ids ) + for quota in quotas: + quota.deleted = False + trans.sa_session.add( quota ) + trans.sa_session.flush() + message += " %s " % quota.name + trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) + @web.expose + @web.require_admin + def purge_quota( self, trans, **kwd ): + # This method should only be called for a Quota that has previously been deleted. + # Purging a deleted Quota deletes all of the following from the database: + # - UserQuotaAssociations where quota_id == Quota.id + # - GroupQuotaAssociations where quota_id == Quota.id + params = util.Params( kwd ) + webapp = params.get( 'webapp', 'galaxy' ) + id = kwd.get( 'id', None ) + ids = util.listify( id ) + error = True + quotas = [] + try: + assert id, 'No quota ids received for undeleting' + for quota_id in ids: + quota = get_quota( trans, quota_id ) + assert quota, 'Quota id (%s) is invalid' % id + assert quota.deleted, "Quota '%s' has not been deleted, so it cannot be purged." % quota.name + quotas.append( quota ) + error = False + except AssertionError, e: + message = str( e ) + if error: + return trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=message, + status='error' ) ) + message = "Purged %d quotas: " % len( ids ) + for quota in quotas: + # Delete UserQuotaAssociations + for uqa in quota.users: + trans.sa_session.delete( uqa ) + # Delete GroupQuotaAssociations + for gqa in quota.groups: + trans.sa_session.delete( gqa ) + trans.sa_session.flush() + message += " %s " % quota.name + trans.response.send_redirect( web.url_for( controller='admin', + action='quotas', + webapp=webapp, + message=util.sanitize_text( message ), + status='done' ) ) # Galaxy Group Stuff @web.expose @web.require_admin @@ -2235,3 +2711,9 @@ if not group: return trans.show_error_message( "Group not found for id (%s)" % str( id ) ) return group +def get_quota( trans, id ): + """Get a Quota from the database by id.""" + # Load user from database + id = trans.security.decode_id( id ) + quota = trans.sa_session.query( trans.model.Quota ).get( id ) + return quota --- a/lib/galaxy/web/controllers/admin.py Thu Aug 04 16:55:13 2011 +1000 +++ b/lib/galaxy/web/controllers/admin.py Thu Aug 04 10:39:16 2011 -0400 @@ -268,8 +268,121 @@ preserve_state = False use_paging = True +class QuotaListGrid( grids.Grid ): + class NameColumn( grids.TextColumn ): + def get_value( self, trans, grid, quota ): + return quota.name + class DescriptionColumn( grids.TextColumn ): + def get_value( self, trans, grid, quota ): + if quota.description: + return quota.description + return '' + class AmountColumn( grids.TextColumn ): + def get_value( self, trans, grid, quota ): + return quota.operation + quota.display_amount + class StatusColumn( grids.GridColumn ): + def get_value( self, trans, grid, quota ): + if quota.deleted: + return "deleted" + elif quota.default: + return "<strong>default for %s users</strong>" % quota.default[0].type + return "" + class UsersColumn( grids.GridColumn ): + def get_value( self, trans, grid, quota ): + if quota.users: + return len( quota.users ) + return 0 + class GroupsColumn( grids.GridColumn ): + def get_value( self, trans, grid, quota ): + if quota.groups: + return len( quota.groups ) + return 0 + + # Grid definition + webapp = "galaxy" + title = "Quotas" + model_class = model.Quota + template='/admin/quota/grid.mako' + default_sort_key = "name" + columns = [ + NameColumn( "Name", + key="name", + link=( lambda item: dict( operation="Manage users and groups", id=item.id, webapp="galaxy" ) if not item.default else dict( operation="Change amount", id=item.id, webapp="galaxy" ) ), + model_class=model.Quota, + attach_popup=True, + filterable="advanced" ), + DescriptionColumn( "Description", + key='description', + model_class=model.Quota, + attach_popup=False, + filterable="advanced" ), + AmountColumn( "Amount", + key='amount', + model_class=model.Quota, + attach_popup=False, + filterable="advanced" ), + UsersColumn( "Users", attach_popup=False ), + GroupsColumn( "Groups", attach_popup=False ), + StatusColumn( "Status", attach_popup=False ), + # Columns that are valid for filtering but are not visible. + grids.DeletedColumn( "Deleted", key="deleted", visible=False, filterable="advanced" ) + ] + columns.append( grids.MulticolFilterColumn( "Search", + cols_to_filter=[ columns[0], columns[1], columns[2] ], + key="free-text-search", + visible=False, + filterable="standard" ) ) + global_actions = [ + grids.GridAction( "Add new quota", dict( controller='admin', action='quotas', operation='create' ) ) + ] + operations = [ grids.GridOperation( "Rename", + condition=( lambda item: not item.deleted ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="rename_quota" ) ), + grids.GridOperation( "Change amount", + condition=( lambda item: not item.deleted ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="edit_quota" ) ), + grids.GridOperation( "Manage users and groups", + condition=( lambda item: not item.default ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="manage_users_and_groups_for_quota" ) ), + grids.GridOperation( "Set as different type of default", + condition=( lambda item: item.default ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="set_quota_default" ) ), + grids.GridOperation( "Set as default", + condition=( lambda item: not item.default ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="set_quota_default" ) ), + grids.GridOperation( "Unset as default", + condition=( lambda item: item.default ), + allow_multiple=False, + url_args=dict( webapp="galaxy", action="unset_quota_default" ) ), + grids.GridOperation( "Delete", + condition=( lambda item: not item.deleted and not item.default ), + allow_multiple=True, + url_args=dict( webapp="galaxy", action="mark_quota_deleted" ) ), + grids.GridOperation( "Undelete", + condition=( lambda item: item.deleted ), + allow_multiple=True, + url_args=dict( webapp="galaxy", action="undelete_quota" ) ), + grids.GridOperation( "Purge", + condition=( lambda item: item.deleted ), + allow_multiple=True, + url_args=dict( webapp="galaxy", action="purge_quota" ) ) ] + standard_filters = [ + grids.GridColumnFilter( "Active", args=dict( deleted=False ) ), + grids.GridColumnFilter( "Deleted", args=dict( deleted=True ) ), + grids.GridColumnFilter( "All", args=dict( deleted='All' ) ) + ] + num_rows_per_page = 50 + preserve_state = False + use_paging = True + class AdminGalaxy( BaseController, Admin ): user_list_grid = UserListGrid() role_list_grid = RoleListGrid() group_list_grid = GroupListGrid() + quota_list_grid = QuotaListGrid() --- a/lib/galaxy/web/controllers/dataset.py Thu Aug 04 16:55:13 2011 +1000 +++ b/lib/galaxy/web/controllers/dataset.py Thu Aug 04 10:39:16 2011 -0400 @@ -936,7 +936,7 @@ assert topmost_parent in history.datasets, "Data does not belong to current history" # If the user is anonymous, make sure the HDA is owned by the current session. if not user: - assert trans.galaxy_session.id in [ s.id for s in hda.history.galaxy_sessions ], 'Invalid history dataset ID' + assert trans.galaxy_session.current_history_id == trans.history.id, 'Invalid history dataset ID' # If the user is known, make sure the HDA is owned by the current user. else: assert topmost_parent.history.user == trans.user, 'Invalid history dataset ID' --- a/lib/galaxy/web/controllers/root.py Thu Aug 04 16:55:13 2011 +1000 +++ b/lib/galaxy/web/controllers/root.py Thu Aug 04 10:39:16 2011 -0400 @@ -126,6 +126,7 @@ hda_id = hda_id, show_deleted = show_deleted, show_hidden=show_hidden, + over_quota=trans.app.quota_agent.get_percent( trans=trans ) >= 100, message=message, status=status ) @@ -192,7 +193,25 @@ @web.json def history_get_disk_size( self, trans ): - return trans.history.get_disk_size( nice_size=True ) + rval = { 'history' : trans.history.get_disk_size( nice_size=True ) } + for k, v in self.__user_get_usage( trans ).items(): + rval['global_' + k] = v + return rval + + @web.json + def user_get_usage( self, trans ): + return self.__user_get_usage( trans ) + + def __user_get_usage( self, trans ): + usage = trans.app.quota_agent.get_usage( trans ) + percent = trans.app.quota_agent.get_percent( trans=trans, usage=usage ) + rval = {} + if percent is None: + rval['usage'] = util.nice_size( usage ) + else: + rval['percent'] = percent + return rval + ## ---- Dataset display / editing ---------------------------------------- --- a/static/june_2007_style/blue/panel_layout.css Thu Aug 04 16:55:13 2011 +1000 +++ b/static/june_2007_style/blue/panel_layout.css Thu Aug 04 10:39:16 2011 -0400 @@ -36,6 +36,12 @@ #masthead a{color:#eeeeee;text-decoration:none;} #masthead .title{font-family:verdana;padding:3px 10px;font-size:175%;font-weight:bold;z-index:-1;} #masthead a:hover{text-decoration:underline;} +.quota-meter-container{position:absolute;top:0;right:0;height:32px;} +.quota-meter{position:absolute;top:8px;right:8px;height:16px;width:100px;background-color:#C1C9E5;;} +.quota-meter-bar{position:absolute;top:0;left:0;height:16px;background-color:#969DB3;;} +.quota-meter-bar-warn{background-color:#FFB400;;} +.quota-meter-bar-error{background-color:#FF4343;;} +.quota-meter-text{position:absolute;top:50%;left:0;width:100px;height:16px;margin-top:-6px;text-align:center;z-index:9001;color:#000;;} .tab-group{margin:0;padding:0 10px;height:100%;white-space:nowrap;cursor:default;background:transparent;} .tab-group .tab{background:#2C3143;position:relative;float:left;margin:0;padding:0 1em;height:32px;line-height:32px;text-align:left;} .tab-group .tab .submenu{display:none;position:absolute;z-index:16000;left:0;top:32px;padding:1em;margin:-1em;padding-top:0;margin-top:0;background-color:rgba(0,0,0,0.5);-moz-border-radius:0 0 1em 1em;-webkit-border-bottom-right-radius:1em;-webkit-border-bottom-left-radius:1em;} --- a/static/june_2007_style/blue_colors.ini Thu Aug 04 16:55:13 2011 +1000 +++ b/static/june_2007_style/blue_colors.ini Thu Aug 04 10:39:16 2011 -0400 @@ -59,6 +59,12 @@ masthead_bg_hatch=- masthead_link=#eeeeee masthead_active_tab_bg=#222532 +# Quota meter +quota_meter_bg=#C1C9E5; +quota_meter_bar=#969DB3; +quota_meter_warn_bar=#FFB400; +quota_meter_error_bar=#FF4343; +quota_meter_text=#000; # ---- Layout ----------------------------------------------------------------- # Overall background color (including space between panels) layout_bg=#eee --- a/static/june_2007_style/masthead.css.tmpl Thu Aug 04 16:55:13 2011 +1000 +++ b/static/june_2007_style/masthead.css.tmpl Thu Aug 04 10:39:16 2011 -0400 @@ -59,4 +59,4 @@ margin-left: -3px; margin-right: -3px; padding-bottom: 10px; margin-bottom: -10px; -} \ No newline at end of file +} --- a/static/june_2007_style/panel_layout.css.tmpl Thu Aug 04 16:55:13 2011 +1000 +++ b/static/june_2007_style/panel_layout.css.tmpl Thu Aug 04 10:39:16 2011 -0400 @@ -259,6 +259,56 @@ text-decoration: underline; } +.quota-meter-container +{ + position: absolute; + top: 0; + right: 0; + height: 32px; +} + +.quota-meter +{ + position: absolute; + top: 8px; + right: 8px; + height: 16px; + width: 100px; + background-color: $quota_meter_bg; +} + +.quota-meter-bar +{ + position: absolute; + top: 0; + left: 0; + height: 16px; + background-color: $quota_meter_bar; +} + +.quota-meter-bar-warn +{ + background-color: $quota_meter_warn_bar; +} + +.quota-meter-bar-error +{ + background-color: $quota_meter_error_bar; +} + +.quota-meter-text +{ + position: absolute; + top: 50%; + left: 0; + width: 100px; + height: 16px; + margin-top: -6px; + text-align: center; + z-index: 9001; + color: $quota_meter_text; +} + ## Tabs .tab-group { --- a/templates/root/history.mako Thu Aug 04 16:55:13 2011 +1000 +++ b/templates/root/history.mako Thu Aug 04 10:39:16 2011 -0400 @@ -272,8 +272,41 @@ %if hda_id: self.location = "#${hda_id}"; %endif + + // Update the Quota Meter + $.ajax( { + type: "POST", + url: "${h.url_for( controller='root', action='user_get_usage' )}", + dataType: "json", + success : function ( data ) { + $.each( data, function( type, val ) { + quota_meter_updater( type, val ); + }); + } + }); }); +// Updates the Quota Meter +var quota_meter_updater = function ( type, val ) { + if ( type == "usage" ) { + $("#quota-meter-bar", window.top.document).css( "width", "0" ); + $("#quota-meter-text", window.top.document).text( "Using " + val ); + } else if ( type == "percent" ) { + $("#quota-meter-bar", window.top.document).removeClass("quota-meter-bar-warn quota-meter-bar-error"); + if ( val >= 100 ) { + $("#quota-meter-bar", window.top.document).addClass("quota-meter-bar-error"); + $("#quota-message-container").slideDown(); + } else if ( val >= 85 ) { + $("#quota-meter-bar", window.top.document).addClass("quota-meter-bar-warn"); + $("#quota-message-container").slideUp(); + } else { + $("#quota-message-container").slideUp(); + } + $("#quota-meter-bar", window.top.document).css( "width", val + "px" ); + $("#quota-meter-text", window.top.document).text( "Using " + val + "%" ); + } +} + // Looks for changes in dataset state using an async request. Keeps // calling itself (via setTimeout) until all datasets are in a terminal // state. @@ -334,7 +367,15 @@ url: "${h.url_for( controller='root', action='history_get_disk_size' )}", dataType: "json", success: function( data ) { - $("#history-size").text( data ); + $.each( data, function( type, val ) { + if ( type == "history" ) { + $("#history-size").text( val ); + } else if ( type == "global_usage" ) { + quota_meter_updater( "usage", val ); + } else if ( type == "global_percent" ) { + quota_meter_updater( "percent", val ); + } + }); } }); check_history_size = false; @@ -471,6 +512,17 @@ %endif </div> +%if over_quota: +<div id="quota-message-container"> +%else: +<div id="quota-message-container" style="display: none;"> +%endif + <div id="quota-message" class="errormessage"> + You are over your disk quota. Tool execution is on hold until your disk usage drops below your allocated quota. + </div> + <br/> +</div> + %if not datasets: <div class="infomessagesmall" id="emptyHistoryMessage"> --- a/templates/user/index.mako Thu Aug 04 16:55:13 2011 +1000 +++ b/templates/user/index.mako Thu Aug 04 10:39:16 2011 -0400 @@ -22,7 +22,12 @@ <li><a href="${h.url_for( controller='user', action='manage_user_info', cntrller=cntrller, webapp='community' )}">${_('Manage your information')}</a></li> %endif </ul> - <p>You are currently using <strong>${trans.user.get_disk_usage( nice_size=True )}</strong> of disk space in this Galaxy instance.</p> + <p> + You are using <strong>${trans.user.get_disk_usage( nice_size=True )}</strong> of disk space in this Galaxy instance. + %if trans.app.config.enable_quotas: + Your disk quota is: <strong>${trans.app.quota_agent.get_quota( trans.user, nice_size=True )}</strong>. + %endif + </p> %else: %if not message: <p>${n_('You are currently not logged in.')}</p> --- a/templates/webapps/galaxy/admin/index.mako Thu Aug 04 16:55:13 2011 +1000 +++ b/templates/webapps/galaxy/admin/index.mako Thu Aug 04 10:39:16 2011 -0400 @@ -55,6 +55,7 @@ </div><div class="toolSectionBody"><div class="toolSectionBg"> + <div class="toolTitle"><a href="${h.url_for( controller='admin', action='quotas', webapp=webapp )}" target="galaxy_main">Manage quotas</a></div><div class="toolTitle"><a href="${h.url_for( controller='library_admin', action='browse_libraries' )}" target="galaxy_main">Manage data libraries</a></div></div></div> --- a/templates/webapps/galaxy/base_panels.mako Thu Aug 04 16:55:13 2011 +1000 +++ b/templates/webapps/galaxy/base_panels.mako Thu Aug 04 10:39:16 2011 -0400 @@ -3,6 +3,11 @@ ## Default title <%def name="title()">Galaxy</%def> +<%def name="javascripts()"> + ${parent.javascripts()} + ${h.js( "jquery.tipsy" )} +</%def> + ## Masthead <%def name="masthead()"> @@ -172,5 +177,45 @@ %endif </a></div> + + ## Quota meter + <% + bar_style = "quota-meter-bar" + usage = 0 + percent = 0 + quota = None + try: + usage = trans.app.quota_agent.get_usage( trans=trans ) + quota = trans.app.quota_agent.get_quota( trans.user ) + percent = trans.app.quota_agent.get_percent( usage=usage, quota=quota ) + if percent is not None: + if percent >= 100: + bar_style += " quota-meter-bar-error" + elif percent >= 85: + bar_style += " quota-meter-bar-warn" + else: + percent = 0 + except AssertionError: + pass # Probably no history yet + tooltip = None + if not trans.user and quota and trans.app.config.allow_user_creation: + if trans.app.quota_agent.default_registered_quota is None or trans.app.quota_agent.default_unregistered_quota < trans.app.quota_agent.default_registered_quota: + tooltip = "Your disk quota is %s. You can increase your quota by registering a Galaxy account." % util.nice_size( quota ) + %> + + <div class="quota-meter-container"> + %if tooltip: + <div id="quota-meter" class="quota-meter tooltip" title="${tooltip}"> + %else: + <div id="quota-meter" class="quota-meter"> + %endif + <div id="quota-meter-bar" class="${bar_style}" style="width: ${percent}px;"></div> + %if quota is not None: + <div id="quota-meter-text" class="quota-meter-text">Using ${percent}%</div> + %else: + <div id="quota-meter-text" class="quota-meter-text">Using ${util.nice_size( usage )}</div> + %endif + </div> + </div></%def> --- a/universe_wsgi.ini.sample Thu Aug 04 16:55:13 2011 +1000 +++ b/universe_wsgi.ini.sample Thu Aug 04 10:39:16 2011 -0400 @@ -454,6 +454,9 @@ # users in the help text. #ftp_upload_site = None +# Enable enforcement of quotas. Quotas can be set from the Admin interface. +#enable_quotas = False + # -- Job Execution # If running multiple Galaxy processes, one can be designated as the job Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.