Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement quota tracking options per ObjectStore. #10221

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -20,6 +20,7 @@ export class MastheadState {
Galaxy.quotaMeter = this.quotaMeter = new QuotaMeter.UserQuotaMeter({
model: Galaxy.user,
quotaUrl: Galaxy.config.quota_url,
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 @@ -27,6 +29,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 @@ -126,6 +129,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 @@ -138,7 +151,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="${quotaUrl}" target="_blank">${using}</a>
<a href="${quotaUrl}" 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 @@ -1737,13 +1737,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 @@ -105,6 +105,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 @@ -347,10 +347,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 @@ -597,6 +597,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