Skip to content
This repository has been archived by the owner on Nov 13, 2019. It is now read-only.

Support balances and add warnings #483

Merged
merged 17 commits into from
Sep 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ gem 'mocha', '~> 1.1', group: :test
gem "spring", group: :test
gem 'autoprefixer-rails', '~> 8.4'
gem 'dotiw'
gem 'local_time', '~> 1.0.3'

# OOD specific gems
gem 'ood_support', '~> 0.0.2'
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (1.8.6)
local_time (1.0.3)
coffee-rails
lograge (0.10.0)
actionpack (>= 4)
activesupport (>= 4)
Expand Down Expand Up @@ -211,6 +213,7 @@ DEPENDENCIES
jbuilder (~> 2.0)
jquery-datatables-rails (~> 3.4)
jquery-rails
local_time (~> 1.0.3)
mocha (~> 1.1)
ood_appkit (~> 1.0)
ood_core (~> 0.1)
Expand Down
71 changes: 71 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,77 @@ be repeated with just `user`, `block_usage`, and `file_usage` changing.*

*Warning: A block must be equal to 1 KB for proper conversions.*

### Balance Warnings

We currently support displaying warnings to users on the Dashboard if their
balance is nearing its limit. This requires an auto-updated (it is
recommended to update this file every day with a cronjob) JSON file
that lists all user balances. The JSON schema for version `1` is given as:

```json
{
"version": 1,
"timestamp": 1525361263,
"config": {
"unit": "RU",
"project_type": "project"
},
"balances": [
{
...
},
{
...
}
]
}
```

Where `version` defines the version of the JSON schema used, `timestamp`
defines when this file was generated, and `balances` is a list of balance objects
(see below).

The value for `config.unit` defines the type of units for balances and `config.project_type` would be project, account, or group, etc. Both values are used in locales and can be any string value.

You can configure the Dashboard to use this JSON file (or files) by setting the
environment variable `OOD_BALANCE_PATH` as a colon-delimited list of all JSON
file paths.

The default threshold for displaying the warning is at `0`, but this
ericfranz marked this conversation as resolved.
Show resolved Hide resolved
can be changed with the environment variable `OOD_BALANCE_THRESHOLD`.

An example is given as:

```shell
# /etc/ood/config/apps/dashboard/env

OOD_BALANCE_PATH="/path/to/balance1.json:/path/to/balance2.json"
OOD_BALANCE_THRESHOLD=1000
```

#### User Balance

If the balance is defined as a `user` quota, then it applies to only that user. Omit the `project` key:

```json
{
"user": "user1",
"value": 10
}
```

#### Project Balance

If the balance is defined as a `project` balance, then it applies to a project/account/group, whatever is defined for `config.project_type`:

```json
{
"user": "user1",
"project": "project1",
"value": 10
}
```

## API

