Commit 2c621f0f by Őry Máté

Merge branch 'feature-operations-hre' into 'master'

Handle and use HumanReadableExceptions
parents 8f0c13b8 0385e921
......@@ -23,6 +23,7 @@ from logging import getLogger
from time import time
from warnings import warn
from django.contrib import messages
from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder
......@@ -46,17 +47,24 @@ class WorkerNotFound(Exception):
def activitycontextimpl(act, on_abort=None, on_commit=None):
try:
yield act
except BaseException as e:
# BaseException is the common parent of Exception and
# system-exiting exceptions, e.g. KeyboardInterrupt
try:
yield act
except HumanReadableException as e:
result = e
raise
except BaseException as e:
# BaseException is the common parent of Exception and
# system-exiting exceptions, e.g. KeyboardInterrupt
result = create_readable(
ugettext_noop("Failure."),
ugettext_noop("Unhandled exception: %(error)s"),
error=unicode(e))
raise
except:
logger.exception("Failed activity %s" % unicode(act))
handler = None if on_abort is None else lambda a: on_abort(a, e)
result = create_readable(ugettext_noop("Failure."),
ugettext_noop("Unhandled exception: "
"%(error)s"),
error=unicode(e))
act.finish(succeeded=False, result=result, event_handler=handler)
raise e
raise
else:
act.finish(succeeded=True, event_handler=on_commit)
......@@ -196,6 +204,10 @@ class ActivityModel(TimeStampedModel):
DeprecationWarning, stacklevel=2)
value = create_readable(user_text_template="",
admin_text_template=value)
elif not hasattr(value, "to_dict"):
warn("Use HumanReadableObject.", DeprecationWarning, stacklevel=2)
value = create_readable(user_text_template="",
admin_text_template=unicode(value))
self.result_data = None if value is None else value.to_dict()
......@@ -361,8 +373,9 @@ class HumanReadableObject(object):
@classmethod
def create(cls, user_text_template, admin_text_template=None, **params):
return cls(user_text_template,
admin_text_template or user_text_template, params)
return cls(user_text_template=user_text_template,
admin_text_template=(admin_text_template
or user_text_template), params=params)
def set(self, user_text_template, admin_text_template=None, **params):
self._set_values(user_text_template,
......@@ -407,10 +420,28 @@ create_readable = HumanReadableObject.create
class HumanReadableException(HumanReadableObject, Exception):
"""HumanReadableObject that is an Exception so can used in except clause.
"""
pass
def __init__(self, level=None, *args, **kwargs):
super(HumanReadableException, self).__init__(*args, **kwargs)
if level is not None:
if hasattr(messages, level):
self.level = level
else:
raise ValueError(
"Level should be the name of an attribute of django."
"contrib.messages (and it should be callable with "
"(request, message)). Like 'error', 'warning'.")
else:
self.level = "error"
def send_message(self, request, level=None):
if request.user and request.user.is_superuser:
msg = self.get_admin_text()
else:
msg = self.get_user_text()
getattr(messages, level or self.level)(request, msg)
def humanize_exception(message, exception=None, **params):
def humanize_exception(message, exception=None, level=None, **params):
"""Return new dynamic-class exception which is based on
HumanReadableException and the original class with the dict of exception.
......@@ -419,8 +450,10 @@ def humanize_exception(message, exception=None, **params):
...
Welcome!
"""
Ex = type("HumanReadable" + type(exception).__name__,
(HumanReadableException, type(exception)),
exception.__dict__)
return Ex.create(message, **params)
ex = Ex.create(message, **params)
if level:
ex.level = level
return ex
......@@ -529,36 +529,38 @@ class VmDetailTest(LoginMixin, TestCase):
def test_permitted_wake_up_wrong_state(self):
c = Client()
self.login(c, "user2")
with patch.object(WakeUpOperation, 'async') as mock_method:
with patch.object(WakeUpOperation, 'async') as mock_method, \
patch.object(Instance.WrongStateError, 'send_message') as wro:
inst = Instance.objects.get(pk=1)
mock_method.side_effect = inst.wake_up
inst.status = 'RUNNING'
inst.set_level(self.u2, 'owner')
with patch('dashboard.views.messages') as msg:
c.post("/dashboard/vm/1/op/wake_up/")
assert msg.error.called
c.post("/dashboard/vm/1/op/wake_up/")
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'RUNNING') # mocked anyway
assert mock_method.called
assert wro.called
def test_permitted_wake_up(self):
c = Client()
self.login(c, "user2")
with patch.object(Instance, 'select_node', return_value=None):
with patch.object(WakeUpOperation, 'async') as new_wake_up:
with patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa:
inst = Instance.objects.get(pk=1)
new_wake_up.side_effect = inst.wake_up
inst.get_remote_queue_name = Mock(return_value='test')
inst.status = 'SUSPENDED'
inst.set_level(self.u2, 'owner')
with patch('dashboard.views.messages') as msg:
response = c.post("/dashboard/vm/1/op/wake_up/")
assert not msg.error.called
self.assertEqual(response.status_code, 302)
self.assertEqual(inst.status, 'RUNNING')
assert new_wake_up.called
assert wuaa.called
with patch.object(Instance, 'select_node', return_value=None), \
patch.object(WakeUpOperation, 'async') as new_wake_up, \
patch('vm.tasks.vm_tasks.wake_up.apply_async') as wuaa, \
patch.object(Instance.WrongStateError, 'send_message') as wro:
inst = Instance.objects.get(pk=1)
new_wake_up.side_effect = inst.wake_up
inst.get_remote_queue_name = Mock(return_value='test')
inst.status = 'SUSPENDED'
inst.set_level(self.u2, 'owner')
with patch('dashboard.views.messages') as msg:
response = c.post("/dashboard/vm/1/op/wake_up/")
assert not msg.error.called
self.assertEqual(response.status_code, 302)
self.assertEqual(inst.status, 'RUNNING')
assert new_wake_up.called
assert wuaa.called
assert not wro.called
def test_unpermitted_wake_up(self):
c = Client()
......
......@@ -71,7 +71,7 @@ from .tables import (
NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable,
GroupListTable, UserKeyListTable
)
from common.models import HumanReadableObject
from common.models import HumanReadableObject, HumanReadableException
from vm.models import (
Instance, instance_activity, InstanceActivity, InstanceTemplate, Interface,
InterfaceTemplate, Lease, Node, NodeActivity, Trait,
......@@ -562,9 +562,13 @@ class OperationView(RedirectToLoginMixin, DetailView):
done = False
try:
task = self.get_op().async(user=request.user, **extra)
except HumanReadableException as e:
e.send_message(request)
logger.exception("Could not start operation")
result = e
except Exception as e:
messages.error(request, _('Could not start operation.'))
logger.exception(e)
logger.exception("Could not start operation")
result = e
else:
wait = self.wait_for_result
......@@ -575,6 +579,10 @@ class OperationView(RedirectToLoginMixin, DetailView):
except TimeoutError:
logger.debug("Result didn't arrive in %ss",
self.wait_for_result, exc_info=True)
except HumanReadableException as e:
e.send_message(request)
logger.exception(e)
result = e
except Exception as e:
messages.error(request, _('Operation failed.'))
logger.debug("Operation failed.", exc_info=True)
......
......@@ -41,7 +41,7 @@ from model_utils.models import TimeStampedModel, StatusModel
from taggit.managers import TaggableManager
from acl.models import AclBase
from common.models import create_readable
from common.models import create_readable, HumanReadableException
from common.operations import OperatedMixin
from ..tasks import vm_tasks, agent_tasks
from .activity import (ActivityInProgressError, instance_activity,
......@@ -276,28 +276,26 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
verbose_name = _('instance')
verbose_name_plural = _('instances')
class InstanceDestroyedError(Exception):
class InstanceError(HumanReadableException):
def __init__(self, instance, message=None):
if message is None:
message = ("The instance (%s) has already been destroyed."
% instance)
def __init__(self, instance, params=None, level=None, **kwargs):
kwargs.update(params or {})
self.instance = kwargs["instance"] = instance
super(Instance.InstanceError, self).__init__(
level, self.message, self.message, kwargs)
Exception.__init__(self, message)
class InstanceDestroyedError(InstanceError):
message = ugettext_noop(
"Instance %(instance)s has already been destroyed.")
self.instance = instance
class WrongStateError(InstanceError):
message = ugettext_noop(
"Current state (%(state)s) of instance %(instance)s is "
"inappropriate for the invoked operation.")
class WrongStateError(Exception):
def __init__(self, instance, message=None):
if message is None:
message = ("The instance's current state (%s) is "
"inappropriate for the invoked operation."
% instance.status)
Exception.__init__(self, message)
self.instance = instance
def __init__(self, instance, params=None, **kwargs):
super(Instance.WrongStateError, self).__init__(
instance, params, state=instance.status)
def __unicode__(self):
parts = (self.name, "(" + str(self.id) + ")")
......
......@@ -26,7 +26,7 @@ from django.utils.translation import ugettext_lazy as _, ugettext_noop
from celery.exceptions import TimeLimitExceeded
from common.models import create_readable
from common.models import create_readable, humanize_exception
from common.operations import Operation, register_operation
from .tasks.local_tasks import (
abortable_async_instance_operation, abortable_async_node_operation,
......@@ -45,6 +45,8 @@ class InstanceOperation(Operation):
async_operation = abortable_async_instance_operation
host_cls = Instance
concurrency_check = True
accept_states = None
deny_states = None
def __init__(self, instance):
super(InstanceOperation, self).__init__(subject=instance)
......@@ -53,11 +55,26 @@ class InstanceOperation(Operation):
def check_precond(self):
if self.instance.destroyed_at:
raise self.instance.InstanceDestroyedError(self.instance)
if self.accept_states:
if self.instance.status not in self.accept_states:
logger.debug("precond failed for %s: %s not in %s",
unicode(self.__class__),
unicode(self.instance.status),
unicode(self.accept_states))
raise self.instance.WrongStateError(self.instance)
if self.deny_states:
if self.instance.status in self.deny_states:
logger.debug("precond failed for %s: %s in %s",
unicode(self.__class__),
unicode(self.instance.status),
unicode(self.accept_states))
raise self.instance.WrongStateError(self.instance)
def check_auth(self, user):
if not self.instance.has_level(user, self.acl_level):
raise PermissionDenied("%s doesn't have the required ACL level." %
user)
raise humanize_exception(ugettext_noop(
"%(acl_level)s level is required for this operation."),
PermissionDenied(), acl_level=self.acl_level)
super(InstanceOperation, self).check_auth(user=user)
......@@ -94,6 +111,7 @@ class AddInterfaceOperation(InstanceOperation):
description = _("Add a new network interface for the specified VLAN to "
"the VM.")
required_perms = ()
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
def rollback(self, net, activity):
with activity.sub_activity(
......@@ -102,14 +120,11 @@ class AddInterfaceOperation(InstanceOperation):
net.destroy()
net.delete()
def check_precond(self):
super(AddInterfaceOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance)
def _operation(self, activity, user, system, vlan, managed=None):
if not vlan.has_level(user, 'user'):
raise PermissionDenied()
raise humanize_exception(ugettext_noop(
"User acces to vlan %(vlan)s is required."),
PermissionDenied(), vlan=vlan)
if managed is None:
managed = vlan.managed
......@@ -141,11 +156,7 @@ class CreateDiskOperation(InstanceOperation):
name = _("create disk")
description = _("Create empty disk for the VM.")
required_perms = ('storage.create_empty_disk', )
def check_precond(self):
super(CreateDiskOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
def _operation(self, user, size, activity, name=None):
from storage.models import Disk
......@@ -183,11 +194,7 @@ class DownloadDiskOperation(InstanceOperation):
abortable = True
has_percentage = True
required_perms = ('storage.download_disk', )
def check_precond(self):
super(DownloadDiskOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
def _operation(self, user, url, task, activity, name=None):
activity.result = url
......@@ -218,11 +225,7 @@ class DeployOperation(InstanceOperation):
name = _("deploy")
description = _("Deploy new virtual machine with network.")
required_perms = ()
def check_precond(self):
super(DeployOperation, self).check_precond()
if self.instance.status in ['RUNNING', 'SUSPENDED']:
raise self.instance.WrongStateError(self.instance)
deny_states = ('SUSPENDED', 'RUNNING')
def is_preferred(self):
return self.instance.status in (self.instance.STATUS.STOPPED,
......@@ -323,6 +326,7 @@ class MigrateOperation(InstanceOperation):
name = _("migrate")
description = _("Live migrate running VM to another node.")
required_perms = ()
accept_states = ('RUNNING', )
def rollback(self, activity):
with activity.sub_activity(
......@@ -330,11 +334,6 @@ class MigrateOperation(InstanceOperation):
"redeploy network (rollback)")):
self.instance.deploy_net()
def check_precond(self):
super(MigrateOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
def check_auth(self, user):
if not user.is_superuser:
raise PermissionDenied()
......@@ -384,11 +383,7 @@ class RebootOperation(InstanceOperation):
name = _("reboot")
description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
required_perms = ()
def check_precond(self):
super(RebootOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('RUNNING', )
def _operation(self, timeout=5):
self.instance.reboot_vm(timeout=timeout)
......@@ -403,11 +398,7 @@ class RemoveInterfaceOperation(InstanceOperation):
name = _("remove interface")
description = _("Remove the specified network interface from the VM.")
required_perms = ()
def check_precond(self):
super(RemoveInterfaceOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
def _operation(self, activity, user, system, interface):
if self.instance.is_running:
......@@ -428,11 +419,7 @@ class RemoveDiskOperation(InstanceOperation):
name = _("remove disk")
description = _("Remove the specified disk from the VM.")
required_perms = ()
def check_precond(self):
super(RemoveDiskOperation, self).check_precond()
if self.instance.status not in ['STOPPED', 'PENDING', 'RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('STOPPED', 'PENDING', 'RUNNING')
def _operation(self, activity, user, system, disk):
if self.instance.is_running and disk.type not in ["iso"]:
......@@ -450,11 +437,7 @@ class ResetOperation(InstanceOperation):
name = _("reset")
description = _("Reset virtual machine (reset button).")
required_perms = ()
def check_precond(self):
super(ResetOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('RUNNING', )
def _operation(self, timeout=5):
self.instance.reset_vm(timeout=timeout)
......@@ -473,6 +456,7 @@ class SaveAsTemplateOperation(InstanceOperation):
""")
abortable = True
required_perms = ('vm.create_template', )
accept_states = ('RUNNING', 'PENDING', 'STOPPED')
def is_preferred(self):
return (self.instance.is_base and
......@@ -493,11 +477,6 @@ class SaveAsTemplateOperation(InstanceOperation):
for disk in self.disks:
disk.destroy()
def check_precond(self):
super(SaveAsTemplateOperation, self).check_precond()
if self.instance.status not in ['RUNNING', 'PENDING', 'STOPPED']:
raise self.instance.WrongStateError(self.instance)
def _operation(self, activity, user, system, timeout=300, name=None,
with_shutdown=True, task=None, **kwargs):
if with_shutdown:
......@@ -567,11 +546,7 @@ class ShutdownOperation(InstanceOperation):
description = _("Shutdown virtual machine with ACPI signal.")
abortable = True
required_perms = ()
def check_precond(self):
super(ShutdownOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('RUNNING', )
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
......@@ -591,11 +566,7 @@ class ShutOffOperation(InstanceOperation):
name = _("shut off")
description = _("Shut off VM (plug-out).")
required_perms = ()
def check_precond(self):
super(ShutOffOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('RUNNING', )
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
......@@ -623,16 +594,12 @@ class SleepOperation(InstanceOperation):
name = _("sleep")
description = _("Suspend virtual machine with memory dump.")
required_perms = ()
accept_states = ('RUNNING', )
def is_preferred(self):
return (not self.instance.is_base and
self.instance.status == self.instance.STATUS.RUNNING)
def check_precond(self):
super(SleepOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
def on_abort(self, activity, error):
if isinstance(error, TimeLimitExceeded):
activity.resultant_state = None
......@@ -670,15 +637,11 @@ class WakeUpOperation(InstanceOperation):
Power on Virtual Machine and load its memory from dump.
""")
required_perms = ()
accept_states = ('SUSPENDED', )
def is_preferred(self):
return self.instance.status == self.instance.STATUS.SUSPENDED
def check_precond(self):
super(WakeUpOperation, self).check_precond()
if self.instance.status not in ['SUSPENDED']:
raise self.instance.WrongStateError(self.instance)
def on_abort(self, activity, error):
activity.resultant_state = 'ERROR'
......@@ -718,11 +681,6 @@ class RenewOperation(InstanceOperation):
required_perms = ()
concurrency_check = False
def check_precond(self):
super(RenewOperation, self).check_precond()
if self.instance.status == 'DESTROYED':
raise self.instance.WrongStateError(self.instance)
def _operation(self, lease=None):
(self.instance.time_of_suspend,
self.instance.time_of_delete) = self.instance.get_renew_times(lease)
......@@ -790,7 +748,8 @@ class FlushOperation(NodeOperation):
def check_auth(self, user):
if not user.is_superuser:
raise PermissionDenied()
raise humanize_exception(ugettext_noop(
"Superuser privileges are required."), PermissionDenied())
super(FlushOperation, self).check_auth(user=user)
......@@ -815,11 +774,7 @@ class ScreenshotOperation(InstanceOperation):
description = _("Get screenshot")
acl_level = "owner"
required_perms = ()
def check_precond(self):
super(ScreenshotOperation, self).check_precond()
if self.instance.status not in ['RUNNING']:
raise self.instance.WrongStateError(self.instance)
accept_states = ('RUNNING', )
def _operation(self):
return self.instance.get_screenshot(timeout=20)
......@@ -835,10 +790,13 @@ class RecoverOperation(InstanceOperation):
description = _("Recover virtual machine from destroyed state.")
acl_level = "owner"
required_perms = ('vm.recover', )
accept_states = ('DESTROYED', )
def check_precond(self):
if not self.instance.destroyed_at:
raise self.instance.WrongStateError(self.instance)
try:
super(RecoverOperation, self).check_precond()
except Instance.InstanceDestroyedError:
pass
def on_commit(self, activity):
activity.resultant_state = 'PENDING'
......@@ -862,11 +820,7 @@ class ResourcesOperation(InstanceOperation):
description = _("Change resources")
acl_level = "owner"
required_perms = ('vm.change_resources', )
def check_precond(self):
super(ResourcesOperation, self).check_precond()
if self.instance.status not in ["STOPPED", "PENDING"]:
raise self.instance.WrongStateError(self.instance)
accept_states = ('STOPPED', 'PENDING', )
def _operation(self, user, num_cores, ram_size, max_ram_size, priority):
......@@ -889,11 +843,7 @@ class PasswordResetOperation(InstanceOperation):
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)
accept_states = ('RUNNING', )
def _operation(self):
self.instance.pw = pwgen()
......
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