Add more information on deploying; fix format.
[ganeti_webmgr.git] / ganeti_web / views / virtual_machine.py
blob1dfa1521f3fdee7bb39f937e21227f11990fc624
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 object_log.views import list_for_object
38 from object_permissions import get_users_any
39 from object_permissions.signals import (view_add_user, view_edit_user,
40 view_remove_user)
41 from object_permissions.views.permissions import view_users, view_permissions
43 from object_log.models import LogItem
44 log_action = LogItem.objects.log_action
46 from ganeti_web.backend.queries import vm_qs_for_users
47 from ganeti_web.caps import has_shutdown_timeout, has_balloonmem
48 from ganeti_web.forms.virtual_machine import (KvmModifyVirtualMachineForm,
49 PvmModifyVirtualMachineForm,
50 HvmModifyVirtualMachineForm,
51 ModifyConfirmForm, MigrateForm,
52 RenameForm, ChangeOwnerForm,
53 ReplaceDisksForm)
54 from ganeti_web.middleware import Http403
55 from ganeti_web.models import Cluster, Job, SSHKey, Node, VirtualMachine
56 from ganeti_web.templatetags.webmgr_tags import render_storage
57 from ganeti_web.util.client import GanetiApiError
58 from ganeti_web.utilities import (cluster_os_list, compare, os_prettify,
59 get_hypervisor)
60 from ganeti_web.views.generic import (NO_PRIVS, LoginRequiredMixin,
61 PagedListView)
64 #XXX No more need for tastypie dependency for 0.8
65 class HttpAccepted(HttpResponse):
66 """
67 Take from tastypie.http
69 In 0.9 when we reorganize the RestAPI, change this back
70 to an import.
71 """
72 status_code = 202
75 def get_vm_and_cluster_or_404(cluster_slug, instance):
76 """
77 Utility function for querying VirtualMachine and Cluster in a single query
78 rather than 2 separate calls to get_object_or_404.
79 """
80 query = VirtualMachine.objects \
81 .filter(cluster__slug=cluster_slug, hostname=instance) \
82 .select_related('cluster')
83 if len(query):
84 return query[0], query[0].cluster
85 raise Http404('Virtual Machine does not exist')
88 class VMListView(LoginRequiredMixin, PagedListView):
89 """
90 View for displaying a list of VirtualMachines.
91 """
92 template_name = "ganeti/virtual_machine/list.html"
94 def get_queryset(self):
95 qs = vm_qs_for_users(self.request.user)
96 return qs.select_related()
98 def get_context_data(self, **kwargs):
99 user = self.request.user
100 context = super(VMListView, self) \
101 .get_context_data(object_list=kwargs["object_list"])
102 context["can_create"] = (user.is_superuser or
103 user.has_any_perms(Cluster, ["create_vm"]))
105 if "order_by" in self.request.GET:
106 context["order"] = self.request.GET["order_by"]
107 else:
108 context["order"] = "hostname"
110 return context
113 class VMListTableView(VMListView):
115 View for displaying the virtual machine table.
117 This is used for ajax calls to reload the table, usually because of a page
118 or sorting change.
120 It's very similar to the ``VMListView``, but has some additional filters
121 which can be applied.
124 def get_queryset(self):
125 qs = super(VMListTableView, self).get_queryset()
127 if "cluster_slug" in self.kwargs:
128 self.cluster = Cluster.objects \
129 .get(slug=self.kwargs["cluster_slug"])
130 qs = qs.filter(cluster=self.cluster)
131 else:
132 self.cluster = None
134 # filter the vms by primary node if applicable
135 if "primary_node" in self.kwargs:
136 inner = Node.objects.filter(hostname=self.kwargs["primary_node"])
137 qs = qs.filter(primary_node=inner)
139 # filter the vms by secondary node if applicable
140 if "secondary_node" in self.kwargs:
141 inner = Node.objects.filter(hostname=self.kwargs["secondary_node"])
142 qs = qs.filter(secondary_node=inner)
144 return qs
147 class VMDeleteView(LoginRequiredMixin, DeleteView):
149 Delete a VM.
152 template_name = "ganeti/virtual_machine/delete.html"
154 def get_context_data(self, **kwargs):
155 kwargs["vm"] = self.vm
156 kwargs["cluster"] = self.cluster
157 return super(VMDeleteView, self).get_context_data(**kwargs)
159 def get_object(self):
160 user = self.request.user
161 vm, cluster = get_vm_and_cluster_or_404(self.kwargs["cluster_slug"],
162 self.kwargs["instance"])
163 if not (
164 user.is_superuser or
165 user.has_any_perms(vm, ["remove", "admin"]) or
166 user.has_perm("admin", cluster)):
167 raise Http403(NO_PRIVS)
169 self.vm = vm
170 self.cluster = cluster
171 return vm
173 def get_success_url(self):
174 return reverse('instance-detail', args=[self.kwargs["cluster_slug"],
175 self.vm.hostname])
177 def delete(self, *args, **kwargs):
178 vm = self.get_object()
179 self._destroy(vm)
180 return HttpResponseRedirect(self.get_success_url())
182 def _destroy(self, instance):
184 Actually destroy a VM.
186 Well, kind of. We won't destroy it immediately if it still exists in
187 Ganeti; in that case, we'll only mark it for later removal and start
188 the Ganeti job to destroy it.
191 # Check that the VM still exists in Ganeti. If it doesn't, then just
192 # delete it.
193 try:
194 instance._refresh()
195 except GanetiApiError, e:
196 if e.code == 404:
197 instance.delete()
198 return
199 raise
201 # Clear any old jobs for this VM.
202 ct = ContentType.objects.get_for_model(VirtualMachine)
203 Job.objects.filter(content_type=ct, object_id=instance.id).delete()
205 # Create the deletion job.
206 job_id = instance.rapi.DeleteInstance(instance.hostname)
207 job = Job.objects.create(job_id=job_id, obj=instance,
208 cluster_id=instance.cluster_id)
210 # Mark the VM as pending deletion. Also disable its cache.
211 instance.last_job = job
212 instance.ignore_cache = True
213 instance.pending_delete = True
214 instance.save()
217 @require_http_methods(["GET", "POST"])
218 @login_required
219 def reinstall(request, cluster_slug, instance):
221 Reinstall a VM.
224 user = request.user
225 instance, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
227 # Check permissions.
228 # XXX Reinstalling is somewhat similar to
229 # deleting in that you destroy data,
230 # so use that for now.
231 if not (
232 user.is_superuser or
233 user.has_any_perms(instance, ["remove", "admin"]) or
234 user.has_perm("admin", cluster)):
235 raise Http403(NO_PRIVS)
237 if request.method == 'GET':
238 return render_to_response(
239 "ganeti/virtual_machine/reinstall.html",
240 {'vm': instance, 'oschoices': cluster_os_list(cluster),
241 'current_os': instance.operating_system,
242 'cluster': cluster},
243 context_instance=RequestContext(request),
246 elif request.method == 'POST':
247 # Reinstall instance
248 if "os" in request.POST:
249 os = request.POST["os"]
250 else:
251 os = instance.operating_system
253 # XXX no_startup=True prevents quota circumventions.
254 # possible future solution would be a checkbox
255 # asking whether they want to start up, and check
256 # quota here if they do (would also involve
257 # checking whether this VM is already running and subtracting that)
259 job_id = instance.rapi \
260 .ReinstallInstance(instance.hostname, os=os, no_startup=True)
261 job = Job.objects.create(job_id=job_id, obj=instance, cluster=cluster)
262 VirtualMachine.objects \
263 .filter(id=instance.id).update(last_job=job, ignore_cache=True)
265 # log information
266 log_action('VM_REINSTALL', user, instance, job)
268 return HttpResponseRedirect(
269 reverse('instance-detail', args=[cluster.slug, instance.hostname]))
272 @login_required
273 def novnc(request,
274 cluster_slug,
275 instance,
276 template="ganeti/virtual_machine/novnc.html"):
277 vm = get_object_or_404(VirtualMachine, hostname=instance,
278 cluster__slug=cluster_slug)
279 user = request.user
280 if not (user.is_superuser
281 or user.has_any_perms(vm, ['admin', 'power'])
282 or user.has_perm('admin', vm.cluster)):
283 return HttpResponseForbidden(_('You do not have permission '
284 'to vnc on this'))
286 return render_to_response(template,
287 {'cluster_slug': cluster_slug,
288 'instance': vm,
290 context_instance=RequestContext(request), )
293 @require_POST
294 @login_required
295 def vnc_proxy(request, cluster_slug, instance):
296 vm = get_object_or_404(VirtualMachine, hostname=instance,
297 cluster__slug=cluster_slug)
298 user = request.user
299 if not (user.is_superuser
300 or user.has_any_perms(vm, ['admin', 'power'])
301 or user.has_perm('admin', vm.cluster)):
302 return HttpResponseForbidden(_('You do not have permission '
303 'to vnc on this'))
305 use_tls = bool(request.POST.get("tls"))
306 result = json.dumps(vm.setup_vnc_forwarding(tls=use_tls))
308 return HttpResponse(result, mimetype="application/json")
311 @require_POST
312 @login_required
313 def shutdown(request, cluster_slug, instance):
314 vm = get_object_or_404(VirtualMachine, hostname=instance,
315 cluster__slug=cluster_slug)
316 user = request.user
318 if not (user.is_superuser or user.has_any_perms(vm, ['admin', 'power']) or
319 user.has_perm('admin', vm.cluster)):
320 msg = _('You do not have permission to shut down this virtual machine')
321 raise Http403(msg)
323 try:
324 job = vm.shutdown()
325 job.refresh()
326 msg = job.info
328 # log information about stopping the machine
329 log_action('VM_STOP', user, vm, job)
330 except GanetiApiError, e:
331 msg = {'__all__': [str(e)]}
333 return HttpResponse(json.dumps(msg), mimetype='application/json')
336 @require_POST
337 @login_required
338 def shutdown_now(request, cluster_slug, instance):
339 vm = get_object_or_404(VirtualMachine, hostname=instance,
340 cluster__slug=cluster_slug)
341 user = request.user
343 if not (user.is_superuser or user.has_any_perms(vm, ['admin', 'power']) or
344 user.has_perm('admin', vm.cluster)):
345 msg = _('You do not have permission to shut down this virtual machine')
346 raise Http403(msg)
348 try:
349 job = vm.shutdown(timeout=0)
350 job.refresh()
351 msg = job.info
353 # log information about stopping the machine
354 log_action('VM_STOP', user, vm, job)
355 except GanetiApiError, e:
356 msg = {'__all__': [str(e)]}
358 return HttpResponse(json.dumps(msg), mimetype='application/json')
361 @require_POST
362 @login_required
363 def startup(request, cluster_slug, instance, rest=False):
364 vm = get_object_or_404(VirtualMachine, hostname=instance,
365 cluster__slug=cluster_slug)
366 user = request.user
367 if not (user.is_superuser or user.has_any_perms(vm, ['admin', 'power']) or
368 user.has_perm('admin', vm.cluster)):
369 msg = _('You do not have permission to start up this virtual machine')
370 if rest:
371 return {"msg": msg, "code": 403}
372 else:
373 raise Http403(msg)
375 # superusers bypass quota checks
376 if not user.is_superuser and vm.owner:
377 # check quota
378 quota = vm.cluster.get_quota(vm.owner)
379 if any(quota.values()):
380 used = vm.owner.used_resources(vm.cluster, only_running=True)
382 if quota['ram'] is not None \
383 and (used['ram'] + vm.ram) > quota['ram']:
384 msg = _('Owner does not have enough RAM remaining on '
385 'this cluster to start the virtual machine.')
386 if rest:
387 return {"msg": msg, "code": 500}
388 else:
389 return HttpResponse(json.dumps([0, msg]),
390 mimetype='application/json')
392 if quota['virtual_cpus'] and \
393 (used['virtual_cpus'] + vm.virtual_cpus) \
394 > quota['virtual_cpus']:
395 msg = _('Owner does not have enough Virtual CPUs remaining '
396 'on this cluster to start the virtual machine.')
397 if rest:
398 return {"msg": msg, "code": 500}
399 else:
400 return HttpResponse(json.dumps([0, msg]),
401 mimetype='application/json')
403 try:
404 job = vm.startup()
405 job.refresh()
406 msg = job.info
408 # log information about starting up the machine
409 log_action('VM_START', user, vm, job)
410 except GanetiApiError, e:
411 msg = {'__all__': [str(e)]}
412 if rest:
413 return {"msg": msg, "code": 200}
414 else:
415 return HttpResponse(json.dumps(msg), mimetype='application/json')
418 @login_required
419 def migrate(request, cluster_slug, instance):
421 view used for initiating a Node Migrate job
423 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
425 user = request.user
426 if not (user.is_superuser or
427 user.has_any_perms(cluster, ['admin', 'migrate'])):
428 raise Http403(NO_PRIVS)
430 if request.method == 'POST':
431 form = MigrateForm(request.POST)
432 if form.is_valid():
433 try:
434 job = vm.migrate(form.cleaned_data['mode'])
435 job.refresh()
436 content = json.dumps(job.info)
438 # log information
439 log_action('VM_MIGRATE', user, vm, job)
440 except GanetiApiError, e:
441 content = json.dumps({'__all__': [str(e)]})
442 else:
443 # error in form return ajax response
444 content = json.dumps(form.errors)
445 return HttpResponse(content, mimetype='application/json')
447 else:
448 form = MigrateForm()
450 return render_to_response('ganeti/virtual_machine/migrate.html',
451 {'form': form, 'vm': vm, 'cluster': cluster},
452 context_instance=RequestContext(request))
455 @login_required
456 def replace_disks(request, cluster_slug, instance):
458 view used for initiating a Replace Disks job
460 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
461 user = request.user
462 if not (user.is_superuser or
463 user.has_any_perms(cluster, ['admin', 'replace_disks'])):
464 raise Http403(NO_PRIVS)
466 if request.method == 'POST':
467 form = ReplaceDisksForm(vm, request.POST)
468 if form.is_valid():
469 try:
470 job = form.save()
471 job.refresh()
472 content = json.dumps(job.info)
474 # log information
475 log_action('VM_REPLACE_DISKS', user, vm, job)
476 except GanetiApiError, e:
477 content = json.dumps({'__all__': [str(e)]})
478 else:
479 # error in form return ajax response
480 content = json.dumps(form.errors)
481 return HttpResponse(content, mimetype='application/json')
483 else:
484 form = ReplaceDisksForm(vm)
486 return render_to_response('ganeti/virtual_machine/replace_disks.html',
487 {'form': form, 'vm': vm, 'cluster': cluster},
488 context_instance=RequestContext(request))
491 @require_POST
492 @login_required
493 def reboot(request, cluster_slug, instance, rest=False):
494 vm = get_object_or_404(VirtualMachine, hostname=instance,
495 cluster__slug=cluster_slug)
496 user = request.user
497 if not (user.is_superuser or
498 user.has_any_perms(vm, ['admin', 'power']) or
499 user.has_perm('admin', vm.cluster)):
501 if rest:
502 return HttpResponseForbidden()
503 else:
504 raise Http403(_('You do not have permission to '
505 'reboot this virtual machine'))
507 try:
508 job = vm.reboot()
509 job.refresh()
510 msg = job.info
512 # log information about restarting the machine
513 log_action('VM_REBOOT', user, vm, job)
514 except GanetiApiError, e:
515 msg = {'__all__': [str(e)]}
516 if rest:
517 return HttpAccepted()
518 else:
519 return HttpResponse(json.dumps(msg), mimetype='application/json')
522 def ssh_keys(request, cluster_slug, instance, api_key):
524 Show all ssh keys which belong to users, who are specified vm's admin
526 if settings.WEB_MGR_API_KEY != api_key:
527 return HttpResponseForbidden(_("You're not allowed to view keys."))
529 vm = get_object_or_404(VirtualMachine, hostname=instance,
530 cluster__slug=cluster_slug)
532 users = get_users_any(vm, ["admin", ]).values_list("id", flat=True)
533 keys = SSHKey.objects \
534 .filter(Q(user__in=users) | Q(user__is_superuser=True)) \
535 .values_list('key', 'user__username') \
536 .order_by('user__username')
538 keys_list = list(keys)
539 return HttpResponse(json.dumps(keys_list), mimetype="application/json")
542 @login_required
543 def detail(request, cluster_slug, instance, rest=False):
545 Display details of virtual machine.
547 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
549 user = request.user
550 cluster_admin = user.is_superuser or user.has_perm('admin', cluster)
552 if not cluster_admin:
553 perms = user.get_perms(vm)
555 if cluster_admin or 'admin' in perms:
556 admin = True
557 remove = True
558 power = True
559 modify = True
560 migrate = True
561 tags = True
562 else:
563 admin = False
564 remove = 'remove' in perms
565 power = 'power' in perms
566 modify = 'modify' in perms
567 tags = 'tags' in perms
568 migrate = 'migrate' in perms
570 if not (admin or power or remove or modify or tags): # TODO REST
571 raise Http403(_('You do not have permission to view '
572 'this virtual machines\'s details'))
574 context = {
575 'cluster': cluster,
576 'instance': vm,
577 'admin': admin,
578 'cluster_admin': cluster_admin,
579 'remove': remove,
580 'power': power,
581 'modify': modify,
582 'migrate': migrate,
583 "has_immediate_shutdown": has_shutdown_timeout(cluster),
586 # check job for pending jobs that should be rendered with a different
587 # detail template. This allows us to reduce the chance that users will do
588 # something strange like rebooting a VM that is being deleted or is not
589 # fully created yet.
590 if vm.pending_delete:
591 template = 'ganeti/virtual_machine/delete_status.html'
592 elif vm.template:
593 template = 'ganeti/virtual_machine/create_status.html'
594 if vm.last_job:
595 context['job'] = vm.last_job
596 else:
597 ct = ContentType.objects.get_for_model(vm)
598 jobs_loc = Job.objects.order_by('-finished') \
599 .filter(content_type=ct, object_id=vm.pk)
600 if jobs_loc.count() > 0:
601 context['job'] = jobs_loc[0]
602 else:
603 context['job'] = None
604 else:
605 template = 'ganeti/virtual_machine/detail.html'
607 if rest:
608 return context
609 else:
610 return render_to_response(template, context,
611 context_instance=RequestContext(request), )
614 @login_required
615 def users(request, cluster_slug, instance, rest=False):
617 Display all of the Users of a VirtualMachine
619 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
621 user = request.user
622 if not (user.is_superuser or user.has_perm('admin', vm) or
623 user.has_perm('admin', cluster)):
624 if rest:
625 return {'msg': NO_PRIVS, 'code': 403}
626 else:
627 raise Http403(NO_PRIVS)
629 url = reverse('vm-permissions', args=[cluster.slug, vm.hostname])
630 return view_users(request, vm, url, rest=rest)
633 @login_required
634 def permissions(request, cluster_slug, instance, user_id=None, group_id=None):
636 Update a users permissions.
638 vm = get_object_or_404(VirtualMachine, hostname=instance,
639 cluster__slug=cluster_slug)
641 user = request.user
642 if not (user.is_superuser or user.has_perm('admin', vm) or
643 user.has_perm('admin', vm.cluster)):
644 raise Http403(NO_PRIVS)
646 url = reverse('vm-permissions', args=[cluster_slug, vm.hostname])
647 return view_permissions(request, vm, url, user_id, group_id)
650 @login_required
651 def object_log(request, cluster_slug, instance, rest=False):
653 Display all of the Users of a VirtualMachine
655 cluster = get_object_or_404(Cluster, slug=cluster_slug)
656 vm = get_object_or_404(VirtualMachine, hostname=instance)
658 user = request.user
659 if not (user.is_superuser or user.has_perm('admin', vm) or
660 user.has_perm('admin', cluster)):
661 raise Http403(NO_PRIVS)
663 if rest:
664 return list_for_object(request, vm, True)
665 else:
666 return list_for_object(request, vm)
669 @login_required
670 def modify(request, cluster_slug, instance):
671 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
673 user = request.user
674 if not (user.is_superuser
675 or user.has_any_perms(vm, ['admin', 'modify'])
676 or user.has_perm('admin', cluster)):
677 raise Http403(
678 'You do not have permissions to edit this virtual machine')
680 hv = get_hypervisor(vm)
681 if hv == 'kvm':
682 hv_form = KvmModifyVirtualMachineForm
683 template = 'ganeti/virtual_machine/edit_kvm.html'
684 elif hv == 'xen-pvm':
685 hv_form = PvmModifyVirtualMachineForm
686 template = 'ganeti/virtual_machine/edit_pvm.html'
687 elif hv == 'xen-hvm':
688 hv_form = HvmModifyVirtualMachineForm
689 template = 'ganeti/virtual_machine/edit_hvm.html'
690 else:
691 hv_form = None
692 template = 'ganeti/virtual_machine/edit_base.html'
693 # XXX no matter what, we're gonna call hv_form() and die. Let's do it
694 # louder than usual. >:3
695 msg = "Hey, guys, implementation error in views/vm.py:modify"
696 raise RuntimeError(msg)
698 if request.method == 'POST':
700 form = hv_form(vm, request.POST)
702 form.owner = vm.owner
703 form.vm = vm
704 form.cluster = cluster
705 if form.is_valid():
706 data = form.cleaned_data
707 request.session['edit_form'] = data
708 request.session['edit_vm'] = vm.id
709 return HttpResponseRedirect(
710 reverse('instance-modify-confirm',
711 args=[cluster.slug,
712 vm.hostname]))
714 elif request.method == 'GET':
715 if 'edit_form' in request.session \
716 and vm.id == request.session['edit_vm']:
717 form = hv_form(vm, request.session['edit_form'])
718 else:
719 form = hv_form(vm)
721 return render_to_response(
722 template,
723 {'cluster': cluster,
724 'instance': vm,
725 'form': form,
726 'balloon': has_balloonmem(cluster)
728 context_instance=RequestContext(request),
732 # XXX mother, did it need to be so long?
733 @login_required
734 def modify_confirm(request, cluster_slug, instance):
735 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
737 hv = get_hypervisor(vm)
738 if hv == 'kvm':
739 hv_form = KvmModifyVirtualMachineForm
740 elif hv == 'xen-pvm':
741 hv_form = PvmModifyVirtualMachineForm
742 elif hv == 'xen-hvm':
743 hv_form = HvmModifyVirtualMachineForm
744 else:
745 hv_form = None
746 # XXX no matter what, we're gonna call hv_form() and die. Let's do it
747 # louder than usual. >:3
748 msg = "Hey, guys, implementation error in views/vm.py:modify_confirm"
749 raise RuntimeError(msg)
751 user = request.user
752 power = user.is_superuser or user.has_any_perms(vm, ['admin', 'power'])
753 if not (user.is_superuser or user.has_any_perms(vm, ['admin', 'modify'])
754 or user.has_perm('admin', cluster)):
755 raise Http403(
756 _('You do not have permissions to edit this virtual machine'))
758 if request.method == "POST":
759 if 'edit' in request.POST:
760 return HttpResponseRedirect(
761 reverse("instance-modify",
762 args=[cluster.slug, vm.hostname]))
763 elif 'reboot' in request.POST or 'save' in request.POST:
764 form = ModifyConfirmForm(request.POST)
765 form.session = request.session
766 form.owner = vm.owner
767 form.vm = vm
768 form.cluster = cluster
770 if form.is_valid():
771 beparams = {}
772 data = form.cleaned_data
773 rapi_dict = data['rapi_dict']
774 nics = rapi_dict.pop('nics')
775 beparams['vcpus'] = rapi_dict.pop('vcpus')
776 if has_balloonmem(cluster):
777 beparams['maxmem'] = rapi_dict.pop('maxmem')
778 beparams['minmem'] = rapi_dict.pop('minmem')
779 else:
780 beparams['memroy'] = rapi_dict.pop('memory')
781 os_name = rapi_dict.pop('os')
782 job_id = cluster.rapi.ModifyInstance(
783 instance,
784 nics=nics,
785 os_name=os_name,
786 hvparams=rapi_dict,
787 beparams=beparams)
788 # Create job and update message on virtual machine detail page
789 job = Job.objects.create(job_id=job_id,
790 obj=vm,
791 cluster=cluster)
792 VirtualMachine.objects \
793 .filter(id=vm.id).update(last_job=job, ignore_cache=True)
794 # log information about modifying this instance
795 log_action('EDIT', user, vm)
796 if 'reboot' in request.POST and vm.info['status'] == 'running':
797 if power:
798 # Reboot the vm
799 job = vm.reboot()
800 log_action('VM_REBOOT', user, vm, job)
801 else:
802 raise Http403(
803 _("Sorry, but you do not have permission "
804 "to reboot this machine."))
806 # Redirect to instance-detail
807 return HttpResponseRedirect(
808 reverse("instance-detail",
809 args=[cluster.slug, vm.hostname]))
811 elif 'cancel' in request.POST:
812 # Remove session variables.
813 if 'edit_form' in request.session:
814 del request.session['edit_form']
815 # Redirect to instance-detail
816 return HttpResponseRedirect(
817 reverse("instance-detail", args=[cluster.slug, vm.hostname]))
819 elif request.method == "GET":
820 form = ModifyConfirmForm()
822 session = request.session
824 if not 'edit_form' in request.session:
825 return HttpResponseBadRequest('Incorrect Session Data')
827 data = session['edit_form']
828 info = vm.info
829 hvparams = info['hvparams']
831 old_set = dict(
832 vcpus=info['beparams']['vcpus'],
833 os=info['os'],
835 if has_balloonmem(cluster):
836 old_set['maxmem'] = info['beparams']['maxmem']
837 old_set['minmem'] = info['beparams']['minmem']
838 else:
839 old_set['memory'] = info['beparams']['memory']
840 nic_count = len(info['nic.links'])
841 for i in xrange(nic_count):
842 old_set['nic_link_%s' % i] = info['nic.links'][i]
843 old_set['nic_mac_%s' % i] = info['nic.macs'][i]
845 # Add hvparams to the old_set
846 old_set.update(hvparams)
848 instance_diff = {}
849 fields = hv_form(vm, data).fields
850 for key in data.keys():
851 if key in ['memory', 'maxmem', 'minmem']:
852 diff = compare(render_storage(old_set[key]),
853 render_storage(data[key]))
854 elif key == 'os':
855 oses = os_prettify([old_set[key], data[key]])
856 if len(oses) > 1:
858 XXX - Special case for a cluster with two different types of
859 optgroups (i.e. Image, Debootstrap).
860 The elements at 00 and 10:
861 The optgroups
862 The elements at 010 and 110:
863 Tuple containing the OS Name and OS value.
864 The elements at 0101 and 1101:
865 String containing the OS Name
867 oses[0][1][0] = list(oses[0][1][0])
868 oses[1][1][0] = list(oses[1][1][0])
869 oses[0][1][0][1] = '%s (%s)' % (oses[0][1][0][1], oses[0][0])
870 oses[1][1][0][1] = '%s (%s)' % (oses[1][1][0][1], oses[1][0])
871 oses = oses[0][1] + oses[1][1]
872 diff = compare(oses[0][1], oses[1][1])
873 else:
874 oses = oses[0][1]
875 diff = compare(oses[0][1], oses[1][1])
876 #diff = compare(oses[0][1], oses[1][1])
877 if key in ['nic_count', 'nic_count_original']:
878 continue
879 elif key not in old_set.keys():
880 diff = ""
881 instance_diff[fields[key].label] = _('Added')
882 else:
883 diff = compare(old_set[key], data[key])
885 if diff != "":
886 label = fields[key].label
887 instance_diff[label] = diff
889 # remove mac if it has not changed
890 for i in xrange(nic_count):
891 if fields['nic_mac_%s' % i].label not in instance_diff:
892 del data['nic_mac_%s' % i]
894 # Repopulate form with changed values
895 form.fields['rapi_dict'] = CharField(widget=HiddenInput,
896 initial=json.dumps(data))
898 return render_to_response(
899 'ganeti/virtual_machine/edit_confirm.html', {
900 'cluster': cluster,
901 'form': form,
902 'instance': vm,
903 'instance_diff': instance_diff,
904 'power': power,
906 context_instance=RequestContext(request),
910 @login_required
911 def rename(request, cluster_slug, instance, rest=False, extracted_params=None):
913 Rename an existing instance
915 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
917 user = request.user
918 if not (user.is_superuser or user.has_any_perms(vm, ['admin', 'modify'])
919 or user.has_perm('admin', cluster)):
920 raise Http403(
921 _('You do not have permissions to edit this virtual machine'))
923 if request.method == 'POST':
924 form = RenameForm(vm, request.POST)
925 params_ok = False
926 if rest and extracted_params is not None:
927 if all(k in extracted_params
928 for k in ("hostname", "ip_check", "name_check")):
929 hostname = extracted_params['hostname']
930 ip_check = extracted_params['ip_check']
931 name_check = extracted_params['name_check']
932 params_ok = True
933 else:
934 return HttpResponseBadRequest()
936 if form.is_valid():
937 data = form.cleaned_data
938 hostname = data['hostname']
939 ip_check = data['ip_check']
940 name_check = data['name_check']
942 if form.is_valid() or params_ok:
943 try:
944 # In order for rename to work correctly, the vm must first be
945 # shutdown.
946 if vm.is_running:
947 job1 = vm.shutdown()
948 log_action('VM_STOP', user, vm, job1)
950 job_id = vm.rapi.RenameInstance(vm.hostname, hostname,
951 ip_check, name_check)
952 job = Job.objects.create(job_id=job_id,
953 obj=vm,
954 cluster=cluster)
955 VirtualMachine.objects.filter(pk=vm.pk) \
956 .update(hostname=hostname, last_job=job, ignore_cache=True)
958 # slip the new hostname to the log action
959 vm.newname = hostname
961 # log information about creating the machine
962 log_action('VM_RENAME', user, vm, job)
964 if not rest:
965 return HttpResponseRedirect(
966 reverse('instance-detail',
967 args=[cluster.slug, hostname]))
968 else:
969 return HttpAccepted()
971 except GanetiApiError, e:
972 msg = 'Error renaming virtual machine: %s' % e
973 form._errors["cluster"] = form.error_class([msg])
974 if rest:
975 return HttpResponse(400, content=msg)
977 elif request.method == 'GET':
978 form = RenameForm(vm)
980 return render_to_response(
981 'ganeti/virtual_machine/rename.html',
982 {'cluster': cluster,
983 'vm': vm,
984 'form': form
986 context_instance=RequestContext(request), )
989 @login_required
990 def reparent(request, cluster_slug, instance):
992 update a virtual machine to have a new owner
994 vm, cluster = get_vm_and_cluster_or_404(cluster_slug, instance)
996 user = request.user
997 if not (user.is_superuser or user.has_perm('admin', cluster)):
998 raise Http403(
999 _('You do not have permissions to change the owner '
1000 'of this virtual machine'))
1002 if request.method == 'POST':
1003 form = ChangeOwnerForm(request.POST)
1004 if form.is_valid():
1005 data = form.cleaned_data
1006 vm.owner = data['owner']
1007 vm.save(force_update=True)
1009 # log information about creating the machine
1010 log_action('VM_MODIFY', user, vm)
1012 return HttpResponseRedirect(
1013 reverse('instance-detail', args=[cluster_slug, instance]))
1015 else:
1016 form = ChangeOwnerForm()
1018 return render_to_response(
1019 'ganeti/virtual_machine/reparent.html', {
1020 'cluster': cluster,
1021 'vm': vm,
1022 'form': form
1024 context_instance=RequestContext(request),
1028 @login_required
1029 def job_status(request, id, rest=False):
1031 Return a list of basic info for running jobs.
1033 ct = ContentType.objects.get_for_model(VirtualMachine)
1034 jobs = Job.objects.filter(status__in=("error", "running", "waiting"),
1035 content_type=ct,
1036 object_id=id).order_by('job_id')
1037 jobs = [j.info for j in jobs]
1039 if rest:
1040 return jobs
1041 else:
1042 return HttpResponse(json.dumps(jobs), mimetype='application/json')
1045 def recv_user_add(sender, editor, user, obj, **kwargs):
1047 receiver for object_permissions.signals.view_add_user, Logs action
1049 log_action('ADD_USER', editor, obj, user)
1052 def recv_user_remove(sender, editor, user, obj, **kwargs):
1054 receiver for object_permissions.signals.view_remove_user, Logs action
1056 log_action('REMOVE_USER', editor, obj, user)
1059 def recv_perm_edit(sender, editor, user, obj, **kwargs):
1061 receiver for object_permissions.signals.view_edit_user, Logs action
1063 log_action('MODIFY_PERMS', editor, obj, user)
1066 view_add_user.connect(recv_user_add, sender=VirtualMachine)
1067 view_remove_user.connect(recv_user_remove, sender=VirtualMachine)
1068 view_edit_user.connect(recv_perm_edit, sender=VirtualMachine)