Commit 15add231 by cloud

Merge branch 'master' into szakdoga-br2

parents 1abc2a1f 7b7c0ff4
......@@ -177,14 +177,15 @@ class AclBase(Model):
@classmethod
def get_objects_with_level(cls, level, user,
group_also=True, owner_also=False):
group_also=True, owner_also=False,
disregard_superuser=False):
logger.debug('%s.get_objects_with_level(%s,%s) called',
unicode(cls), unicode(level), unicode(user))
if user is None or not user.is_authenticated():
return cls.objects.none()
if getattr(user, 'is_superuser', False):
if getattr(user, 'is_superuser', False) and not disregard_superuser:
logger.debug('- superuser granted')
return cls.objects
return cls.objects.all()
if isinstance(level, basestring):
level = cls.get_level_object(level)
logger.debug("- level set by str: %s", unicode(level))
......
......@@ -161,6 +161,20 @@ class AclUserTest(TestCase):
self.assertItemsEqual(
TestModel.get_objects_with_level('alfa', self.u2), [i2])
def test_get_objects_with_level_for_superuser(self):
i1 = TestModel.objects.create(normal_field='Hello1')
i2 = TestModel.objects.create(normal_field='Hello2')
i1.set_level(self.u1, 'alfa')
i2.set_level(self.us, 'alfa')
self.assertItemsEqual(
TestModel.get_objects_with_level('alfa', self.u1), [i1])
self.assertItemsEqual(
TestModel.get_objects_with_level('alfa', self.us), [i1, i2])
self.assertItemsEqual(
TestModel.get_objects_with_level('alfa', self.us,
disregard_superuser=True), [i2])
def test_get_objects_with_level_for_group(self):
i1 = TestModel.objects.create(normal_field='Hello1')
i2 = TestModel.objects.create(normal_field='Hello2')
......
......@@ -5,6 +5,7 @@ from os.path import abspath, basename, dirname, join, normpath, isfile
from sys import path
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import ugettext_lazy as _
from json import loads
......@@ -93,7 +94,13 @@ except:
TIME_ZONE = get_env_variable('DJANGO_TIME_ZONE', default=systz)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#language-code
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = get_env_variable("DJANGO_LANGUAGE_CODE", "en")
# https://docs.djangoproject.com/en/dev/ref/settings/#languages
LANGUAGES = (
('en', _('English')),
('hu', _('Hungarian')),
)
# See: https://docs.djangoproject.com/en/dev/ref/settings/#site-id
SITE_ID = 1
......
......@@ -62,8 +62,9 @@ class Operation(object):
For more information, check the synchronous call's documentation.
"""
logger.info("%s called asynchronously with the following parameters: "
"%r", self.__class__.__name__, kwargs)
logger.info("%s called asynchronously on %s with the following "
"parameters: %r", self.__class__.__name__, self.subject,
kwargs)
activity = self.__prelude(kwargs)
return self.async_operation.apply_async(args=(self.id,
self.subject.pk,
......@@ -84,8 +85,9 @@ class Operation(object):
* user: The User invoking the operation. If this argument is not
present, it'll be provided with a default value of None.
"""
logger.info("%s called (synchronously) with the following parameters: "
"%r", self.__class__.__name__, kwargs)
logger.info("%s called (synchronously) on %s with the following "
"parameters: %r", self.__class__.__name__, self.subject,
kwargs)
activity = self.__prelude(kwargs)
return self._exec_op(activity=activity, **kwargs)
......
# -*- coding: utf-8 -*-
from django import contrib
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from dashboard.models import Profile
class ProfileInline(contrib.admin.TabularInline):
model = Profile
UserAdmin.inlines = (ProfileInline, )
contrib.admin.site.unregister(User)
contrib.admin.site.register(User, UserAdmin)
......@@ -5,6 +5,7 @@ from datetime import timedelta
from django.contrib.auth.models import User
from django.contrib.auth.forms import (
AuthenticationForm, PasswordResetForm, SetPasswordForm,
PasswordChangeForm,
)
from crispy_forms.helper import FormHelper
......@@ -1015,9 +1016,20 @@ class MyProfileForm(forms.ModelForm):
def helper(self):
helper = FormHelper()
helper.layout = Layout('preferred_language', )
helper.add_input(Submit("submit", _("Save")))
helper.add_input(Submit("submit", _("Change language")))
return helper
def save(self, *args, **kwargs):
value = super(MyProfileForm, self).save(*args, **kwargs)
return value
class CirclePasswordChangeForm(PasswordChangeForm):
@property
def helper(self):
helper = FormHelper()
helper.add_input(Submit("submit", _("Change password"),
css_class="btn btn-primary",
css_id="submit-password-button"))
return helper
......@@ -187,7 +187,7 @@ html {
height: 300px;
}
#vm-details-rename, #vm-details-rename *, #vm-details-h1-name, #vm-list-rename, #vm-list-rename *,
#vm-details-rename, #vm-details-h1-name, #vm-details-rename ,
#node-details-rename, #node-details-rename *, #node-details-h1-name, #node-list-rename, #node-list-rename *#group-details-rename, #group-details-rename *, #group-details-h1-name, #group-list-rename, #group-list-rename * {
display: inline;
......@@ -197,7 +197,11 @@ html {
display: none;
}
#vm-details-rename-name, #node-details-rename-name, #group-details-rename-name {
.vm-details-home-name {
max-width: 401px;
}
#node-details-rename-name, #group-details-rename-name {
max-width: 160px;
}
......@@ -467,3 +471,23 @@ footer a, footer a:hover, footer a:visited {
overflow: hidden;
padding-left: 10px;
}
#vm-details-home-description {
display: inline-block;
position: relative;
}
#vm-details-home-description textarea {
min-width: 240px;
min-height: 250px;
}
.vm-details-home-edit-description-click, .vm-details-home-edit-name-click {
cursor: pointer;
}
.vm-details-description-submit {
position: absolute;
bottom: 10px;
right: 20px;
}
......@@ -303,7 +303,8 @@ function deleteObject(data) {
// no need to remove them from DOM
$('a[data-disk-pk="' + data.pk + '"]').parent("li").fadeOut();
$('a[data-disk-pk="' + data.pk + '"]').parent("h4").fadeOut();
} else {
}
else {
$('a[data-'+data['type']+'-pk="' + data['pk'] + '"]').closest('tr').fadeOut(function() {
$(this).remove();
});
......
......@@ -31,33 +31,6 @@ $(function() {
return false;
});
/* rename */
$("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide();
$("#vm-details-rename").css('display', 'inline');
$("#vm-details-rename-name").focus();
});
/* rename ajax */
$('#vm-details-rename-submit').click(function() {
var name = $('#vm-details-rename-name').val();
$.ajax({
method: 'POST',
url: location.href,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
$("#vm-details-h1-name").html(data['new_name']).show();
$('#vm-details-rename').hide();
// addMessage(data['message'], "success");
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
/* remove tag */
$('.vm-details-remove-tag').click(function() {
var to_remove = $.trim($(this).parent('div').text());
......@@ -150,8 +123,7 @@ $(function() {
/* add network button */
$("#vm-details-network-add").click(function() {
$("#vm-details-network-add-for-form").html($("#vm-details-network-add-form").html());
$('input[name="new_network_managed"]').tooltip();
$("#vm-details-network-add-form").toggle();
return false;
});
......@@ -165,6 +137,126 @@ $(function() {
$(".vm-details-help-button").click(function() {
$(".vm-details-help").stop().slideToggle();
});
/* for interface remove buttons */
$('.interface-remove').click(function() {
var interface_pk = $(this).data('interface-pk');
addModalConfirmation(removeInterface,
{ 'url': '/dashboard/interface/' + interface_pk + '/delete/',
'data': [],
'pk': interface_pk,
'type': "interface",
});
return false;
});
/* removing interface post */
function removeInterface(data) {
$.ajax({
type: 'POST',
url: data['url'],
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(re, textStatus, xhr) {
/* remove the html element */
$('a[data-interface-pk="' + data.pk + '"]').closest("div").fadeOut();
/* add the removed element to the list */
network_select = $('select[name="new_network_vlan"]');
name_html = (re.removed_network.managed ? "": "") + " " + re.removed_network.vlan;
option_html = '<option value="' + re.removed_network.vlan_pk + '">' + name_html + '</option>';
// if it's -1 then it's a dummy placeholder so we can use .html
if($("option", network_select)[0].value === "-1") {
network_select.html(option_html);
network_select.next("div").children("button").prop("disabled", false);
} else {
network_select.append(option_html);
}
},
error: function(xhr, textStatus, error) {
addMessage('Uh oh :(', 'danger')
}
});
}
/* rename */
$("#vm-details-h1-name, .vm-details-rename-button").click(function() {
$("#vm-details-h1-name").hide();
$("#vm-details-rename").css('display', 'inline');
$("#vm-details-rename-name").focus();
});
/* rename in home tab */
$(".vm-details-home-edit-name-click").click(function() {
$(".vm-details-home-edit-name-click").hide();
$("#vm-details-home-rename").show();
$("input", $("#vm-details-home-rename")).focus();
});
/* rename ajax */
$('.vm-details-rename-submit').click(function() {
var name = $(this).parent("span").prev("input").val();
$.ajax({
method: 'POST',
url: location.href,
data: {'new_name': name},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
$(".vm-details-home-edit-name").text(data['new_name']).show();
$(".vm-details-home-edit-name").parent("div").show();
$(".vm-details-home-edit-name-click").show();
$(".vm-details-home-rename-form-div").hide();
// update the inputs too
$(".vm-details-rename-submit").parent("span").prev("input").val(data['new_name']);
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
/* update description click */
$(".vm-details-home-edit-description-click").click(function() {
$(".vm-details-home-edit-description-click").hide();
$("#vm-details-home-description").show();
return false;
});
/* description update ajax */
$('.vm-details-description-submit').click(function() {
var description = $(this).prev("textarea").val();
$.ajax({
method: 'POST',
url: location.href,
data: {'new_description': description},
headers: {"X-CSRFToken": getCookie('csrftoken')},
success: function(data, textStatus, xhr) {
var new_desc = data['new_description'];
/* we can't simply use $.text, because we need new lines */
var tagsToReplace = {
'&': "&amp;",
'<': "&lt;",
'>': "&gt;",
};
new_desc = new_desc.replace(/[&<>]/g, function(tag) {
return tagsToReplace[tag] || tag;
});
$(".vm-details-home-edit-description")
.html(new_desc.replace(/\n/g, "<br />"));
$(".vm-details-home-edit-description-click").show();
$("#vm-details-home-description").hide();
// update the textareia
$("vm-details-home-description textarea").text(data['new_description']);
},
error: function(xhr, textStatus, error) {
addMessage("Error during renaming!", "danger");
}
});
return false;
});
});
......
......@@ -14,7 +14,20 @@
<h3 class="no-margin"><i class="icon-desktop"></i> {% trans "My profile" %}</h3>
</div>
<div class="panel-body">
{% crispy form %}
<div class="row">
<div class="col-sm-4" style="margin-bottom: 50px;">
<fieldset>
<legend>{% trans "Password change" %}</legend>
{% crispy forms.change_password %}
</fieldset>
</div>
<div class="col-sm-offset-5 col-sm-3">
<fieldset>
<legend>{% trans "Language selection" %}</legend>
{% crispy forms.change_language %}
</fieldset>
</div>
</div>
</div>
</div>
</div>
......
......@@ -18,14 +18,18 @@ btn-xs" type="submit"><i class="icon-chevron-right"></i></button>
</div>
</div>
<h1>
<div id="vm-details-rename">
<div id="vm-details-rename" class="vm-details-home-rename-form-div">
<form action="" method="POST" id="vm-details-rename-form">
{% csrf_token %}
<input id="vm-details-rename-name" class="form-control" name="new_name" type="text" value="{{ instance.name }}"/>
<button type="submit" id="vm-details-rename-submit" class="btn">{% trans "Rename" %}</button>
<div class="input-group vm-details-home-name">
<input id="vm-details-rename-name" class="form-control input-sm" name="new_name" type="text" value="{{ instance.name }}"/>
<span class="input-group-btn">
<button type="submit" class="btn btn-sm vm-details-rename-submit">{% trans "Rename" %}</button>
</span>
</div>
</form>
</div>
<div id="vm-details-h1-name">
<div id="vm-details-h1-name" class="vm-details-home-edit-name">
{{ instance.name }}
</div>
<small>{{ instance.primary_host.get_fqdn }}</small>
......
......@@ -4,8 +4,46 @@
<dl>
<dt>{% trans "System" %}:</dt>
<dd><i class="icon-{{ os_type_icon }}"></i> {{ instance.system }}</dd>
<dt style="margin-top: 5px;">{% trans "Description" %}:</dt>
<dd><small>{{ instance.description }}</small></dd>
<dt style="margin-top: 5px;">
{% trans "Name" %}:
<a href="#" class="vm-details-home-edit-name-click"><i class="icon-pencil"></i></a>
</dt>
<dd>
<div class="vm-details-home-edit-name-click">
<small class="vm-details-home-edit-name">{{ instance.name }}</small>
</div>
<div class="js-hidden vm-details-home-rename-form-div" id="vm-details-home-rename">
<form method="POST">
{% csrf_token %}
<div class="input-group">
<input type="text" name="new_name" value="{{ instance.name }}" class="form-control input-sm"/>
<span class="input-group-btn">
<button type="submit" class="btn btn-success btn-sm vm-details-rename-submit">
<i class="icon-pencil"></i> {% trans "Rename" %}
</button>
</span>
</div>
</form>
</div>
</dd>
<dt style="margin-top: 5px;">
{% trans "Description" %}:
<a href="#" class="vm-details-home-edit-description-click"><i class="icon-pencil"></i></a>
</dt>
<dd>
{% csrf_token %}
<div class="vm-details-home-edit-description-click">
<small class="vm-details-home-edit-description">{{ instance.description|linebreaks }}</small>
</div>
<div id="vm-details-home-description" class="js-hidden">
<form method="POST">
<textarea name="new_description" class="form-control">{{ instance.description }}</textarea>
<button type="submit" class="btn btn-xs btn-success vm-details-description-submit">
<i class="icon-pencil"></i> {% trans "Update" %}
</button>
</form>
</div>
</dd>
</dl>
<h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="icon-warning-sign text-danger"></i>{% endif %}
......
......@@ -6,13 +6,50 @@
{% trans "Interfaces" %}
</h2>
<div class="row" id="vm-details-network-add-for-form">
<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="icon-plus-sign"></i></button>
</div>
</div>
</form>
<hr />
</div>
</div>
</div>
{% for i in instance.interface_set.all %}
<div>
<h3 class="list-group-item-heading dashboard-vm-details-network-h3">
<i class="icon-{% if i.host %}globe{% else %}link{% endif %}"></i> {{ i.vlan.name }}
{% if not i.host %}(unmanaged) <a href="#" class="btn btn-danger btn-xs">{% trans "remove" %}</a>{% endif %}
{% if not i.host%}({% trans "unmanaged" %}){% endif %}
<a href="{% url "dashboard.views.interface-delete" pk=i.pk %}?next={{ request.path }}" class="btn btn-danger btn-xs interface-remove"
data-interface-pk="{{ i.pk }}">
{% trans "remove" %}
</a>
</h3>
{% if i.host %}
<div class="row">
......@@ -109,31 +146,5 @@
</div>
</div>
{% endif %}
{% endfor %}
<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">
{% if vlans|length == 0 %}
<option value="-1">No more networks!</option>
{% endif %}
{% for v in vlans %}
<option value="{{ v.pk }}">{{ v.name }}</option>
{% endfor %}
</select>
<div class="input-group-btn">
<button type="submit" class="btn btn-success"><i class="icon-plus-sign"></i></button>
</div>
</div>
</form>
<hr />
</div>
</div>
</div>
{% endfor %}
import json
from unittest import skip
from django.test import TestCase
from django.test.client import Client
......@@ -5,6 +7,7 @@ from django.contrib.auth.models import User, Group
from django.core.exceptions import SuspiciousOperation
from django.core.urlresolvers import reverse
from django.contrib.auth.models import Permission
from django.contrib.auth import authenticate
from vm.models import Instance, InstanceTemplate, Lease, Node, Trait
from vm.operations import WakeUpOperation
......@@ -173,6 +176,46 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(response.status_code, 302)
self.assertEqual(inst.interface_set.count(), interface_count + 1)
def test_permitted_network_delete(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
inst.add_interface(vlan=Vlan.objects.get(pk=1), user=self.us)
iface_count = inst.interface_set.count()
c.post("/dashboard/interface/1/delete/")
self.assertEqual(inst.interface_set.count(), iface_count - 1)
def test_permitted_network_delete_w_ajax(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
vlan = Vlan.objects.get(pk=1)
inst.add_interface(vlan=vlan, user=self.us)
iface_count = inst.interface_set.count()
response = c.post("/dashboard/interface/1/delete/",
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
removed_network = json.loads(response.content)['removed_network']
self.assertEqual(removed_network['vlan'], vlan.name)
self.assertEqual(removed_network['vlan_pk'], vlan.pk)
self.assertEqual(removed_network['managed'], vlan.managed)
self.assertEqual(inst.interface_set.count(), iface_count - 1)
def test_unpermitted_network_delete(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'user')
inst.add_interface(vlan=Vlan.objects.get(pk=1), user=self.us)
iface_count = inst.interface_set.count()
response = c.post("/dashboard/interface/1/delete/")
self.assertEqual(iface_count, inst.interface_set.count())
self.assertEqual(response.status_code, 403)
def test_create_vm_w_unpermitted_network(self):
c = Client()
self.login(c, 'user2')
......@@ -558,6 +601,41 @@ class VmDetailTest(LoginMixin, TestCase):
self.assertEqual(response.status_code, 302)
self.assertEqual(instance_count + 2, Instance.objects.all().count())
def test_unpermitted_description_update(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
inst.set_level(self.u1, 'user')
old_desc = inst.description
response = c.post("/dashboard/vm/1/", {'new_description': 'test1234'})
self.assertEqual(response.status_code, 403)
self.assertEqual(Instance.objects.get(pk=1).description, old_desc)
def test_permitted_description_update_w_ajax(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
response = c.post("/dashboard/vm/1/", {'new_description': "naonyo"},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content)['new_description'],
"naonyo")
self.assertEqual(Instance.objects.get(pk=1).description, "naonyo")
def test_permitted_description_update(self):
c = Client()
self.login(c, "user1")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner')
response = c.post("/dashboard/vm/1/", {'new_description': "naonyo"})
self.assertEqual(response.status_code, 302)
self.assertEqual(Instance.objects.get(pk=1).description, "naonyo")
class NodeDetailTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json']
......@@ -991,3 +1069,68 @@ class IndexViewTest(LoginMixin, TestCase):
self.u1.profile.notify("urgent", "dashboard/test_message.txt", )
response = c.get("/dashboard/")
self.assertEqual(response.context['NEW_NOTIFICATIONS_COUNT'], 1)
class ProfileViewTest(LoginMixin, TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
self.p1 = Profile.objects.create(user=self.u1)
self.p1.save()
def test_permitted_language_change(self):
c = Client()
self.login(c, "user1")
old_language_cookie_value = c.cookies['django_language'].value
old_language_db_value = self.u1.profile.preferred_language
response = c.post("/dashboard/profile/", {
'preferred_language': "hu",
})
self.assertEqual(response.status_code, 302)
self.assertNotEqual(old_language_cookie_value,
c.cookies['django_language'].value)
self.assertNotEqual(old_language_db_value,
User.objects.get(
username="user1").profile.preferred_language)
def test_permitted_valid_password_change(self):
c = Client()
self.login(c, "user1")
c.post("/dashboard/profile/", {
'old_password': "password",
'new_password1': "asd",
'new_password2': "asd",
})
self.assertIsNone(authenticate(username="user1", password="password"))
self.assertIsNotNone(authenticate(username="user1", password="asd"))
def test_permitted_invalid_password_changes(self):
c = Client()
self.login(c, "user1")
# wrong current password
c.post("/dashboard/profile/", {
'old_password': "password1",
'new_password1': "asd",
'new_password2': "asd",
})
self.assertIsNotNone(authenticate(username="user1",
password="password"))
self.assertIsNone(authenticate(username="user1", password="asd"))
# wrong pw confirmation
c.post("/dashboard/profile/", {
'old_password': "password",
'new_password1': "asd",
'new_password2': "asd1",
})
self.assertIsNotNone(authenticate(username="user1",
password="password"))
self.assertIsNone(authenticate(username="user1", password="asd"))
......@@ -12,7 +12,7 @@ from .views import (
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmRenewView, DiskRemoveView, get_disk_download_status,
VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
)
urlpatterns = patterns(
......@@ -108,6 +108,9 @@ urlpatterns = patterns(
url(r'^disk/(?P<pk>\d+)/status/$', get_disk_download_status,
name="dashboard.views.disk-status"),
url(r'^interface/(?P<pk>\d+)/delete/$', InterfaceDeleteView.as_view(),
name="dashboard.views.interface-delete"),
url(r'^profile/$', MyPreferencesView.as_view(),
name="dashboard.views.profile"),
)
......@@ -37,6 +37,7 @@ from braces.views import (
from .forms import (
CircleAuthenticationForm, DiskAddForm, HostForm, LeaseForm, MyProfileForm,
NodeForm, TemplateForm, TraitForm, VmCustomizeForm,
CirclePasswordChangeForm
)
from .tables import (NodeListTable, NodeVmListTable,
TemplateListTable, LeaseListTable, GroupListTable,)
......@@ -89,7 +90,7 @@ class IndexView(LoginRequiredMixin, TemplateView):
# instances
favs = Instance.objects.filter(favourite__user=self.request.user)
instances = Instance.get_objects_with_level(
'user', user).filter(destroyed_at=None)
'user', user, disregard_superuser=True).filter(destroyed_at=None)
display = list(favs) + list(set(instances) - set(favs))
for d in display:
d.fav = True if d in favs else False
......@@ -215,7 +216,7 @@ class VmDetailView(CheckedDetailView):
context['vlans'] = Vlan.get_objects_with_level(
'user', self.request.user
).exclude(
).exclude( # exclude already added interfaces
pk__in=Interface.objects.filter(
instance=self.get_object()).values_list("vlan", flat=True)
).all()
......@@ -238,6 +239,7 @@ class VmDetailView(CheckedDetailView):
options = {
'change_password': self.__change_password,
'new_name': self.__set_name,
'new_description': self.__set_description,
'new_tag': self.__add_tag,
'deploy_local': self.__deploy_local,
'to_remove': self.__remove_tag,
......@@ -318,8 +320,30 @@ class VmDetailView(CheckedDetailView):
)
else:
messages.success(request, success_message)
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk}))
return redirect(self.object.get_absolute_url())
def __set_description(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
new_description = request.POST.get("new_description")
Instance.objects.filter(pk=self.object.pk).update(
**{'description': new_description})
success_message = _("VM description successfully updated!")
if request.is_ajax():
response = {
'message': success_message,
'new_description': new_description,
}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(self.object.get_absolute_url())
def __add_tag(self, request):
new_tag = request.POST.get('new_tag')
......@@ -402,12 +426,11 @@ class VmDetailView(CheckedDetailView):
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
vlan = Vlan.objects.get(pk=request.POST.get("new_network_vlan"))
vlan = get_object_or_404(Vlan, pk=request.POST.get("new_network_vlan"))
if not vlan.has_level(request.user, 'user'):
raise PermissionDenied()
try:
Interface.create(vlan=vlan, instance=self.object,
managed=vlan.managed, owner=request.user)
self.object.add_interface(vlan=vlan, user=request.user)
messages.success(request, _("Successfully added new interface!"))
except Exception, e:
error = u' '.join(e.messages)
......@@ -2135,9 +2158,17 @@ class DiskAddView(TemplateView):
class MyPreferencesView(UpdateView):
model = Profile
form_class = MyProfileForm
def get_context_data(self, *args, **kwargs):
context = super(MyPreferencesView, self).get_context_data(*args,
**kwargs)
context['forms'] = {
'change_password': CirclePasswordChangeForm(
user=self.request.user),
'change_language': MyProfileForm(instance=self.get_object()),
}
return context
def get_object(self, queryset=None):
if self.request.user.is_anonymous():
......@@ -2147,10 +2178,36 @@ class MyPreferencesView(UpdateView):
except Profile.DoesNotExist:
raise Http404(_("You don't have a profile."))
def form_valid(self, form):
response = super(MyPreferencesView, self).form_valid(form)
set_language_cookie(self.request, response)
return response
def post(self, request, *args, **kwargs):
self.ojbect = self.get_object()
redirect_response = HttpResponseRedirect(
reverse("dashboard.views.profile"))
if "preferred_language" in request.POST:
form = MyProfileForm(request.POST, instance=self.get_object())
if form.is_valid():
lang = form.cleaned_data.get("preferred_language")
set_language_cookie(self.request, redirect_response, lang)
form.save()
else:
form = CirclePasswordChangeForm(user=request.user,
data=request.POST)
if form.is_valid():
form.save()
if form.is_valid():
return redirect_response
else:
return self.get(request, form=form, *args, **kwargs)
def get(self, request, form=None, *args, **kwargs):
# if this is not here, it won't work
self.object = self.get_object()
context = self.get_context_data(*args, **kwargs)
if form is not None:
# a little cheating, users can't post invalid
# language selection forms (without modifying the HTML)
context['forms']['change_password'] = form
return self.render_to_response(context)
def set_language_cookie(request, response, lang=None):
......@@ -2236,3 +2293,53 @@ class InstanceActivityDetail(SuperuserRequiredMixin, DetailView):
order_by('-started').select_related('user').
prefetch_related('children'))
return ctx
class InterfaceDeleteView(DeleteView):
model = Interface
def get_template_names(self):
if self.request.is_ajax():
return ['dashboard/confirm/ajax-delete.html']
else:
return ['dashboard/confirm/base-delete.html']
def get_context_data(self, **kwargs):
context = super(InterfaceDeleteView, self).get_context_data(**kwargs)
interface = self.get_object()
context['text'] = _("Are you sure you want to remove this interface "
"from <strong>%(vm)s</strong>?" %
{'vm': interface.instance.name})
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
instance = self.object.instance
if not instance.has_level(request.user, "owner"):
raise PermissionDenied()
instance.remove_interface(interface=self.object, user=request.user)
success_url = self.get_success_url()
success_message = _("Interface successfully deleted!")
if request.is_ajax():
return HttpResponse(
json.dumps(
{'message': success_message,
'removed_network': {
'vlan': self.object.vlan.name,
'vlan_pk': self.object.vlan.pk,
'managed': self.object.host is not None,
}}),
content_type="application/json",
)
else:
messages.success(request, success_message)
return HttpResponseRedirect("%s#network" % success_url)
def get_success_url(self):
redirect = self.request.POST.get("next")
if redirect:
return redirect
self.object.instance.get_absolute_url()
......@@ -2,7 +2,7 @@ import re
import logging
from collections import OrderedDict
from netaddr import IPAddress, AddrFormatError
from datetime import datetime, timedelta
from datetime import timedelta
from itertools import product
from .models import (Host, Rule, Vlan, Domain, Record, BlacklistItem,
......@@ -11,6 +11,7 @@ from .iptables import IptRule, IptChain
import django.conf
from django.db.models import Q
from django.template import loader, Context
from django.utils import timezone
settings = django.conf.settings.FIREWALL_SETTINGS
......@@ -134,7 +135,7 @@ class BuildFirewall:
def ipset():
week = datetime.now() - timedelta(days=2)
week = timezone.now() - timedelta(days=2)
filter_ban = (Q(type='tempban', modified_at__gte=week) |
Q(type='permban'))
return BlacklistItem.objects.filter(filter_ban).values('ipv4', 'reason')
......
......@@ -52,6 +52,8 @@
<ul class="nav navbar-nav">
{% include "network/menu.html" %}
</ul>
<a class="navbar-brand pull-right" href="{% url "dashboard.index" %}"
style="color: white; font-size: 10px;"><i class="icon-dashboard"></i> {% trans "dashboard" %}</a>
</div><!-- .collapse .navbar-collapse -->
</div><!-- navbar navbar-inverse navbar-fixed-top -->
<div class="container">
......@@ -75,7 +77,7 @@
<div class="footer-container container">
<footer>
<p class="pull-right"><a href="#">Vissza az oldal tetejére</a></p>
<p>&copy; 2013 BME Közigazgatási Informatikai Központ
<p>&copy; {{ COMPANY_NAME }}
</footer>
</div><!-- .footer-container .container -->
......
......@@ -46,18 +46,13 @@ def restore(disk, user):
disk.restore(task_uuid=restore.request.id, user=user)
class CreateFromURLTask(AbortableTask):
def __init__(self):
self.bind(celery)
def run(self, **kwargs):
Disk = kwargs.pop('cls')
Disk.create_from_url(url=kwargs.pop('url'),
task_uuid=create_from_url.request.id,
abortable_task=self,
**kwargs)
create_from_url = CreateFromURLTask()
@celery.task(base=AbortableTask, bind=True)
def create_from_url(self, **kwargs):
Disk = kwargs.pop('cls')
Disk.create_from_url(url=kwargs.pop('url'),
task_uuid=self.request.id,
abortable_task=self,
**kwargs)
@celery.task
......
......@@ -53,15 +53,12 @@ class Interface(Model):
class Meta:
app_label = 'vm'
db_table = 'vm_interface'
ordering = ("-vlan__managed", )
def __unicode__(self):
return 'cloud-' + str(self.instance.id) + '-' + str(self.vlan.vid)
@property
def destroyed(self):
return self.instance.destroyed_at
@property
def mac(self):
try:
return self.host.mac
......@@ -138,34 +135,16 @@ class Interface(Model):
return iface
def deploy(self):
if self.destroyed:
from .instance import Instance
raise Instance.InstanceDestroyedError(self.instance,
"The associated instance "
"(%s) has already been "
"destroyed" % self.instance)
net_tasks.create.apply_async(
args=[self.get_vmnetwork_desc()],
queue=self.instance.get_remote_queue_name('net'))
queue_name = self.instance.get_remote_queue_name('net')
return net_tasks.create.apply_async(args=[self.get_vmnetwork_desc()],
queue=queue_name).get()
def shutdown(self):
if self.destroyed:
from .instance import Instance
raise Instance.InstanceDestroyedError(self.instance,
"The associated instance "
"(%s) has already been "
"destroyed" % self.instance)
queue_name = self.instance.get_remote_queue_name('net')
net_tasks.destroy.apply_async(args=[self.get_vmnetwork_desc()],
queue=queue_name)
return net_tasks.destroy.apply_async(args=[self.get_vmnetwork_desc()],
queue=queue_name).get()
def destroy(self):
if self.destroyed:
return
self.shutdown()
if self.host is not None:
self.host.delete()
......
......@@ -11,7 +11,8 @@ from celery.exceptions import TimeLimitExceeded
from common.operations import Operation, register_operation
from .tasks.local_tasks import async_instance_operation, async_node_operation
from .models import (
Instance, InstanceActivity, InstanceTemplate, Node, NodeActivity,
Instance, InstanceActivity, InstanceTemplate, Interface, Node,
NodeActivity,
)
......@@ -56,12 +57,34 @@ class InstanceOperation(Operation):
user=user)
class AddInterfaceOperation(InstanceOperation):
activity_code_suffix = 'add_interface'
id = 'add_interface'
name = _("add interface")
description = _("Add a new network interface for the specified VLAN to "
"the VM.")
def _operation(self, activity, user, system, vlan, managed=None):
if managed is None:
managed = vlan.managed
net = Interface.create(base_activity=activity, instance=self.instance,
managed=managed, owner=user, vlan=vlan)
if self.instance.is_running:
net.deploy()
return net
register_operation(AddInterfaceOperation)
class DeployOperation(InstanceOperation):
activity_code_suffix = 'deploy'
id = 'deploy'
name = _("deploy")
description = _("Deploy new virtual machine with network.")
icon = 'play'
def on_commit(self, activity):
activity.resultant_state = 'RUNNING'
......@@ -98,7 +121,6 @@ class DestroyOperation(InstanceOperation):
id = 'destroy'
name = _("destroy")
description = _("Destroy virtual machine and its networks.")
icon = 'remove'
def on_commit(self, activity):
activity.resultant_state = 'DESTROYED'
......@@ -107,6 +129,7 @@ class DestroyOperation(InstanceOperation):
if self.instance.node:
# Destroy networks
with activity.sub_activity('destroying_net'):
self.instance.shutdown_net()
self.instance.destroy_net()
# Delete virtual machine
......@@ -139,7 +162,6 @@ class MigrateOperation(InstanceOperation):
id = 'migrate'
name = _("migrate")
description = _("Live migrate running VM to another node.")
icon = 'truck'
def _operation(self, activity, user, system, to_node=None, timeout=120):
if not to_node:
......@@ -170,7 +192,6 @@ class RebootOperation(InstanceOperation):
id = 'reboot'
name = _("reboot")
description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
icon = 'refresh'
def _operation(self, activity, user, system, timeout=5):
self.instance.reboot_vm(timeout=timeout)
......@@ -179,12 +200,28 @@ class RebootOperation(InstanceOperation):
register_operation(RebootOperation)
class RemoveInterfaceOperation(InstanceOperation):
activity_code_suffix = 'remove_interface'
id = 'remove_interface'
name = _("remove interface")
description = _("Remove the specified network interface from the VM.")
def _operation(self, activity, user, system, interface):
if self.instance.is_running:
interface.shutdown()
interface.destroy()
interface.delete()
register_operation(RemoveInterfaceOperation)
class ResetOperation(InstanceOperation):
activity_code_suffix = 'reset'
id = 'reset'
name = _("reset")
description = _("Reset virtual machine (reset button).")
icon = 'bolt'
def _operation(self, activity, user, system, timeout=5):
self.instance.reset_vm(timeout=timeout)
......@@ -201,7 +238,6 @@ class SaveAsTemplateOperation(InstanceOperation):
Template can be shared with groups and users.
Users can instantiate Virtual Machines from Templates.
""")
icon = 'save'
@staticmethod
def _rename(name):
......@@ -277,7 +313,6 @@ class ShutdownOperation(InstanceOperation):
id = 'shutdown'
name = _("shutdown")
description = _("Shutdown virtual machine with ACPI signal.")
icon = 'off'
def check_precond(self):
super(ShutdownOperation, self).check_precond()
......@@ -307,7 +342,6 @@ class ShutOffOperation(InstanceOperation):
id = 'shut_off'
name = _("shut off")
description = _("Shut off VM (plug-out).")
icon = 'ban-circle'
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
......@@ -334,7 +368,6 @@ class SleepOperation(InstanceOperation):
id = 'sleep'
name = _("sleep")
description = _("Suspend virtual machine with memory dump.")
icon = 'moon'
def check_precond(self):
super(SleepOperation, self).check_precond()
......@@ -374,7 +407,6 @@ class WakeUpOperation(InstanceOperation):
Power on Virtual Machine and load its memory from dump.
""")
icon = 'sun'
def check_precond(self):
super(WakeUpOperation, self).check_precond()
......
from datetime import datetime
from mock import Mock, MagicMock, patch, call
import types
from django.contrib.auth.models import User
from django.test import TestCase
......@@ -180,14 +181,18 @@ class InstanceActivityTestCase(TestCase):
def test_create_no_concurrency_check(self):
instance = MagicMock(spec=Instance)
instance.activity_log.filter.return_value.exists.return_value = True
with patch.object(InstanceActivity, '__new__'):
try:
InstanceActivity.create('test', instance,
concurrency_check=False)
except ActivityInProgressError:
raise AssertionError("'create' method checked for concurrent "
"activities.")
mock_instance_activity_cls = MagicMock(spec=InstanceActivity,
ACTIVITY_CODE_BASE='test')
original_create = InstanceActivity.create
mocked_create = types.MethodType(original_create.im_func,
mock_instance_activity_cls,
original_create.im_class)
try:
mocked_create('test', instance, concurrency_check=False)
except ActivityInProgressError:
raise AssertionError("'create' method checked for concurrent "
"activities.")
def test_create_sub_concurrency_check(self):
iaobj = MagicMock(spec=InstanceActivity)
......@@ -201,10 +206,10 @@ class InstanceActivityTestCase(TestCase):
iaobj.activity_code = 'test'
iaobj.children.filter.return_value.exists.return_value = True
with patch.object(InstanceActivity, '__new__'):
create_sub_func = InstanceActivity.create_sub
with patch('vm.models.activity.InstanceActivity'):
try:
InstanceActivity.create_sub(iaobj, 'test',
concurrency_check=False)
create_sub_func(iaobj, 'test', concurrency_check=False)
except ActivityInProgressError:
raise AssertionError("'create_sub' method checked for "
"concurrent activities.")
......
Django==1.5.2
Django==1.5.6
bpython==0.12
celery==3.0.23
celery==3.1.11
django-braces==1.2.2
django-celery==3.0.23
django-celery==3.1.10
django-crispy-forms==1.4.0
django-model-utils==1.4.0
django-sizefield==0.4
......
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