Commit 3b72505e by Bach Dániel

Merge branch 'master' into feature-store

Conflicts:
	circle/dashboard/static/dashboard/dashboard.css
	circle/dashboard/urls.py
	circle/dashboard/views.py
parents 822e58c5 89d48699
...@@ -260,6 +260,7 @@ THIRD_PARTY_APPS = ( ...@@ -260,6 +260,7 @@ THIRD_PARTY_APPS = (
'taggit', 'taggit',
'statici18n', 'statici18n',
'django_sshkey', 'django_sshkey',
'autocomplete_light',
) )
# Apps specific for this project go here. # Apps specific for this project go here.
......
...@@ -35,7 +35,11 @@ SOUTH_TESTS_MIGRATE = False ...@@ -35,7 +35,11 @@ SOUTH_TESTS_MIGRATE = False
INSTALLED_APPS += ( INSTALLED_APPS += (
'acl.tests', 'acl.tests',
'django_nose',
) )
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = ['--with-doctest']
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
CACHES = { CACHES = {
'default': { 'default': {
......
...@@ -23,6 +23,7 @@ from logging import getLogger ...@@ -23,6 +23,7 @@ from logging import getLogger
from time import time from time import time
from warnings import warn from warnings import warn
from django.contrib import messages
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.cache import cache from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
...@@ -46,17 +47,24 @@ class WorkerNotFound(Exception): ...@@ -46,17 +47,24 @@ class WorkerNotFound(Exception):
def activitycontextimpl(act, on_abort=None, on_commit=None): def activitycontextimpl(act, on_abort=None, on_commit=None):
try: try:
yield act try:
except BaseException as e: yield act
# BaseException is the common parent of Exception and except HumanReadableException as e:
# system-exiting exceptions, e.g. KeyboardInterrupt result = e
raise
except BaseException as e:
# BaseException is the common parent of Exception and
# system-exiting exceptions, e.g. KeyboardInterrupt
result = create_readable(
ugettext_noop("Failure."),
ugettext_noop("Unhandled exception: %(error)s"),
error=unicode(e))
raise
except:
logger.exception("Failed activity %s" % unicode(act))
handler = None if on_abort is None else lambda a: on_abort(a, e) handler = None if on_abort is None else lambda a: on_abort(a, e)
result = create_readable(ugettext_noop("Failure."),
ugettext_noop("Unhandled exception: "
"%(error)s"),
error=unicode(e))
act.finish(succeeded=False, result=result, event_handler=handler) act.finish(succeeded=False, result=result, event_handler=handler)
raise e raise
else: else:
act.finish(succeeded=True, event_handler=on_commit) act.finish(succeeded=True, event_handler=on_commit)
...@@ -70,11 +78,11 @@ activity_code_separator = '.' ...@@ -70,11 +78,11 @@ activity_code_separator = '.'
def has_prefix(activity_code, *prefixes): def has_prefix(activity_code, *prefixes):
"""Determine whether the activity code has the specified prefix. """Determine whether the activity code has the specified prefix.
E.g.: has_prefix('foo.bar.buz', 'foo.bar') == True >>> assert has_prefix('foo.bar.buz', 'foo.bar')
has_prefix('foo.bar.buz', 'foo', 'bar') == True >>> assert has_prefix('foo.bar.buz', 'foo', 'bar')
has_prefix('foo.bar.buz', 'foo.bar', 'buz') == True >>> assert has_prefix('foo.bar.buz', 'foo.bar', 'buz')
has_prefix('foo.bar.buz', 'foo', 'bar', 'buz') == True >>> assert has_prefix('foo.bar.buz', 'foo', 'bar', 'buz')
has_prefix('foo.bar.buz', 'foo', 'buz') == False >>> assert not has_prefix('foo.bar.buz', 'foo', 'buz')
""" """
equal = lambda a, b: a == b equal = lambda a, b: a == b
act_code_parts = split_activity_code(activity_code) act_code_parts = split_activity_code(activity_code)
...@@ -85,11 +93,11 @@ def has_prefix(activity_code, *prefixes): ...@@ -85,11 +93,11 @@ def has_prefix(activity_code, *prefixes):
def has_suffix(activity_code, *suffixes): def has_suffix(activity_code, *suffixes):
"""Determine whether the activity code has the specified suffix. """Determine whether the activity code has the specified suffix.
E.g.: has_suffix('foo.bar.buz', 'bar.buz') == True >>> assert has_suffix('foo.bar.buz', 'bar.buz')
has_suffix('foo.bar.buz', 'bar', 'buz') == True >>> assert has_suffix('foo.bar.buz', 'bar', 'buz')
has_suffix('foo.bar.buz', 'foo.bar', 'buz') == True >>> assert has_suffix('foo.bar.buz', 'foo.bar', 'buz')
has_suffix('foo.bar.buz', 'foo', 'bar', 'buz') == True >>> assert has_suffix('foo.bar.buz', 'foo', 'bar', 'buz')
has_suffix('foo.bar.buz', 'foo', 'buz') == False >>> assert not has_suffix('foo.bar.buz', 'foo', 'buz')
""" """
equal = lambda a, b: a == b equal = lambda a, b: a == b
act_code_parts = split_activity_code(activity_code) act_code_parts = split_activity_code(activity_code)
...@@ -196,6 +204,10 @@ class ActivityModel(TimeStampedModel): ...@@ -196,6 +204,10 @@ class ActivityModel(TimeStampedModel):
DeprecationWarning, stacklevel=2) DeprecationWarning, stacklevel=2)
value = create_readable(user_text_template="", value = create_readable(user_text_template="",
admin_text_template=value) admin_text_template=value)
elif not hasattr(value, "to_dict"):
warn("Use HumanReadableObject.", DeprecationWarning, stacklevel=2)
value = create_readable(user_text_template="",
admin_text_template=unicode(value))
self.result_data = None if value is None else value.to_dict() self.result_data = None if value is None else value.to_dict()
...@@ -361,8 +373,9 @@ class HumanReadableObject(object): ...@@ -361,8 +373,9 @@ class HumanReadableObject(object):
@classmethod @classmethod
def create(cls, user_text_template, admin_text_template=None, **params): def create(cls, user_text_template, admin_text_template=None, **params):
return cls(user_text_template, return cls(user_text_template=user_text_template,
admin_text_template or user_text_template, params) admin_text_template=(admin_text_template
or user_text_template), params=params)
def set(self, user_text_template, admin_text_template=None, **params): def set(self, user_text_template, admin_text_template=None, **params):
self._set_values(user_text_template, self._set_values(user_text_template,
...@@ -375,12 +388,22 @@ class HumanReadableObject(object): ...@@ -375,12 +388,22 @@ class HumanReadableObject(object):
def get_admin_text(self): def get_admin_text(self):
if self.admin_text_template == "": if self.admin_text_template == "":
return "" return ""
return _(self.admin_text_template) % self.params try:
return _(self.admin_text_template) % self.params
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): def get_user_text(self):
if self.user_text_template == "": if self.user_text_template == "":
return "" return ""
return _(self.user_text_template) % self.params try:
return _(self.user_text_template) % self.params
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 to_dict(self): def to_dict(self):
return {"user_text_template": self.user_text_template, return {"user_text_template": self.user_text_template,
...@@ -397,10 +420,28 @@ create_readable = HumanReadableObject.create ...@@ -397,10 +420,28 @@ create_readable = HumanReadableObject.create
class HumanReadableException(HumanReadableObject, Exception): class HumanReadableException(HumanReadableObject, Exception):
"""HumanReadableObject that is an Exception so can used in except clause. """HumanReadableObject that is an Exception so can used in except clause.
""" """
pass def __init__(self, level=None, *args, **kwargs):
super(HumanReadableException, self).__init__(*args, **kwargs)
if level is not None:
if hasattr(messages, level):
self.level = level
else:
raise ValueError(
"Level should be the name of an attribute of django."
"contrib.messages (and it should be callable with "
"(request, message)). Like 'error', 'warning'.")
else:
self.level = "error"
def send_message(self, request, level=None):
if request.user and request.user.is_superuser:
msg = self.get_admin_text()
else:
msg = self.get_user_text()
getattr(messages, level or self.level)(request, msg)
def humanize_exception(message, exception=None, **params):
def humanize_exception(message, exception=None, level=None, **params):
"""Return new dynamic-class exception which is based on """Return new dynamic-class exception which is based on
HumanReadableException and the original class with the dict of exception. HumanReadableException and the original class with the dict of exception.
...@@ -409,8 +450,10 @@ def humanize_exception(message, exception=None, **params): ...@@ -409,8 +450,10 @@ def humanize_exception(message, exception=None, **params):
... ...
Welcome! Welcome!
""" """
Ex = type("HumanReadable" + type(exception).__name__, Ex = type("HumanReadable" + type(exception).__name__,
(HumanReadableException, type(exception)), (HumanReadableException, type(exception)),
exception.__dict__) exception.__dict__)
return Ex.create(message, **params) ex = Ex.create(message, **params)
if level:
ex.level = level
return ex
import autocomplete_light
from django.utils.translation import ugettext as _
from .views import AclUpdateView
class AclUserAutocomplete(autocomplete_light.AutocompleteGenericBase):
search_fields = (
('^first_name', 'last_name', 'username', '^email', 'profile__org_id'),
('^name', 'groupprofile__org_id'),
)
autocomplete_js_attributes = {'placeholder': _("Name of group or user")}
choice_html_format = u'<span data-value="%s"><span>%s</span> %s</span>'
def choice_html(self, choice):
try:
name = choice.get_full_name()
except AttributeError:
name = _('group')
if name:
name = u'(%s)' % name
return self.choice_html_format % (
self.choice_value(choice), self.choice_label(choice), name)
def choices_for_request(self):
user = self.request.user
self.choices = (AclUpdateView.get_allowed_users(user),
AclUpdateView.get_allowed_groups(user))
return super(AclUserAutocomplete, self).choices_for_request()
autocomplete_light.register(AclUserAutocomplete)
...@@ -1322,7 +1322,7 @@ ...@@ -1322,7 +1322,7 @@
"user_permissions": [ "user_permissions": [
115 115
], ],
"password": "pbkdf2_sha256$10000$KIoeMs78MiOj$PnVXn3YJMehbOciBO32CMzqL0ZnQrzrdb7+b5dE13os=", "password": "md5$qLN4mQMOrsUJ$f07129fd1a289a0afb4e09f7a6816a4f",
"email": "test@example.org", "email": "test@example.org",
"date_joined": "2013-09-04T15:29:49.914Z" "date_joined": "2013-09-04T15:29:49.914Z"
} }
......
...@@ -27,6 +27,7 @@ from django.contrib.auth.models import User, Group ...@@ -27,6 +27,7 @@ from django.contrib.auth.models import User, Group
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
import autocomplete_light
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import ( from crispy_forms.layout import (
Layout, Div, BaseInput, Field, HTML, Submit, Fieldset, TEMPLATE_PACK, Layout, Div, BaseInput, Field, HTML, Submit, Fieldset, TEMPLATE_PACK,
...@@ -44,7 +45,6 @@ from django.core.urlresolvers import reverse_lazy ...@@ -44,7 +45,6 @@ from django.core.urlresolvers import reverse_lazy
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
from firewall.models import Vlan, Host from firewall.models import Vlan, Host
from storage.models import Disk
from vm.models import ( from vm.models import (
InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance InstanceTemplate, Lease, InterfaceTemplate, Node, Trait, Instance
) )
...@@ -54,6 +54,7 @@ from .models import Profile, GroupProfile ...@@ -54,6 +54,7 @@ from .models import Profile, GroupProfile
from circle.settings.base import LANGUAGES from circle.settings.base import LANGUAGES
from django.utils.translation import string_concat from django.utils.translation import string_concat
from .virtvalidator import domain_validator
LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")")) LANGUAGES_WITH_CODE = ((l[0], string_concat(l[1], " (", l[0], ")"))
for l in LANGUAGES) for l in LANGUAGES)
...@@ -78,7 +79,7 @@ class VmCustomizeForm(forms.Form): ...@@ -78,7 +79,7 @@ class VmCustomizeForm(forms.Form):
amount = forms.IntegerField(min_value=0, initial=1) amount = forms.IntegerField(min_value=0, initial=1)
disks = forms.ModelMultipleChoiceField( disks = forms.ModelMultipleChoiceField(
queryset=None, required=True) queryset=None, required=False)
networks = forms.ModelMultipleChoiceField( networks = forms.ModelMultipleChoiceField(
queryset=None, required=False) queryset=None, required=False)
...@@ -91,8 +92,7 @@ class VmCustomizeForm(forms.Form): ...@@ -91,8 +92,7 @@ class VmCustomizeForm(forms.Form):
super(VmCustomizeForm, self).__init__(*args, **kwargs) super(VmCustomizeForm, self).__init__(*args, **kwargs)
# set displayed disk and network list # set displayed disk and network list
self.fields['disks'].queryset = Disk.get_objects_with_level( self.fields['disks'].queryset = self.template.disks.all()
'user', self.user).exclude(type="qcow2-snap")
self.fields['networks'].queryset = Vlan.get_objects_with_level( self.fields['networks'].queryset = Vlan.get_objects_with_level(
'user', self.user) 'user', self.user)
...@@ -596,8 +596,12 @@ class TemplateForm(forms.ModelForm): ...@@ -596,8 +596,12 @@ class TemplateForm(forms.ModelForm):
n = self.instance.interface_set.values_list("vlan", flat=True) n = self.instance.interface_set.values_list("vlan", flat=True)
self.initial['networks'] = n self.initial['networks'] = n
self.allowed_fields = ( if self.instance.pk and not self.instance.has_level(self.user,
'name', 'access_method', 'description', 'system', 'tags') 'owner'):
self.allowed_fields = ()
else:
self.allowed_fields = (
'name', 'access_method', 'description', 'system', 'tags')
if self.user.has_perm('vm.change_template_resources'): if self.user.has_perm('vm.change_template_resources'):
self.allowed_fields += tuple(set(self.fields.keys()) - self.allowed_fields += tuple(set(self.fields.keys()) -
set(['raw_data'])) set(['raw_data']))
...@@ -675,6 +679,11 @@ class TemplateForm(forms.ModelForm): ...@@ -675,6 +679,11 @@ class TemplateForm(forms.ModelForm):
@property @property
def helper(self): def helper(self):
submit_kwargs = {}
if self.instance.pk and not self.instance.has_level(self.user,
'owner'):
submit_kwargs['disabled'] = None
helper = FormHelper() helper = FormHelper()
helper.layout = Layout( helper.layout = Layout(
Field("name"), Field("name"),
...@@ -739,7 +748,7 @@ class TemplateForm(forms.ModelForm): ...@@ -739,7 +748,7 @@ class TemplateForm(forms.ModelForm):
Field("tags"), Field("tags"),
), ),
) )
helper.add_input(Submit('submit', 'Save changes')) helper.add_input(Submit('submit', 'Save changes', **submit_kwargs))
return helper return helper
class Meta: class Meta:
...@@ -900,7 +909,8 @@ class VmRenewForm(forms.Form): ...@@ -900,7 +909,8 @@ class VmRenewForm(forms.Form):
self.fields['lease'] = forms.ModelChoiceField(queryset=choices, self.fields['lease'] = forms.ModelChoiceField(queryset=choices,
initial=default, initial=default,
required=True, required=False,
empty_label=None,
label=_('Length')) label=_('Length'))
if len(choices) < 2: if len(choices) < 2:
self.fields['lease'].widget = HiddenInput() self.fields['lease'].widget = HiddenInput()
...@@ -944,6 +954,25 @@ class VmDownloadDiskForm(forms.Form): ...@@ -944,6 +954,25 @@ class VmDownloadDiskForm(forms.Form):
return helper return helper
class VmAddInterfaceForm(forms.Form):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
super(VmAddInterfaceForm, self).__init__(*args, **kwargs)
field = forms.ModelChoiceField(
queryset=choices, required=True, label=_('Vlan'))
if not choices:
field.widget.attrs['disabled'] = 'disabled'
field.empty_label = _('No more networks.')
self.fields['vlan'] = field
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class CircleAuthenticationForm(AuthenticationForm): class CircleAuthenticationForm(AuthenticationForm):
# fields: username, password # fields: username, password
...@@ -1178,6 +1207,11 @@ class UserCreationForm(OrgUserCreationForm): ...@@ -1178,6 +1207,11 @@ class UserCreationForm(OrgUserCreationForm):
return user return user
class AclUserAddForm(forms.Form):
name = forms.CharField(widget=autocomplete_light.TextWidget(
'AclUserAutocomplete', attrs={'class': 'form-control'}))
class UserKeyForm(forms.ModelForm): class UserKeyForm(forms.ModelForm):
name = forms.CharField(required=True, label=_('Name')) name = forms.CharField(required=True, label=_('Name'))
key = forms.CharField( key = forms.CharField(
...@@ -1223,6 +1257,9 @@ class TraitsForm(forms.ModelForm): ...@@ -1223,6 +1257,9 @@ class TraitsForm(forms.ModelForm):
class RawDataForm(forms.ModelForm): class RawDataForm(forms.ModelForm):
raw_data = forms.CharField(validators=[domain_validator],
widget=forms.Textarea(attrs={'rows': 5}),
required=False)
class Meta: class Meta:
model = Instance model = Instance
......
...@@ -77,7 +77,7 @@ class Notification(TimeStampedModel): ...@@ -77,7 +77,7 @@ class Notification(TimeStampedModel):
def send(cls, user, subject, template, context, def send(cls, user, subject, template, context,
valid_until=None, subject_context=None): valid_until=None, subject_context=None):
hro = create_readable(template, user=user, **context) hro = create_readable(template, user=user, **context)
subject = create_readable(subject, subject_context or context) subject = create_readable(subject, **(subject_context or context))
return cls.objects.create(to=user, return cls.objects.create(to=user,
subject_data=subject.to_dict(), subject_data=subject.to_dict(),
message_data=hro.to_dict(), message_data=hro.to_dict(),
...@@ -161,6 +161,11 @@ class Profile(Model): ...@@ -161,6 +161,11 @@ class Profile(Model):
def __unicode__(self): def __unicode__(self):
return self.get_display_name() return self.get_display_name()
class Meta:
permissions = (
('use_autocomplete', _('Can use autocomplete.')),
)
class FutureMember(Model): class FutureMember(Model):
org_id = CharField(max_length=64, help_text=_( org_id = CharField(max_length=64, help_text=_(
......
...@@ -844,3 +844,7 @@ textarea[name="list-new-namelist"] { ...@@ -844,3 +844,7 @@ textarea[name="list-new-namelist"] {
height: 20px; height: 20px;
position: absolute; position: absolute;
} }
#show-all-activities-container {
margin: 20px 0 0 10px;
}
...@@ -56,8 +56,6 @@ $(function () { ...@@ -56,8 +56,6 @@ $(function () {
url: '/dashboard/template/choose/', url: '/dashboard/template/choose/',
success: function(data) { success: function(data) {
$('body').append(data); $('body').append(data);
vmCreateLoaded();
addSliderMiscs();
$('#create-modal').modal('show'); $('#create-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() { $('#create-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#create-modal').remove();
...@@ -372,6 +370,11 @@ $(function () { ...@@ -372,6 +370,11 @@ $(function () {
return false; return false;
}); });
/* don't close notifications window on missclick */
$(document).on("click", ".notification-messages", function() {
return false;
});
$("#notification-button a").click(function() { $("#notification-button a").click(function() {
$('.notification-messages').load("/dashboard/notifications/"); $('.notification-messages').load("/dashboard/notifications/");
$('#notification-button a span[class*="badge-pulse"]').remove(); $('#notification-button a span[class*="badge-pulse"]').remove();
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
$(function() { $(function() {
/* vm operations */ /* vm operations */
$('#ops, #vm-details-resources-disk').on('click', '.operation.btn', function(e) { $('#ops, #vm-details-resources-disk, #vm-details-renew-op, #vm-details-pw-reset, #vm-details-add-interface').on('click', '.operation', function(e) {
var icon = $(this).children("i").addClass('fa-spinner fa-spin'); var icon = $(this).children("i").addClass('fa-spinner fa-spin');
$.ajax({ $.ajax({
...@@ -50,6 +50,9 @@ $(function() { ...@@ -50,6 +50,9 @@ $(function() {
*/ */
if(data.success) { if(data.success) {
$('a[href="#activity"]').trigger("click"); $('a[href="#activity"]').trigger("click");
if(data.with_reload) {
location.reload();
}
/* if there are messages display them */ /* if there are messages display them */
if(data.messages && data.messages.length > 0) { if(data.messages && data.messages.length > 0) {
......
var show_all = false;
var in_progress = false;
$(function() { $(function() {
/* do we need to check for new activities */ /* do we need to check for new activities */
if(decideActivityRefresh()) { if(decideActivityRefresh()) {
checkNewActivity(false, 1); if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
} }
$('a[href="#activity"]').click(function(){ $('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('fa-spin'); $('a[href="#activity"] i').addClass('fa-spin');
checkNewActivity(false, 1); if(!in_progress) {
checkNewActivity(1);
in_progress = true;
}
});
$("#activity-refresh").on("click", "#show-all-activities", function() {
$(this).find("i").addClass("fa-spinner fa-spin");
show_all = !show_all;
$('a[href="#activity"]').trigger("click");
return false;
}); });
/* save resources */ /* save resources */
...@@ -134,11 +151,6 @@ $(function() { ...@@ -134,11 +151,6 @@ $(function() {
return false; return false;
}); });
/* show help */
$(".vm-details-help-button").click(function() {
$(".vm-details-help").stop().slideToggle();
});
/* for interface remove buttons */ /* for interface remove buttons */
$('.interface-remove').click(function() { $('.interface-remove').click(function() {
var interface_pk = $(this).data('interface-pk'); var interface_pk = $(this).data('interface-pk');
...@@ -295,6 +307,10 @@ $(function() { ...@@ -295,6 +307,10 @@ $(function() {
$("#vm-details-connection-string").focus(); $("#vm-details-connection-string").focus();
}); });
$("a.operation-password_reset").click(function() {
if(Boolean($(this).data("disabled"))) return false;
});
}); });
...@@ -315,17 +331,18 @@ function removePort(data) { ...@@ -315,17 +331,18 @@ function removePort(data) {
} }
}); });
} }
function decideActivityRefresh() { function decideActivityRefresh() {
var check = false; var check = false;
/* if something is still spinning */ /* if something is still spinning */
if($('.timeline .activity:first i:first').hasClass('fa-spin')) if($('.timeline .activity i').hasClass('fa-spin'))
check = true; check = true;
/* if there is only one activity */ /* if there is only one activity */
if($('#activity-timeline div[class="activity"]').length < 2) if($('#activity-timeline div[class="activity"]').length < 2)
check = true; check = true;
return check; return check;
} }
...@@ -340,25 +357,25 @@ function changeHTML(html) { ...@@ -340,25 +357,25 @@ function changeHTML(html) {
return html.replace(/data-original-title/g, "title").replace(/title=""/g, "").replace(/\//g, '').replace(/ /g, ''); return html.replace(/data-original-title/g, "title").replace(/title=""/g, "").replace(/\//g, '').replace(/ /g, '');
} }
function checkNewActivity(only_status, runs) { function checkNewActivity(runs) {
// set default only_status to false
only_status = typeof only_status !== 'undefined' ? only_status : false;
var instance = location.href.split('/'); instance = instance[instance.length - 2]; var instance = location.href.split('/'); instance = instance[instance.length - 2];
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/dashboard/vm/' + instance + '/activity/', url: '/dashboard/vm/' + instance + '/activity/',
data: {'only_status': only_status}, data: {'show_all': show_all},
success: function(data) { success: function(data) {
if(!only_status) { if(show_all) { /* replace on longer string freezes the spinning stuff */
$("#activity-refresh").html(data['activities']);
} else {
a = unescapeHTML(data['activities']); a = unescapeHTML(data['activities']);
b = changeHTML($("#activity-timeline").html()); b = changeHTML($("#activity-refresh").html());
if(a != b) if(a != b)
$("#activity-timeline").html(data['activities']); $("#activity-refresh").html(data['activities']);
$("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip();
} }
$("#ops").html(data['ops']);
$("#disk-ops").html(data['disk_ops']);
$("[title]").tooltip();
$("#vm-details-state i").prop("class", "fa " + data['icon']); $("#vm-details-state i").prop("class", "fa " + data['icon']);
$("#vm-details-state span").html(data['human_readable_status'].toUpperCase()); $("#vm-details-state span").html(data['human_readable_status'].toUpperCase());
...@@ -378,14 +395,16 @@ function checkNewActivity(only_status, runs) { ...@@ -378,14 +395,16 @@ function checkNewActivity(only_status, runs) {
if(runs > 0 && decideActivityRefresh()) { if(runs > 0 && decideActivityRefresh()) {
setTimeout( setTimeout(
function() {checkNewActivity(only_status, runs + 1)}, function() {checkNewActivity(runs + 1)},
1000 + Math.exp(runs * 0.05) 1000 + Math.exp(runs * 0.05)
); );
} else {
in_progress = false;
} }
$('a[href="#activity"] i').removeClass('fa-spin'); $('a[href="#activity"] i').removeClass('fa-spin');
}, },
error: function() { error: function() {
in_progress = false;
} }
}); });
} }
...@@ -27,43 +27,6 @@ from django.utils.translation import ugettext_lazy as _ ...@@ -27,43 +27,6 @@ from django.utils.translation import ugettext_lazy as _
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
class VmListTable(Table):
pk = TemplateColumn(
template_name='dashboard/vm-list/column-id.html',
verbose_name="ID",
attrs={'th': {'class': 'vm-list-table-thin'}},
)
name = TemplateColumn(
template_name="dashboard/vm-list/column-name.html"
)
admin = TemplateColumn(
template_name='dashboard/vm-list/column-admin.html',
attrs={'th': {'class': 'vm-list-table-admin'}},
)
details = TemplateColumn(
template_name='dashboard/vm-list/column-details.html',
attrs={'th': {'class': 'vm-list-table-thin'}},
)
actions = TemplateColumn(
template_name='dashboard/vm-list/column-actions.html',
attrs={'th': {'class': 'vm-list-table-thin'}},
)
time_of_suspend = TemplateColumn(
'{{ record.time_of_suspend|timeuntil }}',
verbose_name=_("Suspend in"))
time_of_delete = TemplateColumn(
'{{ record.time_of_delete|timeuntil }}',
verbose_name=_("Delete in"))
class Meta:
model = Instance
attrs = {'class': ('table table-bordered table-striped table-hover '
'vm-list-table')}
fields = ('pk', 'name', 'state', 'time_of_suspend', 'time_of_delete', )
class NodeListTable(Table): class NodeListTable(Table):
pk = Column( pk = Column(
......
...@@ -70,6 +70,7 @@ ...@@ -70,6 +70,7 @@
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script> <script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script> <script src="{{ STATIC_URL }}jsi18n/{{ LANGUAGE_CODE }}/djangojs.js"></script>
{% include 'autocomplete_light/static.html' %}
{% block extra_script %} {% block extra_script %}
{% endblock %} {% endblock %}
...@@ -79,4 +80,11 @@ ...@@ -79,4 +80,11 @@
{% block extra_etc %} {% block extra_etc %}
{% endblock %} {% endblock %}
<script>
yourlabs.TextWidget.prototype.getValue = function(choice) {
return choice.children().html();
}
</script>
</html> </html>
...@@ -10,10 +10,12 @@ ...@@ -10,10 +10,12 @@
<div class="label label-danger"{% if user.is_superuser %} title="{{ d.get_latest_activity_result }}"{% endif %}>{% trans "failed" %}</div> <div class="label label-danger"{% if user.is_superuser %} title="{{ d.get_latest_activity_result }}"{% endif %}>{% trans "failed" %}</div>
{% endif %} {% endif %}
{% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %} {% else %}<span class="disk-list-disk-percentage" data-disk-pk="{{ d.pk }}">{{ d.get_download_percentage }}</span>%{% endif %}
{% if is_owner != False %}
<a href="{% url "dashboard.views.disk-remove" pk=d.pk %}?next={{ request.path }}" <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" data-disk-pk="{{ d.pk }}" class="btn btn-xs btn-danger pull-right disk-remove"
{% if not long_remove %}title="{% trans "Remove" %}"{% endif %} {% if not long_remove %}title="{% trans "Remove" %}"{% endif %}
> >
<i class="fa fa-times"></i>{% if long_remove %} {% trans "Remove" %}{% endif %} <i class="fa fa-times"></i>{% if long_remove %} {% trans "Remove" %}{% endif %}
</a> </a>
{% endif %}
<div style="clear: both;"></div> <div style="clear: both;"></div>
{% load i18n %}
<form action="{{ acl.url }}" method="post">{% csrf_token %}
<table class="table table-striped table-with-form-fields" id="{{table_id}}">
<thead>
<tr>
<th></th>
<th>{% trans "Who" %}</th>
<th>{% trans "What" %}</th>
<th><i class="fa fa-times"></i></th>
</tr>
</thead>
<tbody>
{% for i in acl.users %}
<tr>
<td>
<i class="fa fa-user"></i>
</td>
<td>
<a href="{% url "dashboard.views.profile" username=i.user.username %}"
title="{{ i.user.username }}">
{% include "dashboard/_display-name.html" with user=i.user show_org=True %}
</a>
</td>
<td>
<select class="form-control" name="perm-u-{{i.user.id}}"{% if i.level not in acl.allowed_levels %} disabled{% endif %}>
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%}
{% if id not in acl.allowed_levels %} disabled{% endif %}
value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-u-{{i.user.id}}" title="{% trans "Remove" %}"/>
</td>
</tr>
{% endfor %}
{% for i in acl.groups %}
<tr>
<td><i class="fa fa-group"></i></td>
<td>
<a href="{% url "dashboard.views.group-detail" pk=i.group.pk %}">
{{i.group}}
</a>
</td>
<td>
<select class="form-control" name="perm-g-{{i.group.id}}{% if i.level not in acl.allowed_levels %} disabled{% endif %}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%}
{% if id not in acl.allowed_levels %} disabled{% endif %}
value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-g-{{i.group.id}}" title="{% trans "Remove" %}"/>
</td>
</tr>
{% endfor %}
<tr><td><i class="fa fa-plus"></i></td>
<td>{{aclform.name }}</td>
<td><select class="form-control" name="level">
{% for id, name in acl.levels %}
{% if id in acl.allowed_levels %}
<option value="{{id}}">{{name}}</option>
{% endif %}
{% endfor %}
</select></td><td></td>
</tr>
</tbody>
</table>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
{% load i18n %} {% load i18n %}
<div class="alert alert-info" id="template-choose-alert"> <div class="alert alert-info" id="template-choose-alert">
{% trans "Customize an existing template or create a brand new one from scratch!" %} {% if perms.vm.create_base_template %}
{% trans "Customize an existing template or create a brand new one from scratch." %}
{% else %}
{% trans "Customize an existing template." %}
{% endif %}
</div> </div>
<form action="{% url "dashboard.views.template-choose" %}" method="POST" <form action="{% url "dashboard.views.template-choose" %}" method="POST"
......
...@@ -18,23 +18,22 @@ ...@@ -18,23 +18,22 @@
{% endblock %} {% endblock %}
{% block navbar %} {% block navbar %}
{% if user.is_authenticated and user.pk and not request.token_user %}
<ul class="nav navbar-nav pull-right">
<li class="dropdown" id="notification-button">
<a href="{% url "dashboard.views.notifications" %}" style="color: white; font-size: 12px;"
class="dropdown-toggle" data-toggle="dropdown">
{% trans "Notifications" %}
{% if NEW_NOTIFICATIONS_COUNT > 0 %}
<span class="badge badge-pulse">{{ NEW_NOTIFICATIONS_COUNT }}</span>
{% endif %}
</a>
<ul class="dropdown-menu notification-messages">
<li>{% trans "Loading..." %}</li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav pull-right">
<li class="dropdown" id="notification-button">
<a href="{% url "dashboard.views.notifications" %}" style="color: white; font-size: 12px;"
class="dropdown-toggle" data-toggle="dropdown">
{% trans "Notifications" %}
{% if NEW_NOTIFICATIONS_COUNT > 0 %}
<span class="badge badge-pulse">{{ NEW_NOTIFICATIONS_COUNT }}</span>
{% endif %}
</a>
<ul class="dropdown-menu notification-messages">
<li>{% trans "Loading..." %}</li>
</ul>
</li>
</ul>
{% if user.is_authenticated and user.pk %}
<a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;"> <a class="navbar-brand pull-right" href="{% url "logout" %}?next={% url "login" %}" style="color: white; font-size: 10px;">
<i class="fa fa-sign-out"></i> {% trans "Log out" %} <i class="fa fa-sign-out"></i> {% trans "Log out" %}
</a> </a>
...@@ -48,7 +47,7 @@ ...@@ -48,7 +47,7 @@
<a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a> <a class="navbar-brand pull-right" href="/admin/" style="color: white; font-size: 10px;"><i class="fa fa-cogs"></i> {% trans "Admin" %}</a>
{% endif %} {% endif %}
{% else %} {% else %}
<a class="navbar-brand pull-right" href="{% url "login" %}?next={% url "dashboard.index" %}" style="color: white; font-size: 10px;"><i class="fa fa-sign-in"></i> {% trans "Log in " %}</a> <a class="navbar-brand pull-right" href="{% url "login" %}?next={{ request.path }}" style="color: white; font-size: 10px;"><i class="fa fa-sign-in"></i> {% trans "Log in " %}</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<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"><i class="fa fa-pencil"></i></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"></i></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> <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>
...@@ -104,76 +104,7 @@ ...@@ -104,76 +104,7 @@
<hr /> <hr />
<h3 id="group-detail-perm-header">{% trans "Access permissions"|capfirst %}</h3> <h3 id="group-detail-perm-header">{% trans "Access permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %} {% include "dashboard/_manage_access.html" with table_id="group-detail-perm-table" %}
<table class="table table-striped table-with-form-fields table-bordered" id="group-detail-perm-table">
<thead>
<tr>
<th></th><th>{% trans "Who" %}</th><th>{% trans "What" %}</th><th>{% trans "Remove" %}</th>
</tr>
</thead>
<tbody>
{% for i in acl.users %}
<tr>
<td>
<i class="fa fa-user"></i>
</td>
<td>
<a href="{% url "dashboard.views.profile" username=i.user.username %}" title="{{ i.user.username }}"
>{% include "dashboard/_display-name.html" with user=i.user show_org=True %}</a>
</td>
<td>
<select class="form-control" name="perm-u-{{i.user.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td class="user-remove"><a data-group_pk="{{ group.pk }}" data-member_pk="{{i.user.pk }}" href="{% url "dashboard.views.remove-acluser" member_pk=i.user.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>
</tr>
{% endfor %}
{% for i in acl.groups %}
<tr>
<td>
<i class="fa fa-group"></i>
</td>
<td>
<a href="{% url "dashboard.views.group-detail" pk=i.group.pk %}">{{ i.group }}</a>
</td>
<td>
<select class="form-control" name="perm-g-{{ i.group.pk }}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td class="user-remove"><a data-group_pk="{{ i.pk }}"data-member_pk="{{i.group.pk }}" href="{% url "dashboard.views.remove-aclgroup" member_pk=i.group.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>
</tr>
{% endfor %}
<tr>
<td><i class="fa fa-plus"></i></td>
<td>
<input type="text" class="form-control" name="perm-new-name"
placeholder="{% trans "Name of group or user" %}">
</td>
<td>
<select class="form-control" name="perm-new">
{% for id, name in acl.levels %}
<option value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td></td>
</tr>
</tbody>
</table>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
{% if user.is_superuser %} {% if user.is_superuser %}
<hr /> <hr />
......
...@@ -20,10 +20,12 @@ ...@@ -20,10 +20,12 @@
<div class="clearfix"></div> <div class="clearfix"></div>
</a> </a>
{% empty %} {% empty %}
<div class="alert alert-warning" style="margin: 10px;"> <div class="list-group-item">
<p> <div class="alert alert-warning" style="margin: 10px;">
{% trans "You don't have any templates, however you can still start virtual machines and even save them as new templates!" %} <p>
</p> {% trans "You don't have any templates, however you can still start virtual machines and even save them as new templates!" %}
</p>
</div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
......
...@@ -14,17 +14,17 @@ ...@@ -14,17 +14,17 @@
</h1> </h1>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-4" id="vm-info-pane"> <div class="col-md-5" id="vm-info-pane">
<div class="big"> <div class="big">
<span id="vm-activity-state" class="label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}error{% endif %}{% endif %}"> <span id="vm-activity-state" class="label label-{% if object.get_status_id == 'wait' %}info{% else %}{% if object.succeeded %}success{% else %}error{% endif %}{% endif %}">
<span>{{ object.get_status_id|upper }}</span> <span>{{ object.get_status_id|upper }}</span>
</span> </span>
</div> </div>
<div id="vm-activity-context" class="timeline">
{% include "dashboard/vm-detail/_activity-timeline.html" with active=object %} {% include "dashboard/vm-detail/_activity-timeline.html" with active=object %}
</div>
</div> </div>
<div class="col-md-8"> <div class="col-md-7">
<div class="panel panel-default"> <div class="panel panel-default">
<!--<div class="panel-heading"><h2 class="panel-title">{% trans "Activity" %}</h2></div> --> <!--<div class="panel-heading"><h2 class="panel-title">{% trans "Activity" %}</h2></div> -->
<div class="panel-body"> <div class="panel-body">
......
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
<i class="fa fa-desktop"></i> <i class="fa fa-desktop"></i>
{% trans "Virtual machines owned by the user" %} ({{ instances_owned|length }}) {% trans "Virtual machines owned by the user" %} ({{ instances_owned|length }})
</h4> </h4>
<ul class="dashboard-profile-vm-list"> <ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_owned %} {% for i in instances_owned %}
<li> <li>
<a href="{{ i.get_absolute_url }}"> <a href="{{ i.get_absolute_url }}">
...@@ -85,7 +85,7 @@ ...@@ -85,7 +85,7 @@
<i class="fa fa-desktop"></i> <i class="fa fa-desktop"></i>
{% trans "Virtual machines with access" %} ({{ instances_with_access|length }}) {% trans "Virtual machines with access" %} ({{ instances_with_access|length }})
</h4> </h4>
<ul class="dashboard-profile-vm-list"> <ul class="dashboard-profile-vm-list fa-ul">
{% for i in instances_with_access %} {% for i in instances_with_access %}
<li> <li>
<a href="{{ i.get_absolute_url }}"> <a href="{{ i.get_absolute_url }}">
......
...@@ -29,75 +29,7 @@ ...@@ -29,75 +29,7 @@
<h4 class="no-margin"><i class="fa fa-group"></i> {% trans "Manage access" %}</h4> <h4 class="no-margin"><i class="fa fa-group"></i> {% trans "Manage access" %}</h4>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form action="{% url "dashboard.views.template-acl" pk=object.pk %}" method="post">{% csrf_token %} {% include "dashboard/_manage_access.html" with table_id="template-access-table" %}
<table class="table table-striped table-with-form-fields" id="template-access-table">
<thead>
<tr>
<th></th>
<th>{% trans "Who" %}</th>
<th>{% trans "What" %}</th>
<th><i class="fa fa-times"></i></th>
</tr>
</thead>
<tbody>
{% for i in acl.users %}
<tr>
<td>
<i class="fa fa-user"></i>
</td>
<td>
<a href="{% url "dashboard.views.profile" username=i.user.username %}"
title="{{ i.user.username }}">
{% include "dashboard/_display-name.html" with user=i.user show_org=True %}
</a>
</td>
<td>
<select class="form-control" name="perm-u-{{i.user.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-u-{{i.user.id}}" title="{% trans "Remove" %}"/>
</td>
</tr>
{% endfor %}
{% for i in acl.groups %}
<tr>
<td><i class="fa fa-group"></i></td>
<td>
<a href="{% url "dashboard.views.group-detail" pk=i.group.pk %}">
{{i.group}}
</a>
</td>
<td>
<select class="form-control" name="perm-g-{{i.group.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-g-{{i.group.id}}" title="{% trans "Remove" %}"/>
</td>
</tr>
{% endfor %}
<tr><td><i class="fa fa-plus"></i></td>
<td><input type="text" class="form-control" name="perm-new-name"
placeholder="{% trans "Name of group or user" %}"></td>
<td><select class="form-control" name="perm-new">
{% for id, name in acl.levels %}
<option value="{{id}}">{{name}}</option>
{% endfor %}
</select></td><td></td>
</tr>
</tbody>
</table>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
</div> </div>
</div> </div>
......
...@@ -10,8 +10,8 @@ ...@@ -10,8 +10,8 @@
<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">
<a href="{% url "dashboard.views.template-create" %}" class="pull-right btn btn-success btn-xs"> <a href="{% url "dashboard.views.template-choose" %}" class="pull-right btn btn-success btn-xs template-choose">
<i class="fa fa-plus"></i> {% trans "new base vm" %} <i class="fa fa-plus"></i> {% trans "new template" %}
</a> </a>
<h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}</h3> <h3 class="no-margin"><i class="fa fa-puzzle-piece"></i> {% trans "Templates" %}</h3>
</div> </div>
......
...@@ -98,17 +98,12 @@ ...@@ -98,17 +98,12 @@
</div> </div>
</dd> </dd>
<dd style="font-size: 10px; text-align: right; padding-top: 8px;"> <dd style="font-size: 10px; text-align: right; padding-top: 8px;">
<a id="vm-details-pw-change" href="#">{% trans "Generate new password!" %}</a> <div id="vm-details-pw-reset">
{% with op=op.password_reset %}{% if op %}
<a href="{% if op.disabled %}#{% else %}{{op.get_url}}{% endif %}" class="operation operation-{{op.op}}" data-disabled="{% if op.disabled %}true" title="{% trans "Start the VM to change the password." %}"{% else %}false" {% endif %}>{% trans "Generate new password!" %}</a>
{% endif %}{% endwith %}
</div>
</dd> </dd>
<div id="vm-details-pw-confirm"> {% comment %} TODO Couldn't this use a modal? {% endcomment%}
<dt>
{% trans "Are you sure?" %}
</dt>
<dd>
<a href="#" class="vm-details-pw-confirm-choice label label-success" data-choice="1" data-vm="{{ instance.pk }}">{% trans "Yes" %}</a> /
<a href="#" class="vm-details-pw-confirm-choice label label-danger" data-choice="0">{% trans "No" %}</a>
</dd>
</div>
</dl> </dl>
<div class="input-group" id="dashboard-vm-details-connect-command"> <div class="input-group" id="dashboard-vm-details-connect-command">
......
{% load i18n %} {% load i18n %}
<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 }}">
<span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}"> <span class="timeline-icon{% if a.has_failed %} timeline-icon-failed{% endif %}">
...@@ -7,7 +10,7 @@ ...@@ -7,7 +10,7 @@
<strong{% if a.result %} title="{{ a.result.get_user_text }}"{% endif %}> <strong{% if a.result %} title="{{ a.result.get_user_text }}"{% endif %}>
<a href="{{ a.get_absolute_url }}"> <a href="{{ a.get_absolute_url }}">
{% if a.times > 1 %}({{ a.times }}x){% endif %} {% if a.times > 1 %}({{ a.times }}x){% endif %}
{{ a.readable_name.get_user_text }}</a> {{ a.readable_name.get_user_text|capfirst }}</a>
{% if a.has_percent %} {% if a.has_percent %}
- {{ a.percentage }}% - {{ a.percentage }}%
...@@ -32,7 +35,7 @@ ...@@ -32,7 +35,7 @@
<div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}{% if s.pk == active.pk %} sub-activity-active{% endif %}"> <div data-activity-id="{{ s.pk }}" class="sub-activity{% if s.has_failed %} sub-activity-failed{% endif %}{% if s.pk == active.pk %} sub-activity-active{% endif %}">
<span{% if s.result %} title="{{ s.result.get_user_text }}"{% endif %}> <span{% if s.result %} title="{{ s.result.get_user_text }}"{% endif %}>
<a href="{{ s.get_absolute_url }}"> <a href="{{ s.get_absolute_url }}">
{{ s.readable_name.get_user_text }}</a></span> &ndash; {{ s.readable_name.get_user_text|capfirst }}</a></span> &ndash;
{% if s.finished %} {% if s.finished %}
{{ s.finished|time:"H:i:s" }} {{ s.finished|time:"H:i:s" }}
{% else %} {% else %}
...@@ -47,3 +50,16 @@ ...@@ -47,3 +50,16 @@
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
</div>
{% if show_show_all %}
<div id="show-all-activities-container">
<a id="show-all-activities" href="#">
{% if activities|length > 10 %}
{% trans "Show less activities" %} <i class="fa fa-angle-double-up"></i>
{% else %}
{% trans "Show all activities" %} <i class="fa fa-angle-double-down"></i>
{% endif %}
</a>
</div>
{% endif %}
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
{% for op in ops %} {% for op in ops %}
{% if op.is_disk_operation %} {% if op.is_disk_operation %}
<a href="{{op.get_url}}" class="btn btn-success btn-xs <a href="{{op.get_url}}" class="btn btn-success btn-xs
operation operation-{{op.op}} btn btn-default"> operation operation-{{op.op}}">
<i class="fa fa-{{op.icon}}"></i> <i class="fa fa-{{op.icon}}"></i>
{{op.name}} </a> {{op.name}} </a>
{% endif %} {% endif %}
......
...@@ -14,64 +14,4 @@ ...@@ -14,64 +14,4 @@
{% endif %} {% endif %}
</p> </p>
<h3>{% trans "Permissions"|capfirst %}</h3> <h3>{% trans "Permissions"|capfirst %}</h3>
<form action="{{acl.url}}" method="post">{% csrf_token %} {% include "dashboard/_manage_access.html" with table_id="vm-access-table" %}
<table class="table table-striped table-with-form-fields" id="vm-access-table">
<thead><tr>
<th></th>
<th>{% trans "Who" %}</th>
<th>{% trans "What" %}</th>
<th>{% trans "Remove" %}</th>
</tr></thead>
<tbody>
{% for i in acl.users %}
<tr>
<td><i class="fa fa-user"></i></td>
<td>
<a href="{% url "dashboard.views.profile" username=i.user.username %}" title="{{ i.user.username }}"
>{% include "dashboard/_display-name.html" with user=i.user show_org=True %}</a>
</td>
<td>
<select class="form-control" name="perm-u-{{i.user.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select>
</td>
<td>
<input type="checkbox" name="remove-u-{{i.user.id}}"/>
</td>
</tr>
{% endfor %}
{% for i in acl.groups %}
<tr>
<td><i class="fa fa-group"></i></td>
<td>
<a href="{% url "dashboard.views.group-detail" pk=i.group.pk %}"
>{{ i.group.name }}</a>
</td>
<td>
<select class="form-control" name="perm-g-{{i.group.id}}">
{% for id, name in acl.levels %}
<option{%if id = i.level%} selected="selected"{%endif%} value="{{id}}">{{name}}</option>
{% endfor %}
</select></td>
<td>
<input type="checkbox" name="remove-g-{{i.group.id}}"/>
</td>
</tr>
{% endfor %}
<tr><td><i class="fa fa-plus"></i></td>
<td><input type="text" class="form-control" name="perm-new-name"
placeholder="{% trans "Name of group or user" %}"></td>
<td><select class="form-control" name="perm-new">
{% for id, name in acl.levels %}
<option value="{{id}}">{{name}}</option>
{% endfor %}
</select></td><td></td>
</tr>
</tbody>
</table>
<div class="form-actions">
<button type="submit" class="btn btn-success">{% trans "Save" %}</button>
</div>
</form>
...@@ -2,6 +2,6 @@ ...@@ -2,6 +2,6 @@
<h3>{% trans "Activity" %}</h3> <h3>{% trans "Activity" %}</h3>
<div id="activity-timeline" class="timeline"> <div id="activity-refresh">
{% include "dashboard/vm-detail/_activity-timeline.html" %} {% include "dashboard/vm-detail/_activity-timeline.html" %}
</div> </div>
...@@ -47,12 +47,14 @@ ...@@ -47,12 +47,14 @@
</dl> </dl>
<h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %} <h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="fa fa-warning-sign text-danger"></i>{% endif %}
{% with op=op.renew %} <span id="vm-details-renew-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
operation operation-{{op.op}} btn btn-default"> operation operation-{{op.op}}">
<i class="fa fa-{{op.icon}}"></i> <i class="fa fa-{{op.icon}}"></i>
{{op.name}} </a> {{op.name}} </a>
{% endwith %} {% endif %}{% endwith %}
</span>
</h4> </h4>
<dl> <dl>
<dt>{% trans "Suspended at:" %}</dt> <dt>{% trans "Suspended at:" %}</dt>
......
{% load i18n %} {% load i18n %}
{% load network_tags %} {% load network_tags %}
<h2> <h2>
<a href="#" id="vm-details-network-add" class="btn btn-success pull-right no-js-hidden"> <div id="vm-details-add-interface">
<i class="fa fa-plus"></i> {% trans "add interface" %} {% with op=op.add_interface %}{% if op %}
</a> <a href="{{op.get_url}}" class="btn btn-{{op.effect}} operation pull-right"
{% if op.disabled %}disabled{% endif %}>
<i class="fa fa-{{op.icon}}"></i> {% trans "add interface" %}</a>
{% endif %}{% endwith %}
</div>
{% trans "Interfaces" %} {% trans "Interfaces" %}
</h2> </h2>
<div class="js-hidden row" id="vm-details-network-add-form">
<div class="col-md-12">
<div>
<hr />
<h3>
{% trans "Add new network interface!" %}
</h3>
<form method="POST" action="">
{% csrf_token %}
<div class="input-group" style="max-width: 330px;">
<select name="new_network_vlan" class="form-control font-awesome-font">
{% for v in vlans %}
<option value="{{ v.pk }}">
{% if v.managed %}
&#xf0ac;
{% else %}
&#xf0c1;
{% endif %}
{{ v.name }}
</option>
{% empty %}
<option value="-1">No more networks!</option>
{% endfor %}
</select>
<div class="input-group-btn">
<button {% if vlans|length == 0 %}disabled{% endif %}
type="submit" class="btn btn-success"><i class="fa fa-plus-circle"></i></button>
</div>
</div>
</form>
<hr />
</div>
</div>
</div>
{% for i in instance.interface_set.all %} {% for i in instance.interface_set.all %}
<div> <div>
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% block title-page %}{% trans "Edit raw data" %}{% endblock %}
{% block content %}
<div class="row">
<div class="col-md-7">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin"><i class="fa fa-time"></i> {% trans "Edit raw data" %}</h3>
</div>
<div class="panel-body">
{% with form=form %}
{% include "display-form-errors.html" %}
{% endwith %}
{% crispy form %}
</div>
</div>
</div>
</div>
{% endblock %}
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
<div class="btn-group"> <div class="btn-group">
<button type="button" class="btn btn-xs btn-warning nojs-dropdown-toogle dropdown-toggle" data-toggle="dropdown">Action <i class="fa fa-caret-down"></i></button> <button type="button" class="btn btn-xs btn-warning nojs-dropdown-toogle dropdown-toggle" data-toggle="dropdown">Action <i class="fa fa-caret-down"></i></button>
<ul class="nojs-dropdown-toogle dropdown-menu" role="menu"> <ul class="nojs-dropdown-toogle dropdown-menu" role="menu">
<li><a href="#"><i class="fa fa-refresh"></i> Reboot</a></li> <li><a class="real-link" href="{%url "dashboard.vm.op.destroy" pk=record.pk %}?next={{ request.path }}"><i class="fa fa-times"></i> Discard</a></li>
<li><a href="#"><i class="fa fa-off"></i> Shutdown</a></li>
<li><a data-vm-pk="{{ record.pk }}" class="real-link vm-delete" href="{% url "dashboard.views.delete-vm" pk=record.pk %}?next={{ request.path }}"><i class="fa fa-times"></i> Discard</a></li>
</ul> </ul>
</div> </div>
...@@ -30,6 +30,7 @@ from django.utils import baseconv ...@@ -30,6 +30,7 @@ from django.utils import baseconv
from ..models import Profile from ..models import Profile
from ..views import InstanceActivityDetail, InstanceActivity from ..views import InstanceActivityDetail, InstanceActivity
from ..views import vm_ops, Instance, UnsubscribeFormView from ..views import vm_ops, Instance, UnsubscribeFormView
from ..views import AclUpdateView
from .. import views from .. import views
...@@ -279,6 +280,7 @@ class RenewViewTest(unittest.TestCase): ...@@ -279,6 +280,7 @@ class RenewViewTest(unittest.TestCase):
view = vm_ops['renew'] view = vm_ops['renew']
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4: patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
...@@ -287,7 +289,10 @@ class RenewViewTest(unittest.TestCase): ...@@ -287,7 +289,10 @@ class RenewViewTest(unittest.TestCase):
inst.has_level.return_value = True inst.has_level.return_value = True
go.return_value = inst go.return_value = inst
go4.return_value = MagicMock() go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234).render().status_code == 200 assert view.as_view()(request, pk=1234)
assert not msg.error.called
assert inst.renew.async.called_with(user=request.user, lease=None)
assert inst.renew.async.return_value.get.called
# success would redirect # success would redirect
def test_renew_by_owner_w_param(self): def test_renew_by_owner_w_param(self):
...@@ -429,6 +434,79 @@ class RenewViewTest(unittest.TestCase): ...@@ -429,6 +434,79 @@ class RenewViewTest(unittest.TestCase):
view.as_view()(request, pk=1234)['location']) view.as_view()(request, pk=1234)['location'])
class AclUpdateViewTest(unittest.TestCase):
def test_has_next_level(self):
data = {None: 'user', 'user': 'operator', 'operator': 'owner',
'owner': 'owner'}
for k, v in data.items():
inst = MagicMock(spec=Instance)
inst.has_level.return_value = True
inst.ACL_LEVELS = Instance.ACL_LEVELS
self.assertTrue(AclUpdateView.has_next_level('dummy', inst, k))
inst.has_level.assert_called_with('dummy', v)
def test_set_level_mod_owner(self):
with patch('dashboard.views.messages') as msg:
request = FakeRequestFactory(POST={})
inst = MagicMock(spec=Instance)
inst.owner = request.user
v = AclUpdateView()
v.instance = inst
v.request = request
v.get_level = MagicMock(return_value='owner')
v.check_auth = MagicMock(side_effect=Exception(''))
v.set_level(request.user, 'user')
v.get_level.assert_called_with(request.user)
assert not v.check_auth.called
assert msg.warning.called
def test_set_level_permitted(self):
data = (('user', 'owner', ('user', 'operator', 'owner'), False),
(None, None, ('user', ), True),
('user', None, ('user', ), True),
(None, 'user', ('user', ), True),
('operator', 'owner', ('user', 'operator'), True),
(None, 'user', ('user', 'operator'), False))
for old_level, new_level, allowed_levels, fail in data:
with patch('dashboard.views.messages') as msg:
def has_level(user, level):
return level in allowed_levels
request = FakeRequestFactory(POST={})
inst = MagicMock(spec=Instance)
inst.has_level.side_effect = has_level
inst.ACL_LEVELS = Instance.ACL_LEVELS
v = AclUpdateView()
v.instance = inst
v.request = request
v.is_owner = True
v.get_level = MagicMock(return_value=old_level)
v.set_level(request.user, new_level)
v.get_level.assert_called_with(request.user)
assert (new_level == old_level) ^ inst.has_level.called
assert fail ^ inst.set_level.called
assert fail ^ msg.success.called
def test_readd(self):
request = FakeRequestFactory(POST={'name': 'user0', 'level': 'user'})
with patch('dashboard.views.messages') as msg:
with patch.object(AclUpdateView, 'get_object') as go:
view = AclUpdateView.as_view()
inst = MagicMock(spec=Instance)
go.return_value = inst
view(request)
assert msg.warning.called
def FakeRequestFactory(user=None, **kwargs): def FakeRequestFactory(user=None, **kwargs):
''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for ''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for
mocking out django views; they are MUCH faster than the Django test client. mocking out django views; they are MUCH faster than the Django test client.
......
...@@ -24,10 +24,10 @@ from django.contrib.auth.models import User, Group ...@@ -24,10 +24,10 @@ from django.contrib.auth.models import User, Group
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from dashboard.views import VmAddInterfaceView
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait from vm.models import Instance, InstanceTemplate, Lease, Node, Trait
from vm.operations import WakeUpOperation from vm.operations import WakeUpOperation, AddInterfaceOperation
from ..models import Profile from ..models import Profile
from storage.models import Disk
from firewall.models import Vlan, Host, VlanGroup from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch from mock import Mock, patch
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
...@@ -107,28 +107,6 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -107,28 +107,6 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.get('/dashboard/vm/1/') response = c.get('/dashboard/vm/1/')
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
def test_permitted_vm_delete(self):
c = Client()
self.login(c, 'user2')
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
response = c.post('/dashboard/vm/delete/1/')
self.assertEqual(response.status_code, 302)
def test_not_permitted_vm_delete(self):
c = Client()
self.login(c, 'user2')
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'operator')
response = c.post('/dashboard/vm/delete/1/')
self.assertEqual(response.status_code, 403)
def test_unpermitted_vm_delete(self):
c = Client()
self.login(c, 'user1')
response = c.post('/dashboard/vm/delete/1/')
self.assertEqual(response.status_code, 403)
def test_unpermitted_vm_mass_delete(self): def test_unpermitted_vm_mass_delete(self):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
...@@ -143,33 +121,21 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -143,33 +121,21 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]}) response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
def test_permitted_password_change(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
inst.node = Node.objects.all()[0]
inst.save()
password = inst.pw
response = c.post("/dashboard/vm/1/", {'change_password': True})
self.assertTrue(Instance.get_remote_queue_name.called)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(password, Instance.objects.get(pk=1).pw)
def test_unpermitted_password_change(self): def test_unpermitted_password_change(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner') inst.set_level(self.u1, 'owner')
password = inst.pw password = inst.pw
response = c.post("/dashboard/vm/1/", {'change_password': True}) response = c.post("/dashboard/vm/1/op/password_reset/")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
self.assertEqual(password, Instance.objects.get(pk=1).pw) self.assertEqual(password, Instance.objects.get(pk=1).pw)
def test_unpermitted_network_add_wo_perm(self): def test_unpermitted_network_add_wo_perm(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
response = c.post("/dashboard/vm/1/", {'new_network_vlan': 1}) response = c.post("/dashboard/vm/1/op/add_interface/",
{'vlan': 1})
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
def test_unpermitted_network_add_wo_vlan_perm(self): def test_unpermitted_network_add_wo_vlan_perm(self):
...@@ -177,8 +143,18 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -177,8 +143,18 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
response = c.post("/dashboard/vm/1/", {'new_network_vlan': 1}) interface_count = inst.interface_set.count()
self.assertEqual(response.status_code, 403)
with patch.object(AddInterfaceOperation, 'async') as async:
async.side_effect = inst.add_interface.call
with patch.object(VmAddInterfaceView, 'get_form_kwargs',
autospec=True) as get_form_kwargs:
get_form_kwargs.return_value = {'choices': Vlan.objects.all()}
response = c.post("/dashboard/vm/1/op/add_interface/",
{'vlan': 1})
self.assertEqual(response.status_code, 302)
assert async.called
self.assertEqual(inst.interface_set.count(), interface_count)
def test_permitted_network_add(self): def test_permitted_network_add(self):
c = Client() c = Client()
...@@ -188,9 +164,12 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -188,9 +164,12 @@ class VmDetailTest(LoginMixin, TestCase):
vlan = Vlan.objects.get(id=1) vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u1, 'user') vlan.set_level(self.u1, 'user')
interface_count = inst.interface_set.count() interface_count = inst.interface_set.count()
response = c.post("/dashboard/vm/1/", with patch.object(AddInterfaceOperation, 'async') as mock_method:
{'new_network_vlan': 1}) mock_method.side_effect = inst.add_interface
response = c.post("/dashboard/vm/1/op/add_interface/",
{'vlan': 1})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
assert mock_method.called
self.assertEqual(inst.interface_set.count(), interface_count + 1) self.assertEqual(inst.interface_set.count(), interface_count + 1)
def test_permitted_network_delete(self): def test_permitted_network_delete(self):
...@@ -249,7 +228,6 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -249,7 +228,6 @@ class VmDetailTest(LoginMixin, TestCase):
def test_use_unpermitted_template(self): def test_use_unpermitted_template(self):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
Disk.objects.get(id=1).set_level(self.u1, 'user')
Vlan.objects.get(id=1).set_level(self.u1, 'user') Vlan.objects.get(id=1).set_level(self.u1, 'user')
response = c.post('/dashboard/vm/create/', response = c.post('/dashboard/vm/create/',
{'template': 1, {'template': 1,
...@@ -261,7 +239,6 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -261,7 +239,6 @@ class VmDetailTest(LoginMixin, TestCase):
def test_use_permitted_template(self): def test_use_permitted_template(self):
c = Client() c = Client()
self.login(c, 'user1') self.login(c, 'user1')
Disk.objects.get(id=1).set_level(self.u1, 'user')
InstanceTemplate.objects.get(id=1).set_level(self.u1, 'user') InstanceTemplate.objects.get(id=1).set_level(self.u1, 'user')
Vlan.objects.get(id=1).set_level(self.u1, 'user') Vlan.objects.get(id=1).set_level(self.u1, 'user')
response = c.post('/dashboard/vm/create/', response = c.post('/dashboard/vm/create/',
...@@ -293,7 +270,6 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -293,7 +270,6 @@ class VmDetailTest(LoginMixin, TestCase):
self.login(c, 'user1') self.login(c, 'user1')
tmpl = InstanceTemplate.objects.get(id=1) tmpl = InstanceTemplate.objects.get(id=1)
tmpl.set_level(self.u1, 'owner') tmpl.set_level(self.u1, 'owner')
tmpl.disks.get().set_level(self.u1, 'owner')
Vlan.objects.get(id=1).set_level(self.u1, 'user') Vlan.objects.get(id=1).set_level(self.u1, 'user')
kwargs = tmpl.__dict__.copy() kwargs = tmpl.__dict__.copy()
kwargs.update(name='t1', lease=1, disks=1, raw_data='tst1') kwargs.update(name='t1', lease=1, disks=1, raw_data='tst1')
...@@ -405,8 +381,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -405,8 +381,7 @@ class VmDetailTest(LoginMixin, TestCase):
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1) vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user') vlan.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/", inst.add_interface(user=self.u2, vlan=vlan)
{'new_network_vlan': 1})
host = Host.objects.get( host = Host.objects.get(
interface__in=inst.interface_set.all()) interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get( self.u2.user_permissions.add(Permission.objects.get(
...@@ -425,8 +400,7 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -425,8 +400,7 @@ class VmDetailTest(LoginMixin, TestCase):
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
vlan = Vlan.objects.get(id=1) vlan = Vlan.objects.get(id=1)
vlan.set_level(self.u2, 'user') vlan.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/", inst.add_interface(user=self.u2, vlan=vlan)
{'new_network_vlan': 1})
host = Host.objects.get( host = Host.objects.get(
interface__in=inst.interface_set.all()) interface__in=inst.interface_set.all())
self.u2.user_permissions.add(Permission.objects.get( self.u2.user_permissions.add(Permission.objects.get(
...@@ -533,47 +507,47 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -533,47 +507,47 @@ class VmDetailTest(LoginMixin, TestCase):
def test_permitted_wake_up_wrong_state(self): def test_permitted_wake_up_wrong_state(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
with patch.object(WakeUpOperation, 'async') as mock_method: with patch.object(WakeUpOperation, 'async') as mock_method, \
patch.object(Instance.WrongStateError, 'send_message') as wro:
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
mock_method.side_effect = inst.wake_up mock_method.side_effect = inst.wake_up
inst.manual_state_change('RUNNING') inst.status = 'RUNNING'
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
with patch('dashboard.views.messages') as msg: c.post("/dashboard/vm/1/op/wake_up/")
c.post("/dashboard/vm/1/op/wake_up/")
assert msg.error.called
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'RUNNING') # mocked anyway self.assertEqual(inst.status, 'RUNNING') # mocked anyway
assert mock_method.called assert mock_method.called
assert wro.called
def test_permitted_wake_up(self): def test_permitted_wake_up(self):
c = Client() c = Client()
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), \
with patch.object(WakeUpOperation, 'async') as new_wake_up: patch.object(WakeUpOperation, 'async') as new_wake_up, \
with patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa: patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa, \
inst = Instance.objects.get(pk=1) patch.object(Instance.WrongStateError, 'send_message') as wro:
new_wake_up.side_effect = inst.wake_up inst = Instance.objects.get(pk=1)
inst.get_remote_queue_name = Mock(return_value='test') new_wake_up.side_effect = inst.wake_up
inst.manual_state_change('SUSPENDED') inst.get_remote_queue_name = Mock(return_value='test')
inst.set_level(self.u2, 'owner') inst.status = 'SUSPENDED'
with patch('dashboard.views.messages') as msg: inst.set_level(self.u2, 'owner')
response = c.post("/dashboard/vm/1/op/wake_up/") with patch('dashboard.views.messages') as msg:
assert not msg.error.called response = c.post("/dashboard/vm/1/op/wake_up/")
self.assertEqual(response.status_code, 302) assert not msg.error.called
self.assertEqual(inst.status, 'RUNNING') self.assertEqual(response.status_code, 302)
assert new_wake_up.called self.assertEqual(inst.status, 'RUNNING')
assert wuaa.called assert new_wake_up.called
assert wuaa.called
assert not wro.called
def test_unpermitted_wake_up(self): def test_unpermitted_wake_up(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.manual_state_change('SUSPENDED') inst.status = 'SUSPENDED'
inst.set_level(self.u2, 'user') inst.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/op/wake_up/") response = c.post("/dashboard/vm/1/op/wake_up/")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'SUSPENDED')
def test_non_existing_template_get(self): def test_non_existing_template_get(self):
c = Client() c = Client()
...@@ -593,7 +567,6 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -593,7 +567,6 @@ class VmDetailTest(LoginMixin, TestCase):
'template': 1, 'template': 1,
'cpu_priority': 1, 'cpu_count': 1, 'ram_size': 1, 'cpu_priority': 1, 'cpu_count': 1, 'ram_size': 1,
'network': [], 'network': [],
'disks': [Disk.objects.get(id=1).pk],
}) })
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1250,7 +1223,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1250,7 +1223,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(self.g1.pk) + '/acl/',
{'perm-new-name': 'user3', 'perm-new': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users, len(gp.get_users_with_level())) self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1261,9 +1234,9 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1261,9 +1234,9 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(self.g1.pk) + '/acl/',
{'perm-new-name': 'user3', 'perm-new': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users, len(gp.get_users_with_level())) self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 302)
def test_superuser_add_acluser_to_group(self): def test_superuser_add_acluser_to_group(self):
c = Client() c = Client()
...@@ -1272,7 +1245,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1272,7 +1245,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(self.g1.pk) + '/acl/',
{'perm-new-name': 'user3', 'perm-new': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users + 1, len(gp.get_users_with_level())) self.assertEqual(acl_users + 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1283,7 +1256,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1283,7 +1256,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_users = len(gp.get_users_with_level()) acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(self.g1.pk) + '/acl/',
{'perm-new-name': 'user3', 'perm-new': 'owner'}) {'name': 'user3', 'level': 'owner'})
self.assertEqual(acl_users + 1, len(gp.get_users_with_level())) self.assertEqual(acl_users + 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1293,7 +1266,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1293,7 +1266,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(self.g1.pk) + '/acl/',
{'perm-new-name': 'group2', 'perm-new': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups, len(gp.get_groups_with_level())) self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1304,9 +1277,9 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1304,9 +1277,9 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(self.g1.pk) + '/acl/',
{'perm-new-name': 'group2', 'perm-new': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups, len(gp.get_groups_with_level())) self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 302)
def test_superuser_add_aclgroup_to_group(self): def test_superuser_add_aclgroup_to_group(self):
c = Client() c = Client()
...@@ -1315,7 +1288,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1315,7 +1288,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(self.g1.pk) + '/acl/',
{'perm-new-name': 'group2', 'perm-new': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level())) self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1326,7 +1299,7 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1326,7 +1299,7 @@ class GroupDetailTest(LoginMixin, TestCase):
acl_groups = len(gp.get_groups_with_level()) acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + response = c.post('/dashboard/group/' +
str(self.g1.pk) + '/acl/', str(self.g1.pk) + '/acl/',
{'perm-new-name': 'group2', 'perm-new': 'owner'}) {'name': 'group2', 'level': 'owner'})
self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level())) self.assertEqual(acl_groups + 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
...@@ -1368,84 +1341,6 @@ class GroupDetailTest(LoginMixin, TestCase): ...@@ -1368,84 +1341,6 @@ class GroupDetailTest(LoginMixin, TestCase):
self.assertEqual(user_in_group - 1, self.g1.user_set.count()) self.assertEqual(user_in_group - 1, self.g1.user_set.count())
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
def test_anon_remove_acluser_from_group(self):
c = Client()
gp = self.g1.profile
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/user/' + str(self.u4.pk) + '/')
self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302)
def test_unpermitted_remove_acluser_from_group(self):
c = Client()
self.login(c, 'user3')
gp = self.g1.profile
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/user/' + str(self.u4.pk) + '/')
self.assertEqual(acl_users, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 403)
def test_superuser_remove_acluser_from_group(self):
c = Client()
gp = self.g1.profile
self.login(c, 'superuser')
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/user/' + str(self.u4.pk) + '/')
self.assertEqual(acl_users - 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302)
def test_permitted_remove_acluser_from_group(self):
c = Client()
gp = self.g1.profile
self.login(c, 'user0')
acl_users = len(gp.get_users_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/user/' + str(self.u4.pk) + '/')
self.assertEqual(acl_users - 1, len(gp.get_users_with_level()))
self.assertEqual(response.status_code, 302)
def test_anon_remove_aclgroup_from_group(self):
c = Client()
gp = self.g1.profile
acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/group/' + str(self.g3.pk) + '/')
self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302)
def test_unpermitted_remove_aclgroup_from_group(self):
c = Client()
self.login(c, 'user3')
gp = self.g1.profile
acl_groups = len(gp.get_groups_with_level())
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/group/' + str(self.g3.pk) + '/')
self.assertEqual(acl_groups, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 403)
def test_superuser_remove_aclgroup_from_group(self):
c = Client()
gp = self.g1.profile
acl_groups = len(gp.get_groups_with_level())
self.login(c, 'superuser')
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/group/' + str(self.g3.pk) + '/')
self.assertEqual(acl_groups - 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302)
def test_permitted_remove_aclgroup_from_group(self):
c = Client()
gp = self.g1.profile
acl_groups = len(gp.get_groups_with_level())
self.login(c, 'user0')
response = c.post('/dashboard/group/' + str(self.g1.pk) +
'/remove/acl/group/' + str(self.g3.pk) + '/')
self.assertEqual(acl_groups - 1, len(gp.get_groups_with_level()))
self.assertEqual(response.status_code, 302)
def test_unpermitted_user_add_wo_group_perm(self): def test_unpermitted_user_add_wo_group_perm(self):
user_count = self.g1.user_set.count() user_count = self.g1.user_set.count()
c = Client() c = Client()
...@@ -1774,8 +1669,8 @@ class AclViewTest(LoginMixin, TestCase): ...@@ -1774,8 +1669,8 @@ class AclViewTest(LoginMixin, TestCase):
resp = c.post("/dashboard/vm/1/acl/", { resp = c.post("/dashboard/vm/1/acl/", {
'remove-u-%d' % self.u1.pk: "", 'remove-u-%d' % self.u1.pk: "",
'perm-new-name': "", 'name': "",
'perm-new': "", 'level': "",
}) })
self.assertFalse((self.u1, "user") in inst.get_users_with_level()) self.assertFalse((self.u1, "user") in inst.get_users_with_level())
self.assertEqual(resp.status_code, 302) self.assertEqual(resp.status_code, 302)
...@@ -1788,11 +1683,11 @@ class AclViewTest(LoginMixin, TestCase): ...@@ -1788,11 +1683,11 @@ class AclViewTest(LoginMixin, TestCase):
resp = c.post("/dashboard/vm/1/acl/", { resp = c.post("/dashboard/vm/1/acl/", {
'remove-u-%d' % self.u1.pk: "", 'remove-u-%d' % self.u1.pk: "",
'perm-new-name': "", 'name': "",
'perm-new': "", 'level': "",
}) })
self.assertTrue((self.u1, "user") in inst.get_users_with_level()) self.assertTrue((self.u1, "user") in inst.get_users_with_level())
self.assertEqual(resp.status_code, 403) self.assertEqual(resp.status_code, 302)
def test_instance_original_owner_access_revoke(self): def test_instance_original_owner_access_revoke(self):
c = Client() c = Client()
...@@ -1802,8 +1697,8 @@ class AclViewTest(LoginMixin, TestCase): ...@@ -1802,8 +1697,8 @@ class AclViewTest(LoginMixin, TestCase):
inst.set_level(self.ut, "owner") inst.set_level(self.ut, "owner")
resp = c.post("/dashboard/vm/1/acl/", { resp = c.post("/dashboard/vm/1/acl/", {
'remove-u-%d' % self.ut.pk: "", 'remove-u-%d' % self.ut.pk: "",
'perm-new-name': "", 'name': "",
'perm-new': "", 'level': "",
}) })
self.assertEqual(self.ut, Instance.objects.get(id=1).owner) self.assertEqual(self.ut, Instance.objects.get(id=1).owner)
self.assertTrue((self.ut, "owner") in inst.get_users_with_level()) self.assertTrue((self.ut, "owner") in inst.get_users_with_level())
...@@ -1818,8 +1713,8 @@ class AclViewTest(LoginMixin, TestCase): ...@@ -1818,8 +1713,8 @@ class AclViewTest(LoginMixin, TestCase):
resp = c.post("/dashboard/template/1/acl/", { resp = c.post("/dashboard/template/1/acl/", {
'remove-u-%d' % self.u1.pk: "", 'remove-u-%d' % self.u1.pk: "",
'perm-new-name': "", 'name': "",
'perm-new': "", 'level': "",
}) })
self.assertFalse((self.u1, "user") in tmpl.get_users_with_level()) self.assertFalse((self.u1, "user") in tmpl.get_users_with_level())
self.assertEqual(resp.status_code, 302) self.assertEqual(resp.status_code, 302)
...@@ -1832,11 +1727,11 @@ class AclViewTest(LoginMixin, TestCase): ...@@ -1832,11 +1727,11 @@ class AclViewTest(LoginMixin, TestCase):
resp = c.post("/dashboard/template/1/acl/", { resp = c.post("/dashboard/template/1/acl/", {
'remove-u-%d' % self.u1.pk: "", 'remove-u-%d' % self.u1.pk: "",
'perm-new-name': "", 'name': "",
'perm-new': "", 'level': "",
}) })
self.assertTrue((self.u1, "user") in tmpl.get_users_with_level()) self.assertTrue((self.u1, "user") in tmpl.get_users_with_level())
self.assertEqual(resp.status_code, 403) self.assertEqual(resp.status_code, 302)
def test_template_original_owner_access_revoke(self): def test_template_original_owner_access_revoke(self):
c = Client() c = Client()
...@@ -1846,8 +1741,8 @@ class AclViewTest(LoginMixin, TestCase): ...@@ -1846,8 +1741,8 @@ class AclViewTest(LoginMixin, TestCase):
tmpl.set_level(self.ut, "owner") tmpl.set_level(self.ut, "owner")
resp = c.post("/dashboard/template/1/acl/", { resp = c.post("/dashboard/template/1/acl/", {
'remove-u-%d' % self.ut.pk: "", 'remove-u-%d' % self.ut.pk: "",
'perm-new-name': "", 'name': "",
'perm-new': "", 'level': "",
}) })
self.assertEqual(self.ut, InstanceTemplate.objects.get(id=1).owner) self.assertEqual(self.ut, InstanceTemplate.objects.get(id=1).owner)
self.assertTrue((self.ut, "owner") in tmpl.get_users_with_level()) self.assertTrue((self.ut, "owner") in tmpl.get_users_with_level())
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
from __future__ import absolute_import from __future__ import absolute_import
from django.conf.urls import patterns, url, include from django.conf.urls import patterns, url, include
import autocomplete_light
from vm.models import Instance from vm.models import Instance
from .views import ( from .views import (
AclUpdateView, FavouriteView, GroupAclUpdateView, GroupDelete, AclUpdateView, FavouriteView, GroupAclUpdateView, GroupDelete,
...@@ -27,10 +28,10 @@ from .views import ( ...@@ -27,10 +28,10 @@ from .views import (
NodeDetailView, NodeFlushView, NodeGraphView, NodeList, NodeStatus, NodeDetailView, NodeFlushView, NodeGraphView, NodeList, NodeStatus,
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate, NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView, TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView, TransferOwnershipView, vm_activity, VmCreate, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView, GroupRemoveUserView,
GroupRemoveFutureUserView, GroupRemoveFutureUserView,
GroupCreate, GroupProfileUpdate, GroupCreate, GroupProfileUpdate,
TemplateChoose, TemplateChoose,
...@@ -45,7 +46,10 @@ from .views import ( ...@@ -45,7 +46,10 @@ from .views import (
LeaseAclUpdateView, LeaseAclUpdateView,
) )
autocomplete_light.autodiscover()
urlpatterns = patterns( urlpatterns = patterns(
'', '',
url(r'^$', IndexView.as_view(), name="dashboard.index"), url(r'^$', IndexView.as_view(), name="dashboard.index"),
url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(), url(r'^lease/(?P<pk>\d+)/$', LeaseDetail.as_view(),
...@@ -84,8 +88,6 @@ urlpatterns = patterns( ...@@ -84,8 +88,6 @@ urlpatterns = patterns(
url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'), url(r'^vm/list/$', VmList.as_view(), name='dashboard.views.vm-list'),
url(r'^vm/create/$', VmCreate.as_view(), url(r'^vm/create/$', VmCreate.as_view(),
name='dashboard.views.vm-create'), name='dashboard.views.vm-create'),
url(r'^vm/delete/(?P<pk>\d+)/$', VmDelete.as_view(),
name="dashboard.views.delete-vm"),
url(r'^vm/mass-delete/', VmMassDelete.as_view(), url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'), name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity), url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
...@@ -153,12 +155,6 @@ urlpatterns = patterns( ...@@ -153,12 +155,6 @@ urlpatterns = patterns(
name="dashboard.views.profile"), name="dashboard.views.profile"),
url(r'^profile/(?P<username>[^/]+)/use_gravatar/$', toggle_use_gravatar), url(r'^profile/(?P<username>[^/]+)/use_gravatar/$', toggle_use_gravatar),
url(r'^group/(?P<group_pk>\d+)/remove/acl/user/(?P<member_pk>\d+)/$',
GroupRemoveAclUserView.as_view(),
name="dashboard.views.remove-acluser"),
url(r'^group/(?P<group_pk>\d+)/remove/acl/group/(?P<member_pk>\d+)/$',
GroupRemoveAclGroupView.as_view(),
name="dashboard.views.remove-aclgroup"),
url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$', url(r'^group/(?P<group_pk>\d+)/remove/user/(?P<member_pk>\d+)/$',
GroupRemoveUserView.as_view(), GroupRemoveUserView.as_view(),
name="dashboard.views.remove-user"), name="dashboard.views.remove-user"),
...@@ -184,6 +180,8 @@ urlpatterns = patterns( ...@@ -184,6 +180,8 @@ urlpatterns = patterns(
UserKeyCreate.as_view(), UserKeyCreate.as_view(),
name="dashboard.views.userkey-create"), name="dashboard.views.userkey-create"),
url(r'^autocomplete/', include('autocomplete_light.urls')),
url(r"^store/list/$", StoreList.as_view(), url(r"^store/list/$", StoreList.as_view(),
name="dashboard.views.store-list"), name="dashboard.views.store-list"),
url(r"^store/download/$", store_download, url(r"^store/download/$", store_download,
......
...@@ -21,7 +21,11 @@ from __future__ import unicode_literals, absolute_import ...@@ -21,7 +21,11 @@ from __future__ import unicode_literals, absolute_import
from collections import OrderedDict from collections import OrderedDict
from itertools import chain from itertools import chain
from os import getenv from os import getenv
<<<<<<< HEAD
from os.path import join, normpath, dirname, basename from os.path import join, normpath, dirname, basename
=======
from urlparse import urljoin
>>>>>>> master
import json import json
import logging import logging
import re import re
...@@ -30,8 +34,11 @@ import requests ...@@ -30,8 +34,11 @@ import requests
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.auth.views import login, redirect_to_login from django.contrib.auth.views import login, redirect_to_login
<<<<<<< HEAD
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.messages import warning from django.contrib.messages import warning
=======
>>>>>>> master
from django.contrib.messages.views import SuccessMessageMixin from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import ( from django.core.exceptions import (
PermissionDenied, SuspiciousOperation, PermissionDenied, SuspiciousOperation,
...@@ -39,7 +46,7 @@ from django.core.exceptions import ( ...@@ -39,7 +46,7 @@ from django.core.exceptions import (
from django.core.cache import get_cache from django.core.cache import get_cache
from django.core import signing from django.core import signing
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.db.models import Count from django.db.models import Count, Q
from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import ( from django.shortcuts import (
redirect, render, get_object_or_404, render_to_response, redirect, render, get_object_or_404, render_to_response,
...@@ -59,6 +66,7 @@ from django_tables2 import SingleTableView ...@@ -59,6 +66,7 @@ from django_tables2 import SingleTableView
from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin, from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin,
PermissionRequiredMixin) PermissionRequiredMixin)
from braces.views._access import AccessMixin from braces.views._access import AccessMixin
from celery.exceptions import TimeoutError
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
...@@ -68,13 +76,15 @@ from .forms import ( ...@@ -68,13 +76,15 @@ from .forms import (
UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm, UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
VmSaveForm, UserKeyForm, VmRenewForm, VmSaveForm, UserKeyForm, VmRenewForm,
CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm, CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
TraitsForm, RawDataForm, GroupPermissionForm TraitsForm, RawDataForm, GroupPermissionForm, AclUserAddForm,
VmAddInterfaceForm,
) )
from .tables import ( from .tables import (
NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable, NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable,
GroupListTable, UserKeyListTable GroupListTable, UserKeyListTable
) )
from common.models import HumanReadableObject, HumanReadableException
from vm.models import ( from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface, Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
InterfaceTemplate, Lease, Node, NodeActivity, Trait, InterfaceTemplate, Lease, Node, NodeActivity, Trait,
...@@ -248,27 +258,6 @@ class IndexView(LoginRequiredMixin, TemplateView): ...@@ -248,27 +258,6 @@ class IndexView(LoginRequiredMixin, TemplateView):
return context return context
def get_vm_acl_data(obj):
levels = obj.ACL_LEVELS
users = obj.get_users_with_level()
users = [{'user': u, 'level': l} for u, l in users]
groups = obj.get_groups_with_level()
groups = [{'group': g, 'level': l} for g, l in groups]
return {'users': users, 'groups': groups, 'levels': levels,
'url': reverse('dashboard.views.vm-acl', args=[obj.pk])}
def get_group_acl_data(obj):
aclobj = obj.profile
levels = aclobj.ACL_LEVELS
users = aclobj.get_users_with_level()
users = [{'user': u, 'level': l} for u, l in users]
groups = aclobj.get_groups_with_level()
groups = [{'group': g, 'level': l} for g, l in groups]
return {'users': users, 'groups': groups, 'levels': levels,
'url': reverse('dashboard.views.group-acl', args=[obj.pk])}
class CheckedDetailView(LoginRequiredMixin, DetailView): class CheckedDetailView(LoginRequiredMixin, DetailView):
read_level = 'user' read_level = 'user'
...@@ -323,8 +312,11 @@ class VmDetailView(CheckedDetailView): ...@@ -323,8 +312,11 @@ class VmDetailView(CheckedDetailView):
}) })
# activity data # activity data
context['activities'] = self.object.get_merged_activities( activities = instance.get_merged_activities(self.request.user)
self.request.user) show_show_all = len(activities) > 10
activities = activities[:10]
context['activities'] = activities
context['show_show_all'] = show_show_all
context['vlans'] = Vlan.get_objects_with_level( context['vlans'] = Vlan.get_objects_with_level(
'user', self.request.user 'user', self.request.user
...@@ -332,7 +324,9 @@ class VmDetailView(CheckedDetailView): ...@@ -332,7 +324,9 @@ class VmDetailView(CheckedDetailView):
pk__in=Interface.objects.filter( pk__in=Interface.objects.filter(
instance=self.get_object()).values_list("vlan", flat=True) instance=self.get_object()).values_list("vlan", flat=True)
).all() ).all()
context['acl'] = get_vm_acl_data(instance) context['acl'] = AclUpdateView.get_acl_data(
instance, self.request.user, 'dashboard.views.vm-acl')
context['aclform'] = AclUserAddForm()
context['os_type_icon'] = instance.os_type.replace("unknown", context['os_type_icon'] = instance.os_type.replace("unknown",
"question") "question")
# ipv6 infos # ipv6 infos
...@@ -352,33 +346,19 @@ class VmDetailView(CheckedDetailView): ...@@ -352,33 +346,19 @@ class VmDetailView(CheckedDetailView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
options = { options = {
'change_password': self.__change_password,
'new_name': self.__set_name, 'new_name': self.__set_name,
'new_description': self.__set_description, 'new_description': self.__set_description,
'new_tag': self.__add_tag, 'new_tag': self.__add_tag,
'to_remove': self.__remove_tag, 'to_remove': self.__remove_tag,
'port': self.__add_port, 'port': self.__add_port,
'new_network_vlan': self.__new_network,
'abort_operation': self.__abort_operation, 'abort_operation': self.__abort_operation,
} }
for k, v in options.iteritems(): for k, v in options.iteritems():
if request.POST.get(k) is not None: if request.POST.get(k) is not None:
return v(request) return v(request)
raise Http404() raise Http404()
def __change_password(self, request): raise Http404()
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
self.object.change_password(user=request.user)
messages.success(request, _("Password changed."))
if request.is_ajax():
return HttpResponse("Success.")
else:
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk}))
def __set_name(self, request): def __set_name(self, request):
self.object = self.get_object() self.object = self.get_object()
...@@ -502,24 +482,6 @@ class VmDetailView(CheckedDetailView): ...@@ -502,24 +482,6 @@ class VmDetailView(CheckedDetailView):
return redirect(reverse_lazy("dashboard.views.detail", return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.get_object().pk})) kwargs={'pk': self.get_object().pk}))
def __new_network(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan"))
if not vlan.has_level(request.user, 'user'):
raise PermissionDenied()
try:
self.object.add_interface(vlan=vlan, user=request.user)
messages.success(request, _("Successfully added new interface."))
except Exception, e:
error = u' '.join(e.messages)
messages.error(request, error)
return redirect("%s#network" % reverse_lazy(
"dashboard.views.detail", kwargs={'pk': self.object.pk}))
def __abort_operation(self, request): def __abort_operation(self, request):
self.object = self.get_object() self.object = self.get_object()
...@@ -542,6 +504,7 @@ class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView): ...@@ -542,6 +504,7 @@ class VmTraitsUpdate(SuperuserRequiredMixin, UpdateView):
class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView): class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
form_class = RawDataForm form_class = RawDataForm
model = Instance model = Instance
template_name = 'dashboard/vm-detail/raw_data.html'
def get_success_url(self): def get_success_url(self):
return self.get_object().get_absolute_url() + "#resources" return self.get_object().get_absolute_url() + "#resources"
...@@ -552,6 +515,7 @@ class OperationView(RedirectToLoginMixin, DetailView): ...@@ -552,6 +515,7 @@ class OperationView(RedirectToLoginMixin, DetailView):
template_name = 'dashboard/operate.html' template_name = 'dashboard/operate.html'
show_in_toolbar = True show_in_toolbar = True
effect = None effect = None
wait_for_result = None
@property @property
def name(self): def name(self):
...@@ -613,19 +577,65 @@ class OperationView(RedirectToLoginMixin, DetailView): ...@@ -613,19 +577,65 @@ class OperationView(RedirectToLoginMixin, DetailView):
self.check_auth() self.check_auth()
return super(OperationView, self).get(request, *args, **kwargs) return super(OperationView, self).get(request, *args, **kwargs)
def get_response_data(self, result, done, extra=None, **kwargs):
"""Return serializable data to return to agents requesting json
response to POST"""
if extra is None:
extra = {}
extra["success"] = not isinstance(result, Exception)
extra["done"] = done
if isinstance(result, HumanReadableObject):
extra["message"] = result.get_user_text()
return extra
def post(self, request, extra=None, *args, **kwargs): def post(self, request, extra=None, *args, **kwargs):
self.check_auth() self.check_auth()
self.object = self.get_object() self.object = self.get_object()
if extra is None: if extra is None:
extra = {} extra = {}
result = None
done = False
try: try:
self.get_op().async(user=request.user, **extra) task = self.get_op().async(user=request.user, **extra)
except HumanReadableException as e:
e.send_message(request)
logger.exception("Could not start operation")
result = e
except Exception as e: except Exception as e:
messages.error(request, _('Could not start operation.')) messages.error(request, _('Could not start operation.'))
logger.exception(e) logger.exception("Could not start operation")
result = e
else: else:
messages.success(request, _('Operation is started.')) wait = self.wait_for_result
return redirect("%s#activity" % self.object.get_absolute_url()) if wait:
try:
result = task.get(timeout=wait,
interval=min((wait / 5, .5)))
except TimeoutError:
logger.debug("Result didn't arrive in %ss",
self.wait_for_result, exc_info=True)
except HumanReadableException as e:
e.send_message(request)
logger.exception(e)
result = e
except Exception as e:
messages.error(request, _('Operation failed.'))
logger.debug("Operation failed.", exc_info=True)
result = e
else:
done = True
messages.success(request, _('Operation succeeded.'))
if result is None and not done:
messages.success(request, _('Operation is started.'))
if "/json" in request.META.get("HTTP_ACCEPT", ""):
data = self.get_response_data(result, done,
post_extra=extra, **kwargs)
return HttpResponse(json.dumps(data),
content_type="application/json")
else:
return redirect("%s#activity" % self.object.get_absolute_url())
@classmethod @classmethod
def factory(cls, op, icon='cog', effect='info', extra_bases=(), **kwargs): def factory(cls, op, icon='cog', effect='info', extra_bases=(), **kwargs):
...@@ -652,6 +662,7 @@ class AjaxOperationMixin(object): ...@@ -652,6 +662,7 @@ class AjaxOperationMixin(object):
store.used = True store.used = True
return HttpResponse( return HttpResponse(
json.dumps({'success': True, json.dumps({'success': True,
'with_reload': getattr(self, 'with_reload', False),
'messages': [unicode(m) for m in store]}), 'messages': [unicode(m) for m in store]}),
content_type="application=json" content_type="application=json"
) )
...@@ -691,7 +702,9 @@ class FormOperationMixin(object): ...@@ -691,7 +702,9 @@ class FormOperationMixin(object):
request, extra, *args, **kwargs) request, extra, *args, **kwargs)
if request.is_ajax(): if request.is_ajax():
return HttpResponse( return HttpResponse(
json.dumps({'success': True}), json.dumps({
'success': True,
'with_reload': getattr(self, 'with_reload', False)}),
content_type="application=json" content_type="application=json"
) )
else: else:
...@@ -708,12 +721,32 @@ class RequestFormOperationMixin(FormOperationMixin): ...@@ -708,12 +721,32 @@ class RequestFormOperationMixin(FormOperationMixin):
return val return val
class VmAddInterfaceView(FormOperationMixin, VmOperationView):
op = 'add_interface'
form_class = VmAddInterfaceForm
show_in_toolbar = False
icon = 'globe'
effect = 'success'
with_reload = True
def get_form_kwargs(self):
inst = self.get_op().instance
choices = Vlan.get_objects_with_level(
"user", self.request.user).exclude(
vm_interface__instance__in=[inst])
val = super(VmAddInterfaceView, self).get_form_kwargs()
val.update({'choices': choices})
return val
class VmCreateDiskView(FormOperationMixin, VmOperationView): class VmCreateDiskView(FormOperationMixin, VmOperationView):
op = 'create_disk' op = 'create_disk'
form_class = VmCreateDiskForm form_class = VmCreateDiskForm
show_in_toolbar = False show_in_toolbar = False
icon = 'hdd-o' icon = 'hdd-o'
effect = "success"
is_disk_operation = True is_disk_operation = True
...@@ -723,6 +756,7 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView): ...@@ -723,6 +756,7 @@ class VmDownloadDiskView(FormOperationMixin, VmOperationView):
form_class = VmDownloadDiskForm form_class = VmDownloadDiskForm
show_in_toolbar = False show_in_toolbar = False
icon = 'download' icon = 'download'
effect = "success"
is_disk_operation = True is_disk_operation = True
...@@ -820,6 +854,7 @@ class TokenOperationView(OperationView): ...@@ -820,6 +854,7 @@ class TokenOperationView(OperationView):
logger.info("Request user changed to %s at %s", logger.info("Request user changed to %s at %s",
user, self.request.get_full_path()) user, self.request.get_full_path())
self.request.user = user self.request.user = user
self.request.token_user = True
else: else:
logger.debug("no token supplied to %s", logger.debug("no token supplied to %s",
self.request.get_full_path()) self.request.get_full_path())
...@@ -862,6 +897,7 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView): ...@@ -862,6 +897,7 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
effect = 'info' effect = 'info'
show_in_toolbar = False show_in_toolbar = False
form_class = VmRenewForm form_class = VmRenewForm
wait_for_result = 0.5
def get_form_kwargs(self): def get_form_kwargs(self):
choices = Lease.get_objects_with_level("user", self.request.user) choices = Lease.get_objects_with_level("user", self.request.user)
...@@ -874,6 +910,13 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView): ...@@ -874,6 +910,13 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
val.update({'choices': choices, 'default': default}) val.update({'choices': choices, 'default': default})
return val return val
def get_response_data(self, result, done, extra=None, **kwargs):
extra = super(VmRenewView, self).get_response_data(result, done,
extra, **kwargs)
extra["new_suspend_time"] = unicode(self.get_op().
instance.time_of_suspend)
return extra
vm_ops = OrderedDict([ vm_ops = OrderedDict([
('deploy', VmOperationView.factory( ('deploy', VmOperationView.factory(
...@@ -895,13 +938,19 @@ vm_ops = OrderedDict([ ...@@ -895,13 +938,19 @@ vm_ops = OrderedDict([
op='shut_off', icon='ban', effect='warning')), op='shut_off', icon='ban', effect='warning')),
('recover', VmOperationView.factory( ('recover', VmOperationView.factory(
op='recover', icon='medkit', effect='warning')), op='recover', icon='medkit', effect='warning')),
('nostate', VmOperationView.factory(
op='emergency_change_state', icon='legal', effect='danger')),
('destroy', VmOperationView.factory( ('destroy', VmOperationView.factory(
extra_bases=[TokenOperationView], extra_bases=[TokenOperationView],
op='destroy', icon='times', effect='danger')), op='destroy', icon='times', effect='danger')),
('create_disk', VmCreateDiskView), ('create_disk', VmCreateDiskView),
('download_disk', VmDownloadDiskView), ('download_disk', VmDownloadDiskView),
('add_interface', VmAddInterfaceView),
('renew', VmRenewView), ('renew', VmRenewView),
('resources_change', VmResourcesChangeView), ('resources_change', VmResourcesChangeView),
('password_reset', VmOperationView.factory(
op='password_reset', icon='unlock', effect='warning',
show_in_toolbar=False, wait_for_result=0.5, with_reload=True)),
]) ])
...@@ -1005,7 +1054,10 @@ class GroupDetailView(CheckedDetailView): ...@@ -1005,7 +1054,10 @@ class GroupDetailView(CheckedDetailView):
context['users'] = self.object.user_set.all() context['users'] = self.object.user_set.all()
context['future_users'] = FutureMember.objects.filter( context['future_users'] = FutureMember.objects.filter(
group=self.object) group=self.object)
context['acl'] = get_group_acl_data(self.object) context['acl'] = AclUpdateView.get_acl_data(
self.object.profile, self.request.user,
'dashboard.views.group-acl')
context['aclform'] = AclUserAddForm()
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)
...@@ -1048,7 +1100,7 @@ class GroupDetailView(CheckedDetailView): ...@@ -1048,7 +1100,7 @@ class GroupDetailView(CheckedDetailView):
FutureMember.objects.get_or_create(org_id=name, FutureMember.objects.get_or_create(org_id=name,
group=self.object) group=self.object)
else: else:
warning(request, _('User "%s" not found.') % name) messages.warning(request, _('User "%s" not found.') % name)
def __add_list(self, request): def __add_list(self, request):
if not self.get_has_level()(request.user, 'operator'): if not self.get_has_level()(request.user, 'operator'):
...@@ -1093,120 +1145,169 @@ class GroupPermissionsView(SuperuserRequiredMixin, UpdateView): ...@@ -1093,120 +1145,169 @@ class GroupPermissionsView(SuperuserRequiredMixin, UpdateView):
class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin): class AclUpdateView(LoginRequiredMixin, View, SingleObjectMixin):
def send_success_message(self, whom, old_level, new_level):
if old_level and new_level:
msg = _("Acl user/group %(w)s successfully modified.")
elif not old_level and new_level:
msg = _("Acl user/group %(w)s successfully added.")
elif old_level and not new_level:
msg = _("Acl user/group %(w)s successfully removed.")
if msg:
messages.success(self.request, msg % {'w': whom})
def get_level(self, whom):
for u, level in self.acl_data:
if u == whom:
return level
return None
def post(self, request, *args, **kwargs): @classmethod
instance = self.get_object() def get_acl_data(cls, obj, user, url):
if not (instance.has_level(request.user, "owner") or levels = obj.ACL_LEVELS
getattr(instance, 'owner', None) == request.user): allowed_levels = list(l for l in OrderedDict(levels)
logger.warning('Tried to set permissions of %s by non-owner %s.', if cls.has_next_level(user, obj, l))
unicode(instance), unicode(request.user)) is_owner = 'owner' in allowed_levels
raise PermissionDenied()
self.set_levels(request, instance) allowed_users = cls.get_allowed_users(user)
self.remove_levels(request, instance) allowed_groups = cls.get_allowed_groups(user)
self.add_levels(request, instance)
return redirect("%s#access" % instance.get_absolute_url()) user_levels = list(
{'user': u, 'level': l} for u, l in obj.get_users_with_level()
def set_levels(self, request, instance): if is_owner or u == user or u in allowed_users)
for key, value in request.POST.items():
m = re.match('perm-([ug])-(\d+)', key) group_levels = list(
{'group': g, 'level': l} for g, l in obj.get_groups_with_level()
if is_owner or g in allowed_groups)
return {'users': user_levels,
'groups': group_levels,
'levels': levels,
'allowed_levels': allowed_levels,
'url': reverse(url, args=[obj.pk])}
@classmethod
def has_next_level(self, user, instance, level):
levels = OrderedDict(instance.ACL_LEVELS).keys()
next_levels = dict(zip([None] + levels, levels + levels[-1:]))
# {None: 'user', 'user': 'operator', 'operator: 'owner',
# 'owner: 'owner'}
next_level = next_levels[level]
return instance.has_level(user, next_level)
@classmethod
def get_allowed_groups(cls, user):
if user.has_perm('dashboard.use_autocomplete'):
return Group.objects.all()
else:
profiles = GroupProfile.get_objects_with_level('owner', user)
return Group.objects.filter(groupprofile__in=profiles).distinct()
@classmethod
def get_allowed_users(cls, user):
if user.has_perm('dashboard.use_autocomplete'):
return User.objects.all()
else:
groups = cls.get_allowed_groups(user)
return User.objects.filter(
Q(groups__in=groups) | Q(pk=user.pk)).distinct()
def check_auth(self, whom, old_level, new_level):
if isinstance(whom, Group):
if (not self.is_owner and whom not in
AclUpdateView.get_allowed_groups(self.request.user)):
return False
elif isinstance(whom, User):
if (not self.is_owner and whom not in
AclUpdateView.get_allowed_users(self.request.user)):
return False
return (
AclUpdateView.has_next_level(self.request.user,
self.instance, new_level) and
AclUpdateView.has_next_level(self.request.user,
self.instance, old_level))
def set_level(self, whom, new_level):
user = self.request.user
old_level = self.get_level(whom)
if old_level == new_level:
return
if getattr(self.instance, "owner", None) == whom:
logger.info("Tried to set owner's acl level for %s by %s.",
unicode(self.instance), unicode(user))
msg = _("The original owner cannot be removed, however "
"you can transfer ownership.")
if not getattr(self, 'hide_messages', False):
messages.warning(self.request, msg)
elif self.check_auth(whom, old_level, new_level):
logger.info(
u"Set %s's acl level for %s to %s by %s.", unicode(whom),
unicode(self.instance), new_level, unicode(user))
if not getattr(self, 'hide_messages', False):
self.send_success_message(whom, old_level, new_level)
self.instance.set_level(whom, new_level)
else:
logger.warning(
u"Tried to set %s's acl_level for %s (%s->%s) by %s.",
unicode(whom), unicode(self.instance), old_level, new_level,
unicode(user))
def set_or_remove_levels(self):
for key, value in self.request.POST.items():
m = re.match('(perm|remove)-([ug])-(\d+)', key)
if m: if m:
typ, id = m.groups() cmd, typ, id = m.groups()
if cmd == 'remove':
value = None
entity = {'u': User, 'g': Group}[typ].objects.get(id=id) entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
if getattr(instance, "owner", None) == entity: self.set_level(entity, value)
logger.info("Tried to set owner's acl level for %s by %s.",
unicode(instance), unicode(request.user)) def add_levels(self):
continue name = self.request.POST.get('name', None)
instance.set_level(entity, value) level = self.request.POST.get('level', None)
logger.info("Set %s's acl level for %s to %s by %s.", if not name or not level:
unicode(entity), unicode(instance),
value, unicode(request.user))
def remove_levels(self, request, instance):
for key, value in request.POST.items():
if key.startswith("remove"):
typ = key[7:8] # len("remove-")
id = key[9:] # len("remove-x-")
entity = {'u': User, 'g': Group}[typ].objects.get(id=id)
if getattr(instance, "owner", None) == entity:
logger.info("Tried to remove owner from %s by %s.",
unicode(instance), unicode(request.user))
msg = _("The original owner cannot be removed, however "
"you can transfer ownership.")
messages.warning(request, msg)
continue
instance.set_level(entity, None)
logger.info("Revoked %s's access to %s by %s.",
unicode(entity), unicode(instance),
unicode(request.user))
def add_levels(self, request, instance):
name = request.POST['perm-new-name']
value = request.POST['perm-new']
if not name:
return return
try: try:
entity = search_user(name) entity = search_user(name)
if self.instance.object_level_set.filter(users__in=[entity]):
messages.warning(
self.request, _('User "%s" has already '
'access to this object.') % name)
return
except User.DoesNotExist: except User.DoesNotExist:
entity = None entity = None
try: try:
entity = Group.objects.get(name=name) entity = Group.objects.get(name=name)
if self.instance.object_level_set.filter(groups__in=[entity]):
messages.warning(
self.request, _('Group "%s" has already '
'access to this object.') % name)
return
except Group.DoesNotExist: except Group.DoesNotExist:
warning(request, _('User or group "%s" not found.') % name) messages.warning(
self.request, _('User or group "%s" not found.') % name)
return return
self.set_level(entity, level)
instance.set_level(entity, value) def post(self, request, *args, **kwargs):
logger.info("Set %s's new acl level for %s to %s by %s.", self.instance = self.get_object()
unicode(entity), unicode(instance), self.is_owner = self.instance.has_level(request.user, 'owner')
value, unicode(request.user)) self.acl_data = (self.instance.get_users_with_level() +
self.instance.get_groups_with_level())
self.set_or_remove_levels()
self.add_levels()
return redirect("%s#access" % self.instance.get_absolute_url())
class TemplateAclUpdateView(AclUpdateView): class TemplateAclUpdateView(AclUpdateView):
model = InstanceTemplate model = InstanceTemplate
def post(self, request, *args, **kwargs):
template = self.get_object()
if not (template.has_level(request.user, "owner") or
getattr(template, 'owner', None) == request.user):
logger.warning('Tried to set permissions of %s by non-owner %s.',
unicode(template), unicode(request.user))
raise PermissionDenied()
name = request.POST['perm-new-name']
if (User.objects.filter(username=name).count() +
Group.objects.filter(name=name).count() < 1
and len(name) > 0):
warning(request, _('User or group "%s" not found.') % name)
else:
self.set_levels(request, template)
self.add_levels(request, template)
self.remove_levels(request, template)
post_for_disk = request.POST.copy()
post_for_disk['perm-new'] = 'user'
request.POST = post_for_disk
for d in template.disks.all():
self.set_levels(request, d)
self.add_levels(request, d)
self.remove_levels(request, d)
return redirect(template)
class GroupAclUpdateView(AclUpdateView): class GroupAclUpdateView(AclUpdateView):
model = Group model = Group
def post(self, request, *args, **kwargs): def get_object(self):
instance = self.get_object().profile return super(GroupAclUpdateView, self).get_object().profile
if not (instance.has_level(request.user, "owner") or
getattr(instance, 'owner', None) == request.user):
logger.warning('Tried to set permissions of %s by non-owner %s.',
unicode(instance), unicode(request.user))
raise PermissionDenied()
self.set_levels(request, instance)
self.add_levels(request, instance)
return redirect(reverse("dashboard.views.group-detail",
kwargs=self.kwargs))
class TemplateChoose(LoginRequiredMixin, TemplateView): class TemplateChoose(LoginRequiredMixin, TemplateView):
...@@ -1353,8 +1454,11 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView): ...@@ -1353,8 +1454,11 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
obj = self.get_object() obj = self.get_object()
context = super(TemplateDetail, self).get_context_data(**kwargs) context = super(TemplateDetail, self).get_context_data(**kwargs)
context['acl'] = get_vm_acl_data(obj) context['acl'] = AclUpdateView.get_acl_data(
obj, self.request.user, 'dashboard.views.template-acl')
context['disks'] = obj.disks.all() context['disks'] = obj.disks.all()
context['is_owner'] = obj.has_level(self.request.user, 'owner')
context['aclform'] = AclUserAddForm()
return context return context
def get_success_url(self): def get_success_url(self):
...@@ -1653,34 +1757,6 @@ class GroupRemoveFutureUserView(GroupRemoveUserView): ...@@ -1653,34 +1757,6 @@ class GroupRemoveFutureUserView(GroupRemoveUserView):
return _("Future user successfully removed from group.") return _("Future user successfully removed from group.")
class GroupRemoveAclUserView(GroupRemoveUserView):
def remove_member(self, pk):
container = self.get_object().profile
container.set_level(User.objects.get(pk=pk), None)
def get_success_message(self):
return _("Acl user successfully removed from group.")
class GroupRemoveAclGroupView(GroupRemoveUserView):
def get_context_data(self, **kwargs):
context = super(GroupRemoveUserView, self).get_context_data(**kwargs)
try:
context['member'] = Group.objects.get(pk=self.member_pk)
except User.DoesNotExist:
raise Http404()
return context
def remove_member(self, pk):
container = self.get_object().profile
container.set_level(Group.objects.get(pk=pk), None)
def get_success_message(self):
return _("Acl group successfully removed from group.")
class GroupDelete(CheckedDetailView, DeleteView): class GroupDelete(CheckedDetailView, DeleteView):
"""This stuff deletes the group. """This stuff deletes the group.
...@@ -1807,13 +1883,12 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1807,13 +1883,12 @@ class VmCreate(LoginRequiredMixin, TemplateView):
} }
networks = [InterfaceTemplate(vlan=l, managed=l.managed) networks = [InterfaceTemplate(vlan=l, managed=l.managed)
for l in post['networks']] for l in post['networks']]
disks = post['disks']
ikwargs.update({ ikwargs.update({
'template': template, 'template': template,
'owner': user, 'owner': user,
'networks': networks, 'networks': networks,
'disks': disks, 'disks': list(template.disks.all()),
}) })
amount = post['amount'] amount = post['amount']
...@@ -1855,9 +1930,13 @@ class VmCreate(LoginRequiredMixin, TemplateView): ...@@ -1855,9 +1930,13 @@ class VmCreate(LoginRequiredMixin, TemplateView):
except Exception as e: except Exception as e:
logger.debug('No profile or instance limit: %s', e) logger.debug('No profile or instance limit: %s', e)
else: else:
try:
amount = int(request.POST.get("amount", 1))
except:
amount = limit # TODO this should definitely use a Form
current = Instance.active.filter(owner=user).count() current = Instance.active.filter(owner=user).count()
logger.debug('current use: %d, limit: %d', current, limit) logger.debug('current use: %d, limit: %d', current, limit)
if limit < current: if current + amount > limit:
messages.error(request, messages.error(request,
_('Instance limit (%d) exceeded.') % limit) _('Instance limit (%d) exceeded.') % limit)
if request.is_ajax(): if request.is_ajax():
...@@ -2021,53 +2100,6 @@ class GroupProfileUpdate(SuccessMessageMixin, GroupCodeMixin, ...@@ -2021,53 +2100,6 @@ class GroupProfileUpdate(SuccessMessageMixin, GroupCodeMixin,
return self.form_valid(form) return self.form_valid(form)
class VmDelete(LoginRequiredMixin, DeleteView):
model = Instance
template_name = "dashboard/confirm/base-delete.html"
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_success_url(self):
next = self.request.POST.get('next')
if next:
return next
else:
return reverse_lazy('dashboard.index')
def get_context_data(self, **kwargs):
object = self.get_object()
if not object.has_level(self.request.user, 'owner'):
raise PermissionDenied()
context = super(VmDelete, self).get_context_data(**kwargs)
return context
# github.com/django/django/blob/master/django/views/generic/edit.py#L245
def delete(self, request, *args, **kwargs):
object = self.get_object()
if not object.has_level(request.user, 'owner'):
raise PermissionDenied()
object.destroy.async(user=request.user)
success_url = self.get_success_url()
success_message = _("VM successfully deleted.")
if request.is_ajax():
if request.POST.get('redirect').lower() == "true":
messages.success(request, success_message)
return HttpResponse(
json.dumps({'message': success_message}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect(success_url)
class NodeDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView): class NodeDelete(LoginRequiredMixin, SuperuserRequiredMixin, DeleteView):
"""This stuff deletes the node. """This stuff deletes the node.
...@@ -2345,7 +2377,8 @@ class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -2345,7 +2377,8 @@ class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin,
def get_context_data(self, *args, **kwargs): def get_context_data(self, *args, **kwargs):
obj = self.get_object() obj = self.get_object()
context = super(LeaseDetail, self).get_context_data(*args, **kwargs) context = super(LeaseDetail, self).get_context_data(*args, **kwargs)
context['acl'] = get_vm_acl_data(obj) context['acl'] = AclUpdateView.get_acl_data(
obj, self.request.user, 'dashboard.views.lease-acl')
return context return context
def get_success_url(self): def get_success_url(self):
...@@ -2405,30 +2438,35 @@ def vm_activity(request, pk): ...@@ -2405,30 +2438,35 @@ def vm_activity(request, pk):
raise PermissionDenied() raise PermissionDenied()
response = {} response = {}
only_status = request.GET.get("only_status", "false") show_all = request.GET.get("show_all", "false") == "true"
activities = instance.get_merged_activities(request.user)
show_show_all = len(activities) > 10
if not show_all:
activities = activities[:10]
response['human_readable_status'] = instance.get_status_display() response['human_readable_status'] = instance.get_status_display()
response['status'] = instance.status response['status'] = instance.status
response['icon'] = instance.get_status_icon() response['icon'] = instance.get_status_icon()
if only_status == "false": # instance activity
context = {
'instance': instance,
'activities': instance.get_merged_activities(request.user),
'ops': get_operations(instance, request.user),
}
response['activities'] = render_to_string( context = {
"dashboard/vm-detail/_activity-timeline.html", 'instance': instance,
RequestContext(request, context), 'activities': activities,
) 'show_show_all': show_show_all,
response['ops'] = render_to_string( 'ops': get_operations(instance, request.user),
"dashboard/vm-detail/_operations.html", }
RequestContext(request, context),
) response['activities'] = render_to_string(
response['disk_ops'] = render_to_string( "dashboard/vm-detail/_activity-timeline.html",
"dashboard/vm-detail/_disk-operations.html", RequestContext(request, context),
RequestContext(request, context), )
) response['ops'] = render_to_string(
"dashboard/vm-detail/_operations.html",
RequestContext(request, context),
)
response['disk_ops'] = render_to_string(
"dashboard/vm-detail/_disk-operations.html",
RequestContext(request, context),
)
return HttpResponse( return HttpResponse(
json.dumps(response), json.dumps(response),
...@@ -2818,12 +2856,11 @@ class DiskRemoveView(DeleteView): ...@@ -2818,12 +2856,11 @@ class DiskRemoveView(DeleteView):
def delete(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs):
disk = self.get_object() disk = self.get_object()
if not disk.has_level(request.user, 'owner'):
raise PermissionDenied()
disk = self.get_object()
app = disk.get_appliance() app = disk.get_appliance()
if not app.has_level(request.user, 'owner'):
raise PermissionDenied()
app.remove_disk(disk=disk, user=request.user) app.remove_disk(disk=disk, user=request.user)
disk.destroy() disk.destroy()
...@@ -2844,7 +2881,7 @@ class DiskRemoveView(DeleteView): ...@@ -2844,7 +2881,7 @@ class DiskRemoveView(DeleteView):
@require_GET @require_GET
def get_disk_download_status(request, pk): def get_disk_download_status(request, pk):
disk = Disk.objects.get(pk=pk) disk = Disk.objects.get(pk=pk)
if not disk.has_level(request.user, 'owner'): if not disk.get_appliance().has_level(request.user, 'owner'):
raise PermissionDenied() raise PermissionDenied()
return HttpResponse( return HttpResponse(
...@@ -3249,3 +3286,7 @@ def store_refresh_toplist(request): ...@@ -3249,3 +3286,7 @@ def store_refresh_toplist(request):
cache.set(cache_key, files, 300) cache.set(cache_key, files, 300)
return redirect(reverse("dashboard.index")) return redirect(reverse("dashboard.index"))
def absolute_url(url):
return urljoin(settings.DJANGO_URL, url)
from django.core.exceptions import ValidationError
from lxml import etree as ET
import logging
rng_file = "/usr/share/libvirt/schemas/domain.rng"
# Mandatory xml elements dor parsing
header = "<domain type='kvm'><name>validator</name>\
<memory unit='KiB'>1024</memory>\
<os><type>hvm</type></os>"
footer = "</domain>"
logger = logging.getLogger()
def domain_validator(value):
xml = header + value + footer
try:
parsed_xml = ET.fromstring(xml)
except Exception as e:
raise ValidationError(e.message)
try:
relaxng = ET.RelaxNG(file=rng_file)
except:
logger.critical("%s RelaxNG libvirt domain schema file "
"is missing for validation.", rng_file)
else:
try:
relaxng.assertValid(parsed_xml)
except Exception as e:
raise ValidationError(e.message)
#!/bin/echo Usage: fab --list -f
import contextlib
import datetime
from fabric.api import env, run, settings, sudo, prefix, cd, execute
from fabric.context_managers import shell_env
from fabric.decorators import roles, parallel
env.roledefs['portal'] = ['localhost']
try:
from vm.models import Node as _Node
from storage.models import DataStore as _DataStore
except Exception as e:
print e
else:
env.roledefs['node'] = [unicode(n.host.ipv4)
for n in _Node.objects.filter(enabled=True)]
env.roledefs['storage'] = [_DataStore.objects.get().hostname]
def update_all():
"Update and restart portal+manager, nodes and storage"
execute(stop_portal)
execute(parallel(update_node))
execute(update_storage)
execute(update_portal)
def pip(env, req):
"Install pip requirements"
with _workon(env):
run("pip install -r %s" % req)
@roles('portal')
def migrate():
"Run db migrations"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py migrate")
@roles('portal')
def compile_js():
"Generate JS translation objects"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py compilejsi18n -o dashboard/static/jsi18n")
@roles('portal')
def collectstatic():
"Collect static files"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py collectstatic --noinput")
@roles('portal')
def compile_messages():
"Generate MO translation objects"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py compilemessages")
@roles('portal')
def compile_things():
"Compile translation and collect static files"
compile_js()
collectstatic()
compile_messages()
@roles('portal')
def make_messages():
"Update PO translation templates and commit"
with _workon("circle"), cd("~/circle/circle"):
run("git status")
run("./manage.py makemessages -d djangojs -a --ignore=jsi18n/*")
run("./manage.py makemessages -d django -a")
run("git commit -avm 'update PO templates'")
@roles('portal')
def test(test=""):
"Run portal tests"
with _workon("circle"), cd("~/circle/circle"):
run("./manage.py test --settings=circle.settings.test %s" % test)
def pull(dir="~/circle/circle"):
"Pull from upstream branch (stash any changes)"
now = unicode(datetime.datetime.now())
with cd(dir), shell_env(GIT_AUTHOR_NAME="fabric",
GIT_AUTHOR_EMAIL="fabric@local",
GIT_COMMITTER_NAME="fabric",
GIT_COMMITTER_EMAIL="fabric@local"):
run("git stash save update %s" % now)
run("git pull --ff-only")
@roles('portal')
def update_portal(test=False):
"Update and restart portal+manager"
with _stopped("portal", "mancelery"):
pull()
pip("circle", "~/circle/requirements.txt")
migrate()
compile_things()
if test:
test()
@roles('portal')
def stop_portal(test=False):
"Stop portal and manager"
_stop_services("portal", "mancelery")
@roles('node')
def update_node():
"Update and restart nodes"
with _stopped("node", "agentdriver"):
pull("~/vmdriver")
pip("vmdriver", "~/vmdriver/requirements/production.txt")
pull("~/agentdriver")
pip("agentdriver", "~/agentdriver/requirements.txt")
@parallel
@roles('storage')
def update_storage():
"Update and restart storagedriver"
with _stopped("storage"):
pull("~/storagedriver")
pip("storagedriver", "~/storagedriver/requirements/production.txt")
@parallel
@roles('node')
def checkout(vmdriver="master", agent="master"):
"""Checkout specific branch on nodes"""
with settings(warn_only=True), cd("~/vmdriver"):
run("git checkout %s" % vmdriver)
with settings(warn_only=True), cd("~/agentdriver"):
run("git checkout %s" % agent)
def _stop_services(*services):
"Stop given services (warn only if not running)"
with settings(warn_only=True):
for service in reversed(services):
sudo("stop %s" % service)
def _start_services(*services):
for service in services:
sudo("start %s" % service)
def _restart_service(*services):
"Stop and start services"
_stop_services(*services)
_start_services(*services)
@contextlib.contextmanager
def _stopped(*services):
_stop_services(*services)
yield
_start_services(*services)
def _workon(name):
return prefix("source ~/.virtualenvs/%s/bin/activate && "
"source ~/.virtualenvs/%s/bin/postactivate" % (name, name))
[General]
LangCode=hu
MailingList=cloud@ik.bme.hu
PotBaseDir=./
ProjectID=circle-hu
TargetLangCode=hu
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -6,7 +6,7 @@ msgid "" ...@@ -6,7 +6,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-05-07 14:25+0200\n" "POT-Creation-Date: 2014-07-29 12:56+0200\n"
"PO-Revision-Date: 2014-05-07 15:32+0200\n" "PO-Revision-Date: 2014-05-07 15:32+0200\n"
"Last-Translator: Mate Ory <orymate@ik.bme.hu>\n" "Last-Translator: Mate Ory <orymate@ik.bme.hu>\n"
"Language-Team: Hungarian <cloud@ik.bme.hu>\n" "Language-Team: Hungarian <cloud@ik.bme.hu>\n"
...@@ -17,105 +17,144 @@ msgstr "" ...@@ -17,105 +17,144 @@ msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Lokalize 1.5\n" "X-Generator: Lokalize 1.5\n"
#: dashboard/static/dashboard/dashboard.js:54 #: dashboard/static/dashboard/dashboard.js:68
#: static_collected/dashboard/dashboard.js:68
msgid "Select an option to proceed!" msgid "Select an option to proceed!"
msgstr "Válasszon a folytatáshoz." msgstr "Válasszon a folytatáshoz."
#: dashboard/static/dashboard/dashboard.js:257
#: dashboard/static/dashboard/dashboard.js:304
#: dashboard/static/dashboard/dashboard.js:314
#: static_collected/dashboard/dashboard.js:257
#: static_collected/dashboard/dashboard.js:304
#: static_collected/dashboard/dashboard.js:314
msgid "No result"
msgstr ""
#: dashboard/static/dashboard/profile.js:18
#: static_collected/dashboard/profile.js:18
msgid "You have no permission to change this profile."
msgstr ""
#: dashboard/static/dashboard/profile.js:20
#: static_collected/dashboard/profile.js:20
msgid "Unknown error."
msgstr ""
#: dashboard/static/dashboard/vm-tour.js:20 #: dashboard/static/dashboard/vm-tour.js:20
#: static_collected/dashboard/vm-tour.js:20
msgid "Prev" msgid "Prev"
msgstr "Vissza" msgstr "Vissza"
#: dashboard/static/dashboard/vm-tour.js:22 #: dashboard/static/dashboard/vm-tour.js:22
#: static_collected/dashboard/vm-tour.js:22
msgid "Next" msgid "Next"
msgstr "Tovább" msgstr "Tovább"
#: dashboard/static/dashboard/vm-tour.js:26 #: dashboard/static/dashboard/vm-tour.js:26
#: static_collected/dashboard/vm-tour.js:26
msgid "End tour" msgid "End tour"
msgstr "Befejezés"