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