Move default template for tables out of template into table base class
[ganeti_webmgr.git] / ganeti_web / views / virtual_machine.py
blob71ef0ca49d8bf20ae32cbb3b82a6253c9091b17b
1 # Copyright (C) 2010 Oregon State University et al.
2 # Copyright (C) 2010 Greek Research and Technology Network
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
17 # USA.
20 from django.conf import settings
21 from django.contrib.auth.decorators import login_required
22 from django.contrib.contenttypes.models import ContentType
23 from django.core.urlresolvers import reverse
24 from django.db.models import Q
25 from django.forms import CharField, HiddenInput
26 from django.http import (HttpResponse, HttpResponseRedirect,
27 HttpResponseForbidden, HttpResponseBadRequest,
28 Http404)
29 from django.shortcuts import get_object_or_404, render_to_response
30 from django.template import RequestContext
31 from django.utils import simplejson as json
32 from django.utils.translation import ugettext as _
33 from django.views.decorators.http import require_http_methods, require_POST
34 from django.views.generic.edit import DeleteView
36 from django_tables2 import SingleTableView
38 from object_log.views import list_for_object
39 from object_log.models import LogItem
40 log_action = LogItem.objects.log_action
42 from object_permissions import get_users_any
43 from object_permissions.signals import (view_add_user, view_edit_user,
44 view_remove_user)
45 from object_permissions.views.permissions import view_users, view_permissions
48 from ganeti_web.backend.queries import vm_qs_for_users
49 from ganeti_web.caps import has_shutdown_timeout, has_balloonmem
50 from ganeti_web.forms.virtual_machine import (KvmModifyVirtualMachineForm,
51 PvmModifyVirtualMachineForm,
52 HvmModifyVirtualMachineForm,
53 ModifyConfirmForm, MigrateForm,
54 RenameForm, ChangeOwnerForm,
55 ReplaceDisksForm)
56 from ganeti_web.middleware import Http403
57 from ganeti_web.models import Cluster, Job, SSHKey, VirtualMachine
58 from ganeti_web.templatetags.webmgr_tags import render_storage
59 from ganeti_web.util.client import GanetiApiError
60 from ganeti_web.utilities import (cluster_os_list, compare, os_prettify,
61 get_hypervisor)
62 from ganeti_web.views.generic import (NO_PRIVS, LoginRequiredMixin,
63 PaginationMixin, GWMBaseView)
65 from ganeti_web.views.tables import BaseVMTable
68 #XXX No more need for tastypie dependency for 0.8
69 class HttpAccepted(HttpResponse):
70 """
71 Take from tastypie.http
73 In 0.9 when we reorganize the RestAPI, change this back
74 to an import.
75 """
76 status_code = 202
79 def get_vm_and_cluster_or_404(cluster_slug, instance):
80 """
81 Utility function for querying VirtualMachine and Cluster in a single query
82 rather than 2 separate calls to get_object_or_404.
83 """
84 query = VirtualMachine.objects \
85 .filter(cluster__slug=cluster_slug, hostname=instance) \
86 .select_related('cluster')
87 if len(query):
88 return query[0], query[0].cluster
89 raise Http404('Virtual Machine does not exist')
92 class BaseVMListView(LoginRequiredMixin, PaginationMixin, GWMBaseView,
93 SingleTableView):
94 """
95 A view for listing VirtualMachines. It does so using a custom table object
96 containing the logic for displaying the list.
97 """
98 model = VirtualMachine
99 table_class = BaseVMTable
100 template_name = "ganeti/virtual_machine/list.html"
102 def get_template_names(self):
103 if self.request.is_ajax():
104 template = ['table.html'] # all we need is the table
105 else:
106 template = ['ganeti/virtual_machine/list.html']
107 return template
110 class VMListView(BaseVMListView):
111 def get_queryset(self):
112 # queryset takes precedence over model
113 self.queryset = vm_qs_for_users(self.request.user)
114 qs = super(BaseVMListView, self).get_queryset()
115 return qs
117 def get_context_data(self, **kwargs):
118 context = super(VMListView, self).get_context_data(**kwargs)
119 context["ajax_url"] = reverse("virtualmachine-list")
120 # check perms, and send them to the table for rendering
121 # pass in the Cluster Class to check all clusters
122 can_create = self.can_create(Cluster)
123 context["table"].can_create = context["create_vm"] = can_create
124 return context
127 class VMDeleteView(LoginRequiredMixin, DeleteView):
129 Delete a VM.
132 template_name = "ganeti/virtual_machine/delete.html"
134 def get_context_data(self, **kwargs):
135 kwargs["vm"] = self.vm
136 kwargs["cluster"] = self.cluster
137 return super(VMDeleteView, self).get_context_data(**kwargs)
139 def get_object(self):
140 user = self.request.user
141 vm, cluster = get_vm_and_cluster_or_404(self.kwargs["cluster_slug"],
142 self.kwargs["instance"])
143 if not (
144 user.is_superuser or
145 user.has_any_perms(vm, ["remove", "admin"]) or
146 user.has_perm("admin", cluster)):
147 raise Http403(NO_PRIVS)
149 self.vm = vm
150 self.cluster = cluster
151 return vm
153 def get_success_url(self):
154 return reverse('instance-detail', args=[self.kwargs["cluster_slug"],
155 self.vm.hostname])
157 def delete(self, *args, **kwargs):
158 vm = self.get_object()
159 self._destroy(vm)
160 return HttpResponseRedirect(self.get_success_url())
162 def _destroy(self, instance):
164 Actually destroy a VM.
166 Well, kind of. We won't destroy it immediately if it still exists in
167 Ganeti; in that case, we'll only mark it for later removal and start
168 the Ganeti job to destroy it.
171 # Check that the VM still exists in Ganeti. If it doesn't, then just
172 # delete it.
173 try:
174 instance._refresh()
175 except GanetiApiError, e:
176 if e.code == 404:
177 instance.delete()
178 return
179 raise
181 # Clear any old jobs for this VM.
182 ct = ContentType.objects.get_for_model(VirtualMachine)
183 Job.objects.filter(content_type=ct, object_id=instance.id).delete()
185 # Create the deletion job.
186 job_id = instance.rapi.DeleteInstance(instance.hostname)
187 job = Job.objects.create(job_id=job_id, obj=instance,
188 cluster_id=instance.cluster_id)
190 # Mark the VM as pending deletion. Also disable its cache.
191 instance.last_job = job
192 instance.ignore_cache = True
193 instance.pending_delete = True
194 instance.save()
197 @require_http_methods(["GET", "POST"])
198 @login_required
199 def reinstall(request, cluster_slug, instance):
201 Reinstall a VM.
204 user = request.user
205 instance, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
207 # Check permissions.
208 # XXX Reinstalling is somewhat similar to
209 # deleting in that you destroy data,
210 # so use that for now.
211 if not (
212 user.is_superuser or
213 user.has_any_perms(instance, ["remove", "admin"]) or
214 user.has_perm("admin", cluster)):
215 raise Http403(NO_PRIVS)
217 if request.method == 'GET':
218 return render_to_response(
219 "ganeti/virtual_machine/reinstall.html",
220 {'vm': instance, 'oschoices': cluster_os_list(cluster),
221 'current_os': instance.operating_system,
222 'cluster': cluster},
223 context_instance=RequestContext(request),
226 elif request.method == 'POST':
227 # Reinstall instance
228 if "os" in request.POST:
229 os = request.POST["os"]
230 else:
231 os = instance.operating_system
233 # XXX no_startup=True prevents quota circumventions.
234 # possible future solution would be a checkbox
235 # asking whether they want to start up, and check
236 # quota here if they do (would also involve
237 # checking whether this VM is already running and subtracting that)
239 job_id = instance.rapi \
240 .ReinstallInstance(instance.hostname, os=os, no_startup=True)
241 job = Job.objects.create(job_id=job_id, obj=instance, cluster=cluster)
242 VirtualMachine.objects \
243 .filter(id=instance.id).update(last_job=job, ignore_cache=True)
245 # log information
246 log_action('VM_REINSTALL', user, instance, job)
248 return HttpResponseRedirect(
249 reverse('instance-detail', args=[cluster.slug, instance.hostname]))
252 @login_required
253 def novnc(request,
254 cluster_slug,
255 instance,
256 template="ganeti/virtual_machine/novnc.html"):
257 vm = get_object_or_404(VirtualMachine, hostname=instance,
258 cluster__slug=cluster_slug)
259 user = request.user
260 if not (user.is_superuser
261 or user.has_any_perms(vm, ['admin', 'power'])
262 or user.has_perm('admin', vm.cluster)):
263 return HttpResponseForbidden(_('You do not have permission '
264 'to vnc on this'))
266 return render_to_response(template,
267 {'cluster_slug': cluster_slug,
268 'instance': vm,
270 context_instance=RequestContext(request), )
273 @require_POST
274 @login_required
275 def vnc_proxy(request, cluster_slug, instance):
276 vm = get_object_or_404(VirtualMachine, hostname=instance,
277 cluster__slug=cluster_slug)
278 user = request.user
279 if not (user.is_superuser
280 or user.has_any_perms(vm, ['admin', 'power'])
281 or user.has_perm('admin', vm.cluster)):
282 return HttpResponseForbidden(_('You do not have permission '
283 'to vnc on this'))
285 use_tls = bool(request.POST.get("tls"))
286 result = json.dumps(vm.setup_vnc_forwarding(tls=use_tls))
288 return HttpResponse(result, mimetype="application/json")
291 @require_POST
292 @login_required
293 def shutdown(request, cluster_slug, instance):
294 vm = get_object_or_404(VirtualMachine, hostname=instance,
295 cluster__slug=cluster_slug)
296 user = request.user
298 if not (user.is_superuser or user.has_any_perms(vm, ['admin', 'power']) or
299 user.has_perm('admin', vm.cluster)):
300 msg = _('You do not have permission to shut down this virtual machine')
301 raise Http403(msg)
303 try:
304 job = vm.shutdown()
305 job.refresh()
306 msg = job.info
308 # log information about stopping the machine
309 log_action('VM_STOP', user, vm, job)
310 except GanetiApiError, e:
311 msg = {'__all__': [str(e)]}
313 return HttpResponse(json.dumps(msg), mimetype='application/json')
316 @require_POST
317 @login_required
318 def shutdown_now(request, cluster_slug, instance):
319 vm = get_object_or_404(VirtualMachine, hostname=instance,
320 cluster__slug=cluster_slug)
321 user = request.user
323 if not (user.is_superuser or user.has_any_perms(vm, ['admin', 'power']) or
324 user.has_perm('admin', vm.cluster)):
325 msg = _('You do not have permission to shut down this virtual machine')
326 raise Http403(msg)
328 try:
329 job = vm.shutdown(timeout=0)
330 job.refresh()
331 msg = job.info
333 # log information about stopping the machine
334 log_action('VM_STOP', user, vm, job)
335 except GanetiApiError, e:
336 msg = {'__all__': [str(e)]}
338 return HttpResponse(json.dumps(msg), mimetype='application/json')
341 @require_POST
342 @login_required
343 def startup(request, cluster_slug, instance, rest=False):
344 vm = get_object_or_404(VirtualMachine, hostname=instance,
345 cluster__slug=cluster_slug)
346 user = request.user
347 if not (user.is_superuser or user.has_any_perms(vm, ['admin', 'power']) or
348 user.has_perm('admin', vm.cluster)):
349 msg = _('You do not have permission to start up this virtual machine')
350 if rest:
351 return {"msg": msg, "code": 403}
352 else:
353 raise Http403(msg)
355 # superusers bypass quota checks
356 if not user.is_superuser and vm.owner:
357 # check quota
358 quota = vm.cluster.get_quota(vm.owner)
359 if any(quota.values()):
360 used = vm.owner.used_resources(vm.cluster, only_running=True)
362 if quota['ram'] is not None \
363 and (used['ram'] + vm.ram) > quota['ram']:
364 msg = _('Owner does not have enough RAM remaining on '
365 'this cluster to start the virtual machine.')
366 if rest:
367 return {"msg": msg, "code": 500}
368 else:
369 return HttpResponse(json.dumps([0, msg]),
370 mimetype='application/json')
372 if quota['virtual_cpus'] and \
373 (used['virtual_cpus'] + vm.virtual_cpus) \
374 > quota['virtual_cpus']:
375 msg = _('Owner does not have enough Virtual CPUs remaining '
376 'on this cluster to start the virtual machine.')
377 if rest:
378 return {"msg": msg, "code": 500}
379 else:
380 return HttpResponse(json.dumps([0, msg]),
381 mimetype='application/json')
383 try:
384 job = vm.startup()
385 job.refresh()
386 msg = job.info
388 # log information about starting up the machine
389 log_action('VM_START', user, vm, job)
390 except GanetiApiError, e:
391 msg = {'__all__': [str(e)]}
392 if rest:
393 return {"msg": msg, "code": 200}
394 else:
395 return HttpResponse(json.dumps(msg), mimetype='application/json')
398 @login_required
399 def migrate(request, cluster_slug, instance):
401 view used for initiating a Node Migrate job
403 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
405 user = request.user
406 if not (user.is_superuser or
407 user.has_any_perms(cluster, ['admin', 'migrate'])):
408 raise Http403(NO_PRIVS)
410 if request.method == 'POST':
411 form = MigrateForm(request.POST)
412 if form.is_valid():
413 try:
414 job = vm.migrate(form.cleaned_data['mode'])
415 job.refresh()
416 content = json.dumps(job.info)
418 # log information
419 log_action('VM_MIGRATE', user, vm, job)
420 except GanetiApiError, e:
421 content = json.dumps({'__all__': [str(e)]})
422 else:
423 # error in form return ajax response
424 content = json.dumps(form.errors)
425 return HttpResponse(content, mimetype='application/json')
427 else:
428 form = MigrateForm()
430 return render_to_response('ganeti/virtual_machine/migrate.html',
431 {'form': form, 'vm': vm, 'cluster': cluster},
432 context_instance=RequestContext(request))
435 @login_required
436 def replace_disks(request, cluster_slug, instance):
438 view used for initiating a Replace Disks job
440 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
441 user = request.user
442 if not (user.is_superuser or
443 user.has_any_perms(cluster, ['admin', 'replace_disks'])):
444 raise Http403(NO_PRIVS)
446 if request.method == 'POST':
447 form = ReplaceDisksForm(vm, request.POST)
448 if form.is_valid():
449 try:
450 job = form.save()
451 job.refresh()
452 content = json.dumps(job.info)
454 # log information
455 log_action('VM_REPLACE_DISKS', user, vm, job)
456 except GanetiApiError, e:
457 content = json.dumps({'__all__': [str(e)]})
458 else:
459 # error in form return ajax response
460 content = json.dumps(form.errors)
461 return HttpResponse(content, mimetype='application/json')
463 else:
464 form = ReplaceDisksForm(vm)
466 return render_to_response('ganeti/virtual_machine/replace_disks.html',
467 {'form': form, 'vm': vm, 'cluster': cluster},
468 context_instance=RequestContext(request))
471 @require_POST
472 @login_required
473 def reboot(request, cluster_slug, instance, rest=False):
474 vm = get_object_or_404(VirtualMachine, hostname=instance,
475 cluster__slug=cluster_slug)
476 user = request.user
477 if not (user.is_superuser or
478 user.has_any_perms(vm, ['admin', 'power']) or
479 user.has_perm('admin', vm.cluster)):
481 if rest:
482 return HttpResponseForbidden()
483 else:
484 raise Http403(_('You do not have permission to '
485 'reboot this virtual machine'))
487 try:
488 job = vm.reboot()
489 job.refresh()
490 msg = job.info
492 # log information about restarting the machine
493 log_action('VM_REBOOT', user, vm, job)
494 except GanetiApiError, e:
495 msg = {'__all__': [str(e)]}
496 if rest:
497 return HttpAccepted()
498 else:
499 return HttpResponse(json.dumps(msg), mimetype='application/json')
502 def ssh_keys(request, cluster_slug, instance, api_key):
504 Show all ssh keys which belong to users, who are specified vm's admin
506 if settings.WEB_MGR_API_KEY != api_key:
507 return HttpResponseForbidden(_("You're not allowed to view keys."))
509 vm = get_object_or_404(VirtualMachine, hostname=instance,
510 cluster__slug=cluster_slug)
512 users = get_users_any(vm, ["admin", ]).values_list("id", flat=True)
513 keys = SSHKey.objects \
514 .filter(Q(user__in=users) | Q(user__is_superuser=True)) \
515 .values_list('key', 'user__username') \
516 .order_by('user__username')
518 keys_list = list(keys)
519 return HttpResponse(json.dumps(keys_list), mimetype="application/json")
522 @login_required
523 def detail(request, cluster_slug, instance, rest=False):
525 Display details of virtual machine.
527 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
529 user = request.user
530 cluster_admin = (user.is_superuser or
531 user.has_any_perms(cluster, perms=['admin', 'create_vm']))
533 if not cluster_admin:
534 perms = user.get_perms(vm)
536 if cluster_admin or 'admin' in perms:
537 admin = True
538 remove = True
539 power = True
540 modify = True
541 migrate = True
542 tags = True
543 else:
544 admin = False
545 remove = 'remove' in perms
546 power = 'power' in perms
547 modify = 'modify' in perms
548 tags = 'tags' in perms
549 migrate = 'migrate' in perms
551 if not (admin or power or remove or modify or tags): # TODO REST
552 raise Http403(_('You do not have permission to view '
553 'this virtual machines\'s details'))
555 context = {
556 'cluster': cluster,
557 'instance': vm,
558 'admin': admin,
559 'cluster_admin': cluster_admin,
560 'remove': remove,
561 'power': power,
562 'modify': modify,
563 'migrate': migrate,
564 "has_immediate_shutdown": has_shutdown_timeout(cluster),
567 # check job for pending jobs that should be rendered with a different
568 # detail template. This allows us to reduce the chance that users will do
569 # something strange like rebooting a VM that is being deleted or is not
570 # fully created yet.
571 if vm.pending_delete:
572 template = 'ganeti/virtual_machine/delete_status.html'
573 elif vm.template:
574 template = 'ganeti/virtual_machine/create_status.html'
575 if vm.last_job:
576 context['job'] = vm.last_job
577 else:
578 ct = ContentType.objects.get_for_model(vm)
579 jobs_loc = Job.objects.order_by('-finished') \
580 .filter(content_type=ct, object_id=vm.pk)
581 if jobs_loc.count() > 0:
582 context['job'] = jobs_loc[0]
583 else:
584 context['job'] = None
585 else:
586 template = 'ganeti/virtual_machine/detail.html'
588 if rest:
589 return context
590 else:
591 return render_to_response(template, context,
592 context_instance=RequestContext(request), )
595 @login_required
596 def users(request, cluster_slug, instance, rest=False):
598 Display all of the Users of a VirtualMachine
600 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
602 user = request.user
603 if not (user.is_superuser or user.has_perm('admin', vm) or
604 user.has_perm('admin', cluster)):
605 if rest:
606 return {'msg': NO_PRIVS, 'code': 403}
607 else:
608 raise Http403(NO_PRIVS)
610 url = reverse('vm-permissions', args=[cluster.slug, vm.hostname])
611 return view_users(request, vm, url, rest=rest)
614 @login_required
615 def permissions(request, cluster_slug, instance, user_id=None, group_id=None):
617 Update a users permissions.
619 vm = get_object_or_404(VirtualMachine, hostname=instance,
620 cluster__slug=cluster_slug)
622 user = request.user
623 if not (user.is_superuser or user.has_perm('admin', vm) or
624 user.has_perm('admin', vm.cluster)):
625 raise Http403(NO_PRIVS)
627 url = reverse('vm-permissions', args=[cluster_slug, vm.hostname])
628 return view_permissions(request, vm, url, user_id, group_id)
631 @login_required
632 def object_log(request, cluster_slug, instance, rest=False):
634 Display all of the Users of a VirtualMachine
636 cluster = get_object_or_404(Cluster, slug=cluster_slug)
637 vm = get_object_or_404(VirtualMachine, hostname=instance)
639 user = request.user
640 if not (user.is_superuser or user.has_perm('admin', vm) or
641 user.has_perm('admin', cluster)):
642 raise Http403(NO_PRIVS)
644 if rest:
645 return list_for_object(request, vm, True)
646 else:
647 return list_for_object(request, vm)
650 @login_required
651 def modify(request, cluster_slug, instance):
652 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
654 user = request.user
655 if not (user.is_superuser
656 or user.has_any_perms(vm, ['admin', 'modify'])
657 or user.has_perm('admin', cluster)):
658 raise Http403(
659 'You do not have permissions to edit this virtual machine')
661 hv = get_hypervisor(vm)
662 if hv == 'kvm':
663 hv_form = KvmModifyVirtualMachineForm
664 template = 'ganeti/virtual_machine/edit_kvm.html'
665 elif hv == 'xen-pvm':
666 hv_form = PvmModifyVirtualMachineForm
667 template = 'ganeti/virtual_machine/edit_pvm.html'
668 elif hv == 'xen-hvm':
669 hv_form = HvmModifyVirtualMachineForm
670 template = 'ganeti/virtual_machine/edit_hvm.html'
671 else:
672 hv_form = None
673 template = 'ganeti/virtual_machine/edit_base.html'
674 # XXX no matter what, we're gonna call hv_form() and die. Let's do it
675 # louder than usual. >:3
676 msg = "Hey, guys, implementation error in views/vm.py:modify"
677 raise RuntimeError(msg)
679 if request.method == 'POST':
681 form = hv_form(vm, request.POST)
683 form.owner = vm.owner
684 form.vm = vm
685 form.cluster = cluster
686 if form.is_valid():
687 data = form.cleaned_data
688 request.session['edit_form'] = data
689 request.session['edit_vm'] = vm.id
690 return HttpResponseRedirect(
691 reverse('instance-modify-confirm',
692 args=[cluster.slug,
693 vm.hostname]))
695 elif request.method == 'GET':
696 if 'edit_form' in request.session \
697 and vm.id == request.session['edit_vm']:
698 form = hv_form(vm, request.session['edit_form'])
699 else:
700 form = hv_form(vm)
702 return render_to_response(
703 template,
704 {'cluster': cluster,
705 'instance': vm,
706 'form': form,
707 'balloon': has_balloonmem(cluster)
709 context_instance=RequestContext(request),
713 # XXX mother, did it need to be so long?
714 @login_required
715 def modify_confirm(request, cluster_slug, instance):
716 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
718 hv = get_hypervisor(vm)
719 if hv == 'kvm':
720 hv_form = KvmModifyVirtualMachineForm
721 elif hv == 'xen-pvm':
722 hv_form = PvmModifyVirtualMachineForm
723 elif hv == 'xen-hvm':
724 hv_form = HvmModifyVirtualMachineForm
725 else:
726 hv_form = None
727 # XXX no matter what, we're gonna call hv_form() and die. Let's do it
728 # louder than usual. >:3
729 msg = "Hey, guys, implementation error in views/vm.py:modify_confirm"
730 raise RuntimeError(msg)
732 user = request.user
733 power = user.is_superuser or user.has_any_perms(vm, ['admin', 'power'])
734 if not (user.is_superuser or user.has_any_perms(vm, ['admin', 'modify'])
735 or user.has_perm('admin', cluster)):
736 raise Http403(
737 _('You do not have permissions to edit this virtual machine'))
739 if request.method == "POST":
740 if 'edit' in request.POST:
741 return HttpResponseRedirect(
742 reverse("instance-modify",
743 args=[cluster.slug, vm.hostname]))
744 elif 'reboot' in request.POST or 'save' in request.POST:
745 form = ModifyConfirmForm(request.POST)
746 form.session = request.session
747 form.owner = vm.owner
748 form.vm = vm
749 form.cluster = cluster
751 if form.is_valid():
752 beparams = {}
753 data = form.cleaned_data
754 rapi_dict = data['rapi_dict']
755 nics = rapi_dict.pop('nics')
756 beparams['vcpus'] = rapi_dict.pop('vcpus')
757 if has_balloonmem(cluster):
758 beparams['maxmem'] = rapi_dict.pop('maxmem')
759 beparams['minmem'] = rapi_dict.pop('minmem')
760 else:
761 beparams['memroy'] = rapi_dict.pop('memory')
762 os_name = rapi_dict.pop('os')
763 job_id = cluster.rapi.ModifyInstance(
764 instance,
765 nics=nics,
766 os_name=os_name,
767 hvparams=rapi_dict,
768 beparams=beparams)
769 # Create job and update message on virtual machine detail page
770 job = Job.objects.create(job_id=job_id,
771 obj=vm,
772 cluster=cluster)
773 VirtualMachine.objects \
774 .filter(id=vm.id).update(last_job=job, ignore_cache=True)
775 # log information about modifying this instance
776 log_action('EDIT', user, vm)
777 if 'reboot' in request.POST and vm.info['status'] == 'running':
778 if power:
779 # Reboot the vm
780 job = vm.reboot()
781 log_action('VM_REBOOT', user, vm, job)
782 else:
783 raise Http403(
784 _("Sorry, but you do not have permission "
785 "to reboot this machine."))
787 # Redirect to instance-detail
788 return HttpResponseRedirect(
789 reverse("instance-detail",
790 args=[cluster.slug, vm.hostname]))
792 elif 'cancel' in request.POST:
793 # Remove session variables.
794 if 'edit_form' in request.session:
795 del request.session['edit_form']
796 # Redirect to instance-detail
797 return HttpResponseRedirect(
798 reverse("instance-detail", args=[cluster.slug, vm.hostname]))
800 elif request.method == "GET":
801 form = ModifyConfirmForm()
803 session = request.session
805 if not 'edit_form' in request.session:
806 return HttpResponseBadRequest('Incorrect Session Data')
808 data = session['edit_form']
809 info = vm.info
810 hvparams = info['hvparams']
812 old_set = dict(
813 vcpus=info['beparams']['vcpus'],
814 os=info['os'],
816 if has_balloonmem(cluster):
817 old_set['maxmem'] = info['beparams']['maxmem']
818 old_set['minmem'] = info['beparams']['minmem']
819 else:
820 old_set['memory'] = info['beparams']['memory']
821 nic_count = len(info['nic.links'])
822 for i in xrange(nic_count):
823 old_set['nic_link_%s' % i] = info['nic.links'][i]
824 old_set['nic_mac_%s' % i] = info['nic.macs'][i]
826 # Add hvparams to the old_set
827 old_set.update(hvparams)
829 instance_diff = {}
830 fields = hv_form(vm, data).fields
831 for key in data.keys():
832 if key in ['memory', 'maxmem', 'minmem']:
833 diff = compare(render_storage(old_set[key]),
834 render_storage(data[key]))
835 elif key == 'os':
836 oses = os_prettify([old_set[key], data[key]])
837 if len(oses) > 1:
839 XXX - Special case for a cluster with two different types of
840 optgroups (i.e. Image, Debootstrap).
841 The elements at 00 and 10:
842 The optgroups
843 The elements at 010 and 110:
844 Tuple containing the OS Name and OS value.
845 The elements at 0101 and 1101:
846 String containing the OS Name
848 oses[0][1][0] = list(oses[0][1][0])
849 oses[1][1][0] = list(oses[1][1][0])
850 oses[0][1][0][1] = '%s (%s)' % (oses[0][1][0][1], oses[0][0])
851 oses[1][1][0][1] = '%s (%s)' % (oses[1][1][0][1], oses[1][0])
852 oses = oses[0][1] + oses[1][1]
853 diff = compare(oses[0][1], oses[1][1])
854 else:
855 oses = oses[0][1]
856 diff = compare(oses[0][1], oses[1][1])
857 #diff = compare(oses[0][1], oses[1][1])
858 if key in ['nic_count', 'nic_count_original']:
859 continue
860 elif key not in old_set.keys():
861 diff = ""
862 instance_diff[fields[key].label] = _('Added')
863 else:
864 diff = compare(old_set[key], data[key])
866 if diff != "":
867 label = fields[key].label
868 instance_diff[label] = diff
870 # remove mac if it has not changed
871 for i in xrange(nic_count):
872 if fields['nic_mac_%s' % i].label not in instance_diff:
873 del data['nic_mac_%s' % i]
875 # Repopulate form with changed values
876 form.fields['rapi_dict'] = CharField(widget=HiddenInput,
877 initial=json.dumps(data))
879 return render_to_response(
880 'ganeti/virtual_machine/edit_confirm.html', {
881 'cluster': cluster,
882 'form': form,
883 'instance': vm,
884 'instance_diff': instance_diff,
885 'power': power,
887 context_instance=RequestContext(request),
891 @login_required
892 def rename(request, cluster_slug, instance, rest=False, extracted_params=None):
894 Rename an existing instance
896 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
898 user = request.user
899 if not (user.is_superuser or user.has_any_perms(vm, ['admin', 'modify'])
900 or user.has_perm('admin', cluster)):
901 raise Http403(
902 _('You do not have permissions to edit this virtual machine'))
904 if request.method == 'POST':
905 form = RenameForm(vm, request.POST)
906 params_ok = False
907 if rest and extracted_params is not None:
908 if all(k in extracted_params
909 for k in ("hostname", "ip_check", "name_check")):
910 hostname = extracted_params['hostname']
911 ip_check = extracted_params['ip_check']
912 name_check = extracted_params['name_check']
913 params_ok = True
914 else:
915 return HttpResponseBadRequest()
917 if form.is_valid():
918 data = form.cleaned_data
919 hostname = data['hostname']
920 ip_check = data['ip_check']
921 name_check = data['name_check']
923 if form.is_valid() or params_ok:
924 try:
925 # In order for rename to work correctly, the vm must first be
926 # shutdown.
927 if vm.is_running:
928 job1 = vm.shutdown()
929 log_action('VM_STOP', user, vm, job1)
931 job_id = vm.rapi.RenameInstance(vm.hostname, hostname,
932 ip_check, name_check)
933 job = Job.objects.create(job_id=job_id,
934 obj=vm,
935 cluster=cluster)
936 VirtualMachine.objects.filter(pk=vm.pk) \
937 .update(hostname=hostname, last_job=job, ignore_cache=True)
939 # slip the new hostname to the log action
940 vm.newname = hostname
942 # log information about creating the machine
943 log_action('VM_RENAME', user, vm, job)
945 if not rest:
946 return HttpResponseRedirect(
947 reverse('instance-detail',
948 args=[cluster.slug, hostname]))
949 else:
950 return HttpAccepted()
952 except GanetiApiError, e:
953 msg = 'Error renaming virtual machine: %s' % e
954 form._errors["cluster"] = form.error_class([msg])
955 if rest:
956 return HttpResponse(400, content=msg)
958 elif request.method == 'GET':
959 form = RenameForm(vm)
961 return render_to_response(
962 'ganeti/virtual_machine/rename.html',
963 {'cluster': cluster,
964 'vm': vm,
965 'form': form
967 context_instance=RequestContext(request), )
970 @login_required
971 def reparent(request, cluster_slug, instance):
973 update a virtual machine to have a new owner
975 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
977 user = request.user
978 if not (user.is_superuser or user.has_perm('admin', cluster)):
979 raise Http403(
980 _('You do not have permissions to change the owner '
981 'of this virtual machine'))
983 if request.method == 'POST':
984 form = ChangeOwnerForm(request.POST)
985 if form.is_valid():
986 data = form.cleaned_data
987 vm.owner = data['owner']
988 vm.save(force_update=True)
990 # log information about creating the machine
991 log_action('VM_MODIFY', user, vm)
993 return HttpResponseRedirect(
994 reverse('instance-detail', args=[cluster_slug, instance]))
996 else:
997 form = ChangeOwnerForm()
999 return render_to_response(
1000 'ganeti/virtual_machine/reparent.html', {
1001 'cluster': cluster,
1002 'vm': vm,
1003 'form': form
1005 context_instance=RequestContext(request),
1009 @login_required
1010 def job_status(request, id, rest=False):
1012 Return a list of basic info for running jobs.
1014 ct = ContentType.objects.get_for_model(VirtualMachine)
1015 jobs = Job.objects.filter(status__in=("error", "running", "waiting"),
1016 content_type=ct,
1017 object_id=id).order_by('job_id')
1018 jobs = [j.info for j in jobs]
1020 if rest:
1021 return jobs
1022 else:
1023 return HttpResponse(json.dumps(jobs), mimetype='application/json')
1026 def recv_user_add(sender, editor, user, obj, **kwargs):
1028 receiver for object_permissions.signals.view_add_user, Logs action
1030 log_action('ADD_USER', editor, obj, user)
1033 def recv_user_remove(sender, editor, user, obj, **kwargs):
1035 receiver for object_permissions.signals.view_remove_user, Logs action
1037 log_action('REMOVE_USER', editor, obj, user)
1040 def recv_perm_edit(sender, editor, user, obj, **kwargs):
1042 receiver for object_permissions.signals.view_edit_user, Logs action
1044 log_action('MODIFY_PERMS', editor, obj, user)
1047 view_add_user.connect(recv_user_add, sender=VirtualMachine)
1048 view_remove_user.connect(recv_user_remove, sender=VirtualMachine)
1049 view_edit_user.connect(recv_perm_edit, sender=VirtualMachine)