Skip to content

Commit

Permalink
Implement quota tracking options per ObjectStore.
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Sep 30, 2020
1 parent c737148 commit 7bcb285
Show file tree
Hide file tree
Showing 33 changed files with 1,133 additions and 205 deletions.
44 changes: 44 additions & 0 deletions client/src/components/Quota/QuotaUsage.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<template>
<div>
<b>{{ quotaUsage.name }}</b>
<progress-bar
:note="title"
:ok-count="quotaUsage.quota_percent"
:total="100"
v-if="quotaUsage.quota_percent < 99"
/>
<progress-bar :note="title" :error-count="quotaUsage.quota_percent" :total="100" v-else />
<p>
<i>Using {{ quotaUsage.nice_total_disk_usage }} out of {{ quotaUsage.quota }}.</i>
</p>
<hr />
</div>
</template>

<script>
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import ProgressBar from "components/ProgressBar";
Vue.use(BootstrapVue);
export default {
components: {
ProgressBar,
},
props: {
quotaUsage: {
type: Object,
},
},
computed: {
title() {
if (this.quotaUsage.quota_percent == null) {
return `Unlimited`;
} else {
return `Using ${this.quotaUsage.quota_percent}%.`;
}
},
},
};
</script>
76 changes: 76 additions & 0 deletions client/src/components/Quota/QuotaUsageDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<template>
<b-modal visible ok-only ok-title="Close" hide-header>
<b-alert v-if="errorMessage" variant="danger" show v-html="errorMessage" />
<div v-else-if="usage == null">
<span class="fa fa-spinner fa-spin" />
<span>Please wait...</span>
</div>
<div class="d-block" style="overflow: hidden;" v-else>
<div v-for="item in effectiveQuotaSourceLabels" :key="item.id">
<quota-usage :quotaUsage="item" />
</div>
</div>
</b-modal>
</template>

<script>
import axios from "axios";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { getAppRoot } from "onload/loadConfig";
import { errorMessageAsString } from "utils/simple-error";
import QuotaUsage from "./QuotaUsage";
Vue.use(BootstrapVue);
export default {
components: {
QuotaUsage,
},
props: {
quotaSourceLabels: {
type: Array,
},
},
data() {
return {
usage: null,
errorMessage: null,
};
},
created() {
const url = `${getAppRoot()}api/users/current/usage`;
axios
.get(url)
.then((response) => {
this.usage = response.data;
})
.catch((error) => {
this.errorMessage = errorMessageAsString(error);
});
},
computed: {
effectiveQuotaSourceLabels() {
const labels = [];
const usageAsDict = this.usageAsDict;
labels.push({ id: "_default_", name: "Default Quota", ...usageAsDict["_default_"] });
for (const label of this.quotaSourceLabels) {
const usage = usageAsDict[label];
labels.push({ id: label, name: `Quota Source: ${label}`, ...usage });
}
return labels;
},
usageAsDict() {
const asDict = {};
for (const usage of this.usage) {
if (usage.quota_source_label == null) {
asDict["_default_"] = usage;
} else {
asDict[usage.quota_source_label] = usage;
}
}
return asDict;
},
},
};
</script>
1 change: 1 addition & 0 deletions client/src/components/Quota/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { showQuotaDialog } from "./show";
11 changes: 11 additions & 0 deletions client/src/components/Quota/show.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Vue from "vue";
import QuotaUsageDialog from "./QuotaUsageDialog";