### iHPC CLI
Expand Down
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,5 @@ pre.motd-monospaced {
@import "batch_connect/sessions";
@import "batch_connect/session_contexts";
@import "insufficient_quota";
@import "insufficient_balance";
@import "fa_shims";
26 changes: 26 additions & 0 deletions app/assets/stylesheets/insufficient_balance.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.insufficient-balance {
padding-bottom: 10px;

.insufficient-balance-group {
.insufficient-balance-resource {
padding: 5px 0 5px 0;

&:first-child {
padding-top: 0;
}

&:last-child {
padding-bottom: 0;
}

.progress {
margin: 5px 0 5px 0;
}
}
}

footer {
margin-top: 20px;
font-size: 12px;
}
}
9 changes: 9 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception

before_action :set_user, :set_nav_groups, :set_announcements, :set_locale
before_action :set_my_balances, only: [:index, :new, :featured]

def set_locale
I18n.locale = ::Configuration.locale
Expand Down Expand Up @@ -66,4 +67,12 @@ def set_my_quotas
::Configuration.quota_paths.each { |path| @my_quotas += Quota.find(path, OodSupport::User.new.name) }
@my_quotas
end

# Set a list of my balances which can be used to display warnings if there is
# an insufficient balance
def set_my_balances
@my_balances = []
::Configuration.balance_paths.each { |path| @my_balances += Balance.find(path, OodSupport::User.new.name) }
@my_balances
end
end
99 changes: 99 additions & 0 deletions app/models/balance.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# This describes balances for a given user
require 'open-uri'

class Balance
class InvalidBalanceFile < StandardError; end

attr_reader :user, :project, :value, :project_type, :unit, :updated_at

class << self

# Get balance objects only for requested user in JSON file(s)
#
# KeyError and JSON::ParserErrors shall be non-fatal errors
def find(balance_path, user)
raw = open(balance_path).read
raise InvalidBalanceFile.new("No content returned when attempting to read balance file") if raw.nil? || raw.empty?

# Attempt to parse raw JSON into an object
json = JSON.parse(raw)
raise InvalidBalanceFile.new("Balance file expected to be a JSON object with balances array section") unless json.is_a?(Hash) && json["balances"].respond_to?(:each)

#FIXME: any validation of the structure here? otherwise we don't need the complexity of the code below
# until we have more than one balance version schema, which we do not
# so assume version is 1
config = json["config"] || {}
build_balances(json["balances"], json["timestamp"], config, user)
rescue StandardError => e
Rails.logger.error("Error #{e.class} when reading and parsing balance file #{balance_path} for user #{user}: #{e.message}")
[]
end

private

# Parse JSON object using version 1 formatting
def build_balances(balance_hashes, updated_at, config, user)
balances = []
balance_hashes.each do |balance|
balance = balance.to_h.compact.symbolize_keys
config = config.to_h.compact.symbolize_keys
next unless user == balance[:user]
balances << Balance.new(
user: balance.fetch(:user).to_s,
project: balance.fetch(:project, nil).to_s,
value: balance.fetch(:value).to_i,
project_type: config.fetch(:project_type, nil).to_s,
unit: config[:unit].to_s,
updated_at: Time.at(updated_at.to_i),
)
end
balances
end
end

# @param params [#to_h] list of parameters that define balance object
# @option params [#to_s] :user user name
# @option params [#to_s] :project project name
# @option params [#to_i] :value balance value
# @option params [#to_s] :project_type project type
# @option params [#to_s] :unit value unit
# @option params [#to_i] :updated_at time when balance was generated
def initialize(params)
params = params.to_h.compact.symbolize_keys

@user = params.fetch(:user).to_s
@project = params.fetch(:project, nil).to_s
@value = params.fetch(:value).to_i
@project_type = params.fetch(:project_type, nil).to_s
@unit = params.fetch(:unit).to_s
@updated_at = Time.at(params.fetch(:updated_at).to_i)
end

def balance_object
@project.presence || @user
end

# Unit + balance text
def units_balance
return "#{@unit} balance" if @unit.present?
'Balance'
end

# Plural units
def balanace_units
return @unit.pluralize if @unit.present?
'resources'
end

def sufficient?(threshold: 0)
@value.to_f > threshold.to_f
end

def insufficient?(threshold: 0)
!sufficient?(threshold: threshold)
end

def to_s
I18n.translate('dashboard.balance_message', unit: @unit, units_balance: units_balance, value: @value)
end
end
1 change: 1 addition & 0 deletions app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
<%= render "layouts/browser_warning" %>

<%= render partial: "shared/insufficient_quota", locals: { quotas: @my_quotas } if @my_quotas && @my_quotas.any? %>
<%= render partial: "shared/insufficient_balance", locals: { balances: @my_balances } if @my_balances && @my_balances.any? %>

<script type="text/coffee-script-template" id="js-alert-danger-template">
<div class="alert alert-danger alert-dismissible" role="alert">
Expand Down
16 changes: 16 additions & 0 deletions app/views/shared/_insufficient_balance.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<%- if balances.try(:any?) { |balance| balance.insufficient?(threshold: Configuration.balance_threshold) } -%>
<div class="alert alert-danger insufficient-balance" role="alert">
<div class="insufficient-balance-group">
<%- balances.select{ |balance| balance.insufficient?(threshold: Configuration.balance_threshold) }.each do |balance| -%>
<div class="insufficient-balance-resource">
<%=
render(
partial: "shared/insufficient_balance_resource",
locals: { balance: balance }
)
%>
</div>
<%- end -%>
</div>
</div>
<%- end -%>
12 changes: 12 additions & 0 deletions app/views/shared/_insufficient_balance_resource.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<h4>
<%= t('dashboard.balance_warning_prefix_html', units_balance: balance.units_balance, unit: balance.unit) %>
<strong><%= balance.project_type %> <%= balance.balance_object %></strong>
<small class="pull-right hidden-xs hidden-sm">
<%= t('dashboard.balance_reload_message_html', last_update: local_time(balance.updated_at, format: '%Y/%m/%d at %l:%M%p')) %>
</small>
</h4>
<ul class="list-unstyled">
<li>
<%= balance.to_s %>. <%= t('dashboard.balance_additional_message', balanace_units: balance.balanace_units, unit: balance.unit) %>
</li>
</ul>
19 changes: 19 additions & 0 deletions config/configuration_singleton.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,25 @@ def quota_threshold
ENV.fetch("OOD_QUOTA_THRESHOLD", 0.95).to_f
end

# The paths to the JSON files that store the balance information
# Can be URL or File path. colon delimited string; though colon in URL is
# ignored if URL has format: scheme://path (colons preceeding // are ignored)
#
# /path/to/balance.json:https://osc.edu/balance.json
#
#
# @return [Array<String>] balance paths
def balance_paths
# regex uses negative lookahead to ignore : preceeding //
ENV.fetch("OOD_BALANCE_PATH", "").strip.split(/:(?!\/\/)/)
ericfranz marked this conversation as resolved.
Show resolved Hide resolved
end

# The threshold for determining if there is sufficient balance remaining
# @return [Float] threshold factor
def balance_threshold
ENV.fetch("OOD_BALANCE_THRESHOLD", 0).to_f
ericfranz marked this conversation as resolved.
Show resolved Hide resolved
end

# Load the dotenv local files first, then the /etc dotenv files and
# the .env and .env.production or .env.development files.
#
Expand Down
5 changes: 5 additions & 0 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ en:
quota_reload_message: "Reload page to see updated quota. Quotas are updated every 5 minutes."
quota_warning_prefix_html: "Quota limit warning for"

balance_additional_message: "Consider requesting additional %{balanace_units}"
balance_reload_message_html: "Reload page to see updated balance. Balances are updated daily.<br>Last update was %{last_update}"
balance_warning_prefix_html: "%{units_balance} warning for"
balance_message: "%{units_balance} is %{value}"

welcome_html: |
%{logo_img_tag}
<p class="lead">OnDemand provides an integrated, single access point for all of your HPC resources.</p>
Expand Down
25 changes: 25 additions & 0 deletions test/fixtures/balance.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"balances": [
{
"project": "PZS0708",
"user": "tdockendorf",
"value": 0
},
{
"project": "PZS0708",
"user": "djohnson",
"value": 20
},
{
"project": "PZS0714",
"user": "efranz",
"value": 0
}
],
"config": {
"unit": "RU",
"project_type": "project"
},
"timestamp": 1567190705,
"version": 1
}