Commit 3a814456 by Őry Máté

Merge branch 'master' into feature-improve-inline-helps

parents 2b7a692c 7e1854d6
......@@ -368,9 +368,9 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
from shutilwhich import which
from saml2 import BINDING_HTTP_POST, BINDING_HTTP_REDIRECT
# INSTALLED_APPS += ( # needed only for testing djangosaml2
# 'djangosaml',
# )
INSTALLED_APPS += (
'djangosaml2',
)
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'djangosaml2.backends.Saml2Backend',
......
......@@ -27,6 +27,7 @@ from warnings import warn
from django.contrib import messages
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.exceptions import PermissionDenied
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import (
CharField, DateTimeField, ForeignKey, NullBooleanField
......@@ -413,6 +414,10 @@ class HumanReadableObject(object):
self._set_values(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.admin_text_template = admin_text_template
self.params = params
......@@ -451,6 +456,12 @@ class HumanReadableObject(object):
self.user_text_template, unicode(self.params))
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):
return {"user_text_template": self.user_text_template,
"admin_text_template": self.admin_text_template,
......@@ -481,13 +492,34 @@ class HumanReadableException(HumanReadableObject, Exception):
self.level = "error"
def send_message(self, request, level=None):
if request.user and request.user.is_superuser:
msg = self.get_admin_text()
else:
msg = self.get_user_text()
msg = self.get_text(request.user)
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):
"""Return new dynamic-class exception which is based on
HumanReadableException and the original class with the dict of exception.
......
......@@ -18,10 +18,10 @@
from inspect import getargspec
from logging import getLogger
from .models import activity_context, has_suffix
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__)
......@@ -31,6 +31,7 @@ class Operation(object):
"""
async_queue = 'localhost.man'
required_perms = None
superuser_required = False
do_not_call_in_templates = True
abortable = False
has_percentage = False
......@@ -143,13 +144,26 @@ class Operation(object):
def check_precond(self):
pass
def check_auth(self, user):
if self.required_perms is None:
@classmethod
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(
"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."
% 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):
raise NotImplementedError
......@@ -185,14 +199,17 @@ class OperatedMixin(object):
def __getattr__(self, name):
# NOTE: __getattr__ is only called if the attribute doesn't already
# 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, {})
op = ops.get(name)
if op:
return op(self)
return op
else:
raise AttributeError("%r object has no attribute %r" %
(self.__class__.__name__, name))
(cls.__name__, name))
def get_available_operations(self, user):
"""Yield Operations that match permissions of user and preconditions.
......
......@@ -867,3 +867,76 @@ textarea[name="list-new-namelist"] {
border-bottom: 1px dotted #aaa;
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() {
ram_fire = true;
$(".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-count-slider").simpleSlider("setDisabled", $(".cpu-count-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
* if dir is true, then redirect to the dashboard landing page
......
......@@ -28,6 +28,9 @@ function vmCreateLoaded() {
$('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove();
});
$("#create-modal").on("shown.bs.modal", function() {
setDefaultSliderValues();
});
});
return false;
});
......@@ -217,6 +220,8 @@ function vmCustomizeLoaded() {
});
if(error) return true;
$(this).find("i").prop("class", "fa fa-spinner fa-spin");
$.ajax({
url: '/dashboard/vm/create/',
headers: {"X-CSRFToken": getCookie('csrftoken')},
......
......@@ -14,6 +14,7 @@ $(function() {
$('.vm-list-table tbody').find('tr').mousedown(function() {
var retval = true;
if(!$(this).data("vm-pk")) return;
if (ctrlDown) {
setRowColor($(this));
if(!$(this).hasClass('vm-list-selected')) {
......@@ -46,86 +47,20 @@ $(function() {
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
if(selected.length > 0) {
$('.vm-list-group-control a').attr('disabled', false);
for(var i = 0; i < selected.length; i++) {
$('.vm-list-table tbody tr').eq(selected[i]).find('.btn').attr('disabled', true);
}
$('#vm-mass-ops .mass-operation').attr('disabled', false);
} else {
$('.vm-list-group-control a').attr('disabled', true);
$('#vm-mass-ops .mass-operation').attr('disabled', true);
}
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) {
// 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 */
$("#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 */
/* select all */
......@@ -133,27 +68,69 @@ $(function() {
$('.vm-list-table tbody tr').each(function() {
var index = $(this).index();
var vm = $(this).data("vm-pk");
if(!isAlreadySelected(vm)) {
if(vm && !isAlreadySelected(vm)) {
selected.push({'index': index, 'vm': vm});
$(this).addClass('vm-list-selected');
}
});
if(selected.length > 0)
$('.vm-list-group-control a').attr('disabled', false);
$('#vm-mass-ops .mass-operation').attr('disabled', false);
return false;
});
/* mass vm delete */
$('#vm-list-group-delete').click(function() {
addModalConfirmation(massDeleteVm,
{
'url': '/dashboard/vm/mass-delete/',
'data': {
'selected': selected,
'v': collectIds(selected)
/* mass operations */
$("#vm-mass-ops").on('click', '.mass-operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin');
params = "?" + selected.map(function(a){return "vm=" + a.vm}).join("&");
$.ajax({
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;
});
......@@ -181,8 +158,65 @@ $(function() {
$(".vm-list-table th a").on("click", function(event) {
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) {
for(var i=0; i<selected.length; i++)
if(selected[i].vm == vm)
......
{% load i18n %}
{% load hro %}
{% for n in notifications %}
<li class="notification-message" id="msg-{{n.id}}">
<span class="notification-message-subject">
{% if n.status == "new" %}<i class="fa fa-envelope-alt"></i> {% endif %}
{{ n.subject.get_user_text }}
{% if n.status == "new" %}<i class="fa fa-envelope-o"></i> {% endif %}
{{ n.subject|get_text:user }}
</span>
<span class="notification-message-date pull-right" title="{{n.created}}">
{{ n.created|timesince }}
</span>
<div style="clear: both;"></div>
<div class="notification-message-text">
{{ n.message.get_user_text|safe }}
{{ n.message|get_text:user|safe }}
</div>
</li>
{% 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 %}
......@@ -3,8 +3,10 @@
<div class="panel-heading">
<div class="pull-right toolbar">
<div class="btn-group">
<a href="#index-graph-view" data-index-box="node" class="btn btn-default btn-xs"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled"><i class="fa fa-list"></i></a>
<a href="#index-graph-view" data-index-box="node" class="btn btn-default btn-xs"
data-container="body"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled"
data-container="body"><i class="fa fa-list"></i></a>
</div>
<span class="btn btn-default btn-xs infobtn" title="{% trans "List of compute nodes, also called worker nodes or hypervisors, which run the virtual machines." %}"><i class="fa fa-info-circle"></i></span>
......
......@@ -3,10 +3,12 @@
<div class="panel-heading">
<div class="pull-right toolbar">
<div class="btn-group">
<a href="#index-graph-view" data-index-box="vm" class="btn
btn-default btn-xs" title="{% trans "summary view" %}"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="vm" class="btn
btn-default btn-xs disabled" title="{% trans "list view" %}"><i class="fa fa-list"></i></a>
<a href="#index-graph-view" data-index-box="vm" class="btn btn-default btn-xs"
data-container="body"
title="{% trans "summary view" %}"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="vm" class="btn btn-default btn-xs disabled"
data-container="body"
title="{% trans "list view" %}"><i class="fa fa-list"></i></a>
</div>
<span class="btn btn-default btn-xs infobtn" title="{% trans "List of your current virtual machines. Favourited ones are ahead of others." %}"><i class="fa fa-info-circle"></i></span>
</div>
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load hro %}
{% block content %}
<div class="body-content">
<div class="page-header">
<h1>
{{ object.instance.name }}:
{% if user.is_superuser %}
{{object.readable_name.get_admin_text}}
{% else %}
{{object.readable_name.get_user_text}}
{% endif %}
<h1><i class="fa fa-{{icon}}"></i>
{{ object.instance.name }}: {{object.readable_name|get_text:user}}
</h1>
</div>
<div class="row">
......@@ -58,7 +54,7 @@
<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>
<dd>{{object.resultant_state|default:'n/a'}}</dd>
......
......@@ -11,7 +11,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<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 class="panel-body">
{% with form=form %}
......
......@@ -10,7 +10,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<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 class="panel-body">
{% with form=form %}
......
{% load i18n %}
{% load crispy_forms_tags %}
{% block question %}
<p>
{% blocktrans with op=op.name count count=vm_count %}
Do you want to perform the <strong>{{op}}</strong> operation on the following instance?
{% plural %}
Do you want to perform the <strong>{{op}}</strong> operation on the following {{ count }} instances?
{% endblocktrans %}
</p>
<p class="text-info">{{op.description}}</p>
{% endblock %}
<form method="POST" action="{{url}}">{% csrf_token %}
{% block formfields %}{% endblock %}
{% for i in instances %}
<div class="panel panel-default mass-op-panel">
<i class="fa {{ i.get_status_icon }} fa-fw"></i>
{{ i.name }} ({{ i.pk }})
<div style="float: right;" title="{{ i.disabled }}" class="status-icon">
<span class="fa-stack">
<i class="fa fa-stack-2x fa-square {{ i.disabled|yesno:"minus,check" }}"></i>
<i class="fa fa-stack-1x fa-inverse fa-{% if i.disabled %}{{i.disabled_icon|default:"minus"}}{% else %}check{% endif %}"></i>
</span>
</div>
</div>
<input type="checkbox" name="vm" value="{{ i.pk }}" {% if not i.disabled %}checked{% endif %}
style="display: none;"/>
{% endfor %}
<div class="pull-right">
<a class="btn btn-default" href="{% url "dashboard.views.vm-list" %}"
data-dismiss="modal">{% trans "Cancel" %}</a>
<button class="btn btn-{{ opview.effect }}" type="submit" id="op-form-send">
{% if opview.icon %}<i class="fa fa-fw fa-{{opview.icon}}"></i> {% endif %}{{ opview.name|capfirst }}
</button>
</div>
</form>
{% load i18n %}
{% load hro %}
<div id="activity-timeline" class="timeline">
{% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}">
......@@ -16,10 +17,7 @@
<div data-activity-id="{{ s.pk }}"
class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}"
>
{% if user.is_superuser %}
{{ s.readable_name.get_admin_text }}
{% else %}
{{ s.readable_name.get_user_text }}{% endif %}
{{ s.readable_name|get_text:user }}
&ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
......
......@@ -31,7 +31,7 @@
<i class="fa fa-plus"></i> {% trans "new lease" %}
</a>
{% endif %}
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Leases" %}</h3>
<h3 class="no-margin"><i class="fa fa-clock-o"></i> {% trans "Leases" %}</h3>
</div>
<div class="panel-body">
<div class="" style="max-width: 600px;">
......
{% load i18n %}
{% load hro %}
<div id="activity-timeline" class="timeline">
{% for a in activities %}
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" data-activity-id="{{ a.pk }}">
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
<i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-plus{% endif %}"></i>
<i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i>
</span>
<strong{% if a.result %} title="{{ a.result.get_user_text }}"{% endif %}>
<strong{% if a.result %} title="{{ a.result|get_text:user }}"{% endif %}>
<a href="{{ a.get_absolute_url }}">
{% if a.times > 1 %}({{ a.times }}x){% endif %}
{{ a.readable_name.get_user_text|capfirst }}</a>
{{ a.readable_name|get_text:user|capfirst }}</a>
{% if a.has_percent %}
- {{ a.percentage }}%
......@@ -33,9 +33,9 @@
<div class="sub-timeline">
{% for s in a.children.all %}
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}{% if s.pk == active.pk %} sub-activity-active{% endif %}">
<span{% if s.result %} title="{{ s.result.get_user_text }}"{% endif %}>
<span{% if s.result %} title="{{ s.result|get_text:user }}"{% endif %}>
<a href="{{ s.get_absolute_url }}">
{{ s.readable_name.get_user_text|capfirst }}</a></span> &ndash;
{{ s.readable_name|get_text:user|capfirst }}</a></span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
......
......@@ -7,7 +7,7 @@
<span class="operation operation-{{op.op}} btn btn-default disabled btn-xs">
{% else %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn
btn-{{op.effect}} btn-xs" title="{{op.name|capfirst}}: {{op.description|truncatewords:20}}">
btn-{{op.effect}} btn-xs" title="{{op.name|capfirst}}: {{op.description|truncatewords:15}}">
{% endif %}
<i class="fa fa-{{op.icon}}"></i>
<span{% if not op.is_preferred %} class="sr-only"{% endif %}>{{op.name}}</span>
......
......@@ -15,7 +15,19 @@
</div>
<h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3>
</div>
<div class="pull-right" style="max-width: 300px; margin-top: 15px; margin-right: 15px;">
<div class="panel-body">
<div class="row">
<div class="col-md-8 vm-list-group-control" id="vm-mass-ops">
<strong>{% trans "Group actions" %}</strong>
<button id="vm-list-group-select-all" class="btn btn-info btn-xs">{% trans "Select all" %}</button>
{% for o in ops %}
<a href="{{ o.get_url }}" class="btn btn-xs btn-{{ o.effect }} mass-operation"
title="{{ o.name|capfirst }}" disabled>
<i class="fa fa-{{ o.icon }}"></i>
</a>
{% endfor %}
</div><!-- .vm-list-group-control -->
<div class="col-md-4" id="vm-list-search">
<form action="" method="GET">
<div class="input-group">
{{ search_form.s }}
......@@ -25,31 +37,14 @@
<i class="fa fa-search"></i>
</button>
</div>
</div>
</div><!-- .input-group -->
</form>
</div>
<div class="panel-body vm-list-group-control">
<p>
<strong>{% trans "Group actions" %}</strong>
<!--
<button id="vm-list-group-select-all" class="btn btn-info btn-xs">{% trans "Select all" %}</button>
<a href="#" class="btn btn-default btn-xs" title="{% trans "Migrate" %}" disabled>
<i class="fa fa-truck"></i>
</a>
<a href="#" class="btn btn-default btn-xs" title="{% trans "Reboot" %}" disabled>
<i class="fa fa-refresh"></i>
</a>
<a href="#" class="btn btn-default btn-xs" title="{% trans "Shutdown" %}" disabled>
<i class="fa fa-power-off"></i>
</a>
-->
<a title="{% trans "Destroy" %}" id="vm-list-group-delete" disabled href="#" class="btn btn-danger btn-xs" disabled>
<i class="fa fa-times"></i>
</a>
</p>
</div>
</div><!-- .col-md-4 #vm-list-search -->
</div><!-- .row -->
</div><!-- .panel-body -->
<div class="panel-body">
<table class="table table-bordered table-striped table-hover vm-list-table">
<table class="table table-bordered table-striped table-hover vm-list-table"
id="vm-list-table">
<thead><tr>
<th data-sort="int" class="orderable pk sortable vm-list-table-thin" style="min-width: 50px;">
{% trans "ID" as t %}
......@@ -76,12 +71,21 @@
<tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}">
<td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td>
<td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td>
<td class="state">{{ i.get_status_display }}</td>
<td class="state">
<i class="fa fa-fw
{% if i.is_in_status_change %}
fa-spin fa-spinner
{% else %}
{{ i.get_status_icon }}{% endif %}"></i>
<span>{{ i.get_status_display }}</span>
</td>
<td>
{% include "dashboard/_display-name.html" with user=i.owner show_org=True %}
</td>
{% if user.is_superuser %}
<td data-sort-value="{{ i.node.normalized_name }}">{{ i.node.name|default:"-" }}</td>
<td class="node "data-sort-value="{{ i.node.normalized_name }}">
{{ i.node.name|default:"-" }}
</td>
{% endif %}
</tr>
{% empty %}
......@@ -115,6 +119,5 @@
{% block extra_js %}
<script src="{{ STATIC_URL}}dashboard/vm-list.js"></script>
<script src="{{ STATIC_URL}}dashboard/vm-common.js"></script>
<script src="{{ STATIC_URL}}dashboard/js/stupidtable.min.js"></script>
{% endblock %}
from django.template import Library
register = Library()
@register.filter
def get_text(human_readable, user):
if human_readable is None:
return u""
else:
return human_readable.get_text(user)
......@@ -29,7 +29,7 @@ from django.utils import baseconv
from ..models import Profile
from ..views import InstanceActivityDetail, InstanceActivity
from ..views import vm_ops, Instance, UnsubscribeFormView
from ..views import vm_ops, vm_mass_ops, Instance, UnsubscribeFormView
from ..views import AclUpdateView
from .. import views
......@@ -259,6 +259,114 @@ class VmOperationViewTestCase(unittest.TestCase):
self.assertEquals(rend.status_code, 200)
class VmMassOperationViewTestCase(unittest.TestCase):
def test_available(self):
request = FakeRequestFactory(superuser=True)
view = vm_mass_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.destroy = Instance._ops['destroy'](inst)
go.return_value = [inst]
self.assertEquals(
view.as_view()(request, pk=1234).render().status_code, 200)
def test_unpermitted_choice(self):
"User has user level, but not the needed ownership."
request = FakeRequestFactory()
view = vm_mass_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.has_level = lambda self, l: {"user": True, "owner": False}[l]
inst.destroy = Instance._ops['destroy'](inst)
inst.destroy._operate = MagicMock()
go.return_value = [inst]
view.as_view()(request, pk=1234).render()
assert not inst.destroy._operate.called
def test_unpermitted(self):
request = FakeRequestFactory()
view = vm_mass_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.destroy = Instance._ops['destroy'](inst)
inst.has_level.return_value = False
go.return_value = [inst]
with self.assertRaises(PermissionDenied):
view.as_view()(request, pk=1234).render()
def test_migrate(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=True)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.has_level.return_value = True
go.return_value = [inst]
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location']
assert not msg.error.called
def test_migrate_failed(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=True)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.migrate.async.side_effect = Exception
inst.has_level.return_value = True
go.return_value = [inst]
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location']
assert msg.error.called
def test_migrate_wo_permission(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=False)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.has_level.return_value = True
go.return_value = [inst]
go4.return_value = MagicMock()
with self.assertRaises(PermissionDenied):
assert view.as_view()(request, pk=1234)['location']
def test_migrate_template(self):
"""check if GET dialog's template can be rendered"""
request = FakeRequestFactory(superuser=True)
view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.has_level.return_value = True
go.return_value = [inst]
self.assertEquals(
view.as_view()(request, pk=1234).render().status_code, 200)
class RenewViewTest(unittest.TestCase):
def test_renew_template(self):
......
......@@ -107,20 +107,6 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.get('/dashboard/vm/1/')
self.assertEqual(response.status_code, 200)
def test_unpermitted_vm_mass_delete(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 403)
def test_permitted_vm_mass_delete(self):
c = Client()
self.login(c, 'user2')
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 302)
def test_unpermitted_password_change(self):
c = Client()
self.login(c, "user2")
......
......@@ -29,7 +29,7 @@ from .views import (
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
VmDetailVncTokenView, VmGraphView, VmList,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveUserView,
GroupRemoveFutureUserView,
......@@ -49,7 +49,6 @@ from .views import (
autocomplete_light.autodiscover()
urlpatterns = patterns(
'',
url(r'^$', IndexView.as_view(), name="dashboard.index"),
url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(),
......@@ -74,7 +73,7 @@ urlpatterns = patterns(
url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(),
name="dashboard.views.template-delete"),
url(r'^vm/(?P<pk>\d+)/op/', include('dashboard.vm.urls')),
url(r'^vm/', include('dashboard.vm.urls')),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
......@@ -88,8 +87,6 @@ urlpatterns = patterns(
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'),
url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'),
......
......@@ -77,7 +77,10 @@ from .tables import (
NodeListTable, TemplateListTable, LeaseListTable,
GroupListTable, UserKeyListTable
)
from common.models import HumanReadableObject, HumanReadableException
from common.models import (
HumanReadableObject, HumanReadableException, fetch_human_exception,
create_readable,
)
from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
InterfaceTemplate, Lease, Node, NodeActivity, Trait,
......@@ -309,7 +312,7 @@ class VmDetailView(CheckedDetailView):
activities = instance.get_merged_activities(user)
show_show_all = len(activities) > 10
activities = activities[:10]
context['activities'] = activities
context['activities'] = _format_activities(activities)
context['show_show_all'] = show_show_all
latest = instance.get_latest_activity_in_progress()
context['is_new_state'] = (latest and
......@@ -561,6 +564,10 @@ class OperationView(RedirectToLoginMixin, DetailView):
setattr(self, '_opobj', getattr(self.get_object(), self.op))
return self._opobj
@classmethod
def get_operation_class(cls):
return cls.model.get_operation_class(cls.op)
def get_context_data(self, **kwargs):
ctx = super(OperationView, self).get_context_data(**kwargs)
ctx['op'] = self.get_op()
......@@ -576,6 +583,10 @@ class OperationView(RedirectToLoginMixin, DetailView):
logger.debug("OperationView.check_auth(%s)", unicode(self))
self.get_op().check_auth(self.request.user)
@classmethod
def check_perms(cls, user):
cls.get_operation_class().check_perms(user)
def get(self, request, *args, **kwargs):
self.check_auth()
return super(OperationView, self).get(request, *args, **kwargs)
......@@ -1010,6 +1021,112 @@ def get_operations(instance, user):
return ops
class MassOperationView(OperationView):
template_name = 'dashboard/mass-operate.html'
def check_auth(self):
self.get_op().check_perms(self.request.user)
for i in self.get_object():
if not i.has_level(self.request.user, "user"):
raise PermissionDenied(
"You have no user access to instance %d" % i.pk)
@classmethod
def get_urlname(cls):
return 'dashboard.vm.mass-op.%s' % cls.op
@classmethod
def get_url(cls):
return reverse("dashboard.vm.mass-op.%s" % cls.op)
def get_op(self, instance=None):
if instance:
return getattr(instance, self.op)
else:
return Instance._ops[self.op]
def get_context_data(self, **kwargs):
ctx = super(MassOperationView, self).get_context_data(**kwargs)
instances = self.get_object()
ctx['instances'] = self._get_operable_instances(
instances, self.request.user)
ctx['vm_count'] = sum(1 for i in ctx['instances'] if not i.disabled)
return ctx
def _call_operations(self, extra):
request = self.request
user = request.user
instances = self.get_object()
for i in instances:
try:
self.get_op(i).async(user=user, **extra)
except HumanReadableException as e:
e.send_message(request)
except Exception as e:
# pre-existing errors should have been catched when the
# confirmation dialog was constructed
messages.error(request, _(
"Failed to execute %(op)s operation on "
"instance %(instance)s.") % {"op": self.name,
"instance": i})
def get_object(self):
vms = getattr(self.request, self.request.method).getlist("vm")
return Instance.objects.filter(pk__in=vms)
def _get_operable_instances(self, instances, user):
for i in instances:
try:
op = self.get_op(i)
op.check_auth(user)
op.check_precond()
except PermissionDenied as e:
i.disabled = create_readable(
_("You are not permitted to execute %(op)s on instance "
"%(instance)s."), instance=i.pk, op=self.name)
i.disabled_icon = "lock"
except Exception as e:
i.disabled = fetch_human_exception(e)
else:
i.disabled = None
return instances
def post(self, request, extra=None, *args, **kwargs):
self.check_auth()
if extra is None:
extra = {}
self._call_operations(extra)
if request.is_ajax():
store = messages.get_messages(request)
store.used = True
return HttpResponse(
json.dumps({'messages': [unicode(m) for m in store]}),
content_type="application/json"
)
else:
return redirect(reverse("dashboard.views.vm-list"))
@classmethod
def factory(cls, vm_op, extra_bases=(), **kwargs):
return type(str(cls.__name__ + vm_op.op),
tuple(list(extra_bases) + [cls, vm_op]), kwargs)
class MassMigrationView(MassOperationView, VmMigrateView):
template_name = 'dashboard/_vm-mass-migrate.html'
vm_mass_ops = OrderedDict([
('deploy', MassOperationView.factory(vm_ops['deploy'])),
('wake_up', MassOperationView.factory(vm_ops['wake_up'])),
('sleep', MassOperationView.factory(vm_ops['sleep'])),
('reboot', MassOperationView.factory(vm_ops['reboot'])),
('reset', MassOperationView.factory(vm_ops['reset'])),
('shut_off', MassOperationView.factory(vm_ops['shut_off'])),
('migrate', MassMigrationView),
('destroy', MassOperationView.factory(vm_ops['destroy'])),
])
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
template_name = "dashboard/node-detail.html"
model = Node
......@@ -1582,11 +1699,41 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
def get_context_data(self, *args, **kwargs):
context = super(VmList, self).get_context_data(*args, **kwargs)
context['ops'] = []
for k, v in vm_mass_ops.iteritems():
try:
v.check_perms(user=self.request.user)
except PermissionDenied:
pass
else:
context['ops'].append(v)
context['search_form'] = self.search_form
return context
def get(self, *args, **kwargs):
if self.request.is_ajax():
return self._create_ajax_request()
else:
self.search_form = VmListSearchForm(self.request.GET)
self.search_form.full_clean()
return super(VmList, self).get(*args, **kwargs)
def _create_ajax_request(self):
if self.request.GET.get("compact") is not None:
instances = Instance.get_objects_with_level(
"user", self.request.user).filter(destroyed_at=None)
statuses = {}
for i in instances:
statuses[i.pk] = {
'status': i.get_status_display(),
'icon': i.get_status_icon(),
'in_status_change': i.is_in_status_change(),
}
if self.request.user.is_superuser:
statuses[i.pk]['node'] = i.node.name if i.node else "-"
return HttpResponse(json.dumps(statuses),
content_type="application/json")
else:
favs = Instance.objects.filter(
favourite__user=self.request.user).values_list('pk', flat=True)
instances = Instance.get_objects_with_level(
......@@ -1598,15 +1745,12 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
'icon': i.get_status_icon(),
'host': "" if not i.primary_host else i.primary_host.hostname,
'status': i.get_status_display(),
'fav': i.pk in favs} for i in instances]
'fav': i.pk in favs,
} for i in instances]
return HttpResponse(
json.dumps(list(instances)), # instances is ValuesQuerySet
content_type="application/json",
)
else:
self.search_form = VmListSearchForm(self.request.GET)
self.search_form.full_clean()
return super(VmList, self).get(*args, **kwargs)
def get_queryset(self):
logger.debug('VmList.get_queryset() called. User: %s',
......@@ -1969,7 +2113,7 @@ class VmCreate(LoginRequiredMixin, TemplateView):
"Successfully created %(count)d VM.", # this should not happen
"Successfully created %(count)d VMs.", len(instances)) % {
'count': len(instances)})
path = reverse("dashboard.index")
path = "%s?stype=owned" % reverse("dashboard.views.vm-list")
else:
messages.success(request, _("VM successfully created."))
path = instances[0].get_absolute_url()
......@@ -2371,48 +2515,6 @@ class PortDelete(LoginRequiredMixin, DeleteView):
kwargs={'pk': self.kwargs.get("pk")})
class VmMassDelete(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
vms = request.GET.getlist('v[]')
objects = Instance.objects.filter(pk__in=vms)
return render(request, "dashboard/confirm/mass-delete.html",
{'objects': objects})
def post(self, request, *args, **kwargs):
vms = request.POST.getlist('vms')
names = []
if vms is not None:
for i in Instance.objects.filter(pk__in=vms):
if not i.has_level(request.user, 'owner'):
logger.info('Tried to delete instance #%d without owner '
'permission by %s.', i.pk,
unicode(request.user))
# no need for rollback or proper error message, this can't
# normally happen:
raise PermissionDenied()
try:
i.destroy.async(user=request.user)
names.append(i.name)
except Exception as e:
logger.error(e)
success_message = ungettext_lazy(
"Mass delete complete, the following VM was deleted: %s.",
"Mass delete complete, the following VMs were deleted: %s.",
len(names)) % u', '.join(names)
# we can get this only via AJAX ...
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json"
)
else:
messages.success(request, success_message)
next = request.GET.get('next')
return redirect(next if next else reverse_lazy('dashboard.index'))
class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
SuccessMessageMixin, CreateView):
model = Lease
......@@ -2501,7 +2603,8 @@ def vm_activity(request, pk):
response = {}
show_all = request.GET.get("show_all", "false") == "true"
activities = instance.get_merged_activities(request.user)
activities = _format_activities(
instance.get_merged_activities(request.user))
show_show_all = len(activities) > 10
if not show_all:
activities = activities[:10]
......@@ -2743,7 +2846,7 @@ class NodeGraphView(SuperuserRequiredMixin, GraphViewBase):
model = Node
def get_prefix(self, instance):
return 'circle.%s' % instance.name
return 'circle.%s' % instance.host.hostname
def get_title(self, instance, metric):
return '%s - %s' % (instance.name, metric)
......@@ -2958,6 +3061,20 @@ def get_disk_download_status(request, pk):
)
def _get_activity_icon(act):
op = act.get_operation()
if op and op.id in vm_ops:
return vm_ops[op.id].icon
else:
return "cog"
def _format_activities(acts):
for i in acts:
i.icon = _get_activity_icon(i)
return acts
class InstanceActivityDetail(CheckedDetailView):
model = InstanceActivity
context_object_name = 'instanceactivity' # much simpler to mock object
......@@ -2968,8 +3085,9 @@ class InstanceActivityDetail(CheckedDetailView):
def get_context_data(self, **kwargs):
ctx = super(InstanceActivityDetail, self).get_context_data(**kwargs)
ctx['activities'] = self.object.instance.get_activities(
self.request.user)
ctx['activities'] = _format_activities(
self.object.instance.get_activities(self.request.user))
ctx['icon'] = _get_activity_icon(self.object)
return ctx
......
......@@ -17,9 +17,17 @@
from django.conf.urls import patterns, url
from ..views import vm_ops
from ..views import vm_ops, vm_mass_ops
urlpatterns = patterns('',
*(url(r'^%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems()))
urlpatterns = patterns(
'',
*(url(r'^(?P<pk>\d+)/op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems())
)
urlpatterns += patterns(
'',
*(url(r'^mass_op/%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_mass_ops.iteritems())
)
......@@ -101,7 +101,7 @@ def pull(dir="~/circle/circle"):
@roles('portal')
def update_portal(test=False):
"Update and restart portal+manager"
with _stopped("portal", "mancelery"):
with _stopped("portal", "manager"):
pull()
pip("circle", "~/circle/requirements.txt")
migrate()
......@@ -113,7 +113,7 @@ def update_portal(test=False):
@roles('portal')
def stop_portal(test=False):
"Stop portal and manager"
_stop_services("portal", "mancelery")
_stop_services("portal", "manager")
@roles('node')
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-08-01 19:36+0200\n"
"POT-Creation-Date: 2014-08-29 09:21+0200\n"
"PO-Revision-Date: 2014-08-01 21:03+0200\n"
"Last-Translator: Mate Ory <ory.mate@ik.bme.hu>\n"
"Language-Team: Hungarian <cloud@ik.bme.hu>\n"
......@@ -23,11 +23,11 @@ msgid "Select an option to proceed!"
msgstr "Válasszon a folytatáshoz."
#: dashboard/static/dashboard/dashboard.js:258
#: dashboard/static/dashboard/dashboard.js:305
#: dashboard/static/dashboard/dashboard.js:315
#: static_collected/dashboard/dashboard.js:257
#: static_collected/dashboard/dashboard.js:304
#: static_collected/dashboard/dashboard.js:314
#: dashboard/static/dashboard/dashboard.js:306
#: dashboard/static/dashboard/dashboard.js:316
#: static_collected/dashboard/dashboard.js:258
#: static_collected/dashboard/dashboard.js:306
#: static_collected/dashboard/dashboard.js:316
msgid "No result"
msgstr "Nincs eredmény"
......@@ -41,12 +41,15 @@ msgstr "Nincs jogosultsága a profil módosításához."
msgid "Unknown error."
msgstr "Ismeretlen hiba."
#: dashboard/static/dashboard/vm-create.js:108
#: dashboard/static/dashboard/vm-create.js:171
#: dashboard/static/dashboard/vm-create.js:111
#: dashboard/static/dashboard/vm-create.js:174
#: static_collected/dashboard/vm-create.js:111
#: static_collected/dashboard/vm-create.js:174
msgid "No more networks."
msgstr "Nincs több hálózat."
#: dashboard/static/dashboard/vm-create.js:140
#: dashboard/static/dashboard/vm-create.js:143
#: static_collected/dashboard/vm-create.js:143
msgid "Not added to any network"
msgstr "Nincs hálózathoz adva"
......@@ -428,5 +431,3 @@ msgstr "Tegnap"
#: static_collected/admin/js/admin/DateTimeShortcuts.js:203
msgid "Tomorrow"
msgstr "Holnap"
# end
......@@ -31,7 +31,6 @@ celery = Celery('manager',
'storage.tasks.local_tasks',
'storage.tasks.periodic_tasks',
'firewall.tasks.local_tasks',
'monitor.tasks.local_periodic_tasks',
'dashboard.tasks.local_periodic_tasks',
])
......@@ -42,20 +41,8 @@ celery.conf.update(
CELERY_QUEUES=(
Queue(HOSTNAME + '.man', Exchange('manager', type='direct'),
routing_key="manager"),
Queue(HOSTNAME + '.monitor', Exchange('monitor', type='direct'),
routing_key="monitor"),
),
CELERYBEAT_SCHEDULE={
'vm.update_domain_states': {
'task': 'vm.tasks.local_periodic_tasks.update_domain_states',
'schedule': timedelta(seconds=10),
'options': {'queue': 'localhost.man'}
},
'vm.garbage_collector': {
'task': 'vm.tasks.local_periodic_tasks.garbage_collector',
'schedule': timedelta(minutes=10),
'options': {'queue': 'localhost.man'}
},
'storage.periodic_tasks': {
'task': 'storage.tasks.periodic_tasks.garbage_collector',
'schedule': timedelta(hours=1),
......@@ -67,24 +54,6 @@ celery.conf.update(
'schedule': timedelta(hours=24),
'options': {'queue': 'localhost.man'}
},
'monitor.measure_response_time': {
'task': 'monitor.tasks.local_periodic_tasks.'
'measure_response_time',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.man'}
},
'monitor.check_celery_queues': {
'task': 'monitor.tasks.local_periodic_tasks.'
'check_celery_queues',
'schedule': timedelta(seconds=60),
'options': {'queue': 'localhost.man'}
},
'monitor.instance_per_template': {
'task': 'monitor.tasks.local_periodic_tasks.'
'instance_per_template',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.man'}
},
}
)
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from celery import Celery
from datetime import timedelta
from kombu import Queue, Exchange
from os import getenv
HOSTNAME = "localhost"
CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/")
celery = Celery('monitor',
broker=getenv("AMQP_URI"),
include=['vm.tasks.local_periodic_tasks',
'monitor.tasks.local_periodic_tasks',
])
celery.conf.update(
CELERY_RESULT_BACKEND='cache',
CELERY_CACHE_BACKEND=CACHE_URI,
CELERY_TASK_RESULT_EXPIRES=300,
CELERY_QUEUES=(
Queue(HOSTNAME + '.monitor', Exchange('monitor', type='direct'),
routing_key="monitor"),
),
CELERYBEAT_SCHEDULE={
'vm.update_domain_states': {
'task': 'vm.tasks.local_periodic_tasks.update_domain_states',
'schedule': timedelta(seconds=10),
'options': {'queue': 'localhost.monitor'}
},
'monitor.measure_response_time': {
'task': 'monitor.tasks.local_periodic_tasks.'
'measure_response_time',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.monitor'}
},
'monitor.check_celery_queues': {
'task': 'monitor.tasks.local_periodic_tasks.'
'check_celery_queues',
'schedule': timedelta(seconds=60),
'options': {'queue': 'localhost.monitor'}
},
'monitor.instance_per_template': {
'task': 'monitor.tasks.local_periodic_tasks.'
'instance_per_template',
'schedule': timedelta(seconds=30),
'options': {'queue': 'localhost.monitor'}
},
}
)
# Copyright 2014 Budapest University of Technology and Economics (BME IK)
#
# This file is part of CIRCLE Cloud.
#
# CIRCLE is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# CIRCLE is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from celery import Celery
from datetime import timedelta
from kombu import Queue, Exchange
from os import getenv
HOSTNAME = "localhost"
CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/")
celery = Celery('manager.slow',
broker=getenv("AMQP_URI"),
include=['vm.tasks.local_tasks',
'vm.tasks.local_periodic_tasks',
'storage.tasks.local_tasks',
'storage.tasks.periodic_tasks',
])
celery.conf.update(
CELERY_RESULT_BACKEND='cache',
CELERY_CACHE_BACKEND=CACHE_URI,
CELERY_TASK_RESULT_EXPIRES=300,
CELERY_QUEUES=(
Queue(HOSTNAME + '.man.slow', Exchange('manager.slow', type='direct'),
routing_key="manager.slow"),
),
CELERYBEAT_SCHEDULE={
'vm.garbage_collector': {
'task': 'vm.tasks.local_periodic_tasks.garbage_collector',
'schedule': timedelta(minutes=10),
'options': {'queue': 'localhost.man.slow'}
},
}
)
......@@ -90,7 +90,8 @@ class InstanceActivity(ActivityModel):
@classmethod
def create(cls, code_suffix, instance, task_uuid=None, user=None,
concurrency_check=True, readable_name=None):
concurrency_check=True, readable_name=None,
resultant_state=None):
readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities
......@@ -100,14 +101,14 @@ class InstanceActivity(ActivityModel):
activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
act = cls(activity_code=activity_code, instance=instance, parent=None,
resultant_state=None, started=timezone.now(),
resultant_state=resultant_state, started=timezone.now(),
readable_name_data=readable_name.to_dict(),
task_uuid=task_uuid, user=user)
act.save()
return act
def create_sub(self, code_suffix, task_uuid=None, concurrency_check=True,
readable_name=None):
readable_name=None, resultant_state=None):
readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities
......@@ -117,7 +118,8 @@ class InstanceActivity(ActivityModel):
act = InstanceActivity(
activity_code=join_activity_code(self.activity_code, code_suffix),
instance=self.instance, parent=self, resultant_state=None,
instance=self.instance, parent=self,
resultant_state=resultant_state,
readable_name_data=readable_name.to_dict(), started=timezone.now(),
task_uuid=task_uuid, user=self.user)
act.save()
......@@ -190,18 +192,23 @@ class InstanceActivity(ActivityModel):
readable_name=readable_name)
return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
def get_operation(self):
return self.instance.get_operation_from_activity_code(
self.activity_code)
@contextmanager
def instance_activity(code_suffix, instance, on_abort=None, on_commit=None,
task_uuid=None, user=None, concurrency_check=True,
readable_name=None):
readable_name=None, resultant_state=None):
"""Create a transactional context for an instance activity.
"""
if not readable_name:
warn("Set readable_name", stacklevel=3)
act = InstanceActivity.create(code_suffix, instance, task_uuid, user,
concurrency_check,
readable_name=readable_name)
readable_name=readable_name,
resultant_state=resultant_state)
return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
......
......@@ -32,6 +32,7 @@ from django.core.exceptions import PermissionDenied
from django.db.models import (BooleanField, CharField, DateTimeField,
IntegerField, ForeignKey, Manager,
ManyToManyField, permalink, SET_NULL, TextField)
from django.db import IntegrityError
from django.dispatch import Signal
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _, ugettext_noop
......@@ -926,8 +927,16 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
def allocate_vnc_port(self):
if self.vnc_port is None:
while True:
try:
self.vnc_port = find_unused_vnc_port()
self.save()
except IntegrityError:
# Another thread took this port get another one
logger.debug("Port %s is in use.", self.vnc_port)
pass
else:
break
def yield_vnc_port(self):
if self.vnc_port is not None:
......@@ -943,7 +952,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
'ERROR': 'fa-warning',
'PENDING': 'fa-rocket',
'DESTROYED': 'fa-trash-o',
'MIGRATING': 'fa-truck'}.get(self.status, 'fa-question')
'MIGRATING': 'fa-truck migrating-icon'
}.get(self.status, 'fa-question')
def get_activities(self, user=None):
acts = (self.activity_log.filter(parent=None).
......@@ -993,3 +1003,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
instance=self, succeeded=None, parent=None).latest("started")
except InstanceActivity.DoesNotExist:
return None
def is_in_status_change(self):
latest = self.get_latest_activity_in_progress()
return (latest and latest.resultant_state is not None
and self.status != latest.resultant_state)
......@@ -260,7 +260,7 @@ class Node(OperatedMixin, TimeStampedModel):
@method_cache(10)
def monitor_info(self):
metrics = ('cpu.usage', 'memory.usage')
prefix = 'circle.%s.' % self.name
prefix = 'circle.%s.' % self.host.hostname
params = [('target', '%s%s' % (prefix, metric))
for metric in metrics]
params.append(('from', '-5min'))
......
......@@ -55,6 +55,7 @@ class InstanceOperation(Operation):
concurrency_check = True
accept_states = None
deny_states = None
resultant_state = None
def __init__(self, instance):
super(InstanceOperation, self).__init__(subject=instance)
......@@ -99,12 +100,14 @@ class InstanceOperation(Operation):
"provided as parameter.")
return parent.create_sub(code_suffix=self.activity_code_suffix,
readable_name=name)
readable_name=name,
resultant_state=self.resultant_state)
else:
return InstanceActivity.create(
code_suffix=self.activity_code_suffix, instance=self.instance,
readable_name=name, user=user,
concurrency_check=self.concurrency_check)
concurrency_check=self.concurrency_check,
resultant_state=self.resultant_state)
def is_preferred(self):
"""If this is the recommended op in the current state of the instance.
......@@ -215,6 +218,7 @@ class DownloadDiskOperation(InstanceOperation):
has_percentage = True
required_perms = ('storage.download_disk', )
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
async_queue = "localhost.man.slow"
def _operation(self, user, url, task, activity, name=None):
activity.result = url
......@@ -250,14 +254,22 @@ class DeployOperation(InstanceOperation):
"and network configuration).")
required_perms = ()
deny_states = ('SUSPENDED', 'RUNNING')
resultant_state = 'RUNNING'
def is_preferred(self):
return self.instance.status in (self.instance.STATUS.STOPPED,
self.instance.STATUS.PENDING,
self.instance.STATUS.ERROR)
def on_abort(self, activity, error):
activity.resultant_state = 'STOPPED'
def on_commit(self, activity):
activity.resultant_state = 'RUNNING'
activity.result = create_readable(
ugettext_noop("virtual machine successfully "
"deployed to node: %(node)s"),
node=self.instance.node)
def _operation(self, activity, timeout=15):
# Allocate VNC port and host node
......@@ -272,9 +284,11 @@ class DeployOperation(InstanceOperation):
# Deploy VM on remote machine
if self.instance.state not in ['PAUSED']:
rn = create_readable(ugettext_noop("deploy virtual machine"),
ugettext_noop("deploy vm to %(node)s"),
node=self.instance.node)
with activity.sub_activity(
'deploying_vm', readable_name=ugettext_noop(
"deploy virtual machine")) as deploy_act:
'deploying_vm', readable_name=rn) as deploy_act:
deploy_act.result = self.instance.deploy_vm(timeout=timeout)
# Establish network connection (vmdriver)
......@@ -305,9 +319,7 @@ class DestroyOperation(InstanceOperation):
description = _("Permanently destroy virtual machine, its network "
"settings and disks.")
required_perms = ()
def on_commit(self, activity):
activity.resultant_state = 'DESTROYED'
resultant_state = 'DESTROYED'
def _operation(self, activity):
# Destroy networks
......@@ -355,7 +367,9 @@ class MigrateOperation(InstanceOperation):
description = _("Move virtual machine to an other worker node with a few "
"seconds of interruption (live migration).")
required_perms = ()
superuser_required = True
accept_states = ('RUNNING', )
async_queue = "localhost.man.slow"
def rollback(self, activity):
with activity.sub_activity(
......@@ -363,12 +377,6 @@ class MigrateOperation(InstanceOperation):
"redeploy network (rollback)")):
self.instance.deploy_net()
def check_auth(self, user):
if not user.is_superuser:
raise PermissionDenied()
super(MigrateOperation, self).check_auth(user=user)
def _operation(self, activity, to_node=None, timeout=120):
if not to_node:
with activity.sub_activity('scheduling',
......@@ -506,6 +514,7 @@ class SaveAsTemplateOperation(InstanceOperation):
abortable = True
required_perms = ('vm.create_template', )
accept_states = ('RUNNING', 'PENDING', 'STOPPED')
async_queue = "localhost.man.slow"
def is_preferred(self):
return (self.instance.is_base and
......@@ -603,9 +612,7 @@ class ShutdownOperation(InstanceOperation):
abortable = True
required_perms = ()
accept_states = ('RUNNING', )
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
resultant_state = 'STOPPED'
def _operation(self, task=None):
self.instance.shutdown_vm(task=task)
......@@ -629,9 +636,7 @@ class ShutOffOperation(InstanceOperation):
"of a physical machine.")
required_perms = ()
accept_states = ('RUNNING', )
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
resultant_state = 'STOPPED'
def _operation(self, activity):
# Shutdown networks
......@@ -664,6 +669,8 @@ class SleepOperation(InstanceOperation):
"storage resources, and keep network resources allocated.")
required_perms = ()
accept_states = ('RUNNING', )
resultant_state = 'SUSPENDED'
async_queue = "localhost.man.slow"
def is_preferred(self):
return (not self.instance.is_base and
......@@ -675,9 +682,6 @@ class SleepOperation(InstanceOperation):
else:
activity.resultant_state = 'ERROR'
def on_commit(self, activity):
activity.resultant_state = 'SUSPENDED'
def _operation(self, activity, timeout=240):
# Destroy networks
with activity.sub_activity('shutdown_net', readable_name=ugettext_noop(
......@@ -706,6 +710,7 @@ class WakeUpOperation(InstanceOperation):
"virtual machine from this state.")
required_perms = ()
accept_states = ('SUSPENDED', )
resultant_state = 'RUNNING'
def is_preferred(self):
return self.instance.status == self.instance.STATUS.SUSPENDED
......@@ -713,9 +718,6 @@ class WakeUpOperation(InstanceOperation):
def on_abort(self, activity, error):
activity.resultant_state = 'ERROR'
def on_commit(self, activity):
activity.resultant_state = 'RUNNING'
def _operation(self, activity, timeout=60):
# Schedule vm
self.instance.allocate_vnc_port()
......@@ -840,6 +842,8 @@ class FlushOperation(NodeOperation):
name = _("flush")
description = _("Disable node and move all instances to other ones.")
required_perms = ()
superuser_required = True
async_queue = "localhost.man.slow"
def on_abort(self, activity, error):
from manager.scheduler import TraitsUnsatisfiableException
......@@ -847,13 +851,6 @@ class FlushOperation(NodeOperation):
if self.node_enabled:
self.node.enable(activity.user, activity)
def check_auth(self, user):
if not user.is_superuser:
raise humanize_exception(ugettext_noop(
"Superuser privileges are required."), PermissionDenied())
super(FlushOperation, self).check_auth(user=user)
def _operation(self, activity, user):
self.node_enabled = self.node.enabled
self.node.disable(user, activity)
......@@ -896,6 +893,7 @@ class RecoverOperation(InstanceOperation):
acl_level = "owner"
required_perms = ('vm.recover', )
accept_states = ('DESTROYED', )
resultant_state = 'PENDING'
def check_precond(self):
try:
......@@ -903,9 +901,6 @@ class RecoverOperation(InstanceOperation):
except Instance.InstanceDestroyedError:
pass
def on_commit(self, activity):
activity.resultant_state = 'PENDING'
def _operation(self):
for disk in self.instance.disks.all():
disk.destroyed = None
......
description "CIRCLE manager"
start on runlevel [2345]
stop on runlevel [!2345]
pre-start script
start moncelery
start mancelery
start slowcelery
end script
post-stop script
stop moncelery
stop mancelery
stop slowcelery
end script
description "CIRCLE mancelery"
start on runlevel [2345]
stop on runlevel [!2345]
description "CIRCLE mancelery for common jobs"
respawn
respawn limit 30 30
setgid cloud
setuid cloud
......@@ -12,6 +10,6 @@ script
cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 1
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 10
end script
description "CIRCLE moncelery for monitoring jobs"
respawn
respawn limit 30 30
setgid cloud
setuid cloud
script
cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.moncelery worker --autoreload --loglevel=info --hostname=moncelery -B -c 3
end script
description "CIRCLE mancelery for slow jobs"
respawn
respawn limit 30 30
setgid cloud
setuid cloud
script
cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.slowcelery worker --autoreload --loglevel=info --hostname=slowcelery -B -c 5
end script
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 sign in to comment