export function showQuotaDialog(options = {}) {
const instance = Vue.extend(QuotaUsageDialog);
const vm = document.createElement("div");
document.body.appendChild(vm);
new instance({
propsData: options,
}).$mount(vm);
}
1 change: 1 addition & 0 deletions client/src/layout/masthead.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export class MastheadState {
// add quota meter to masthead
Galaxy.quotaMeter = this.quotaMeter = new QuotaMeter.UserQuotaMeter({
model: Galaxy.user,
quotaSourceLabels: Galaxy.config.quota_source_labels,
});

// loop through beforeunload functions if the user attempts to unload the page
Expand Down
15 changes: 14 additions & 1 deletion client/src/mvc/user/user-quotameter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import _ from "underscore";
import baseMVC from "mvc/base-mvc";
import _l from "utils/localization";

import { showQuotaDialog } from "components/Quota";

var logNamespace = "user";
//==============================================================================
/** @class View to display a user's disk/storage usage
Expand All @@ -26,6 +28,7 @@ var UserQuotaMeter = Backbone.View.extend(baseMVC.LoggableMixin).extend(
initialize: function (options) {
this.log(`${this}.initialize:`, options);
_.extend(this.options, options);
this.useQuotaSourceLabels = options.quotaSourceLabels.length > 0;

//this.bind( 'all', function( event, data ){ this.log( this + ' event:', event, data ); }, this );
this.listenTo(this.model, "change:quota_percent change:total_disk_usage", this.render);
Expand Down Expand Up @@ -125,6 +128,16 @@ var UserQuotaMeter = Backbone.View.extend(baseMVC.LoggableMixin).extend(

this.$el.html(meterHtml);
this.$el.find(".quota-meter-text").tooltip();
const $link = this.$el.find(".quota-meter-link");
if (this.useQuotaSourceLabels) {
$link.click(() => {
showQuotaDialog({
quotaSourceLabels: this.options.quotaSourceLabels,
});
});
} else {
$link.attr("href", "https://galaxyproject.org/support/account-quotas/");
}
return this;
},

Expand All @@ -136,7 +149,7 @@ var UserQuotaMeter = Backbone.View.extend(baseMVC.LoggableMixin).extend(
return `<div id="quota-meter" class="quota-meter progress">
<div class="progress-bar" style="width: ${data.quota_percent}%"></div>
<div class="quota-meter-text" data-placement="left" ${title}>
<a href="https://galaxyproject.org/support/account-quotas/" target="_blank">${using}</a>
<a class="quota-meter-link" target="_blank">${using}</a>
</div>
</div>`;
},
Expand Down
8 changes: 7 additions & 1 deletion lib/galaxy/actions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,13 @@ def _create_quota(self, params, decode_id=None):
raise ActionInputError("Operation for an unlimited quota must be '='.")
else:
# Create the quota
quota = self.app.model.Quota(name=params.name, description=params.description, amount=create_amount, operation=params.operation)
quota = self.app.model.Quota(
name=params.name,
description=params.description,
amount=create_amount,
operation=params.operation,
quota_source_label=params.quota_source_label,
)
self.sa_session.add(quota)
# If this is a default quota, create the DefaultQuotaAssociation
if params.default != 'no':
Expand Down
8 changes: 6 additions & 2 deletions lib/galaxy/jobs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1730,13 +1730,17 @@ def fail():
tool=self.tool, stdout=job.stdout, stderr=job.stderr)

collected_bytes = 0
quota_source_info = None
# Once datasets are collected, set the total dataset size (includes extra files)
for dataset_assoc in job.output_datasets:
if not dataset_assoc.dataset.dataset.purged:
# assume all datasets in a job get written to the same objectstore
quota_source_info = dataset_assoc.dataset.dataset.quota_source_info
collected_bytes += dataset_assoc.dataset.set_total_size()

if job.user:
job.user.adjust_total_disk_usage(collected_bytes)
user = job.user
if user and collected_bytes > 0 and quota_source_info is not None and quota_source_info.use:
user.adjust_total_disk_usage(collected_bytes, quota_source_info.label)

# Empirically, we need to update job.user and
# job.workflow_invocation_step.workflow_invocation in separate
Expand Down
1 change: 1 addition & 0 deletions lib/galaxy/managers/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def _use_config(config, key, **context):
'python' : _defaults_to((sys.version_info.major, sys.version_info.minor)),
'select_type_workflow_threshold' : _use_config,
'file_sources_configured' : lambda config, key, **context: self.app.file_sources.custom_sources_configured,
'quota_source_labels' : lambda config, key, **context: list(self.app.object_store.get_quota_source_map().get_quota_source_labels()),
'upload_from_form_button' : _use_config,
}

Expand Down
5 changes: 3 additions & 2 deletions lib/galaxy/managers/hdas.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,9 @@ def purge(self, hda, flush=True):
quota_amount_reduction = hda.quota_amount(user)
super().purge(hda, flush=flush)
# decrease the user's space used
if quota_amount_reduction:
user.adjust_total_disk_usage(-quota_amount_reduction)
quota_source_info = hda.dataset.quota_source_info
if quota_amount_reduction and quota_source_info.use:
user.adjust_total_disk_usage(-quota_amount_reduction, quota_source_info.label)
return hda

# .... states
Expand Down
15 changes: 12 additions & 3 deletions lib/galaxy/managers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,10 @@ def sharing_roles(self, user):
def default_permissions(self, user):
return self.app.security_agent.user_get_default_permissions(user)

def quota(self, user, total=False):
def quota(self, user, total=False, quota_source_label=None):
if total:
return self.app.quota_agent.get_quota_nice_size(user)
return self.app.quota_agent.get_percent(user=user)
return self.app.quota_agent.get_quota_nice_size(user, quota_source_label=quota_source_label)
return self.app.quota_agent.get_percent(user=user, quota_source_label=quota_source_label)

def tags_used(self, user, tag_models=None):
"""
Expand Down Expand Up @@ -580,6 +580,15 @@ def add_serializers(self):
'tags_used' : lambda i, k, **c: self.user_manager.tags_used(i),
})

def serialize_disk_usage(self, user):
rval = user.dictify_usage()
for usage in rval:
quota_source_label = usage["quota_source_label"]
usage["quota_percent"] = self.user_manager.quota(user, quota_source_label=quota_source_label)
usage["quota"] = self.user_manager.quota(user, total=True, quota_source_label=quota_source_label)
usage["nice_total_disk_usage"] = util.nice_size(usage["total_disk_usage"])
return rval


class UserDeserializer(base.ModelDeserializer):
"""
Expand Down
Loading

0 comments on commit 7bcb285

Please sign in to comment.