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
.coverage
*,cover
coverage.xml
.noseids
# Gettext object file:
*.mo
......
......@@ -71,6 +71,17 @@ class AclBase(Model):
"""Define permission levels for Users/Groups per object."""
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
def get_level_object(cls, level):
......@@ -229,7 +240,7 @@ class AclBase(Model):
levelfilter,
content_type=ct, level__weight__gte=level.weight).distinct()
clsfilter = Q(object_level_set__in=ols.all())
return cls.objects.filter(clsfilter)
return cls.objects.filter(clsfilter).distinct()
def save(self, *args, **kwargs):
super(AclBase, self).save(*args, **kwargs)
......
......@@ -514,9 +514,18 @@ LOGIN_REDIRECT_URL = "/"
AGENT_DIR = get_env_variable(
'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:
git_env = {'GIT_DIR': join(AGENT_DIR, '.git')}
git_env = {'GIT_DIR': join(join(AGENT_DIR, "agent-linux"), '.git')}
AGENT_VERSION = check_output(
('git', 'log', '-1', r'--pretty=format:%h', 'HEAD'), env=git_env)
except:
......
......@@ -32,6 +32,7 @@ from django.core.serializers.json import DjangoJSONEncoder
from django.db.models import (
CharField, DateTimeField, ForeignKey, NullBooleanField
)
from django.template import defaultfilters
from django.utils import timezone
from django.utils.encoding import force_text
from django.utils.functional import Promise
......@@ -48,7 +49,15 @@ class WorkerNotFound(Exception):
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):
result = None
try:
try:
yield act
......@@ -61,7 +70,7 @@ def activitycontextimpl(act, on_abort=None, on_commit=None):
result = create_readable(
ugettext_noop("Failure."),
ugettext_noop("Unhandled exception: %(error)s"),
error=unicode(e))
error=get_error_msg(e))
raise
except:
logger.exception("Failed activity %s" % unicode(act))
......@@ -428,6 +437,14 @@ class HumanReadableObject(object):
admin_text_template = admin_text_template._proxy____args[0]
self.user_text_template = user_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
@classmethod
......@@ -444,24 +461,27 @@ class HumanReadableObject(object):
def from_dict(cls, d):
return None if d is None else cls(**d)
def get_admin_text(self):
if self.admin_text_template == "":
def _get_parsed_text(self, key):
value = getattr(self, key)
if value == "":
return ""
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:
logger.exception("Can't render admin_text_template '%s' %% %s",
self.admin_text_template, unicode(self.params))
return self.get_user_text()
def get_user_text(self):
if self.user_text_template == "":
return ""
try:
return _(self.user_text_template) % self.params
return self._get_parsed_text("user_text_template")
except KeyError:
logger.exception("Can't render user_text_template '%s' %% %s",
self.user_text_template, unicode(self.params))
return self.user_text_template
def get_text(self, user):
......
......@@ -26,6 +26,17 @@ from .models import activity_context, has_suffix, humanize_exception
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):
"""Base class for VM operations.
"""
......@@ -36,6 +47,10 @@ class Operation(object):
abortable = False
has_percentage = False
@classmethod
def get_activity_code_suffix(cls):
return cls.id
def __call__(self, **kwargs):
return self.call(**kwargs)
......@@ -62,6 +77,8 @@ class Operation(object):
parent_activity = auxargs.pop('parent_activity')
if parent_activity and user is None and not skip_auth_check:
user = parent_activity.user
if user is None: # parent was a system call
skip_auth_check = True
# check for unexpected keyword arguments
argspec = getargspec(self._operation)
......@@ -232,7 +249,7 @@ class OperatedMixin(object):
operation could be found.
"""
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)
else:
return None
......
......@@ -22,6 +22,7 @@ from mock import MagicMock
from .models import TestClass
from ..models import HumanSortField
from ..models import activitycontextimpl
class MethodCacheTestCase(TestCase):
......@@ -80,3 +81,22 @@ class TestHumanSortField(TestCase):
test_result = HumanSortField.get_normalized_value(obj, val)
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):
class AbortEx(Exception):
pass
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
op = TestOp(MagicMock())
op.async_operation = MagicMock(
apply_async=MagicMock(side_effect=AbortEx))
......@@ -44,9 +42,7 @@ class OperationTestCase(TestCase):
class AbortEx(Exception):
pass
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
op = TestOp(MagicMock())
with patch.object(Operation, 'create_activity', side_effect=AbortEx):
with patch.object(Operation, 'check_precond') as chk_pre:
try:
......@@ -55,9 +51,7 @@ class OperationTestCase(TestCase):
self.assertTrue(chk_pre.called)
def test_auth_check_on_non_system_call(self):
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
op = TestOp(MagicMock())
user = MagicMock()
with patch.object(Operation, 'check_auth') as check_auth:
with patch.object(Operation, 'check_precond'), \
......@@ -67,9 +61,7 @@ class OperationTestCase(TestCase):
check_auth.assert_called_with(user)
def test_no_auth_check_on_system_call(self):
op = Operation(MagicMock())
op.activity_code_suffix = 'test'
op.id = 'test'
op = TestOp(MagicMock())
with patch.object(Operation, 'check_auth', side_effect=AssertionError):
with patch.object(Operation, 'check_precond'), \
patch.object(Operation, 'create_activity'), \
......@@ -77,39 +69,25 @@ class OperationTestCase(TestCase):
op.call(system=True)
def test_no_exception_for_more_arguments_when_operation_takes_kwargs(self):
class KwargOp(Operation):
activity_code_suffix = 'test'
id = 'test'
def _operation(self, **kwargs):
pass
op = KwargOp(MagicMock())
with patch.object(KwargOp, 'create_activity'), \
patch.object(KwargOp, '_exec_op'):
op = TestOp(MagicMock())
with patch.object(TestOp, 'create_activity'), \
patch.object(TestOp, '_exec_op'):
op.call(system=True, foo=42)
def test_exception_for_unexpected_arguments(self):
class TestOp(Operation):
activity_code_suffix = 'test'
id = 'test'
def _operation(self):
pass
op = TestOp(MagicMock())
with patch.object(TestOp, 'create_activity'), \
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):
class TestOp(Operation):
activity_code_suffix = 'test'
id = 'test'
def _operation(self, foo):
pass
op = TestOp(MagicMock())
with patch.object(TestOp, 'create_activity'):
self.assertRaises(TypeError, op.call, system=True)
class TestOp(Operation):
id = 'test'
def _operation(self, foo):
pass
......@@ -18,6 +18,7 @@
from __future__ import absolute_import
from datetime import timedelta
from urlparse import urlparse
from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm,
......@@ -39,6 +40,7 @@ from django.contrib.auth.forms import UserCreationForm as OrgUserCreationForm
from django.forms.widgets import TextInput, HiddenInput
from django.template import Context
from django.template.loader import render_to_string
from django.utils.html import escape
from django.utils.translation import ugettext_lazy as _
from sizefield.widgets import FileSizeWidget
from django.core.urlresolvers import reverse_lazy
......@@ -69,9 +71,7 @@ priority_choices = (
)
class VmSaveForm(forms.Form):
name = forms.CharField(max_length=100, label=_('Name'),
help_text=_('Human readable name of template.'))
class NoFormTagMixin(object):
@property
def helper(self):
......@@ -80,6 +80,27 @@ class VmSaveForm(forms.Form):
return helper
class OperationForm(NoFormTagMixin, forms.Form):
pass
class VmSaveForm(OperationForm):
name = forms.CharField(max_length=100, label=_('Name'),
help_text=_('Human readable name of template.'))
def __init__(self, *args, **kwargs):
default = kwargs.pop('default', None)
clone = kwargs.pop('clone', False)
super(VmSaveForm, self).__init__(*args, **kwargs)
if default:
self.fields['name'].initial = default
if clone:
self.fields.insert(2, "clone", forms.BooleanField(
required=False, label=_("Clone template permissions"),
help_text=_("Clone the access list of parent template. Useful "
"for updating a template.")))
class VmCustomizeForm(forms.Form):
name = forms.CharField(widget=forms.TextInput(attrs={
'class': "form-control",
......@@ -185,7 +206,7 @@ class VmCustomizeForm(forms.Form):
del self.cleaned_data[name]
class GroupCreateForm(forms.ModelForm):
class GroupCreateForm(NoFormTagMixin, forms.ModelForm):
description = forms.CharField(label=_("Description"), required=False,
widget=forms.Textarea(attrs={'rows': 3}))
......@@ -224,9 +245,8 @@ class GroupCreateForm(forms.ModelForm):
@property
def helper(self):
helper = FormHelper(self)
helper = super(GroupCreateForm, self).helper
helper.add_input(Submit("submit", _("Create")))
helper.form_tag = False
return helper
class Meta:
......@@ -234,7 +254,7 @@ class GroupCreateForm(forms.ModelForm):
fields = ('name', )
class GroupProfileUpdateForm(forms.ModelForm):
class GroupProfileUpdateForm(NoFormTagMixin, forms.ModelForm):
def __init__(self, *args, **kwargs):
new_groups = kwargs.pop('new_groups', None)
......@@ -253,9 +273,8 @@ class GroupProfileUpdateForm(forms.ModelForm):
@property
def helper(self):
helper = FormHelper(self)
helper = super(GroupProfileUpdateForm, self).helper
helper.add_input(Submit("submit", _("Save")))
helper.form_tag = False
return helper
def save(self, commit=True):
......@@ -270,17 +289,16 @@ class GroupProfileUpdateForm(forms.ModelForm):
fields = ('description', 'org_id')
class HostForm(forms.ModelForm):
class HostForm(NoFormTagMixin, forms.ModelForm):
def setowner(self, user):
self.instance.owner = user
def __init__(self, *args, **kwargs):
super(HostForm, self).__init__(*args, **kwargs)
self.helper = FormHelper(self)
self.helper.form_show_labels = False
self.helper.form_tag = False
self.helper.layout = Layout(
@property
def helper(self):
helper = super(HostForm, self).helper
helper.form_show_labels = False
helper.layout = Layout(
Div(
Div( # host
Div(
......@@ -337,6 +355,7 @@ class HostForm(forms.ModelForm):
),
),
)
return helper
class Meta:
model = Host
......@@ -717,7 +736,7 @@ class LeaseForm(forms.ModelForm):
model = Lease
class VmRenewForm(forms.Form):
class VmRenewForm(OperationForm):
force = forms.BooleanField(required=False, label=_(
"Set expiration times even if they are shorter than "
......@@ -737,14 +756,27 @@ class VmRenewForm(forms.Form):
self.fields['lease'].widget = HiddenInput()
self.fields['save'].widget = HiddenInput()
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class VmMigrateForm(forms.Form):
live_migration = forms.BooleanField(
required=False, initial=True, label=_("Live migration"),
help_text=_(
"Live migration is a way of moving virtual machines between "
"hosts with a service interruption of at most some seconds. "
"Please note that it can take very long and cause "
"much network traffic in case of busy machines."))
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
default = kwargs.pop('default')
super(VmMigrateForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'to_node', forms.ModelChoiceField(
queryset=choices, initial=default, required=False,
widget=forms.RadioSelect(), label=_("Node")))
class VmStateChangeForm(forms.Form):
class VmStateChangeForm(OperationForm):
interrupt = forms.BooleanField(required=False, label=_(
"Forcibly interrupt all running activities."),
......@@ -763,31 +795,25 @@ class VmStateChangeForm(forms.Form):
self.fields['interrupt'].widget = HiddenInput()
self.fields['new_state'].initial = status
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class RedeployForm(forms.Form):
class RedeployForm(OperationForm):
with_emergency_change_state = forms.BooleanField(
required=False, initial=True, label=_("use emergency state change"))
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class VmCreateDiskForm(forms.Form):
class VmCreateDiskForm(OperationForm):
name = forms.CharField(max_length=100, label=_("Name"))
size = forms.CharField(
widget=FileSizeWidget, initial=(10 << 30), label=_('Size'),
help_text=_('Size of disk to create in bytes or with units '
'like MB or GB.'))
def __init__(self, *args, **kwargs):
default = kwargs.pop('default', None)
super(VmCreateDiskForm, self).__init__(*args, **kwargs)
if default:
self.fields['name'].initial = default
def clean_size(self):
size_in_bytes = self.cleaned_data.get("size")
if not size_in_bytes.isdigit() and len(size_in_bytes) > 0:
......@@ -795,14 +821,8 @@ class VmCreateDiskForm(forms.Form):
" GB or MB!"))
return size_in_bytes
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class VmDiskResizeForm(forms.Form):
class VmDiskResizeForm(OperationForm):
size = forms.CharField(
widget=FileSizeWidget, initial=(10 << 30), label=_('Size'),
help_text=_('Size to resize the disk in bytes or with units '
......@@ -835,27 +855,60 @@ class VmDiskResizeForm(forms.Form):
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
helper = super(VmDiskResizeForm, self).helper
if self.disk:
helper.layout = Layout(
HTML(_("<label>Disk:</label> %s") % self.disk),
HTML(_("<label>Disk:</label> %s") % escape(self.disk)),
Field('disk'), Field('size'))
return helper
class VmDownloadDiskForm(forms.Form):
name = forms.CharField(max_length=100, label=_("Name"))
url = forms.CharField(label=_('URL'), validators=[URLValidator(), ])
class VmDiskRemoveForm(OperationForm):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
self.disk = kwargs.pop('default')
super(VmDiskRemoveForm, self).__init__(*args, **kwargs)
self.fields.insert(0, 'disk', forms.ModelChoiceField(
queryset=choices, initial=self.disk, required=True,
empty_label=None, label=_('Disk')))
if self.disk:
self.fields['disk'].widget = HiddenInput()
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
helper = super(VmDiskRemoveForm, self).helper
if self.disk:
helper.layout = Layout(
AnyTag(
"div",
HTML(_("<label>Disk:</label> %s") % escape(self.disk)),
css_class="form-group",
),
Field("disk"),
)
return helper
class VmAddInterfaceForm(forms.Form):
class VmDownloadDiskForm(OperationForm):
name = forms.CharField(max_length=100, label=_("Name"), required=False)
url = forms.CharField(label=_('URL'), validators=[URLValidator(), ])
def clean(self):
cleaned_data = super(VmDownloadDiskForm, self).clean()
if not cleaned_data['name']:
if cleaned_data['url']:
cleaned_data['name'] = urlparse(
cleaned_data['url']).path.split('/')[-1]
if not cleaned_data['name']:
raise forms.ValidationError(
_("Could not find filename in URL, "
"please specify a name explicitly."))
return cleaned_data
class VmAddInterfaceForm(OperationForm):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
super(VmAddInterfaceForm, self).__init__(*args, **kwargs)
......@@ -867,11 +920,19 @@ class VmAddInterfaceForm(forms.Form):
field.empty_label = _('No more networks.')
self.fields['vlan'] = field
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class VmDeployForm(OperationForm):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices', None)
super(VmDeployForm, self).__init__(*args, **kwargs)
if choices is not None:
self.fields.insert(0, 'node', forms.ModelChoiceField(
queryset=choices, required=False, label=_('Node'), help_text=_(
"Deploy virtual machine to this node "
"(blank allows scheduling automatically).")))
class CircleAuthenticationForm(AuthenticationForm):
......
......@@ -236,6 +236,9 @@ class GroupProfile(AclBase):
help_text=_('Unique identifier of the group at the organization.'))
description = TextField(blank=True)
def __unicode__(self):
return self.group.name
def save(self, *args, **kwargs):
if not self.org_id:
self.org_id = None
......
......@@ -411,14 +411,25 @@ $(function () {
$(this).removeClass("btn-default").addClass("btn-primary");
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) {
return '<a href="/dashboard/vm/' + pk + '/" class="list-group-item' +
(is_last ? ' list-group-item-last' : '') + '">' +
'<span class="index-vm-list-name">' +
'<i class="fa ' + icon + '" title="' + _status + '"></i> ' + name +
'</span>' +
(is_last ? ' list-group-item-last' : '') + '">' +
'<span class="index-vm-list-name">' +
'<i class="fa ' + icon + '" title="' + _status + '"></i> ' + safe_tags_replace(name) +
'</span>' +
'<small class="text-muted"> ' + host + '</small>' +
'<div class="pull-right dashboard-vm-favourite" data-vm="' + pk + '">' +
(fav ? '<i class="fa fa-star text-primary title-favourite" title="Unfavourite"></i>' :
......@@ -430,14 +441,14 @@ function generateVmHTML(pk, name, host, icon, _status, fav, is_last) {
function generateGroupHTML(url, name, is_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>';
}
function generateNodeHTML(name, icon, _status, url, is_last) {
return '<a href="' + url + '" class="list-group-item real-link' + (is_last ? ' list-group-item-last' : '') + '">' +
'<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>' +
'<div style="clear: both;"></div>' +
'</a>';
......@@ -445,7 +456,7 @@ function generateNodeHTML(name, icon, _status, url, is_last) {
function generateNodeTagHTML(name, icon, _status, label , url) {
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> ';
}
......@@ -619,7 +630,7 @@ function addModalConfirmation(func, data) {
}
function clientInstalledAction(location) {
setCookie('downloaded_client', true, 365 * 24 * 60 * 60, "/");
setCookie('downloaded_client', true, 365 * 24 * 60 * 60 * 1000, "/");
window.location.href = location;
$('#confirmation-modal').modal("hide");
}
......@@ -687,3 +698,17 @@ $(function() {
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);
}