Commit 15d3d135 by Kálmán Viktor

Merge branch 'master' into feature-template-list

parents a304c0e4 4268ac9d
...@@ -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.
......
...@@ -867,3 +867,76 @@ textarea[name="list-new-namelist"] { ...@@ -867,3 +867,76 @@ textarea[name="list-new-namelist"] {
border-bottom: 1px dotted #aaa; border-bottom: 1px dotted #aaa;
padding: 5px 0px; padding: 5px 0px;
} }
#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;
}
...@@ -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
......
...@@ -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() {
$('.vm-list-table tbody tr').each(function() { $('.vm-list-table tbody tr').each(function() {
var index = $(this).index(); var index = $(this).index();
var vm = $(this).data("vm-pk"); var vm = $(this).data("vm-pk");
if(!isAlreadySelected(vm)) { if(vm && !isAlreadySelected(vm)) {
selected.push({'index': index, 'vm': vm}); selected.push({'index': index, 'vm': vm});
$(this).addClass('vm-list-selected'); $(this).addClass('vm-list-selected');
} }
}); });
if(selected.length > 0) if(selected.length > 0)
$('.vm-list-group-control a').attr('disabled', false); $('#vm-mass-ops .mass-operation').attr('disabled', false);
return false; return false;
}); });
/* mass vm delete */
$('#vm-list-group-delete').click(function() { /* mass operations */
addModalConfirmation(massDeleteVm, $("#vm-mass-ops").on('click', '.mass-operation', function(e) {
{ var icon = $(this).children("i").addClass('fa-spinner fa-spin');
'url': '/dashboard/vm/mass-delete/', params = "?" + selected.map(function(a){return "vm=" + a.vm}).join("&");
'data': {
'selected': selected, $.ajax({
'v': collectIds(selected) type: 'GET',
url: $(this).attr('href') + params,
success: function(data) {
icon.removeClass("fa-spinner fa-spin");
$('body').append(data);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
$("[title]").tooltip({'placement': "left"});
}
});
return false;
});
$("body").on("click", "#op-form-send", function() {
var url = $(this).closest("form").prop("action");
$(this).find("i").prop("class", "fa fa-fw fa-spinner fa-spin");
$.ajax({
url: url,
headers: {"X-CSRFToken": getCookie('csrftoken')},
type: 'POST',
data: $(this).closest('form').serialize(),
success: function(data, textStatus, xhr) {
/* hide the modal we just submitted */
$('#confirmation-modal').modal("hide");
updateStatuses(1);
/* if there are messages display them */
if(data.messages && data.messages.length > 0) {
addMessage(data.messages.join("<br />"), "danger");
}
},
error: function(xhr, textStatus, error) {
$('#confirmation-modal').modal("hide");
if (xhr.status == 500) {
addMessage("500 Internal Server Error", "danger");
} else {
addMessage(xhr.status + " " + xhr.statusText, "danger");
} }
} }
); });
return false; return false;
}); });
...@@ -181,8 +158,65 @@ $(function() { ...@@ -181,8 +158,65 @@ $(function() {
$(".vm-list-table th a").on("click", function(event) { $(".vm-list-table th a").on("click", function(event) {
event.preventDefault(); event.preventDefault();
}); });
$(document).on("click", ".mass-migrate-node", function() {
$(this).find('input[type="radio"]').prop("checked", true);
});
if(checkStatusUpdate()) {
updateStatuses(1);
}
}); });
function checkStatusUpdate() {
icons = $("#vm-list-table tbody td.state i");
if(icons.hasClass("fa-spin") || icons.hasClass("migrating-icon")) {
return true;
}
}
function updateStatuses(runs) {
$.get("/dashboard/vm/list/?compact", function(result) {
$("#vm-list-table tbody tr").each(function() {
vm = $(this).data("vm-pk");
status_td = $(this).find("td.state");
status_icon = status_td.find("i");
status_text = status_td.find("span");
if(vm in result) {
if(result[vm].in_status_change) {
if(!status_icon.hasClass("fa-spin")) {
status_icon.prop("class", "fa fa-fw fa-spinner fa-spin");
}
}
else if(result[vm].status == "MIGRATING") {
if(!status_icon.hasClass("migrating-icon")) {
status_icon.prop("class", "fa fa-fw " + result[vm].icon + " migrating-icon");
}
} else {
status_icon.prop("class", "fa fa-fw " + result[vm].icon);
}
status_text.text(result[vm].status);
if("node" in result[vm]) {
$(this).find(".node").text(result[vm].node);
}
} else {
$(this).remove();
}
});
if(checkStatusUpdate()) {
setTimeout(
function() {updateStatuses(runs + 1)},
1000 + Math.exp(runs * 0.05)
);
}
});
}
function isAlreadySelected(vm) { function isAlreadySelected(vm) {
for(var i=0; i<selected.length; i++) for(var i=0; i<selected.length; i++)
if(selected[i].vm == vm) if(selected[i].vm == vm)
......
{% load i18n %} {% load i18n %}
{% load hro %}
{% for n in notifications %} {% for n in notifications %}
<li class="notification-message" id="msg-{{n.id}}"> <li class="notification-message" id="msg-{{n.id}}">
<span class="notification-message-subject"> <span class="notification-message-subject">
{% if n.status == "new" %}<i class="fa fa-envelope-o"></i> {% endif %} {% if n.status == "new" %}<i class="fa fa-envelope-o"></i> {% endif %}
{{ n.subject.get_user_text }} {{ n.subject|get_text:user }}
</span> </span>
<span class="notification-message-date pull-right" title="{{n.created}}"> <span class="notification-message-date pull-right" title="{{n.created}}">
{{ n.created|timesince }} {{ n.created|timesince }}
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
<div class="notification-message-text"> <div class="notification-message-text">
{{ n.message.get_user_text|safe }} {{ n.message|get_text:user|safe }}
</div> </div>
</li> </li>
{% empty %} {% empty %}
......
{% extends "dashboard/mass-operate.html" %}
{% load i18n %}
{% load sizefieldtags %}
{% block formfields %}
<hr />
<ul id="vm-migrate-node-list" class="list-unstyled">
<li class="panel panel-default panel-primary mass-migrate-node">
<div class="panel-body">
<label for="migrate-to-none">
<strong>{% trans "Reschedule" %}</strong>
</label>
<input id="migrate-to-none" type="radio" name="node" value="" style="float: right;" checked="checked">
<span class="vm-migrate-node-property">
{% trans "This option will reschedule each virtual machine to the optimal node." %}
</span>
<div style="clear: both;"></div>
</div>
</li>
{% for n in nodes %}
<li class="panel panel-default mass-migrate-node">
<div class="panel-body">
<label for="migrate-to-{{n.pk}}">
<strong>{{ n }}</strong>
</label>
<input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;"/>
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div>
</div>
</li>
{% endfor %}
</ul>
<hr />
{% endblock %}
{% extends "dashboard/base.html" %} {% extends "dashboard/base.html" %}
{% load i18n %} {% load i18n %}
{% load hro %}
{% block content %} {% block content %}
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
<h1><i class="fa fa-{{icon}}"></i> <h1><i class="fa fa-{{icon}}"></i>
{{ object.instance.name }}: {{ object.instance.name }}: {{object.readable_name|get_text:user}}
{% if user.is_superuser %}
{{object.readable_name.get_admin_text}}
{% else %}
{{object.readable_name.get_user_text}}
{% endif %}
</h1> </h1>
</div> </div>
<div class="row"> <div class="row">
...@@ -58,7 +54,7 @@ ...@@ -58,7 +54,7 @@
<dt>{% trans "result" %}</dt> <dt>{% trans "result" %}</dt>
<dd><textarea class="form-control">{% if user.is_superuser %}{{object.result.get_admin_text}}{% else %}{{object.result.get_user_text}}{% endif %}</textarea></dd> <dd><textarea class="form-control">{{object.result|get_text:user}}</textarea></dd>
<dt>{% trans "resultant state" %}</dt> <dt>{% trans "resultant state" %}</dt>
<dd>{{object.resultant_state|default:'n/a'}}</dd> <dd>{{object.resultant_state|default:'n/a'}}</dd>
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a> <a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Create lease" %}</h3> <h3 class="no-margin"><i class="fa fa-clock-o"></i> {% trans "Create lease" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% with form=form %} {% with form=form %}
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a> <a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a>
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Edit lease" %}</h3> <h3 class="no-margin"><i class="fa fa-clock-o"></i> {% trans "Edit lease" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% with form=form %}