Commit 88da7a1c authored by Kostas Papadimitriou's avatar Kostas Papadimitriou
Browse files

cyclades ui: Expose and use the new astakos quota api

- Removed /userdata/quota view in respect of astakos quotas api delegate
  views served in the same urls used by astakos api (/astakos/api/quotas,
  /astakos/api/resources). Base url is configurable using the
  UI_ACCOUNTS_API_URL (defaults to /astakos/api).
- Refactored quotas js client to use backbone Collection/Model
  mechanisms.
- Refresh quotas using the common api update mechanism used by vms and
  networks (deprecated UI_QUOTAS_UPDATE_INTERVAL setting)
- Extended main view initialization steps to include quotas/resources
  loading before the initial layout renedring
parent 0f47d695
......@@ -39,9 +39,6 @@
## consecutive API calls (aligning changes-since attribute).
#UI_CHANGES_SINCE_ALIGNMENT = 0
#
## How often to check for user usage changes
#UI_QUOTAS_UPDATE_INTERVAL = 10000
#
## URL to redirect not authenticated users
#UI_LOGIN_URL = "/im/login"
#
......@@ -183,6 +180,8 @@
## The name of the grouped network view
#UI_GROUPED_PUBLIC_NETWORK_NAME = 'Internet'
#
## Endpoint to make account specific requests (resources/quotas)
#UI_ACCOUNTS_API_URL = '/astakos/api'
#
################
## UI EXTENSIONS
......
......@@ -42,6 +42,8 @@ from django.conf import settings
ASTAKOS_URL = getattr(settings, 'ASTAKOS_URL', None)
USER_CATALOG_URL = urlparse.urljoin(ASTAKOS_URL, "user_catalogs")
USER_FEEDBACK_URL = urlparse.urljoin(ASTAKOS_URL, "feedback")
USER_QUOTA_URL = urlparse.urljoin(ASTAKOS_URL, "astakos/api/quotas")
RESOURCES_URL = urlparse.urljoin(ASTAKOS_URL, "astakos/api/resources")
from objpool.http import PooledHTTPConnection
......@@ -67,6 +69,24 @@ def proxy(request, url, headers={}, body=None):
return HttpResponse(data, status=status)
@csrf_exempt
def delegate_to_resources_service(request):
logger.debug("Delegate resources request to %s" % RESOURCES_URL)
token = request.META.get('HTTP_X_AUTH_TOKEN')
headers = {'X-Auth-Token': token}
return proxy(request, RESOURCES_URL, headers=headers,
body=request.raw_post_data)
@csrf_exempt
def delegate_to_user_quota_service(request):
logger.debug("Delegate quotas request to %s" % USER_QUOTA_URL)
token = request.META.get('HTTP_X_AUTH_TOKEN')
headers = {'X-Auth-Token': token}
return proxy(request, USER_QUOTA_URL, headers=headers,
body=request.raw_post_data)
@csrf_exempt
def delegate_to_feedback_service(request):
logger.debug("Delegate feedback request to %s" % USER_FEEDBACK_URL)
......
......@@ -51,5 +51,9 @@ if PROXY_USER_SERVICES:
urlpatterns += patterns(
'',
(r'^feedback/?$', 'synnefo.api.delegate.delegate_to_feedback_service'),
(r'^user_catalogs/?$', 'synnefo.api.delegate.delegate_to_user_catalogs_service'))
(r'^user_catalogs/?$',
'synnefo.api.delegate.delegate_to_user_catalogs_service'),
(r'^astakos/api/resources/?$',
'synnefo.api.delegate.delegate_to_resources_service'),
(r'^astakos/api/quotas/?$',
'synnefo.api.delegate.delegate_to_user_quota_service'))
......@@ -615,7 +615,7 @@
var _success = _.bind(function() {
if (success) { success() };
snf.ui.main.load_user_quotas();
synnefo.storage.quotas.get('cyclades.network.private').decrease();
}, this);
var _error = _.bind(function() {
this.set({state: previous_state, status: previous_status})
......@@ -1441,7 +1441,7 @@
// set state after successful call
self.state('DESTROY');
success.apply(this, arguments);
snf.ui.main.load_user_quotas();
synnefo.storage.quotas.get('cyclades.vm').decrease();
},
error, 'destroy', params);
......@@ -1693,7 +1693,7 @@
var cb = function() {
callback();
snf.ui.main.load_user_quotas();
synnefo.storage.quotas.get('cyclades.network.private').increase();
}
return this.api_call(this.path, "create", params, cb);
},
......@@ -2122,15 +2122,17 @@
}
}
opts = {name: name, imageRef: image.id, flavorRef: flavor.id, metadata:meta}
opts = {name: name, imageRef: image.id, flavorRef: flavor.id,
metadata:meta}
opts = _.extend(opts, extra);
var cb = function(data) {
snf.ui.main.load_user_quotas();
synnefo.storage.quotas.get('cyclades.vm').increase();
callback(data);
}
this.api_call(this.path, "create", {'server': opts}, undefined, undefined, cb, {critical: true});
this.api_call(this.path, "create", {'server': opts}, undefined,
undefined, cb, {critical: true});
},
load_missing_images: function(callback) {
......@@ -2365,7 +2367,103 @@
this.create(m.attributes, options);
}
});
models.Quota = models.Model.extend({
initialize: function() {
models.Quota.__super__.initialize.apply(this, arguments);
this.bind("change", this.check, this);
this.check();
},
check: function() {
var usage, limit;
usage = this.get('usage');
limit = this.get('limit');
if (usage >= limit) {
this.trigger("available");
} else {
this.trigger("unavailable");
}
},
increase: function(val) {
if (val === undefined) { val = 1};
this.set({'usage': this.get('usage') + val})
},
decrease: function(val) {
if (val === undefined) { val = 1};
this.set({'usage': this.get('usage') - val})
},
can_consume: function() {
var usage, limit;
usage = this.get('usage');
limit = this.get('limit');
if (usage >= limit) {
return false
} else {
return true
}
},
is_bytes: function() {
return this.get('resource').get('unit') == 'bytes';
},
get_available: function() {
var value = this.get('limit') - this.get('usage');
if (value < 0) { return value }
return value
},
get_readable: function(key) {
var value;
if (key == 'available') {
value = this.get_available();
} else {
value = this.get(key)
}
if (!this.is_bytes()) {
return value + "";
}
return snf.util.readablizeBytes(value);
}
});
models.Quotas = models.Collection.extend({
model: models.Quota,
api_type: 'accounts',
path: 'quotas',
parse: function(resp) {
return _.map(resp.system, function(value, key) {
var available = (value.limit - value.usage) || 0;
return _.extend(value, {'name': key, 'id': key,
'available': available,
'resource': snf.storage.resources.get(key)});
})
}
})
models.Resource = models.Model.extend({
api_type: 'accounts',
path: 'resources'
});
models.Resources = models.Collection.extend({
api_type: 'accounts',
path: 'resources',
model: models.Network,
parse: function(resp) {
return _.map(resp, function(value, key) {
return _.extend(value, {'name': key, 'id': key});
})
}
});
// storage initialization
snf.storage.images = new models.Images();
......@@ -2374,9 +2472,7 @@
snf.storage.vms = new models.VMS();
snf.storage.keys = new models.PublicKeys();
snf.storage.nics = new models.NICs();
//snf.storage.vms.fetch({update:true});
//snf.storage.images.fetch({update:true});
//snf.storage.flavors.fetch({update:true});
snf.storage.resources = new models.Resources();
snf.storage.quotas = new models.Quotas();
})(this);
......@@ -678,10 +678,8 @@
image_excluded = storage.flavors.unavailable_values_for_image(this.current_image);
}
if (snf.user.quota) {
quotas = this.get_vm_params_quotas();
user_excluded = storage.flavors.unavailable_values_for_quotas(quotas);
}
quotas = this.get_vm_params_quotas();
user_excluded = storage.flavors.unavailable_values_for_quotas(quotas);
unavailable.disk = user_excluded.disk.concat(image_excluded.disk);
unavailable.ram = user_excluded.ram.concat(image_excluded.ram);
......@@ -691,10 +689,11 @@
},
get_vm_params_quotas: function() {
var quotas = synnefo.storage.quotas;
var quota = {
'ram': snf.user.quota.get_available('cyclades.ram'),
'cpu': snf.user.quota.get_available('cyclades.cpu'),
'disk': snf.user.quota.get_available('cyclades.disk')
'ram': quotas.get('cyclades.ram').get('available'),
'cpu': quotas.get('cyclades.cpu').get('available'),
'disk': quotas.get('cyclades.disk').get('available')
}
return quota;
},
......@@ -769,7 +768,7 @@
};
}, this));
this.$("#create-vm-flavor-options .flavor-options.ram li").each(_.bind(function(i, el){
this.$("#create-vm-flavor-options .flavor-options.mem li").each(_.bind(function(i, el){
var el_value = $(el).data("value");
if (this.unavailable_values.ram.indexOf(el_value) > -1) {
$(el).addClass("disabled");
......@@ -941,11 +940,11 @@
},
update_quota_display: function() {
if (!snf.user.quota || !snf.user.quota.data) { return };
var quotas = synnefo.storage.quotas;
_.each(["disk", "ram", "cpu"], function(type) {
var available_dsp = snf.user.quota.get_available_readable(type);
var available = snf.user.quota.get_available(type);
var available_dsp = quotas.get('cyclades.'+type).get_readable('available');
var available = quotas.get('cyclades.'+type).get('available');
var content = "({0} left)".format(available_dsp);
if (available <= 0) { content = "(None left)" }
......
......@@ -231,9 +231,12 @@
this.fix_position();
}, this));
storage.vms.bind("change:pending_action", _.bind(this.handle_action_add, this, "vms"));
storage.vms.bind("change:reboot_required", _.bind(this.handle_action_add, this, "reboots"));
storage.networks.bind("change:actions", _.bind(this.handle_action_add, this, "nets"));
storage.vms.bind("change:pending_action",
_.bind(this.handle_action_add, this, "vms"));
storage.vms.bind("change:reboot_required",
_.bind(this.handle_action_add, this, "reboots"));
storage.networks.bind("change:actions",
_.bind(this.handle_action_add, this, "nets"));
},
handle_action_add: function(type, model, action) {
......@@ -532,6 +535,7 @@
storage.vms.bind("add", _.bind(this.check_empty, this));
storage.vms.bind("change:status", _.bind(this.check_empty, this));
storage.vms.bind("reset", _.bind(this.check_empty, this));
storage.quotas.bind("change", _.bind(this.update_create_buttons_status, this));
},
......@@ -592,7 +596,7 @@
$(".css-panes").show();
},
items_to_load: 4,
items_to_load: 6,
completed_items: 0,
check_status: function(loaded) {
this.completed_items++;
......@@ -629,6 +633,7 @@
self.update_status("networks", 1);
self.check_status();
}});
},
init_intervals: function() {
......@@ -642,11 +647,13 @@
this._networks = storage.networks.get_fetcher.apply(storage.networks, _.clone(fetcher_params));
this._vms = storage.vms.get_fetcher.apply(storage.vms, _.clone(fetcher_params));
this._quotas = storage.quotas.get_fetcher.apply(storage.quotas, _.clone(fetcher_params));
},
stop_intervals: function() {
if (this._networks) { this._networks.stop(); }
if (this._vms) { this._vms.stop(); }
if (this._quotas) { this._quotas.stop(); }
this.intervals_stopped = true;
},
......@@ -665,6 +672,13 @@
this.init_intervals();
}
if (this._quotas) {
this._quotas.stop();
this._quotas.start();
} else {
this.init_intervals();
}
this.intervals_stopped = false;
},
......@@ -683,7 +697,6 @@
snf.config.update_hidden_views = uhv;
window.setTimeout(function() {
self.update_status("layout", 0);
self.load_initialize_overlays();
}, 20);
},
......@@ -732,6 +745,18 @@
self.update_status("flavors", 1);
self.check_status()
}});
this.update_status("resources", 0);
storage.resources.fetch({refresh:true, update:false, success: function(){
self.update_status("resources", 1);
self.update_status("quotas", 0);
self.check_status();
storage.quotas.fetch({refresh:true, update:true, success: function() {
self.update_status("quotas", 1);
self.update_status("layout", 1);
self.check_status()
}})
}})
},
update_status: function(ns, state) {
......@@ -779,72 +804,11 @@
}
},
quota_handlers_initialized: false,
load_user_quotas: function(repeat) {
var main_view = this;
if (!snf.user.quota) {
snf.user.quota = new snf.quota.Quota("cyclades");
main_view.init_quotas_handlers();
}
snf.api.sync('read', undefined, {
url: synnefo.config.quota_url,
success: function(d) {
snf.user.quota.load(d);
},
complete: function() {
if (repeat) {
setTimeout(function(){
main_view.load_user_quotas(1);
}, synnefo.config.quotas_update_interval || 10000);
}
}
});
},
check_quotas: function(type) {
var storage = synnefo.storage[type];
var consumed = storage.length;
var quotakey = {
'networks': 'cyclades.network.private',
'vms': 'cyclades.vm'
}
if (type == "networks") {
consumed = storage.filter(function(net){
return !net.is_public() && !net.is_deleted();
}).length;
}
var limit = snf.user.quota.get_limit(quotakey[type]);
if (snf.user.quota && snf.user.quota.data && consumed >= limit) {
storage.trigger("quota_reached");
} else {
storage.trigger("quota_free");
}
},
init_quotas_handlers: function() {
var self = this, event;
snf.user.quota.bind("cyclades.vm.quota.changed", function() {
this.check_quotas("vms");
}, this);
var event = "cyclades.network.private.quota.changed";
snf.user.quota.bind(event, function() {
this.check_quotas("networks");
}, this);
},
// initial view based on user cookie
show_initial_view: function() {
this.set_vm_view_handlers();
this.load_user_quotas(1);
this.hide_loading_view();
bb.history.start();
this.trigger("ready");
},
......@@ -853,6 +817,25 @@
this.router.vm_details_view(vm.id);
}
},
update_create_buttons_status: function() {
var nets = storage.quotas.get('cyclades.network.private');
var vms = storage.quotas.get('cyclades.vm');
if (!nets || !vms) { return }
if (!nets.can_consume()) {
$("#networks-pane a.createbutton").addClass("disabled");
} else {
$("#networks-pane a.createbutton").removeClass("disabled");
}
if (!vms.can_consume()) {
$("#createcontainer #create").addClass("disabled");
} else {
$("#createcontainer #create").removeClass("disabled");
}
},
set_vm_view_handlers: function() {
var self = this;
......@@ -861,17 +844,6 @@
if ($(this).hasClass("disabled")) { return }
self.router.vm_create_view();
});
synnefo.storage.vms.bind("quota_reached", function(){
$("#createcontainer #create").addClass("disabled");
$("#createcontainer #create").attr("title", "Machines limit reached");
});
synnefo.storage.vms.bind("quota_free", function(){
$("#createcontainer #create").removeClass("disabled");
$("#createcontainer #create").attr("title", "");
});
},
check_empty: function() {
......
......@@ -558,6 +558,8 @@
<div id="loading-view" class="hidden clearfix">
<div class="header clearfix images off">Loading images...<span></span></div>
<div class="header clearfix flavors off">Loading flavors...<span></span></div>
<div class="header clearfix resources off">Loading resources...<span></span></div>
<div class="header clearfix quotas off">Loading quotas...<span></span></div>
<div class="header clearfix vms off">Loading machines...<span></span></div>
<div class="header clearfix networks off">Loading networks...<span></span></div>
<div class="header clearfix layout off">Rendering layout...<span></span></div>
......@@ -611,7 +613,8 @@
// TODO: make it dynamic
synnefo.config.api_urls = {
'compute': {{ compute_api_url|safe }},
'glance': {{ glance_api_url|safe }}
'glance': {{ glance_api_url|safe }},
'accounts': {{ accounts_api_url|safe }},
};
// TODO: configurable userdata urls in models.js
......@@ -649,7 +652,6 @@
synnefo.config.grouped_public_network_name = {{ grouped_public_network_name|safe }};
synnefo.config.vm_hostname_format = {{ vm_hostname_format|safe }};
synnefo.config.automatic_network_range_format = {{ automatic_network_range_format|safe }};
synnefo.config.quota_url = '{% url synnefo.ui.views.user_quota %}';
synnefo.config.custom_image_help_url = '{{ custom_image_help_url|safe }}';
synnefo.config.quotas_update_interval = {{ quotas_update_interval }};
......
......@@ -37,7 +37,6 @@ import os
urlpatterns = patterns('',
url(r'^$', 'synnefo.ui.views.home', name='ui_index'),
url(r'^userquota$', 'synnefo.ui.views.user_quota', name='ui_userquota'),
url(r'userdata/', include('synnefo.ui.userdata.urls'))
)
......
......@@ -62,7 +62,6 @@ UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT = \
getattr(settings, "UI_UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT", 3)
UPDATE_INTERVAL_FAST = getattr(settings, "UI_UPDATE_INTERVAL_FAST", 2500)
UPDATE_INTERVAL_MAX = getattr(settings, "UI_UPDATE_INTERVAL_MAX", 10000)
QUOTAS_UPDATE_INTERVAL = getattr(settings, "UI_QUOTAS_UPDATE_INTERVAL", 10000)
# predefined values settings
VM_IMAGE_COMMON_METADATA = \
......@@ -161,6 +160,7 @@ GROUPED_PUBLIC_NETWORK_NAME = \
USER_CATALOG_URL = getattr(settings, 'UI_USER_CATALOG_URL', '/user_catalogs')
FEEDBACK_POST_URL = getattr(settings, 'UI_FEEDBACK_POST_URL', '/feedback')
TRANSLATE_UUIDS = not getattr(settings, 'TRANSLATE_UUIDS', False)
ACCOUNTS_API_URL = getattr(settings, 'UI_ACCOUNTS_API_URL', '/astakos/api')
def template(name, request, context):
......@@ -189,16 +189,16 @@ def home(request):
'compute_api_url': json.dumps(COMPUTE_API_URL),
'user_catalog_url': json.dumps(USER_CATALOG_URL),
'feedback_post_url': json.dumps(FEEDBACK_POST_URL),
'accounts_api_url': json.dumps(ACCOUNTS_API_URL),
'translate_uuids': json.dumps(TRANSLATE_UUIDS),
# update interval settings
'update_interval': UPDATE_INTERVAL,
'update_interval_increase': UPDATE_INTERVAL_INCREASE,
'update_interval_increase_after_calls':
UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT,
UPDATE_INTERVAL_INCREASE_AFTER_CALLS_COUNT,
'update_interval_fast': UPDATE_INTERVAL_FAST,
'update_interval_max': UPDATE_INTERVAL_MAX,
'changes_since_alignment': CHANGES_SINCE_ALIGNMENT,
'quotas_update_interval': QUOTAS_UPDATE_INTERVAL,
'image_icons': IMAGE_ICONS,
'logout_redirect': LOGOUT_URL,
'login_redirect': LOGIN_URL,
......@@ -255,14 +255,6 @@ def machines_console(request):
return template('machines_console', request, context)
def user_quota(request):
get_user(request, settings.ASTAKOS_URL, usage=True)
response = json.dumps(request.user['usage'])
return HttpResponse(response, mimetype="application/json")
def js_tests(request):
return template('tests', request, {})
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment