Commit 21a68f69 by Őry Máté

Merge branch 'feature-operations-view'

Add operations to vm-details views

* vm-detail context
* vm-detail templates
* replace op buttons at ajax status update
* add tests
* re-implement migrate op
parents 61c537bd e9c09351
...@@ -13,6 +13,7 @@ class Operation(object): ...@@ -13,6 +13,7 @@ class Operation(object):
""" """
async_queue = 'localhost.man' async_queue = 'localhost.man'
required_perms = () required_perms = ()
do_not_call_in_templates = True
def __call__(self, **kwargs): def __call__(self, **kwargs):
return self.call(**kwargs) return self.call(**kwargs)
...@@ -127,6 +128,19 @@ class OperatedMixin(object): ...@@ -127,6 +128,19 @@ class OperatedMixin(object):
raise AttributeError("%r object has no attribute %r" % raise AttributeError("%r object has no attribute %r" %
(self.__class__.__name__, name)) (self.__class__.__name__, name))
def get_available_operations(self, user):
"""Yield Operations that match permissions of user and preconditions.
"""
for name in getattr(self, operation_registry_name, {}):
try:
op = getattr(self, name)
op.check_auth(user)
op.check_precond()
except:
pass # unavailable
else:
yield op
def register_operation(op_cls, op_id=None, target_cls=None): def register_operation(op_cls, op_id=None, target_cls=None):
"""Register the specified operation with the target class. """Register the specified operation with the target class.
......
from __future__ import absolute_import
from datetime import timedelta from datetime import timedelta
from django.contrib.auth.models import User from django.contrib.auth.models import User
......
from __future__ import absolute_import
from itertools import chain from itertools import chain
from logging import getLogger from logging import getLogger
......
...@@ -2,22 +2,21 @@ ...@@ -2,22 +2,21 @@
$(function() { $(function() {
/* vm migrate */ /* vm operations */
$('.vm-migrate').click(function(e) { $('#ops').on('click', '.operation.btn', function(e) {
var icon = $(this).children("i"); var icon = $(this).children("i").addClass('icon-spinner icon-spin');
var vm = $(this).data("vm-pk");
icon.removeClass("icon-truck").addClass("icon-spinner icon-spin");
$.ajax({ $.ajax({
type: 'GET', type: 'GET',
url: '/dashboard/vm/' + vm + '/migrate/', url: $(this).attr('href'),
success: function(data) { success: function(data) {
icon.addClass("icon-truck").removeClass("icon-spinner icon-spin"); icon.removeClass("icon-spinner icon-spin");
$('body').append(data); $('body').append(data);
$('#create-modal').modal('show'); $('#confirmation-modal').modal('show');
$('#create-modal').on('hidden.bs.modal', function() { $('#confirmation-modal').on('hidden.bs.modal', function() {
$('#create-modal').remove(); $('#confirmation-modal').remove();
}); });
$('#vm-migrate-node-list li').click(function(e) { $('#vm-migrate-node-list li').click(function(e) {
var li = $(this).closest('li'); var li = $(this).closest('li');
if (li.find('input').attr('disabled')) if (li.find('input').attr('disabled'))
......
...@@ -211,6 +211,7 @@ function checkNewActivity(only_status, runs) { ...@@ -211,6 +211,7 @@ function checkNewActivity(only_status, runs) {
success: function(data) { success: function(data) {
if(!only_status) { if(!only_status) {
$("#activity-timeline").html(data['activities']); $("#activity-timeline").html(data['activities']);
$("#ops").html(data['ops']);
$("[title]").tooltip(); $("[title]").tooltip();
} }
......
from __future__ import absolute_import
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django_tables2 import Table, A from django_tables2 import Table, A
from django_tables2.columns import (TemplateColumn, Column, BooleanColumn, from django_tables2.columns import (TemplateColumn, Column, BooleanColumn,
......
{% extends "dashboard/base.html" %}
{% load i18n %}
{% block content %}
<div class="body-content">
<div class="panel panel-default" style="margin-top: 60px;">
<div class="panel-heading">
<h3 class="no-margin">
{% if title %}
{{ title }}
{% else %}
{% trans "Confirmation" %}
{% endif %}
</h3>
</div>
<div class="panel-body">
{{ body|safe|default:"(body missing from context.)" }}
</div>
</div>
{% endblock %}
{% load i18n %}
<div class="modal fade" id="confirmation-modal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
{{ body|safe|default:"(body missing from context.)" }}
<div class="clearfix"></div>
</div>
<div class="clearfix"></div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div>
{% extends "dashboard/operate.html" %}
{% load i18n %} {% load i18n %}
{% load sizefieldtags %} {% load sizefieldtags %}
<form method="POST" action="{% url "dashboard.views.vm-migrate" pk=vm.pk %}"> {% block question %}
{% csrf_token %} <p>
<ul id="vm-migrate-node-list"> {% blocktrans with obj=object op=op.name %}
{% with current=vm.node.pk selected=vm.select_node.pk %} Choose a compute node to migrate {{obj}} to.
{% endblocktrans %}
</p>
<p class="text-info">{{op.name}}: {{op.description}}</p>
{% endblock %}
{% block formfields %}
<ul id="vm-migrate-node-list" class="list-unstyled">
{% with current=object.node.pk selected=object.select_node.pk %}
{% for n in nodes %} {% for n in nodes %}
<li class="panel panel-default"><div class="panel-body"> <li class="panel panel-default"><div class="panel-body">
<label for="migrate-to-{{n.pk}}"> <label for="migrate-to-{{n.pk}}">
...@@ -22,5 +31,4 @@ ...@@ -22,5 +31,4 @@
{% endfor %} {% endfor %}
{% endwith %} {% endwith %}
</ul> </ul>
<button type="submit" class="btn btn-primary btn-sm"><i class="icon-truck"></i> Migrate</button> {% endblock %}
</form>
{% load i18n %}
{% block question %}
<p>
{% blocktrans with obj=object op=op.name %}
Do you want to do the following operation on {{obj}}:
<strong>{{op}}</strong>?
{% endblocktrans %}
</p>
<p class="text-info">{{op.name}}: {{op.description}}</p>
{% endblock %}
<form method="POST" action="{{url}}">{% csrf_token %}
{% block formfields %}{% endblock %}
<div class="pull-right">
<a class="btn btn-default" href="{{object.get_absolute_url}}"
data-dismiss="modal">{% trans "Cancel" %}</a>
<button class="btn btn-danger" type="submit">{% if op.icon %}<i class="icon-{{op.icon}}"></i> {% endif %}{{ op|capfirst }}</button>
</div>
</form>
...@@ -6,54 +6,8 @@ ...@@ -6,54 +6,8 @@
{% block content %} {% block content %}
<div class="body-content"> <div class="body-content">
<div class="page-header"> <div class="page-header">
<div class="pull-right" style="padding-top: 15px;"> <div class="pull-right" style="padding-top: 15px;" id="ops">
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}"> {% include "dashboard/vm-detail/_operations.html" %}
{% csrf_token %}
<input type="hidden" name="sleep" />
<button title="{% trans "Sleep" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-moon"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="deploy" />
<button title="{% trans "Deploy" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-play"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="wake_up" />
<button title="{% trans "Wake up" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-sun"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="shut_down" />
<button title="{% trans "Shut down" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-off"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="reboot" />
<button title="{% trans "Reboot (ctrl + alt + del)" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-refresh"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="reset" />
<button title="{% trans "Reset (power cycle)" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-bolt"></i></button>
</form>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="shut_off"/>
<button title="{% trans "Shut off" %}" class="btn btn-default btn-xs" type="submit">
<i class="icon-ban-circle"></i>
</button>
</form>
<a title="Migrate" data-vm-pk="{{ instance.pk }}" href="{% url "dashboard.views.vm-migrate" pk=instance.pk %}" class="btn btn-default btn-xs vm-migrate">
<i class="icon-truck"></i>
</a>
<form style="display: inline;" method="POST" action="{% url "dashboard.views.detail" pk=instance.pk %}">
{% csrf_token %}
<input type="hidden" name="save_as" />
<button title="{% trans "Save as template" %}" class="btn btn-default btn-xs" type="submit"><i class="icon-save"></i></button>
</form>
<a title="{% trans "Destroy" %}" href="{% url "dashboard.views.delete-vm" pk=instance.pk %}" class="btn btn-default btn-xs vm-delete" data-vm-pk="{{ instance.pk }}"><i class="icon-remove"></i></a>
<a title="{% trans "Help" %}" href="#" class="btn btn-default btn-xs vm-details-help-button"><i class="icon-question"></i></a>
</div> </div>
<h1> <h1>
<div id="vm-details-rename"> <div id="vm-details-rename">
...@@ -68,46 +22,6 @@ ...@@ -68,46 +22,6 @@
</div> </div>
<small>{{ instance.primary_host.get_fqdn }}</small> <small>{{ instance.primary_host.get_fqdn }}</small>
</h1> </h1>
<div class="vm-details-help js-hidden">
<ul style="list-style: none;">
<li>
<strong>{% trans "Sleep" %}:</strong>
{% trans "Suspend virtual machine with memory dump." %}
</li>
<li>
<strong>{% trans "Wake up" %}:</strong>
{% trans "Wake up suspended machine." %}
</li>
<li>
<strong>{% trans "Shutdown" %}:</strong>
{% trans "Shutdown virtual machine with ACPI signal." %}
</li>
<li>
<strong>{% trans "Reboot (ctrl + alt + del)" %}:</strong>
{% trans "Reboot virtual machine with Ctrl+Alt+Del signal." %}
</li>
<li>
<strong>{% trans "Reset (power cycle)" %}:</strong>
{% trans "Reset virtual machine (reset button)" %}
</li>
<li>
<strong>{% trans "Shut off" %}:</strong>
{% trans "Shut off VM. (plug-out)" %}
</li>
<li>
<strong>{% trans "Migrate" %}:</strong>
{% trans "Live migrate running vm to another node." %}
</li>
<li>
<strong>{% trans "Save as template" %}:</strong>
{% trans "Shut down the virtual machine, and save it as a new template." %}
</li>
<li>
<strong>{% trans "Destroy" %}:</strong>
{% trans "Remove virtual machine and its networks." %}
</li>
</ul>
</div>
<div style="clear: both;"></div> <div style="clear: both;"></div>
</div> </div>
<div class="row"> <div class="row">
......
{% load i18n %}
{% for op in ops %}
<a href="{{op.get_url}}" class="operation operation-{{op.op}} btn btn-default btn-xs"
title="{{op.name}}: {{op.description}}">
<i class="icon-{{op.icon}}"></i>
<span class="sr-only">{{op.name}}</span>
</a>
{% endfor %}
...@@ -3,10 +3,11 @@ from factory import Factory, Sequence ...@@ -3,10 +3,11 @@ from factory import Factory, Sequence
from mock import patch, MagicMock from mock import patch, MagicMock
from django.contrib.auth.models import User from django.contrib.auth.models import User
# from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.http import HttpRequest, Http404 from django.http import HttpRequest, Http404
from dashboard.views import InstanceActivityDetail, InstanceActivity from ..views import InstanceActivityDetail, InstanceActivity
from ..views import vm_ops, Instance
class ViewUserTestCase(unittest.TestCase): class ViewUserTestCase(unittest.TestCase):
...@@ -36,6 +37,81 @@ class ViewUserTestCase(unittest.TestCase): ...@@ -36,6 +37,81 @@ class ViewUserTestCase(unittest.TestCase):
self.assertEquals(view(request, pk=1234).render().status_code, 200) self.assertEquals(view(request, pk=1234).render().status_code, 200)
class VmOperationViewTestCase(unittest.TestCase):
def test_available(self):
request = FakeRequestFactory(superuser=True)
view = vm_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.destroy = Instance._ops['destroy'](inst)
go.return_value = inst
self.assertEquals(
view.as_view()(request, pk=1234).render().status_code, 200)
def test_unpermitted(self):
request = FakeRequestFactory()
view = vm_ops['destroy']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.destroy = Instance._ops['destroy'](inst)
inst.has_level.return_value = False
go.return_value = inst
with self.assertRaises(PermissionDenied):
view.as_view()(request, pk=1234).render()
def test_migrate(self):
request = FakeRequestFactory(POST={'node': 1})
view = vm_ops['migrate']
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.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.has_level.return_value = True
go.return_value = inst
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location']
assert not msg.error.called
def test_migrate_failed(self):
request = FakeRequestFactory(POST={'node': 1})
view = vm_ops['migrate']
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.migrate = Instance._ops['migrate'](inst)
inst.migrate.async = MagicMock()
inst.migrate.async.side_effect = Exception
inst.has_level.return_value = True
go.return_value = inst
go4.return_value = MagicMock()
assert view.as_view()(request, pk=1234)['location']
assert msg.error.called
def test_migrate_template(self):
request = FakeRequestFactory()
view = vm_ops['migrate']
with patch.object(view, 'get_object') as go:
inst = MagicMock(spec=Instance)
inst._meta.object_name = "Instance"
inst.migrate = Instance._ops['migrate'](inst)
inst.has_level.return_value = True
go.return_value = inst
self.assertEquals(
view.as_view()(request, pk=1234).render().status_code, 200)
def FakeRequestFactory(*args, **kwargs): def FakeRequestFactory(*args, **kwargs):
''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for ''' FakeRequestFactory, FakeMessages and FakeRequestContext are good for
mocking out django views; they are MUCH faster than the Django test client. mocking out django views; they are MUCH faster than the Django test client.
...@@ -48,12 +124,12 @@ def FakeRequestFactory(*args, **kwargs): ...@@ -48,12 +124,12 @@ def FakeRequestFactory(*args, **kwargs):
request = HttpRequest() request = HttpRequest()
request.user = user request.user = user
request.session = kwargs.get('session', {}) request.session = kwargs.get('session', {})
if kwargs.get('POST'): if kwargs.get('POST') is not None:
request.method = 'POST' request.method = 'POST'
request.POST = kwargs.get('POST') request.POST = kwargs.get('POST')
else: else:
request.method = 'GET' request.method = 'GET'
request.POST = kwargs.get('GET', {}) request.GET = kwargs.get('GET', {})
return request return request
......
...@@ -495,9 +495,11 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -495,9 +495,11 @@ class VmDetailTest(LoginMixin, TestCase):
mock_method.side_effect = inst.wake_up mock_method.side_effect = inst.wake_up
inst.manual_state_change('RUNNING') inst.manual_state_change('RUNNING')
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
self.assertRaises(inst.WrongStateError, c.post, with patch('dashboard.views.messages') as msg:
"/dashboard/vm/1/", {'wake_up': True}) c.post("/dashboard/vm/1/op/wake_up/")
self.assertEqual(inst.status, 'RUNNING') assert msg.error.called
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'RUNNING') # mocked anyway
assert mock_method.called assert mock_method.called
def test_permitted_wake_up(self): def test_permitted_wake_up(self):
...@@ -511,7 +513,9 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -511,7 +513,9 @@ class VmDetailTest(LoginMixin, TestCase):
inst.get_remote_queue_name = Mock(return_value='test') inst.get_remote_queue_name = Mock(return_value='test')
inst.manual_state_change('SUSPENDED') inst.manual_state_change('SUSPENDED')
inst.set_level(self.u2, 'owner') inst.set_level(self.u2, 'owner')
response = c.post("/dashboard/vm/1/", {'wake_up': True}) 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(response.status_code, 302)
self.assertEqual(inst.status, 'RUNNING') self.assertEqual(inst.status, 'RUNNING')
assert new_wake_up.called assert new_wake_up.called
...@@ -523,8 +527,11 @@ class VmDetailTest(LoginMixin, TestCase): ...@@ -523,8 +527,11 @@ class VmDetailTest(LoginMixin, TestCase):
inst = Instance.objects.get(pk=1) inst = Instance.objects.get(pk=1)
inst.manual_state_change('SUSPENDED') inst.manual_state_change('SUSPENDED')
inst.set_level(self.u2, 'user') inst.set_level(self.u2, 'user')
response = c.post("/dashboard/vm/1/", {'wake_up': True}) with patch('dashboard.views.messages') as msg:
self.assertEqual(response.status_code, 403) response = c.post("/dashboard/vm/1/op/wake_up/")
assert msg.error.called
self.assertEqual(response.status_code, 302)
inst = Instance.objects.get(pk=1)
self.assertEqual(inst.status, 'SUSPENDED') self.assertEqual(inst.status, 'SUSPENDED')
def test_non_existing_template_get(self): def test_non_existing_template_get(self):
......
from django.conf.urls import patterns, url from __future__ import absolute_import
from django.conf.urls import patterns, url, include
from vm.models import Instance from vm.models import Instance
from .views import ( from .views import (
...@@ -34,7 +35,7 @@ urlpatterns = patterns( ...@@ -34,7 +35,7 @@ urlpatterns = patterns(
name="dashboard.views.template-list"), name="dashboard.views.template-list"),
url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(), url(r"^template/delete/(?P<pk>\d+)/$", TemplateDelete.as_view(),
name="dashboard.views.template-delete"), name="dashboard.views.template-delete"),
url(r'^vm/(?P<pk>\d+)/op/', include('dashboard.vm.urls')),
url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(), url(r'^vm/(?P<pk>\d+)/remove_port/(?P<rule>\d+)/$', PortDelete.as_view(),
name='dashboard.views.remove-port'), name='dashboard.views.remove-port'),
url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(), url(r'^vm/(?P<pk>\d+)/$', VmDetailView.as_view(),
......
from django.conf.urls import patterns, url
from ..views import vm_ops
urlpatterns = patterns('',
*(url(r'^%s/$' % op, v.as_view(), name=v.get_urlname())
for op, v in vm_ops.iteritems()))
...@@ -60,6 +60,7 @@ class DeployOperation(InstanceOperation): ...@@ -60,6 +60,7 @@ class DeployOperation(InstanceOperation):
id = 'deploy' id = 'deploy'
name = _("deploy") name = _("deploy")
description = _("Deploy new virtual machine with network.") description = _("Deploy new virtual machine with network.")
icon = 'play'
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'RUNNING' activity.resultant_state = 'RUNNING'
...@@ -96,6 +97,7 @@ class DestroyOperation(InstanceOperation): ...@@ -96,6 +97,7 @@ class DestroyOperation(InstanceOperation):
id = 'destroy' id = 'destroy'
name = _("destroy") name = _("destroy")
description = _("Destroy virtual machine and its networks.") description = _("Destroy virtual machine and its networks.")
icon = 'remove'
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'DESTROYED' activity.resultant_state = 'DESTROYED'
...@@ -136,6 +138,7 @@ class MigrateOperation(InstanceOperation): ...@@ -136,6 +138,7 @@ class MigrateOperation(InstanceOperation):
id = 'migrate' id = 'migrate'
name = _("migrate") name = _("migrate")
description = _("Live migrate running VM to another node.") description = _("Live migrate running VM to another node.")
icon = 'truck'
def _operation(self, activity, user, system, to_node=None, timeout=120): def _operation(self, activity, user, system, to_node=None, timeout=120):
if not to_node: if not to_node:
...@@ -166,6 +169,7 @@ class RebootOperation(InstanceOperation): ...@@ -166,6 +169,7 @@ class RebootOperation(InstanceOperation):
id = 'reboot' id = 'reboot'
name = _("reboot") name = _("reboot")
description = _("Reboot virtual machine with Ctrl+Alt+Del signal.") description = _("Reboot virtual machine with Ctrl+Alt+Del signal.")
icon = 'refresh'
def _operation(self, activity, user, system, timeout=5): def _operation(self, activity, user, system, timeout=5):
self.instance.reboot_vm(timeout=timeout) self.instance.reboot_vm(timeout=timeout)
...@@ -179,6 +183,7 @@ class ResetOperation(InstanceOperation): ...@@ -179,6 +183,7 @@ class ResetOperation(InstanceOperation):
id = 'reset' id = 'reset'
name = _("reset") name = _("reset")
description = _("Reset virtual machine (reset button).") description = _("Reset virtual machine (reset button).")
icon = 'bolt'
def _operation(self, activity, user, system, timeout=5): def _operation(self, activity, user, system, timeout=5):
self.instance.reset_vm(timeout=timeout) self.instance.reset_vm(timeout=timeout)
...@@ -195,6 +200,7 @@ class SaveAsTemplateOperation(InstanceOperation): ...@@ -195,6 +200,7 @@ class SaveAsTemplateOperation(InstanceOperation):
Template can be shared with groups and users. Template can be shared with groups and users.
Users can instantiate Virtual Machines from Templates. Users can instantiate Virtual Machines from Templates.
""") """)
icon = 'save'
def _operation(self, activity, name, user, system, timeout=300, def _operation(self, activity, name, user, system, timeout=300,
with_shutdown=True, **kwargs): with_shutdown=True, **kwargs):
...@@ -260,6 +266,7 @@ class ShutdownOperation(InstanceOperation): ...@@ -260,6 +266,7 @@ class ShutdownOperation(InstanceOperation):
id = 'shutdown' id = 'shutdown'
name = _("shutdown") name = _("shutdown")
description = _("Shutdown virtual machine with ACPI signal.") description = _("Shutdown virtual machine with ACPI signal.")
icon = 'off'
def check_precond(self): def check_precond(self):
super(ShutdownOperation, self).check_precond() super(ShutdownOperation, self).check_precond()
...@@ -289,6 +296,7 @@ class ShutOffOperation(InstanceOperation): ...@@ -289,6 +296,7 @@ class ShutOffOperation(InstanceOperation):
id = 'shut_off' id = 'shut_off'
name = _("shut off") name = _("shut off")
description = _("Shut off VM (plug-out).") description = _("Shut off VM (plug-out).")
icon = 'ban-circle'
def on_commit(self, activity): def on_commit(self, activity):
activity.resultant_state = 'STOPPED' activity.resultant_state = 'STOPPED'
...@@ -315,6 +323,7 @@ class SleepOperation(InstanceOperation): ...@@ -315,6 +323,7 @@ class SleepOperation(InstanceOperation):
id = 'sleep' id = 'sleep'
name = _("sleep") name = _("sleep")
description = _("Suspend virtual machine with memory dump.") description = _("Suspend virtual machine with memory dump.")
icon = 'moon'
def check_precond(self): def check_precond(self):
super(SleepOperation, self).check_precond() super(SleepOperation, self).check_precond()
...@@ -354,6 +363,7 @@ class WakeUpOperation(InstanceOperation): ...@@ -354,6 +363,7 @@ class WakeUpOperation(InstanceOperation):
Power on Virtual Machine and load its memory from dump. Power on Virtual Machine and load its memory from dump.
""") """)
icon = 'sun'
def check_precond(self): def check_precond(self):
super(WakeUpOperation, self).check_precond() super(WakeUpOperation, self).check_precond()
......
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