Commit 0b76aed5 by Csók Tamás

Merge remote-tracking branch 'origin/master' into issue-218

parents d3fc9a1f 8eb07036
...@@ -71,6 +71,17 @@ class AclBase(Model): ...@@ -71,6 +71,17 @@ class AclBase(Model):
"""Define permission levels for Users/Groups per object.""" """Define permission levels for Users/Groups per object."""
object_level_set = GenericRelation(ObjectLevel) object_level_set = GenericRelation(ObjectLevel)
def clone_acl(self, other):
"""Clone full ACL from other object."""
assert self.id != other.id or type(self) != type(other)
self.object_level_set.clear()
for i in other.object_level_set.all():
ol = self.object_level_set.create(level=i.level)
for j in i.users.all():
ol.users.add(j)
for j in i.groups.all():
ol.groups.add(j)
@classmethod @classmethod
def get_level_object(cls, level): def get_level_object(cls, level):
......
...@@ -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(
'', '',
......
...@@ -49,7 +49,15 @@ class WorkerNotFound(Exception): ...@@ -49,7 +49,15 @@ class WorkerNotFound(Exception):
pass pass
def get_error_msg(exception):
try:
return unicode(exception)
except UnicodeDecodeError:
return unicode(str(exception), encoding='utf-8', errors='replace')
def activitycontextimpl(act, on_abort=None, on_commit=None): def activitycontextimpl(act, on_abort=None, on_commit=None):
result = None
try: try:
try: try:
yield act yield act
...@@ -62,7 +70,7 @@ def activitycontextimpl(act, on_abort=None, on_commit=None): ...@@ -62,7 +70,7 @@ def activitycontextimpl(act, on_abort=None, on_commit=None):
result = create_readable( result = create_readable(
ugettext_noop("Failure."), ugettext_noop("Failure."),
ugettext_noop("Unhandled exception: %(error)s"), ugettext_noop("Unhandled exception: %(error)s"),
error=unicode(e)) error=get_error_msg(e))
raise raise
except: except:
logger.exception("Failed activity %s" % unicode(act)) logger.exception("Failed activity %s" % unicode(act))
......
...@@ -22,6 +22,7 @@ from mock import MagicMock ...@@ -22,6 +22,7 @@ from mock import MagicMock
from .models import TestClass from .models import TestClass
from ..models import HumanSortField from ..models import HumanSortField
from ..models import activitycontextimpl
class MethodCacheTestCase(TestCase): class MethodCacheTestCase(TestCase):
...@@ -80,3 +81,22 @@ class TestHumanSortField(TestCase): ...@@ -80,3 +81,22 @@ class TestHumanSortField(TestCase):
test_result = HumanSortField.get_normalized_value(obj, val) test_result = HumanSortField.get_normalized_value(obj, val)
self.assertEquals(test_result, result) self.assertEquals(test_result, result)
class ActivityContextTestCase(TestCase):
class MyException(Exception):
pass
def test_unicode(self):
act = MagicMock()
gen = activitycontextimpl(act)
gen.next()
with self.assertRaises(self.MyException):
gen.throw(self.MyException(u'test\xe1'))
def test_str(self):
act = MagicMock()
gen = activitycontextimpl(act)
gen.next()
with self.assertRaises(self.MyException):
gen.throw(self.MyException('test\xbe'))
...@@ -40,7 +40,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm ...@@ -40,7 +40,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput from django.forms.widgets import TextInput, HiddenInput
from django.template import Context from django.template import Context
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import escape from django.utils.html import escape, format_html
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from sizefield.widgets import FileSizeWidget from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
...@@ -90,9 +90,15 @@ class VmSaveForm(OperationForm): ...@@ -90,9 +90,15 @@ class VmSaveForm(OperationForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
default = kwargs.pop('default', None) default = kwargs.pop('default', None)
clone = kwargs.pop('clone', False)
super(VmSaveForm, self).__init__(*args, **kwargs) super(VmSaveForm, self).__init__(*args, **kwargs)
if default: if default:
self.fields['name'].initial = default self.fields['name'].initial = default
if clone:
self.fields.insert(2, "clone", forms.BooleanField(
required=False, label=_("Clone template permissions"),
help_text=_("Clone the access list of parent template. Useful "
"for updating a template.")))
class VmCustomizeForm(forms.Form): class VmCustomizeForm(forms.Form):
...@@ -929,6 +935,56 @@ class VmDeployForm(OperationForm): ...@@ -929,6 +935,56 @@ class VmDeployForm(OperationForm):
"(blank allows scheduling automatically)."))) "(blank allows scheduling automatically).")))
class VmPortRemoveForm(OperationForm):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.rule = kwargs.pop('default')
super(VmPortRemoveForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'rule', forms.ModelChoiceField(
queryset=choices, initial=self.rule, required=True,
empty_label=None, label=_('Port')))
if self.rule:
self.fields['rule'].widget = HiddenInput()
class VmPortAddForm(OperationForm):
port = forms.IntegerField(required=True, label=_('Port'),
min_value=1, max_value=65535)
proto = forms.ChoiceField((('tcp', 'tcp'), ('udp', 'udp')),
required=True, label=_('Protocol'))
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.host = kwargs.pop('default')
super(VmPortAddForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'host', forms.ModelChoiceField(
queryset=choices, initial=self.host, required=True,
empty_label=None, label=_('Host')))
if self.host:
self.fields['host'].widget = HiddenInput()
@property
def helper(self):
helper = super(VmPortAddForm, self).helper
if self.host:
helper.layout = Layout(
AnyTag(
"div",
HTML(format_html(
_("<label>Host:</label> {0}"), self.host)),
css_class="form-group",
),
Field("host"),
Field("proto"),
Field("port"),
)
return helper
class CircleAuthenticationForm(AuthenticationForm): class CircleAuthenticationForm(AuthenticationForm):
# fields: username, password # fields: username, password
......
/* ===========================================================
# bootstrap-tour - v0.9.1
# http://bootstraptour.com
# ==============================================================
# Copyright 2012-2013 Ulrich Sossou
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
*/
.tour-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1100;background-color:#000;opacity:.8}.tour-step-backdrop{position:relative;z-index:1101;background:inherit}.tour-step-background{position:absolute;z-index:1100;background:inherit;border-radius:6px}.popover[class*=tour-]{z-index:1100}.popover[class*=tour-] .popover-navigation{padding:9px 14px}.popover[class*=tour-] .popover-navigation [data-role=end]{float:right}.popover[class*=tour-] .popover-navigation [data-role=prev],.popover[class*=tour-] .popover-navigation [data-role=next],.popover[class*=tour-] .popover-navigation [data-role=end]{cursor:pointer}.popover[class*=tour-] .popover-navigation [data-role=prev].disabled,.popover[class*=tour-] .popover-navigation [data-role=next].disabled,.popover[class*=tour-] .popover-navigation [data-role=end].disabled{cursor:default}.popover[class*=tour-].orphan{position:fixed;margin-top:0}.popover[class*=tour-].orphan .arrow{display:none}
\ No newline at end of file
...@@ -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 {
...@@ -688,7 +688,7 @@ textarea[name="new_members"] { ...@@ -688,7 +688,7 @@ textarea[name="new_members"] {
.dashboard-vm-details-connect-command { .dashboard-vm-details-connect-command {
/* for mobile view */ /* for mobile view */
margin-bottom: 20px; padding-bottom: 20px;
} }
#store-list-list { #store-list-list {
...@@ -961,6 +961,14 @@ textarea[name="new_members"] { ...@@ -961,6 +961,14 @@ textarea[name="new_members"] {
cursor: pointer cursor: pointer
} }
#vm-details-resources-disk {
padding: 2px 5px 10px 5px;
}
#vm-details-start-template-tour {
margin-right: 5px;
}
#vm-activity-state { #vm-activity-state {
margin-bottom: 15px; margin-bottom: 15px;
} }
...@@ -974,6 +982,24 @@ textarea[name="new_members"] { ...@@ -974,6 +982,24 @@ textarea[name="new_members"] {
color: orange; color: orange;
} }
.introjs-skipbutton {
color: #333;
}
.introjs-button:focus {
text-decoration: none;
color: #333;
outline: none;
}
.introjs-button:hover:not(.introjs-disabled) {
color: #428bca;
}
.introjs-tooltip {
min-width: 250px;
}
#vm-info-pane { #vm-info-pane {
margin-bottom: 20px; margin-bottom: 20px;
} }
...@@ -1016,3 +1042,12 @@ textarea[name="new_members"] { ...@@ -1016,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 */
......
.introjs-overlay{position:absolute;z-index:999999;background-color:#000;opacity:0;background:-moz-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:-webkit-gradient(radial,center center,0px,center center,100%,color-stop(0%,rgba(0,0,0,0.4)),color-stop(100%,rgba(0,0,0,0.9)));background:-webkit-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:-o-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:-ms-radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);background:radial-gradient(center,ellipse cover,rgba(0,0,0,0.4) 0,rgba(0,0,0,0.9) 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#66000000',endColorstr='#e6000000',GradientType=1);-ms-filter:"alpha(opacity=50)";filter:alpha(opacity=50);-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-fixParent{z-index:auto !important;opacity:1.0 !important}.introjs-showElement,tr.introjs-showElement>td,tr.introjs-showElement>th{z-index:9999999 !important}.introjs-relativePosition,tr.introjs-showElement>td,tr.introjs-showElement>th{position:relative}.introjs-helperLayer{position:absolute;z-index:9999998;background-color:#FFF;background-color:rgba(255,255,255,.9);border:1px solid #777;border:1px solid rgba(0,0,0,.5);border-radius:4px;box-shadow:0 2px 15px rgba(0,0,0,.4);-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-helperNumberLayer{position:absolute;top:-16px;left:-16px;z-index:9999999999 !important;padding:2px;font-family:Arial,verdana,tahoma;font-size:13px;font-weight:bold;color:white;text-align:center;text-shadow:1px 1px 1px rgba(0,0,0,.3);background:#ff3019;background:-webkit-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ff3019),color-stop(100%,#cf0404));background:-moz-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-ms-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-o-linear-gradient(top,#ff3019 0,#cf0404 100%);background:linear-gradient(to bottom,#ff3019 0,#cf0404 100%);width:20px;height:20px;line-height:20px;border:3px solid white;border-radius:50%;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3019',endColorstr='#cf0404',GradientType=0);filter:progid:DXImageTransform.Microsoft.Shadow(direction=135,strength=2,color=ff0000);box-shadow:0 2px 5px rgba(0,0,0,.4)}.introjs-arrow{border:5px solid white;content:'';position:absolute}.introjs-arrow.top{top:-10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.top-right{top:-10px;right:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.top-middle{top:-10px;left:50%;margin-left:-5px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.right{right:-10px;top:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent;border-left-color:white}.introjs-arrow.bottom{bottom:-10px;border-top-color:white;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.introjs-arrow.left{left:-10px;top:10px;border-top-color:transparent;border-right-color:white;border-bottom-color:transparent;border-left-color:transparent}.introjs-tooltip{position:absolute;padding:10px;background-color:white;min-width:200px;max-width:300px;border-radius:3px;box-shadow:0 1px 10px rgba(0,0,0,.4);-webkit-transition:opacity .1s ease-out;-moz-transition:opacity .1s ease-out;-ms-transition:opacity .1s ease-out;-o-transition:opacity .1s ease-out;transition:opacity .1s ease-out}.introjs-tooltipbuttons{text-align:right}.introjs-button{position:relative;overflow:visible;display:inline-block;padding:.3em .8em;border:1px solid #d4d4d4;margin:0;text-decoration:none;text-shadow:1px 1px 0 #fff;font:11px/normal sans-serif;color:#333;white-space:nowrap;cursor:pointer;outline:0;background-color:#ececec;background-image:-webkit-gradient(linear,0 0,0 100%,from(#f4f4f4),to(#ececec));background-image:-moz-linear-gradient(#f4f4f4,#ececec);background-image:-o-linear-gradient(#f4f4f4,#ececec);background-image:linear-gradient(#f4f4f4,#ececec);-webkit-background-clip:padding;-moz-background-clip:padding;-o-background-clip:padding-box;-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;zoom:1;*display:inline;margin-top:10px}.introjs-button:hover{border-color:#bcbcbc;text-decoration:none;box-shadow:0 1px 1px #e3e3e3}.introjs-button:focus,.introjs-button:active{background-image:-webkit-gradient(linear,0 0,0 100%,from(#ececec),to(#f4f4f4));background-image:-moz-linear-gradient(#ececec,#f4f4f4);background-image:-o-linear-gradient(#ececec,#f4f4f4);background-image:linear-gradient(#ececec,#f4f4f4)}.introjs-button::-moz-focus-inner{padding:0;border:0}.introjs-skipbutton{margin-right:5px;color:#7a7a7a}.introjs-prevbutton{-webkit-border-radius:.2em 0 0 .2em;-moz-border-radius:.2em 0 0 .2em;border-radius:.2em 0 0 .2em;border-right:0}.introjs-nextbutton{-webkit-border-radius:0 .2em .2em 0;-moz-border-radius:0 .2em .2em 0;border-radius:0 .2em .2em 0}.introjs-disabled,.introjs-disabled:hover,.introjs-disabled:focus{color:#9a9a9a;border-color:#d4d4d4;box-shadow:none;cursor:default;background-color:#f4f4f4;background-image:none;text-decoration:none}.introjs-bullets{text-align:center}.introjs-bullets ul{clear:both;margin:15px auto 0;padding:0;display:inline-block}.introjs-bullets ul li{list-style:none;float:left;margin:0 2px}.introjs-bullets ul li a{display:block;width:6px;height:6px;background:#ccc;border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px;text-decoration:none}.introjs-bullets ul li a:hover{background:#999}.introjs-bullets ul li a.active{background:#999}.introjsFloatingElement{position:absolute;height:0;width:0;left:50%;top:50%}
.introjs-helperLayer *,
.introjs-helperLayer *:before,
.introjs-helperLayer *:after {
-webkit-box-sizing: content-box;
-moz-box-sizing: content-box;
-ms-box-sizing: content-box;
-o-box-sizing: content-box;
box-sizing: content-box;
}
...@@ -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();
......
...@@ -2,7 +2,7 @@ $(function() { ...@@ -2,7 +2,7 @@ $(function() {
/* for template removes buttons */ /* for template removes buttons */
$('.template-delete').click(function() { $('.template-delete').click(function() {
var template_pk = $(this).data('template-pk'); var template_pk = $(this).data('template-pk');
addModalConfirmation(deleteTemplate, addModalConfirmationOrDisplayMessage(deleteTemplate,
{ 'url': '/dashboard/template/delete/' + template_pk + '/', { 'url': '/dashboard/template/delete/' + template_pk + '/',
'data': [], 'data': [],
'template_pk': template_pk, 'template_pk': template_pk,
...@@ -13,7 +13,7 @@ $(function() { ...@@ -13,7 +13,7 @@ $(function() {
/* for lease removes buttons */ /* for lease removes buttons */
$('.lease-delete').click(function() { $('.lease-delete').click(function() {
var lease_pk = $(this).data('lease-pk'); var lease_pk = $(this).data('lease-pk');
addModalConfirmation(deleteLease, addModalConfirmationOrDisplayMessage(deleteLease,
{ 'url': '/dashboard/lease/delete/' + lease_pk + '/', { 'url': '/dashboard/lease/delete/' + lease_pk + '/',
'data': [], 'data': [],
'lease_pk': lease_pk, 'lease_pk': lease_pk,
...@@ -81,3 +81,29 @@ function deleteLease(data) { ...@@ -81,3 +81,29 @@ function deleteLease(data) {
} }
}); });
} }
function addModalConfirmationOrDisplayMessage(func, data) {
$.ajax({
type: 'GET',
url: data['url'],
data: jQuery.param(data['data']),
success: function(result) {
$('body').append(result);
$('#confirmation-modal').modal('show');
$('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove();
});
$('#confirmation-modal-button').click(function() {
func(data);
$('#confirmation-modal').modal('hide');
});
},
error: function(xhr, textStatus, error) {
if(xhr.status === 403) {
addMessage(gettext("Only the owners can delete the selected object."), "warning");
} else {
addMessage(gettext("An error occurred. (") + xhr.status + ")", 'danger')
}
}
});
}
...@@ -19,7 +19,7 @@ $(function() { ...@@ -19,7 +19,7 @@ $(function() {
$('#vm-migrate-node-list li input:checked').closest('li').addClass('panel-primary'); $('#vm-migrate-node-list li input:checked').closest('li').addClass('panel-primary');
} }
}); });
return false; e.preventDefault();
}); });
/* if the operation fails show the modal again */ /* if the operation fails show the modal again */
......
...@@ -28,7 +28,7 @@ $(function() { ...@@ -28,7 +28,7 @@ $(function() {
}); });
/* save resources */ /* save resources */
$('#vm-details-resources-save').click(function() { $('#vm-details-resources-save').click(function(e) {
var error = false; var error = false;
$(".cpu-count-input, .ram-input").each(function() { $(".cpu-count-input, .ram-input").each(function() {
if(!$(this)[0].checkValidity()) { if(!$(this)[0].checkValidity()) {
...@@ -61,7 +61,7 @@ $(function() { ...@@ -61,7 +61,7 @@ $(function() {
} }
} }
}); });
return false; e.preventDefault();
}); });
/* remove tag */ /* remove tag */
...@@ -205,11 +205,11 @@ $(function() { ...@@ -205,11 +205,11 @@ $(function() {
}); });
/* rename in home tab */ /* rename in home tab */
$(".vm-details-home-edit-name-click").click(function() { $(".vm-details-home-edit-name-click").click(function(e) {
$(".vm-details-home-edit-name-click").hide(); $(".vm-details-home-edit-name-click").hide();
$("#vm-details-home-rename").show(); $("#vm-details-home-rename").show();
$("input", $("#vm-details-home-rename")).select(); $("input", $("#vm-details-home-rename")).select();
return false; e.preventDefault();
}); });
/* rename ajax */ /* rename ajax */
...@@ -236,7 +236,7 @@ $(function() { ...@@ -236,7 +236,7 @@ $(function() {
}); });
/* update description click */ /* update description click */
$(".vm-details-home-edit-description-click").click(function() { $(".vm-details-home-edit-description-click").click(function(e) {
$(".vm-details-home-edit-description-click").hide(); $(".vm-details-home-edit-description-click").hide();
$("#vm-details-home-description").show(); $("#vm-details-home-description").show();
var ta = $("#vm-details-home-description textarea"); var ta = $("#vm-details-home-description textarea");
...@@ -244,7 +244,7 @@ $(function() { ...@@ -244,7 +244,7 @@ $(function() {
ta.val(""); ta.val("");
ta.focus(); ta.focus();
ta.val(tmp) ta.val(tmp)
return false; e.preventDefault();
}); });
/* description update ajax */ /* description update ajax */
...@@ -316,6 +316,24 @@ $(function() { ...@@ -316,6 +316,24 @@ $(function() {
if(Boolean($(this).data("disabled"))) return false; if(Boolean($(this).data("disabled"))) return false;
}); });
$("#dashboard-tutorial-toggle").click(function() {
var box = $("#alert-new-template");
var list = box.find("ol")
list.stop().slideToggle(function() {
var url = box.find("form").prop("action");
var hidden = list.css("display") === "none";
box.find("button i").prop("class", "fa fa-caret-" + (hidden ? "down" : "up"));
$.ajax({
type: 'POST',
url: url,
data: {'hidden': hidden},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {}
});
});
return false;
});
}); });
...@@ -344,9 +362,6 @@ function decideActivityRefresh() { ...@@ -344,9 +362,6 @@ function decideActivityRefresh() {
/* if something is still spinning */ /* if something is still spinning */
if($('.timeline .activity i').hasClass('fa-spin')) if($('.timeline .activity i').hasClass('fa-spin'))
check = true; check = true;
/* if there is only one activity */
if($('#activity-timeline div[class="activity"]').length < 2)
check = true;
return check; return check;
} }
...@@ -377,6 +392,7 @@ function checkNewActivity(runs) { ...@@ -377,6 +392,7 @@ function checkNewActivity(runs) {
} else { } else {
icon.prop("class", "fa " + data['icon']); icon.prop("class", "fa " + data['icon']);
} }
$("#vm-details-state").data("status", data['status']);
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase()); $("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
if(data['status'] == "RUNNING") { if(data['status'] == "RUNNING") {
if(data['connect_uri']) { if(data['connect_uri']) {
......
...@@ -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" %}'),
......
{% extends "dashboard/operate.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block formfields %}
{% if form %}
{% crispy form %}
{% endif %}
{% if form.fields.rule.initial != None %}
{% with rule=form.fields.rule.initial %}
<dl>
<dt>{% trans "Port" %}:</dt>
<dd>{{ rule.dport }}/{{ rule.proto }}</dd>
<dt>{% trans "Host" %}:</dt>
<dd>{{ rule.host.hostname }}</dd>
<dt>{% trans "Vlan" %}:</dt>
<dd>{{ rule.host.vlan.name }}</dd>
</dl>
{% endwith %}
{% endif %}
{% endblock %}
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
{% block extra_link %} {% block extra_link %}
<link rel="stylesheet" href="{{ STATIC_URL }}dashboard/loopj-jquery-simple-slider/css/simple-slider.css"/> <link rel="stylesheet" href="{{ STATIC_URL }}dashboard/loopj-jquery-simple-slider/css/simple-slider.css"/>
<link href="{{ STATIC_URL }}dashboard/bootstrap-tour.min.css" rel="stylesheet"> <link rel="stylesheet" href="{{ STATIC_URL }}dashboard/introjs/introjs.min.css">
<link href="{{ STATIC_URL }}dashboard/dashboard.css" rel="stylesheet"> <link href="{{ STATIC_URL }}dashboard/dashboard.css" rel="stylesheet">
{% endblock %} {% endblock %}
......
{% 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 %}
{% extends "dashboard/base.html" %} {% extends "dashboard/base.html" %}
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
{% load i18n %} {% load i18n %}
{% load static %}
{% block title-page %}{{ group.name }} | {% trans "group" %}{% endblock %} {% block title-page %}{{ group.name }} | {% trans "group" %}{% endblock %}
...@@ -8,9 +9,15 @@ ...@@ -8,9 +9,15 @@
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
<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 group-details-rename-button"><i class="fa fa-pencil"></i></a> <a title="{% trans "Rename" %}" href="#" class="btn btn-default btn-xs group-details-rename-button">
<a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}"><i class="fa fa-trash-o"></i></a> <i class="fa fa-pencil"></i>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs group-details-help-button"><i class="fa fa-question"></i></a> </a>
<a title="{% trans "Delete" %}" data-group-pk="{{ group.pk }}" class="btn btn-default btn-xs real-link group-delete" href="{% url "dashboard.views.delete-group" pk=group.pk %}">
<i class="fa fa-trash-o"></i>
</a>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs group-details-help-button">
<i class="fa fa-question"></i>
</a>
</div> </div>
<h1> <h1>
<div id="group-details-rename"> <div id="group-details-rename">
...@@ -39,21 +46,18 @@ ...@@ -39,21 +46,18 @@
</li> </li>
</ul> </ul>
</div> </div>
</div> </div><!-- .page-header -->
<div class="row"> <div class="row">
<div class="col-md-12" id="group-detail-pane"> <div class="col-md-12" id="group-detail-pane">
<div class="panel panel-default" id="group-detail-panel"> <div class="panel panel-default panel-body" id="group-detail-panel">
<div class="tab-content panel-body" id="group-form-body">
<form method="POST" action="{% url "dashboard.views.group-update" pk=group.pk %}"> <form method="POST" action="{% url "dashboard.views.group-update" pk=group.pk %}">
{% csrf_token %} {% csrf_token %}
{% crispy group_profile_form %} {% crispy group_profile_form %}
</form> </form>
<hr />
<hr />
<h3>{% trans "Available objects for this group" %}</h3> <h3>{% trans "Available objects for this group" %}</h3>
<ul class="dashboard-profile-vm-list fa-ul"> <ul class="dashboard-profile-vm-list fa-ul">
{% for i in vm_objects %} {% for i in vm_objects %}
<li> <li>
<a href="{{ i.get_absolute_url }}"> <a href="{{ i.get_absolute_url }}">
...@@ -78,17 +82,19 @@ ...@@ -78,17 +82,19 @@
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<hr />
<hr />
<h3>{% trans "User list"|capfirst %} <h3>
{% trans "User list" %}
{% if perms.auth.add_user %} {% if perms.auth.add_user %}
<a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">{% trans "Create user" %}</a> <a href="{% url "dashboard.views.create-user" group.pk %}" class="btn btn-success pull-right">
{% trans "Create user" %}
</a>
{% endif %} {% endif %}
</h3> </h3>
<form action="" method="post">{% csrf_token %} <form action="" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields table-bordered" id="group-detail-user-table"> <table class="table table-striped table-with-form-fields table-bordered" id="group-detail-user-table">
<tbody> <tbody>
<thead><tr><th></th><th>{% trans "Who" %}</th><th>{% trans "Remove" %}</th></tr></thead> <thead><tr><th></th><th>{% trans "Who" %}</th><th>{% trans "Remove" %}</th></tr></thead>
{% for i in users %} {% for i in users %}
...@@ -101,7 +107,9 @@ ...@@ -101,7 +107,9 @@
>{% include "dashboard/_display-name.html" with user=i show_org=True %}</a> >{% include "dashboard/_display-name.html" with user=i show_org=True %}</a>
</td> </td>
<td> <td>
<a data-group_pk="{{ group.pk }}" data-member_pk="{{i.pk}}" href="{% url "dashboard.views.remove-user" member_pk=i.pk group_pk=group.pk %}" class="real-link delete-from-group btn btn-link btn-xs"><i class="fa fa-times"><span class="sr-only">{% trans "remove" %}</span></i></a> <a data-group_pk="{{ group.pk }}" data-member_pk="{{i.pk}}" href="{% url "dashboard.views.remove-user" member_pk=i.pk group_pk=group.pk %}" class="real-link delete-from-group btn btn-link btn-xs"><i class="fa fa-times">
<span class="sr-only">{% trans "remove" %}</span></i>
</a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
...@@ -132,29 +140,31 @@ ...@@ -132,29 +140,31 @@
<button type="submit" class="btn btn-success">{% trans "Save" %}</button> <button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div> </div>
</form> </form>
<hr />
<h3 id="group-detail-perm-header">{% trans "Access permissions"|capfirst %}</h3>
{% include "dashboard/_manage_access.html" with table_id="group-detail-perm-table" %}
{% if user.is_superuser %}
<hr /> <hr />
<h3 id="group-detail-perm-header">{% trans "Access permissions" %}</h3>
{% include "dashboard/_manage_access.html" with table_id="group-detail-perm-table" %}
<script type="text/javascript" src="/static/admin/js/jquery.min.js"></script> {% if user.is_superuser %}
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script> <hr />
{{ group_perm_form.media }}
<h3>{% trans "Group permissions" %}</h3> <script type="text/javascript" src="/static/admin/js/jquery.min.js"></script>
<script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
{{ group_perm_form.media }}
<div id="group-detail-permissions"> <h3>{% trans "Group permissions" %}</h3>
{% crispy group_perm_form %}
</div>
<link rel="stylesheet" type="text/css" href="/static/admin/css/widgets.css" /> <div id="group-detail-permissions">
{% crispy group_perm_form %}
{% endif %}
</div> </div>
<link rel="stylesheet" type="text/css" href="/static/admin/css/widgets.css" />
{% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block extra_js %}
<script type="text/javascript" src="{% static "dashboard/group-details.js" %}"></script>
{% endblock %}
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,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-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">
......
...@@ -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>
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
{% 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>
...@@ -13,6 +14,7 @@ ...@@ -13,6 +14,7 @@
<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>
{% endif %}
<h1> <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">
......
...@@ -16,16 +16,19 @@ ...@@ -16,16 +16,19 @@
{% endif %} {% endif %}
</div> </div>
{% load crispy_forms_tags %} {% load crispy_forms_tags %}
<style> <style>
.row { .row {
margin-bottom: 15px; margin-bottom: 15px;
} }
</style> </style>
{% if request.user.is_superuser %}
<form action="{% url "dashboard.views.node-addtrait" node.pk %}" method="POST"> <form action="{% url "dashboard.views.node-addtrait" node.pk %}" method="POST">
{% csrf_token %} {% csrf_token %}
{% crispy trait_form %} {% crispy trait_form %}
</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>
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<div class="list-group-item"> <div class="list-group-item">
<div class="row"> <div class="row">
<div class="col-sm-6"> <div class="col-sm-6">
<a href="{% url "dashboard.views.store-upload"%}?directory={{ current }}" <a href="{% url "dashboard.views.store-upload"%}?directory={{ current|urlencode }}"
class="btn btn-info btn-xs js-hidden"> class="btn btn-info btn-xs js-hidden">
{% trans "Upload" %} {% trans "Upload" %}
</a> </a>
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
method="POST" enctype="multipart/form-data" class="no-js-hidden" method="POST" enctype="multipart/form-data" class="no-js-hidden"
id="store-upload-form"> id="store-upload-form">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="current_dir" value="{{ current }}"/> <input type="hidden" name="current_dir" value="{{ current|urlencode }}"/>
<input type="hidden" name="next" value="{{ next_url }}"/> <input type="hidden" name="next" value="{{ next_url }}"/>
<div class="input-group" style="max-width: 350px;"> <div class="input-group" style="max-width: 350px;">
<span class="input-group-btn" id="store-upload-browse"> <span class="input-group-btn" id="store-upload-browse">
...@@ -33,12 +33,12 @@ ...@@ -33,12 +33,12 @@
</div><!-- .col-sm-6 upload --> </div><!-- .col-sm-6 upload -->
<div class="col-sm-6"> <div class="col-sm-6">
<a href="{% url "dashboard.views.store-remove" %}?path={{ current }}" <a href="{% url "dashboard.views.store-remove" %}?path={{ current|urlencode }}"
class="btn btn-danger btn-xs pull-right store-action-button" class="btn btn-danger btn-xs pull-right store-action-button"
title="{% trans "Remove directory" %}"> title="{% trans "Remove directory" %}">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
</a> </a>
<a href="{% url "dashboard.views.store-download" %}?path={{ current }}" <a href="{% url "dashboard.views.store-download" %}?path={{ current|urlencode }}"
class="btn btn-primary btn-xs pull-right store-action-button" class="btn btn-primary btn-xs pull-right store-action-button"
title="{% trans "Download directory" %}"> title="{% trans "Download directory" %}">
<i class="fa fa-cloud-download"></i> <i class="fa fa-cloud-download"></i>
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
</div><!-- .list-group --> </div><!-- .list-group -->
<div class="list-group" id="store-list-list"> <div class="list-group" id="store-list-list">
<a href="{% url "dashboard.views.store-list" %}?directory={{ up_url }}" <a href="{% url "dashboard.views.store-list" %}?directory={{ up_url|urlencode }}"
class="list-group-item store-list-item" data-item-type="D"> class="list-group-item store-list-item" data-item-type="D">
{% if current == "/" %} {% if current == "/" %}
<div class="store-list-item-icon"> <div class="store-list-item-icon">
...@@ -85,8 +85,8 @@ ...@@ -85,8 +85,8 @@
{% for f in root %} {% for f in root %}
<a class="list-group-item store-list-item" data-item-type="{{ f.TYPE }}" <a class="list-group-item store-list-item" data-item-type="{{ f.TYPE }}"
href="{% if f.TYPE == "D" %}{% url "dashboard.views.store-list" %}?directory={{ f.path }}{% else %} href="{% if f.TYPE == "D" %}{% url "dashboard.views.store-list" %}?directory={{ f.path|urlencode }}{% else %}
{% url "dashboard.views.store-download" %}?path={{ f.path }}{% endif %}" {% url "dashboard.views.store-download" %}?path={{ f.path|urlencode }}{% endif %}"
> >
<div class="store-list-item-icon"> <div class="store-list-item-icon">
<i class=" <i class="
...@@ -122,12 +122,12 @@ ...@@ -122,12 +122,12 @@
</dl> </dl>
</div> </div>
<div class="col-sm-2" style="text-align: right;"> <div class="col-sm-2" style="text-align: right;">
<a href="{% url "dashboard.views.store-download" %}?path={{ f.path }}" <a href="{% url "dashboard.views.store-download" %}?path={{ f.path|urlencode }}"
class="btn btn-primary btn-sm store-download-button"> class="btn btn-primary btn-sm store-download-button">
<i class="fa fa-download"></i> <i class="fa fa-download"></i>
{% trans "Download" %} {% trans "Download" %}
</a> </a>
<a href="{% url "dashboard.views.store-remove" %}?path={{ f.path }}" <a href="{% url "dashboard.views.store-remove" %}?path={{ f.path|urlencode }}"
class="btn btn-danger btn-xs store-remove-button"> class="btn btn-danger btn-xs store-remove-button">
<i class="fa fa-times"></i> <i class="fa fa-times"></i>
{% trans "Remove" %} {% trans "Remove" %}
......
...@@ -11,7 +11,9 @@ ...@@ -11,7 +11,9 @@
<div class="col-md-7"> <div class="col-md-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-puzzle-piece"></i> {% trans "Edit template" %}</h3> <h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Edit template" %}</h3>
</div> </div>
<div class="panel-body"> <div class="panel-body">
...@@ -65,6 +67,38 @@ ...@@ -65,6 +67,38 @@
</div> </div>
<div class="col-md-5"> <div class="col-md-5">
{% if is_owner %}
<div class="panel panel-default">
<div class="panel-heading">
<a href="{% url "dashboard.views.template-delete" pk=object.pk %}"
class="btn btn-xs btn-danger pull-right">
{% trans "Delete" %}
</a>
<h4 class="no-margin"><i class="fa fa-times"></i> {% trans "Delete template" %}</h4>
</div>
</div>
{% endif %}
<div class="panel panel-default">
<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 panel-default">
<div class="panel-heading"> <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>
......
{% 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,14 +6,25 @@ ...@@ -6,14 +6,25 @@
{% block content %} {% block content %}
{% if instance.is_base %} {% if instance.is_base %}
<div class="alert alert-info alert-new-template"> <div class="alert alert-info alert-new-template" id="alert-new-template" style="position: relative;">
<strong>{% trans "This is the master vm of your new template" %}</strong> <form action="{% url "dashboard.views.vm-toggle-tutorial" pk=instance.pk %}"
<div id="vm-details-template-tour-button" class="pull-right"> method="POST">
<a href="#" class="btn btn-default btn-lg pull-right vm-details-start-template-tour"> {% csrf_token %}
<input name="hidden" type="hidden"
value="{{ hide_tutorial|yesno:"false,true" }}"/>
<button type="submit"
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>
{% trans "Toggle tutorial panel" %}
</button>
<a href="#" class="btn btn-default btn-sm pull-right"
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>
</div> </form>
<ol> <strong>{% trans "This is the master vm of your new template" %}</strong>
<ol {% if hide_tutorial %}style="display: none;"{% endif %}>
<li>{% trans "Modify the virtual machine to suit your needs <strong>(optional)</strong>" %} <li>{% trans "Modify the virtual machine to suit your needs <strong>(optional)</strong>" %}
<ul> <ul>
<li>{% trans "Change the description" %}</li> <li>{% trans "Change the description" %}</li>
...@@ -65,7 +76,7 @@ ...@@ -65,7 +76,7 @@
<div class="row"> <div class="row">
<div class="col-md-4" id="vm-info-pane"> <div class="col-md-4" id="vm-info-pane">
<div class="big"> <div class="big">
<span id="vm-details-state" class="label label-success"> <span id="vm-details-state" class="label label-success" data-status="{{ instance.status }}">
<i class="fa <i class="fa
{% if is_new_state %} {% if is_new_state %}
fa-spinner fa-spin fa-spinner fa-spin
...@@ -110,7 +121,14 @@ ...@@ -110,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>
...@@ -202,7 +220,7 @@ ...@@ -202,7 +220,7 @@
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
<script src="{{ STATIC_URL }}dashboard/bootstrap-tour.min.js"></script> <script src="{{ STATIC_URL }}dashboard/introjs/intro.min.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-details.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-details.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-common.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-common.js"></script>
<script src="{{ STATIC_URL }}dashboard/vm-console.js"></script> <script src="{{ STATIC_URL }}dashboard/vm-console.js"></script>
......
...@@ -3,7 +3,8 @@ ...@@ -3,7 +3,8 @@
<div id="activity-timeline" class="timeline"> <div id="activity-timeline" class="timeline">
{% for a in activities %} {% for a in activities %}
<div class="activity{% if a.pk == active.pk %} activity-active{%endif%}" data-activity-id="{{ a.pk }}"> <div class="activity{% if a.pk == active.pk %} activity-active{%endif%}"
data-activity-id="{{ a.pk }}" data-activity-code="{{ a.activity_code }}">
<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>
...@@ -33,7 +34,8 @@ ...@@ -33,7 +34,8 @@
{% if a.children.count > 0 %} {% if a.children.count > 0 %}
<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 }}" data-activity-code="{{ s.activity_code }}"
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_text:user }}"{% 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_text:user|capfirst }}</a></span> &ndash; {{ s.readable_name|get_text:user|capfirst }}</a></span> &ndash;
......
{% load i18n %} {% load i18n %}
<div class="vm-details-network-port-add pull-right"> <div class="vm-details-network-port-add pull-right">
<form action="" method="POST"> <form action="{{ op.add_port.get_url }}" method="POST">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="host_pk" value="{{ i.host.pk }}"/> <input type="hidden" name="host" value="{{ i.host.pk }}"/>
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<span class="input-group-addon"> <span class="input-group-addon">
<i class="fa fa-plus"></i> <i class="fa fa-long-arrow-right"></i> <i class="fa fa-plus"></i> <i class="fa fa-long-arrow-right"></i>
</span> </span>
<input type="text" class="form-control" size="5" style="width: 80px;" name="port"/> <input type="number" class="form-control" size="5" min="1" max="65535"
style="width: 80px;" name="port" required/>
<span class="input-group-addon">/</span> <span class="input-group-addon">/</span>
<select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select> <select class="form-control" name="proto" style="width: 70px;"><option>tcp</option><option>udp</option></select>
<div class="input-group-btn"> <div class="input-group-btn">
......
...@@ -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 %}
......
{% load i18n %} {% load i18n %}
<div class="row"> <div class="row">
<div class="col-md-4"> <div class="col-md-4">
<dl> <dl id="home_name_and_description">
<dt>{% trans "System" %}:</dt> <dt>{% trans "System" %}:</dt>
<dd><i class="fa fa-{{ os_type_icon }}"></i> {{ instance.system }}</dd> <dd><i class="fa fa-{{ os_type_icon }}"></i> {{ instance.system }}</dd>
<dt style="margin-top: 5px;"> <dt style="margin-top: 5px;">
...@@ -52,7 +52,10 @@ ...@@ -52,7 +52,10 @@
</dd> </dd>
</dl> </dl>
<h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %} <div id="home_expiration_and_lease">
<h4>
{% trans "Expiration" %}
{% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %}
<span id="vm-details-renew-op"> <span id="vm-details-renew-op">
{% with op=op.renew %}{% if op %} {% with op=op.renew %}{% if op %}
<a href="{{op.get_url}}" class="btn btn-success btn-xs <a href="{{op.get_url}}" class="btn btn-success btn-xs
...@@ -76,6 +79,7 @@ ...@@ -76,6 +79,7 @@
</span> </span>
</dd> </dd>
</dl> </dl>
</div>
<div style="font-weight: bold;">{% trans "Tags" %}</div> <div style="font-weight: bold;">{% trans "Tags" %}</div>
<div id="vm-details-tags" style="margin-bottom: 20px;"> <div id="vm-details-tags" style="margin-bottom: 20px;">
......
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
{{ l.private }}/{{ l.proto }} {{ l.private }}/{{ l.proto }}
</td> </td>
<td> <td>
<a href="{% url "dashboard.views.remove-port" pk=instance.pk rule=l.ipv4.pk %}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv4.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a> <a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv4.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
...@@ -110,7 +110,7 @@ ...@@ -110,7 +110,7 @@
{{ l.private }}/{{ l.proto }} {{ l.private }}/{{ l.proto }}
</td> </td>
<td> <td>
<a href="{% url "dashboard.views.remove-port" pk=instance.pk rule=l.ipv4.pk %}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a> <a href="{{ op.remove_port.get_url }}?rule={{ l.ipv4.pk }}" class="btn btn-link btn-xs vm-details-remove-port" data-rule="{{ l.ipv6.pk }}" title="{% trans "Remove" %}"><i class="fa fa-times"><span class="sr-only">{% trans "Remove" %}</span></i></a>
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
......
...@@ -18,12 +18,9 @@ ...@@ -18,12 +18,9 @@
{% endif %} {% endif %}
</form> </form>
<hr /> <hr />
<div id="vm-details-resources-disk">
<div class="row" id="vm-details-resources-disk">
<div class="col-sm-11">
<h3> <h3>
{% trans "Disks" %} {% trans "Disks" %}
<div class="pull-right"> <div class="pull-right">
...@@ -33,10 +30,8 @@ ...@@ -33,10 +30,8 @@
</div> </div>
</h3> </h3>
<div class="row" id="vm-details-disk-add-for-form"></div>
{% if not instance.disks.all %} {% if not instance.disks.all %}
{% trans "No disks are added!" %} {% trans "No disks are added." %}
{% endif %} {% endif %}
{% for d in instance.disks.all %} {% for d in instance.disks.all %}
<h4 class="list-group-item-heading dashboard-vm-details-network-h3"> <h4 class="list-group-item-heading dashboard-vm-details-network-h3">
...@@ -45,7 +40,6 @@ ...@@ -45,7 +40,6 @@
{% endwith %} {% endwith %}
</h4> </h4>
{% endfor %} {% endfor %}
</div>
</div> </div>
{% if user.is_superuser %} {% if user.is_superuser %}
......
...@@ -26,7 +26,8 @@ from django.contrib.auth import authenticate ...@@ -26,7 +26,8 @@ from django.contrib.auth import authenticate
from dashboard.views import VmAddInterfaceView from dashboard.views import VmAddInterfaceView
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait from vm.models import Instance, InstanceTemplate, Lease, Node, Trait
from vm.operations import WakeUpOperation, AddInterfaceOperation from vm.operations import (WakeUpOperation, AddInterfaceOperation,
AddPortOperation)
from ..models import Profile from ..models import Profile
from firewall.models import Vlan, Host, VlanGroup from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch from mock import Mock, patch
...@@ -299,7 +300,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -299,7 +300,7 @@ class VmDetailTest(LoginMixin, TestCase):
leases = Lease.objects.count() leases = Lease.objects.count()
response = c.post("/dashboard/lease/delete/1/") response = c.post("/dashboard/lease/delete/1/")
# redirect to the login page # redirect to the login page
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 403)
self.assertEqual(leases, Lease.objects.count()) self.assertEqual(leases, Lease.objects.count())
def test_notification_read(self): def test_notification_read(self):
...@@ -335,51 +336,48 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -335,51 +336,48 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
response = c.post("/dashboard/vm/1/", {'port': True, vlan = Vlan.objects.get(id=1)
'proto': 'tcp', vlan.set_level(self.u2, 'user')
'port': '1337'}) inst.add_interface(user=self.u2, vlan=vlan)
host = Host.objects.get(
interface__in=inst.interface_set.all())
with patch.object(AddPortOperation, 'async') as mock_method:
mock_method.side_effect = inst.add_port
response = c.post("/dashboard/vm/1/op/add_port/", {
'proto': 'tcp', 'host': host.pk, 'port': '1337'})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_unpermitted_add_port_wo_obj_levels(self): def test_unpermitted_add_port_wo_obj_levels(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.'))
response = c.post("/dashboard/vm/1/", {'port': True,
'proto': 'tcp',
'port': '1337'})
self.assertEqual(response.status_code, 403)
def test_unpermitted_add_port_w_bad_host(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner') vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user')
inst.add_interface(user=self.u2, vlan=vlan, system=True)
host = Host.objects.get(
interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get( self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.')) name='Can configure port forwards.'))
response = c.post("/dashboard/vm/1/", {'proto': 'tcp', with patch.object(AddPortOperation, 'async') as mock_method:
'host_pk': '9999', mock_method.side_effect = inst.add_port
'port': '1337'}) response = c.post("/dashboard/vm/1/op/add_port/", {
'proto': 'tcp', 'host': host.pk, 'port': '1337'})
assert not mock_method.called
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_permitted_add_port_w_unhandled_exception(self): def test_unpermitted_add_port_w_bad_host(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user')
inst.add_interface(user=self.u2, vlan=vlan)
host = Host.objects.get(
interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get( self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.')) name='Can configure port forwards.'))
port_count = len(host.list_ports()) with patch.object(AddPortOperation, 'async') as mock_method:
response = c.post("/dashboard/vm/1/", {'proto': 'tcp', mock_method.side_effect = inst.add_port
'host_pk': host.pk, response = c.post("/dashboard/vm/1/op/add_port/", {
'port': 'invalid_port'}) 'proto': 'tcp', 'host': '9999', 'port': '1337'})
self.assertEqual(response.status_code, 302) assert not mock_method.called
self.assertEqual(len(host.list_ports()), port_count) self.assertEqual(response.status_code, 200)
def test_permitted_add_port(self): def test_permitted_add_port(self):
c = Client() c = Client()
...@@ -394,9 +392,11 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -394,9 +392,11 @@ class VmDetailTest(LoginMixin, TestCase):
self.u2.user_permissions.add(Permission.objects.get( self.u2.user_permissions.add(Permission.objects.get(
name='Can configure port forwards.')) name='Can configure port forwards.'))
port_count = len(host.list_ports()) port_count = len(host.list_ports())
response = c.post("/dashboard/vm/1/", {'proto': 'tcp', with patch.object(AddPortOperation, 'async') as mock_method:
'host_pk': host.pk, mock_method.side_effect = inst.add_port
'port': '1337'}) response = c.post("/dashboard/vm/1/op/add_port/", {
'proto': 'tcp', 'host': host.pk, 'port': '1337'})
assert mock_method.called
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(len(host.list_ports()), port_count + 1) self.assertEqual(len(host.list_ports()), port_count + 1)
...@@ -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):
......
...@@ -26,9 +26,9 @@ from .views import ( ...@@ -26,9 +26,9 @@ from .views import (
InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail, InstanceActivityDetail, LeaseCreate, LeaseDelete, LeaseDetail,
MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete, MyPreferencesView, NodeAddTraitView, NodeCreate, NodeDelete,
NodeDetailView, NodeList, NodeStatus, NodeDetailView, NodeList, NodeStatus,
NotificationView, PortDelete, 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,
...@@ -45,8 +45,11 @@ from .views import ( ...@@ -45,8 +45,11 @@ from .views import (
VmTraitsUpdate, VmRawDataUpdate, VmTraitsUpdate, VmRawDataUpdate,
GroupPermissionsView, GroupPermissionsView,
LeaseAclUpdateView, LeaseAclUpdateView,
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
...@@ -77,15 +80,15 @@ urlpatterns = patterns( ...@@ -77,15 +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'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(), url(r'^template/(?P<pk>\d+)/tx/$', TransferTemplateOwnershipView.as_view(),
name='dashboard.views.remove-port'), 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(),
...@@ -99,14 +102,20 @@ urlpatterns = patterns( ...@@ -99,14 +102,20 @@ urlpatterns = patterns(
name='dashboard.views.vm-traits'), name='dashboard.views.vm-traits'),
url(r'^vm/(?P<pk>\d+)/raw_data/$', VmRawDataUpdate.as_view(), url(r'^vm/(?P<pk>\d+)/raw_data/$', VmRawDataUpdate.as_view(),
name='dashboard.views.vm-raw-data'), name='dashboard.views.vm-raw-data'),
url(r'^vm/(?P<pk>\d+)/toggle_tutorial/$', toggle_template_tutorial,
name='dashboard.views.vm-toggle-tutorial'),
url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'), url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(), url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
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 = [{
......
...@@ -23,6 +23,7 @@ from os.path import join, normpath, dirname, basename ...@@ -23,6 +23,7 @@ from os.path import join, normpath, dirname, basename
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.template.defaultfilters import urlencode
from django.core.cache import get_cache from django.core.cache import get_cache
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -55,7 +56,7 @@ class StoreList(LoginRequiredMixin, TemplateView): ...@@ -55,7 +56,7 @@ class StoreList(LoginRequiredMixin, TemplateView):
context['current'] = directory context['current'] = directory
context['next_url'] = "%s%s?directory=%s" % ( context['next_url'] = "%s%s?directory=%s" % (
settings.DJANGO_URL.rstrip("/"), settings.DJANGO_URL.rstrip("/"),
reverse("dashboard.views.store-list"), directory) reverse("dashboard.views.store-list"), urlencode(directory))
return context return context
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
...@@ -112,7 +113,7 @@ def store_upload(request): ...@@ -112,7 +113,7 @@ def store_upload(request):
next_url = "%s%s?directory=%s" % ( next_url = "%s%s?directory=%s" % (
settings.DJANGO_URL.rstrip("/"), settings.DJANGO_URL.rstrip("/"),
reverse("dashboard.views.store-list"), directory) reverse("dashboard.views.store-list"), urlencode(directory))
return render(request, "dashboard/store/upload.html", return render(request, "dashboard/store/upload.html",
{'directory': directory, 'action': action, {'directory': directory, 'action': action,
...@@ -168,7 +169,7 @@ class StoreRemove(LoginRequiredMixin, TemplateView): ...@@ -168,7 +169,7 @@ class StoreRemove(LoginRequiredMixin, TemplateView):
return redirect("%s?directory=%s" % ( return redirect("%s?directory=%s" % (
reverse("dashboard.views.store-list"), reverse("dashboard.views.store-list"),
dirname(dirname(path)), urlencode(dirname(dirname(path))),
)) ))
...@@ -185,7 +186,7 @@ def store_new_directory(request): ...@@ -185,7 +186,7 @@ def store_new_directory(request):
name, path, unicode(request.user)) name, path, unicode(request.user))
messages.error(request, _("Unable to create folder.")) messages.error(request, _("Unable to create folder."))
return redirect("%s?directory=%s" % ( return redirect("%s?directory=%s" % (
reverse("dashboard.views.store-list"), path)) reverse("dashboard.views.store-list"), urlencode(path)))
@require_POST @require_POST
......
...@@ -26,13 +26,13 @@ from django.core.urlresolvers import reverse, reverse_lazy ...@@ -26,13 +26,13 @@ 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,
) )
from braces.views import ( from braces.views import (
LoginRequiredMixin, PermissionRequiredMixin, SuperuserRequiredMixin, LoginRequiredMixin, PermissionRequiredMixin,
) )
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
...@@ -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__)
...@@ -240,6 +243,16 @@ class TemplateDelete(LoginRequiredMixin, DeleteView): ...@@ -240,6 +243,16 @@ class TemplateDelete(LoginRequiredMixin, DeleteView):
else: else:
return ['dashboard/confirm/base-delete.html'] return ['dashboard/confirm/base-delete.html']
def get(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "owner"):
message = _("Only the owners can delete the selected template.")
if request.is_ajax():
raise PermissionDenied()
else:
messages.warning(request, message)
return redirect(self.get_success_url())
return super(TemplateDelete, self).get(request, *args, **kwargs)
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
object = self.get_object() object = self.get_object()
if not object.has_level(request.user, 'owner'): if not object.has_level(request.user, 'owner'):
...@@ -382,13 +395,17 @@ class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin, ...@@ -382,13 +395,17 @@ class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
def get_success_url(self): def get_success_url(self):
return reverse_lazy("dashboard.views.template-list") return reverse_lazy("dashboard.views.template-list")
def form_valid(self, form):
retval = super(LeaseCreate, self).form_valid(form)
self.object.set_level(self.request.user, "owner")
return retval
class LeaseAclUpdateView(AclUpdateView): class LeaseAclUpdateView(AclUpdateView):
model = Lease model = Lease
class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin, class LeaseDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
SuccessMessageMixin, UpdateView):
model = Lease model = Lease
form_class = LeaseForm form_class = LeaseForm
template_name = "dashboard/lease-edit.html" template_name = "dashboard/lease-edit.html"
...@@ -404,8 +421,21 @@ class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -404,8 +421,21 @@ class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin,
def get_success_url(self): def get_success_url(self):
return reverse_lazy("dashboard.views.lease-detail", kwargs=self.kwargs) return reverse_lazy("dashboard.views.lease-detail", kwargs=self.kwargs)
def get(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "owner"):
message = _("Only the owners can modify the selected lease.")
messages.warning(request, message)
return redirect(reverse_lazy("dashboard.views.template-list"))
return super(LeaseDetail, self).get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "owner"):
raise PermissionDenied()
return super(LeaseDetail, self).post(request, *args, **kwargs)
class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): class LeaseDelete(LoginRequiredMixin, DeleteView):
model = Lease model = Lease
def get_success_url(self): def get_success_url(self):
...@@ -431,10 +461,22 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): ...@@ -431,10 +461,22 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
c['disable_submit'] = True c['disable_submit'] = True
return c return c
def get(self, request, *args, **kwargs):
if not self.get_object().has_level(request.user, "owner"):
message = _("Only the owners can delete the selected lease.")
if request.is_ajax():
raise PermissionDenied()
else:
messages.warning(request, message)
return redirect(self.get_success_url())
return super(LeaseDelete, self).get(request, *args, **kwargs)
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
object = self.get_object() object = self.get_object()
if (object.instancetemplate_set.count() > 0): if not object.has_level(request.user, "owner"):
raise PermissionDenied()
if object.instancetemplate_set.count() > 0:
raise SuspiciousOperation() raise SuspiciousOperation()
object.delete() object.delete()
...@@ -449,3 +491,20 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): ...@@ -449,3 +491,20 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, 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)
...@@ -34,6 +34,10 @@ reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$') ...@@ -34,6 +34,10 @@ reverse_domain_re = re.compile(r'^(%\([abcd]\)d|[a-z0-9.-])+$')
ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$') ipv6_template_re = re.compile(r'^(%\([abcd]\)[dxX]|[A-Za-z0-9:-])+$')
class mac_custom(mac_unix):
word_fmt = '%.2X'
class MACAddressFormField(forms.Field): class MACAddressFormField(forms.Field):
default_error_messages = { default_error_messages = {
'invalid': _(u'Enter a valid MAC address. %s'), 'invalid': _(u'Enter a valid MAC address. %s'),
...@@ -51,9 +55,6 @@ class MACAddressField(models.Field): ...@@ -51,9 +55,6 @@ class MACAddressField(models.Field):
description = _('MAC Address object') description = _('MAC Address object')
__metaclass__ = models.SubfieldBase __metaclass__ = models.SubfieldBase
class mac_custom(mac_unix):
word_fmt = '%.2X'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['max_length'] = 17 kwargs['max_length'] = 17
super(MACAddressField, self).__init__(*args, **kwargs) super(MACAddressField, self).__init__(*args, **kwargs)
...@@ -65,7 +66,7 @@ class MACAddressField(models.Field): ...@@ -65,7 +66,7 @@ class MACAddressField(models.Field):
if isinstance(value, EUI): if isinstance(value, EUI):
return value return value
return EUI(value, dialect=MACAddressField.mac_custom) return EUI(value, dialect=mac_custom)
def get_internal_type(self): def get_internal_type(self):
return 'CharField' return 'CharField'
......
...@@ -243,6 +243,13 @@ class Rule(models.Model): ...@@ -243,6 +243,13 @@ class Rule(models.Model):
return retval return retval
@classmethod
def portforwards(cls, host=None):
qs = cls.objects.filter(dport__isnull=False, direction='in')
if host is not None:
qs = qs.filter(host=host)
return qs
class Meta: class Meta:
verbose_name = _("rule") verbose_name = _("rule")
verbose_name_plural = _("rules") verbose_name_plural = _("rules")
...@@ -762,7 +769,7 @@ class Host(models.Model): ...@@ -762,7 +769,7 @@ class Host(models.Model):
Return a list of ports with forwarding rules set. Return a list of ports with forwarding rules set.
""" """
retval = [] retval = []
for rule in self.rules.filter(dport__isnull=False, direction='in'): for rule in Rule.portforwards(host=self):
forward = { forward = {
'proto': rule.proto, 'proto': rule.proto,
'private': rule.dport, 'private': rule.dport,
......
...@@ -416,6 +416,7 @@ class Disk(TimeStampedModel): ...@@ -416,6 +416,7 @@ class Disk(TimeStampedModel):
"Operation aborted by user."), e) "Operation aborted by user."), e)
disk.size = result['size'] disk.size = result['size']
disk.type = result['type'] disk.type = result['type']
disk.checksum = result.get('checksum', None)
disk.is_ready = True disk.is_ready = True
disk.save() disk.save()
return disk return disk
......
...@@ -817,7 +817,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -817,7 +817,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
return acts return acts
def get_merged_activities(self, user=None): def get_merged_activities(self, user=None):
whitelist = ("create_disk", "download_disk") whitelist = ("create_disk", "download_disk", "add_port", "remove_port")
acts = self.get_activities(user) acts = self.get_activities(user)
merged_acts = [] merged_acts = []
latest = None latest = None
......
...@@ -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
......
...@@ -15,226 +15,26 @@ ...@@ -15,226 +15,26 @@
# You should have received a copy of the GNU General Public License along # You should have received a copy of the GNU General Public License along
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from common.models import create_readable
from manager.mancelery import celery
from vm.tasks.agent_tasks import (restart_networking, change_password,
set_time, set_hostname, start_access_server,
cleanup, update, append,
change_ip, update_legacy)
from firewall.models import Host
import time
import os
from base64 import encodestring
from hashlib import md5
from StringIO import StringIO
from tarfile import TarFile, TarInfo
from django.conf import settings
from django.db.models import Q
from django.utils import timezone
from django.utils.translation import ugettext_noop from django.utils.translation import ugettext_noop
from celery.result import TimeoutError
from monitor.client import Client
def send_init_commands(instance, act):
vm = instance.vm_name
queue = instance.get_remote_queue_name("agent")
with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')):
cleanup.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('change_password',
readable_name=ugettext_noop('change password')):
change_password.apply_async(queue=queue, args=(vm, instance.pw))
with act.sub_activity('set_time', readable_name=ugettext_noop('set time')):
set_time.apply_async(queue=queue, args=(vm, time.time()))
with act.sub_activity('set_hostname',
readable_name=ugettext_noop('set hostname')):
set_hostname.apply_async(
queue=queue, args=(vm, instance.short_hostname))
def send_networking_commands(instance, act):
queue = instance.get_remote_queue_name("agent")
with act.sub_activity('change_ip',
readable_name=ugettext_noop('change ip')):
change_ip.apply_async(queue=queue, args=(
instance.vm_name, ) + get_network_configs(instance))
with act.sub_activity('restart_networking',
readable_name=ugettext_noop('restart networking')):
restart_networking.apply_async(queue=queue, args=(instance.vm_name, ))
def create_linux_tar():
def exclude(tarinfo):
ignored = ('./.', './misc', './windows')
if any(tarinfo.name.startswith(x) for x in ignored):
return None
else:
return tarinfo
f = StringIO()
with TarFile.open(fileobj=f, mode='w:gz') as tar: from manager.mancelery import celery
agent_path = os.path.join(settings.AGENT_DIR, "agent-linux")
tar.add(agent_path, arcname='.', filter=exclude)
version_fileobj = StringIO(settings.AGENT_VERSION)
version_info = TarInfo(name='version.txt')
version_info.size = len(version_fileobj.buf)
tar.addfile(version_info, version_fileobj)
return encodestring(f.getvalue()).replace('\n', '')
def create_windows_tar():
f = StringIO()
agent_path = os.path.join(settings.AGENT_DIR, "agent-win")
with TarFile.open(fileobj=f, mode='w|gz') as tar:
tar.add(agent_path, arcname='.')
version_fileobj = StringIO(settings.AGENT_VERSION)
version_info = TarInfo(name='version.txt')
version_info.size = len(version_fileobj.buf)
tar.addfile(version_info, version_fileobj)
return encodestring(f.getvalue()).replace('\n', '')
@celery.task @celery.task
def agent_started(vm, version=None, system=None): def agent_started(vm, version=None, system=None):
from vm.models import Instance, InstanceActivity from vm.models import Instance
instance = Instance.objects.get(id=int(vm.split('-')[-1])) instance = Instance.objects.get(id=int(vm.split('-')[-1]))
queue = instance.get_remote_queue_name("agent") instance.agent_started(
initialized = instance.activity_log.filter( user=instance.owner, old_version=version, agent_system=system)
activity_code='vm.Instance.agent.cleanup').exists()
with instance.activity(code_suffix='agent',
readable_name=ugettext_noop('agent'),
concurrency_check=False) as act:
with act.sub_activity('starting',
readable_name=ugettext_noop('starting')):
pass
for i in InstanceActivity.objects.filter(
(Q(activity_code__endswith='.os_boot') |
Q(activity_code__endswith='.agent_wait')),
instance=instance, finished__isnull=True):
i.finish(True)
if version and version != settings.AGENT_VERSION:
try:
update_agent(instance, act, system, settings.AGENT_VERSION)
except TimeoutError:
pass
else:
act.sub_activity('agent_wait', readable_name=ugettext_noop(
"wait agent restarting"), interruptible=True)
return # agent is going to restart
if not initialized:
measure_boot_time(instance)
send_init_commands(instance, act)
send_networking_commands(instance, act)
with act.sub_activity('start_access_server',
readable_name=ugettext_noop(
'start access server')):
start_access_server.apply_async(queue=queue, args=(vm, ))
def measure_boot_time(instance):
if not instance.template:
return
from vm.models import InstanceActivity
deploy_time = InstanceActivity.objects.filter(
instance=instance, activity_code="vm.Instance.deploy"
).latest("finished").finished
total_boot_time = (timezone.now() - deploy_time).total_seconds()
Client().send([
"template.%(pk)d.boot_time %(val)f %(time)s" % {
'pk': instance.template.pk,
'val': total_boot_time,
'time': time.time(),
}
])
@celery.task @celery.task
def agent_stopped(vm): def agent_stopped(vm):
from vm.models import Instance, InstanceActivity from vm.models import Instance, InstanceActivity
from vm.models.activity import ActivityInProgressError
instance = Instance.objects.get(id=int(vm.split('-')[-1])) instance = Instance.objects.get(id=int(vm.split('-')[-1]))
qs = InstanceActivity.objects.filter(instance=instance, qs = InstanceActivity.objects.filter(
activity_code='vm.Instance.agent') instance=instance, activity_code='vm.Instance.agent')
act = qs.latest('id') act = qs.latest('id')
try: with act.sub_activity('stopping', concurrency_check=False,
with act.sub_activity('stopping',
readable_name=ugettext_noop('stopping')): readable_name=ugettext_noop('stopping')):
pass pass
except ActivityInProgressError:
pass
def get_network_configs(instance):
interfaces = {}
for host in Host.objects.filter(interface__instance=instance):
interfaces[str(host.mac)] = host.get_network_config()
return (interfaces, settings.FIREWALL_SETTINGS['rdns_ip'])
def update_agent(instance, act=None, system=None, version=None):
if act:
act = act.sub_activity(
'update',
readable_name=create_readable(
ugettext_noop('update to %(version)s'),
version=settings.AGENT_VERSION))
else:
act = instance.activity(
code_suffix='agent.update',
readable_name=create_readable(
ugettext_noop('update agent to %(version)s'),
version=settings.AGENT_VERSION))
with act:
queue = instance.get_remote_queue_name("agent")
if system == "Windows":
executable = os.listdir(os.path.join(settings.AGENT_DIR,
"agent-win"))[0]
# executable = "agent-winservice-%(version)s.exe" % {
# 'version': version}
data = create_windows_tar()
elif system == "Linux":
executable = ""
data = create_linux_tar()
else:
executable = ""
# Legacy update method
return update_legacy.apply_async(
queue=queue,
args=(instance.vm_name, create_linux_tar())
).get(timeout=60)
checksum = md5(data).hexdigest()
chunk_size = 1024 * 1024
chunk_number = 0
index = 0
filename = version + ".tar"
while True:
chunk = data[index:index+chunk_size]
if chunk:
append.apply_async(
queue=queue,
args=(instance.vm_name, chunk,
filename, chunk_number)).get(timeout=60)
index = index + chunk_size
chunk_number = chunk_number + 1
else:
update.apply_async(
queue=queue,
args=(instance.vm_name, filename, executable, checksum)
).get(timeout=60)
break
...@@ -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