Commit bf256930 by Őry Máté

Merge branch 'feature-vm-gc' into 'master'

Feature: destroy&suspend expired VMs

See #80

*  suspend/destroy if expired
*  notification about suspend/destroy happened
*  renew from gui #45
*  notify about expiration coming
*  tests
parents 1ffe10b4 e185ea3e
...@@ -151,9 +151,9 @@ class AclBase(Model): ...@@ -151,9 +151,9 @@ class AclBase(Model):
return True return True
return False return False
def get_users_with_level(self): def get_users_with_level(self, **kwargs):
logger.debug('%s.get_users_with_level() called', unicode(self)) logger.debug('%s.get_users_with_level() called', unicode(self))
object_levels = (self.object_level_set.select_related( object_levels = (self.object_level_set.filter(**kwargs).select_related(
'users', 'level').all()) 'users', 'level').all())
users = [] users = []
for object_level in object_levels: for object_level in object_levels:
......
...@@ -29,3 +29,12 @@ CACHES = { ...@@ -29,3 +29,12 @@ CACHES = {
'BACKEND': 'django.core.cache.backends.dummy.DummyCache' 'BACKEND': 'django.core.cache.backends.dummy.DummyCache'
} }
} }
LOGGING['loggers']['djangosaml2'] = {'handlers': ['console'],
'level': 'CRITICAL'}
LOGGING['handlers']['console'] = {'level': 'WARNING',
'class': 'logging.StreamHandler',
'formatter': 'simple'}
for i in LOCAL_APPS:
LOGGING['loggers'][i] = {'handlers': ['console'], 'level': 'CRITICAL'}
...@@ -1372,6 +1372,35 @@ ...@@ -1372,6 +1372,35 @@
} }
}, },
{ {
"pk": 12,
"model": "vm.instance",
"fields": {
"destroyed": null,
"disks": [],
"boot_menu": false,
"owner": 1,
"time_of_delete": null,
"max_ram_size": 200,
"pw": "ads",
"time_of_suspend": null,
"ram_size": 200,
"priority": 4,
"active_since": null,
"template": null,
"access_method": "nx",
"lease": 1,
"node": null,
"description": "",
"arch": "x86_64",
"name": "vanneve",
"created": "2013-09-16T09:05:59.991Z",
"raw_data": "",
"vnc_port": 1235,
"num_cores": 2,
"modified": "2013-10-14T07:27:38.192Z"
}
},
{
"pk": 1, "pk": 1,
"model": "firewall.domain", "model": "firewall.domain",
"fields": { "fields": {
......
...@@ -5,7 +5,8 @@ from django.conf import settings ...@@ -5,7 +5,8 @@ from django.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.auth.signals import user_logged_in from django.contrib.auth.signals import user_logged_in
from django.db.models import ( from django.db.models import (
Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField Model, ForeignKey, OneToOneField, CharField, IntegerField, TextField,
DateTimeField,
) )
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import ugettext_lazy as _, override from django.utils.translation import ugettext_lazy as _, override
...@@ -34,12 +35,13 @@ class Notification(TimeStampedModel): ...@@ -34,12 +35,13 @@ class Notification(TimeStampedModel):
to = ForeignKey(User) to = ForeignKey(User)
subject = CharField(max_length=128) subject = CharField(max_length=128)
message = TextField() message = TextField()
valid_until = DateTimeField(null=True, default=None)
class Meta: class Meta:
ordering = ['-created'] ordering = ['-created']
@classmethod @classmethod
def send(cls, user, subject, template, context={}): def send(cls, user, subject, template, context={}, valid_until=None):
try: try:
language = user.profile.preferred_language language = user.profile.preferred_language
except: except:
...@@ -48,7 +50,8 @@ class Notification(TimeStampedModel): ...@@ -48,7 +50,8 @@ class Notification(TimeStampedModel):
context['user'] = user context['user'] = user
rendered = render_to_string(template, context) rendered = render_to_string(template, context)
subject = unicode(subject) subject = unicode(subject)
return cls.objects.create(to=user, subject=subject, message=rendered) return cls.objects.create(to=user, subject=subject, message=rendered,
valid_until=valid_until)
class Profile(Model): class Profile(Model):
...@@ -62,8 +65,9 @@ class Profile(Model): ...@@ -62,8 +65,9 @@ class Profile(Model):
help_text=_('Unique identifier of the person, e.g. a student number.')) help_text=_('Unique identifier of the person, e.g. a student number.'))
instance_limit = IntegerField(default=5) instance_limit = IntegerField(default=5)
def notify(self, subject, template, context={}): def notify(self, subject, template, context={}, valid_until=None):
return Notification.send(self.user, subject, template, context) return Notification.send(self.user, subject, template, context,
valid_until)
class GroupProfile(AclBase): class GroupProfile(AclBase):
......
...@@ -3,6 +3,10 @@ $(function() { ...@@ -3,6 +3,10 @@ $(function() {
if(decideActivityRefresh()) { if(decideActivityRefresh()) {
checkNewActivity(false, 1); checkNewActivity(false, 1);
} }
$('a[href="#activity"]').click(function(){
$('a[href="#activity"] i').addClass('icon-spin');
checkNewActivity(false,0);
});
/* save resources */ /* save resources */
$('#vm-details-resources-save').click(function() { $('#vm-details-resources-save').click(function() {
...@@ -211,13 +215,13 @@ function checkNewActivity(only_state, runs) { ...@@ -211,13 +215,13 @@ function checkNewActivity(only_state, runs) {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled"); $("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
} }
if(decideActivityRefresh()) { if(runs > 0 && decideActivityRefresh()) {
console.log("szia");
setTimeout( setTimeout(
function() {checkNewActivity(only_state, runs + 1)}, function() {checkNewActivity(only_state, runs + 1)},
1000 + runs * 250 1000 + Math.exp(runs * 0.05)
); );
} }
$('a[href="#activity"] i').removeClass('icon-spin');
}, },
error: function() { error: function() {
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="no-margin">
{%blocktrans with instance=instance.name%}
Renewing <em>{{instance}}</em>
{%endblocktrans%}
</h3>
</div>
<div class="panel-body">
{%blocktrans with object=instance.name%}
Do you want to renew <strong>{{ object }}</strong>?
{%endblocktrans%}
{%blocktrans with suspend=time_of_suspend delete=time_of_delete|default:"n/a" %}
The instance will be suspended at <em>{{suspend}}</em>
and removed at <em>{{delete}}</em> if you renew it now.
{%endblocktrans%}
<div class="pull-right">
<form action="" method="POST">
{% csrf_token %}
<a class="btn btn-default"
href="{{instance.get_absolute_path}}">{% trans "Back" %}</a>
<button class="btn btn-danger">{% trans "Renew" %}</button>
</form>
</div>
</div>
</div>
{% endblock %}
{%load i18n%}
{%blocktrans with instance=instance.name url=instance.get_absolute_url %}
Your instance <a href="{{url}}">{{instance}}</a> has been destroyed due to expiration.
{%endblocktrans%}
{%load i18n%}
{%blocktrans with instance=instance.name url=instance.get_absolute_url suspend=instance.time_of_suspend delete=instance.time_of_delete %}
Your instance <a href="{{url}}">{{instance}}</a> is going to expire.
It will be suspended at {{suspend}} and destroyed at {{delete}}.
{%endblocktrans%}
{%blocktrans with token=token url=instance.get_absolute_url %}
Please, either <a href="{{token}}">renew</a> or <a href="{{url}}">destroy</a>
it now.
{%endblocktrans%}
{%load i18n%}
{%blocktrans with instance=instance.name url=instance.get_absolute_url %}
Your instance <a href="{{url}}">{{instance}}</a> has been suspended due to expiration.
{%endblocktrans%}
...@@ -8,6 +8,16 @@ ...@@ -8,6 +8,16 @@
<dd><small>{{ instance.description }}</small></dd> <dd><small>{{ instance.description }}</small></dd>
</dl> </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>
</h4>
<dl>
<dt>{% trans "Suspended at:" %}</dt>
<dd><i class="icon-moon"></i> {{ instance.time_of_suspend|timeuntil }}</dd>
<dt>{% trans "Destroyed at:" %}</dt>
<dd><i class="icon-remove"></i> {{ instance.time_of_delete|timeuntil }}</dd>
</dl>
<div style="font-weight: bold;">{% trans "Tags" %}</div> <div style="font-weight: bold;">{% trans "Tags" %}</div>
<div id="vm-details-tags" style="margin-bottom: 20px;"> <div id="vm-details-tags" style="margin-bottom: 20px;">
<div id="vm-details-tags-list"> <div id="vm-details-tags-list">
......
...@@ -3,6 +3,7 @@ from django.contrib.auth.models import User ...@@ -3,6 +3,7 @@ from django.contrib.auth.models import User
from django.test import TestCase from django.test import TestCase
from ..models import Profile from ..models import Profile
from ..views import search_user
class NotificationTestCase(TestCase): class NotificationTestCase(TestCase):
...@@ -25,3 +26,27 @@ class NotificationTestCase(TestCase): ...@@ -25,3 +26,27 @@ class NotificationTestCase(TestCase):
assert 'user1' in msg.message assert 'user1' in msg.message
assert 'testme' in msg.message assert 'testme' in msg.message
assert msg in self.u1.notification_set.all() assert msg in self.u1.notification_set.all()
class ProfileTestCase(TestCase):
def setUp(self):
self.u1 = User.objects.create(username='user1')
Profile.objects.get_or_create(user=self.u1)
self.u2 = User.objects.create(username='user2',
email='john@example.org')
Profile.objects.get_or_create(user=self.u2, org_id='apple')
def test_search_user_by_name(self):
self.assertEqual(search_user('user1'), self.u1)
self.assertEqual(search_user('user2'), self.u2)
def test_search_user_by_mail(self):
self.assertEqual(search_user('john@example.org'), self.u2)
def test_search_user_by_orgid(self):
self.assertEqual(search_user('apple'), self.u2)
def test_search_user_nonexist(self):
with self.assertRaises(User.DoesNotExist):
search_user('no-such-user')
...@@ -2,9 +2,11 @@ from django.test import TestCase ...@@ -2,9 +2,11 @@ from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core.exceptions import SuspiciousOperation from django.core.exceptions import SuspiciousOperation
from django.core.urlresolvers import reverse
from vm.models import Instance, InstanceTemplate, Lease, Node from vm.models import Instance, InstanceTemplate, Lease, Node
from ..models import Profile from ..models import Profile
from ..views import VmRenewView
from storage.models import Disk from storage.models import Disk
from firewall.models import Vlan from firewall.models import Vlan
...@@ -356,3 +358,97 @@ class TransferOwnershipViewTest(LoginMixin, TestCase): ...@@ -356,3 +358,97 @@ class TransferOwnershipViewTest(LoginMixin, TestCase):
self.login(c, 'user2') self.login(c, 'user2')
response = c.post(url) response = c.post(url)
self.assertEquals(Instance.objects.get(pk=1).owner.pk, self.u2.pk) 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)
...@@ -9,7 +9,7 @@ from .views import ( ...@@ -9,7 +9,7 @@ from .views import (
FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete, FavouriteView, NodeStatus, GroupList, TemplateDelete, LeaseDelete,
VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete, VmGraphView, TemplateAclUpdateView, GroupDetailView, GroupDelete,
GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView, GroupAclUpdateView, GroupUserDelete, NotificationView, NodeGraphView,
VmMigrateView, VmDetailVncTokenView, VmMigrateView, VmDetailVncTokenView, VmRenewView,
) )
urlpatterns = patterns( urlpatterns = patterns(
...@@ -53,6 +53,8 @@ urlpatterns = patterns( ...@@ -53,6 +53,8 @@ urlpatterns = patterns(
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity), url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
url(r'^vm/(?P<pk>\d+)/migrate/$', VmMigrateView.as_view(), url(r'^vm/(?P<pk>\d+)/migrate/$', VmMigrateView.as_view(),
name='dashboard.views.vm-migrate'), name='dashboard.views.vm-migrate'),
url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(),
name='dashboard.views.vm-renew'),
url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'), url(r'^node/list/$', NodeList.as_view(), name='dashboard.views.node-list'),
url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(), url(r'^node/(?P<pk>\d+)/$', NodeDetailView.as_view(),
......
...@@ -7,7 +7,7 @@ import requests ...@@ -7,7 +7,7 @@ import requests
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.contrib.auth.views import login from django.contrib.auth.views import login, redirect_to_login
from django.contrib.messages import warning from django.contrib.messages import warning
from django.core.exceptions import ( from django.core.exceptions import (
PermissionDenied, SuspiciousOperation, PermissionDenied, SuspiciousOperation,
...@@ -15,7 +15,7 @@ from django.core.exceptions import ( ...@@ -15,7 +15,7 @@ from django.core.exceptions import (
from django.core import signing from django.core import signing
from django.core.urlresolvers import reverse, reverse_lazy from django.core.urlresolvers import reverse, reverse_lazy
from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import redirect, render from django.shortcuts import redirect, render, get_object_or_404
from django.views.decorators.http import require_GET from django.views.decorators.http import require_GET
from django.views.generic.detail import SingleObjectMixin from django.views.generic.detail import SingleObjectMixin
from django.views.generic import (TemplateView, DetailView, View, DeleteView, from django.views.generic import (TemplateView, DetailView, View, DeleteView,
...@@ -27,7 +27,9 @@ from django.template.loader import render_to_string ...@@ -27,7 +27,9 @@ from django.template.loader import render_to_string
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django_tables2 import SingleTableView from django_tables2 import SingleTableView
from braces.views import LoginRequiredMixin, SuperuserRequiredMixin from braces.views import (
LoginRequiredMixin, SuperuserRequiredMixin, AccessMixin
)
from .forms import ( from .forms import (
VmCustomizeForm, TemplateForm, LeaseForm, NodeForm, HostForm, VmCustomizeForm, TemplateForm, LeaseForm, NodeForm, HostForm,
...@@ -48,9 +50,10 @@ def search_user(keyword): ...@@ -48,9 +50,10 @@ def search_user(keyword):
try: try:
return User.objects.get(username=keyword) return User.objects.get(username=keyword)
except User.DoesNotExist: except User.DoesNotExist:
return User.objects.get(email=keyword) try:
except User.DoesNotExist: return User.objects.get(profile__org_id=keyword)
return User.objects.get(profile__org_id=keyword) except User.DoesNotExist:
return User.objects.get(email=keyword)
# github.com/django/django/blob/stable/1.6.x/django/contrib/messages/views.py # github.com/django/django/blob/stable/1.6.x/django/contrib/messages/views.py
...@@ -1533,6 +1536,163 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView): ...@@ -1533,6 +1536,163 @@ class TransferOwnershipView(LoginRequiredMixin, DetailView):
kwargs={'pk': obj.pk})) 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())
@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): class TransferOwnershipConfirmView(LoginRequiredMixin, View):
"""User can accept an ownership offer.""" """User can accept an ownership offer."""
......
...@@ -28,11 +28,16 @@ celery.conf.update( ...@@ -28,11 +28,16 @@ celery.conf.update(
'schedule': timedelta(seconds=5), 'schedule': timedelta(seconds=5),
'options': {'queue': 'localhost.man'} 'options': {'queue': 'localhost.man'}
}, },
'vm.periodic_tasks': { 'vm.update_domain_states': {
'task': 'vm.tasks.local_periodic_tasks.update_domain_states', 'task': 'vm.tasks.local_periodic_tasks.update_domain_states',
'schedule': timedelta(seconds=10), 'schedule': timedelta(seconds=10),
'options': {'queue': 'localhost.man'} 'options': {'queue': 'localhost.man'}
}, },
'vm.garbage_collector': {
'task': 'vm.tasks.local_periodic_tasks.garbage_collector',
'schedule': timedelta(minutes=10),
'options': {'queue': 'localhost.man'}
},
'storage.periodic_tasks': { 'storage.periodic_tasks': {
'task': 'storage.tasks.periodic_tasks.garbage_collector', 'task': 'storage.tasks.periodic_tasks.garbage_collector',
'schedule': timedelta(hours=1), 'schedule': timedelta(hours=1),
......
...@@ -302,7 +302,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -302,7 +302,7 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
def clean(self, *args, **kwargs): def clean(self, *args, **kwargs):
if self.time_of_delete is None: if self.time_of_delete is None:
self.renew(which='delete') self._do_renew(which='delete')
super(Instance, self).clean(*args, **kwargs) super(Instance, self).clean(*args, **kwargs)
@classmethod @classmethod
...@@ -355,9 +355,16 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -355,9 +355,16 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
params = dict(template=template, owner=owner, pw=pwgen()) params = dict(template=template, owner=owner, pw=pwgen())
params.update([(f, getattr(template, f)) for f in common_fields]) params.update([(f, getattr(template, f)) for f in common_fields])
params.update(kwargs) # override defaults w/ user supplied values params.update(kwargs) # override defaults w/ user supplied values
if '%d' not in params['name']:
params['name'] += ' %d'
return [cls.__create_instance(params, disks, networks, req_traits, instances = []
tags) for i in xrange(amount)] for i in xrange(amount):
real_params = params
real_params['name'] = real_params['name'].replace('%d', str(i))
instances.append(cls.__create_instance(real_params, disks,
networks, req_traits, tags))
return instances
@classmethod @classmethod
def __create_instance(cls, params, disks, networks, req_traits, tags): def __create_instance(cls, params, disks, networks, req_traits, tags):
...@@ -556,16 +563,98 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel): ...@@ -556,16 +563,98 @@ class Instance(AclBase, VirtualMachineDescModel, TimeStampedModel):
else: else:
raise Node.DoesNotExist() raise Node.DoesNotExist()
def renew(self, which='both'): def _is_notified_about_expiration(self):
renews = self.activity_log.filter(activity_code__endswith='renew')
cond = {'activity_code__endswith': 'notification_about_expiration'}
if len(renews) > 0:
cond['finished__gt'] = renews[0].started
return self.activity_log.filter(**cond).exists()
def notify_owners_about_expiration(self, again=False):
"""Notify owners about vm expiring soon if they aren't already.
:param again: Notify already notified owners.
"""
if not again and self._is_notified_about_expiration():
return False
success, failed = [], []
def on_commit(act):
act.result = {'failed': failed, 'success': success}
with instance_activity('notification_about_expiration', instance=self,
on_commit=on_commit):
from dashboard.views import VmRenewView
level = self.get_level_object("owner")
for u, ulevel in self.get_users_with_level(level__pk=level.pk):
try:
token = VmRenewView.get_token_url(self, u)
u.profile.notify(
_('%s expiring soon') % unicode(self),
'dashboard/notifications/vm-expiring.html',
{'instance': self, 'token': token}, valid_until=min(
self.time_of_delete, self.time_of_suspend))
except Exception as e:
failed.append((u, e))
else:
success.append(u)
return True
def is_expiring(self, threshold=0.1):
"""Returns if an instance will expire soon.
Soon means that the time of suspend or delete comes in 10% of the
interval what the Lease allows. This rate is configurable with the
only parameter, threshold (0.1 = 10% by default).
"""
return (self._is_suspend_expiring(self, threshold) or
self._is_delete_expiring(self, threshold))
def _is_suspend_expiring(self, threshold=0.1):
interval = self.lease.suspend_interval
if interval is not None:
limit = timezone.now() + threshold * self.lease.suspend_interval
return limit > self.time_of_suspend
else:
return False
def _is_delete_expiring(self, threshold=0.1):
interval = self.lease.delete_interval
if interval is not None:
limit = timezone.now() + threshold * self.lease.delete_interval
return limit > self.time_of_delete
else:
return False
def get_renew_times(self):
"""Returns new suspend and delete times if renew would be called.
"""
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. """Renew virtual machine instance leases.
""" """
if which not in ['suspend', 'delete', 'both']: if base_activity is None:
raise ValueError('No such expiration type.') act = instance_activity(code_suffix='renew', instance=self,
if which in ['suspend', 'both']: user=user)
self.time_of_suspend = timezone.now() + self.lease.suspend_interval else:
if which in ['delete', 'both']: act = base_activity.sub_activity('renew')
self.time_of_delete = timezone.now() + self.lease.delete_interval with act:
self.save() if which not in ('suspend', 'delete', 'both'):
raise ValueError('No such expiration type.')
self._do_renew(which)
self.save()
def change_password(self, user=None): def change_password(self, user=None):
"""Generate new password for the vm """Generate new password for the vm
......
import logging
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from manager.mancelery import celery from manager.mancelery import celery
from vm.models import Node from vm.models import Node, Instance
logger = logging.getLogger(__name__)
@celery.task(ignore_result=True) @celery.task(ignore_result=True)
...@@ -7,3 +13,44 @@ def update_domain_states(): ...@@ -7,3 +13,44 @@ def update_domain_states():
nodes = Node.objects.filter(enabled=True).all() nodes = Node.objects.filter(enabled=True).all()
for node in nodes: for node in nodes:
node.update_vm_states() node.update_vm_states()
@celery.task(ignore_result=True)
def garbage_collector(timeout=15):
"""Garbage collector for instances.
Suspends and destroys expired instances.
:param timeout: Seconds before TimeOut exception
:type timeout: int
"""
now = timezone.now()
for i in Instance.objects.filter(destroyed=None).all():
if i.time_of_delete and now < i.time_of_delete:
i.destroy_async()
logger.info("Expired instance %d destroyed.", i.pk)
try:
i.owner.profile.notify(
_('%s destroyed') % unicode(i),
'dashboard/notifications/vm-destroyed.html',
{'instance': i})
except Exception as e:
logger.debug('Could not notify owner of instance %d .%s',
i.pk, unicode(e))
elif (i.time_of_suspend and now < i.time_of_suspend and
i.state == 'RUNNING'):
i.sleep_async()
logger.info("Expired instance %d suspended." % i.pk)
try:
i.owner.profile.notify(
_('%s suspended') % unicode(i),
'dashboard/notifications/vm-suspended.html',
{'instance': i})
except Exception as e:
logger.debug('Could not notify owner of instance %d .%s',
i.pk, unicode(e))
elif i.is_expiring():
logger.debug("Instance %d expires soon." % i.pk)
i.notify_owners_about_expiration()
else:
logger.debug("Instance %d didn't expire." % i.pk)
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