Commit fc553851 by Bach Dániel

Merge remote-tracking branch 'origin/master' into feature-port-operations

Conflicts:
	circle/dashboard/urls.py
parents c4872897 833d5490
...@@ -64,6 +64,13 @@ CACHES = { ...@@ -64,6 +64,13 @@ CACHES = {
########## END CACHE CONFIGURATION ########## END CACHE CONFIGURATION
########## ROSETTA CONFIGURATION
INSTALLED_APPS += (
'rosetta',
)
########## END ROSETTA CONFIGURATION
########## TOOLBAR CONFIGURATION ########## TOOLBAR CONFIGURATION
# https://github.com/django-debug-toolbar/django-debug-toolbar#installation # https://github.com/django-debug-toolbar/django-debug-toolbar#installation
if get_env_variable('DJANGO_TOOLBAR', 'FALSE') == 'TRUE': if get_env_variable('DJANGO_TOOLBAR', 'FALSE') == 'TRUE':
......
...@@ -18,9 +18,11 @@ ...@@ -18,9 +18,11 @@
from django.conf.urls import patterns, include, url from django.conf.urls import patterns, include, url
from django.views.generic import TemplateView from django.views.generic import TemplateView
from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.shortcuts import redirect
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from circle.settings.base import get_env_variable from circle.settings.base import get_env_variable
from dashboard.views import circle_login, HelpView from dashboard.views import circle_login, HelpView
...@@ -71,6 +73,13 @@ urlpatterns = patterns( ...@@ -71,6 +73,13 @@ urlpatterns = patterns(
) )
if 'rosetta' in settings.INSTALLED_APPS:
urlpatterns += patterns(
'',
url(r'^rosetta/', include('rosetta.urls')),
)
if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE':
urlpatterns += patterns( urlpatterns += patterns(
'', '',
......
...@@ -216,7 +216,7 @@ html { ...@@ -216,7 +216,7 @@ html {
} }
#vm-list-rename-name, #node-list-rename-name, #group-list-rename-name { #vm-list-rename-name, #node-list-rename-name, #group-list-rename-name {
max-width: 100px; max-width: 150px;
} }
.label-tag { .label-tag {
...@@ -1042,3 +1042,12 @@ textarea[name="new_members"] { ...@@ -1042,3 +1042,12 @@ textarea[name="new_members"] {
#vm-migrate-node-list li { #vm-migrate-node-list li {
cursor: pointer; cursor: pointer;
} }
.group-list-table .actions,
.group-list-table .admin,
.group-list-table .number_of_users,
.group-list-table .pk {
width: 1px;
white-space: nowrap;
text-align: center;
}
...@@ -50,6 +50,21 @@ $(function () { ...@@ -50,6 +50,21 @@ $(function () {
return false; return false;
}); });
$('.tx-tpl-ownership').click(function(e) {
$.ajax({
type: 'GET',
url: $('.tx-tpl-ownership').attr('href'),
success: function(data) {
$('body').append(data);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
}
});
return false;
});
$('.template-choose').click(function(e) { $('.template-choose').click(function(e) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
...@@ -428,7 +443,7 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { ...@@ -428,7 +443,7 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) {
return '<a href="/dashboard/vm/' + pk + '/" class="list-group-item' + return '<a href="/dashboard/vm/' + pk + '/" class="list-group-item' +
(is_last ? ' list-group-item-last' : '') + '">' + (is_last ? ' list-group-item-last' : '') + '">' +
'<span class="index-vm-list-name">' + '<span class="index-vm-list-name">' +
'<i class="fa ' + icon + '" title="' + _status + '"></i> ' + name + '<i class="fa ' + icon + '" title="' + _status + '"></i> ' + safe_tags_replace(name) +
'</span>' + '</span>' +
'<small class="text-muted"> ' + host + '</small>' + '<small class="text-muted"> ' + host + '</small>' +
'<div class="pull-right dashboard-vm-favourite" data-vm="' + pk + '">' + '<div class="pull-right dashboard-vm-favourite" data-vm="' + pk + '">' +
...@@ -441,14 +456,14 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { ...@@ -441,14 +456,14 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) {
function generateGroupHTML(url, name, is_last) { function generateGroupHTML(url, name, is_last) {
return '<a href="' + url + '" class="list-group-item real-link' + (is_last ? " list-group-item-last" : "") +'">'+ return '<a href="' + url + '" class="list-group-item real-link' + (is_last ? " list-group-item-last" : "") +'">'+
'<i class="fa fa-users"></i> '+ name + '<i class="fa fa-users"></i> '+ safe_tags_replace(name) +
'</a>'; '</a>';
} }
function generateNodeHTML(name, icon, _status, url, is_last) { function generateNodeHTML(name, icon, _status, url, is_last) {
return '<a href="' + url + '" class="list-group-item real-link' + (is_last ? ' list-group-item-last' : '') + '">' + return '<a href="' + url + '" class="list-group-item real-link' + (is_last ? ' list-group-item-last' : '') + '">' +
'<span class="index-node-list-name">' + '<span class="index-node-list-name">' +
'<i class="fa ' + icon + '" title="' + _status + '"></i> ' + name + '<i class="fa ' + icon + '" title="' + _status + '"></i> ' + safe_tags_replace(name) +
'</span>' + '</span>' +
'<div style="clear: both;"></div>' + '<div style="clear: both;"></div>' +
'</a>'; '</a>';
...@@ -456,7 +471,7 @@ function generateNodeHTML(name, icon, _status, url, is_last) { ...@@ -456,7 +471,7 @@ function generateNodeHTML(name, icon, _status, url, is_last) {
function generateNodeTagHTML(name, icon, _status, label , url) { function generateNodeTagHTML(name, icon, _status, label , url) {
return '<a href="' + url + '" class="label ' + label + '" >' + return '<a href="' + url + '" class="label ' + label + '" >' +
'<i class="fa ' + icon + '" title="' + _status + '"></i> ' + name + '<i class="fa ' + icon + '" title="' + _status + '"></i> ' + safe_tags_replace(name) +
'</a> '; '</a> ';
} }
...@@ -678,3 +693,18 @@ function getParameterByName(name) { ...@@ -678,3 +693,18 @@ function getParameterByName(name) {
results = regex.exec(location.search); results = regex.exec(location.search);
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
} }
var tagsToReplace = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
function replaceTag(tag) {
return tagsToReplace[tag] || tag;
}
function safe_tags_replace(str) {
return str.replace(/[&<>]/g, replaceTag);
}
$(function() {
$(".disk-list-disk-percentage").each(function() {
var disk = $(this).data("disk-pk");
var element = $(this);
refreshDisk(disk, element);
});
});
function refreshDisk(disk, element) {
$.get("/dashboard/disk/" + disk + "/status/", function(result) {
if(result.percentage == null || result.failed == "True") {
location.reload();
} else {
var diff = result.percentage - parseInt(element.html());
var refresh = 5 - diff;
refresh = refresh < 1 ? 1 : (result.percentage == 0 ? 1 : refresh);
if(isNaN(refresh)) refresh = 2; // this should not happen
element.html(result.percentage);
setTimeout(function() {refreshDisk(disk, element)}, refresh * 1000);
}
});
}
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
data: {'new_name': name}, data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) { success: function(data, textStatus, xhr) {
$("#group-details-h1-name").html(data['new_name']).show(); $("#group-details-h1-name").text(data['new_name']).show();
$('#group-details-rename').hide(); $('#group-details-rename').hide();
// addMessage(data['message'], "success"); // addMessage(data['message'], "success");
}, },
......
...@@ -3,6 +3,7 @@ $(function() { ...@@ -3,6 +3,7 @@ $(function() {
$("#group-list-rename-button, .group-details-rename-button").click(function() { $("#group-list-rename-button, .group-details-rename-button").click(function() {
$("#group-list-column-name", $(this).closest("tr")).hide(); $("#group-list-column-name", $(this).closest("tr")).hide();
$("#group-list-rename", $(this).closest("tr")).css('display', 'inline'); $("#group-list-rename", $(this).closest("tr")).css('display', 'inline');
$("#group-list-rename").find("input").select();
}); });
/* rename ajax */ /* rename ajax */
......
...@@ -15,7 +15,7 @@ $(function() { ...@@ -15,7 +15,7 @@ $(function() {
data: {'new_name': name}, data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')}, headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) { success: function(data, textStatus, xhr) {
$("#node-details-h1-name").html(data['new_name']).show(); $("#node-details-h1-name").text(data['new_name']).show();
$('#node-details-rename').hide(); $('#node-details-rename').hide();
// addMessage(data['message'], "success"); // addMessage(data['message'], "success");
}, },
......
...@@ -12,40 +12,6 @@ $(function() { ...@@ -12,40 +12,6 @@ $(function() {
tr.removeClass('danger'); tr.removeClass('danger');
} }
/* rename */
$("#node-list-rename-button, .node-details-rename-button").click(function() {
$("#node-list-column-name", $(this).closest("tr")).hide();
$("#node-list-rename", $(this).closest("tr")).css('display', 'inline');
});
/* rename ajax */
$('.node-list-rename-submit').click(function() {
var row = $(this).closest("tr")
var name = $('#node-list-rename-name', row).val();
var url = '/dashboard/node/' + 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) {
$("#node-list-column-name", row).html(
$("<a/>", {
'class': "real-link",
href: "/dashboard/node/" + data['node_pk'] + "/",
text: data['new_name']
})
).show();
$('#node-list-rename', row).hide();
// addMessage(data['message'], "success");
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
function statuschangeSuccess(tr){ function statuschangeSuccess(tr){
var tspan=tr.children('.enabled').children(); var tspan=tr.children('.enabled').children();
......
...@@ -88,18 +88,21 @@ class GroupListTable(Table): ...@@ -88,18 +88,21 @@ class GroupListTable(Table):
number_of_users = TemplateColumn( number_of_users = TemplateColumn(
orderable=False, orderable=False,
verbose_name=_("Number of users"),
template_name='dashboard/group-list/column-users.html', template_name='dashboard/group-list/column-users.html',
attrs={'th': {'class': 'group-list-table-admin'}}, attrs={'th': {'class': 'group-list-table-admin'}},
) )
admin = TemplateColumn( admin = TemplateColumn(
orderable=False, orderable=False,
verbose_name=_("Admin"),
template_name='dashboard/group-list/column-admin.html', template_name='dashboard/group-list/column-admin.html',
attrs={'th': {'class': 'group-list-table-admin'}}, attrs={'th': {'class': 'group-list-table-admin'}},
) )
actions = TemplateColumn( actions = TemplateColumn(
orderable=False, orderable=False,
verbose_name=_("Actions"),
attrs={'th': {'class': 'group-list-table-thin'}}, attrs={'th': {'class': 'group-list-table-thin'}},
template_code=('{% include "dashboard/group-list/column-' template_code=('{% include "dashboard/group-list/column-'
'actions.html" with btn_size="btn-xs" %}'), 'actions.html" with btn_size="btn-xs" %}'),
......
...@@ -15,11 +15,11 @@ ...@@ -15,11 +15,11 @@
<form action="{% url "dashboard.views.status-node" pk=object.pk %}" method="POST"> <form action="{% url "dashboard.views.status-node" pk=object.pk %}" method="POST">
{% csrf_token %} {% csrf_token %}
<button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button> <button type="button" class="btn btn-default" data-dismiss="modal">{% trans "Cancel" %}</button>
<input type="hidden" name="change_status" value=""/> <input type="hidden" name="change_status" value=""/>
<button class="btn btn-warning">{% blocktrans with status=status %}Yes, {{status}}{% endblocktrans %}</button> <button class="btn btn-warning">{% blocktrans with status=status %}Yes, {{status}}{% endblocktrans %}</button>
</form> </form>
</div> </div>
<div class="clearfix"></div> <div class="clearfix"></div>
</div> </div>
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
{{ text }} {{ text }}
{% else %} {% else %}
{%blocktrans with object=object%} {%blocktrans with object=object%}
Are you sure you want to remove <strong>{{ member }}</strong> from <strong>{{ object }}</strong>? Are you sure you want to remove <strong>{{ member }}</strong> from <strong>{{ object }}</strong>?
{%endblocktrans%} {%endblocktrans%}
{% endif %} {% endif %}
<br /> <br />
......
...@@ -23,9 +23,9 @@ ...@@ -23,9 +23,9 @@
<div class="pull-right"> <div class="pull-right">
<form action="" method="POST"> <form action="" method="POST">
{% csrf_token %} {% csrf_token %}
<a class="btn btn-default">{% trans "Back" %}</a> <a class="btn btn-default">{% trans "Back" %}</a>
<input type="hidden" name="flush" value=""/> <input type="hidden" name="flush" value=""/>
<button class="btn btn-warning">{% trans "Yes" %}</button> <button class="btn btn-warning">{% trans "Yes" %}</button>
</form> </form>
</div> </div>
</div> </div>
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
{% csrf_token %} {% csrf_token %}
<a class="btn btn-default">{% trans "Cancel" %}</a> <a class="btn btn-default">{% trans "Cancel" %}</a>
<button type="button" class="btn btn-default" data-dismiss="modal"></button> <button type="button" class="btn btn-default" data-dismiss="modal"></button>
<input type="hidden" name="change_status" value=""/> <input type="hidden" name="change_status" value=""/>
<button class="btn btn-warning">{% blocktrans with status=status %}Yes, {{status}}{% endblocktrans %}</button> <button class="btn btn-warning">{% blocktrans with status=status %}Yes, {{status}}{% endblocktrans %}</button>
</form> </form>
</div> </div>
......
...@@ -6,15 +6,15 @@ ...@@ -6,15 +6,15 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="no-margin"> <h3 class="no-margin">
{% trans "Ownership transfer" %} {% trans "Ownership transfer" %}
</h3> </h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% blocktrans with owner=instance.owner name=instance.name id=instance.id%} {% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
<strong>{{ owner }}</strong> offered to take the ownership of <strong>{{ owner }}</strong> offered to take the ownership of
virtual machine <strong>{{name}} ({{id}})</strong>. virtual machine <strong>{{name}} ({{id}})</strong>.
Do you accept the responsility of being the host's owner? Do you accept the responsility of being the host's owner?
{% endblocktrans %} {% endblocktrans %}
<div class="pull-right"> <div class="pull-right">
<form action="" method="POST"> <form action="" method="POST">
{% csrf_token %} {% csrf_token %}
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{% trans "Ownership transfer" %}
</h3>
</div>
<div class="panel-body">
{% blocktrans with owner=instance.owner name=instance.name id=instance.id%}
<strong>{{ owner }}</strong> offered to take the ownership of
template <strong>{{name}} ({{id}})</strong>.
Do you accept the responsility of being the template's owner?
{% endblocktrans %}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default" href="{% url "dashboard.index" %}">{% trans "No" %}</a>
<input type="hidden" name="key" value="{{ key }}"/>
<button class="btn btn-danger" type="submit">{% trans "Yes" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
...@@ -10,10 +10,10 @@ ...@@ -10,10 +10,10 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-group"></i> Your groups</h3> <h3 class="no-margin"><i class="fa fa-group"></i> {% trans "Groups" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<div id="table_container"> <div id="table_container">
<div id="rendered_table" class="panel-body"> <div id="rendered_table" class="panel-body">
{% render_table table %} {% render_table table %}
</div> </div>
......
...@@ -26,7 +26,7 @@ ...@@ -26,7 +26,7 @@
<div class="col-sm-6 text-right"> <div class="col-sm-6 text-right">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.group-list" %}"> <a class="btn btn-primary btn-xs" href="{% url "dashboard.views.group-list" %}">
<i class="fa fa-chevron-circle-right"></i> <i class="fa fa-chevron-circle-right"></i>
{% if more_groups > 0 %} {% if more_groups > 0 %}
{% blocktrans count more=more_groups %} {% blocktrans count more=more_groups %}
<strong>{{ more }}</strong> more <strong>{{ more }}</strong> more
{% plural %} {% plural %}
......
...@@ -74,9 +74,11 @@ ...@@ -74,9 +74,11 @@
{% trans "list" %} {% trans "list" %}
{% endif %} {% endif %}
</a> </a>
{% if request.user.is_superuser %}
<a class="btn btn-success btn-xs node-create" href="{% url "dashboard.views.node-create" %}"> <a class="btn btn-success btn-xs node-create" href="{% url "dashboard.views.node-create" %}">
<i class="fa fa-plus-circle"></i> {% trans "new" %} <i class="fa fa-plus-circle"></i> {% trans "new" %}
</a> </a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
</div> </div>
{% endif %} {% endif %}
{% if user.is_superuser %} {% if perms.vm.view_statistics %}
<div class="col-lg-4 col-sm-6"> <div class="col-lg-4 col-sm-6">
{% include "dashboard/index-nodes.html" %} {% include "dashboard/index-nodes.html" %}
</div> </div>
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
<div class="col-md-12"> <div class="col-md-12">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-plus"></i> {% trans "Add Trait" %}</h3> <h3 class="no-margin"><i class="fa fa-plus"></i> {% trans "Add Trait" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
{% with form=form %} {% with form=form %}
......
...@@ -6,14 +6,16 @@ ...@@ -6,14 +6,16 @@
{% block content %} {% block content %}
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
{% if request.user.is_superuser %}
<div class="pull-right" id="ops"> <div class="pull-right" id="ops">
{% include "dashboard/vm-detail/_operations.html" %} {% include "dashboard/vm-detail/_operations.html" %}
</div> </div>
<div class="pull-right" style="padding-top: 15px;"> <div class="pull-right" style="padding-top: 15px;">
<a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs node-details-rename-button"><i class="fa fa-pencil"></i></a> <a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs node-details-rename-button"><i class="fa fa-pencil"></i></a>
<a title="{% trans "Delete" %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-delete" href="{% url "dashboard.views.delete-node" pk=node.pk %}"><i class="fa fa-trash-o"></i></a> <a title="{% trans "Delete" %}" data-node-pk="{{ node.pk }}" class="btn btn-default btn-xs real-link node-delete" href="{% url "dashboard.views.delete-node" pk=node.pk %}"><i class="fa fa-trash-o"></i></a>
</div> </div>
<h1> {% endif %}
<h1>
<div id="node-details-rename"> <div id="node-details-rename">
<form action="" method="POST" id="node-details-rename-form"> <form action="" method="POST" id="node-details-rename-form">
{% csrf_token %} {% csrf_token %}
...@@ -69,26 +71,26 @@ ...@@ -69,26 +71,26 @@
{% trans "Resources" %} {% trans "Resources" %}
</a> </a>
</li> </li>
<li> <li>
<a href="{% url "dashboard.views.vm-list" %}?s=node:{{ node.name }}" <a href="{% url "dashboard.views.vm-list" %}?s=node:{{ node.name }}"
target="blank" class="text-center"> target="blank" class="text-center">
<i class="fa fa-desktop fa-2x"></i><br> <i class="fa fa-desktop fa-2x"></i><br>
{% trans "Virtual Machines" %} {% trans "Virtual Machines" %}
</a> </a>
</li> </li>
<li> <li>
<a href="#activity" data-toggle="pill" class="text-center"> <a href="#activity" data-toggle="pill" class="text-center">
<i class="fa fa-clock-o fa-2x"></i><br> <i class="fa fa-clock-o fa-2x"></i><br>
{% trans "Activity" %} {% trans "Activity" %}
</a> </a>
</li> </li>
</ul> </ul>
<div id="panel-body" class="tab-content panel-body"> <div id="panel-body" class="tab-content panel-body">
<div class="tab-pane active" id="home">{% include "dashboard/node-detail/home.html" %}</div> <div class="tab-pane active" id="home">{% include "dashboard/node-detail/home.html" %}</div>
<div class="tab-pane" id="resources">{% include "dashboard/node-detail/resources.html" %}</div> <div class="tab-pane" id="resources">{% include "dashboard/node-detail/resources.html" %}</div>
<div class="tab-pane" id="activity">{% include "dashboard/node-detail/activity.html" %}</div> <div class="tab-pane" id="activity">{% include "dashboard/node-detail/activity.html" %}</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -8,24 +8,27 @@ ...@@ -8,24 +8,27 @@
{% for t in node.traits.all %} {% for t in node.traits.all %}
<div class="label label-success label-tag" style="display: inline-block"> <div class="label label-success label-tag" style="display: inline-block">
{{ t }} {{ t }}
<a data-trait-pk="{{ t.pk }}" href="#" class="node-details-remove-trait"><i class="fa fa-times"></i></a> <a data-trait-pk="{{ t.pk }}" href="#" class="node-details-remove-trait"><i class="fa fa-times"></i></a>
</div> </div>
{% endfor %} {% endfor %}
{% else %} {% else %}
<small>{% trans "No trait added!" %}</small> <small>{% trans "No trait added!" %}</small>
{% endif %} {% endif %}
</div> </div>
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
<style> <style>
.row { .row {
margin-bottom: 15px; margin-bottom: 15px;
} }
</style> </style>
<form action="{% url "dashboard.views.node-addtrait" node.pk %}" method="POST"> {% if request.user.is_superuser %}
{% csrf_token %} <form action="{% url "dashboard.views.node-addtrait" node.pk %}" method="POST">
{% crispy trait_form %} {% csrf_token %}
</form> {% crispy trait_form %}
</form>
{% endif %}
</div><!-- id:node-details-traits --> </div><!-- id:node-details-traits -->
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
......
...@@ -10,6 +10,16 @@ ...@@ -10,6 +10,16 @@
<dt>{% trans "Enabled" %}:</dt><dd>{{ node.enabled }}</dd> <dt>{% trans "Enabled" %}:</dt><dd>{{ node.enabled }}</dd>
<dt>{% trans "Host online" %}:</dt><dd> {{ node.online }}</dd> <dt>{% trans "Host online" %}:</dt><dd> {{ node.online }}</dd>
<dt>{% trans "Priority" %}:</dt><dd>{{ node.priority }}</dd> <dt>{% trans "Priority" %}:</dt><dd>{{ node.priority }}</dd>
<dt>{% trans "Driver Version:" %}</dt>
<dd>
{% if node.driver_version %}
{{ node.driver_version.branch }} at
{{ node.driver_version.commit }} ({{ node.driver_version.commit_text }})
{% if node.driver_version.is_dirty %}
<span class="label label-danger">{% trans "with uncommitted changes!" %}</span>
{% endif %}
{% endif %}
</dd>
<dt>{% trans "Host owner" %}:</dt> <dt>{% trans "Host owner" %}:</dt>
<dd> <dd>
{% include "dashboard/_display-name.html" with user=node.host.owner show_org=True %} {% include "dashboard/_display-name.html" with user=node.host.owner show_org=True %}
...@@ -18,10 +28,12 @@ ...@@ -18,10 +28,12 @@
<dt>{% trans "Host name" %}:</dt> <dt>{% trans "Host name" %}:</dt>
<dd> <dd>
{{ node.host.hostname }} {{ node.host.hostname }}
{% if request.user.is_superuser %}
<a href="{{ node.host.get_absolute_url }}" class="btn btn-default btn-xs"> <a href="{{ node.host.get_absolute_url }}" class="btn btn-default btn-xs">
<i class="fa fa-pencil"></i> <i class="fa fa-pencil"></i>
{% trans "Edit host" %} {% trans "Edit host" %}
</a> </a>
{% endif %}
</dd> </dd>
</dl> </dl>
......
...@@ -81,6 +81,26 @@ ...@@ -81,6 +81,26 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h4 class="no-margin"><i class="fa fa-user"></i> {% trans "Owner" %}</h4>
</div>
<div class="panel-body">
{% if user == object.owner %}
{% blocktrans %}You are the current owner of this template.{% endblocktrans %}
{% else %}
{% url "dashboard.views.profile" username=object.owner.username as url %}
{% blocktrans with owner=object.owner name=object.owner.get_full_name%}
The current owner of this template is <a href="{{url}}">{{name}} ({{owner}})</a>.
{% endblocktrans %}
{% endif %}
{% if user == object.owner or user.is_superuser %}
<a href="{% url "dashboard.views.template-transfer-ownership" object.pk %}"
class="btn btn-link tx-tpl-ownership">{% trans "Transfer ownership..." %}</a>
{% endif %}
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="fa fa-group"></i> {% trans "Manage access" %}</h4> <h4 class="no-margin"><i class="fa fa-group"></i> {% trans "Manage access" %}</h4>
</div> </div>
<div class="panel-body"> <div class="panel-body">
......
{% load i18n %}
<div class="pull-right">
<form action="{% url "dashboard.views.template-transfer-ownership" pk=object.pk %}" method="POST" style="max-width: 400px;">
{% csrf_token %}
<label>
{{ form.name.label }}
</label>
<div class="input-group">
{{form.name}}
<div class="input-group-btn">
<input type="submit" value="{% trans "Save" %}" class="btn btn-primary">
</div>
</div>
</form>
</div>
...@@ -6,19 +6,19 @@ ...@@ -6,19 +6,19 @@
{% block content %} {% block content %}
{% if instance.is_base %} {% if instance.is_base %}
<div class="alert alert-info alert-new-template" id="alert-new-template" style="position: relative;"> <div class="alert alert-info alert-new-template" id="alert-new-template" style="position: relative;">
<form action="{% url "dashboard.views.vm-toggle-tutorial" pk=instance.pk %}" <form action="{% url "dashboard.views.vm-toggle-tutorial" pk=instance.pk %}"
method="POST"> method="POST">
{% csrf_token %} {% csrf_token %}
<input name="hidden" type="hidden" <input name="hidden" type="hidden"
value="{{ hide_tutorial|yesno:"false,true" }}"/> value="{{ hide_tutorial|yesno:"false,true" }}"/>
<button type="submit" <button type="submit"
id="dashboard-tutorial-toggle" class="btn btn-sm pull-right btn-success"> id="dashboard-tutorial-toggle" class="btn btn-sm pull-right btn-success">
<i class="fa fa-caret-{% if hide_tutorial %}down{% else %}up{% endif %}"></i> <i class="fa fa-caret-{% if hide_tutorial %}down{% else %}up{% endif %}"></i>
{% trans "Toggle tutorial panel" %} {% trans "Toggle tutorial panel" %}
</button> </button>
<a href="#" class="btn btn-default btn-sm pull-right" <a href="#" class="btn btn-default btn-sm pull-right"
id="vm-details-start-template-tour"> id="vm-details-start-template-tour">
<i class="fa fa-play"></i> {% trans "Start template tutorial" %} <i class="fa fa-play"></i> {% trans "Start template tutorial" %}
</a> </a>
...@@ -121,7 +121,14 @@ ...@@ -121,7 +121,14 @@
<dd style="font-size: 10px; text-align: right; padding-top: 8px;"> <dd style="font-size: 10px; text-align: right; padding-top: 8px;">
<div id="vm-details-pw-reset"> <div id="vm-details-pw-reset">
{% with op=op.password_reset %}{% if op %} {% with op=op.password_reset %}{% if op %}
<a href="{% if op.disabled %}#{% else %}{{op.get_url}}{% endif %}" class="operation operation-{{op.op}}" data-disabled="{% if op.disabled %}true" title="{% trans "Start the VM to change the password." %}"{% else %}false" {% endif %}>{% trans "Generate new password!" %}</a> <a href="{% if op.disabled %}#{% else %}{{op.get_url}}{% endif %}"
class="operation operation-{{op.op}}"
{% if op.disabled %}
data-disabled="true"
title="{% if instance.has_agent %}{% trans "Start the VM to change the password." %}{% else %}{% trans "This machine has no agent installed." %}{% endif %}"
{% endif %}>
{% trans "Generate new password!" %}
</a>
{% endif %}{% endwith %} {% endif %}{% endwith %}
</div> </div>
</dd> </dd>
...@@ -149,7 +156,7 @@ ...@@ -149,7 +156,7 @@
</div> </div>
{% endfor %} {% endfor %}
{% if instance.get_connect_uri %} {% if instance.get_connect_uri %}
<div id="dashboard-vm-details-connect" class="operation-wrapper"> <div id="dashboard-vm-details-connect" class="operation-wrapper">
{% if client_download %} {% if client_download %}
<a id="dashboard-vm-details-connect-button" class="btn btn-xs btn-default operation " href="{{ instance.get_connect_uri}}" title="{% trans "Connect via the CIRCLE Client" %}"> <a id="dashboard-vm-details-connect-button" class="btn btn-xs btn-default operation " href="{{ instance.get_connect_uri}}" title="{% trans "Connect via the CIRCLE Client" %}">
<i class="fa fa-external-link"></i> {% trans "Connect" %} <i class="fa fa-external-link"></i> {% trans "Connect" %}
......
...@@ -4,8 +4,9 @@ ...@@ -4,8 +4,9 @@
{% if user == instance.owner %} {% if user == instance.owner %}
{% blocktrans %}You are the current owner of this instance.{% endblocktrans %} {% blocktrans %}You are the current owner of this instance.{% endblocktrans %}
{% else %} {% else %}
{% blocktrans with owner=instance.owner %} {% url "dashboard.views.profile" username=instance.owner.username as url %}
The current owner of this instance is {{owner}}. {% blocktrans with owner=instance.owner name=instance.owner.get_full_name%}
The current owner of this instance is <a href="{{url}}">{{name}} ({{owner}})</a>.
{% endblocktrans %} {% endblocktrans %}
{% endif %} {% endif %}
{% if user == instance.owner or user.is_superuser %} {% if user == instance.owner or user.is_superuser %}
......
...@@ -637,7 +637,7 @@ class NodeDetailTest(LoginMixin, TestCase): ...@@ -637,7 +637,7 @@ class NodeDetailTest(LoginMixin, TestCase):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
response = c.get('/dashboard/node/25555/') response = c.get('/dashboard/node/25555/')
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 403)
def test_anon_node_page(self): def test_anon_node_page(self):
c = Client() c = Client()
...@@ -667,7 +667,7 @@ class NodeDetailTest(LoginMixin, TestCase): ...@@ -667,7 +667,7 @@ class NodeDetailTest(LoginMixin, TestCase):
node = Node.objects.get(pk=1) node = Node.objects.get(pk=1)
old_name = node.name old_name = node.name
response = c.post("/dashboard/node/1/", {'new_name': 'test1235'}) response = c.post("/dashboard/node/1/", {'new_name': 'test1235'})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 403)
self.assertEqual(Node.objects.get(pk=1).name, old_name) self.assertEqual(Node.objects.get(pk=1).name, old_name)
def test_permitted_set_name(self): def test_permitted_set_name(self):
...@@ -721,7 +721,7 @@ class NodeDetailTest(LoginMixin, TestCase): ...@@ -721,7 +721,7 @@ class NodeDetailTest(LoginMixin, TestCase):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
response = c.post("/dashboard/node/1/", {'to_remove': traitid}) response = c.post("/dashboard/node/1/", {'to_remove': traitid})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 403)
self.assertEqual(Node.objects.get(pk=1).traits.count(), trait_count) self.assertEqual(Node.objects.get(pk=1).traits.count(), trait_count)
def test_permitted_remove_trait(self): def test_permitted_remove_trait(self):
......
...@@ -27,8 +27,8 @@ from .views import ( ...@@ -27,8 +27,8 @@ from .views import (
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete, MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeList, NodeStatus, NodeDetailView, NodeList, NodeStatus,
NotificationView, TemplateAclUpdateView, TemplateCreate, NotificationView, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView, TemplateDelete, TemplateDetail, TemplateList,
TransferOwnershipView, vm_activity, VmCreate, VmDetailView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmList, VmDetailVncTokenView, VmList,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveUserView, GroupRemoveUserView,
...@@ -48,6 +48,8 @@ from .views import ( ...@@ -48,6 +48,8 @@ from .views import (
toggle_template_tutorial, toggle_template_tutorial,
ClientCheck, TokenLogin, ClientCheck, TokenLogin,
VmGraphView, NodeGraphView, NodeListGraphView, VmGraphView, NodeGraphView, NodeListGraphView,
TransferInstanceOwnershipView, TransferInstanceOwnershipConfirmView,
TransferTemplateOwnershipView, TransferTemplateOwnershipConfirmView,
) )
from .views.vm import vm_ops, vm_mass_ops from .views.vm import vm_ops, vm_mass_ops
from .views.node import node_ops from .views.node import node_ops
...@@ -78,13 +80,15 @@ urlpatterns = patterns( ...@@ -78,13 +80,15 @@ urlpatterns = patterns(
name="dashboard.views.template-list"), name="dashboard.views.template-list"),
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'^template/(?P<pk>\d+)/tx/$', TransferTemplateOwnershipView.as_view(),
name='dashboard.views.template-transfer-ownership'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
name='dashboard.views.detail'), name='dashboard.views.detail'),
url(r'^vm/(?P<pk>\d+)/vnctoken/$', VmDetailVncTokenView.as_view(), url(r'^vm/(?P<pk>\d+)/vnctoken/$', VmDetailVncTokenView.as_view(),
name='dashboard.views.detail-vnc'), name='dashboard.views.detail-vnc'),
url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance), url(r'^vm/(?P<pk>\d+)/acl/$', AclUpdateView.as_view(model=Instance),
name='dashboard.views.vm-acl'), name='dashboard.views.vm-acl'),
url(r'^vm/(?P<pk>\d+)/tx/$', TransferOwnershipView.as_view(), url(r'^vm/(?P<pk>\d+)/tx/$', TransferInstanceOwnershipView.as_view(),
name='dashboard.views.vm-transfer-ownership'), name='dashboard.views.vm-transfer-ownership'),
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(),
...@@ -106,8 +110,12 @@ urlpatterns = patterns( ...@@ -106,8 +110,12 @@ urlpatterns = patterns(
name='dashboard.views.node-detail'), name='dashboard.views.node-detail'),
url(r'^node/(?P<pk>\d+)/add-trait/$', NodeAddTraitView.as_view(), url(r'^node/(?P<pk>\d+)/add-trait/$', NodeAddTraitView.as_view(),
name='dashboard.views.node-addtrait'), name='dashboard.views.node-addtrait'),
url(r'^tx/(?P<key>.*)/?$', TransferOwnershipConfirmView.as_view(), url(r'^vm/tx/(?P<key>.*)/?$',
TransferInstanceOwnershipConfirmView.as_view(),
name='dashboard.views.vm-transfer-ownership-confirm'), name='dashboard.views.vm-transfer-ownership-confirm'),
url(r'^template/tx/(?P<key>.*)/?$',
TransferTemplateOwnershipConfirmView.as_view(),
name='dashboard.views.template-transfer-ownership-confirm'),
url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(), url(r'^node/delete/(?P<pk>\d+)/$', NodeDelete.as_view(),
name="dashboard.views.delete-node"), name="dashboard.views.delete-node"),
url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(), url(r'^node/status/(?P<pk>\d+)/$', NodeStatus.as_view(),
......
...@@ -26,7 +26,7 @@ from django.http import HttpResponse, Http404 ...@@ -26,7 +26,7 @@ from django.http import HttpResponse, Http404
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import View from django.views.generic import View
from braces.views import LoginRequiredMixin, SuperuserRequiredMixin from braces.views import LoginRequiredMixin
from vm.models import Instance, Node from vm.models import Instance, Node
...@@ -142,22 +142,28 @@ class VmGraphView(GraphViewBase): ...@@ -142,22 +142,28 @@ class VmGraphView(GraphViewBase):
base = VmMetric base = VmMetric
class NodeGraphView(SuperuserRequiredMixin, GraphViewBase): class NodeGraphView(GraphViewBase):
model = Node model = Node
base = NodeMetric base = NodeMetric
def get_object(self, request, pk): def get_object(self, request, pk):
if not self.request.user.has_perm('vm.view_statistics'):
raise PermissionDenied()
return self.model.objects.get(id=pk) return self.model.objects.get(id=pk)
class NodeListGraphView(SuperuserRequiredMixin, GraphViewBase): class NodeListGraphView(GraphViewBase):
model = Node model = Node
base = Metric base = Metric
def get_object(self, request, pk): def get_object(self, request, pk):
if not self.request.user.has_perm('vm.view_statistics'):
raise PermissionDenied()
return Node.objects.filter(enabled=True) return Node.objects.filter(enabled=True)
def get(self, request, metric, time, *args, **kwargs): def get(self, request, metric, time, *args, **kwargs):
if not self.request.user.has_perm('vm.view_statistics'):
raise PermissionDenied()
return super(NodeListGraphView, self).get(request, None, metric, time) return super(NodeListGraphView, self).get(request, None, metric, time)
......
...@@ -62,7 +62,7 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -62,7 +62,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
}) })
# nodes # nodes
if user.is_superuser: if user.has_perm('vm.view_statistics'):
nodes = Node.objects.all() nodes = Node.objects.all()
context.update({ context.update({
'nodes': nodes[:5], 'nodes': nodes[:5],
......
...@@ -75,13 +75,18 @@ node_ops = OrderedDict([ ...@@ -75,13 +75,18 @@ node_ops = OrderedDict([
]) ])
class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, class NodeDetailView(LoginRequiredMixin,
GraphMixin, DetailView): GraphMixin, DetailView):
template_name = "dashboard/node-detail.html" template_name = "dashboard/node-detail.html"
model = Node model = Node
form = None form = None
form_class = TraitForm form_class = TraitForm
def get(self, *args, **kwargs):
if not self.request.user.has_perm('vm.view_statistics'):
raise PermissionDenied()
return super(NodeDetailView, self).get(*args, **kwargs)
def get_context_data(self, form=None, **kwargs): def get_context_data(self, form=None, **kwargs):
if form is None: if form is None:
form = self.form_class() form = self.form_class()
...@@ -98,6 +103,8 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -98,6 +103,8 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin,
return context return context
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if not request.user.is_superuser:
raise PermissionDenied()
if request.POST.get('new_name'): if request.POST.get('new_name'):
return self.__set_name(request) return self.__set_name(request)
if request.POST.get('to_remove'): if request.POST.get('to_remove'):
...@@ -145,13 +152,14 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -145,13 +152,14 @@ class NodeDetailView(LoginRequiredMixin, SuperuserRequiredMixin,
return redirect(self.object.get_absolute_url()) return redirect(self.object.get_absolute_url())
class NodeList(LoginRequiredMixin, SuperuserRequiredMixin, class NodeList(LoginRequiredMixin, GraphMixin, SingleTableView):
GraphMixin, SingleTableView):
template_name = "dashboard/node-list.html" template_name = "dashboard/node-list.html"
table_class = NodeListTable table_class = NodeListTable
table_pagination = False table_pagination = False
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
if not self.request.user.has_perm('vm.view_statistics'):
raise PermissionDenied()
if self.request.is_ajax(): if self.request.is_ajax():
nodes = Node.objects.all() nodes = Node.objects.all()
nodes = [{ nodes = [{
......
...@@ -26,7 +26,7 @@ from django.core.urlresolvers import reverse, reverse_lazy ...@@ -26,7 +26,7 @@ from django.core.urlresolvers import reverse, reverse_lazy
from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404 from django.shortcuts import redirect, get_object_or_404
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _, ugettext_noop
from django.views.generic import ( from django.views.generic import (
TemplateView, CreateView, DeleteView, UpdateView, TemplateView, CreateView, DeleteView, UpdateView,
) )
...@@ -44,7 +44,10 @@ from ..forms import ( ...@@ -44,7 +44,10 @@ from ..forms import (
) )
from ..tables import TemplateListTable, LeaseListTable from ..tables import TemplateListTable, LeaseListTable
from .util import AclUpdateView, FilterMixin from .util import (
AclUpdateView, FilterMixin,
TransferOwnershipConfirmView, TransferOwnershipView,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -488,3 +491,20 @@ class LeaseDelete(LoginRequiredMixin, DeleteView): ...@@ -488,3 +491,20 @@ class LeaseDelete(LoginRequiredMixin, DeleteView):
else: else:
messages.success(request, success_message) messages.success(request, success_message)
return HttpResponseRedirect(success_url) return HttpResponseRedirect(success_url)
class TransferTemplateOwnershipConfirmView(TransferOwnershipConfirmView):
template = "dashboard/confirm/transfer-template-ownership.html"
model = InstanceTemplate
class TransferTemplateOwnershipView(TransferOwnershipView):
confirm_view = TransferTemplateOwnershipConfirmView
model = InstanceTemplate
notification_msg = ugettext_noop(
'%(user)s offered you to take the ownership of '
'his/her template called %(instance)s. '
'<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>')
token_url = 'dashboard.views.template-transfer-ownership-confirm'
template = "dashboard/template-tx-owner.html"
...@@ -24,14 +24,15 @@ from urlparse import urljoin ...@@ -24,14 +24,15 @@ from urlparse import urljoin
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.exceptions import PermissionDenied from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.views import redirect_to_login from django.contrib.auth.views import redirect_to_login
from django.db.models import Q from django.db.models import Q
from django.http import HttpResponse, HttpResponseRedirect from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect, render
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _, ugettext_noop
from django.views.generic import DetailView, View from django.views.generic import DetailView, View
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
...@@ -40,7 +41,8 @@ from braces.views._access import AccessMixin ...@@ -40,7 +41,8 @@ from braces.views._access import AccessMixin
from celery.exceptions import TimeoutError from celery.exceptions import TimeoutError
from common.models import HumanReadableException, HumanReadableObject from common.models import HumanReadableException, HumanReadableObject
from ..models import GroupProfile from ..models import GroupProfile, Profile
from ..forms import TransferOwnershipForm
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
saml_available = hasattr(settings, "SAML_CONFIG") saml_available = hasattr(settings, "SAML_CONFIG")
...@@ -563,3 +565,132 @@ class GraphMixin(object): ...@@ -563,3 +565,132 @@ class GraphMixin(object):
def absolute_url(url): def absolute_url(url):
return urljoin(settings.DJANGO_URL, url) return urljoin(settings.DJANGO_URL, url)
class TransferOwnershipView(CheckedDetailView, DetailView):
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/_modal.html']
else:
return ['dashboard/nojs-wrapper.html']
def get_context_data(self, *args, **kwargs):
context = super(TransferOwnershipView, self).get_context_data(
*args, **kwargs)
context['form'] = TransferOwnershipForm()
context.update({
'box_title': _("Transfer ownership"),
'ajax_title': True,
'template': self.template,
})
return context
def post(self, request, *args, **kwargs):
form = TransferOwnershipForm(request.POST)
if not form.is_valid():
return self.get(request)
try:
new_owner = search_user(request.POST['name'])
except User.DoesNotExist:
messages.error(request, _('Can not find specified user.'))
return self.get(request, *args, **kwargs)
except KeyError:
raise SuspiciousOperation()
obj = self.get_object()
if not (obj.owner == request.user or
request.user.is_superuser):
raise PermissionDenied()
token = signing.dumps(
(obj.pk, new_owner.pk),
salt=self.confirm_view.get_salt())
token_path = reverse(self.token_url, args=[token])
try:
new_owner.profile.notify(
ugettext_noop('Ownership offer'),
self.notification_msg,
{'instance': obj, 'token': token_path})
except Profile.DoesNotExist:
messages.error(request, _('Can not notify selected user.'))
else:
messages.success(request,
_('User %s is notified about the offer.') % (
unicode(new_owner), ))
return redirect(obj.get_absolute_url())
class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"""User can accept an ownership offer."""
max_age = 3 * 24 * 3600
success_message = _("Ownership successfully transferred to you.")
@classmethod
def get_salt(cls):
return unicode(cls) + unicode(cls.model)
def get(self, request, key, *args, **kwargs):
"""Confirm ownership transfer based on token.
"""
logger.debug('Confirm dialog for token %s.', key)
try:
instance, new_owner = self.get_instance(key, request.user)
except PermissionDenied:
messages.error(request, _('This token is for an other user.'))
raise
except SuspiciousOperation:
messages.error(request, _('This token is invalid or has expired.'))
raise PermissionDenied()
return render(request, self.template,
dictionary={'instance': instance, 'key': key})
def change_owner(self, instance, new_owner):
instance.owner = new_owner
instance.clean()
instance.save()
def post(self, request, key, *args, **kwargs):
"""Really transfer ownership based on token.
"""
instance, owner = self.get_instance(key, request.user)
old = instance.owner
self.change_owner(instance, request.user)
messages.success(request, self.success_message)
logger.info('Ownership of %s transferred from %s to %s.',
unicode(instance), unicode(old), unicode(request.user))
if old.profile:
old.profile.notify(
ugettext_noop('Ownership accepted'),
ugettext_noop('Your ownership offer of %(instance)s has been '
'accepted by %(user)s.'),
{'instance': instance})
return redirect(instance.get_absolute_url())
def get_instance(self, key, user):
"""Get object based on signed token.
"""
try:
instance, new_owner = (
signing.loads(key, max_age=self.max_age,
salt=self.get_salt()))
except (signing.BadSignature, ValueError, TypeError) as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
try:
instance = self.model.objects.get(id=instance)
except self.model.DoesNotExist as e:
logger.error('Tried token to nonexistent instance %d. '
'Token: %s, user: %s. %s',
instance, key, unicode(user), unicode(e))
raise Http404()
if new_owner != user.pk:
logger.error('%s (%d) tried the token for %s. Token: %s.',
unicode(user), user.pk, new_owner, key)
raise PermissionDenied()
return (instance, new_owner)
...@@ -29,7 +29,7 @@ from django.core import signing ...@@ -29,7 +29,7 @@ from django.core import signing
from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse, Http404, HttpResponseRedirect from django.http import HttpResponse, Http404, HttpResponseRedirect
from django.shortcuts import redirect, get_object_or_404, render from django.shortcuts import redirect, get_object_or_404
from django.template import RequestContext from django.template import RequestContext
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import ( from django.utils.translation import (
...@@ -37,7 +37,7 @@ from django.utils.translation import ( ...@@ -37,7 +37,7 @@ from django.utils.translation import (
) )
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
from django.views.generic import ( from django.views.generic import (
UpdateView, ListView, TemplateView, DeleteView, DetailView, View, UpdateView, ListView, TemplateView, DeleteView
) )
from braces.views import SuperuserRequiredMixin, LoginRequiredMixin from braces.views import SuperuserRequiredMixin, LoginRequiredMixin
...@@ -54,17 +54,18 @@ from vm.models import ( ...@@ -54,17 +54,18 @@ from vm.models import (
) )
from .util import ( from .util import (
CheckedDetailView, AjaxOperationMixin, OperationView, AclUpdateView, CheckedDetailView, AjaxOperationMixin, OperationView, AclUpdateView,
FormOperationMixin, FilterMixin, search_user, GraphMixin, FormOperationMixin, FilterMixin, GraphMixin,
TransferOwnershipConfirmView, TransferOwnershipView,
) )
from ..forms import ( from ..forms import (
AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm, AclUserOrGroupAddForm, VmResourcesForm, TraitsForm, RawDataForm,
VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm, VmAddInterfaceForm, VmCreateDiskForm, VmDownloadDiskForm, VmSaveForm,
VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm, VmRenewForm, VmStateChangeForm, VmListSearchForm, VmCustomizeForm,
TransferOwnershipForm, VmDiskResizeForm, RedeployForm, VmDiskRemoveForm, VmDiskResizeForm, RedeployForm, VmDiskRemoveForm,
VmMigrateForm, VmDeployForm, VmMigrateForm, VmDeployForm,
VmPortRemoveForm, VmPortAddForm, VmPortRemoveForm, VmPortAddForm,
) )
from ..models import Favourite, Profile from ..models import Favourite
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -1284,139 +1285,29 @@ class FavouriteView(TemplateView): ...@@ -1284,139 +1285,29 @@ class FavouriteView(TemplateView):
return HttpResponse("Added.") return HttpResponse("Added.")
class TransferOwnershipView(CheckedDetailView, DetailView): class TransferInstanceOwnershipConfirmView(TransferOwnershipConfirmView):
template = "dashboard/confirm/transfer-instance-ownership.html"
model = Instance model = Instance
def get_template_names(self): def change_owner(self, instance, new_owner):
if self.request.is_ajax(): with instance.activity(
return ['dashboard/_modal.html'] code_suffix='ownership-transferred',
else: readable_name=ugettext_noop("transfer ownership"),
return ['dashboard/nojs-wrapper.html'] concurrency_check=False, user=new_owner):
super(TransferInstanceOwnershipConfirmView, self).change_owner(
def get_context_data(self, *args, **kwargs): instance, new_owner)
context = super(TransferOwnershipView, self).get_context_data(
*args, **kwargs)
context['form'] = TransferOwnershipForm()
context.update({
'box_title': _("Transfer ownership"),
'ajax_title': True,
'template': "dashboard/vm-detail/tx-owner.html",
})
return context
def post(self, request, *args, **kwargs):
form = TransferOwnershipForm(request.POST)
if not form.is_valid():
return self.get(request)
try:
new_owner = search_user(request.POST['name'])
except User.DoesNotExist:
messages.error(request, _('Can not find specified user.'))
return self.get(request, *args, **kwargs)
except KeyError:
raise SuspiciousOperation()
obj = self.get_object()
if not (obj.owner == request.user or
request.user.is_superuser):
raise PermissionDenied()
token = signing.dumps((obj.pk, new_owner.pk),
salt=TransferOwnershipConfirmView.get_salt())
token_path = reverse(
'dashboard.views.vm-transfer-ownership-confirm', args=[token])
try:
new_owner.profile.notify(
ugettext_noop('Ownership offer'),
ugettext_noop('%(user)s offered you to take the ownership of '
'his/her virtual machine called %(instance)s. '
'<a href="%(token)s" '
'class="btn btn-success btn-small">Accept</a>'),
{'instance': obj, 'token': token_path})
except Profile.DoesNotExist:
messages.error(request, _('Can not notify selected user.'))
else:
messages.success(request,
_('User %s is notified about the offer.') % (
unicode(new_owner), ))
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': obj.pk}))
class TransferOwnershipConfirmView(LoginRequiredMixin, View): class TransferInstanceOwnershipView(TransferOwnershipView):
"""User can accept an ownership offer.""" confirm_view = TransferInstanceOwnershipConfirmView
model = Instance
max_age = 3 * 24 * 3600 notification_msg = ugettext_noop(
success_message = _("Ownership successfully transferred to you.") '%(user)s offered you to take the ownership of '
'his/her virtual machine called %(instance)s. '
@classmethod '<a href="%(token)s" '
def get_salt(cls): 'class="btn btn-success btn-small">Accept</a>')
return unicode(cls) token_url = 'dashboard.views.vm-transfer-ownership-confirm'
template = "dashboard/vm-detail/tx-owner.html"
def get(self, request, key, *args, **kwargs):
"""Confirm ownership transfer based on token.
"""
logger.debug('Confirm dialog for token %s.', key)
try:
instance, new_owner = self.get_instance(key, request.user)
except PermissionDenied:
messages.error(request, _('This token is for an other user.'))
raise
except SuspiciousOperation:
messages.error(request, _('This token is invalid or has expired.'))
raise PermissionDenied()
return render(request,
"dashboard/confirm/base-transfer-ownership.html",
dictionary={'instance': instance, 'key': key})
def post(self, request, key, *args, **kwargs):
"""Really transfer ownership based on token.
"""
instance, owner = self.get_instance(key, request.user)
old = instance.owner
with instance.activity(code_suffix='ownership-transferred',
concurrency_check=False, user=request.user):
instance.owner = request.user
instance.clean()
instance.save()
messages.success(request, self.success_message)
logger.info('Ownership of %s transferred from %s to %s.',
unicode(instance), unicode(old), unicode(request.user))
if old.profile:
old.profile.notify(
ugettext_noop('Ownership accepted'),
ugettext_noop('Your ownership offer of %(instance)s has been '
'accepted by %(user)s.'),
{'instance': instance})
return redirect(instance.get_absolute_url())
def get_instance(self, key, user):
"""Get object based on signed token.
"""
try:
instance, new_owner = (
signing.loads(key, max_age=self.max_age,
salt=self.get_salt()))
except (signing.BadSignature, ValueError, TypeError) as e:
logger.error('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(user), unicode(e))
raise SuspiciousOperation()
try:
instance = Instance.objects.get(id=instance)
except Instance.DoesNotExist as e:
logger.error('Tried token to nonexistent instance %d. '
'Token: %s, user: %s. %s',
instance, key, unicode(user), unicode(e))
raise Http404()
if new_owner != user.pk:
logger.error('%s (%d) tried the token for %s. Token: %s.',
unicode(user), user.pk, new_owner, key)
raise PermissionDenied()
return (instance, new_owner)
@login_required @login_required
......
...@@ -88,7 +88,9 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -88,7 +88,9 @@ class Node(OperatedMixin, TimeStampedModel):
class Meta: class Meta:
app_label = 'vm' app_label = 'vm'
db_table = 'vm_node' db_table = 'vm_node'
permissions = () permissions = (
('view_statistics', _('Can view Node box and statistics.')),
)
ordering = ('-enabled', 'normalized_name') ordering = ('-enabled', 'normalized_name')
def __unicode__(self): def __unicode__(self):
...@@ -288,6 +290,11 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -288,6 +290,11 @@ class Node(OperatedMixin, TimeStampedModel):
@property @property
@node_available @node_available
def driver_version(self):
return self.info.get('driver_version')
@property
@node_available
def cpu_usage(self): def cpu_usage(self):
return self.monitor_info.get('cpu.percent') / 100 return self.monitor_info.get('cpu.percent') / 100
......
...@@ -717,7 +717,7 @@ class SaveAsTemplateOperation(InstanceOperation): ...@@ -717,7 +717,7 @@ class SaveAsTemplateOperation(InstanceOperation):
with_shutdown=True, clone=False, task=None, **kwargs): with_shutdown=True, clone=False, task=None, **kwargs):
try: try:
self.instance._cleanup(parent_activity=activity, user=user) self.instance._cleanup(parent_activity=activity, user=user)
except Instance.WrongStateError: except:
pass pass
if with_shutdown: if with_shutdown:
...@@ -1135,6 +1135,7 @@ class ActivateOperation(NodeOperation): ...@@ -1135,6 +1135,7 @@ class ActivateOperation(NodeOperation):
def _operation(self): def _operation(self):
self.node.enabled = True self.node.enabled = True
self.node.schedule_enabled = True self.node.schedule_enabled = True
self.node.get_info(invalidate_cache=True)
self.node.save() self.node.save()
...@@ -1156,6 +1157,7 @@ class PassivateOperation(NodeOperation): ...@@ -1156,6 +1157,7 @@ class PassivateOperation(NodeOperation):
def _operation(self): def _operation(self):
self.node.enabled = True self.node.enabled = True
self.node.schedule_enabled = False self.node.schedule_enabled = False
self.node.get_info(invalidate_cache=True)
self.node.save() self.node.save()
...@@ -1214,13 +1216,27 @@ class RecoverOperation(InstanceOperation): ...@@ -1214,13 +1216,27 @@ class RecoverOperation(InstanceOperation):
except Instance.InstanceDestroyedError: except Instance.InstanceDestroyedError:
pass pass
def _operation(self): def _operation(self, user, activity):
for disk in self.instance.disks.all(): with activity.sub_activity(
disk.destroyed = None 'recover_instance',
disk.restore() readable_name=ugettext_noop("recover instance")):
disk.save() self.instance.destroyed_at = None
self.instance.destroyed_at = None for disk in self.instance.disks.all():
self.instance.save() disk.destroyed = None
disk.restore()
disk.save()
self.instance.status = 'PENDING'
self.instance.save()
try:
self.instance.renew(parent_activity=activity)
except:
pass
if self.instance.template:
for net in self.instance.template.interface_set.all():
self.instance.add_interface(
parent_activity=activity, user=user, vlan=net.vlan)
@register_operation @register_operation
......
...@@ -2,4 +2,5 @@ ...@@ -2,4 +2,5 @@
-r base.txt -r base.txt
coverage==3.7.1 coverage==3.7.1
django-debug-toolbar==1.1 django-debug-toolbar==1.1
django-rosetta==0.7.4
Sphinx==1.2.2 Sphinx==1.2.2
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