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.
......
...@@ -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,
......
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.
<!--
Collection name attribute represents the name of the menu, e.g., to use menu "File" use "file" or "Help" use "help". You can add new menus.
If you type a relative script file beware the this script is located in $KDEHOME/share/apps/applicationname/
The following example adds an action with the text "Export..." into the "File" menu
<KrossScripting>
<collection name="file" text="File" comment="File menu">
<script name="export" text="Export..." comment="Export content" file="export.py" />
</collection>
</KrossScripting>
-->
...@@ -23,61 +23,7 @@ ...@@ -23,61 +23,7 @@
<div class="page-header"> <div class="page-header">
<h3>{% trans "Manage access" %}</h3> <h3>{% trans "Manage access" %}</h3>
</div> </div>
<form action="{% url "network.vlan-acl" vid=vlan_vid %}" method="post">{% csrf_token %} {% include "dashboard/_manage_access.html" with table_id="vlan-access-table" %}
<table class="table table-striped table-with-form-fields" id="vlan-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>{{i.user}}</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>{{i.group}}</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>
{% endblock %} {% endblock %}
...@@ -63,9 +63,9 @@ class VlanAclTest(LoginMixin, TestCase): ...@@ -63,9 +63,9 @@ class VlanAclTest(LoginMixin, TestCase):
vlan = Vlan.objects.get(vid=1) vlan = Vlan.objects.get(vid=1)
self.assertEqual([], vlan.get_users_with_level()) self.assertEqual([], vlan.get_users_with_level())
resp = c.post("/network/vlans/1/acl/", { resp = c.post("/network/vlans/2/acl/", {
'perm-new-name': "user1", 'name': "user1",
'perm-new': "user", 'level': "user",
}) })
vlan = Vlan.objects.get(vid=1) vlan = Vlan.objects.get(vid=1)
...@@ -80,10 +80,10 @@ class VlanAclTest(LoginMixin, TestCase): ...@@ -80,10 +80,10 @@ class VlanAclTest(LoginMixin, TestCase):
vlan.set_level(self.u1, "user") vlan.set_level(self.u1, "user")
self.assertTrue((self.u1, "user") in vlan.get_users_with_level()) self.assertTrue((self.u1, "user") in vlan.get_users_with_level())
resp = c.post("/network/vlans/1/acl/", { resp = c.post("/network/vlans/2/acl/", {
'perm-u-%d' % self.u1.pk: "operator", 'perm-u-%d' % self.u1.pk: "operator",
'perm-new': "", 'level': "",
'perm-new-name': "", 'name': "",
}) })
self.assertTrue((self.u1, "operator") in vlan.get_users_with_level()) self.assertTrue((self.u1, "operator") in vlan.get_users_with_level())
...@@ -97,10 +97,10 @@ class VlanAclTest(LoginMixin, TestCase): ...@@ -97,10 +97,10 @@ class VlanAclTest(LoginMixin, TestCase):
vlan.set_level(self.u1, "user") vlan.set_level(self.u1, "user")
self.assertTrue((self.u1, "user") in vlan.get_users_with_level()) self.assertTrue((self.u1, "user") in vlan.get_users_with_level())
resp = c.post("/network/vlans/1/acl/", { resp = c.post("/network/vlans/2/acl/", {
'remove-u-%d' % self.u1.pk: "", 'remove-u-%d' % self.u1.pk: "",
'perm-new': "", 'level': "",
'perm-new-name': "", 'name': "",
}) })
self.assertTrue((self.u1, "user") not in vlan.get_users_with_level()) self.assertTrue((self.u1, "user") not in vlan.get_users_with_level())
......
...@@ -84,7 +84,7 @@ urlpatterns = patterns( ...@@ -84,7 +84,7 @@ urlpatterns = patterns(
url('^vlans/$', VlanList.as_view(), name='network.vlan_list'), url('^vlans/$', VlanList.as_view(), name='network.vlan_list'),
url('^vlans/create$', VlanCreate.as_view(), name='network.vlan_create'), url('^vlans/create$', VlanCreate.as_view(), name='network.vlan_create'),
url('^vlans/(?P<vid>\d+)/$', VlanDetail.as_view(), name='network.vlan'), url('^vlans/(?P<vid>\d+)/$', VlanDetail.as_view(), name='network.vlan'),
url('^vlans/(?P<vid>\d+)/acl/$', VlanAclUpdateView.as_view(), url('^vlans/(?P<pk>\d+)/acl/$', VlanAclUpdateView.as_view(),
name='network.vlan-acl'), name='network.vlan-acl'),
url('^vlans/delete/(?P<vid>\d+)/$', VlanDelete.as_view(), url('^vlans/delete/(?P<vid>\d+)/$', VlanDelete.as_view(),
name="network.vlan_delete"), name="network.vlan_delete"),
......
...@@ -42,6 +42,7 @@ from operator import itemgetter ...@@ -42,6 +42,7 @@ from operator import itemgetter
from itertools import chain from itertools import chain
import json import json
from dashboard.views import AclUpdateView from dashboard.views import AclUpdateView
from dashboard.forms import AclUserAddForm
class SuccessMessageMixin(FormMixin): class SuccessMessageMixin(FormMixin):
...@@ -629,19 +630,8 @@ class VlanList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView): ...@@ -629,19 +630,8 @@ class VlanList(LoginRequiredMixin, SuperuserRequiredMixin, SingleTableView):
table_pagination = False table_pagination = False
def get_vlan_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}
class VlanAclUpdateView(AclUpdateView): class VlanAclUpdateView(AclUpdateView):
model = Vlan model = Vlan
slug_field = "vid"
slug_url_kwarg = "vid"
class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
...@@ -662,7 +652,9 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin, ...@@ -662,7 +652,9 @@ class VlanDetail(LoginRequiredMixin, SuperuserRequiredMixin,
context['host_list'] = SmallHostTable(q) context['host_list'] = SmallHostTable(q)
context['vlan_vid'] = self.kwargs.get('vid') context['vlan_vid'] = self.kwargs.get('vid')
context['acl'] = get_vlan_acl_data(self.get_object()) context['acl'] = AclUpdateView.get_acl_data(
self.object, self.request.user, 'network.vlan-acl')
context['aclform'] = AclUserAddForm()
return context return context
success_url = reverse_lazy('network.vlan_list') success_url = reverse_lazy('network.vlan_list')
......
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
import logging
logging.basicConfig(filename='/var/tmp/0015_disk_migration.log',level=logging.INFO)
logger = logging.getLogger()
class Migration(DataMigration):
def forwards(self, orm):
"Write your forwards methods here."
# Note: Don't use "from appname.models import ModelName".
# Use orm.ModelName to refer to models in this application,
# and orm['appname.ModelName'] for models in other applications.
disks = orm.Disk
for disk in disks.objects.all():
if disk.base is None and disk.destroyed is None:
logger.info("Set disk %s (%s) with filename: %s to ready.",
disk.name, disk.pk, disk.filename)
disk.is_ready = True
disk.save()
def backwards(self, orm):
"Write your backwards methods here."
models = {
u'storage.datastore': {
'Meta': {'ordering': "[u'name']", 'object_name': 'DataStore'},
'hostname': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '40'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '100'}),
'path': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '200'})
},
u'storage.disk': {
'Meta': {'ordering': "[u'name']", 'object_name': 'Disk'},
'base': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'derivatives'", 'null': 'True', 'to': u"orm['storage.Disk']"}),
'created': ('model_utils.fields.AutoCreatedField', [], {'default': 'datetime.datetime.now'}),
'datastore': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['storage.DataStore']"}),
'destroyed': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}),
'dev_num': ('django.db.models.fields.CharField', [], {'default': "u'a'", 'max_length': '1'}),
'filename': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '256'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_ready': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'modified': ('model_utils.fields.AutoLastModifiedField', [], {'default': 'datetime.datetime.now'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100', 'blank': 'True'}),
'size': ('sizefield.models.FileSizeField', [], {'default': 'None', 'null': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '10'})
}
}
complete_apps = ['storage']
symmetrical = True
...@@ -27,14 +27,13 @@ from celery.contrib.abortable import AbortableAsyncResult ...@@ -27,14 +27,13 @@ from celery.contrib.abortable import AbortableAsyncResult
from django.db.models import (Model, BooleanField, CharField, DateTimeField, from django.db.models import (Model, BooleanField, CharField, DateTimeField,
ForeignKey) ForeignKey)
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _, ugettext_noop
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from sizefield.models import FileSizeField from sizefield.models import FileSizeField
from acl.models import AclBase
from .tasks import local_tasks, storage_tasks from .tasks import local_tasks, storage_tasks
from celery.exceptions import TimeoutError from celery.exceptions import TimeoutError
from common.models import WorkerNotFound from common.models import WorkerNotFound, HumanReadableException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -76,15 +75,10 @@ class DataStore(Model): ...@@ -76,15 +75,10 @@ class DataStore(Model):
destroyed__isnull=False) if disk.is_deletable] destroyed__isnull=False) if disk.is_deletable]
class Disk(AclBase, TimeStampedModel): class Disk(TimeStampedModel):
"""A virtual disk. """A virtual disk.
""" """
ACL_LEVELS = (
('user', _('user')), # see all details
('operator', _('operator')),
('owner', _('owner')), # superuser, can delete, delegate perms
)
TYPES = [('qcow2-norm', 'qcow2 normal'), ('qcow2-snap', 'qcow2 snapshot'), TYPES = [('qcow2-norm', 'qcow2 normal'), ('qcow2-snap', 'qcow2 snapshot'),
('iso', 'iso'), ('raw-ro', 'raw read-only'), ('raw-rw', 'raw')] ('iso', 'iso'), ('raw-ro', 'raw read-only'), ('raw-rw', 'raw')]
name = CharField(blank=True, max_length=100, verbose_name=_("name")) name = CharField(blank=True, max_length=100, verbose_name=_("name"))
...@@ -110,43 +104,73 @@ class Disk(AclBase, TimeStampedModel): ...@@ -110,43 +104,73 @@ class Disk(AclBase, TimeStampedModel):
('create_empty_disk', _('Can create an empty disk.')), ('create_empty_disk', _('Can create an empty disk.')),
('download_disk', _('Can download a disk.'))) ('download_disk', _('Can download a disk.')))
class WrongDiskTypeError(Exception): class DiskError(HumanReadableException):
admin_message = None
def __init__(self, type, message=None):
if message is None: def __init__(self, disk, params=None, level=None, **kwargs):
message = ("Operation can't be invoked on a disk of type '%s'." kwargs.update(params or {})
% type) self.disc = kwargs["disk"] = disk
super(Disk.DiskError, self).__init__(
Exception.__init__(self, message) level, self.message, self.admin_message or self.message,
kwargs)
self.type = type
class WrongDiskTypeError(DiskError):
class DiskInUseError(Exception): message = ugettext_noop("Operation can't be invoked on disk "
"'%(name)s' of type '%(type)s'.")
def __init__(self, disk, message=None):
if message is None: admin_message = ugettext_noop(
message = ("The requested operation can't be performed on " "Operation can't be invoked on disk "
"disk '%s (%s)' because it is in use." % "'%(name)s' (%(pk)s) of type '%(type)s'.")
(disk.name, disk.filename))
def __init__(self, disk, params=None, **kwargs):
Exception.__init__(self, message) super(Disk.WrongDiskTypeError, self).__init__(
disk, params, type=disk.type, name=disk.name, pk=disk.pk)
self.disk = disk
class DiskInUseError(DiskError):
class DiskIsNotReady(Exception): message = ugettext_noop(
"The requested operation can't be performed on "
""" Exception for operations that need a deployed disk. "disk '%(name)s' because it is in use.")
"""
admin_message = ugettext_noop(
def __init__(self, disk, message=None): "The requested operation can't be performed on "
if message is None: "disk '%(name)s' (%(pk)s) because it is in use.")
message = ("The requested operation can't be performed on "
"disk '%s (%s)' because it has never been" def __init__(self, disk, params=None, **kwargs):
"deployed." % (disk.name, disk.filename)) super(Disk.DiskInUseError, self).__init__(
disk, params, name=disk.name, pk=disk.pk)
Exception.__init__(self, message)
class DiskIsNotReady(DiskError):
self.disk = disk message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' because it has never been deployed.")
admin_message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' (%(pk)s) [%(filename)s] because it has never been"
"deployed.")
def __init__(self, disk, params=None, **kwargs):
super(Disk.DiskIsNotReady, self).__init__(
disk, params, name=disk.name, pk=disk.pk,
filename=disk.filename)
class DiskBaseIsNotReady(DiskError):
message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' because its base has never been deployed.")
admin_message = ugettext_noop(
"The requested operation can't be performed on "
"disk '%(name)s' (%(pk)s) [%(filename)s] because its base "
"'%(b_name)s' (%(b_pk)s) [%(b_filename)s] has never been"
"deployed.")
def __init__(self, disk, params=None, **kwargs):
base = kwargs.get('base')
super(Disk.DiskBaseIsNotReady, self).__init__(
disk, params, name=disk.name, pk=disk.pk,
filename=disk.filename, b_name=base.name,
b_pk=base.pk, b_filename=base.filename)
@property @property
def path(self): def path(self):
...@@ -225,15 +249,14 @@ class Disk(AclBase, TimeStampedModel): ...@@ -225,15 +249,14 @@ class Disk(AclBase, TimeStampedModel):
return any(i.state != 'STOPPED' for i in self.instance_set.all()) return any(i.state != 'STOPPED' for i in self.instance_set.all())
def get_appliance(self): def get_appliance(self):
"""Return an Instance or InstanceTemplate object where the disk is used """Return the Instance or InstanceTemplate object where the disk
is used
""" """
instance = self.instance_set.all() from vm.models import Instance
template = self.template_set.all() try:
app = list(instance) + list(template) return self.instance_set.get()
if len(app) > 0: except Instance.DoesNotExist:
return app[0] return self.template_set.get()
else:
return None
def get_exclusive(self): def get_exclusive(self):
"""Get an instance of the disk for exclusive usage. """Get an instance of the disk for exclusive usage.
...@@ -247,7 +270,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -247,7 +270,7 @@ class Disk(AclBase, TimeStampedModel):
} }
if self.type not in type_mapping.keys(): if self.type not in type_mapping.keys():
raise self.WrongDiskTypeError(self.type) raise self.WrongDiskTypeError(self)
new_type = type_mapping[self.type] new_type = type_mapping[self.type]
...@@ -320,6 +343,8 @@ class Disk(AclBase, TimeStampedModel): ...@@ -320,6 +343,8 @@ class Disk(AclBase, TimeStampedModel):
if self.is_ready: if self.is_ready:
return True return True
if self.base and not self.base.is_ready:
raise self.DiskBaseIsNotReady(self, base=self.base)
queue_name = self.get_remote_queue_name('storage', priority="fast") queue_name = self.get_remote_queue_name('storage', priority="fast")
disk_desc = self.get_disk_desc() disk_desc = self.get_disk_desc()
if self.base is not None: if self.base is not None:
...@@ -424,7 +449,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -424,7 +449,7 @@ class Disk(AclBase, TimeStampedModel):
'iso': ("iso", self), 'iso': ("iso", self),
} }
if self.type not in mapping.keys(): if self.type not in mapping.keys():
raise self.WrongDiskTypeError(self.type) raise self.WrongDiskTypeError(self)
if self.is_in_use: if self.is_in_use:
raise self.DiskInUseError(self) raise self.DiskInUseError(self)
...@@ -440,7 +465,7 @@ class Disk(AclBase, TimeStampedModel): ...@@ -440,7 +465,7 @@ class Disk(AclBase, TimeStampedModel):
disk = Disk.create(datastore=self.datastore, disk = Disk.create(datastore=self.datastore,
base=new_base, base=new_base,
name=self.name, size=self.size, name=self.name, size=self.size,
type=new_type) type=new_type, dev_num=self.dev_num)
queue_name = self.get_remote_queue_name("storage", priority="slow") queue_name = self.get_remote_queue_name("storage", priority="slow")
remote = storage_tasks.merge.apply_async(kwargs={ remote = storage_tasks.merge.apply_async(kwargs={
...@@ -457,4 +482,6 @@ class Disk(AclBase, TimeStampedModel): ...@@ -457,4 +482,6 @@ class Disk(AclBase, TimeStampedModel):
AbortableAsyncResult(remote.id).abort() AbortableAsyncResult(remote.id).abort()
disk.destroy() disk.destroy()
raise Exception("Save as aborted by use.") raise Exception("Save as aborted by use.")
disk.is_ready = True
disk.save()
return disk return disk
...@@ -13,6 +13,7 @@ from .instance import InstanceTemplate ...@@ -13,6 +13,7 @@ from .instance import InstanceTemplate
from .instance import Instance from .instance import Instance
from .instance import post_state_changed from .instance import post_state_changed
from .instance import pre_state_changed from .instance import pre_state_changed
from .instance import pwgen
from .network import InterfaceTemplate from .network import InterfaceTemplate
from .network import Interface from .network import Interface
from .node import Node from .node import Node
...@@ -22,5 +23,5 @@ __all__ = [ ...@@ -22,5 +23,5 @@ __all__ = [
'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate', 'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate',
'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed', 'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed',
'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease', 'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease',
'node_activity', 'node_activity', 'pwgen'
] ]
...@@ -30,7 +30,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext_noop ...@@ -30,7 +30,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext_noop
from common.models import ( from common.models import (
ActivityModel, activitycontextimpl, create_readable, join_activity_code, ActivityModel, activitycontextimpl, create_readable, join_activity_code,
HumanReadableObject, HumanReadableObject, HumanReadableException,
) )
from manager.mancelery import celery from manager.mancelery import celery
...@@ -39,16 +39,17 @@ from manager.mancelery import celery ...@@ -39,16 +39,17 @@ from manager.mancelery import celery
logger = getLogger(__name__) logger = getLogger(__name__)
class ActivityInProgressError(Exception): class ActivityInProgressError(HumanReadableException):
def __init__(self, activity, message=None): @classmethod
if message is None: def create(cls, activity):
message = ("Another activity is currently in progress: '%s'." obj = super(ActivityInProgressError, cls).create(
% activity.activity_code) ugettext_noop("%(activity)s activity is currently in progress."),
ugettext_noop("%(activity)s (%(pk)s) activity is currently "
Exception.__init__(self, message) "in progress."),
activity=activity.readable_name, pk=activity.pk)
self.activity = activity obj.activity = activity
return obj
def _normalize_readable_name(name, default=None): def _normalize_readable_name(name, default=None):
...@@ -95,7 +96,7 @@ class InstanceActivity(ActivityModel): ...@@ -95,7 +96,7 @@ class InstanceActivity(ActivityModel):
# Check for concurrent activities # Check for concurrent activities
active_activities = instance.activity_log.filter(finished__isnull=True) active_activities = instance.activity_log.filter(finished__isnull=True)
if concurrency_check and active_activities.exists(): if concurrency_check and active_activities.exists():
raise ActivityInProgressError(active_activities[0]) raise ActivityInProgressError.create(active_activities[0])
activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix) activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
act = cls(activity_code=activity_code, instance=instance, parent=None, act = cls(activity_code=activity_code, instance=instance, parent=None,
...@@ -112,7 +113,7 @@ class InstanceActivity(ActivityModel): ...@@ -112,7 +113,7 @@ class InstanceActivity(ActivityModel):
# Check for concurrent activities # Check for concurrent activities
active_children = self.children.filter(finished__isnull=True) active_children = self.children.filter(finished__isnull=True)
if concurrency_check and active_children.exists(): if concurrency_check and active_children.exists():
raise ActivityInProgressError(active_children[0]) raise ActivityInProgressError.create(active_children[0])
act = InstanceActivity( act = InstanceActivity(
activity_code=join_activity_code(self.activity_code, code_suffix), activity_code=join_activity_code(self.activity_code, code_suffix),
......
...@@ -41,7 +41,7 @@ from model_utils.models import TimeStampedModel, StatusModel ...@@ -41,7 +41,7 @@ from model_utils.models import TimeStampedModel, StatusModel
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from acl.models import AclBase from acl.models import AclBase
from common.models import create_readable from common.models import create_readable, HumanReadableException
from common.operations import OperatedMixin from common.operations import OperatedMixin
from ..tasks import vm_tasks, agent_tasks from ..tasks import vm_tasks, agent_tasks
from .activity import (ActivityInProgressError, instance_activity, from .activity import (ActivityInProgressError, instance_activity,
...@@ -271,32 +271,31 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -271,32 +271,31 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
('create_vm', _('Can create a new VM.')), ('create_vm', _('Can create a new VM.')),
('config_ports', _('Can configure port forwards.')), ('config_ports', _('Can configure port forwards.')),
('recover', _('Can recover a destroyed VM.')), ('recover', _('Can recover a destroyed VM.')),
('emergency_change_state', _('Can change VM state to NOSTATE.')),
) )
verbose_name = _('instance') verbose_name = _('instance')
verbose_name_plural = _('instances') verbose_name_plural = _('instances')
class InstanceDestroyedError(Exception): class InstanceError(HumanReadableException):
def __init__(self, instance, message=None): def __init__(self, instance, params=None, level=None, **kwargs):
if message is None: kwargs.update(params or {})
message = ("The instance (%s) has already been destroyed." self.instance = kwargs["instance"] = instance
% instance) super(Instance.InstanceError, self).__init__(
level, self.message, self.message, kwargs)
Exception.__init__(self, message) class InstanceDestroyedError(InstanceError):
message = ugettext_noop(
"Instance %(instance)s has already been destroyed.")
self.instance = instance class WrongStateError(InstanceError):
message = ugettext_noop(
"Current state (%(state)s) of instance %(instance)s is "
"inappropriate for the invoked operation.")
class WrongStateError(Exception): def __init__(self, instance, params=None, **kwargs):
super(Instance.WrongStateError, self).__init__(
def __init__(self, instance, message=None): instance, params, state=instance.status)
if message is None:
message = ("The instance's current state (%s) is "
"inappropriate for the invoked operation."
% instance.status)
Exception.__init__(self, message)
self.instance = instance
def __unicode__(self): def __unicode__(self):
parts = (self.name, "(" + str(self.id) + ")") parts = (self.name, "(" + str(self.id) + ")")
...@@ -404,13 +403,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -404,13 +403,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
""" """
disks = template.disks.all() if disks is None else disks disks = template.disks.all() if disks is None else disks
for disk in disks:
if not disk.has_level(owner, 'user'):
raise PermissionDenied()
elif (disk.type == 'qcow2-snap'
and not disk.has_level(owner, 'owner')):
raise PermissionDenied()
networks = (template.interface_set.all() if networks is None networks = (template.interface_set.all() if networks is None
else networks) else networks)
...@@ -444,27 +436,15 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -444,27 +436,15 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
self.time_of_suspend, self.time_of_delete = self.get_renew_times() self.time_of_suspend, self.time_of_delete = self.get_renew_times()
super(Instance, self).clean(*args, **kwargs) super(Instance, self).clean(*args, **kwargs)
def manual_state_change(self, new_state="NOSTATE", reason=None, user=None):
""" Manually change state of an Instance.
Can be used to recover VM after administrator fixed problems.
"""
# TODO cancel concurrent activity (if exists)
act = InstanceActivity.create(
code_suffix='manual_state_change', instance=self, user=user,
readable_name=create_readable(ugettext_noop(
"force %(state)s state"), state=new_state))
act.finished = act.started
act.result = reason
act.resultant_state = new_state
act.succeeded = True
act.save()
def vm_state_changed(self, new_state): def vm_state_changed(self, new_state):
# log state change # log state change
try: try:
act = InstanceActivity.create(code_suffix='vm_state_changed', act = InstanceActivity.create(
instance=self) code_suffix='vm_state_changed',
readable_name=create_readable(
ugettext_noop("vm state changed to %(state)s"),
state=new_state),
instance=self)
except ActivityInProgressError: except ActivityInProgressError:
pass # discard state change if another activity is in progress. pass # discard state change if another activity is in progress.
else: else:
...@@ -663,6 +643,13 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -663,6 +643,13 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
:param again: Notify already notified owners. :param again: Notify already notified owners.
""" """
notification_msg = ugettext_noop(
'Your instance <a href="%(url)s">%(instance)s</a> is going to '
'expire. It will be suspended at %(suspend)s and destroyed at '
'%(delete)s. Please, either <a href="%(token)s">renew</a> '
'or <a href="%(url)s">destroy</a> it now.')
if not again and self._is_notified_about_expiration(): if not again and self._is_notified_about_expiration():
return False return False
success, failed = [], [] success, failed = [], []
...@@ -687,20 +674,26 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -687,20 +674,26 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
readable_name=ugettext_noop( readable_name=ugettext_noop(
"notify owner about expiration"), "notify owner about expiration"),
on_commit=on_commit): on_commit=on_commit):
from dashboard.views import VmRenewView from dashboard.views import VmRenewView, absolute_url
level = self.get_level_object("owner") level = self.get_level_object("owner")
for u, ulevel in self.get_users_with_level(level__pk=level.pk): for u, ulevel in self.get_users_with_level(level__pk=level.pk):
try: try:
token = VmRenewView.get_token_url(self, u) token = VmRenewView.get_token_url(self, u)
u.profile.notify( u.profile.notify(
_('%s expiring soon') % unicode(self), ugettext_noop('%(instance)s expiring soon'),
'dashboard/notifications/vm-expiring.html', notification_msg, url=self.get_absolute_url(),
{'instance': self, 'token': token}, valid_until=min( instance=self, suspend=self.time_of_suspend,
self.time_of_delete, self.time_of_suspend)) token=token, delete=self.time_of_delete)
except Exception as e: except Exception as e:
failed.append((u, e)) failed.append((u, e))
else: else:
success.append(u) success.append(u)
if self.status == "RUNNING":
token = absolute_url(
VmRenewView.get_token_url(self, self.owner))
queue = self.get_remote_queue_name("agent")
agent_tasks.send_expiration.apply_async(
queue=queue, args=(self.vm_name, token))
return True return True
def is_expiring(self, threshold=0.1): def is_expiring(self, threshold=0.1):
...@@ -740,24 +733,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -740,24 +733,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
timezone.now() + lease.suspend_interval, timezone.now() + lease.suspend_interval,
timezone.now() + lease.delete_interval) timezone.now() + lease.delete_interval)
def change_password(self, user=None):
"""Generate new password for the vm
:param self: The virtual machine.
:param user: The user who's issuing the command.
"""
self.pw = pwgen()
with instance_activity(code_suffix='change_password', instance=self,
readable_name=ugettext_noop("change password"),
user=user):
queue = self.get_remote_queue_name("agent")
agent_tasks.change_password.apply_async(queue=queue,
args=(self.vm_name,
self.pw))
self.save()
def select_node(self): def select_node(self):
"""Returns the node the VM should be deployed or migrated to. """Returns the node the VM should be deployed or migrated to.
""" """
......
...@@ -71,3 +71,8 @@ def del_keys(vm, keys): ...@@ -71,3 +71,8 @@ def del_keys(vm, keys):
@celery.task(name='agent.get_keys') @celery.task(name='agent.get_keys')
def get_keys(vm): def get_keys(vm):
pass pass
@celery.task(name='agent.send_expiration')
def send_expiration(vm, url):
pass
...@@ -25,22 +25,25 @@ from StringIO import StringIO ...@@ -25,22 +25,25 @@ from StringIO import StringIO
from tarfile import TarFile, TarInfo from tarfile import TarFile, TarInfo
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext_noop
from celery.result import TimeoutError from celery.result import TimeoutError
from monitor.client import Client from monitor.client import Client
def send_init_commands(instance, act, vm): def send_init_commands(instance, act, vm):
queue = instance.get_remote_queue_name("agent") queue = instance.get_remote_queue_name("agent")
with act.sub_activity('cleanup', readable_name=ugettext_noop('cleanup')):
with act.sub_activity('cleanup'):
cleanup.apply_async(queue=queue, args=(vm, )) cleanup.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('restart_networking'): with act.sub_activity('restart_networking',
readable_name=ugettext_noop('restart networking')):
restart_networking.apply_async(queue=queue, args=(vm, )) restart_networking.apply_async(queue=queue, args=(vm, ))
with act.sub_activity('change_password'): with act.sub_activity('change_password',
readable_name=ugettext_noop('change password')):
change_password.apply_async(queue=queue, args=(vm, instance.pw)) change_password.apply_async(queue=queue, args=(vm, instance.pw))
with act.sub_activity('set_time'): with act.sub_activity('set_time', readable_name=ugettext_noop('set time')):
set_time.apply_async(queue=queue, args=(vm, time.time())) set_time.apply_async(queue=queue, args=(vm, time.time()))
with act.sub_activity('set_hostname'): with act.sub_activity('set_hostname',
readable_name=ugettext_noop('set hostname')):
set_hostname.apply_async( set_hostname.apply_async(
queue=queue, args=(vm, instance.primary_host.hostname)) queue=queue, args=(vm, instance.primary_host.hostname))
...@@ -73,13 +76,20 @@ def agent_started(vm, version=None): ...@@ -73,13 +76,20 @@ def agent_started(vm, version=None):
initialized = InstanceActivity.objects.filter( initialized = InstanceActivity.objects.filter(
instance=instance, activity_code='vm.Instance.agent.cleanup').exists() instance=instance, activity_code='vm.Instance.agent.cleanup').exists()
with instance_activity(code_suffix='agent', instance=instance) as act: with instance_activity(code_suffix='agent',
with act.sub_activity('starting'): readable_name=ugettext_noop('agent'),
instance=instance) as act:
with act.sub_activity('starting',
readable_name=ugettext_noop('starting')):
pass pass
if version and version != settings.AGENT_VERSION: if version and version != settings.AGENT_VERSION:
try: try:
with act.sub_activity('update'): with act.sub_activity(
'update',
readable_name=ugettext_noop('update to %(version)s'),
version=settings.AGENT_VERSION
):
update.apply_async( update.apply_async(
queue=queue, queue=queue,
args=(vm, create_agent_tar())).get(timeout=10) args=(vm, create_agent_tar())).get(timeout=10)
...@@ -91,7 +101,10 @@ def agent_started(vm, version=None): ...@@ -91,7 +101,10 @@ def agent_started(vm, version=None):
measure_boot_time(instance) measure_boot_time(instance)
send_init_commands(instance, act, vm) send_init_commands(instance, act, vm)
with act.sub_activity('start_access_server'): with act.sub_activity(
'start_access_server',
readable_name=ugettext_noop('start access server')
):
start_access_server.apply_async(queue=queue, args=(vm, )) start_access_server.apply_async(queue=queue, args=(vm, ))
...@@ -122,5 +135,5 @@ def agent_stopped(vm): ...@@ -122,5 +135,5 @@ def agent_stopped(vm):
qs = InstanceActivity.objects.filter(instance=instance, qs = InstanceActivity.objects.filter(instance=instance,
activity_code='vm.Instance.agent') activity_code='vm.Instance.agent')
act = qs.latest('id') act = qs.latest('id')
with act.sub_activity('stopping'): with act.sub_activity('stopping', readable_name=ugettext_noop('stopping')):
pass pass
...@@ -66,8 +66,8 @@ class InstanceTestCase(TestCase): ...@@ -66,8 +66,8 @@ class InstanceTestCase(TestCase):
inst = MagicMock(spec=Instance, node=node, vnc_port=port) inst = MagicMock(spec=Instance, node=node, vnc_port=port)
inst.save.side_effect = AssertionError inst.save.side_effect = AssertionError
with patch('vm.models.instance.InstanceActivity') as ia: with patch('vm.models.instance.InstanceActivity') as ia:
ia.create.side_effect = ActivityInProgressError(MagicMock()) ia.create.side_effect = ActivityInProgressError.create(MagicMock())
Instance.vm_state_changed(inst, 'STOPPED') Instance.status = 'STOPPED'
self.assertEquals(inst.node, node) self.assertEquals(inst.node, node)
self.assertEquals(inst.vnc_port, port) self.assertEquals(inst.vnc_port, port)
...@@ -210,7 +210,7 @@ class NodeTestCase(TestCase): ...@@ -210,7 +210,7 @@ class NodeTestCase(TestCase):
node.enabled = True node.enabled = True
node.STATES = Node.STATES node.STATES = Node.STATES
self.assertEqual(Node.get_state(node), "ONLINE") self.assertEqual(Node.get_state(node), "ONLINE")
assert isinstance(Node.get_status_display(node), _("").__class__) assert isinstance(Node.get_status_display(node), _("x").__class__)
class InstanceActivityTestCase(TestCase): class InstanceActivityTestCase(TestCase):
......
...@@ -4,6 +4,7 @@ billiard==3.3.0.17 ...@@ -4,6 +4,7 @@ billiard==3.3.0.17
bpython==0.12 bpython==0.12
celery==3.1.11 celery==3.1.11
Django==1.6.3 Django==1.6.3
django-autocomplete-light==1.4.14
django-braces==1.4.0 django-braces==1.4.0
django-celery==3.1.10 django-celery==3.1.10
django-crispy-forms==1.4.0 django-crispy-forms==1.4.0
...@@ -20,7 +21,6 @@ kombu==3.0.15 ...@@ -20,7 +21,6 @@ kombu==3.0.15
logutils==0.3.3 logutils==0.3.3
MarkupSafe==0.21 MarkupSafe==0.21
netaddr==0.7.11 netaddr==0.7.11
nose==1.3.1
pip-tools==0.3.4 pip-tools==0.3.4
psycopg2==2.5.2 psycopg2==2.5.2
Pygments==1.6 Pygments==1.6
...@@ -33,3 +33,5 @@ six==1.6.1 ...@@ -33,3 +33,5 @@ six==1.6.1
South==0.8.4 South==0.8.4
sqlparse==0.1.11 sqlparse==0.1.11
pika==0.9.13 pika==0.9.13
Fabric==1.9.0
lxml==3.3.5
...@@ -3,3 +3,5 @@ ...@@ -3,3 +3,5 @@
coverage==3.7.1 coverage==3.7.1
factory-boy==2.3.1 factory-boy==2.3.1
mock==1.0.1 mock==1.0.1
django-nose==1.2
nose==1.3.3
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment