Commit 83f9b7ce by Bach Dániel

Merge branch 'feature-pass-reset-op'

Conflicts:
	circle/dashboard/static/dashboard/vm-common.js
parents 4684a0a9 e601b2bc
...@@ -98,17 +98,12 @@ ...@@ -98,17 +98,12 @@
</div> </div>
</dd> </dd>
<dd style="font-size: 10px; text-align: right; padding-top: 8px;"> <dd style="font-size: 10px; text-align: right; padding-top: 8px;">
<a id="vm-details-pw-change" href="#">{% trans "Generate new password!" %}</a> <div id="vm-details-pw-reset">
</dd> {% with op=op.password_reset %}{% if op %}
<div id="vm-details-pw-confirm"> {% comment %} TODO Couldn't this use a modal? {% endcomment%} <a href="{{op.get_url}}" class="btn operation btn-default btn-xs" {% if op.disabled %}disabled{% endif %}>{% trans "Generate new password!" %}</a>
<dt> {% endif %}{% endwith %}
{% trans "Are you sure?" %}
</dt>
<dd>
<a href="#" class="vm-details-pw-confirm-choice label label-success" data-choice="1" data-vm="{{ instance.pk }}">{% trans "Yes" %}</a> /
<a href="#" class="vm-details-pw-confirm-choice label label-danger" data-choice="0">{% trans "No" %}</a>
</dd>
</div> </div>
</dd>
</dl> </dl>
<div class="input-group" id="dashboard-vm-details-connect-command"> <div class="input-group" id="dashboard-vm-details-connect-command">
......
...@@ -143,26 +143,13 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -143,26 +143,13 @@ class VmDetailTest(LoginMixin, TestCase):
response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]}) response = c.post('/dashboard/vm/mass-delete/', {'vms': [1]})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
def test_permitted_password_change(self):
c = Client()
self.login(c, "user2")
inst = Instance.objects.get(pk=1)
inst.set_level(self.u2, 'owner')
inst.node = Node.objects.all()[0]
inst.save()
password = inst.pw
response = c.post("/dashboard/vm/1/", {'change_password': True})
self.assertTrue(Instance.get_remote_queue_name.called)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(password, Instance.objects.get(pk=1).pw)
def test_unpermitted_password_change(self): def test_unpermitted_password_change(self):
c = Client() c = Client()
self.login(c, "user2") self.login(c, "user2")
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.set_level(self.u1, 'owner') inst.set_level(self.u1, 'owner')
password = inst.pw password = inst.pw
response = c.post("/dashboard/vm/1/", {'change_password': True}) response = c.post("/dashboard/vm/1/op/password_reset/")
self.assertEqual(response.status_code, 403) self.assertEqual(response.status_code, 403)
self.assertEqual(password, Instance.objects.get(pk=1).pw) self.assertEqual(password, Instance.objects.get(pk=1).pw)
......
...@@ -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
...@@ -304,7 +305,6 @@ class VmDetailView(CheckedDetailView): ...@@ -304,7 +305,6 @@ class VmDetailView(CheckedDetailView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
options = { options = {
'change_password': self.__change_password,
'new_name': self.__set_name, 'new_name': self.__set_name,
'new_description': self.__set_description, 'new_description': self.__set_description,
'new_tag': self.__add_tag, 'new_tag': self.__add_tag,
...@@ -319,19 +319,6 @@ class VmDetailView(CheckedDetailView): ...@@ -319,19 +319,6 @@ class VmDetailView(CheckedDetailView):
raise Http404() raise Http404()
def __change_password(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
self.object.change_password(user=request.user)
messages.success(request, _("Password changed."))
if request.is_ajax():
return HttpResponse("Success.")
else:
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk}))
def __set_name(self, request): def __set_name(self, request):
self.object = self.get_object() self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'): if not self.object.has_level(request.user, 'owner'):
...@@ -486,6 +473,7 @@ class OperationView(RedirectToLoginMixin, DetailView): ...@@ -486,6 +473,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,18 +535,51 @@ class OperationView(RedirectToLoginMixin, DetailView): ...@@ -547,18 +535,51 @@ 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, 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"] = result is not None
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
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:
wait = self.wait_for_result
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:
messages.success(request, _('Operation succeeded.'))
if result is None:
messages.success(request, _('Operation is started.')) messages.success(request, _('Operation is started.'))
if "/json" in request.META.get("HTTP_ACCEPT", ""):
data = self.get_response_data(result, post_extra=extra, **kwargs)
return HttpResponse(json.dumps(data),
content_type="application/json")
else:
return redirect("%s#activity" % self.object.get_absolute_url()) return redirect("%s#activity" % self.object.get_absolute_url())
@classmethod @classmethod
...@@ -586,6 +607,7 @@ class AjaxOperationMixin(object): ...@@ -586,6 +607,7 @@ class AjaxOperationMixin(object):
store.used = True store.used = True
return HttpResponse( return HttpResponse(
json.dumps({'success': True, json.dumps({'success': True,
'with_reload': getattr(self, 'with_reload', False),
'messages': [unicode(m) for m in store]}), 'messages': [unicode(m) for m in store]}),
content_type="application=json" content_type="application=json"
) )
...@@ -860,6 +882,9 @@ vm_ops = OrderedDict([ ...@@ -860,6 +882,9 @@ vm_ops = OrderedDict([
('add_interface', VmAddInterfaceView), ('add_interface', VmAddInterfaceView),
('renew', VmRenewView), ('renew', VmRenewView),
('resources_change', VmResourcesChangeView), ('resources_change', VmResourcesChangeView),
('password_reset', VmOperationView.factory(
op='password_reset', icon='unlock', effect='warning',
show_in_toolbar=False, wait_for_result=0.5, with_reload=True)),
]) ])
......
...@@ -13,6 +13,7 @@ from .instance import InstanceTemplate ...@@ -13,6 +13,7 @@ from .instance import InstanceTemplate
from .instance import Instance from .instance import Instance
from .instance import post_state_changed from .instance import post_state_changed
from .instance import pre_state_changed from .instance import pre_state_changed
from .instance import pwgen
from .network import InterfaceTemplate from .network import InterfaceTemplate
from .network import Interface from .network import Interface
from .node import Node from .node import Node
...@@ -22,5 +23,5 @@ __all__ = [ ...@@ -22,5 +23,5 @@ __all__ = [
'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate', 'NamedBaseResourceConfig', 'VirtualMachineDescModel', 'InstanceTemplate',
'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed', 'Instance', 'instance_activity', 'post_state_changed', 'pre_state_changed',
'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease', 'InterfaceTemplate', 'Interface', 'Trait', 'Node', 'NodeActivity', 'Lease',
'node_activity', 'node_activity', 'pwgen'
] ]
...@@ -43,7 +43,7 @@ from taggit.managers import TaggableManager ...@@ -43,7 +43,7 @@ from taggit.managers import TaggableManager
from acl.models import AclBase from acl.models import AclBase
from common.models import create_readable from common.models import create_readable
from common.operations import OperatedMixin from common.operations import OperatedMixin
from ..tasks import vm_tasks, agent_tasks from ..tasks import vm_tasks
from .activity import (ActivityInProgressError, instance_activity, from .activity import (ActivityInProgressError, instance_activity,
InstanceActivity) InstanceActivity)
from .common import BaseResourceConfigModel, Lease from .common import BaseResourceConfigModel, Lease
...@@ -719,24 +719,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin, ...@@ -719,24 +719,6 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
timezone.now() + lease.suspend_interval, timezone.now() + lease.suspend_interval,
timezone.now() + lease.delete_interval) timezone.now() + lease.delete_interval)
def change_password(self, user=None):
"""Generate new password for the vm
:param self: The virtual machine.
:param user: The user who's issuing the command.
"""
self.pw = pwgen()
with instance_activity(code_suffix='change_password', instance=self,
readable_name=ugettext_noop("change password"),
user=user):
queue = self.get_remote_queue_name("agent")
agent_tasks.change_password.apply_async(queue=queue,
args=(self.vm_name,
self.pw))
self.save()
def select_node(self): def select_node(self):
"""Returns the node the VM should be deployed or migrated to. """Returns the node the VM should be deployed or migrated to.
""" """
......
...@@ -33,8 +33,9 @@ from .tasks.local_tasks import ( ...@@ -33,8 +33,9 @@ from .tasks.local_tasks import (
) )
from .models import ( from .models import (
Instance, InstanceActivity, InstanceTemplate, Interface, Node, Instance, InstanceActivity, InstanceTemplate, Interface, Node,
NodeActivity, NodeActivity, pwgen
) )
from .tasks import agent_tasks
logger = getLogger(__name__) logger = getLogger(__name__)
...@@ -881,3 +882,27 @@ class ResourcesOperation(InstanceOperation): ...@@ -881,3 +882,27 @@ class ResourcesOperation(InstanceOperation):
register_operation(ResourcesOperation) register_operation(ResourcesOperation)
class PasswordResetOperation(InstanceOperation):
activity_code_suffix = 'Password reset'
id = 'password_reset'
name = _("password reset")
description = _("Password reset")
acl_level = "owner"
required_perms = ()
def check_precond(self):
super(PasswordResetOperation, self).check_precond()
if self.instance.status not in ["RUNNING"]:
raise self.instance.WrongStateError(self.instance)
def _operation(self):
self.instance.pw = pwgen()
queue = self.instance.get_remote_queue_name("agent")
agent_tasks.change_password.apply_async(
queue=queue, args=(self.instance.vm_name, self.instance.pw))
self.instance.save()
register_operation(PasswordResetOperation)
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