Commit 67ab8684 by Kálmán Viktor

Merge branch 'master' into feature-pipeline

Conflicts:
	circle/dashboard/static/dashboard/dashboard.js
	circle/dashboard/static/dashboard/disk-list.js
	circle/dashboard/static/dashboard/group-details.js
	circle/dashboard/static/dashboard/node-details.js
	circle/dashboard/static/dashboard/node-list.js
	circle/dashboard/static/dashboard/vm-details.js
	circle/dashboard/static/dashboard/vm-tour.js
	circle/dashboard/templates/dashboard/base.html
	circle/dashboard/templates/dashboard/index-nodes.html
	circle/dashboard/templates/dashboard/vm-detail.html
parents 368fa52f 387eb4e8
...@@ -23,6 +23,7 @@ celerybeat-schedule ...@@ -23,6 +23,7 @@ celerybeat-schedule
.coverage .coverage
*,cover *,cover
coverage.xml coverage.xml
.noseids
# Gettext object file: # Gettext object file:
*.mo *.mo
......
...@@ -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):
...@@ -229,7 +240,7 @@ class AclBase(Model): ...@@ -229,7 +240,7 @@ class AclBase(Model):
levelfilter, levelfilter,
content_type=ct, level__weight__gte=level.weight).distinct() content_type=ct, level__weight__gte=level.weight).distinct()
clsfilter = Q(object_level_set__in=ols.all()) clsfilter = Q(object_level_set__in=ols.all())
return cls.objects.filter(clsfilter) return cls.objects.filter(clsfilter).distinct()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super(AclBase, self).save(*args, **kwargs) super(AclBase, self).save(*args, **kwargs)
......
...@@ -514,9 +514,18 @@ LOGIN_REDIRECT_URL = "/" ...@@ -514,9 +514,18 @@ LOGIN_REDIRECT_URL = "/"
AGENT_DIR = get_env_variable( AGENT_DIR = get_env_variable(
'DJANGO_AGENT_DIR', join(unicode(expanduser("~")), 'agent')) 'DJANGO_AGENT_DIR', join(unicode(expanduser("~")), 'agent'))
# AGENT_DIR is the root directory for the agent.
# The directory structure SHOULD be:
# /home/username/agent
# |-- agent-linux
# | |-- .git
# | +-- ...
# |-- agent-win
# | +-- agent-win-%(version).exe
#
try: try:
git_env = {'GIT_DIR': join(AGENT_DIR, '.git')} git_env = {'GIT_DIR': join(join(AGENT_DIR, "agent-linux"), '.git')}
AGENT_VERSION = check_output( AGENT_VERSION = check_output(
('git', 'log', '-1', r'--pretty=format:%h', 'HEAD'), env=git_env) ('git', 'log', '-1', r'--pretty=format:%h', 'HEAD'), env=git_env)
except: except:
......
...@@ -32,6 +32,7 @@ from django.core.serializers.json import DjangoJSONEncoder ...@@ -32,6 +32,7 @@ from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import ( from django.db.models import (
CharField, DateTimeField, ForeignKey, NullBooleanField CharField, DateTimeField, ForeignKey, NullBooleanField
) )
from django.template import defaultfilters
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.functional import Promise from django.utils.functional import Promise
...@@ -48,7 +49,15 @@ class WorkerNotFound(Exception): ...@@ -48,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
...@@ -61,7 +70,7 @@ def activitycontextimpl(act, on_abort=None, on_commit=None): ...@@ -61,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))
...@@ -428,6 +437,14 @@ class HumanReadableObject(object): ...@@ -428,6 +437,14 @@ class HumanReadableObject(object):
admin_text_template = admin_text_template._proxy____args[0] admin_text_template = admin_text_template._proxy____args[0]
self.user_text_template = user_text_template self.user_text_template = user_text_template
self.admin_text_template = admin_text_template self.admin_text_template = admin_text_template
for k, v in params.iteritems():
try:
v = timezone.datetime.strptime(
v, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.UTC())
except (ValueError, TypeError): # Mock raises TypeError
pass
if isinstance(v, timezone.datetime):
params[k] = defaultfilters.date(v, "DATETIME_FORMAT")
self.params = params self.params = params
@classmethod @classmethod
...@@ -444,24 +461,27 @@ class HumanReadableObject(object): ...@@ -444,24 +461,27 @@ class HumanReadableObject(object):
def from_dict(cls, d): def from_dict(cls, d):
return None if d is None else cls(**d) return None if d is None else cls(**d)
def get_admin_text(self): def _get_parsed_text(self, key):
if self.admin_text_template == "": value = getattr(self, key)
if value == "":
return "" return ""
try: try:
return _(self.admin_text_template) % self.params return _(value) % self.params
except KeyError:
logger.exception("Can't render %s '%s' %% %s",
key, value, unicode(self.params))
raise
def get_admin_text(self):
try:
return self._get_parsed_text("admin_text_template")
except KeyError: except KeyError:
logger.exception("Can't render admin_text_template '%s' %% %s",
self.admin_text_template, unicode(self.params))
return self.get_user_text() return self.get_user_text()
def get_user_text(self): def get_user_text(self):
if self.user_text_template == "":
return ""
try: try:
return _(self.user_text_template) % self.params return self._get_parsed_text("user_text_template")
except KeyError: except KeyError:
logger.exception("Can't render user_text_template '%s' %% %s",
self.user_text_template, unicode(self.params))
return self.user_text_template return self.user_text_template
def get_text(self, user): def get_text(self, user):
......
...@@ -26,6 +26,17 @@ from .models import activity_context, has_suffix, humanize_exception ...@@ -26,6 +26,17 @@ from .models import activity_context, has_suffix, humanize_exception
logger = getLogger(__name__) logger = getLogger(__name__)
class SubOperationMixin(object):
required_perms = ()
def create_activity(self, parent, user, kwargs):
if not parent:
raise TypeError("SubOperation can only be called with "
"parent_activity specified.")
return super(SubOperationMixin, self).create_activity(
parent, user, kwargs)
class Operation(object): class Operation(object):
"""Base class for VM operations. """Base class for VM operations.
""" """
...@@ -36,6 +47,10 @@ class Operation(object): ...@@ -36,6 +47,10 @@ class Operation(object):
abortable = False abortable = False
has_percentage = False has_percentage = False
@classmethod
def get_activity_code_suffix(cls):
return cls.id
def __call__(self, **kwargs): def __call__(self, **kwargs):
return self.call(**kwargs) return self.call(**kwargs)
...@@ -62,6 +77,8 @@ class Operation(object): ...@@ -62,6 +77,8 @@ class Operation(object):
parent_activity = auxargs.pop('parent_activity') parent_activity = auxargs.pop('parent_activity')
if parent_activity and user is None and not skip_auth_check: if parent_activity and user is None and not skip_auth_check:
user = parent_activity.user user = parent_activity.user
if user is None: # parent was a system call
skip_auth_check = True
# check for unexpected keyword arguments # check for unexpected keyword arguments
argspec = getargspec(self._operation) argspec = getargspec(self._operation)
...@@ -232,7 +249,7 @@ class OperatedMixin(object): ...@@ -232,7 +249,7 @@ class OperatedMixin(object):
operation could be found. operation could be found.
""" """
for op in getattr(self, operation_registry_name, {}).itervalues(): for op in getattr(self, operation_registry_name, {}).itervalues():
if has_suffix(activity_code, op.activity_code_suffix): if has_suffix(activity_code, op.get_activity_code_suffix()):
return op(self) return op(self)
else: else:
return None return None
......
...@@ -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'))
...@@ -27,9 +27,7 @@ class OperationTestCase(TestCase): ...@@ -27,9 +27,7 @@ class OperationTestCase(TestCase):
class AbortEx(Exception): class AbortEx(Exception):
pass pass
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
op.async_operation = MagicMock( op.async_operation = MagicMock(
apply_async=MagicMock(side_effect=AbortEx)) apply_async=MagicMock(side_effect=AbortEx))
...@@ -44,9 +42,7 @@ class OperationTestCase(TestCase): ...@@ -44,9 +42,7 @@ class OperationTestCase(TestCase):
class AbortEx(Exception): class AbortEx(Exception):
pass pass
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
with patch.object(Operation, 'create_activity', side_effect=AbortEx): with patch.object(Operation, 'create_activity', side_effect=AbortEx):
with patch.object(Operation, 'check_precond') as chk_pre: with patch.object(Operation, 'check_precond') as chk_pre:
try: try:
...@@ -55,9 +51,7 @@ class OperationTestCase(TestCase): ...@@ -55,9 +51,7 @@ class OperationTestCase(TestCase):
self.assertTrue(chk_pre.called) self.assertTrue(chk_pre.called)
def test_auth_check_on_non_system_call(self): def test_auth_check_on_non_system_call(self):
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
user = MagicMock() user = MagicMock()
with patch.object(Operation, 'check_auth') as check_auth: with patch.object(Operation, 'check_auth') as check_auth:
with patch.object(Operation, 'check_precond'), \ with patch.object(Operation, 'check_precond'), \
...@@ -67,9 +61,7 @@ class OperationTestCase(TestCase): ...@@ -67,9 +61,7 @@ class OperationTestCase(TestCase):
check_auth.assert_called_with(user) check_auth.assert_called_with(user)
def test_no_auth_check_on_system_call(self): def test_no_auth_check_on_system_call(self):
op = Operation(MagicMock()) op = TestOp(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
with patch.object(Operation, 'check_auth', side_effect=AssertionError): with patch.object(Operation, 'check_auth', side_effect=AssertionError):
with patch.object(Operation, 'check_precond'), \ with patch.object(Operation, 'check_precond'), \
patch.object(Operation, 'create_activity'), \ patch.object(Operation, 'create_activity'), \
...@@ -77,39 +69,25 @@ class OperationTestCase(TestCase): ...@@ -77,39 +69,25 @@ class OperationTestCase(TestCase):
op.call(system=True) op.call(system=True)
def test_no_exception_for_more_arguments_when_operation_takes_kwargs(self): def test_no_exception_for_more_arguments_when_operation_takes_kwargs(self):
class KwargOp(Operation): op = TestOp(MagicMock())
activity_code_suffix = 'test' with patch.object(TestOp, 'create_activity'), \
id = 'test' patch.object(TestOp, '_exec_op'):
def _operation(self, **kwargs):
pass
op = KwargOp(MagicMock())
with patch.object(KwargOp, 'create_activity'), \
patch.object(KwargOp, '_exec_op'):
op.call(system=True, foo=42) op.call(system=True, foo=42)
def test_exception_for_unexpected_arguments(self): def test_exception_for_unexpected_arguments(self):
class TestOp(Operation):
activity_code_suffix = 'test'
id = 'test'
def _operation(self):
pass
op = TestOp(MagicMock()) op = TestOp(MagicMock())
with patch.object(TestOp, 'create_activity'), \ with patch.object(TestOp, 'create_activity'), \
patch.object(TestOp, '_exec_op'): patch.object(TestOp, '_exec_op'):
self.assertRaises(TypeError, op.call, system=True, foo=42) self.assertRaises(TypeError, op.call, system=True, bar=42)
def test_exception_for_missing_arguments(self): def test_exception_for_missing_arguments(self):
class TestOp(Operation): op = TestOp(MagicMock())
activity_code_suffix = 'test' with patch.object(TestOp, 'create_activity'):
self.assertRaises(TypeError, op.call, system=True)
class TestOp(Operation):
id = 'test' id = 'test'
def _operation(self, foo): def _operation(self, foo):
pass pass
op = TestOp(MagicMock())
with patch.object(TestOp, 'create_activity'):
self.assertRaises(TypeError, op.call, system=True)
...@@ -236,6 +236,9 @@ class GroupProfile(AclBase): ...@@ -236,6 +236,9 @@ class GroupProfile(AclBase):
help_text=_('Unique identifier of the group at the organization.')) help_text=_('Unique identifier of the group at the organization.'))
description = TextField(blank=True) description = TextField(blank=True)
def __unicode__(self):
return self.group.name
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.org_id: if not self.org_id:
self.org_id = None self.org_id = None
......
...@@ -411,13 +411,24 @@ $(function () { ...@@ -411,13 +411,24 @@ $(function () {
$(this).removeClass("btn-default").addClass("btn-primary"); $(this).removeClass("btn-default").addClass("btn-primary");
return false; return false;
}); });
// vm migrate select for node
$(document).on("click", "#vm-migrate-node-list li", function(e) {
var li = $(this).closest('li');
if (li.find('input').attr('disabled'))
return true;
$('#vm-migrate-node-list li').removeClass('panel-primary');
li.addClass('panel-primary').find('input').prop("checked", true);
return true;
});
}); });
function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { 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 + '">' +
...@@ -430,14 +441,14 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) { ...@@ -430,14 +441,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>';
...@@ -445,7 +456,7 @@ function generateNodeHTML(name, icon, _status, url, is_last) { ...@@ -445,7 +456,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="' + icon + '" title="' + _status + '"></i> ' + name + '<i class="fa ' + icon + '" title="' + _status + '"></i> ' + safe_tags_replace(name) +
'</a> '; '</a> ';
} }
...@@ -619,7 +630,7 @@ function addModalConfirmation(func, data) { ...@@ -619,7 +630,7 @@ function addModalConfirmation(func, data) {
} }
function clientInstalledAction(location) { function clientInstalledAction(location) {
setCookie('downloaded_client', true, 365 * 24 * 60 * 60, "/"); setCookie('downloaded_client', true, 365 * 24 * 60 * 60 * 1000, "/");
window.location.href = location; window.location.href = location;
$('#confirmation-modal').modal("hide"); $('#confirmation-modal').modal("hide");
} }
...@@ -687,3 +698,17 @@ $(function() { ...@@ -687,3 +698,17 @@ $(function() {
return choice.children().html(); return choice.children().html();
} }
}); });
var tagsToReplace = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
function replaceTag(tag) {
return tagsToReplace[tag] || tag;
}
function safe_tags_replace(str) {
return str.replace(/[&<>]/g, replaceTag);
}
...@@ -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 {
...@@ -539,7 +539,7 @@ footer a, footer a:hover, footer a:visited { ...@@ -539,7 +539,7 @@ footer a, footer a:hover, footer a:visited {
} }
#dashboard-template-list a small { #dashboard-template-list a small {
max-width: 50%; max-width: 45%;
float: left; float: left;
padding-top: 2px; padding-top: 2px;
text-overflow: ellipsis; text-overflow: ellipsis;
...@@ -699,7 +699,7 @@ textarea[name="new_members"] { ...@@ -699,7 +699,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 {
...@@ -1014,6 +1014,14 @@ textarea[name="new_members"] { ...@@ -1014,6 +1014,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;
} }
...@@ -1027,6 +1035,24 @@ textarea[name="new_members"] { ...@@ -1027,6 +1035,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;
} }
...@@ -1053,7 +1079,8 @@ textarea[name="new_members"] { ...@@ -1053,7 +1079,8 @@ textarea[name="new_members"] {
max-width: 100%; max-width: 100%;
} }
#vm-list-table tbody td:nth-child(3) { #vm-list-table td.state,
#vm-list-table td.memory {
white-space: nowrap; white-space: nowrap;
} }
...@@ -1064,3 +1091,16 @@ textarea[name="new_members"] { ...@@ -1064,3 +1091,16 @@ textarea[name="new_members"] {
.disk-resize-btn { .disk-resize-btn {
margin-right: 5px; margin-right: 5px;
} }
#vm-migrate-node-list li {
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;
}
$(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");
}, },
......
...@@ -10,41 +10,6 @@ $(function() { ...@@ -10,41 +10,6 @@ $(function() {
$('.true').closest("tr").removeClass('danger'); $('.true').closest("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();
var buttons=tr.children('.actions').children('.btn-group').children('.dropdown-menu').children('li').children('.node-enable'); var buttons=tr.children('.actions').children('.btn-group').children('.dropdown-menu').children('li').children('.node-enable');
......
...@@ -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')
}
}
});
}
...@@ -16,19 +16,10 @@ $(function() { ...@@ -16,19 +16,10 @@ $(function() {
$('#confirmation-modal').on('hidden.bs.modal', function() { $('#confirmation-modal').on('hidden.bs.modal', function() {
$('#confirmation-modal').remove(); $('#confirmation-modal').remove();
}); });
$('#vm-migrate-node-list li').click(function(e) {
var li = $(this).closest('li');
if (li.find('input').attr('disabled'))
return true;
$('#vm-migrate-node-list li').removeClass('panel-primary');
li.addClass('panel-primary').find('input').attr('checked', true);
return false;
});
$('#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 */
...@@ -51,7 +42,8 @@ $(function() { ...@@ -51,7 +42,8 @@ $(function() {
if(data.success) { if(data.success) {
$('a[href="#activity"]').trigger("click"); $('a[href="#activity"]').trigger("click");
if(data.with_reload) { if(data.with_reload) {
location.reload(); // when the activity check stops the page will reload
reload_vm_detail = true;
} }
/* if there are messages display them */ /* if there are messages display them */
......
var show_all = false; var show_all = false;
var in_progress = false; var in_progress = false;
var activity_hash = 5; var activity_hash = 5;
var Websock_native; var Websock_native; // not sure
var reload_vm_detail = false;
$(function() { $(function() {
/* do we need to check for new activities */ /* do we need to check for new activities */
...@@ -28,7 +29,7 @@ $(function() { ...@@ -28,7 +29,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 +62,7 @@ $(function() { ...@@ -61,7 +62,7 @@ $(function() {
} }
} }
}); });
return false; e.preventDefault();
}); });
/* remove tag */ /* remove tag */
...@@ -205,11 +206,11 @@ $(function() { ...@@ -205,11 +206,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,15 +237,15 @@ $(function() { ...@@ -236,15 +237,15 @@ $(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");
var tmp = ta.val(); var tmp = ta.val();
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 +317,24 @@ $(function() { ...@@ -316,6 +317,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 +363,6 @@ function decideActivityRefresh() { ...@@ -344,9 +363,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,8 +393,9 @@ function checkNewActivity(runs) { ...@@ -377,8 +393,9 @@ function checkNewActivity(runs) {
} else { } else {
icon.prop("class", "fa " + data.icon); icon.prop("class", "fa " + data.icon);
} }
$("#vm-details-state span").html(data.human_readable_status.toUpperCase()); $("#vm-details-state").data("status", data['status']);
if(data.status == "RUNNING") { $("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
if(data['status'] == "RUNNING") {
if(data['connect_uri']) { if(data['connect_uri']) {
$("#dashboard-vm-details-connect-button").removeClass('disabled'); $("#dashboard-vm-details-connect-button").removeClass('disabled');
} }
...@@ -405,6 +422,7 @@ function checkNewActivity(runs) { ...@@ -405,6 +422,7 @@ function checkNewActivity(runs) {
); );
} else { } else {
in_progress = false; in_progress = false;
if(reload_vm_detail) location.reload();
} }
$('a[href="#activity"] i').removeClass('fa-spin'); $('a[href="#activity"] i').removeClass('fa-spin');
}, },
......
...@@ -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" %}'),
......
<img src="{{ STATIC_URL}}dashboard/img/logo.png" style="height: 25px;"/>
<img src="{{ STATIC_URL}}local-logo.png" style="padding-left: 2px; height: 25px;"/>
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
<i class="fa {% if d.is_downloading %}fa-refresh fa-spin{% else %}fa-file{% if d.failed %}" style="color: #d9534f;{% endif %}{% endif %}"></i> <i class="fa fa-file"></i>
{{ d.name }} (#{{ d.id }}) - {{ d.name }} (#{{ d.id }}) - {{ d.size|filesize }}
{% if not d.is_downloading %}
{% if not d.failed %} {% if op.remove_disk %}
{% if d.size %}{{ d.size|filesize }}{% endif %} <span class="operation-wrapper">
{% else %} <a href="{{ op.remove_disk.get_url }}?disk={{d.pk}}"
<div class="label label-danger"{% if user.is_superuser %} title="{{ d.get_latest_activity_result }}"{% endif %}>{% trans "failed" %}</div> class="btn btn-xs btn-{{ op.remove_disk.effect}} pull-right operation disk-remove-btn
{% endif %} {% if op.remove_disk.disabled %}disabled{% endif %}">
{% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %} <i class="fa fa-{{ op.remove_disk.icon }}"></i> {% trans "Remove" %}
{% if is_owner != False %}
<a href="{% url "dashboard.views.disk-remove" pk=d.pk %}?next={{ request.path }}"
data-disk-pk="{{ d.pk }}" class="btn btn-xs btn-danger pull-right disk-remove"
{% if not long_remove %}title="{% trans "Remove" %}"{% endif %}>
<i class="fa fa-times"></i>{% if long_remove %} {% trans "Remove" %}{% endif %}
</a> </a>
{% if op.resize_disk %} </span>
{% endif %}
{% if op.resize_disk %}
<span class="operation-wrapper"> <span class="operation-wrapper">
<a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}" <a href="{{ op.resize_disk.get_url }}?disk={{d.pk}}"
class="btn btn-xs btn-warning pull-right operation disk-resize-btn"> class="btn btn-xs btn-{{ op.resize_disk.effect }} pull-right operation disk-resize-btn
<i class="fa fa-arrows-alt"></i> {% trans "Resize" %} {% if op.resize_disk.disabled %}disabled{% endif %}">
<i class="fa fa-{{ op.resize_disk.icon }}"></i> {% trans "Resize" %}
</a> </a>
</span> </span>
{% endif %}
{% endif %} {% endif %}
<div style="clear: both;"></div> <div style="clear: both;"></div>
{% if request.user.is_superuser %}
<small>{% trans "File name" %}: {{ d.filename }}</small>
{% endif %}
{% extends "dashboard/mass-operate.html" %} {% extends "dashboard/mass-operate.html" %}
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
{% load crispy_forms_tags %}
{% block formfields %} {% block formfields %}
...@@ -11,20 +12,20 @@ ...@@ -11,20 +12,20 @@
<label for="migrate-to-none"> <label for="migrate-to-none">
<strong>{% trans "Reschedule" %}</strong> <strong>{% trans "Reschedule" %}</strong>
</label> </label>
<input id="migrate-to-none" type="radio" name="node" value="" style="float: right;" checked="checked"> <input id="migrate-to-none" type="radio" name="to_node" value="" style="float: right;" checked="checked">
<span class="vm-migrate-node-property"> <span class="vm-migrate-node-property">
{% trans "This option will reschedule each virtual machine to the optimal node." %} {% trans "This option will reschedule each virtual machine to the optimal node." %}
</span> </span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>
</li> </li>
{% for n in nodes %} {% for n in form.fields.to_node.queryset.all %}
<li class="panel panel-default mass-migrate-node"> <li class="panel panel-default mass-migrate-node">
<div class="panel-body"> <div class="panel-body">
<label for="migrate-to-{{n.pk}}"> <label for="migrate-to-{{n.pk}}">
<strong>{{ n }}</strong> <strong>{{ n }}</strong>
</label> </label>
<input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;"/> <input id="migrate-to-{{n.pk}}" type="radio" name="to_node" value="{{ n.pk }}" style="float: right;"/>
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span> <span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span> <span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
...@@ -32,5 +33,6 @@ ...@@ -32,5 +33,6 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
{{ form.live_migration|as_crispy_field }}
<hr /> <hr />
{% endblock %} {% endblock %}
{% extends "dashboard/operate.html" %} {% extends "dashboard/operate.html" %}
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
{% load crispy_forms_tags %}
{% block question %} {% block question %}
<p> <p>
...@@ -13,24 +14,27 @@ Choose a compute node to migrate {{obj}} to. ...@@ -13,24 +14,27 @@ Choose a compute node to migrate {{obj}} to.
{% block formfields %} {% block formfields %}
<ul id="vm-migrate-node-list" class="list-unstyled"> <ul id="vm-migrate-node-list" class="list-unstyled">
{% with current=object.node.pk selected=object.select_node.pk %} {% with current=object.node.pk recommended=form.fields.to_node.initial.pk %}
{% for n in nodes %} {% for n in form.fields.to_node.queryset.all %}
<li class="panel panel-default"><div class="panel-body"> <li class="panel panel-default"><div class="panel-body">
<label for="migrate-to-{{n.pk}}"> <label for="migrate-to-{{n.pk}}">
<strong>{{ n }}</strong> <strong>{{ n }}</strong>
<div class="label label-primary"><i class="fa {{n.get_status_icon}}"></i> <div class="label label-primary">
{{n.get_status_display}}</div> <i class="fa {{n.get_status_icon}}"></i> {{n.get_status_display}}</div>
{% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %} {% if current == n.pk %}<div class="label label-info">{% trans "current" %}</div>{% endif %}
{% if selected == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %} {% if recommended == n.pk %}<div class="label label-success">{% trans "recommended" %}</div>{% endif %}
</label> </label>
<input id="migrate-to-{{n.pk}}" type="radio" name="node" value="{{ n.pk }}" style="float: right;" <input id="migrate-to-{{n.pk}}" type="radio" name="to_node" value="{{ n.pk }}" style="float: right;"
{% if current == n.pk %}disabled="disabled"{% endif %} {% if current == n.pk %}disabled="disabled"{% endif %}
{% if selected == n.pk %}checked="checked"{% endif %} /> {% if recommended == n.pk %}checked="checked"{% endif %}
/>
<span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span> <span class="vm-migrate-node-property">{% trans "CPU load" %}: {{ n.cpu_usage }}</span>
<span class="vm-migrate-node-property">{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span> <span class="vm-migrate-node-property">
{% trans "RAM usage" %}: {{ n.byte_ram_usage|filesize }}/{{ n.ram_size|filesize }}</span>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div></li> </li>
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}
</ul> </ul>
{{ form.live_migration|as_crispy_field }}
{% endblock %} {% endblock %}
...@@ -5,10 +5,16 @@ ...@@ -5,10 +5,16 @@
{% block title-site %}Dashboard | CIRCLE{% endblock %} {% block title-site %}Dashboard | CIRCLE{% endblock %}
{% 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/introjs/introjs.min.css">
<link href="{{ STATIC_URL }}dashboard/dashboard.css" rel="stylesheet">
{% endblock %}
{% block navbar-brand %} {% block navbar-brand %}
<a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;"> <a class="navbar-brand" href="{% url "dashboard.index" %}" style="padding: 10px 15px;">
<img src="{% static "dashboard/img/logo.png" %}" style="height: 25px;"/> {% include "branding.html" %}
</a> </a>
{% endblock %} {% 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,26 +46,55 @@ ...@@ -39,26 +46,55 @@
</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>
<ul class="dashboard-profile-vm-list fa-ul">
{% for i in vm_objects %}
<li>
<a href="{{ i.get_absolute_url }}">
<i class="fa fa-li {{ i.get_status_icon }}"></i>
{{ i }}
</a>
</li>
{% endfor %}
{% for t in template_objects %}
<li>
<a href="{{ t.get_absolute_url }}">
<i class="fa fa-li fa-puzzle-piece"></i>
{{ t }}
</a>
</li>
{% endfor %}
{% for g in group_objects %}
<li>
<a href="{{ g.get_absolute_url }}">
<i class="fa fa-li fa-users"></i>
{{ g }}
</a>
</li>
{% endfor %}
</ul>
<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 %}
...@@ -71,7 +107,9 @@ ...@@ -71,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 %}
...@@ -102,29 +140,31 @@ ...@@ -102,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>
<div id="group-detail-permissions"> <script type="text/javascript" src="/static/admin/js/jquery.min.js"></script>
{% crispy group_perm_form %} <script type="text/javascript" src="/static/admin/js/jquery.init.js"></script>
</div> {{ group_perm_form.media }}
<link rel="stylesheet" type="text/css" href="/static/admin/css/widgets.css" /> <h3>{% trans "Group permissions" %}</h3>
{% endif %} <div id="group-detail-permissions">
{% crispy group_perm_form %}
</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 %}
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,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">
......
{% load i18n %} {% load i18n %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<div class="pull-right toolbar"> <div class="pull-right toolbar">
<div class="btn-group"> <div class="btn-group">
...@@ -7,9 +7,10 @@ ...@@ -7,9 +7,10 @@
data-container="body"><i class="fa fa-dashboard"></i></a> data-container="body"><i class="fa fa-dashboard"></i></a>
<a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled" <a href="#index-list-view" data-index-box="node" class="btn btn-default btn-xs disabled"
data-container="body"><i class="fa fa-list"></i></a> data-container="body"><i class="fa fa-list"></i></a>
</div> </div>
<span class="btn btn-default btn-xs infobtn" title="{% trans "List of compute nodes, also called worker nodes or hypervisors, which run the virtual machines." %}"><i class="fa fa-info-circle"></i></span> <span class="btn btn-default btn-xs infobtn" title="{% trans "List of compute nodes, also called worker nodes or hypervisors, which run the virtual machines." %}">
<i class="fa fa-info-circle"></i>
</span>
</div> </div>
<h3 class="no-margin"> <h3 class="no-margin">
<i class="fa fa-sitemap"></i> {% trans "Nodes" %} <i class="fa fa-sitemap"></i> {% trans "Nodes" %}
...@@ -28,19 +29,43 @@ ...@@ -28,19 +29,43 @@
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
</div><!-- #node-list-view -->
<div class="panel-body" id="node-graph-view" style="display: none; min-height: 204px;">
<p class="pull-right">
<input class="knob" data-fgColor="chartreuse"
data-thickness=".4" data-width="60" data-height="60" data-readOnly="true"
value="{% widthratio node_num.running sum_node_num 100 %}">
</p>
<p>
<span class="big">
<big>{{ node_num.running }}</big> running
</span>
+ <big>{{ node_num.missing }}</big>
missing + <br><big>{{ node_num.disabled }}</big> disabled + <big>{{ node_num.offline }}</big> offline
</p>
<ul class="list-inline" id="dashboard-node-taglist">
{% for i in nodes %}
<a href="{{ i.get_absolute_url }}" class="label {{i.get_status_label}}" >
<i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> {{ i.name }}</a>
{% endfor %}
</ul>
<div class="clearfix"></div>
</div>
<div href="#" class="list-group-item list-group-footer"> <div href="#" class="list-group-item list-group-footer">
<div class="row"> <div class="row">
<div class="col-xs-6"> <div class="col-sm-6 col-xs-6 input-group input-group-sm">
<form action="{% url "dashboard.views.node-list" %}" method="GET" id="dashboard-node-search-form"> <input id="dashboard-node-search-input" type="text" class="form-control"
<div class="input-group input-group-sm"> placeholder="{% trans "Search..." %}" />
<input id="dashboard-node-search-input" name="s" type="text" class="form-control" placeholder="{% trans "Search..." %}" />
<div class="input-group-btn"> <div class="input-group-btn">
<button type="submit" class="form-control btn btn-primary" title="search"><i class="fa fa-search"></i></button> <button type="submit" class="btn btn-primary" title="{% trans "Search" %}" data-container="body">
</div> <i class="fa fa-search"></i>
</button>
</div> </div>
</form>
</div> </div>
<div class="col-xs-6 text-right"> <div class="col-sm-6 text-right">
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.node-list" %}"> <a class="btn btn-primary btn-xs" href="{% url "dashboard.views.node-list" %}">
<i class="fa fa-chevron-circle-right"></i> <i class="fa fa-chevron-circle-right"></i>
{% if more_nodes > 0 %} {% if more_nodes > 0 %}
...@@ -49,33 +74,12 @@ ...@@ -49,33 +74,12 @@
{% trans "list" %} {% trans "list" %}
{% endif %} {% endif %}
</a> </a>
<a class="btn btn-success btn-xs node-create" href="{% url "dashboard.views.node-create" %}"><i class="fa fa-plus-circle"></i> {% trans "new" %}</a> {% if request.user.is_superuser %}
</div> <a class="btn btn-success btn-xs node-create" href="{% url "dashboard.views.node-create" %}">
</div> <i class="fa fa-plus-circle"></i> {% trans "new" %}
</div>
</div>
<div class="panel-body" id="node-graph-view" style="display: none">
<p class="pull-right"> <input class="knob" data-fgColor="chartreuse" data-thickness=".4" data-width="60" data-height="60" data-readOnly="true" value="{% widthratio node_num.running sum_node_num 100 %}"></p>
<p><span class="big"><big>{{ node_num.running }}</big> running </span>
+ <big>{{ node_num.missing }}</big> missing + <br><big>{{ node_num.disabled }}</big> disabled + <big>{{ node_num.offline }}</big> offline</p>
<ul class="list-inline" id="dashboard-node-taglist">
{% for i in nodes %}
<a href="{{ i.get_absolute_url }}" class="label {{i.get_status_label}}" >
<i class="fa {{ i.get_status_icon }}" title="{{ i.get_status_display }}"></i> {{ i.name }}</a>
{% endfor %}
</ul>
<div class="clearfix"></div>
<div class="row">
<div class="col-sm-6 text-right pull-right">
{% if more_nodes >= 0 %}
<a class="btn btn-primary btn-xs" href="{% url "dashboard.views.node-list" %}">
<i class="fa fa-chevron-circle-right"></i> {% blocktrans with count=more_nodes %}<strong>{{count}}</strong> more{% endblocktrans %}
</a> </a>
{% endif %} {% endif %}
<a class="btn btn-success btn-xs node-create" href="{% url "dashboard.views.node-create" %}"><i class="fa fa-plus-circle"></i> {% trans "new" %}</a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,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>
......
...@@ -58,6 +58,26 @@ ...@@ -58,6 +58,26 @@
<dt>{% trans "resultant state" %}</dt> <dt>{% trans "resultant state" %}</dt>
<dd>{{object.resultant_state|default:'n/a'}}</dd> <dd>{{object.resultant_state|default:'n/a'}}</dd>
<dt>{% trans "subactivities" %}</dt>
{% for s in object.children.all %}
<dd>
<span{% if s.result %} title="{{ s.result|get_text:user }}"{% endif %}>
<a href="{{ s.get_absolute_url }}">
{{ s.readable_name|get_text:user|capfirst }}</a></span> &ndash;
{% if s.finished %}
{{ s.finished|time:"H:i:s" }}
{% else %}
<i class="fa fa-refresh fa-spin" class="sub-activity-loading-icon"></i>
{% endif %}
{% if s.has_failed %}
<div class="label label-danger">{% trans "failed" %}</div>
{% endif %}
</dd>
{% empty %}
<dd>{% trans "none" %}</dd>
{% endfor %}
</div>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,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>
...@@ -14,6 +15,7 @@ ...@@ -14,6 +15,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">
......
...@@ -18,10 +18,12 @@ ...@@ -18,10 +18,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>
......
{% load i18n %} {% load i18n %}
<div id="node-list-column-vm"> <div id="node-list-column-vm">
<a class="real-link" href="{% url "dashboard.views.vm-list" %}?s=node:{{ record.name }}"> <a class="real-link" href="{% url "dashboard.views.vm-list" %}?s=node_exact:{{ record.name }}">
{{ value }} {{ value }}
</a> </a>
</div> </div>
...@@ -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" %}
......
...@@ -12,7 +12,9 @@ ...@@ -12,7 +12,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">
...@@ -66,6 +68,18 @@ ...@@ -66,6 +68,18 @@
</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 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>
...@@ -87,7 +101,13 @@ ...@@ -87,7 +101,13 @@
{% endif %} {% endif %}
{% for d in disks %} {% for d in disks %}
<li> <li>
{% include "dashboard/_disk-list-element.html" %} <i class="fa fa-file"></i>
{{ d.name }} (#{{ d.id }}) -
<a href="{% url "dashboard.views.disk-remove" pk=d.pk %}?next={{ request.path }}"
data-disk-pk="{{ d.pk }}" class="btn btn-xs btn-danger pull-right disk-remove"
{% if not long_remove %}title="{% trans "Remove" %}"{% endif %}>
<i class="fa fa-times"></i>{% if long_remove %} {% trans "Remove" %}{% endif %}
</a>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
......
...@@ -8,14 +8,25 @@ ...@@ -8,14 +8,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>
...@@ -67,7 +78,7 @@ ...@@ -67,7 +78,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
......
...@@ -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="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;">
......
...@@ -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 %}
......
...@@ -73,6 +73,10 @@ ...@@ -73,6 +73,10 @@
{% trans "Lease" as t %} {% trans "Lease" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %} {% include "dashboard/vm-list/header-link.html" with name=t sort="lease" %}
</th> </th>
<th data-sort="string" class="orderable sortable">
{% trans "Memory" as t %}
{% include "dashboard/vm-list/header-link.html" with name=t sort="ram_size" %}
</th>
{% if user.is_superuser %} {% if user.is_superuser %}
<th data-sort="string" class="orderable sortable"> <th data-sort="string" class="orderable sortable">
{% trans "IP address" as t %} {% trans "IP address" as t %}
...@@ -87,7 +91,9 @@ ...@@ -87,7 +91,9 @@
{% for i in object_list %} {% for i in object_list %}
<tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}"> <tr class="{% cycle 'odd' 'even' %}" data-vm-pk="{{ i.pk }}">
<td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td> <td class="pk"><div id="vm-{{i.pk}}">{{i.pk}}</div> </td>
<td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">{{ i.name }}</a> </td> <td class="name"><a class="real-link" href="{% url "dashboard.views.detail" i.pk %}">
{{ i.name }}</a>
</td>
<td class="state"> <td class="state">
<i class="fa fa-fw <i class="fa fa-fw
{% if show_acts_in_progress and i.is_in_status_change %} {% if show_acts_in_progress and i.is_in_status_change %}
...@@ -105,7 +111,12 @@ ...@@ -105,7 +111,12 @@
{# include "dashboard/_display-name.html" with user=i.owner show_org=True #} {# include "dashboard/_display-name.html" with user=i.owner show_org=True #}
</td> </td>
<td class="lease "data-sort-value="{{ i.lease.name }}"> <td class="lease "data-sort-value="{{ i.lease.name }}">
<span title="{{ i.time_of_suspend|timeuntil }} | {{ i.time_of_delete|timeuntil }}">
{{ i.lease.name }} {{ i.lease.name }}
</span>
</td>
<td class="memory "data-sort-value="{{ i.ram_size }}">
{{ i.ram_size }} MiB
</td> </td>
{% if user.is_superuser %} {% if user.is_superuser %}
<td class="ip_addr "data-sort-value="{{ i.ipv4 }}"> <td class="ip_addr "data-sort-value="{{ i.ipv4 }}">
......
...@@ -34,6 +34,13 @@ from ..views import AclUpdateView ...@@ -34,6 +34,13 @@ from ..views import AclUpdateView
from .. import views from .. import views
class QuerySet(list):
model = MagicMock()
def get(self, *args, **kwargs):
return self.pop()
class ViewUserTestCase(unittest.TestCase): class ViewUserTestCase(unittest.TestCase):
def test_404(self): def test_404(self):
...@@ -145,58 +152,66 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -145,58 +152,66 @@ class VmOperationViewTestCase(unittest.TestCase):
view.as_view()(request, pk=1234).render() view.as_view()(request, pk=1234).render()
def test_migrate(self): def test_migrate(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=True) request = FakeRequestFactory(
POST={'to_node': 1, 'live_migration': True}, superuser=True)
view = vm_ops['migrate'] view = vm_ops['migrate']
node = MagicMock(pk=1, name='node1')
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
patch('dashboard.views.util.messages') as msg, \ patch('dashboard.views.util.messages') as msg, \
patch('dashboard.views.vm.get_object_or_404') as go4: patch.object(view, 'get_form_kwargs') as form_kwargs:
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst) inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock() inst.migrate.async = MagicMock()
inst.has_level.return_value = True inst.has_level.return_value = True
form_kwargs.return_value = {
'default': 100, 'choices': QuerySet([node])}
go.return_value = inst go.return_value = inst
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location'] assert view.as_view()(request, pk=1234)['location']
assert not msg.error.called assert not msg.error.called
assert go4.called inst.migrate.async.assert_called_once_with(
to_node=node, live_migration=True, user=request.user)
def test_migrate_failed(self): def test_migrate_failed(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=True) request = FakeRequestFactory(POST={'to_node': 1}, superuser=True)
view = vm_ops['migrate'] view = vm_ops['migrate']
node = MagicMock(pk=1, name='node1')
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
patch('dashboard.views.util.messages') as msg, \ patch('dashboard.views.util.messages') as msg, \
patch('dashboard.views.vm.get_object_or_404') as go4: patch.object(view, 'get_form_kwargs') as form_kwargs:
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst) inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock() inst.migrate.async = MagicMock()
inst.migrate.async.side_effect = Exception inst.migrate.async.side_effect = Exception
inst.has_level.return_value = True inst.has_level.return_value = True
form_kwargs.return_value = {
'default': 100, 'choices': QuerySet([node])}
go.return_value = inst go.return_value = inst
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location'] assert view.as_view()(request, pk=1234)['location']
assert inst.migrate.async.called
assert msg.error.called assert msg.error.called
assert go4.called
def test_migrate_wo_permission(self): def test_migrate_wo_permission(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=False) request = FakeRequestFactory(POST={'to_node': 1}, superuser=False)
view = vm_ops['migrate'] view = vm_ops['migrate']
node = MagicMock(pk=1, name='node1')
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
patch('dashboard.views.vm.get_object_or_404') as go4: patch.object(view, 'get_form_kwargs') as form_kwargs:
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst) inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock() inst.migrate.async = MagicMock()
inst.has_level.return_value = True inst.has_level.return_value = True
form_kwargs.return_value = {
'default': 100, 'choices': QuerySet([node])}
go.return_value = inst go.return_value = inst
go4.return_value = MagicMock()
with self.assertRaises(PermissionDenied): with self.assertRaises(PermissionDenied):
assert view.as_view()(request, pk=1234)['location'] assert view.as_view()(request, pk=1234)['location']
assert go4.called assert not inst.migrate.async.called
def test_migrate_template(self): def test_migrate_template(self):
"""check if GET dialog's template can be rendered""" """check if GET dialog's template can be rendered"""
...@@ -219,6 +234,7 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -219,6 +234,7 @@ class VmOperationViewTestCase(unittest.TestCase):
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
patch('dashboard.views.util.messages') as msg: patch('dashboard.views.util.messages') as msg:
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst.name = "asd"
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
inst.save_as_template = Instance._ops['save_as_template'](inst) inst.save_as_template = Instance._ops['save_as_template'](inst)
inst.save_as_template.async = MagicMock() inst.save_as_template.async = MagicMock()
...@@ -235,6 +251,7 @@ class VmOperationViewTestCase(unittest.TestCase): ...@@ -235,6 +251,7 @@ class VmOperationViewTestCase(unittest.TestCase):
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
patch('dashboard.views.util.messages') as msg: patch('dashboard.views.util.messages') as msg:
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst.name = "asd"
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
inst.save_as_template = Instance._ops['save_as_template'](inst) inst.save_as_template = Instance._ops['save_as_template'](inst)
inst.save_as_template.async = MagicMock() inst.save_as_template.async = MagicMock()
...@@ -301,7 +318,7 @@ class VmMassOperationViewTestCase(unittest.TestCase): ...@@ -301,7 +318,7 @@ class VmMassOperationViewTestCase(unittest.TestCase):
view.as_view()(request, pk=1234).render() view.as_view()(request, pk=1234).render()
def test_migrate(self): def test_migrate(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=True) request = FakeRequestFactory(POST={'to_node': 1}, superuser=True)
view = vm_mass_ops['migrate'] view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
...@@ -318,7 +335,7 @@ class VmMassOperationViewTestCase(unittest.TestCase): ...@@ -318,7 +335,7 @@ class VmMassOperationViewTestCase(unittest.TestCase):
assert not msg2.error.called assert not msg2.error.called
def test_migrate_failed(self): def test_migrate_failed(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=True) request = FakeRequestFactory(POST={'to_node': 1}, superuser=True)
view = vm_mass_ops['migrate'] view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
...@@ -334,7 +351,7 @@ class VmMassOperationViewTestCase(unittest.TestCase): ...@@ -334,7 +351,7 @@ class VmMassOperationViewTestCase(unittest.TestCase):
assert msg.error.called assert msg.error.called
def test_migrate_wo_permission(self): def test_migrate_wo_permission(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=False) request = FakeRequestFactory(POST={'to_node': 1}, superuser=False)
view = vm_mass_ops['migrate'] view = vm_mass_ops['migrate']
with patch.object(view, 'get_object') as go: with patch.object(view, 'get_object') as go:
......
...@@ -299,7 +299,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -299,7 +299,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):
...@@ -512,20 +512,20 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -512,20 +512,20 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, "user2") self.login(c, "user2")
with patch.object(Instance, 'select_node', return_value=None), \ with patch.object(Instance, 'select_node', return_value=None), \
patch.object(WakeUpOperation, 'async') as new_wake_up, \ patch.object(WakeUpOperation, 'async') as new_wake_up, \
patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa, \
patch.object(Instance.WrongStateError, 'send_message') as wro: patch.object(Instance.WrongStateError, 'send_message') as wro:
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
new_wake_up.side_effect = inst.wake_up new_wake_up.side_effect = inst.wake_up
inst._wake_up_vm = Mock()
inst.get_remote_queue_name = Mock(return_value='test') inst.get_remote_queue_name = Mock(return_value='test')
inst.status = 'SUSPENDED' inst.status = 'SUSPENDED'
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
with patch('dashboard.views.messages') as msg: with patch('dashboard.views.messages') as msg:
response = c.post("/dashboard/vm/1/op/wake_up/") response = c.post("/dashboard/vm/1/op/wake_up/")
assert not msg.error.called assert not msg.error.called
assert inst._wake_up_vm.called
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
self.assertEqual(inst.status, 'RUNNING') self.assertEqual(inst.status, 'RUNNING')
assert new_wake_up.called assert new_wake_up.called
assert wuaa.called
assert not wro.called assert not wro.called
def test_unpermitted_wake_up(self): def test_unpermitted_wake_up(self):
...@@ -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):
......
...@@ -45,6 +45,7 @@ from .views import ( ...@@ -45,6 +45,7 @@ from .views import (
VmTraitsUpdate, VmRawDataUpdate, VmTraitsUpdate, VmRawDataUpdate,
GroupPermissionsView, GroupPermissionsView,
LeaseAclUpdateView, LeaseAclUpdateView,
toggle_template_tutorial,
ClientCheck, TokenLogin, ClientCheck, TokenLogin,
VmGraphView, NodeGraphView, NodeListGraphView, VmGraphView, NodeGraphView, NodeListGraphView,
) )
...@@ -99,6 +100,8 @@ urlpatterns = patterns( ...@@ -99,6 +100,8 @@ 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(),
......
...@@ -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)
......
...@@ -39,6 +39,7 @@ from ..forms import ( ...@@ -39,6 +39,7 @@ from ..forms import (
GroupCreateForm, GroupProfileUpdateForm, GroupCreateForm, GroupProfileUpdateForm,
) )
from ..models import FutureMember, GroupProfile from ..models import FutureMember, GroupProfile
from vm.models import Instance, InstanceTemplate
from ..tables import GroupListTable from ..tables import GroupListTable
from .util import CheckedDetailView, AclUpdateView, search_user, saml_available from .util import CheckedDetailView, AclUpdateView, search_user, saml_available
...@@ -100,6 +101,15 @@ class GroupDetailView(CheckedDetailView): ...@@ -100,6 +101,15 @@ class GroupDetailView(CheckedDetailView):
context['group_profile_form'] = GroupProfileUpdate.get_form_object( context['group_profile_form'] = GroupProfileUpdate.get_form_object(
self.request, self.object.profile) self.request, self.object.profile)
context.update({
'group_objects': GroupProfile.get_objects_with_group_level(
"operator", self.get_object()),
'vm_objects': Instance.get_objects_with_group_level(
"user", self.get_object()),
'template_objects': InstanceTemplate.get_objects_with_group_level(
"user", self.get_object()),
})
if self.request.user.is_superuser: if self.request.user.is_superuser:
context['group_perm_form'] = GroupPermissionForm( context['group_perm_form'] = GroupPermissionForm(
instance=self.object) instance=self.object)
......
...@@ -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
......
...@@ -32,11 +32,12 @@ from django.views.generic import ( ...@@ -32,11 +32,12 @@ from django.views.generic import (
) )
from braces.views import ( from braces.views import (
LoginRequiredMixin, PermissionRequiredMixin, SuperuserRequiredMixin, LoginRequiredMixin, PermissionRequiredMixin,
) )
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from vm.models import InstanceTemplate, InterfaceTemplate, Instance, Lease from vm.models import InstanceTemplate, InterfaceTemplate, Instance, Lease
from storage.models import Disk
from ..forms import ( from ..forms import (
TemplateForm, TemplateListSearchForm, AclUserOrGroupAddForm, LeaseForm, TemplateForm, TemplateListSearchForm, AclUserOrGroupAddForm, LeaseForm,
...@@ -239,6 +240,16 @@ class TemplateDelete(LoginRequiredMixin, DeleteView): ...@@ -239,6 +240,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'):
...@@ -319,6 +330,57 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -319,6 +330,57 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
return kwargs return kwargs
class DiskRemoveView(DeleteView):
model = Disk
def get_queryset(self):
qs = super(DiskRemoveView, self).get_queryset()
return qs.exclude(template_set=None)
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, **kwargs):
context = super(DiskRemoveView, self).get_context_data(**kwargs)
disk = self.get_object()
template = disk.template_set.get()
if not template.has_level(self.request.user, 'owner'):
raise PermissionDenied()
context['title'] = _("Disk remove confirmation")
context['text'] = _("Are you sure you want to remove "
"<strong>%(disk)s</strong> from "
"<strong>%(app)s</strong>?" % {'disk': disk,
'app': template}
)
return context
def delete(self, request, *args, **kwargs):
disk = self.get_object()
template = disk.template_set.get()
if not template.has_level(request.user, 'owner'):
raise PermissionDenied()
template.remove_disk(disk=disk, user=request.user)
disk.destroy()
next_url = request.POST.get("next")
success_url = next_url if next_url else template.get_absolute_url()
success_message = _("Disk successfully removed.")
if request.is_ajax():
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect("%s#resources" % success_url)
class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin, class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
SuccessMessageMixin, CreateView): SuccessMessageMixin, CreateView):
model = Lease model = Lease
...@@ -330,13 +392,17 @@ class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin, ...@@ -330,13 +392,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"
...@@ -352,8 +418,21 @@ class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -352,8 +418,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):
...@@ -379,10 +458,22 @@ class LeaseDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): ...@@ -379,10 +458,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()
......
...@@ -184,7 +184,7 @@ class OperationView(RedirectToLoginMixin, DetailView): ...@@ -184,7 +184,7 @@ class OperationView(RedirectToLoginMixin, DetailView):
@classmethod @classmethod
def get_urlname(cls): def get_urlname(cls):
return 'dashboard.vm.op.%s' % cls.op return 'dashboard.%s.op.%s' % (cls.model._meta.model_name, cls.op)
@classmethod @classmethod
def get_instance_url(cls, pk, key=None, *args, **kwargs): def get_instance_url(cls, pk, key=None, *args, **kwargs):
......
...@@ -94,6 +94,10 @@ def make_messages(): ...@@ -94,6 +94,10 @@ def make_messages():
def test(test=""): def test(test=""):
"Run portal tests" "Run portal tests"
with _workon("circle"), cd("~/circle/circle"): with _workon("circle"), cd("~/circle/circle"):
if test == "f":
test = "--failed"
else:
test += " --with-id"
run("./manage.py test --settings=circle.settings.test %s" % test) run("./manage.py test --settings=circle.settings.test %s" % test)
......
...@@ -874,7 +874,7 @@ class Record(models.Model): ...@@ -874,7 +874,7 @@ class Record(models.Model):
verbose_name=_('host')) verbose_name=_('host'))
type = models.CharField(max_length=6, choices=CHOICES_type, type = models.CharField(max_length=6, choices=CHOICES_type,
verbose_name=_('type')) verbose_name=_('type'))
address = models.CharField(max_length=200, address = models.CharField(max_length=400,
verbose_name=_('address')) verbose_name=_('address'))
ttl = models.IntegerField(default=600, verbose_name=_('ttl')) ttl = models.IntegerField(default=600, verbose_name=_('ttl'))
owner = models.ForeignKey(User, verbose_name=_('owner')) owner = models.ForeignKey(User, verbose_name=_('owner'))
......
...@@ -29,26 +29,24 @@ settings = django.conf.settings.FIREWALL_SETTINGS ...@@ -29,26 +29,24 @@ settings = django.conf.settings.FIREWALL_SETTINGS
logger = getLogger(__name__) logger = getLogger(__name__)
def _apply_once(name, queues, task, data): def _apply_once(name, tasks, queues, task, data):
"""Reload given networking component if needed. """Reload given networking component if needed.
""" """
lockname = "%s_lock" % name if name not in tasks:
if not cache.get(lockname):
return return
cache.delete(lockname)
data = data() data = data()
for queue in queues: for queue in queues:
try: try:
task.apply_async(args=data, queue=queue, expires=60).get(timeout=5) task.apply_async(args=data, queue=queue, expires=60).get(timeout=2)
logger.info("%s configuration is reloaded. (queue: %s)", logger.info("%s configuration is reloaded. (queue: %s)",
name, queue) name, queue)
except TimeoutError as e: except TimeoutError as e:
logger.critical('%s (queue: %s)', e, queue) logger.critical('%s (queue: %s, task: %s)', e, queue, name)
except: except:
logger.critical('Unhandled exception: queue: %s data: %s', logger.critical('Unhandled exception: queue: %s data: %s task: %s',
queue, data, exc_info=True) queue, data, name, exc_info=True)
def get_firewall_queues(): def get_firewall_queues():
...@@ -68,19 +66,28 @@ def reloadtask_worker(): ...@@ -68,19 +66,28 @@ def reloadtask_worker():
from remote_tasks import (reload_dns, reload_dhcp, reload_firewall, from remote_tasks import (reload_dns, reload_dhcp, reload_firewall,
reload_firewall_vlan, reload_blacklist) reload_firewall_vlan, reload_blacklist)
tasks = []
for i in ('dns', 'dhcp', 'firewall', 'firewall_vlan', 'blacklist'):
lockname = "%s_lock" % i
if cache.get(lockname):
tasks.append(i)
cache.delete(lockname)
logger.info("reloadtask_worker: Reload %s", ", ".join(tasks))
firewall_queues = get_firewall_queues() firewall_queues = get_firewall_queues()
dns_queues = [("%s.dns" % i) for i in dns_queues = [("%s.dns" % i) for i in
settings.get('dns_queues', [gethostname()])] settings.get('dns_queues', [gethostname()])]
_apply_once('dns', dns_queues, reload_dns, _apply_once('dns', tasks, dns_queues, reload_dns,
lambda: (dns(), )) lambda: (dns(), ))
_apply_once('dhcp', firewall_queues, reload_dhcp, _apply_once('dhcp', tasks, firewall_queues, reload_dhcp,
lambda: (dhcp(), )) lambda: (dhcp(), ))
_apply_once('firewall', firewall_queues, reload_firewall, _apply_once('firewall', tasks, firewall_queues, reload_firewall,
lambda: (BuildFirewall().build_ipt())) lambda: (BuildFirewall().build_ipt()))
_apply_once('firewall_vlan', firewall_queues, reload_firewall_vlan, _apply_once('firewall_vlan', tasks, firewall_queues, reload_firewall_vlan,
lambda: (vlan(), )) lambda: (vlan(), ))
_apply_once('blacklist', firewall_queues, reload_blacklist, _apply_once('blacklist', tasks, firewall_queues, reload_blacklist,
lambda: (list(ipset()), )) lambda: (list(ipset()), ))
......
...@@ -16,12 +16,15 @@ ...@@ -16,12 +16,15 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from celery import Celery from celery import Celery
from celery.signals import worker_ready
from datetime import timedelta from datetime import timedelta
from kombu import Queue, Exchange from kombu import Queue, Exchange
from os import getenv from os import getenv
HOSTNAME = "localhost" HOSTNAME = "localhost"
CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/") CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/")
QUEUE_NAME = HOSTNAME + '.man'
celery = Celery('manager', celery = Celery('manager',
broker=getenv("AMQP_URI"), broker=getenv("AMQP_URI"),
...@@ -57,3 +60,10 @@ celery.conf.update( ...@@ -57,3 +60,10 @@ celery.conf.update(
} }
) )
@worker_ready.connect()
def cleanup_tasks(conf=None, **kwargs):
'''Discard all task and clean up activity.'''
from vm.models.activity import cleanup
cleanup(queue_name=QUEUE_NAME)
...@@ -16,12 +16,14 @@ ...@@ -16,12 +16,14 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from celery import Celery from celery import Celery
from celery.signals import worker_ready
from datetime import timedelta from datetime import timedelta
from kombu import Queue, Exchange from kombu import Queue, Exchange
from os import getenv from os import getenv
HOSTNAME = "localhost" HOSTNAME = "localhost"
CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/") CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/")
QUEUE_NAME = HOSTNAME + '.monitor'
celery = Celery('monitor', celery = Celery('monitor',
broker=getenv("AMQP_URI"), broker=getenv("AMQP_URI"),
...@@ -34,7 +36,7 @@ celery.conf.update( ...@@ -34,7 +36,7 @@ celery.conf.update(
CELERY_CACHE_BACKEND=CACHE_URI, CELERY_CACHE_BACKEND=CACHE_URI,
CELERY_TASK_RESULT_EXPIRES=300, CELERY_TASK_RESULT_EXPIRES=300,
CELERY_QUEUES=( CELERY_QUEUES=(
Queue(HOSTNAME + '.monitor', Exchange('monitor', type='direct'), Queue(QUEUE_NAME, Exchange('monitor', type='direct'),
routing_key="monitor"), routing_key="monitor"),
), ),
CELERYBEAT_SCHEDULE={ CELERYBEAT_SCHEDULE={
...@@ -70,3 +72,10 @@ celery.conf.update( ...@@ -70,3 +72,10 @@ celery.conf.update(
} }
) )
@worker_ready.connect()
def cleanup_tasks(conf=None, **kwargs):
'''Discard all task and clean up activity.'''
from vm.models.activity import cleanup
cleanup(queue_name=QUEUE_NAME)
...@@ -16,12 +16,14 @@ ...@@ -16,12 +16,14 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from celery import Celery from celery import Celery
from celery.signals import worker_ready
from datetime import timedelta from datetime import timedelta
from kombu import Queue, Exchange from kombu import Queue, Exchange
from os import getenv from os import getenv
HOSTNAME = "localhost" HOSTNAME = "localhost"
CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/") CACHE_URI = getenv("CACHE_URI", "pylibmc://127.0.0.1:11211/")
QUEUE_NAME = HOSTNAME + '.man.slow'
celery = Celery('manager.slow', celery = Celery('manager.slow',
broker=getenv("AMQP_URI"), broker=getenv("AMQP_URI"),
...@@ -36,7 +38,7 @@ celery.conf.update( ...@@ -36,7 +38,7 @@ celery.conf.update(
CELERY_CACHE_BACKEND=CACHE_URI, CELERY_CACHE_BACKEND=CACHE_URI,
CELERY_TASK_RESULT_EXPIRES=300, CELERY_TASK_RESULT_EXPIRES=300,
CELERY_QUEUES=( CELERY_QUEUES=(
Queue(HOSTNAME + '.man.slow', Exchange('manager.slow', type='direct'), Queue(QUEUE_NAME, Exchange('manager.slow', type='direct'),
routing_key="manager.slow"), routing_key="manager.slow"),
), ),
CELERYBEAT_SCHEDULE={ CELERYBEAT_SCHEDULE={
...@@ -48,3 +50,10 @@ celery.conf.update( ...@@ -48,3 +50,10 @@ celery.conf.update(
} }
) )
@worker_ready.connect()
def cleanup_tasks(conf=None, **kwargs):
'''Discard all task and clean up activity.'''
from vm.models.activity import cleanup
cleanup(queue_name=QUEUE_NAME)
...@@ -657,7 +657,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -657,7 +657,7 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
context = super(VlanDetail, self).get_context_data(**kwargs) context = super(VlanDetail, self).get_context_data(**kwargs)
q = Host.objects.filter(interface__in=Interface.objects.filter( q = Host.objects.filter(interface__in=Interface.objects.filter(
vlan=self.object, instance__destroyed_at=None vlan=self.object
)) ))
context['host_list'] = SmallHostTable(q) context['host_list'] = SmallHostTable(q)
......
...@@ -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
...@@ -490,6 +491,9 @@ class Disk(TimeStampedModel): ...@@ -490,6 +491,9 @@ class Disk(TimeStampedModel):
disk.destroy() disk.destroy()
raise humanize_exception(ugettext_noop( raise humanize_exception(ugettext_noop(
"Operation aborted by user."), e) "Operation aborted by user."), e)
except:
disk.destroy()
raise
disk.is_ready = True disk.is_ready = True
disk.save() disk.save()
return disk return disk
# flake8: noqa # flake8: noqa
from .activity import InstanceActivity from .activity import InstanceActivity
from .activity import instance_activity
from .activity import NodeActivity from .activity import NodeActivity
from .activity import node_activity from .activity import node_activity
from .common import BaseResourceConfigModel from .common import BaseResourceConfigModel
from .common import Lease from .common import Lease
from .common import NamedBaseResourceConfig from .common import NamedBaseResourceConfig
from .common import Trait from .common import Trait
from .instance import InstanceActiveManager
from .instance import VirtualMachineDescModel from .instance import VirtualMachineDescModel
from .instance import InstanceTemplate from .instance import InstanceTemplate
from .instance import Instance from .instance import Instance
...@@ -19,9 +17,9 @@ from .network import Interface ...@@ -19,9 +17,9 @@ from .network import Interface
from .node import Node from .node import Node
__all__ = [ __all__ = [
'InstanceActivity', 'InstanceActiveManager', 'BaseResourceConfigModel', 'InstanceActivity', 'BaseResourceConfigModel',
'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate', 'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate',
'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed', 'Instance', 'post_state_changed', 'pre_state_changed', 'InterfaceTemplate',
'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease', 'node_activity',
'node_activity', 'pwgen' 'pwgen'
] ]
...@@ -20,7 +20,6 @@ from contextlib import contextmanager ...@@ -20,7 +20,6 @@ from contextlib import contextmanager
from logging import getLogger from logging import getLogger
from warnings import warn from warnings import warn
from celery.signals import worker_ready
from celery.contrib.abortable import AbortableAsyncResult from celery.contrib.abortable import AbortableAsyncResult
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
...@@ -206,21 +205,6 @@ class InstanceActivity(ActivityModel): ...@@ -206,21 +205,6 @@ class InstanceActivity(ActivityModel):
self.activity_code) self.activity_code)
@contextmanager
def instance_activity(code_suffix, instance, on_abort=None, on_commit=None,
task_uuid=None, user=None, concurrency_check=True,
readable_name=None, resultant_state=None):
"""Create a transactional context for an instance activity.
"""
if not readable_name:
warn("Set readable_name", stacklevel=3)
act = InstanceActivity.create(code_suffix, instance, task_uuid, user,
concurrency_check,
readable_name=readable_name,
resultant_state=resultant_state)
return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
class NodeActivity(ActivityModel): class NodeActivity(ActivityModel):
ACTIVITY_CODE_BASE = join_activity_code('vm', 'Node') ACTIVITY_CODE_BASE = join_activity_code('vm', 'Node')
node = ForeignKey('Node', related_name='activity_log', node = ForeignKey('Node', related_name='activity_log',
...@@ -278,15 +262,15 @@ def node_activity(code_suffix, node, task_uuid=None, user=None, ...@@ -278,15 +262,15 @@ def node_activity(code_suffix, node, task_uuid=None, user=None,
return activitycontextimpl(act) return activitycontextimpl(act)
@worker_ready.connect()
def cleanup(conf=None, **kwargs): def cleanup(conf=None, **kwargs):
# TODO check if other manager workers are running # TODO check if other manager workers are running
from celery.task.control import discard_all
discard_all()
msg_txt = ugettext_noop("Manager is restarted, activity is cleaned up. " msg_txt = ugettext_noop("Manager is restarted, activity is cleaned up. "
"You can try again now.") "You can try again now.")
message = create_readable(msg_txt, msg_txt) message = create_readable(msg_txt, msg_txt)
queue_name = kwargs.get('queue_name', None)
for i in InstanceActivity.objects.filter(finished__isnull=True): for i in InstanceActivity.objects.filter(finished__isnull=True):
op = i.get_operation()
if op and op.async_queue == queue_name:
i.finish(False, result=message) i.finish(False, result=message)
logger.error('Forced finishing stale activity %s', i) logger.error('Forced finishing stale activity %s', i)
for i in NodeActivity.objects.filter(finished__isnull=True): for i in NodeActivity.objects.filter(finished__isnull=True):
......
...@@ -26,7 +26,6 @@ from django.utils.translation import ugettext_lazy as _, ugettext_noop ...@@ -26,7 +26,6 @@ from django.utils.translation import ugettext_lazy as _, ugettext_noop
from common.models import create_readable from common.models import create_readable
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from ..tasks import net_tasks from ..tasks import net_tasks
from .activity import instance_activity
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -120,10 +119,10 @@ class Interface(Model): ...@@ -120,10 +119,10 @@ class Interface(Model):
host.hostname = instance.vm_name host.hostname = instance.vm_name
# Get addresses from firewall # Get addresses from firewall
if base_activity is None: if base_activity is None:
act_ctx = instance_activity( act_ctx = instance.activity(
code_suffix='allocating_ip', code_suffix='allocating_ip',
readable_name=ugettext_noop("allocate IP address"), readable_name=ugettext_noop("allocate IP address"),
instance=instance, user=owner) user=owner)
else: else:
act_ctx = base_activity.sub_activity( act_ctx = base_activity.sub_activity(
'allocating_ip', 'allocating_ip',
......
...@@ -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):
...@@ -114,8 +116,8 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -114,8 +116,8 @@ class Node(OperatedMixin, TimeStampedModel):
def get_info(self): def get_info(self):
return self.remote_query(vm_tasks.get_info, return self.remote_query(vm_tasks.get_info,
priority='fast', priority='fast',
default={'core_num': '', default={'core_num': 0,
'ram_size': '0', 'ram_size': 0,
'architecture': ''}) 'architecture': ''})
info = property(get_info) info = property(get_info)
...@@ -313,10 +315,11 @@ class Node(OperatedMixin, TimeStampedModel): ...@@ -313,10 +315,11 @@ class Node(OperatedMixin, TimeStampedModel):
def get_status_label(self): def get_status_label(self):
return { return {
'OFFLINE': 'label-warning', 'OFFLINE': 'label-warning',
'DISABLED': 'label-warning', 'DISABLED': 'label-danger',
'MISSING': 'label-danger', 'MISSING': 'label-danger',
'ONLINE': 'label-success'}.get(self.get_state(), 'ACTIVE': 'label-success',
'label-danger') 'PASSIVE': 'label-warning',
}.get(self.get_state(), 'label-danger')
@node_available @node_available
def update_vm_states(self): def update_vm_states(self):
......
...@@ -53,8 +53,18 @@ def start_access_server(vm): ...@@ -53,8 +53,18 @@ def start_access_server(vm):
pass pass
@celery.task(name='agent.update_legacy')
def update_legacy(vm, data, executable=None):
pass
@celery.task(name='agent.append')
def append(vm, data, filename, chunk_number):
pass
@celery.task(name='agent.update') @celery.task(name='agent.update')
def update(vm, data): def update(vm, filename, executable, checksum):
pass pass
......
...@@ -15,169 +15,26 @@ ...@@ -15,169 +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, change_ip)
from firewall.models import Host
import time
from base64 import encodestring
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_agent_tar(): from manager.mancelery import celery
def exclude(tarinfo):
if tarinfo.name.startswith('./.git'):
return None
else:
return tarinfo
f = StringIO()
with TarFile.open(fileobj=f, mode='w|gz') as tar:
tar.add(settings.AGENT_DIR, 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', '')
@celery.task @celery.task
def agent_started(vm, version=None): def agent_started(vm, version=None, system=None):
from vm.models import Instance, instance_activity, 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,
instance=instance) 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)
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
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')
with act.sub_activity('stopping', readable_name=ugettext_noop('stopping')): with act.sub_activity('stopping', concurrency_check=False,
readable_name=ugettext_noop('stopping')):
pass 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):
if act:
act = act.sub_activity(
'update',
readable_name=create_readable(
ugettext_noop('update to %(version)s'),
version=settings.AGENT_VERSION))
else:
from vm.models import instance_activity
act = instance_activity(
code_suffix='agent.update', instance=instance,
readable_name=create_readable(
ugettext_noop('update agent to %(version)s'),
version=settings.AGENT_VERSION))
with act:
queue = instance.get_remote_queue_name("agent")
update.apply_async(
queue=queue,
args=(instance.vm_name, create_agent_tar())).get(timeout=10)
...@@ -29,7 +29,8 @@ from ..models import ( ...@@ -29,7 +29,8 @@ from ..models import (
) )
from ..models.instance import find_unused_port, ActivityInProgressError from ..models.instance import find_unused_port, ActivityInProgressError
from ..operations import ( from ..operations import (
DeployOperation, DestroyOperation, FlushOperation, MigrateOperation, RemoteOperationMixin, DeployOperation, DestroyOperation, FlushOperation,
MigrateOperation,
) )
...@@ -89,7 +90,7 @@ class InstanceTestCase(TestCase): ...@@ -89,7 +90,7 @@ class InstanceTestCase(TestCase):
self.assertFalse(inst.save.called) self.assertFalse(inst.save.called)
def test_destroy_sets_destroyed(self): def test_destroy_sets_destroyed(self):
inst = Mock(destroyed_at=None, spec=Instance, inst = Mock(destroyed_at=None, spec=Instance, _delete_vm=Mock(),
InstanceDestroyedError=Instance.InstanceDestroyedError) InstanceDestroyedError=Instance.InstanceDestroyedError)
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.disks.all.return_value = [] inst.disks.all.return_value = []
...@@ -105,7 +106,8 @@ class InstanceTestCase(TestCase): ...@@ -105,7 +106,8 @@ class InstanceTestCase(TestCase):
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING' inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst) migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr: with patch('vm.operations.vm_tasks.migrate') as migr, \
patch.object(RemoteOperationMixin, "_operation"):
act = MagicMock() act = MagicMock()
with patch.object(MigrateOperation, 'create_activity', with patch.object(MigrateOperation, 'create_activity',
return_value=act): return_value=act):
...@@ -121,7 +123,8 @@ class InstanceTestCase(TestCase): ...@@ -121,7 +123,8 @@ class InstanceTestCase(TestCase):
inst.node = MagicMock(spec=Node) inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING' inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst) migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr: with patch('vm.operations.vm_tasks.migrate') as migr, \
patch.object(RemoteOperationMixin, "_operation"):
inst.select_node.side_effect = AssertionError inst.select_node.side_effect = AssertionError
act = MagicMock() act = MagicMock()
with patch.object(MigrateOperation, 'create_activity', with patch.object(MigrateOperation, 'create_activity',
...@@ -138,19 +141,22 @@ class InstanceTestCase(TestCase): ...@@ -138,19 +141,22 @@ class InstanceTestCase(TestCase):
inst.status = 'RUNNING' inst.status = 'RUNNING'
e = Exception('abc') e = Exception('abc')
setattr(e, 'libvirtError', '') setattr(e, 'libvirtError', '')
inst.migrate_vm.side_effect = e
migrate_op = MigrateOperation(inst) migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr: migrate_op.rollback = Mock()
with patch('vm.operations.vm_tasks.migrate') as migr, \
patch.object(RemoteOperationMixin, '_operation') as remop:
act = MagicMock() act = MagicMock()
remop.side_effect = e
with patch.object(MigrateOperation, 'create_activity', with patch.object(MigrateOperation, 'create_activity',
return_value=act): return_value=act):
self.assertRaises(Exception, migrate_op, system=True) self.assertRaises(Exception, migrate_op, system=True)
remop.assert_called()
migr.apply_async.assert_called() migr.apply_async.assert_called()
self.assertIn(call.sub_activity( self.assertIn(call.sub_activity(
u'rollback_net', readable_name=u'redeploy network (rollback)'), u'scheduling', readable_name=u'schedule'), act.mock_calls)
act.mock_calls) migrate_op.rollback.assert_called()
inst.allocate_node.assert_called() inst.select_node.assert_called()
def test_status_icon(self): def test_status_icon(self):
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
......
...@@ -16,9 +16,10 @@ ...@@ -16,9 +16,10 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>. # with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
from django.test import TestCase from django.test import TestCase
from mock import MagicMock
from common.operations import operation_registry_name as op_reg_name from common.operations import operation_registry_name as op_reg_name
from vm.models import Instance, Node from vm.models import Instance, InstanceActivity, Node
from vm.operations import ( from vm.operations import (
DeployOperation, DestroyOperation, FlushOperation, MigrateOperation, DeployOperation, DestroyOperation, FlushOperation, MigrateOperation,
RebootOperation, ResetOperation, SaveAsTemplateOperation, RebootOperation, ResetOperation, SaveAsTemplateOperation,
...@@ -45,6 +46,22 @@ class MigrateOperationTestCase(TestCase): ...@@ -45,6 +46,22 @@ class MigrateOperationTestCase(TestCase):
def test_operation_registered(self): def test_operation_registered(self):
assert MigrateOperation.id in getattr(Instance, op_reg_name) assert MigrateOperation.id in getattr(Instance, op_reg_name)
def test_operation_wo_to_node_param(self):
class MigrateException(Exception):
pass
inst = MagicMock(spec=Instance)
act = MagicMock(spec=InstanceActivity)
op = MigrateOperation(inst)
op._get_remote_args = MagicMock(side_effect=MigrateException())
inst.select_node = MagicMock(return_value='test')
self.assertRaises(
MigrateException, op._operation,
act, to_node=None)
assert inst.select_node.called
op._get_remote_args.assert_called_once_with(
to_node='test', live_migration=True)
class RebootOperationTestCase(TestCase): class RebootOperationTestCase(TestCase):
def test_operation_registered(self): def test_operation_registered(self):
......
...@@ -6,9 +6,14 @@ respawn limit 30 30 ...@@ -6,9 +6,14 @@ respawn limit 30 30
setgid cloud setgid cloud
setuid cloud setuid cloud
kill timeout 360
kill signal SIGTERM
script script
cd /home/cloud/circle/circle cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate . /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate . /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 10 ./manage.py celery -f --app=manager.mancelery purge
exec ./manage.py celery --app=manager.mancelery worker --autoreload --loglevel=info --hostname=mancelery -B -c 3
end script end script
...@@ -3,6 +3,7 @@ description "CIRCLE moncelery for monitoring jobs" ...@@ -3,6 +3,7 @@ description "CIRCLE moncelery for monitoring jobs"
respawn respawn
respawn limit 30 30 respawn limit 30 30
setgid cloud setgid cloud
setuid cloud setuid cloud
...@@ -10,5 +11,7 @@ script ...@@ -10,5 +11,7 @@ script
cd /home/cloud/circle/circle cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate . /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate . /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.moncelery worker --autoreload --loglevel=info --hostname=moncelery -B -c 3 ./manage.py celery -f --app=manager.moncelery purge
exec ./manage.py celery --app=manager.moncelery worker --autoreload --loglevel=info --hostname=moncelery -B -c 2
end script end script
description "CIRCLE mancelery for slow jobs" description "CIRCLE slowcelery for resource intensive or long jobs"
respawn respawn
respawn limit 30 30 respawn limit 30 30
...@@ -6,9 +6,15 @@ respawn limit 30 30 ...@@ -6,9 +6,15 @@ respawn limit 30 30
setgid cloud setgid cloud
setuid cloud setuid cloud
kill timeout 360
kill signal INT
script script
cd /home/cloud/circle/circle cd /home/cloud/circle/circle
. /home/cloud/.virtualenvs/circle/bin/activate . /home/cloud/.virtualenvs/circle/bin/activate
. /home/cloud/.virtualenvs/circle/bin/postactivate . /home/cloud/.virtualenvs/circle/bin/postactivate
exec ./manage.py celery --app=manager.slowcelery worker --autoreload --loglevel=info --hostname=slowcelery -B -c 5 ./manage.py celery -f --app=manager.slowcelery purge
exec ./manage.py celery --app=manager.slowcelery worker --autoreload --loglevel=info --hostname=slowcelery -B -c 1
end script end script
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment