Commit db31f971 by Kálmán Viktor

Merge branch 'master' into custom-connect-command

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
parents d0ae4fc4 4268ac9d
...@@ -368,9 +368,9 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': ...@@ -368,9 +368,9 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
from shutilwhich import which from shutilwhich import which
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
# INSTALLED_APPS += ( # needed only for testing djangosaml2 INSTALLED_APPS += (
# 'djangosaml', 'djangosaml2',
# ) )
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend', 'djangosaml2.backends.Saml2Backend',
......
...@@ -27,6 +27,7 @@ from warnings import warn ...@@ -27,6 +27,7 @@ from warnings import warn
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import ( from django.db.models import (
CharField, DateTimeField, ForeignKey, NullBooleanField CharField, DateTimeField, ForeignKey, NullBooleanField
...@@ -413,6 +414,10 @@ class HumanReadableObject(object): ...@@ -413,6 +414,10 @@ class HumanReadableObject(object):
self._set_values(user_text_template, admin_text_template, params) self._set_values(user_text_template, admin_text_template, params)
def _set_values(self, user_text_template, admin_text_template, params): def _set_values(self, user_text_template, admin_text_template, params):
if isinstance(user_text_template, Promise):
user_text_template = user_text_template._proxy____args[0]
if isinstance(admin_text_template, Promise):
admin_text_template = admin_text_template._proxy____args[0]
self.user_text_template = user_text_template self.user_text_template = user_text_template
self.admin_text_template = admin_text_template self.admin_text_template = admin_text_template
self.params = params self.params = params
...@@ -451,6 +456,12 @@ class HumanReadableObject(object): ...@@ -451,6 +456,12 @@ class HumanReadableObject(object):
self.user_text_template, unicode(self.params)) self.user_text_template, unicode(self.params))
return self.user_text_template return self.user_text_template
def get_text(self, user):
if user and user.is_superuser:
return self.get_admin_text()
else:
return self.get_user_text()
def to_dict(self): def to_dict(self):
return {"user_text_template": self.user_text_template, return {"user_text_template": self.user_text_template,
"admin_text_template": self.admin_text_template, "admin_text_template": self.admin_text_template,
...@@ -481,13 +492,34 @@ class HumanReadableException(HumanReadableObject, Exception): ...@@ -481,13 +492,34 @@ class HumanReadableException(HumanReadableObject, Exception):
self.level = "error" self.level = "error"
def send_message(self, request, level=None): def send_message(self, request, level=None):
if request.user and request.user.is_superuser: msg = self.get_text(request.user)
msg = self.get_admin_text()
else:
msg = self.get_user_text()
getattr(messages, level or self.level)(request, msg) getattr(messages, level or self.level)(request, msg)
def fetch_human_exception(exception, user=None):
"""Fetch user readable message from exception.
>>> r = humanize_exception("foo", Exception())
>>> fetch_human_exception(r, User())
u'foo'
>>> fetch_human_exception(r).get_text(User())
u'foo'
>>> fetch_human_exception(Exception(), User())
u'Unknown error'
>>> fetch_human_exception(PermissionDenied(), User())
u'Permission Denied'
"""
if not isinstance(exception, HumanReadableException):
if isinstance(exception, PermissionDenied):
exception = create_readable(ugettext_noop("Permission Denied"))
else:
exception = create_readable(ugettext_noop("Unknown error"),
ugettext_noop("Unknown error: %(ex)s"),
ex=unicode(exception))
return exception.get_text(user) if user else exception
def humanize_exception(message, exception=None, level=None, **params): def humanize_exception(message, exception=None, level=None, **params):
"""Return new dynamic-class exception which is based on """Return new dynamic-class exception which is based on
HumanReadableException and the original class with the dict of exception. HumanReadableException and the original class with the dict of exception.
......
...@@ -18,10 +18,10 @@ ...@@ -18,10 +18,10 @@
from inspect import getargspec from inspect import getargspec
from logging import getLogger from logging import getLogger
from .models import activity_context, has_suffix
from django.core.exceptions import PermissionDenied, ImproperlyConfigured from django.core.exceptions import PermissionDenied, ImproperlyConfigured
from django.utils.translation import ugettext_noop
from .models import activity_context, has_suffix, humanize_exception
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -31,6 +31,7 @@ class Operation(object): ...@@ -31,6 +31,7 @@ class Operation(object):
""" """
async_queue = 'localhost.man' async_queue = 'localhost.man'
required_perms = None required_perms = None
superuser_required = False
do_not_call_in_templates = True do_not_call_in_templates = True
abortable = False abortable = False
has_percentage = False has_percentage = False
...@@ -143,13 +144,26 @@ class Operation(object): ...@@ -143,13 +144,26 @@ class Operation(object):
def check_precond(self): def check_precond(self):
pass pass
def check_auth(self, user): @classmethod
if self.required_perms is None: def check_perms(cls, user):
"""Check if user is permitted to run this operation on any instance
"""
if cls.required_perms is None:
raise ImproperlyConfigured( raise ImproperlyConfigured(
"Set required_perms to () if none needed.") "Set required_perms to () if none needed.")
if not user.has_perms(self.required_perms): if not user.has_perms(cls.required_perms):
raise PermissionDenied("%s doesn't have the required permissions." raise PermissionDenied("%s doesn't have the required permissions."
% user) % user)
if cls.superuser_required and not user.is_superuser:
raise humanize_exception(ugettext_noop(
"Superuser privileges are required."), PermissionDenied())
def check_auth(self, user):
"""Check if user is permitted to run this operation on this instance
"""
self.check_perms(user)
def create_activity(self, parent, user, kwargs): def create_activity(self, parent, user, kwargs):
raise NotImplementedError raise NotImplementedError
...@@ -185,14 +199,17 @@ class OperatedMixin(object): ...@@ -185,14 +199,17 @@ class OperatedMixin(object):
def __getattr__(self, name): def __getattr__(self, name):
# NOTE: __getattr__ is only called if the attribute doesn't already # NOTE: __getattr__ is only called if the attribute doesn't already
# exist in your __dict__ # exist in your __dict__
cls = self.__class__ return self.get_operation_class(name)(self)
@classmethod
def get_operation_class(cls, name):
ops = getattr(cls, operation_registry_name, {}) ops = getattr(cls, operation_registry_name, {})
op = ops.get(name) op = ops.get(name)
if op: if op:
return op(self) return op
else: else:
raise AttributeError("%r object has no attribute %r" % raise AttributeError("%r object has no attribute %r" %
(self.__class__.__name__, name)) (cls.__name__, name))
def get_available_operations(self, user): def get_available_operations(self, user):
"""Yield Operations that match permissions of user and preconditions. """Yield Operations that match permissions of user and preconditions.
......
...@@ -636,12 +636,8 @@ class LeaseForm(forms.ModelForm): ...@@ -636,12 +636,8 @@ class LeaseForm(forms.ModelForm):
Field('name'), Field('name'),
Field("suspend_interval_seconds", type="hidden", value="0"), Field("suspend_interval_seconds", type="hidden", value="0"),
Field("delete_interval_seconds", type="hidden", value="0"), Field("delete_interval_seconds", type="hidden", value="0"),
HTML(string_concat("<label>", _("Suspend in"), "</label>")),
Div( Div(
Div(
HTML(_("Suspend in")),
css_class="input-group-addon",
style="width: 100px;",
),
NumberField("suspend_hours", css_class="form-control"), NumberField("suspend_hours", css_class="form-control"),
Div( Div(
HTML(_("hours")), HTML(_("hours")),
...@@ -664,12 +660,8 @@ class LeaseForm(forms.ModelForm): ...@@ -664,12 +660,8 @@ class LeaseForm(forms.ModelForm):
), ),
css_class="input-group interval-input", css_class="input-group interval-input",
), ),
HTML(string_concat("<label>", _("Delete in"), "</label>")),
Div( Div(
Div(
HTML(_("Delete in")),
css_class="input-group-addon",
style="width: 100px;",
),
NumberField("delete_hours", css_class="form-control"), NumberField("delete_hours", css_class="form-control"),
Div( Div(
HTML(_("hours")), HTML(_("hours")),
...@@ -693,7 +685,7 @@ class LeaseForm(forms.ModelForm): ...@@ -693,7 +685,7 @@ class LeaseForm(forms.ModelForm):
css_class="input-group interval-input", css_class="input-group interval-input",
) )
) )
helper.add_input(Submit("submit", "Save changes")) helper.add_input(Submit("submit", _("Save changes")))
return helper return helper
class Meta: class Meta:
...@@ -705,6 +697,8 @@ class VmRenewForm(forms.Form): ...@@ -705,6 +697,8 @@ class VmRenewForm(forms.Form):
force = forms.BooleanField(required=False, label=_( force = forms.BooleanField(required=False, label=_(
"Set expiration times even if they are shorter than " "Set expiration times even if they are shorter than "
"the current value.")) "the current value."))
save = forms.BooleanField(required=False, label=_(
"Save selected lease."))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices') choices = kwargs.pop('choices')
...@@ -716,6 +710,32 @@ class VmRenewForm(forms.Form): ...@@ -716,6 +710,32 @@ class VmRenewForm(forms.Form):
empty_label=None, label=_('Length'))) empty_label=None, label=_('Length')))
if len(choices) < 2: if len(choices) < 2:
self.fields['lease'].widget = HiddenInput() self.fields['lease'].widget = HiddenInput()
self.fields['save'].widget = HiddenInput()
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class VmStateChangeForm(forms.Form):
interrupt = forms.BooleanField(required=False, label=_(
"Forcibly interrupt all running activities."),
help_text=_("Set all activities to finished state, "
"but don't interrupt any tasks."))
new_state = forms.ChoiceField(Instance.STATUS, label=_(
"New status"))
def __init__(self, *args, **kwargs):
show_interrupt = kwargs.pop('show_interrupt')
status = kwargs.pop('status')
super(VmStateChangeForm, self).__init__(*args, **kwargs)
if not show_interrupt:
self.fields['interrupt'].widget = HiddenInput()
self.fields['new_state'].initial = status
@property @property
def helper(self): def helper(self):
...@@ -1164,9 +1184,9 @@ class VmResourcesForm(forms.ModelForm): ...@@ -1164,9 +1184,9 @@ class VmResourcesForm(forms.ModelForm):
vm_search_choices = ( vm_search_choices = (
(0, _("owned")), ("owned", _("owned")),
(1, _("shared")), ("shared", _("shared")),
(2, _("all")), ("all", _("all")),
) )
...@@ -1185,5 +1205,5 @@ class VmListSearchForm(forms.Form): ...@@ -1185,5 +1205,5 @@ class VmListSearchForm(forms.Form):
# set initial value, otherwise it would be overwritten by request.GET # set initial value, otherwise it would be overwritten by request.GET
if not self.data.get("stype"): if not self.data.get("stype"):
data = self.data.copy() data = self.data.copy()
data['stype'] = 2 data['stype'] = "all"
self.data = data self.data = data
...@@ -874,3 +874,75 @@ textarea[name="list-new-namelist"] { ...@@ -874,3 +874,75 @@ textarea[name="list-new-namelist"] {
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
} }
#vm-list-table .migrating-icon {
-webkit-animation: passing 2s linear infinite;
animation: passing 2s linear infinite;
}
@-webkit-keyframes passing {
0% {
-webkit-transform: translateX(50%);
transform: translateX(50%);
opacity: 0;
}
50% {
-webkit-transform: translateX(0%);
transform: translateX(0%);
opacity: 1;
}
100% {
-webkit-transform: translateX(-50%);
transform: translateX(-50%);
opacity: 0;
}
}
@keyframes passing {
0% {
-webkit-transform: translateX(50%);
-ms-transform: translateX(50%);
transform: translateX(50%);
opacity: 0;
}
50% {
-webkit-transform: translateX(0%);
-ms-transform: translateX(0%);
transform: translateX(0%);
opacity: 1;
}
100% {
-webkit-transform: translateX(-50%);
-ms-transform: translateX(-50%);
transform: translateX(-50%);
opacity: 0;
}
}
.mass-migrate-node {
cursor: pointer;
}
.mass-op-panel {
padding: 6px 10px;
}
.mass-op-panel .check {
color: #449d44;
}
.mass-op-panel .minus {
color: #d9534f;
}
.mass-op-panel .status-icon {
font-size: .8em;
}
#vm-list-search, #vm-mass-ops {
margin-top: 8px;
}
...@@ -262,7 +262,7 @@ $(function () { ...@@ -262,7 +262,7 @@ $(function () {
$("#dashboard-vm-search-form").submit(function() { $("#dashboard-vm-search-form").submit(function() {
var vm_list_items = $("#dashboard-vm-list .list-group-item"); var vm_list_items = $("#dashboard-vm-list .list-group-item");
if(vm_list_items.length == 1) { if(vm_list_items.length == 1 && vm_list_items.first().prop("href")) {
window.location.href = vm_list_items.first().prop("href"); window.location.href = vm_list_items.first().prop("href");
return false; return false;
} }
...@@ -488,14 +488,19 @@ function addSliderMiscs() { ...@@ -488,14 +488,19 @@ function addSliderMiscs() {
ram_fire = true; ram_fire = true;
$(".ram-slider").simpleSlider("setValue", parseInt(val)); $(".ram-slider").simpleSlider("setValue", parseInt(val));
}); });
$(".cpu-priority-input").trigger("change");
$(".cpu-count-input, .ram-input").trigger("input"); setDefaultSliderValues();
$(".cpu-priority-slider").simpleSlider("setDisabled", $(".cpu-priority-input").prop("disabled")); $(".cpu-priority-slider").simpleSlider("setDisabled", $(".cpu-priority-input").prop("disabled"));
$(".cpu-count-slider").simpleSlider("setDisabled", $(".cpu-count-input").prop("disabled")); $(".cpu-count-slider").simpleSlider("setDisabled", $(".cpu-count-input").prop("disabled"));
$(".ram-slider").simpleSlider("setDisabled", $(".ram-input").prop("disabled")); $(".ram-slider").simpleSlider("setDisabled", $(".ram-input").prop("disabled"));
} }
function setDefaultSliderValues() {
$(".cpu-priority-input").trigger("change");
$(".ram-input, .cpu-count-input").trigger("input");
}
/* deletes the VM with the pk /* deletes the VM with the pk
* if dir is true, then redirect to the dashboard landing page * if dir is true, then redirect to the dashboard landing page
......
var ctrlDown, shiftDown = false;
var ctrlKey = 17;
var shiftKey = 16;
var selected = [];
$(function() { $(function() {
$(document).keydown(function(e) {
if (e.keyCode == ctrlKey) ctrlDown = true;
if (e.keyCode == shiftKey) shiftDown = true;
}).keyup(function(e) {
if (e.keyCode == ctrlKey) ctrlDown = false;
if (e.keyCode == shiftKey) shiftDown = false;
});
$('.group-list-table tbody').find('tr').mousedown(function() {
var retval = true;
if (ctrlDown) {
setRowColor($(this));
if(!$(this).hasClass('group-list-selected')) {
selected.splice(selected.indexOf($(this).index()), 1);
} else {
selected.push($(this).index());
}
retval = false;
} else if(shiftDown) {
if(selected.length > 0) {
start = selected[selected.length - 1] + 1;
end = $(this).index();
if(start > end) {
var tmp = start - 1; start = end; end = tmp - 1;
}
for(var i = start; i <= end; i++) {
if(selected.indexOf(i) < 0) {
selected.push(i);
setRowColor($('.group-list-table tbody tr').eq(i));
}
}
}
retval = false;
} else {
$('.group-list-selected').removeClass('group-list-selected');
$(this).addClass('group-list-selected');
selected = [$(this).index()];
}
// reset btn disables
$('.group-list-table tbody tr .btn').attr('disabled', false);
// show/hide group controls
if(selected.length > 1) {
$('.group-list-group-control a').attr('disabled', false);
for(var i = 0; i < selected.length; i++) {
$('.group-list-table tbody tr').eq(selected[i]).find('.btn').attr('disabled', true);
}
} else {
$('.group-list-group-control a').attr('disabled', true);
}
return retval;
});
$('#group-list-group-migrate').click(function() {
console.log(collectIds(selected));
});
$('tbody a').mousedown(function(e) {
// parent tr doesn't get selected when clicked
e.stopPropagation();
});
$('tbody a').click(function(e) {
// browser doesn't jump to top when clicked the buttons
if(!$(this).hasClass('real-link')) {
return false;
}
});
/* rename */ /* rename */
$("#group-list-rename-button, .group-details-rename-button").click(function() { $("#group-list-rename-button, .group-details-rename-button").click(function() {
$("#group-list-column-name", $(this).closest("tr")).hide(); $("#group-list-column-name", $(this).closest("tr")).hide();
...@@ -113,51 +34,4 @@ $(function() { ...@@ -113,51 +34,4 @@ $(function() {
return false; return false;
}); });
/* group actions */
/* select all */
$('#group-list-group-select-all').click(function() {
$('.group-list-table tbody tr').each(function() {
var index = $(this).index();
if(selected.indexOf(index) < 0) {
selected.push(index);
$(this).addClass('group-list-selected');
}
});
if(selected.length > 0)
$('.group-list-group-control a').attr('disabled', false);
return false;
});
/* mass vm delete */
$('#group-list-group-delete').click(function() {
addModalConfirmation(massDeleteVm,
{
'url': '/dashboard/group/mass-delete/',
'data': {
'selected': selected,
'v': collectIds(selected)
}
}
);
return false;
});
}); });
function collectIds(rows) {
var ids = [];
for(var i = 0; i < rows.length; i++) {
var div = $('td:first-child div', $('.group-list-table tbody tr').eq(rows[i]));
ids.push(div.prop('id').replace('node-', ''));
}
return ids;
}
function setRowColor(row) {
if(!row.hasClass('group-list-selected')) {
row.addClass('group-list-selected');
} else {
row.removeClass('group-list-selected');
}
}
...@@ -28,6 +28,9 @@ function vmCreateLoaded() { ...@@ -28,6 +28,9 @@ function vmCreateLoaded() {
$('#create-modal').on('hidden.bs.modal', function() { $('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#create-modal').remove();
}); });
$("#create-modal").on("shown.bs.modal", function() {
setDefaultSliderValues();
});
}); });
return false; return false;
}); });
...@@ -217,6 +220,8 @@ function vmCustomizeLoaded() { ...@@ -217,6 +220,8 @@ function vmCustomizeLoaded() {
}); });
if(error) return true; if(error) return true;
$(this).find("i").prop("class", "fa fa-spinner fa-spin");
$.ajax({ $.ajax({
url: '/dashboard/vm/create/', url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
......
...@@ -14,6 +14,7 @@ $(function() { ...@@ -14,6 +14,7 @@ $(function() {
$('.vm-list-table tbody').find('tr').mousedown(function() { $('.vm-list-table tbody').find('tr').mousedown(function() {
var retval = true; var retval = true;
if(!$(this).data("vm-pk")) return;
if (ctrlDown) { if (ctrlDown) {
setRowColor($(this)); setRowColor($(this));
if(!$(this).hasClass('vm-list-selected')) { if(!$(this).hasClass('vm-list-selected')) {
...@@ -46,86 +47,20 @@ $(function() { ...@@ -46,86 +47,20 @@ $(function() {
selected = [{'index': $(this).index(), 'vm': $(this).data("vm-pk")}]; selected = [{'index': $(this).index(), 'vm': $(this).data("vm-pk")}];
} }
// reset btn disables
$('.vm-list-table tbody tr .btn').attr('disabled', false);
// show/hide group controls // show/hide group controls
if(selected.length > 0) { if(selected.length > 0) {
$('.vm-list-group-control a').attr('disabled', false); $('#vm-mass-ops .mass-operation').attr('disabled', false);
for(var i = 0; i < selected.length; i++) {
$('.vm-list-table tbody tr').eq(selected[i]).find('.btn').attr('disabled', true);
}
} else { } else {
$('.vm-list-group-control a').attr('disabled', true); $('#vm-mass-ops .mass-operation').attr('disabled', true);
} }
return retval; return retval;
}); });
$('#vm-list-group-migrate').click(function() {
// pass?
});
$('.vm-list-details').popover({
'placement': 'auto',
'html': true,
'trigger': 'hover'
});
$('.vm-list-connect').popover({
'placement': 'left',
'html': true,
'trigger': 'click'
});
$('tbody a').mousedown(function(e) { $('tbody a').mousedown(function(e) {
// parent tr doesn't get selected when clicked // parent tr doesn't get selected when clicked
e.stopPropagation(); e.stopPropagation();
}); });
$('tbody a').click(function(e) {
// browser doesn't jump to top when clicked the buttons
if(!$(this).hasClass('real-link')) {
return false;
}
});
/* rename */
$("#vm-list-rename-button, .vm-details-rename-button").click(function() {
$("#vm-list-column-name", $(this).closest("tr")).hide();
$("#vm-list-rename", $(this).closest("tr")).css('display', 'inline');
$("#vm-list-rename-name", $(this).closest("tr")).focus();
});
/* rename ajax */
$('.vm-list-rename-submit').click(function() {
var row = $(this).closest("tr")
var name = $('#vm-list-rename-name', row).val();
var url = '/dashboard/vm/' + row.children("td:first-child").text().replace(" ", "") + '/';
$.ajax({
method: 'POST',
url: url,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
$("#vm-list-column-name", row).html(
$("<a/>", {
'class': "real-link",
href: "/dashboard/vm/" + data['vm_pk'] + "/",
text: data['new_name']
})
).show();
$('#vm-list-rename', row).hide();
// addMessage(data['message'], "success");
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
/* group actions */ /* group actions */
/* select all */ /* select all */
...@@ -133,27 +68,69 @@ $(function() { ...@@ -133,27 +68,69 @@ $(function() {