Commit a8fb5a86 by Bach Dániel

Merge branch 'feature-fix-acls'

Conflicts:
	circle/vm/operations.py
parents f7488817 f32bb524
# register a signal do update permissions every migration.
# This is based on app django_extensions update_permissions command
from south.signals import post_migrate
def update_permissions_after_migration(app, **kwargs):
"""
Update app permission just after every migration.
This is based on app django_extensions update_permissions
management command.
"""
from django.conf import settings
from django.db.models import get_app, get_models
from django.contrib.auth.management import create_permissions
create_permissions(get_app(app), get_models(), 2 if settings.DEBUG else 0)
post_migrate.connect(update_permissions_after_migration)
......@@ -20,7 +20,7 @@ from logging import getLogger
from .models import activity_context, has_suffix
from django.core.exceptions import PermissionDenied
from django.core.exceptions import PermissionDenied, ImproperlyConfigured
logger = getLogger(__name__)
......@@ -30,7 +30,7 @@ class Operation(object):
"""Base class for VM operations.
"""
async_queue = 'localhost.man'
required_perms = ()
required_perms = None
do_not_call_in_templates = True
abortable = False
has_percentage = False
......@@ -141,6 +141,9 @@ class Operation(object):
pass
def check_auth(self, user):
if self.required_perms is None:
raise ImproperlyConfigured(
"Set required_perms to () if none needed.")
if not user.has_perms(self.required_perms):
raise PermissionDenied("%s doesn't have the required permissions."
% user)
......
......@@ -1240,6 +1240,24 @@
}
},
{
"pk": 1367,
"model": "auth.permission",
"fields": {
"codename": "create_vm",
"name": "Can create a new VM.",
"content_type": 28
}
},
{
"pk": 1368,
"model": "auth.permission",
"fields": {
"codename": "access_console",
"name": "Can access the graphical console of a VM.",
"content_type": 28
}
},
{
"pk": 1,
"model": "auth.group",
"fields": {
......
......@@ -25,6 +25,7 @@ from django.contrib.auth.forms import (
)
from django.contrib.auth.models import User, Group
from django.core.validators import URLValidator
from django.core.exceptions import PermissionDenied, ValidationError
from crispy_forms.helper import FormHelper
from crispy_forms.layout import (
......@@ -593,6 +594,17 @@ class TemplateForm(forms.ModelForm):
n = self.instance.interface_set.values_list("vlan", flat=True)
self.initial['networks'] = n
self.allowed_fields = (
'name', 'access_method', 'description', 'system', 'tags')
if self.user.has_perm('vm.change_template_resources'):
self.allowed_fields += tuple(set(self.fields.keys()) -
set(['raw_data']))
if self.user.is_superuser:
self.allowed_fields += ('raw_data', )
for name, field in self.fields.items():
if name not in self.allowed_fields:
field.widget.attrs['disabled'] = 'disabled'
if not self.instance.pk and len(self.errors) < 1:
self.instance.priority = 20
self.instance.ram_size = 512
......@@ -603,14 +615,35 @@ class TemplateForm(forms.ModelForm):
return User.objects.get(pk=self.instance.owner.pk)
return self.user
def clean_raw_data(self):
# if raw_data has changed and the user is not superuser
if "raw_data" in self.changed_data and not self.user.is_superuser:
old_raw_data = InstanceTemplate.objects.get(
pk=self.instance.pk).raw_data
return old_raw_data
else:
return self.cleaned_data['raw_data']
def _clean_fields(self):
try:
old = InstanceTemplate.objects.get(pk=self.instance.pk)
except InstanceTemplate.DoesNotExist:
old = None
for name, field in self.fields.items():
if name in self.allowed_fields:
value = field.widget.value_from_datadict(
self.data, self.files, self.add_prefix(name))
try:
if isinstance(field, forms.FileField):
initial = self.initial.get(name, field.initial)
value = field.clean(value, initial)
else:
value = field.clean(value)
self.cleaned_data[name] = value
if hasattr(self, 'clean_%s' % name):
value = getattr(self, 'clean_%s' % name)()
self.cleaned_data[name] = value
except ValidationError as e:
self._errors[name] = self.error_class(e.messages)
if name in self.cleaned_data:
del self.cleaned_data[name]
elif old:
if name == 'networks':
self.cleaned_data[name] = [
i.vlan for i in self.instance.interface_set.all()]
else:
self.cleaned_data[name] = getattr(old, name)
def save(self, commit=True):
data = self.cleaned_data
......@@ -624,6 +657,8 @@ class TemplateForm(forms.ModelForm):
networks = InterfaceTemplate.objects.filter(
template=self.instance).values_list("vlan", flat=True)
for m in data['networks']:
if not m.has_level(self.user, "user"):
raise PermissionDenied()
if m.pk not in networks:
InterfaceTemplate(vlan=m, managed=m.managed,
template=self.instance).save()
......@@ -635,10 +670,6 @@ class TemplateForm(forms.ModelForm):
@property
def helper(self):
kwargs_raw_data = {}
if not self.user.is_superuser:
kwargs_raw_data['readonly'] = None
helper = FormHelper()
helper.layout = Layout(
Field("name"),
......@@ -690,7 +721,7 @@ class TemplateForm(forms.ModelForm):
_("Virtual machine settings"),
Field('access_method'),
Field('boot_menu'),
Field('raw_data', **kwargs_raw_data),
Field('raw_data'),
Field('req_traits'),
Field('description'),
Field("parent", type="hidden"),
......
......@@ -192,6 +192,9 @@
},
mousedown: function(ev) {
if (this.element[0].disabled) {
return false;
}
// Touch: Get the original event:
if (this.touchCapable && ev.type === 'touchstart') {
......
......@@ -11,13 +11,14 @@ $(function() {
/* save resources */
$('#vm-details-resources-save').click(function() {
$('i.icon-save', this).removeClass("icon-save").addClass("icon-refresh icon-spin");
var vm = $(this).data("vm");
$.ajax({
type: 'POST',
url: location.href,
url: "/dashboard/vm/" + vm + "/op/resources_change/",
data: $('#vm-details-resources-form').serialize(),
success: function(data, textStatus, xhr) {
addMessage(data['message'], 'success');
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
$('a[href="#activity"]').trigger("click");
},
error: function(xhr, textStatus, error) {
$("#vm-details-resources-save i").removeClass('icon-refresh icon-spin').addClass("icon-save");
......@@ -330,7 +331,7 @@ function decideActivityRefresh() {
/* unescapes html got via the request, also removes whitespaces and replaces all ' with " */
function unescapeHTML(html) {
return html.replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&').replace(/&ndash;/g, "–").replace(/\//g, "").replace(/'/g, '"').replace(/ /g, '');
return html.replace(/&lt;/g,'<').replace(/&gt;/g,'>').replace(/&amp;/g,'&').replace(/&ndash;/g, "–").replace(/\//g, "").replace(/'/g, '"').replace(/&#39;/g, "'").replace(/ /g, '');
}
/* the html page contains some tags that were modified via js (titles for example), we delete these
......@@ -367,6 +368,14 @@ function checkNewActivity(only_status, runs) {
$("[data-target=#_console]").attr("data-toggle", "_pill").attr("href", "#").parent("li").addClass("disabled");
}
if(data['status'] == "STOPPED") {
$(".enabled-when-stopped").prop("disabled", false);
$(".hide-when-stopped").hide();
} else {
$(".enabled-when-stopped").prop("disabled", true);
$(".hide-when-stopped").show();
}
if(runs > 0 && decideActivityRefresh()) {
setTimeout(
function() {checkNewActivity(only_status, runs + 1)},
......
......@@ -16,10 +16,12 @@
<div class="clearfix"></div>
</div>
{% endfor %}
{% if perms.vm.create_base_template %}
<div class="panel panel-default template-choose-list-element">
<input type="radio" name="parent" value="base_vm"/>
{% trans "Create a new base VM without disk" %}
</div>
{% endif %}
<button type="submit" id="template-choose-next-button" class="btn btn-success pull-right">{% trans "Next" %}</button>
<div class="clearfix"></div>
</div>
......
{% load i18n %}
<div class="btn-toolbar">
{% if perms.vm.access_console %}
<button id="sendCtrlAltDelButton" class="btn btn-danger btn-sm">{% trans "Send Ctrl+Alt+Del" %}</button>
<button id="sendPasswordButton" class="btn btn-default btn-sm">{% trans "Type password" %}</button>
{% endif %}
<button id="getScreenshotButton" class="btn btn-info btn-sm pull-right" data-vm-pk="{{ instance.pk }}"><i class="icon-picture"></i> {% trans "Screenshot" %}</button>
</div>
{% if perms.vm.access_console %}
<div class="alert alert-info" id="noVNC_status">
</div>
{% endif %}
<div id="vm-console-screenshot">
<button class="btn btn-danger btn-sm pull-right">{% trans "Close" %}</button>
......@@ -14,6 +18,7 @@
<hr />
</div>
{% if perms.vm.access_console %}
<canvas id="noVNC_canvas" width="640px" height="20px">Canvas not supported.
</canvas>
......@@ -22,3 +27,4 @@
var INCLUDE_URI = '{{ STATIC_URL }}dashboard/novnc/';
var VNC_URL = "{{ vnc_url }}";
</script>
{% endif %}
......@@ -33,11 +33,20 @@
</div>
</p>
{% if can_change_resources %}
<p class="row">
<div class="col-sm-12">
<button type="submit" class="btn btn-success btn-sm" id="vm-details-resources-save"><i class="icon-save"></i> {% trans "Save resources" %}</button>
<button type="submit" class="btn btn-success btn-sm enabled-when-stopped" id="vm-details-resources-save"
data-vm="{{ instance.pk }}"
{% if not op.resources_change %}disabled{% endif %}>
<i class="icon-save"></i> {% trans "Save resources" %}
</button>
<span class="hide-when-stopped"
{% if op.resources_change %}style="display: none;"{% endif %}
>{% trans "Stop your VM to change resources." %}</span>
</div>
</p>
{% endif %}
</form>
<hr />
......
<a href="{% url "dashboard.views.vm-migrate" pk=record.pk %}" class="btn btn-default btn-xs vm-migrate" data-vm-pk="{{ record.pk }}" title data-original-title="Migrate">
<a href="{% url "dashboard.vm.op.migrate" pk=record.pk %}" class="btn btn-default btn-xs vm-migrate" data-vm-pk="{{ record.pk }}" title data-original-title="Migrate">
<i class="icon-truck"></i>
</a>
<a id="vm-list-rename-button" class="btn btn-default btn-xs" title data-original-title="Rename">
......
......@@ -159,7 +159,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert not msg.error.called
def test_migrate_failed(self):
request = FakeRequestFactory(POST={'node': 1})
request = FakeRequestFactory(POST={'node': 1}, superuser=True)
view = vm_ops['migrate']
with patch.object(view, 'get_object') as go, \
......@@ -177,7 +177,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert msg.error.called
def test_migrate_template(self):
request = FakeRequestFactory()
request = FakeRequestFactory(superuser=True)
view = vm_ops['migrate']
with patch.object(view, 'get_object') as go:
......@@ -190,7 +190,7 @@ class VmOperationViewTestCase(unittest.TestCase):
view.as_view()(request, pk=1234).render().status_code, 200)
def test_save_as_wo_name(self):
request = FakeRequestFactory(POST={})
request = FakeRequestFactory(POST={}, has_perms_mock=True)
view = vm_ops['save_as_template']
with patch.object(view, 'get_object') as go, \
......@@ -224,7 +224,7 @@ class VmOperationViewTestCase(unittest.TestCase):
assert not msg.error.called
def test_save_as_template(self):
request = FakeRequestFactory()
request = FakeRequestFactory(has_perms_mock=True)
view = vm_ops['save_as_template']
with patch.object(view, 'get_object') as go:
......@@ -246,6 +246,8 @@ def FakeRequestFactory(*args, **kwargs):
user = UserFactory()
user.is_authenticated = lambda: kwargs.get('authenticated', True)
user.is_superuser = kwargs.get('superuser', False)
if kwargs.get('has_perms_mock', False):
user.has_perms = MagicMock(return_value=True)
request = HttpRequest()
request.user = user
......
......@@ -63,6 +63,8 @@ class VmDetailTest(LoginMixin, TestCase):
self.g1.user_set.add(self.u1)
self.g1.user_set.add(self.u2)
self.g1.save()
self.u1.user_permissions.add(Permission.objects.get(
codename='create_vm'))
settings["default_vlangroup"] = 'public'
VlanGroup.objects.create(name='public')
......@@ -1544,6 +1546,8 @@ class VmDetailVncTest(LoginMixin, TestCase):
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'operator')
self.u1.user_permissions.add(Permission.objects.get(
codename='access_console'))
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 200)
......@@ -1554,6 +1558,8 @@ class VmDetailVncTest(LoginMixin, TestCase):
inst.node = Node.objects.all()[0]
inst.save()
inst.set_level(self.u1, 'user')
self.u1.user_permissions.add(Permission.objects.get(
codename='access_console'))
response = c.get('/dashboard/vm/1/vnctoken/')
self.assertEqual(response.status_code, 403)
......
......@@ -28,7 +28,7 @@ from .views import (
NotificationView, PortDelete, TemplateAclUpdateView, TemplateCreate,
TemplateDelete, TemplateDetail, TemplateList, TransferOwnershipConfirmView,
TransferOwnershipView, vm_activity, VmCreate, VmDelete, VmDetailView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete, VmMigrateView,
VmDetailVncTokenView, VmGraphView, VmList, VmMassDelete,
VmRenewView, DiskRemoveView, get_disk_download_status, InterfaceDeleteView,
GroupRemoveAclUserView, GroupRemoveAclGroupView, GroupRemoveUserView,
GroupRemoveFutureUserView,
......@@ -83,8 +83,6 @@ urlpatterns = patterns(
url(r'^vm/mass-delete/', VmMassDelete.as_view(),
name='dashboard.view.mass-delete-vm'),
url(r'^vm/(?P<pk>\d+)/activity/$', vm_activity),
url(r'^vm/(?P<pk>\d+)/migrate/$', VmMigrateView.as_view(),
name='dashboard.views.vm-migrate'),
url(r'^vm/(?P<pk>\d+)/renew/((?P<key>.*)/?)$', VmRenewView.as_view(),
name='dashboard.views.vm-renew'),
url(r'^vm/activity/(?P<pk>\d+)/$', InstanceActivityDetail.as_view(),
......
......@@ -244,6 +244,8 @@ class VmDetailVncTokenView(CheckedDetailView):
self.object = self.get_object()
if not self.object.has_level(request.user, 'operator'):
raise PermissionDenied()
if not request.user.has_perm('vm.access_console'):
raise PermissionDenied()
if self.object.node:
with instance_activity(code_suffix='console-accessed',
instance=self.object, user=request.user,
......@@ -294,13 +296,14 @@ class VmDetailView(CheckedDetailView):
if self.request.user.is_superuser:
context['traits_form'] = TraitsForm(instance=instance)
context['raw_data_form'] = RawDataForm(instance=instance)
# resources change perm
context['can_change_resources'] = self.request.user.has_perm(
"vm.change_resources")
return context
def post(self, request, *args, **kwargs):
if (request.POST.get('ram-size') and request.POST.get('cpu-count')
and request.POST.get('cpu-priority')):
return self.__set_resources(request)
options = {
'change_password': self.__change_password,
'new_name': self.__set_name,
......@@ -328,33 +331,6 @@ class VmDetailView(CheckedDetailView):
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk}))
def __set_resources(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
raise PermissionDenied()
if not request.user.has_perm('vm.change_resources'):
raise PermissionDenied()
resources = {
'num_cores': request.POST.get('cpu-count'),
'ram_size': request.POST.get('ram-size'),
'max_ram_size': request.POST.get('ram-size'), # TODO: max_ram
'priority': request.POST.get('cpu-priority')
}
Instance.objects.filter(pk=self.object.pk).update(**resources)
success_message = _("Resources successfully updated.")
if request.is_ajax():
response = {'message': success_message}
return HttpResponse(
json.dumps(response),
content_type="application/json"
)
else:
messages.success(request, success_message)
return redirect(reverse_lazy("dashboard.views.detail",
kwargs={'pk': self.object.pk}))
def __set_name(self, request):
self.object = self.get_object()
if not self.object.has_level(request.user, 'owner'):
......@@ -606,8 +582,9 @@ class VmOperationView(OperationView):
model = Instance
context_object_name = 'instance' # much simpler to mock object
def post(self, request, *args, **kwargs):
resp = super(VmOperationView, self).post(request, *args, **kwargs)
def post(self, request, extra=None, *args, **kwargs):
resp = super(VmOperationView, self).post(request, extra, *args,
**kwargs)
if request.is_ajax():
store = messages.get_messages(request)
store.used = True
......@@ -699,6 +676,29 @@ class VmSaveView(FormOperationMixin, VmOperationView):
effect = 'info'
form_class = VmSaveForm
class VmResourcesChangeView(VmOperationView):
op = 'resources_change'
icon = "save"
show_in_toolbar = False
def post(self, request, extra=None, *args, **kwargs):
if extra is None:
extra = {}
resources = {
'num_cores': "cpu-count",
'priority': "cpu-priority",
'ram_size': "ram-size",
"max_ram_size": "ram-size", # TODO
}
for k, v in resources.iteritems():
extra[k] = request.POST.get(v)
return super(VmResourcesChangeView, self).post(request, extra,
*args, **kwargs)
vm_ops = OrderedDict([
('deploy', VmOperationView.factory(
op='deploy', icon='play', effect='success')),
......@@ -1012,7 +1012,7 @@ class GroupAclUpdateView(AclUpdateView):
kwargs=self.kwargs))
class TemplateChoose(TemplateView):
class TemplateChoose(LoginRequiredMixin, TemplateView):
def get_template_names(self):
if self.request.is_ajax():
......@@ -1045,6 +1045,9 @@ class TemplateChoose(TemplateView):
else:
template = get_object_or_404(InstanceTemplate, pk=template)
if not template.has_level(request.user, "user"):
raise PermissionDenied()
instance = Instance.create_from_template(
template=template, owner=request.user, is_base=True)
......@@ -1072,7 +1075,7 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return context
def get(self, *args, **kwargs):
if not self.request.user.has_perm('vm.create_template'):
if not self.request.user.has_perm('vm.create_base_template'):
raise PermissionDenied()
return super(TemplateCreate, self).get(*args, **kwargs)
......@@ -1083,7 +1086,7 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return kwargs
def post(self, request, *args, **kwargs):
if not self.request.user.has_perm('vm.create_template'):
if not self.request.user.has_perm('vm.create_base_template'):
raise PermissionDenied()
form = self.form_class(request.POST, user=request.user)
......@@ -1105,8 +1108,6 @@ class TemplateCreate(SuccessMessageMixin, CreateView):
return redirect("%s#resources" % inst.get_absolute_url())
return super(TemplateCreate, self).post(self, request, args, kwargs)
def __create_networks(self, vlans, user):
networks = []
for v in vlans:
......@@ -1167,12 +1168,6 @@ class TemplateDetail(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
template = self.get_object()
if not template.has_level(request.user, 'owner'):
raise PermissionDenied()
for disk in self.get_object().disks.all():
if not disk.has_level(request.user, 'user'):
raise PermissionDenied()
for network in self.get_object().interface_set.all():
if not network.vlan.has_level(request.user, "user"):
raise PermissionDenied()
return super(TemplateDetail, self).post(self, request, args, kwargs)
def get_form_kwargs(self):
......@@ -1546,6 +1541,9 @@ class VmCreate(LoginRequiredMixin, TemplateView):
return ['dashboard/nojs-wrapper.html']
def get(self, request, form=None, *args, **kwargs):
if not request.user.has_perm('vm.create_vm'):
raise PermissionDenied()
form_error = form is not None
template = (form.template.pk if form_error
else request.GET.get("template"))
......@@ -1651,6 +1649,9 @@ class VmCreate(LoginRequiredMixin, TemplateView):
def post(self, request, *args, **kwargs):
user = request.user
if not request.user.has_perm('vm.create_vm'):
raise PermissionDenied()
# limit chekcs
try:
limit = user.profile.instance_limit
......
......@@ -106,6 +106,9 @@ class Disk(AclBase, TimeStampedModel):
ordering = ['name']
verbose_name = _('disk')
verbose_name_plural = _('disks')
permissions = (
('create_empty_disk', _('Can create an empty disk.')),
('download_disk', _('Can download a disk.')))
class WrongDiskTypeError(Exception):
......
......@@ -151,6 +151,10 @@ class InstanceTemplate(AclBase, VirtualMachineDescModel, TimeStampedModel):
ordering = ('name', )
permissions = (
('create_template', _('Can create an instance template.')),
('create_base_template',
_('Can create an instance template (base).')),
('change_template_resources',
_('Can change resources of a template.')),
)
verbose_name = _('template')
verbose_name_plural = _('templates')
......@@ -263,6 +267,7 @@ class Instance(AclBase, VirtualMachineDescModel, StatusModel, OperatedMixin,
('access_console', _('Can access the graphical console of a VM.')),
('change_resources', _('Can change resources of a running VM.')),
('set_resources', _('Can change resources of a new VM.')),
('create_vm', _('Can create a new VM.')),
('config_ports', _('Can configure port forwards.')),
('recover', _('Can recover a destroyed VM.')),
)
......
......@@ -42,6 +42,7 @@ class InstanceOperation(Operation):
acl_level = 'owner'
async_operation = abortable_async_instance_operation
host_cls = Instance
concurrency_check = True
def __init__(self, instance):
super(InstanceOperation, self).__init__(subject=instance)
......@@ -73,7 +74,7 @@ class InstanceOperation(Operation):
else:
return InstanceActivity.create(
code_suffix=self.activity_code_suffix, instance=self.instance,
user=user)
user=user, concurrency_check=self.concurrency_check)
def is_preferred(self):
"""If this is the recommended op in the current state of the instance.
......@@ -87,6 +88,7 @@ class AddInterfaceOperation(InstanceOperation):
name = _("add interface")
description = _("Add a new network interface for the specified VLAN to "
"the VM.")
required_perms = ()
def _operation(self, activity, user, system, vlan, managed=None):
if managed is None:
......@@ -109,6 +111,7 @@ class CreateDiskOperation(InstanceOperation):
id = 'create_disk'
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()
......@@ -123,6 +126,7 @@ class CreateDiskOperation(InstanceOperation):
if not name:
name = "new disk"
disk = Disk.create(size=size, name=name, type="qcow2-norm")
disk.full_clean()
self.instance.disks.add(disk)
register_operation(CreateDiskOperation)
......@@ -135,6 +139,7 @@ class DownloadDiskOperation(InstanceOperation):
description = _("Download disk for the VM.")
abortable = True
has_percentage = True
required_perms = ('storage.download_disk', )
def check_precond(self):
super(DownloadDiskOperation, self).check_precond()
......@@ -148,6 +153,7 @@ class DownloadDiskOperation(InstanceOperation):
from storage.models import Disk
disk = Disk.download(url=url, name=name, task=task)
disk.full_clean()
self.instance.disks.add(disk)
register_operation(DownloadDiskOperation)
......@@ -158,6 +164,12 @@ class DeployOperation(InstanceOperation):
id = 'deploy'
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)
def is_preferred(self):
return self.instance.status in (self.instance.STATUS.STOPPED,
......@@ -198,6 +210,7 @@ class DestroyOperation(InstanceOperation):
id = 'destroy'
name = _("destroy")
description = _("Destroy virtual machine and its networks.")
required_perms = ()
def on_commit(self, activity):
activity.resultant_state = 'DESTROYED'
......@@ -239,11 +252,23 @@ class MigrateOperation(InstanceOperation):
id = 'migrate'
name = _("migrate")
description = _("Live migrate running VM to another node.")
required_perms = ()
def rollback(self, activity):
with activity.sub_activity('rollback_net'):
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()
super(MigrateOperation, self).check_auth(user=user)
def _operation(self, activity, to_node=None, timeout=120):
if not to_node:
with activity.sub_activity('scheduling') as sa:
......@@ -278,6 +303,12 @@ class RebootOperation(InstanceOperation):
id = 'reboot'
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)
def _operation(self, timeout=5):
self.instance.reboot_vm(timeout=timeout)
......@@ -291,6 +322,7 @@ class RemoveInterfaceOperation(InstanceOperation):
id = 'remove_interface'
name = _("remove interface")
description = _("Remove the specified network interface from the VM.")
required_perms = ()
def _operation(self, activity, user, system, interface):
if self.instance.is_running:
......@@ -308,6 +340,7 @@ class RemoveDiskOperation(InstanceOperation):
id = 'remove_disk'
name = _("remove disk")
description = _("Remove the specified disk from the VM.")
required_perms = ()
def check_precond(self):
super(RemoveDiskOperation, self).check_precond()
......@@ -328,6 +361,12 @@ class ResetOperation(InstanceOperation):
id = 'reset'
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)
def _operation(self, timeout=5):
self.instance.reset_vm(timeout=timeout)
......@@ -345,6 +384,7 @@ class SaveAsTemplateOperation(InstanceOperation):
Users can instantiate Virtual Machines from Templates.
""")
abortable = True
required_perms = ('vm.create_template', )
def is_preferred(self):
return (self.instance.is_base and
......@@ -365,6 +405,11 @@ 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:
......@@ -435,6 +480,7 @@ class ShutdownOperation(InstanceOperation):
name = _("shutdown")
description = _("Shutdown virtual machine with ACPI signal.")
abortable = True
required_perms = ()
def check_precond(self):
super(ShutdownOperation, self).check_precond()
......@@ -458,6 +504,12 @@ class ShutOffOperation(InstanceOperation):
id = 'shut_off'
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)
def on_commit(self, activity):
activity.resultant_state = 'STOPPED'
......@@ -484,6 +536,7 @@ class SleepOperation(InstanceOperation):
id = 'sleep'
name = _("sleep")
description = _("Suspend virtual machine with memory dump.")
required_perms = ()
def is_preferred(self):
return (not self.instance.is_base and
......@@ -527,6 +580,7 @@ class WakeUpOperation(InstanceOperation):
Power on Virtual Machine and load its memory from dump.
""")
required_perms = ()
def is_preferred(self):
return (self.instance.is_base and
......@@ -593,6 +647,7 @@ class FlushOperation(NodeOperation):
id = 'flush'
name = _("flush")
description = _("Disable node and move all instances to other ones.")
required_perms = ()
def on_abort(self, activity, error):
from manager.scheduler import TraitsUnsatisfiableException
......@@ -600,6 +655,12 @@ class FlushOperation(NodeOperation):
if self.node_enabled:
self.node.enable(activity.user, activity)
def check_auth(self, user):
if not user.is_superuser:
raise PermissionDenied()
super(FlushOperation, self).check_auth(user=user)
def _operation(self, activity, user):
self.node_enabled = self.node.enabled
self.node.disable(user, activity)
......@@ -617,6 +678,7 @@ class ScreenshotOperation(InstanceOperation):
name = _("screenshot")
description = _("Get screenshot")
acl_level = "owner"
required_perms = ()
def check_precond(self):
super(ScreenshotOperation, self).check_precond()
......@@ -655,3 +717,31 @@ class RecoverOperation(InstanceOperation):
register_operation(RecoverOperation)
class ResourcesOperation(InstanceOperation):
activity_code_suffix = 'Resources change'
id = 'resources_change'
name = _("resources change")
description = _("Change resources")
acl_level = "owner"
concurrency_check = False
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)
def _operation(self, user, num_cores, ram_size, max_ram_size, priority):
self.instance.num_cores = num_cores
self.instance.ram_size = ram_size
self.instance.max_ram_size = max_ram_size
self.instance.priority = priority
self.instance.full_clean()
self.instance.save()
register_operation(ResourcesOperation)
......@@ -103,6 +103,7 @@ class InstanceTestCase(TestCase):
inst = Mock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr:
act = MagicMock()
......@@ -118,6 +119,7 @@ class InstanceTestCase(TestCase):
inst = MagicMock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
migrate_op = MigrateOperation(inst)
with patch('vm.models.instance.vm_tasks.migrate') as migr:
inst.select_node.side_effect = AssertionError
......@@ -133,6 +135,7 @@ class InstanceTestCase(TestCase):
inst = Mock(destroyed_at=None, spec=Instance)
inst.interface_set.all.return_value = []
inst.node = MagicMock(spec=Node)
inst.status = 'RUNNING'
e = Exception('abc')
setattr(e, 'libvirtError', '')
inst.migrate_vm.side_effect = e
......@@ -372,6 +375,7 @@ class InstanceActivityTestCase(TestCase):
node = MagicMock(spec=Node, enabled=True)
node.instance_set.all.return_value = insts
user = MagicMock(spec=User)
user.is_superuser = MagicMock(return_value=True)
flush_op = FlushOperation(node)
with patch.object(FlushOperation, 'create_activity') as create_act:
......@@ -383,6 +387,7 @@ class InstanceActivityTestCase(TestCase):
node.disable.assert_called_with(user, act)
for i in insts:
i.migrate.assert_called()
user.is_superuser.assert_called()
def test_flush_disabled_wo_user(self):
insts = [MagicMock(spec=Instance, migrate=MagicMock()),
......
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