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 %} {% 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 i18n %}
{% load hro %}
<div id="activity-timeline" class="timeline"> <div id="activity-timeline" class="timeline">
{% for a in activities %} {% for a in activities %}
<div class="activity" data-activity-id="{{ a.pk }}"> <div class="activity" data-activity-id="{{ a.pk }}">
...@@ -16,10 +17,7 @@ ...@@ -16,10 +17,7 @@
<div data-activity-id="{{ s.pk }}" <div data-activity-id="{{ s.pk }}"
class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}"
> >
{% if user.is_superuser %} {{ s.readable_name|get_text:user }}
{{ s.readable_name.get_admin_text }}
{% else %}
{{ s.readable_name.get_user_text }}{% endif %}
&ndash; &ndash;
{% if s.finished %} {% if s.finished %}
{{ s.finished|time:"H:i:s" }} {{ s.finished|time:"H:i:s" }}
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
<i class="fa fa-plus"></i> {% trans "new lease" %} <i class="fa fa-plus"></i> {% trans "new lease" %}
</a> </a>
{% endif %} {% 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>
<div class="panel-body"> <div class="panel-body">
<div class="" style="max-width: 600px;"> <div class="" style="max-width: 600px;">
......
{% load i18n %} {% load i18n %}
{% load hro %}
<div id="activity-timeline" class="timeline"> <div id="activity-timeline" class="timeline">
{% for a in activities %} {% for a in activities %}
...@@ -7,10 +7,10 @@ ...@@ -7,10 +7,10 @@
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}"> <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-{{a.icon}}{% endif %}"></i> <i class="fa {% if not a.finished %}fa-refresh fa-spin {% else %}fa-{{a.icon}}{% endif %}"></i>
</span> </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 }}"> <a href="{{ a.get_absolute_url }}">
{% if a.times > 1 %}({{ a.times }}x){% endif %} {% 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 %} {% if a.has_percent %}
- {{ a.percentage }}% - {{ a.percentage }}%
...@@ -33,9 +33,9 @@ ...@@ -33,9 +33,9 @@
<div class="sub-timeline"> <div class="sub-timeline">
{% for s in a.children.all %} {% 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 %}"> <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 }}"> <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 %} {% if s.finished %}
{{ s.finished|time:"H:i:s" }} {{ s.finished|time:"H:i:s" }}
{% else %} {% else %}
......
...@@ -15,7 +15,19 @@ ...@@ -15,7 +15,19 @@
</div> </div>
<h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3> <h3 class="no-margin"><i class="fa fa-desktop"></i> {% trans "Virtual machines" %}</h3>
</div> </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"> <form action="" method="GET">
<div class="input-group"> <div class="input-group">
{{ search_form.s }} {{ search_form.s }}
...@@ -25,31 +37,14 @@ ...@@ -25,31 +37,14 @@
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
</button> </button>
</div> </div>
</div> </div><!-- .input-group -->
</form> </form>
</div> </div><!-- .col-md-4 #vm-list-search -->
<div class="panel-body vm-list-group-control"> </div><!-- .row -->
<p> </div><!-- .panel-body -->
<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 class="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> <thead><tr>
<th data-sort="int" class="orderable pk sortable vm-list-table-thin" style="min-width: 50px;"> <th data-sort="int" class="orderable pk sortable vm-list-table-thin" style="min-width: 50px;">
{% trans "ID" as t %} {% trans "ID" as t %}
...@@ -76,12 +71,21 @@ ...@@ -76,12 +71,21 @@
<tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}"> <tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}">
<td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td> <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="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> <td>
{% include "dashboard/_display-name.html" with user=i.owner show_org=True %} {% include "dashboard/_display-name.html" with user=i.owner show_org=True %}
</td> </td>
{% if user.is_superuser %} {% 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 %} {% endif %}
</tr> </tr>
{% empty %} {% empty %}
...@@ -115,6 +119,5 @@ ...@@ -115,6 +119,5 @@
{% block extra_js %} {% block extra_js %}
<script src="{{ STATIC_URL}}dashboard/vm-list.js"></script> <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> <script src="{{ STATIC_URL}}dashboard/js/stupidtable.min.js"></script>
{% endblock %} {% 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 ...@@ -29,7 +29,7 @@ from django.utils import baseconv
from ..models import Profile from ..models import Profile
from ..views import InstanceActivityDetail, InstanceActivity 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 ..views import AclUpdateView
from .. import views from .. import views
...@@ -259,6 +259,114 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -259,6 +259,114 @@ class VmOperationViewTestCase(unittest.TestCase):
self.assertEquals(rend.status_code, 200) 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): class RenewViewTest(unittest.TestCase):
def test_renew_template(self): def test_renew_template(self):
......
...@@ -107,20 +107,6 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -107,20 +107,6 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.get('/dashboard/vm/1/') response = c.get('/dashboard/vm/1/')
self.assertEqual(response.status_code, 200) 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): def test_unpermitted_password_change(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
......
...@@ -29,7 +29,7 @@ from .views import ( ...@@ -29,7 +29,7 @@ from .views import (
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate, NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView, TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView, TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmDetailVncTokenView, VmGraphView, VmList,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveUserView, GroupRemoveUserView,
GroupRemoveFutureUserView, GroupRemoveFutureUserView,
...@@ -49,7 +49,6 @@ from .views import ( ...@@ -49,7 +49,6 @@ from .views import (
autocomplete_light.autodiscover() autocomplete_light.autodiscover()
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^$', IndexView.as_view(), name="dashboard.index"), url(r'^$', IndexView.as_view(), name="dashboard.index"),
url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(), url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(),
...@@ -74,7 +73,7 @@ urlpatterns = patterns( ...@@ -74,7 +73,7 @@ urlpatterns = patterns(
url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(), url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(),
name="dashboard.views.template-delete"), 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(), url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'), name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
...@@ -88,8 +87,6 @@ urlpatterns = patterns( ...@@ -88,8 +87,6 @@ urlpatterns = patterns(
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'), url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(), url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'), 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/(?P<pk>\d+)/activity/$', vm_activity),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(), url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'), name='dashboard.views.vm-activity'),
......
...@@ -78,7 +78,10 @@ from .tables import ( ...@@ -78,7 +78,10 @@ from .tables import (
NodeListTable, TemplateListTable, LeaseListTable, NodeListTable, TemplateListTable, LeaseListTable,
GroupListTable, UserKeyListTable GroupListTable, UserKeyListTable
) )
from common.models import HumanReadableObject, HumanReadableException from common.models import (
HumanReadableObject, HumanReadableException, fetch_human_exception,
create_readable,
)
from vm.models import ( from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface, Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
InterfaceTemplate, Lease, Node, NodeActivity, Trait, InterfaceTemplate, Lease, Node, NodeActivity, Trait,
...@@ -608,6 +611,10 @@ class OperationView(RedirectToLoginMixin, DetailView): ...@@ -608,6 +611,10 @@ class OperationView(RedirectToLoginMixin, DetailView):
setattr(self, '_opobj', getattr(self.get_object(), self.op)) setattr(self, '_opobj', getattr(self.get_object(), self.op))
return self._opobj return self._opobj
@classmethod
def get_operation_class(cls):
return cls.model.get_operation_class(cls.op)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super(OperationView, self).get_context_data(**kwargs) ctx = super(OperationView, self).get_context_data(**kwargs)
ctx['op'] = self.get_op() ctx['op'] = self.get_op()
...@@ -623,6 +630,10 @@ class OperationView(RedirectToLoginMixin, DetailView): ...@@ -623,6 +630,10 @@ class OperationView(RedirectToLoginMixin, DetailView):
logger.debug("OperationView.check_auth(%s)", unicode(self)) logger.debug("OperationView.check_auth(%s)", unicode(self))
self.get_op().check_auth(self.request.user) 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): def get(self, request, *args, **kwargs):
self.check_auth() self.check_auth()
return super(OperationView, self).get(request, *args, **kwargs) return super(OperationView, self).get(request, *args, **kwargs)
...@@ -1057,6 +1068,112 @@ def get_operations(instance, user): ...@@ -1057,6 +1068,112 @@ def get_operations(instance, user):
return ops 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): class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, DetailView):
template_name = "dashboard/node-detail.html" template_name = "dashboard/node-detail.html"
model = Node model = Node
...@@ -1638,11 +1755,41 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView): ...@@ -1638,11 +1755,41 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
context = super(VmList, self).get_context_data(*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 context['search_form'] = self.search_form
return context return context
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
if self.request.is_ajax(): 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( favs = Instance.objects.filter(
favourite__user=self.request.user).values_list('pk', flat=True) favourite__user=self.request.user).values_list('pk', flat=True)
instances = Instance.get_objects_with_level( instances = Instance.get_objects_with_level(
...@@ -1654,15 +1801,12 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView): ...@@ -1654,15 +1801,12 @@ class VmList(LoginRequiredMixin, FilterMixin, ListView):
'icon': i.get_status_icon(), 'icon': i.get_status_icon(),
'host': "" if not i.primary_host else i.primary_host.hostname, 'host': "" if not i.primary_host else i.primary_host.hostname,
'status': i.get_status_display(), '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( return HttpResponse(
json.dumps(list(instances)), # instances is ValuesQuerySet json.dumps(list(instances)), # instances is ValuesQuerySet
content_type="application/json", 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): def get_queryset(self):
logger.debug('VmList.get_queryset() called. User: %s', logger.debug('VmList.get_queryset() called. User: %s',
...@@ -1979,7 +2123,7 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1979,7 +2123,7 @@ class VmCreate(LoginRequiredMixin, TemplateView):
"Successfully created %(count)d VM.", # this should not happen "Successfully created %(count)d VM.", # this should not happen
"Successfully created %(count)d VMs.", len(instances)) % { "Successfully created %(count)d VMs.", len(instances)) % {
'count': len(instances)}) 'count': len(instances)})
path = reverse("dashboard.index") path = "%s?stype=owned" % reverse("dashboard.views.vm-list")
else: else:
messages.success(request, _("VM successfully created.")) messages.success(request, _("VM successfully created."))
path = instances[0].get_absolute_url() path = instances[0].get_absolute_url()
...@@ -2381,48 +2525,6 @@ class PortDelete(LoginRequiredMixin, DeleteView): ...@@ -2381,48 +2525,6 @@ class PortDelete(LoginRequiredMixin, DeleteView):
kwargs={'pk': self.kwargs.get("pk")}) 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, class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
SuccessMessageMixin, CreateView): SuccessMessageMixin, CreateView):
model = Lease model = Lease
...@@ -2754,7 +2856,7 @@ class NodeGraphView(SuperuserRequiredMixin, GraphViewBase): ...@@ -2754,7 +2856,7 @@ class NodeGraphView(SuperuserRequiredMixin, GraphViewBase):
model = Node model = Node
def get_prefix(self, instance): def get_prefix(self, instance):
return 'circle.%s' % instance.name return 'circle.%s' % instance.host.hostname
def get_title(self, instance, metric): def get_title(self, instance, metric):
return '%s - %s' % (instance.name, metric) return '%s - %s' % (instance.name, metric)
......
...@@ -17,9 +17,17 @@ ...@@ -17,9 +17,17 @@
from django.conf.urls import patterns, url from django.conf.urls import patterns, url
from ..views import vm_ops from ..views import vm_ops, vm_mass_ops
urlpatterns = patterns('', urlpatterns = patterns(
*(url(r'^%s/$' % op, v.as_view(), name=v.get_urlname()) '',
for op, v in vm_ops.iteritems())) *(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())
)
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -6,7 +6,7 @@ msgid "" ...@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2014-08-01 21:03+0200\n"
"Last-Translator: Mate Ory <ory.mate@ik.bme.hu>\n" "Last-Translator: Mate Ory <ory.mate@ik.bme.hu>\n"
"Language-Team: Hungarian <cloud@ik.bme.hu>\n" "Language-Team: Hungarian <cloud@ik.bme.hu>\n"
...@@ -23,11 +23,11 @@ msgid "Select an option to proceed!" ...@@ -23,11 +23,11 @@ msgid "Select an option to proceed!"
msgstr "Válasszon a folytatáshoz." msgstr "Válasszon a folytatáshoz."
#: dashboard/static/dashboard/dashboard.js:258 #: dashboard/static/dashboard/dashboard.js:258
#: dashboard/static/dashboard/dashboard.js:305 #: dashboard/static/dashboard/dashboard.js:306
#: dashboard/static/dashboard/dashboard.js:315 #: dashboard/static/dashboard/dashboard.js:316
#: static_collected/dashboard/dashboard.js:257 #: static_collected/dashboard/dashboard.js:258
#: static_collected/dashboard/dashboard.js:304 #: static_collected/dashboard/dashboard.js:306
#: static_collected/dashboard/dashboard.js:314 #: static_collected/dashboard/dashboard.js:316
msgid "No result" msgid "No result"
msgstr "Nincs eredmény" msgstr "Nincs eredmény"
...@@ -41,12 +41,15 @@ msgstr "Nincs jogosultsága a profil módosításához." ...@@ -41,12 +41,15 @@ msgstr "Nincs jogosultsága a profil módosításához."
msgid "Unknown error." msgid "Unknown error."
msgstr "Ismeretlen hiba." msgstr "Ismeretlen hiba."
#: dashboard/static/dashboard/vm-create.js:108 #: dashboard/static/dashboard/vm-create.js:111
#: dashboard/static/dashboard/vm-create.js:171 #: 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." msgid "No more networks."
msgstr "Nincs több hálózat." 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" msgid "Not added to any network"
msgstr "Nincs hálózathoz adva" msgstr "Nincs hálózathoz adva"
...@@ -428,5 +431,3 @@ msgstr "Tegnap" ...@@ -428,5 +431,3 @@ msgstr "Tegnap"
#: static_collected/admin/js/admin/DateTimeShortcuts.js:203 #: static_collected/admin/js/admin/DateTimeShortcuts.js:203
msgid "Tomorrow" msgid "Tomorrow"
msgstr "Holnap" msgstr "Holnap"
# end
...@@ -90,7 +90,8 @@ class InstanceActivity(ActivityModel): ...@@ -90,7 +90,8 @@ class InstanceActivity(ActivityModel):
@classmethod @classmethod
def create(cls, code_suffix, instance, task_uuid=None, user=None, 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) readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities # Check for concurrent activities
...@@ -100,14 +101,14 @@ class InstanceActivity(ActivityModel): ...@@ -100,14 +101,14 @@ class InstanceActivity(ActivityModel):
activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix) activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
act = cls(activity_code=activity_code, instance=instance, parent=None, 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(), readable_name_data=readable_name.to_dict(),
task_uuid=task_uuid, user=user) task_uuid=task_uuid, user=user)
act.save() act.save()
return act return act
def create_sub(self, code_suffix, task_uuid=None, concurrency_check=True, 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) readable_name = _normalize_readable_name(readable_name, code_suffix)
# Check for concurrent activities # Check for concurrent activities
...@@ -117,7 +118,8 @@ class InstanceActivity(ActivityModel): ...@@ -117,7 +118,8 @@ class InstanceActivity(ActivityModel):
act = InstanceActivity( act = InstanceActivity(
activity_code=join_activity_code(self.activity_code, code_suffix), 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(), readable_name_data=readable_name.to_dict(), started=timezone.now(),
task_uuid=task_uuid, user=self.user) task_uuid=task_uuid, user=self.user)
act.save() act.save()
...@@ -198,14 +200,15 @@ class InstanceActivity(ActivityModel): ...@@ -198,14 +200,15 @@ class InstanceActivity(ActivityModel):
@contextmanager @contextmanager
def instance_activity(code_suffix, instance, on_abort=None, on_commit=None, def instance_activity(code_suffix, instance, on_abort=None, on_commit=None,
task_uuid=None, user=None, concurrency_check=True, 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. """Create a transactional context for an instance activity.
""" """
if not readable_name: if not readable_name:
warn("Set readable_name", stacklevel=3) warn("Set readable_name", stacklevel=3)
act = InstanceActivity.create(code_suffix, instance, task_uuid, user, act = InstanceActivity.create(code_suffix, instance, task_uuid, user,
concurrency_check, 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) return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
......
...@@ -952,7 +952,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -952,7 +952,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
'ERROR': 'fa-warning', 'ERROR': 'fa-warning',
'PENDING': 'fa-rocket', 'PENDING': 'fa-rocket',
'DESTROYED': 'fa-trash-o', '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): def get_activities(self, user=None):
acts = (self.activity_log.filter(parent=None). acts = (self.activity_log.filter(parent=None).
...@@ -1002,3 +1003,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -1002,3 +1003,8 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
instance=self, succeeded=None, parent=None).latest("started") instance=self, succeeded=None, parent=None).latest("started")
except InstanceActivity.DoesNotExist: except InstanceActivity.DoesNotExist:
return None 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): ...@@ -260,7 +260,7 @@ class Node(OperatedMixin, TimeStampedModel):
@method_cache(10) @method_cache(10)
def monitor_info(self): def monitor_info(self):
metrics = ('cpu.usage', 'memory.usage') metrics = ('cpu.usage', 'memory.usage')
prefix = 'circle.%s.' % self.name prefix = 'circle.%s.' % self.host.hostname
params = [('target', '%s%s' % (prefix, metric)) params = [('target', '%s%s' % (prefix, metric))
for metric in metrics] for metric in metrics]
params.append(('from', '-5min')) params.append(('from', '-5min'))
......
...@@ -55,6 +55,7 @@ class InstanceOperation(Operation): ...@@ -55,6 +55,7 @@ class InstanceOperation(Operation):
concurrency_check = True concurrency_check = True
accept_states = None accept_states = None
deny_states = None deny_states = None
resultant_state = None
def __init__(self, instance): def __init__(self, instance):
super(InstanceOperation, self).__init__(subject=instance) super(InstanceOperation, self).__init__(subject=instance)
...@@ -99,12 +100,14 @@ class InstanceOperation(Operation): ...@@ -99,12 +100,14 @@ class InstanceOperation(Operation):
"provided as parameter.") "provided as parameter.")
return parent.create_sub(code_suffix=self.activity_code_suffix, return parent.create_sub(code_suffix=self.activity_code_suffix,
readable_name=name) readable_name=name,
resultant_state=self.resultant_state)
else: else:
return InstanceActivity.create( return InstanceActivity.create(
code_suffix=self.activity_code_suffix, instance=self.instance, code_suffix=self.activity_code_suffix, instance=self.instance,
readable_name=name, user=user, readable_name=name, user=user,
concurrency_check=self.concurrency_check) concurrency_check=self.concurrency_check,
resultant_state=self.resultant_state)
def is_preferred(self): def is_preferred(self):
"""If this is the recommended op in the current state of the instance. """If this is the recommended op in the current state of the instance.
...@@ -250,6 +253,7 @@ class DeployOperation(InstanceOperation): ...@@ -250,6 +253,7 @@ class DeployOperation(InstanceOperation):
"and network configuration).") "and network configuration).")
required_perms = () required_perms = ()
deny_states = ('SUSPENDED', 'RUNNING') deny_states = ('SUSPENDED', 'RUNNING')
resultant_state = 'RUNNING'
def is_preferred(self): def is_preferred(self):
return self.instance.status in (self.instance.STATUS.STOPPED, return self.instance.status in (self.instance.STATUS.STOPPED,
...@@ -314,9 +318,7 @@ class DestroyOperation(InstanceOperation): ...@@ -314,9 +318,7 @@ class DestroyOperation(InstanceOperation):
description = _("Permanently destroy virtual machine, its network " description = _("Permanently destroy virtual machine, its network "
"settings and disks.") "settings and disks.")
required_perms = () required_perms = ()
resultant_state = 'DESTROYED'
def on_commit(self, activity):
activity.resultant_state = 'DESTROYED'
def _operation(self, activity): def _operation(self, activity):
# Destroy networks # Destroy networks
...@@ -364,6 +366,7 @@ class MigrateOperation(InstanceOperation): ...@@ -364,6 +366,7 @@ class MigrateOperation(InstanceOperation):
description = _("Move virtual machine to an other worker node with a few " description = _("Move virtual machine to an other worker node with a few "
"seconds of interruption (live migration).") "seconds of interruption (live migration).")
required_perms = () required_perms = ()
superuser_required = True
accept_states = ('RUNNING', ) accept_states = ('RUNNING', )
def rollback(self, activity): def rollback(self, activity):
...@@ -372,12 +375,6 @@ class MigrateOperation(InstanceOperation): ...@@ -372,12 +375,6 @@ class MigrateOperation(InstanceOperation):
"redeploy network (rollback)")): "redeploy network (rollback)")):
self.instance.deploy_net() 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): def _operation(self, activity, to_node=None, timeout=120):
if not to_node: if not to_node:
with activity.sub_activity('scheduling', with activity.sub_activity('scheduling',
...@@ -612,9 +609,7 @@ class ShutdownOperation(InstanceOperation): ...@@ -612,9 +609,7 @@ class ShutdownOperation(InstanceOperation):
abortable = True abortable = True
required_perms = () required_perms = ()
accept_states = ('RUNNING', ) accept_states = ('RUNNING', )
resultant_state = 'STOPPED'
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
def _operation(self, task=None): def _operation(self, task=None):
self.instance.shutdown_vm(task=task) self.instance.shutdown_vm(task=task)
...@@ -638,9 +633,7 @@ class ShutOffOperation(InstanceOperation): ...@@ -638,9 +633,7 @@ class ShutOffOperation(InstanceOperation):
"of a physical machine.") "of a physical machine.")
required_perms = () required_perms = ()
accept_states = ('RUNNING', ) accept_states = ('RUNNING', )
resultant_state = 'STOPPED'
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
def _operation(self, activity): def _operation(self, activity):
# Shutdown networks # Shutdown networks
...@@ -673,6 +666,7 @@ class SleepOperation(InstanceOperation): ...@@ -673,6 +666,7 @@ class SleepOperation(InstanceOperation):
"storage resources, and keep network resources allocated.") "storage resources, and keep network resources allocated.")
required_perms = () required_perms = ()
accept_states = ('RUNNING', ) accept_states = ('RUNNING', )
resultant_state = 'SUSPENDED'
def is_preferred(self): def is_preferred(self):
return (not self.instance.is_base and return (not self.instance.is_base and
...@@ -684,9 +678,6 @@ class SleepOperation(InstanceOperation): ...@@ -684,9 +678,6 @@ class SleepOperation(InstanceOperation):
else: else:
activity.resultant_state = 'ERROR' activity.resultant_state = 'ERROR'
def on_commit(self, activity):
activity.resultant_state = 'SUSPENDED'
def _operation(self, activity, timeout=240): def _operation(self, activity, timeout=240):
# Destroy networks # Destroy networks
with activity.sub_activity('shutdown_net', readable_name=ugettext_noop( with activity.sub_activity('shutdown_net', readable_name=ugettext_noop(
...@@ -715,6 +706,7 @@ class WakeUpOperation(InstanceOperation): ...@@ -715,6 +706,7 @@ class WakeUpOperation(InstanceOperation):
"virtual machine from this state.") "virtual machine from this state.")
required_perms = () required_perms = ()
accept_states = ('SUSPENDED', ) accept_states = ('SUSPENDED', )
resultant_state = 'RUNNING'
def is_preferred(self): def is_preferred(self):
return self.instance.status == self.instance.STATUS.SUSPENDED return self.instance.status == self.instance.STATUS.SUSPENDED
...@@ -722,9 +714,6 @@ class WakeUpOperation(InstanceOperation): ...@@ -722,9 +714,6 @@ class WakeUpOperation(InstanceOperation):
def on_abort(self, activity, error): def on_abort(self, activity, error):
activity.resultant_state = 'ERROR' activity.resultant_state = 'ERROR'
def on_commit(self, activity):
activity.resultant_state = 'RUNNING'
def _operation(self, activity, timeout=60): def _operation(self, activity, timeout=60):
# Schedule vm # Schedule vm
self.instance.allocate_vnc_port() self.instance.allocate_vnc_port()
...@@ -849,6 +838,7 @@ class FlushOperation(NodeOperation): ...@@ -849,6 +838,7 @@ class FlushOperation(NodeOperation):
name = _("flush") name = _("flush")
description = _("Disable node and move all instances to other ones.") description = _("Disable node and move all instances to other ones.")
required_perms = () required_perms = ()
superuser_required = True
def on_abort(self, activity, error): def on_abort(self, activity, error):
from manager.scheduler import TraitsUnsatisfiableException from manager.scheduler import TraitsUnsatisfiableException
...@@ -856,13 +846,6 @@ class FlushOperation(NodeOperation): ...@@ -856,13 +846,6 @@ class FlushOperation(NodeOperation):
if self.node_enabled: if self.node_enabled:
self.node.enable(activity.user, activity) 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): def _operation(self, activity, user):
self.node_enabled = self.node.enabled self.node_enabled = self.node.enabled
self.node.disable(user, activity) self.node.disable(user, activity)
...@@ -905,6 +888,7 @@ class RecoverOperation(InstanceOperation): ...@@ -905,6 +888,7 @@ class RecoverOperation(InstanceOperation):
acl_level = "owner" acl_level = "owner"
required_perms = ('vm.recover', ) required_perms = ('vm.recover', )
accept_states = ('DESTROYED', ) accept_states = ('DESTROYED', )
resultant_state = 'PENDING'
def check_precond(self): def check_precond(self):
try: try:
...@@ -912,9 +896,6 @@ class RecoverOperation(InstanceOperation): ...@@ -912,9 +896,6 @@ class RecoverOperation(InstanceOperation):
except Instance.InstanceDestroyedError: except Instance.InstanceDestroyedError:
pass pass
def on_commit(self, activity):
activity.resultant_state = 'PENDING'
def _operation(self): def _operation(self):
for disk in self.instance.disks.all(): for disk in self.instance.disks.all():
disk.destroyed = None disk.destroyed = None
......
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