Commit eaea5499 by Őry Máté

Merge branch 'feature-agent-renew' into 'master'

Feature Agent Renew

* send notification to agent about expiration
* provide machine-readable response for renew
parents 0dba3a9f 735f6921
...@@ -908,7 +908,8 @@ class VmRenewForm(forms.Form): ...@@ -908,7 +908,8 @@ class VmRenewForm(forms.Form):
self.fields['lease'] = forms.ModelChoiceField(queryset=choices, self.fields['lease'] = forms.ModelChoiceField(queryset=choices,
initial=default, initial=default,
required=True, required=False,
empty_label=None,
label=_('Length')) label=_('Length'))
if len(choices) < 2: if len(choices) < 2:
self.fields['lease'].widget = HiddenInput() self.fields['lease'].widget = HiddenInput()
......
...@@ -280,6 +280,7 @@ class RenewViewTest(unittest.TestCase): ...@@ -280,6 +280,7 @@ class RenewViewTest(unittest.TestCase):
view = vm_ops['renew'] view = vm_ops['renew']
with patch.object(view, 'get_object') as go, \ with patch.object(view, 'get_object') as go, \
patch('dashboard.views.messages') as msg, \
patch('dashboard.views.get_object_or_404') as go4: patch('dashboard.views.get_object_or_404') as go4:
inst = MagicMock(spec=Instance) inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance" inst._meta.object_name = "Instance"
...@@ -288,7 +289,10 @@ class RenewViewTest(unittest.TestCase): ...@@ -288,7 +289,10 @@ class RenewViewTest(unittest.TestCase):
inst.has_level.return_value = True inst.has_level.return_value = True
go.return_value = inst go.return_value = inst
go4.return_value = MagicMock() go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234).render().status_code == 200 assert view.as_view()(request, pk=1234)
assert not msg.error.called
assert inst.renew.async.called_with(user=request.user, lease=None)
assert inst.renew.async.return_value.get.called
# success would redirect # success would redirect
def test_renew_by_owner_w_param(self): def test_renew_by_owner_w_param(self):
......
...@@ -52,6 +52,7 @@ from django_tables2 import SingleTableView ...@@ -52,6 +52,7 @@ from django_tables2 import SingleTableView
from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin, from braces.views import (LoginRequiredMixin, SuperuserRequiredMixin,
PermissionRequiredMixin) PermissionRequiredMixin)
from braces.views._access import AccessMixin from braces.views._access import AccessMixin
from celery.exceptions import TimeoutError
from django_sshkey.models import UserKey from django_sshkey.models import UserKey
...@@ -69,6 +70,7 @@ from .tables import ( ...@@ -69,6 +70,7 @@ from .tables import (
NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable, NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable,
GroupListTable, UserKeyListTable GroupListTable, UserKeyListTable
) )
from common.models import HumanReadableObject
from vm.models import ( from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface, Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
InterfaceTemplate, Lease, Node, NodeActivity, Trait, InterfaceTemplate, Lease, Node, NodeActivity, Trait,
...@@ -486,6 +488,7 @@ class OperationView(RedirectToLoginMixin, DetailView): ...@@ -486,6 +488,7 @@ class OperationView(RedirectToLoginMixin, DetailView):
template_name = 'dashboard/operate.html' template_name = 'dashboard/operate.html'
show_in_toolbar = True show_in_toolbar = True
effect = None effect = None
wait_for_result = None
@property @property
def name(self): def name(self):
...@@ -547,19 +550,57 @@ class OperationView(RedirectToLoginMixin, DetailView): ...@@ -547,19 +550,57 @@ class OperationView(RedirectToLoginMixin, DetailView):
self.check_auth() self.check_auth()
return super(OperationView, self).get(request, *args, **kwargs) return super(OperationView, self).get(request, *args, **kwargs)
def get_response_data(self, result, done, extra=None, **kwargs):
"""Return serializable data to return to agents requesting json
response to POST"""
if extra is None:
extra = {}
extra["success"] = not isinstance(result, Exception)
extra["done"] = done
if isinstance(result, HumanReadableObject):
extra["message"] = result.get_user_text()
return extra
def post(self, request, extra=None, *args, **kwargs): def post(self, request, extra=None, *args, **kwargs):
self.check_auth() self.check_auth()
self.object = self.get_object() self.object = self.get_object()
if extra is None: if extra is None:
extra = {} extra = {}
result = None
done = False
try: try:
self.get_op().async(user=request.user, **extra) task = self.get_op().async(user=request.user, **extra)
except Exception as e: except Exception as e:
messages.error(request, _('Could not start operation.')) messages.error(request, _('Could not start operation.'))
logger.exception(e) logger.exception(e)
result = e
else: else:
messages.success(request, _('Operation is started.')) wait = self.wait_for_result
return redirect("%s#activity" % self.object.get_absolute_url()) if wait:
try:
result = task.get(timeout=wait,
interval=min((wait / 5, .5)))
except TimeoutError:
logger.debug("Result didn't arrive in %ss",
self.wait_for_result, exc_info=True)
except Exception as e:
messages.error(request, _('Operation failed.'))
logger.debug("Operation failed.", exc_info=True)
result = e
else:
done = True
messages.success(request, _('Operation succeeded.'))
if result is None and not done:
messages.success(request, _('Operation is started.'))
if "/json" in request.META.get("HTTP_ACCEPT", ""):
data = self.get_response_data(result, done,
post_extra=extra, **kwargs)
return HttpResponse(json.dumps(data),
content_type="application/json")
else:
return redirect("%s#activity" % self.object.get_absolute_url())
@classmethod @classmethod
def factory(cls, op, icon='cog', effect='info', extra_bases=(), **kwargs): def factory(cls, op, icon='cog', effect='info', extra_bases=(), **kwargs):
...@@ -817,6 +858,7 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView): ...@@ -817,6 +858,7 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
effect = 'info' effect = 'info'
show_in_toolbar = False show_in_toolbar = False
form_class = VmRenewForm form_class = VmRenewForm
wait_for_result = 0.5
def get_form_kwargs(self): def get_form_kwargs(self):
choices = Lease.get_objects_with_level("user", self.request.user) choices = Lease.get_objects_with_level("user", self.request.user)
...@@ -829,6 +871,13 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView): ...@@ -829,6 +871,13 @@ class VmRenewView(FormOperationMixin, TokenOperationView, VmOperationView):
val.update({'choices': choices, 'default': default}) val.update({'choices': choices, 'default': default})
return val return val
def get_response_data(self, result, done, extra=None, **kwargs):
extra = super(VmRenewView, self).get_response_data(result, done,
extra, **kwargs)
extra["new_suspend_time"] = unicode(self.get_op().
instance.time_of_suspend)
return extra
vm_ops = OrderedDict([ vm_ops = OrderedDict([
('deploy', VmOperationView.factory( ('deploy', VmOperationView.factory(
......
...@@ -687,6 +687,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -687,6 +687,11 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
failed.append((u, e)) failed.append((u, e))
else: else:
success.append(u) success.append(u)
if self.status == "RUNNING":
token = VmRenewView.get_token_url(self, self.owner)
queue = self.get_remote_queue_name("agent")
agent_tasks.send_expiration.apply_async(
queue=queue, args=(self.vm_name, token))
return True return True
def is_expiring(self, threshold=0.1): def is_expiring(self, threshold=0.1):
......
...@@ -71,3 +71,8 @@ def del_keys(vm, keys): ...@@ -71,3 +71,8 @@ def del_keys(vm, keys):
@celery.task(name='agent.get_keys') @celery.task(name='agent.get_keys')
def get_keys(vm): def get_keys(vm):
pass pass
@celery.task(name='agent.send_expiration')
def send_expiration(vm, url):
pass
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