Commit 6658f290 by Őry Máté

Merge branch 'feature-renew-op-rebased' into 'master'

Feature Renew Op

🆗 refactor renew to an operation
🆗 token+operation
🆗 view
🆗 Lease acl views @kviktor
parents a245f33d 31295e76
......@@ -59,10 +59,10 @@ def create_levels(app, created_models, verbosity, db=DEFAULT_DB_ALIAS,
]
Level.objects.using(db).bulk_create(levels)
if verbosity >= 2:
print("Adding levels [%s]." % ", ".join(levels))
print("Adding levels [%s]." % ", ".join(unicode(l) for l in levels))
print("Searched: [%s]." % ", ".join(
[unicode(l) for l in searched_levels]))
print("All: [%s]." % ", ".join([unicode(l) for l in all_levels]))
unicode(l) for l in searched_levels))
print("All: [%s]." % ", ".join(unicode(l) for l in all_levels))
# set weights
for ctype, codename, weight in level_weights:
......
......@@ -46,8 +46,9 @@ CACHES = {
LOGGING['loggers']['djangosaml2'] = {'handlers': ['console'],
'level': 'CRITICAL'}
LOGGING['handlers']['console'] = {'level': 'WARNING',
level = environ.get('LOGLEVEL', 'CRITICAL')
LOGGING['handlers']['console'] = {'level': level,
'class': 'logging.StreamHandler',
'formatter': 'simple'}
for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': 'CRITICAL'}
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': level}
......@@ -59,6 +59,8 @@ class Operation(object):
skip_auth_check = auxargs.pop('system')
user = auxargs.pop('user')
parent_activity = auxargs.pop('parent_activity')
if parent_activity and user is None and not skip_auth_check:
user = parent_activity.user
# check for unexpected keyword arguments
argspec = getargspec(self._operation)
......
......@@ -612,6 +612,9 @@ class TemplateForm(forms.ModelForm):
self.instance.ram_size = 512
self.instance.num_cores = 2
self.fields["lease"].queryset = Lease.get_objects_with_level(
"operator", self.user)
def clean_owner(self):
if self.instance.pk is not None:
return User.objects.get(pk=self.instance.owner.pk)
......@@ -888,6 +891,27 @@ class LeaseForm(forms.ModelForm):
model = Lease
class VmRenewForm(forms.Form):
def __init__(self, *args, **kwargs):
choices = kwargs.pop('choices')
default = kwargs.pop('default')
super(VmRenewForm, self).__init__(*args, **kwargs)
self.fields['lease'] = forms.ModelChoiceField(queryset=choices,
initial=default,
required=True,
label=_('Length'))
if len(choices) < 2:
self.fields['lease'].widget = HiddenInput()
@property
def helper(self):
helper = FormHelper(self)
helper.form_tag = False
return helper
class VmCreateDiskForm(forms.Form):
name = forms.CharField(max_length=100, label=_("Name"))
size = forms.CharField(
......
......@@ -6,7 +6,7 @@
{% block content %}
<div class="row">
<div class="col-md-12">
<div class="col-md-7">
<div class="panel panel-default">
<div class="panel-heading">
<a class="pull-right btn btn-default btn-xs" href="{% url "dashboard.views.template-list" %}">{% trans "Back" %}</a>
......@@ -20,6 +20,85 @@
</div>
</div>
</div>
<div class="col-md-5">
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="no-margin"><i class="icon-group"></i> {% trans "Manage access" %}</h4>
</div>
<div class="panel-body">
<form action="{% url "dashboard.views.lease-acl" pk=object.pk %}" method="post">{% csrf_token %}
<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="icon-remove"></i></th>
</tr>
</thead>
<tbody>
{% for i in acl.users %}
<tr>
<td>
<i class="icon-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="icon-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="icon-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 %}
......@@ -26,9 +26,11 @@
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
{% if perms.vm.create_leases %}
<a href="{% url "dashboard.views.lease-create" %}" class="pull-right btn btn-success btn-xs" style="margin-right: 10px;">
<i class="icon-plus"></i> {% trans "new lease" %}
</a>
{% endif %}
<h3 class="no-margin"><i class="icon-time"></i> {% trans "Leases" %}</h3>
</div>
<div class="panel-body">
......
......@@ -47,7 +47,12 @@
</dl>
<h4>{% trans "Expiration" %} {% if instance.is_expiring %}<i class="icon-warning-sign text-danger"></i>{% endif %}
<a href="{% url "dashboard.views.vm-renew" instance.pk "" %}" class="btn btn-success btn-xs pull-right">{% trans "renew" %}</a>
{% with op=op.renew %}
<a href="{{op.get_url}}" class="btn btn-success btn-xs
operation operation-{{op.op}} btn btn-default">
<i class="icon-{{op.icon}}"></i>
{{op.name}} </a>
{% endwith %}
</h4>
<dl>
<dt>{% trans "Suspended at:" %}</dt>
......
......@@ -16,13 +16,15 @@
# with CIRCLE. If not, see <http://www.gnu.org/licenses/>.
import unittest
import warnings
from factory import Factory, Sequence
from mock import patch, MagicMock
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.core.signing import TimestampSigner, JSONSerializer, b64_encode
from django.http import HttpRequest, Http404
from django.http import HttpRequest, Http404, QueryDict
from django.utils import baseconv
from ..models import Profile
......@@ -142,7 +144,7 @@ class VmOperationViewTestCase(unittest.TestCase):
view.as_view()(request, pk=1234).render()
def test_migrate(self):
request = FakeRequestFactory(POST={'node': 1})
request = FakeRequestFactory(POST={'node': 1}, superuser=True)
view = vm_ops['migrate']
with patch.object(view, 'get_object') as go, \
......@@ -176,7 +178,24 @@ class VmOperationViewTestCase(unittest.TestCase):
assert view.as_view()(request, pk=1234)['location']
assert msg.error.called
def test_migrate_wo_permission(self):
request = FakeRequestFactory(POST={'node': 1}, superuser=False)
view = vm_ops['migrate']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.has_level.return_value = True
go.return_value = inst
go4.return_value = MagicMock()
with self.assertRaises(PermissionDenied):
assert view.as_view()(request, pk=1234)['location']
def test_migrate_template(self):
"""check if GET dialog's template can be rendered"""
request = FakeRequestFactory(superuser=True)
view = vm_ops['migrate']
......@@ -207,7 +226,8 @@ class VmOperationViewTestCase(unittest.TestCase):
assert not msg.error.called
def test_save_as_w_name(self):
request = FakeRequestFactory(POST={'name': 'foobar'})
request = FakeRequestFactory(POST={'name': 'foobar'},
has_perms_mock=True)
view = vm_ops['save_as_template']
with patch.object(view, 'get_object') as go, \
......@@ -238,26 +258,204 @@ class VmOperationViewTestCase(unittest.TestCase):
self.assertEquals(rend.status_code, 200)
def FakeRequestFactory(*args, **kwargs):
class RenewViewTest(unittest.TestCase):
def test_renew_template(self):
request = FakeRequestFactory(has_perms_mock=True)
view = vm_ops['renew']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.name = 'foo'
inst.renew = Instance._ops['renew'](inst)
inst.has_level.return_value = True
go.return_value = inst
rend = view.as_view()(request, pk=1234).render()
self.assertEquals(rend.status_code, 200)
def test_renew_by_owner_wo_param(self):
request = FakeRequestFactory(POST={}, has_perms_mock=True)
view = vm_ops['renew']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock()
inst.has_level.return_value = True
go.return_value = inst
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234).render().status_code == 200
# success would redirect
def test_renew_by_owner_w_param(self):
request = FakeRequestFactory(POST={'length': 1}, has_perms_mock=True)
view = vm_ops['renew']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock()
inst.has_level.return_value = True
go.return_value = inst
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)
assert not msg.error.called
def test_renew_get_by_anon_wo_key(self):
request = FakeRequestFactory(authenticated=False)
view = vm_ops['renew']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock()
inst.has_level.return_value = False
go.return_value = inst
go4.return_value = MagicMock()
self.assertIn('login',
view.as_view()(request, pk=1234)['location'])
def test_renew_get_by_nonowner_wo_key(self):
request = FakeRequestFactory(has_perms_mock=True)
view = vm_ops['renew']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock()
inst.has_level.return_value = False
go.return_value = inst
go4.return_value = MagicMock()
with self.assertRaises(PermissionDenied):
assert view.as_view()(request, pk=1234)
def test_renew_post_by_nonowner_wo_key(self):
request = FakeRequestFactory(POST={'length': 1}, has_perms_mock=True)
view = vm_ops['renew']
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance, pk=11)
inst._meta.object_name = "Instance"
inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock()
inst.has_level.return_value = False
go.return_value = inst
go4.return_value = MagicMock()
with self.assertRaises(PermissionDenied):
assert view.as_view()(request, pk=1234)
def test_renew_get_by_nonowner_w_key(self):
user = FakeRequestFactory(superuser=True).user
view = vm_ops['renew']
inst = MagicMock(spec=Instance, pk=11)
inst._meta.object_name = "Instance"
inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock()
key = view.get_token_url(inst, user).split('?')[1].split('=')[1]
request = FakeRequestFactory(GET={'k': key}) # other user!
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.User.objects') as gu, \
patch('dashboard.views.Lease.get_objects_with_level') as gol:
gol.return_value = views.Lease.objects.all()
gu.get.return_value = user
go.return_value = inst
assert view.as_view()(request, pk=1234).render().status_code == 200
def test_renew_post_by_anon_w_key(self):
user = FakeRequestFactory(authenticated=True).user
view = vm_ops['renew']
inst = MagicMock(spec=Instance, pk=11)
inst._meta.object_name = "Instance"
inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock()
inst.has_level = lambda user, level: user.is_authenticated()
key = view.get_token_url(inst, user).split('?')[1].split('=')[1]
request = FakeRequestFactory(GET={'k': key}, authenticated=False)
with patch.object(view, 'get_object') as go, \
patch('dashboard.views.Lease.get_objects_with_level') as gol:
go.return_value = inst
gol.return_value = views.Lease.objects.all()
assert view.as_view()(request, pk=1234).render().status_code == 200
def test_renew_post_by_anon_w_invalid_key(self):
view = vm_ops['renew']
key = "invalid"
inst = MagicMock(spec=Instance, pk=11)
inst._meta.object_name = "Instance"
inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock()
inst.has_level.return_value = False
request = FakeRequestFactory(GET={'k': key}, authenticated=False)
with patch.object(view, 'get_object') as go:
go.return_value = inst
self.assertIn('login',
view.as_view()(request, pk=1234)['location'])
def test_renew_post_by_anon_w_expired_key(self):
def side(max_age=None, *args, **kwargs):
if max_age:
raise views.signing.BadSignature
user = FakeRequestFactory(authenticated=False).user
view = vm_ops['renew']
inst = MagicMock(spec=Instance, pk=11)
inst._meta.object_name = "Instance"
inst.renew = Instance._ops['renew'](inst)
inst.renew.async = MagicMock()
inst.has_level.return_value = False
key = view.get_token_url(inst, user).split('?')[1].split('=')[1]
with patch('dashboard.views.signing.loads') as loader, \
patch.object(view, 'get_object') as go:
loader.return_value = (inst.pk, user.pk)
loader.side_effect = side
request = FakeRequestFactory(GET={'k': key}, user=user)
go.return_value = inst
self.assertIn('login',
view.as_view()(request, pk=1234)['location'])
def FakeRequestFactory(user=None, **kwargs):
''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for
mocking out django views; they are MUCH faster than the Django test client.
'''
if user is None:
user = UserFactory()
user.is_authenticated = lambda: kwargs.get('authenticated', True)
user.is_superuser = kwargs.get('superuser', False)
if kwargs.get('has_perms_mock', False):
user.is_authenticated = lambda: kwargs.pop('authenticated', True)
user.is_superuser = kwargs.pop('superuser', False)
if kwargs.pop('has_perms_mock', False):
user.has_perms = MagicMock(return_value=True)
user.save()
request = HttpRequest()
request.user = user
request.session = kwargs.get('session', {})
request.session = kwargs.pop('session', {})
if kwargs.get('POST') is not None:
request.method = 'POST'
request.POST = kwargs.get('POST')
request.POST = QueryDict('', mutable=True)
request.POST.update(kwargs.pop('POST'))
else:
request.method = 'GET'
request.GET = kwargs.get('GET', {})
request.GET = QueryDict('', mutable=True)
request.GET.update(kwargs.pop('GET', {}))
if len(kwargs):
warnings.warn("FakeRequestFactory kwargs unused: " + unicode(kwargs))
return request
......
......@@ -21,14 +21,12 @@ import json
from django.test import TestCase
from django.test.client import Client
from django.contrib.auth.models import User, Group
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
from ..models import Profile
from ..views import VmRenewView
from storage.models import Disk
from firewall.models import Vlan, Host, VlanGroup
from mock import Mock, patch
......@@ -568,10 +566,8 @@ class VmDetailTest(LoginMixin, TestCase):
inst = Instance.objects.get(pk=1)
inst.manual_state_change('SUSPENDED')
inst.set_level(self.u2, 'user')
with patch('dashboard.views.messages') as msg:
response = c.post("/dashboard/vm/1/op/wake_up/")
assert msg.error.called
self.assertEqual(response.status_code, 302)
self.assertEqual(response.status_code, 403)
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'SUSPENDED')
......@@ -1631,100 +1627,6 @@ class TransferOwnershipViewTest(LoginMixin, TestCase):
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk)
class RenewViewTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json']
def setUp(self):
self.u1 = User.objects.create(username='user1')
self.u1.set_password('password')
self.u1.save()
Profile.objects.create(user=self.u1)
self.u2 = User.objects.create(username='user2', is_staff=True)
self.u2.set_password('password')
self.u2.save()
Profile.objects.create(user=self.u2)
self.us = User.objects.create(username='superuser', is_superuser=True)
self.us.set_password('password')
self.us.save()
Profile.objects.create(user=self.us)
inst = Instance.objects.get(pk=1)
inst.owner = self.u1
inst.save()
def test_renew_by_owner(self):
c = Client()
ct = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
self.login(c, 'user1')
response = c.get('/dashboard/vm/1/renew/')
self.assertEquals(response.status_code, 200)
response = c.post('/dashboard/vm/1/renew/')
self.assertEquals(response.status_code, 302)
ct2 = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
self.assertEquals(ct + 1, ct2)
def test_renew_get_by_nonowner_wo_key(self):
c = Client()
self.login(c, 'user2')
response = c.get('/dashboard/vm/1/renew/')
self.assertEquals(response.status_code, 403)
def test_renew_post_by_nonowner_wo_key(self):
c = Client()
self.login(c, 'user2')
response = c.post('/dashboard/vm/1/renew/')
self.assertEquals(response.status_code, 403)
def test_renew_get_by_nonowner_w_key(self):
key = VmRenewView.get_token_url(Instance.objects.get(pk=1), self.u2)
c = Client()
response = c.get(key)
self.assertEquals(response.status_code, 200)
def test_renew_post_by_anon_w_key(self):
key = VmRenewView.get_token_url(Instance.objects.get(pk=1), self.u2)
ct = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
c = Client()
response = c.post(key)
self.assertEquals(response.status_code, 302)
ct2 = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
self.assertEquals(ct + 1, ct2)
def test_renew_post_by_anon_w_invalid_key(self):
class Mockinst(object):
pk = 2
key = VmRenewView.get_token_url(Mockinst(), self.u2)
ct = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
c = Client()
self.login(c, 'user2')
response = c.get(key)
self.assertEquals(response.status_code, 404)
response = c.post(key)
self.assertEquals(response.status_code, 404)
ct2 = Instance.objects.get(pk=1).activity_log.\
filter(activity_code__endswith='renew').count()
self.assertEquals(ct, ct2)
def test_renew_post_by_anon_w_expired_key(self):
key = reverse(VmRenewView.url_name, args=(
12, 'WzEyLDFd:1WLbSi:2zIb8SUNAIRIOMTmSmKSSit2gpY'))
ct = Instance.objects.get(pk=12).activity_log.\
filter(activity_code__endswith='renew').count()
c = Client()
self.login(c, 'user2')
response = c.get(key)
self.assertEquals(response.status_code, 302)
response = c.post(key)
self.assertEquals(response.status_code, 403)
ct2 = Instance.objects.get(pk=12).activity_log.\
filter(activity_code__endswith='renew').count()
self.assertEquals(ct, ct2)
class IndexViewTest(LoginMixin, TestCase):
fixtures = ['test-vm-fixture.json', 'node.json']
......
......@@ -29,7 +29,7 @@ from .views import (
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView,
GroupRemoveFutureUserView,
GroupCreate, GroupProfileUpdate,
......@@ -40,6 +40,7 @@ from .views import (
UserKeyDelete, UserKeyDetail, UserKeyCreate,
VmTraitsUpdate, VmRawDataUpdate,
GroupPermissionsView,
LeaseAclUpdateView,
)
urlpatterns = patterns(
......@@ -51,6 +52,8 @@ urlpatterns = patterns(
name="dashboard.views.lease-create"),
url(r'^lease/delete/(?P<pk>\d+)/$', LeaseDelete.as_view(),
name="dashboard.views.lease-delete"),
url(r'^lease/(?P<pk>\d+)/acl/$', LeaseAclUpdateView.as_view(),
name="dashboard.views.lease-acl"),
url(r'^template/create/$', TemplateCreate.as_view(),
name="dashboard.views.template-create"),
......@@ -84,8 +87,6 @@ urlpatterns = patterns(
url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(),
name='dashboard.views.vm-renew'),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
name='dashboard.views.vm-activity'),
url(r'^vm/(?P<pk>\d+)/screenshot/$', get_vm_screenshot,
......
......@@ -60,7 +60,7 @@ from .forms import (
CircleAuthenticationForm, HostForm, LeaseForm, MyProfileForm,
NodeForm, TemplateForm, TraitForm, VmCustomizeForm, GroupCreateForm,
UserCreationForm, GroupProfileUpdateForm, UnsubscribeForm,
VmSaveForm, UserKeyForm,
VmSaveForm, UserKeyForm, VmRenewForm,
CirclePasswordChangeForm, VmCreateDiskForm, VmDownloadDiskForm,
TraitsForm, RawDataForm, GroupPermissionForm
)
......@@ -91,6 +91,23 @@ def search_user(keyword):
return User.objects.get(email=keyword)
class RedirectToLoginMixin(AccessMixin):
redirect_exception_classes = (PermissionDenied, )
def dispatch(self, request, *args, **kwargs):
try:
return super(RedirectToLoginMixin, self).dispatch(
request, *args, **kwargs)
except self.redirect_exception_classes:
if not request.user.is_authenticated():
return redirect_to_login(request.get_full_path(),
self.get_login_url(),
self.get_redirect_field_name())
else:
raise
class GroupCodeMixin(object):
@classmethod
......@@ -498,7 +515,7 @@ class VmRawDataUpdate(SuperuserRequiredMixin, UpdateView):
return self.get_object().get_absolute_url() + "#resources"
class OperationView(DetailView):
class OperationView(RedirectToLoginMixin, DetailView):
template_name = 'dashboard/operate.html'
show_in_toolbar = True
......@@ -519,8 +536,16 @@ class OperationView(DetailView):
def get_urlname(cls):
return 'dashboard.vm.op.%s' % cls.op
def get_url(self):
return reverse(self.get_urlname(), args=(self.get_object().pk, ))
@classmethod
def get_instance_url(cls, pk, key=None, *args, **kwargs):
url = reverse(cls.get_urlname(), args=(pk, ) + args, kwargs=kwargs)
if key is None:
return url
else:
return "%s?k=%s" % (url, key)
def get_url(self, **kwargs):
return self.get_instance_url(self.get_object().pk, **kwargs)
def get_template_names(self):
if self.request.is_ajax():
......@@ -541,15 +566,23 @@ class OperationView(DetailView):
ctx = super(OperationView, self).get_context_data(**kwargs)
ctx['op'] = self.get_op()
ctx['opview'] = self
ctx['url'] = self.request.path
url = self.request.path
if self.request.GET:
url += '?' + self.request.GET.urlencode()
ctx['url'] = url
ctx['template'] = super(OperationView, self).get_template_names()[0]
return ctx
def check_auth(self):
logger.debug("OperationView.check_auth(%s)", unicode(self))
self.get_op().check_auth(self.request.user)
def get(self, request, *args, **kwargs):
self.get_op().check_auth(request.user)
self.check_auth()
return super(OperationView, self).get(request, *args, **kwargs)
def post(self, request, extra=None, *args, **kwargs):
self.check_auth()
self.object = self.get_object()
if extra is None:
extra = {}
......@@ -557,31 +590,31 @@ class OperationView(DetailView):
self.get_op().async(user=request.user, **extra)
except Exception as e:
messages.error(request, _('Could not start operation.'))
logger.error(e)
logger.exception(e)
else:
messages.success(request, _('Operation is started.'))
return redirect("%s#activity" % self.object.get_absolute_url())
@classmethod
def factory(cls, op, icon='cog', effect='info'):
def factory(cls, op, icon='cog', effect='info', extra_bases=(), **kwargs):
kwargs.update({'op': op, 'icon': icon, 'effect': effect})
return type(str(cls.__name__ + op),
(cls, ), {'op': op, 'icon': icon, 'effect': effect})
tuple(list(extra_bases) + [cls]), kwargs)
@classmethod
def bind_to_object(cls, instance, **kwargs):
v = cls()
v.get_object = lambda: instance
me = cls()
me.get_object = lambda: instance
for key, value in kwargs.iteritems():
setattr(v, key, value)
return v
setattr(me, key, value)
return me
class VmOperationView(OperationView):
model = Instance
context_object_name = 'instance' # much simpler to mock object
class AjaxOperationMixin(object):
def post(self, request, extra=None, *args, **kwargs):
resp = super(VmOperationView, self).post(request, extra, *args,
**kwargs)
resp = super(AjaxOperationMixin, self).post(
request, extra, *args, **kwargs)
if request.is_ajax():
store = messages.get_messages(request)
store.used = True
......@@ -594,22 +627,32 @@ class VmOperationView(OperationView):
return resp
class VmOperationView(AjaxOperationMixin, OperationView):
model = Instance
context_object_name = 'instance' # much simpler to mock object
class FormOperationMixin(object):
form_class = None
def get_form_kwargs(self):
return {}
def get_context_data(self, **kwargs):
ctx = super(FormOperationMixin, self).get_context_data(**kwargs)
if self.request.method == 'POST':
ctx['form'] = self.form_class(self.request.POST)
ctx['form'] = self.form_class(self.request.POST,
**self.get_form_kwargs())
else:
ctx['form'] = self.form_class()
ctx['form'] = self.form_class(**self.get_form_kwargs())
return ctx
def post(self, request, extra=None, *args, **kwargs):
if extra is None:
extra = {}
form = self.form_class(self.request.POST)
form = self.form_class(self.request.POST, **self.get_form_kwargs())
if form.is_valid():
extra.update(form.cleaned_data)
resp = super(FormOperationMixin, self).post(
......@@ -625,6 +668,14 @@ class FormOperationMixin(object):
return self.get(request)
class RequestFormOperationMixin(FormOperationMixin):
def get_form_kwargs(self):
val = super(FormOperationMixin, self).get_form_kwargs()
val.update({'request': self.request})
return val
class VmCreateDiskView(FormOperationMixin, VmOperationView):
op = 'create_disk'
......@@ -696,12 +747,108 @@ class VmResourcesChangeView(VmOperationView):
*args, **kwargs)
class TokenOperationView(OperationView):
"""Abstract operation view with token support.
User can do the action with a valid token instead of logging in.
"""
token_max_age = 3 * 24 * 3600
redirect_exception_classes = (PermissionDenied, SuspiciousOperation, )
@classmethod
def get_salt(cls):
return unicode(cls)
@classmethod
def get_token(cls, instance, user):
t = tuple([getattr(i, 'pk', i) for i in [instance, user]])
return signing.dumps(t, salt=cls.get_salt(), compress=True)
@classmethod
def get_token_url(cls, instance, user):
key = cls.get_token(instance, user)
return cls.get_instance_url(instance.pk, key)
def check_auth(self):
if 'k' in self.request.GET:
try: # check if token is needed at all
return super(TokenOperationView, self).check_auth()
except Exception:
op = self.get_op()
pk = op.instance.pk
key = self.request.GET.get('k')
logger.debug("checking token supplied to %s",
self.request.get_full_path())
try:
user = self.validate_key(pk, key)
except signing.SignatureExpired:
messages.error(self.request, _('The token has expired.'))
else:
logger.info("Request user changed to %s at %s",
user, self.request.get_full_path())
self.request.user = user
else:
logger.debug("no token supplied to %s",
self.request.get_full_path())
return super(TokenOperationView, self).check_auth()
def validate_key(self, pk, key):
"""Get object based on signed token.
"""
try:
data = signing.loads(key, salt=self.get_salt())
logger.debug('Token data: %s', unicode(data))
instance, user = data
logger.debug('Extracted token data: instance: %s, user: %s',
unicode(instance), unicode(user))
except (signing.BadSignature, ValueError, TypeError) as e:
logger.warning('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(self.request.user), unicode(e))
raise SuspiciousOperation()
try:
instance, user = signing.loads(key, max_age=self.token_max_age,
salt=self.get_salt())
logger.debug('Extracted non-expired token data: %s, %s',
unicode(instance), unicode(user))
except signing.BadSignature as e:
raise signing.SignatureExpired()
if pk != instance:
logger.debug('pk (%d) != instance (%d)', pk, instance)
raise SuspiciousOperation()
user = User.objects.get(pk=user)
return user
class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
op = 'renew'
icon = 'calendar'
effect = 'info'
show_in_toolbar = False
form_class = VmRenewForm
def get_form_kwargs(self):
choices = Lease.get_objects_with_level("user", self.request.user)
default = self.get_op().instance.lease
if default and default not in choices:
choices = list(choices) + [default]
val = super(VmRenewView, self).get_form_kwargs()
val.update({'choices': choices, 'default': default})
return val
vm_ops = OrderedDict([
('deploy', VmOperationView.factory(
op='deploy', icon='play', effect='success')),
('wake_up', VmOperationView.factory(
op='wake_up', icon='sun', effect='success')),
('sleep', VmOperationView.factory(
extra_bases=[TokenOperationView],
op='sleep', icon='moon', effect='info')),
('migrate', VmMigrateView),
('save_as_template', VmSaveView),
......@@ -716,9 +863,11 @@ vm_ops = OrderedDict([
('recover', VmOperationView.factory(
op='recover', icon='medkit', effect='warning')),
('destroy', VmOperationView.factory(
extra_bases=[TokenOperationView],
op='destroy', icon='remove', effect='danger')),
('create_disk', VmCreateDiskView),
('download_disk', VmDownloadDiskView),
('renew', VmRenewView),
])
......@@ -2136,10 +2285,11 @@ class VmMassDelete(LoginRequiredMixin, View):
return redirect(next if next else reverse_lazy('dashboard.index'))
class LeaseCreate(LoginRequiredMixin, SuperuserRequiredMixin,
class LeaseCreate(LoginRequiredMixin, PermissionRequiredMixin,
SuccessMessageMixin, CreateView):
model = Lease
form_class = LeaseForm
permission_required = 'vm.create_leases'
template_name = "dashboard/lease-create.html"
success_message = _("Successfully created a new lease.")
......@@ -2147,6 +2297,10 @@ class LeaseCreate(LoginRequiredMixin, SuperuserRequiredMixin,
return reverse_lazy("dashboard.views.template-list")
class LeaseAclUpdateView(AclUpdateView):
model = Lease
class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin,
SuccessMessageMixin, UpdateView):
model = Lease
......@@ -2154,6 +2308,12 @@ class LeaseDetail(LoginRequiredMixin, SuperuserRequiredMixin,
template_name = "dashboard/lease-edit.html"
success_message = _("Successfully modified lease.")
def get_context_data(self, *args, **kwargs):
obj = self.get_object()
context = super(LeaseDetail, self).get_context_data(*args, **kwargs)
context['acl'] = get_vm_acl_data(obj)
return context
def get_success_url(self):
return reverse_lazy("dashboard.views.lease-detail", kwargs=self.kwargs)
......@@ -2295,163 +2455,6 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
kwargs={'pk': obj.pk}))
class AbstractVmFunctionView(AccessMixin, View):
"""Abstract instance-action view.
User can do the action with a valid token or if has at least required_level
ACL level for the instance.
Children should at least implement/add template_name, success_message,
url_name, and do_action().
"""
token_max_age = 3 * 24 * 3600
required_level = 'owner'
success_message = _("Failed to perform requested action.")
@classmethod
def check_acl(cls, instance, user):
if not instance.has_level(user, cls.required_level):
raise PermissionDenied()
@classmethod
def get_salt(cls):
return unicode(cls)
@classmethod
def get_token(cls, instance, user, *args):
t = tuple([getattr(i, 'pk', i) for i in [instance, user] + list(args)])
return signing.dumps(t, salt=cls.get_salt(), compress=True)
@classmethod
def get_token_url(cls, instance, user, *args):
key = cls.get_token(instance, user, *args)
args = (instance.pk, key) + args
return reverse(cls.url_name, args=args)
# this wont work, CBVs suck: reverse(cls.as_view(), args=args)
def get_template_names(self):
return [self.template_name]
def get(self, request, pk, key=None, *args, **kwargs):
class LoginNeeded(Exception):
pass
pk = int(pk)
instance = get_object_or_404(Instance, pk=pk)
try:
if key:
logger.debug('Confirm dialog for token %s.', key)
try:
self.validate_key(pk, key)
except signing.SignatureExpired:
messages.error(request, _(
'The token has expired, please log in.'))
raise LoginNeeded()
self.key = key
else:
if not request.user.is_authenticated():
raise LoginNeeded()
self.check_acl(instance, request.user)
except LoginNeeded:
return redirect_to_login(request.get_full_path(),
self.get_login_url(),
self.get_redirect_field_name())
except SuspiciousOperation as e:
messages.error(request, _('This token is invalid.'))
logger.warning('This token %s is invalid. %s', key, unicode(e))
raise PermissionDenied()
return render(request, self.get_template_names(),
self.get_context(instance))
def post(self, request, pk, key=None, *args, **kwargs):
class LoginNeeded(Exception):
pass
pk = int(pk)
instance = get_object_or_404(Instance, pk=pk)
try:
if not request.user.is_authenticated() and key:
try:
user = self.validate_key(pk, key)
except signing.SignatureExpired:
messages.error(request, _(
'The token has expired, please log in.'))
raise LoginNeeded()
self.key = key
else:
user = request.user
self.check_acl(instance, request.user)
except LoginNeeded:
return redirect_to_login(request.get_full_path(),
self.get_login_url(),
self.get_redirect_field_name())
except SuspiciousOperation as e:
messages.error(request, _('This token is invalid.'))
logger.warning('This token %s is invalid. %s', key, unicode(e))
raise PermissionDenied()
if self.do_action(instance, user):
messages.success(request, self.success_message)
else:
messages.error(request, self.fail_message)
return HttpResponseRedirect(instance.get_absolute_url())
def validate_key(self, pk, key):
"""Get object based on signed token.
"""
try:
data = signing.loads(key, salt=self.get_salt())
logger.debug('Token data: %s', unicode(data))
instance, user = data
logger.debug('Extracted token data: instance: %s, user: %s',
unicode(instance), unicode(user))
except (signing.BadSignature, ValueError, TypeError) as e:
logger.warning('Tried invalid token. Token: %s, user: %s. %s',
key, unicode(self.request.user), unicode(e))
raise SuspiciousOperation()
try:
instance, user = signing.loads(key, max_age=self.token_max_age,
salt=self.get_salt())
logger.debug('Extracted non-expired token data: %s, %s',
unicode(instance), unicode(user))
except signing.BadSignature as e:
raise signing.SignatureExpired()
if pk != instance:
logger.debug('pk (%d) != instance (%d)', pk, instance)
raise SuspiciousOperation()
user = User.objects.get(pk=user)
return user
def do_action(self, instance, user): # noqa
raise NotImplementedError('Please override do_action(instance, user)')
def get_context(self, instance):
context = {'instance': instance}
if getattr(self, 'key', None) is not None:
context['key'] = self.key
return context
class VmRenewView(AbstractVmFunctionView):
"""User can renew an instance."""
template_name = 'dashboard/confirm/base-renew.html'
success_message = _("Virtual machine is successfully renewed.")
url_name = 'dashboard.views.vm-renew'
def get_context(self, instance):
context = super(VmRenewView, self).get_context(instance)
(context['time_of_suspend'],
context['time_of_delete']) = instance.get_renew_times()
return context
def do_action(self, instance, user):
instance.renew(user=user)
logger.info('Instance %s renewed by %s.', unicode(instance),
unicode(user))
return True
class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"""User can accept an ownership offer."""
......
......@@ -18,12 +18,14 @@
from __future__ import absolute_import, unicode_literals
from datetime import timedelta, datetime
from django.db.models import Model, CharField, IntegerField
from django.db.models import Model, CharField, IntegerField, permalink
from django.utils.translation import ugettext_lazy as _
from django.utils.timesince import timeuntil
from model_utils.models import TimeStampedModel
from acl.models import AclBase
ARCHITECTURES = (('x86_64', 'x86-64 (64 bit)'),
('i686', 'x86 (32 bit)'))
......@@ -66,13 +68,18 @@ class NamedBaseResourceConfig(BaseResourceConfigModel, TimeStampedModel):
return self.name
class Lease(Model):
class Lease(AclBase):
"""Lease times for VM instances.
Specifies a time duration until suspension and deletion of a VM
instance.
"""
ACL_LEVELS = (
('user', _('user')), # use this lease
('operator', _('operator')), # share this lease
('owner', _('owner')), # change this lease
)
name = CharField(max_length=100, unique=True,
verbose_name=_('name'))
suspend_interval_seconds = IntegerField(
......@@ -88,6 +95,9 @@ class Lease(Model):
app_label = 'vm'
db_table = 'vm_lease'
ordering = ['name', ]
permissions = (
('create_leases', _('Can create new leases.')),
)
@property
def suspend_interval(self):
......@@ -141,6 +151,10 @@ class Lease(Model):
's': self.get_readable_suspend_time(),
'r': self.get_readable_delete_time()}
@permalink
def get_absolute_url(self):
return ('dashboard.views.lease-detail', None, {'pk': self.pk})
class Trait(Model):
name = CharField(max_length=50, verbose_name=_('name'))
......
......@@ -439,10 +439,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
for cps in customized_params]
def clean(self, *args, **kwargs):
if self.time_of_suspend is None:
self._do_renew(which='suspend')
if self.time_of_delete is None:
self._do_renew(which='delete')
self.time_of_suspend, self.time_of_delete = self.get_renew_times()
super(Instance, self).clean(*args, **kwargs)
def manual_state_change(self, new_state="NOSTATE", reason=None, user=None):
......@@ -715,36 +712,14 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
else:
return False
def get_renew_times(self):
def get_renew_times(self, lease=None):
"""Returns new suspend and delete times if renew would be called.
"""
if lease is None:
lease = self.lease
return (
timezone.now() + self.lease.suspend_interval,
timezone.now() + self.lease.delete_interval)
def _do_renew(self, which='both'):
"""Set expiration times to renewed values.
"""
time_of_suspend, time_of_delete = self.get_renew_times()
if which in ('suspend', 'both'):
self.time_of_suspend = time_of_suspend
if which in ('delete', 'both'):
self.time_of_delete = time_of_delete
def renew(self, which='both', base_activity=None, user=None):
"""Renew virtual machine instance leases.
"""
if base_activity is None:
act_ctx = instance_activity(code_suffix='renew', instance=self,
user=user)
else:
act_ctx = base_activity.sub_activity('renew')
with act_ctx:
if which not in ('suspend', 'delete', 'both'):
raise ValueError('No such expiration type.')
self._do_renew(which)
self.save()
timezone.now() + lease.suspend_interval,
timezone.now() + lease.delete_interval)
def change_password(self, user=None):
"""Generate new password for the vm
......
......@@ -200,7 +200,7 @@ class DeployOperation(InstanceOperation):
with activity.sub_activity('booting'):
self.instance.resume_vm(timeout=timeout)
self.instance.renew(which='both', base_activity=activity)
self.instance.renew(parent_activity=activity)
register_operation(DeployOperation)
......@@ -613,12 +613,30 @@ class WakeUpOperation(InstanceOperation):
self.instance.deploy_net()
# Renew vm
self.instance.renew(which='both', base_activity=activity)
self.instance.renew(parent_activity=activity)
register_operation(WakeUpOperation)
class RenewOperation(InstanceOperation):
activity_code_suffix = 'renew'
id = 'renew'
name = _("renew")
description = _("Renew expiration times")
acl_level = "operator"
required_perms = ()
concurrency_check = False
def _operation(self, lease=None):
(self.instance.time_of_suspend,
self.instance.time_of_delete) = self.instance.get_renew_times(lease)
self.instance.save()
register_operation(RenewOperation)
class NodeOperation(Operation):
async_operation = abortable_async_node_operation
host_cls = Node
......
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